1 /** 2 This module has a lot of dependencies 3 4 dmd yourapp.d arsd/htmlwidget.d arsd/simpledisplay.d arsd/curl.d arsd/color.d arsd/dom.d arsd/characterencodings.d arsd/imagedraft.d -J. -version=browser 5 6 -version=browser is important so dom.d has the extensibility hook this module uses. 7 8 9 10 The idea here is to be a quick html window, displayed using the simpledisplay.d 11 module. 12 13 Nothing fancy, the html+css support is spotty and it has some text layout bugs... 14 but it can work for a simple thing. 15 16 It has no javascript support, but you can (and must, for even links to work) add 17 event listeners in your D code. 18 */ 19 module arsd.htmlwidget; 20 21 public import arsd.simpledisplay; 22 import arsd.png; 23 24 public import arsd.dom; 25 26 import std.range; 27 import std.conv; 28 import std.stdio; 29 import std.string; 30 import std.algorithm : max, min; 31 32 alias void delegate(Element, Event) EventHandler; 33 34 struct CssSize { 35 string definition; 36 37 int getPixels(int oneEm, int oneHundredPercent) 38 // out (ret) { assert(ret >= 0, to!string(ret) ~ " " ~ definition); } 39 body { 40 if(definition.length == 0 || definition == "none") 41 return 0; 42 43 if(definition == "auto") 44 return 0; 45 46 if(isNumeric(definition)) 47 return to!int(definition); 48 49 if(definition[$-1] == '%') { 50 if(oneHundredPercent < 0) 51 return 0; 52 return cast(int) (to!real(definition[0 .. $-1]) * oneHundredPercent); 53 } 54 if(definition[$-2 .. $] == "px") 55 return to!int(definition[0 .. $ - 2]); 56 if(definition[$-2 .. $] == "em") 57 return cast(int) (to!real(definition[0 .. $-2]) * oneEm); 58 59 // FIXME: other units of measure... 60 61 return 0; 62 } 63 } 64 65 66 Color readColor(string v) { 67 v = v.toLower; 68 switch(v) { 69 case "transparent": 70 return Color(0, 0, 0, 0); 71 case "red": 72 return Color(255, 0, 0); 73 case "green": 74 return Color(0, 255, 0); 75 case "blue": 76 return Color(0, 0, 255); 77 case "yellow": 78 return Color(255, 255, 0); 79 case "white": 80 return Color(255, 255, 255); 81 case "black": 82 return Color(0, 0, 0); 83 default: 84 if(v[0] == '#') { 85 return Color.fromString(v); 86 } else { 87 goto case "transparent"; 88 } 89 } 90 } 91 92 enum TableDisplay : int { 93 table = 1, 94 row = 2, 95 cell = 3, 96 caption = 4 97 } 98 99 class LayoutData { 100 Element element; 101 this(Element parent) { 102 element = parent; 103 element.expansionHook = cast(void*) this; 104 105 parseStyle; 106 } 107 108 void parseStyle() { 109 // reset to defaults 110 renderInline = true; 111 outsideNormalFlow = false; 112 renderValueAsText = false; 113 doNotDraw = false; 114 115 if(element.nodeType != 1) { 116 return; // only tags get style 117 } 118 119 // legitimate attributes FIXME: do these belong here? 120 121 if(element.hasAttribute("colspan")) 122 tableColspan = to!int(element.colspan); 123 else 124 tableColspan = 1; 125 if(element.hasAttribute("rowspan")) 126 tableRowspan = to!int(element.rowspan); 127 else 128 tableRowspan = 1; 129 130 131 if(element.tagName == "img" && element.src().indexOf(".png") != -1) { // HACK 132 try { 133 auto bytes = cast(ubyte[]) curl(absolutizeUrl(element.src, _contextHack.currentUrl)); 134 auto bytesArr = [bytes]; 135 auto png = pngFromBytes(bytesArr); 136 image = new Image(png.header.width, png.header.height); 137 138 width = CssSize(to!string(image.width) ~ "px"); 139 height = CssSize(to!string(image.height) ~ "px"); 140 141 int y; 142 foreach(line; png.byRgbaScanline) { 143 foreach(x, color; line.pixels) 144 image[x, y] = color; 145 146 y++; 147 } 148 } catch (Throwable t) { 149 writeln(t.toString); 150 image = null; 151 } 152 } 153 154 CssSize readSize(string v) { 155 return CssSize(v); 156 /* 157 if(v.indexOf("px") == -1) 158 return 0; 159 160 return to!int(v[0 .. $-2]); 161 */ 162 } 163 164 auto style = element.computedStyle; 165 166 //if(element.tagName == "a") 167 //assert(0, style.toString); 168 169 foreach(item; style.properties) { 170 string value = item.value; 171 172 Element curr = element; 173 while(value == "inherit" && curr.parentNode !is null) { 174 curr = curr.parentNode; 175 value = curr.computedStyle.getValue(item.name); 176 } 177 178 if(value == "inherit") 179 assert(0, item.name ~ " came in as inherit all the way up the chain"); 180 181 switch(item.name) { 182 case "attribute-as-text": 183 renderValueAsText = true; 184 break; 185 case "margin-bottom": 186 marginBottom = readSize(value); 187 break; 188 case "margin-top": 189 marginTop = readSize(value); 190 break; 191 case "margin-left": 192 marginLeft = readSize(value); 193 break; 194 case "margin-right": 195 marginRight = readSize(value); 196 break; 197 case "padding-bottom": 198 paddingBottom = readSize(value); 199 break; 200 case "padding-top": 201 paddingTop = readSize(value); 202 break; 203 case "padding-left": 204 paddingLeft = readSize(value); 205 break; 206 case "padding-right": 207 paddingRight = readSize(value); 208 break; 209 case "visibility": 210 if(value == "hidden") 211 doNotDraw = true; 212 else 213 doNotDraw = false; 214 break; 215 case "width": 216 if(value == "auto") 217 width = CssSize(); 218 else 219 width = readSize(value); 220 break; 221 case "height": 222 if(value == "auto") 223 height = CssSize(); 224 else 225 height = readSize(value); 226 break; 227 case "display": 228 tableDisplay = 0; 229 switch(value) { 230 case "block": 231 renderInline = false; 232 break; 233 case "inline": 234 renderInline = true; 235 break; 236 case "none": 237 doNotRender = true; 238 break; 239 case "list-item": 240 renderInline = false; 241 // FIXME - show the list marker too 242 break; 243 case "inline-block": 244 renderInline = false; // FIXME 245 break; 246 case "table": 247 renderInline = false; 248 case "inline-table": 249 tableDisplay = TableDisplay.table; 250 break; 251 case "table-row": 252 tableDisplay = TableDisplay.row; 253 break; 254 case "table-cell": 255 tableDisplay = TableDisplay.cell; 256 break; 257 case "table-caption": 258 tableDisplay = TableDisplay.caption; 259 break; 260 case "run-in": 261 262 /* do these even matter? */ 263 case "table-header-group": 264 case "table-footer-group": 265 case "table-row-group": 266 case "table-column": 267 case "table-column-group": 268 default: 269 // FIXME 270 } 271 272 if(value == "table-row") 273 renderInline = false; 274 break; 275 case "position": 276 position = value; 277 if(position == "absolute" || position == "fixed") 278 outsideNormalFlow = true; 279 break; 280 case "top": 281 top = CssSize(value); 282 break; 283 case "bottom": 284 bottom = CssSize(value); 285 break; 286 case "right": 287 right = CssSize(value); 288 break; 289 case "left": 290 left = CssSize(value); 291 break; 292 case "color": 293 foregroundColor = readColor(value); 294 break; 295 case "background-color": 296 backgroundColor = readColor(value); 297 break; 298 case "float": 299 switch(value) { 300 case "none": cssFloat = 0; outsideNormalFlow = false; break; 301 case "left": cssFloat = 1; outsideNormalFlow = true; break; 302 case "right": cssFloat = 2; outsideNormalFlow = true; break; 303 default: assert(0); 304 } 305 break; 306 case "clear": 307 switch(value) { 308 case "none": floatClear = 0; break; 309 case "left": floatClear = 1; break; 310 case "right": floatClear = 2; break; 311 case "both": floatClear = 1; break; // FIXME 312 default: assert(0); 313 } 314 break; 315 case "border": 316 borderWidth = CssSize("1px"); 317 break; 318 default: 319 } 320 } 321 322 // FIXME 323 if(tableDisplay == TableDisplay.row) { 324 renderInline = false; 325 } else if(tableDisplay == TableDisplay.cell) 326 renderInline = true; 327 } 328 329 static LayoutData get(Element e) { 330 if(e.expansionHook is null) 331 return new LayoutData(e); 332 return cast(LayoutData) e.expansionHook; 333 } 334 335 EventHandler[][string] bubblingEventHandlers; 336 EventHandler[][string] capturingEventHandlers; 337 EventHandler[string] defaultEventHandlers; 338 339 int absoluteLeft() { 340 int a = offsetLeft; 341 // FIXME: dead wrong 342 /* 343 auto p = offsetParent; 344 while(p) { 345 auto l = LayoutData.get(p); 346 a += l.offsetLeft; 347 p = l.offsetParent; 348 }*/ 349 350 return a; 351 } 352 353 int absoluteTop() { 354 int a = offsetTop; 355 /* 356 auto p = offsetParent; 357 while(p) { 358 auto l = LayoutData.get(p); 359 a += l.offsetTop; 360 p = l.offsetParent; 361 }*/ 362 363 return a; 364 } 365 366 int offsetWidth; 367 int offsetHeight; 368 int offsetLeft; 369 int offsetTop; 370 Element offsetParent; 371 372 CssSize borderWidth; 373 374 CssSize paddingLeft; 375 CssSize paddingRight; 376 CssSize paddingTop; 377 CssSize paddingBottom; 378 379 CssSize marginLeft; 380 CssSize marginRight; 381 CssSize marginTop; 382 CssSize marginBottom; 383 384 CssSize width; 385 CssSize height; 386 387 string position; 388 389 CssSize left; 390 CssSize top; 391 CssSize right; 392 CssSize bottom; 393 394 Color borderColor; 395 Color backgroundColor; 396 Color foregroundColor; 397 398 int zIndex; 399 400 401 /* pseudo classes */ 402 bool hover; 403 bool active; 404 bool focus; 405 bool link; 406 bool visited; 407 bool selected; 408 bool checked; 409 /* done */ 410 411 /* CSS styles */ 412 bool doNotRender; 413 bool doNotDraw; 414 bool renderInline; 415 bool renderValueAsText; 416 417 int tableDisplay; // 1= table, 2 = table-row, 3 = table-cell, 4 = table-caption 418 int tableColspan; 419 int tableRowspan; 420 int cssFloat; 421 int floatClear; 422 423 string textToRender; 424 425 bool outsideNormalFlow; 426 427 /* Efficiency flags */ 428 429 static bool someRepaintRequired; 430 bool repaintRequired; 431 432 void invalidate() { 433 repaintRequired = true; 434 someRepaintRequired = true; 435 } 436 437 void paintCompleted() { 438 repaintRequired = false; 439 someRepaintRequired = false; // FIXME 440 } 441 442 Image image; 443 } 444 445 Element elementFromPoint(Document document, int x, int y) { 446 int winningZIndex = int.min; 447 Element winner; 448 foreach(element; document.mainBody.tree) { 449 if(element.nodeType == 3) // do I want this? 450 continue; 451 auto e = LayoutData.get(element); 452 if(e.doNotRender) 453 continue; 454 if( 455 e.zIndex >= winningZIndex 456 && 457 x >= e.absoluteLeft() && x < e.absoluteLeft() + e.offsetWidth 458 && 459 y >= e.absoluteTop() && y < e.absoluteTop() + e.offsetHeight 460 ) { 461 winner = e.element; 462 winningZIndex = e.zIndex; 463 } 464 } 465 466 return winner; 467 } 468 469 int longestLine(string a) { 470 int longest = 0; 471 foreach(l; a.split("\n")) 472 if(l.length > longest) 473 longest = l.length; 474 return longest; 475 } 476 477 int getTableCells(Element row) { 478 int count; 479 foreach(c; row.childNodes) { 480 auto l = LayoutData.get(c); 481 if(l.tableDisplay == TableDisplay.cell) 482 count += l.tableColspan; 483 } 484 485 return count; 486 } 487 488 // returns: dom structure changed 489 bool layout(Element element, int containerWidth, int containerHeight, int cx, int cy, bool canWrap, int parentContainerWidth = 0) { 490 auto oneEm = 16; 491 492 if(element.tagName == "head") 493 return false; 494 495 auto l = LayoutData.get(element); 496 497 if(l.doNotRender) 498 return false; 499 500 if(element.nodeType == 3 && element.nodeValue.strip.length == 0) { 501 l.doNotRender = true; 502 return false; 503 } 504 505 if(!l.renderInline) { 506 cx += l.marginLeft.getPixels(oneEm, containerWidth); // FIXME: does this belong here? 507 //cy += l.marginTop.getPixels(oneEm, containerHeight); 508 containerWidth -= l.marginLeft.getPixels(oneEm, containerWidth) + l.marginRight.getPixels(oneEm, containerWidth); 509 //containerHeight -= l.marginTop.getPixels(oneEm, containerHeight) + l.marginBottom.getPixels(oneEm, containerHeight); 510 } 511 512 l.offsetLeft = cx; 513 l.offsetTop = cy; 514 515 //if(!l.renderInline) { 516 cx += l.paddingLeft.getPixels(oneEm, containerWidth); 517 cy += l.paddingTop.getPixels(oneEm, containerHeight); 518 containerWidth -= l.paddingLeft.getPixels(oneEm, containerWidth) + l.paddingRight.getPixels(oneEm, containerWidth); 519 containerHeight -= l.paddingTop.getPixels(oneEm, containerHeight) + l.paddingBottom.getPixels(oneEm, containerHeight); 520 //} 521 522 auto initialX = cx; 523 auto initialY = cy; 524 auto availableWidth = containerWidth; 525 auto availableHeight = containerHeight; 526 527 int fx; // current position for floats 528 int fy; 529 530 531 int boundingWidth; 532 int boundingHeight; 533 534 int biggestWidth; 535 int biggestHeight; 536 537 int lastMarginBottom; 538 int lastMarginApplied; 539 540 bool hasContentLeft; 541 542 543 int cssWidth = l.width.getPixels(oneEm, containerWidth); 544 int cssHeight = l.height.getPixels(oneEm, containerHeight); 545 546 bool widthSet = false; 547 548 if(l.tableDisplay == TableDisplay.cell && !widthSet) { 549 l.offsetWidth = l.tableColspan * parentContainerWidth / getTableCells(l.element.parentNode); 550 widthSet = true; 551 containerWidth = l.offsetWidth; 552 availableWidth = containerWidth; 553 } 554 555 556 int skip; 557 startAgain: 558 // now, we layout the children to collect all that info together 559 foreach(i, child; element.childNodes) { 560 if(skip) { 561 skip--; 562 continue; 563 } 564 565 auto childLayout = LayoutData.get(child); 566 567 if(!childLayout.outsideNormalFlow && !childLayout.renderInline && hasContentLeft) { 568 cx = initialX; 569 cy += biggestHeight; 570 availableWidth = containerWidth; 571 availableHeight -= biggestHeight; 572 hasContentLeft = false; 573 574 biggestHeight = 0; 575 } 576 577 if(childLayout.floatClear) { 578 cx = initialX; 579 580 if(max(fy, cy) != cy) 581 availableHeight -= fy - cy; 582 583 cy = max(fy, cy); 584 hasContentLeft = false; 585 biggestHeight = 0; 586 } 587 588 auto currentMargin = childLayout.marginTop.getPixels(oneEm, containerHeight); 589 currentMargin = max(currentMargin, lastMarginBottom) - lastMarginBottom; 590 if(currentMargin < 0) 591 currentMargin = 0; 592 if(!lastMarginApplied && max(currentMargin, lastMarginBottom) > 0) 593 currentMargin = max(currentMargin, lastMarginBottom); 594 595 lastMarginApplied = currentMargin; 596 597 cy += currentMargin; 598 containerHeight -= currentMargin; 599 600 bool changed = layout(child, availableWidth, availableHeight, cx, cy, !l.renderInline, containerWidth); 601 602 if(childLayout.cssFloat) { 603 childLayout.offsetTop += fy; 604 foreach(bele; child.tree) { 605 auto lolol = LayoutData.get(bele); 606 lolol.offsetTop += fy; 607 } 608 609 fx += childLayout.offsetWidth; 610 fy += childLayout.offsetHeight; 611 } 612 613 if(childLayout.doNotRender || childLayout.outsideNormalFlow) 614 continue; 615 616 //if(childLayout.offsetHeight < 0) 617 //childLayout.offsetHeight = 0; 618 //if(childLayout.offsetWidth < 0) 619 //childLayout.offsetWidth = 0; 620 621 assert(childLayout.offsetHeight >= 0); 622 assert(childLayout.offsetWidth >= 0); 623 624 // inline elements can't have blocks inside 625 //if(!childLayout.renderInline) 626 //l.renderInline = false; 627 628 lastMarginBottom = childLayout.marginBottom.getPixels(oneEm, containerHeight); 629 630 if(childLayout.offsetWidth > biggestWidth) 631 biggestWidth = childLayout.offsetWidth; 632 if(childLayout.offsetHeight > biggestHeight) 633 biggestHeight = childLayout.offsetHeight; 634 635 availableWidth -= childLayout.offsetWidth; 636 637 638 if(cx + childLayout.offsetWidth > boundingWidth) 639 boundingWidth = cx + childLayout.offsetWidth; 640 641 // if the dom was changed, it was to wrap... 642 if(changed || availableWidth <= 0) { 643 // gotta move to a new line 644 availableWidth = containerWidth; 645 cx = initialX; 646 cy += biggestHeight; 647 biggestHeight = 0; 648 availableHeight -= childLayout.offsetHeight; 649 hasContentLeft = false; 650 //writeln("new line now at ", cy); 651 } else { 652 // can still use this one 653 cx += childLayout.offsetWidth; 654 hasContentLeft = true; 655 } 656 657 if(changed) { 658 skip = i; 659 writeln("dom changed"); 660 goto startAgain; 661 } 662 } 663 664 if(hasContentLeft) 665 cy += biggestHeight; // line-height 666 667 boundingHeight = cy - initialY + l.paddingTop.getPixels(oneEm, containerHeight) + l.paddingBottom.getPixels(oneEm, containerHeight); 668 669 // And finally, layout this element itself 670 if(element.nodeType == 3) { 671 l.textToRender = replace(element.nodeValue,"\n", " ").replace("\t", " ").replace("\r", " ").squeeze(" "); 672 if(l.textToRender.length == 0) { 673 l.doNotRender = true; 674 return false; 675 } 676 677 auto lineWidth = containerWidth / 6; 678 679 bool startedWithSpace = l.textToRender[0] == ' '; 680 681 if(l.textToRender.length > lineWidth) 682 l.textToRender = wrap(l.textToRender, lineWidth); 683 684 if(l.textToRender[$-1] == '\n') 685 l.textToRender = l.textToRender[0 .. $-1]; 686 687 if(startedWithSpace && l.textToRender[0] != ' ') 688 l.textToRender = " " ~ l.textToRender; 689 690 bool contentChanged = false; 691 // we can wrap so let's do it 692 /* 693 auto lineIdx = l.textToRender.indexOf("\n"); 694 if(canWrap && lineIdx != -1) { 695 writeln("changing ***", l.textToRender, "***"); 696 auto remaining = l.textToRender[lineIdx + 1 .. $]; 697 l.textToRender = l.textToRender[0 .. lineIdx]; 698 699 Element[] txt; 700 txt ~= new TextNode(element.parentDocument, l.textToRender); 701 txt ~= new TextNode(element.parentDocument, "\n"); 702 txt ~= new TextNode(element.parentDocument, remaining); 703 704 element.parentNode.replaceChild(element, txt); 705 contentChanged = true; 706 } 707 */ 708 709 if(l.textToRender.length != 0) { 710 l.offsetHeight = count(l.textToRender, "\n") * 16 + 16; // lines * line-height 711 l.offsetWidth = l.textToRender.longestLine * 6; // inline 712 } else { 713 l.offsetWidth = 0; 714 l.offsetHeight = 0; 715 } 716 717 l.renderInline = true; 718 719 //writefln("Text %s at (%s, %s) with size %sx%s", element.tagName, l.offsetLeft, l.offsetTop, l.offsetWidth, l.offsetHeight); 720 721 return contentChanged; 722 } 723 724 // images get special treatment too 725 if(l.image !is null) { 726 if(!widthSet) 727 l.offsetWidth = l.image.width; 728 l.offsetHeight = l.image.height; 729 //writefln("Image %s at (%s, %s) with size %sx%s", element.tagName, l.offsetLeft, l.offsetTop, l.offsetWidth, l.offsetHeight); 730 731 return false; 732 } 733 734 /* 735 // tables constrain floats... 736 if(l.tableDisplay == TableDisplay.cell) { 737 l.offsetHeight += fy; 738 } 739 */ 740 741 // layout an inline element... 742 if(l.renderInline) { 743 //if(l.tableDisplay == TableDisplay.cell) { 744 //auto ow = widthSet ? l.offsetWidth : 0; 745 //l.offsetWidth = min(ow, boundingWidth - initialX); 746 //if(l.offsetWidth < 0) 747 //l.offsetWidth = 0; 748 //} else 749 if(!widthSet) { 750 l.offsetWidth = boundingWidth - initialX; // FIXME: padding? 751 if(l.offsetWidth < 0) 752 l.offsetWidth = 0; 753 } 754 755 l.offsetHeight = max(boundingHeight, biggestHeight); 756 //writefln("Inline element %s at (%s, %s) with size %sx%s", element.tagName, l.offsetLeft, l.offsetTop, l.offsetWidth, l.offsetHeight); 757 // and layout a block element 758 } else { 759 l.offsetWidth = containerWidth; 760 l.offsetHeight = boundingHeight; 761 762 //writefln("Block element %s at (%s, %s) with size %sx%s", element.tagName, l.offsetLeft, l.offsetTop, l.offsetWidth, l.offsetHeight); 763 } 764 765 if(l.position == "absolute") { 766 l.offsetTop = l.top.getPixels(oneEm, containerHeight); 767 l.offsetLeft = l.left.getPixels(oneEm, containerWidth); 768 // l.offsetRight = l.right.getPixels(oneEm, containerWidth); 769 // l.offsetBottom = l.bottom.getPixels(oneEm, containerHeight); 770 } else if(l.position == "relative") { 771 l.offsetTop = l.top.getPixels(oneEm, containerHeight); 772 l.offsetLeft = l.left.getPixels(oneEm, containerWidth); 773 // l.offsetRight = l.right.getPixels(oneEm, containerWidth); 774 // l.offsetBottom = l.bottom.getPixels(oneEm, containerHeight); 775 } 776 777 // table cells need special treatment 778 if(!l.tableDisplay) { 779 if(cssWidth) { 780 l.offsetWidth = cssWidth; 781 containerWidth = min(containerWidth, cssWidth); 782 // not setting widthSet since this is just a hint 783 } 784 if(cssHeight) { 785 l.offsetHeight = cssHeight; 786 containerHeight = min(containerHeight, cssHeight); 787 } 788 } 789 790 791 792 /* 793 // table cell 794 if(l.tableDisplay == 2) { 795 l.offsetWidth = containerWidth; 796 } 797 */ 798 799 // a table row, and all it's cell children, have the same height 800 if(l.tableDisplay == TableDisplay.row) { 801 int maxHeight = 0; 802 foreach(e; element.childNodes) { 803 auto el = LayoutData.get(e); 804 if(el.tableDisplay == TableDisplay.cell) { 805 if(el.offsetHeight > maxHeight) 806 maxHeight = el.offsetHeight; 807 } 808 } 809 810 foreach(e; element.childNodes) { 811 auto el = LayoutData.get(e); 812 if(el.tableDisplay == TableDisplay.cell) { 813 el.offsetHeight = maxHeight; 814 } 815 } 816 l.offsetHeight = maxHeight; 817 } 818 819 // every column in a table has equal width 820 821 // assert(l.offsetHeight == 0 || l.offsetHeight > 10, format("%s on %s %s", l.offsetHeight, element.tagName, element.id ~ "." ~ element.className)); 822 823 return false; 824 825 } 826 827 int scrollTop = 0; 828 829 void drawElement(ScreenPainter p, Element ele, int startingX, int startingY) { 830 auto oneEm = 1; 831 832 // margin is handled in the layout phase, but border, padding, and obviously, content are handled here 833 834 auto l = LayoutData.get(ele); 835 836 if(l.doNotDraw) 837 return; 838 839 if(l.doNotRender) 840 return; 841 startingX = 0; // FIXME 842 startingY = 0; // FIXME why does this fix things? 843 int cx = l.offsetLeft + startingX, cy = l.offsetTop + startingY, cw = l.offsetWidth, ch = l.offsetHeight; 844 845 if(l.image !is null) { 846 p.drawImage(Point(cx, cy - scrollTop), l.image); 847 } 848 849 //if(cw <= 0 || ch <= 0) 850 // return; 851 852 if(l.borderWidth.getPixels(oneEm, 1) > 0) { 853 p.fillColor = Color(0, 0, 0, 0); 854 p.outlineColor = l.borderColor; 855 // FIXME: handle actual widths by selecting a pen 856 p.drawRectangle(Point(cx, cy - scrollTop), cw, ch); // draws the border 857 } 858 859 int sx = cx, sy = cy; 860 861 cx += l.borderWidth.getPixels(oneEm, 1); 862 cy += l.borderWidth.getPixels(oneEm, 1); 863 cw -= l.borderWidth.getPixels(oneEm, 1) * 2; 864 ch -= l.borderWidth.getPixels(oneEm, 1) * 2; 865 866 p.fillColor = l.backgroundColor; 867 p.outlineColor = Color(0, 0, 0, 0); 868 869 if(ele.tagName == "body") { // HACK to make the body bg apply to the whole window 870 cx = 0; 871 cy = 0; 872 cw = p.window.width; 873 ch = p.window.height; 874 p.drawRectangle(Point(0, 0), p.window.width, p.window.height); // draw the padding box 875 } else 876 877 p.drawRectangle(Point(cx, cy - scrollTop), cw, ch); // draw the padding box 878 879 if(l.renderValueAsText && ele.value.length) { 880 p.outlineColor = l.foregroundColor; 881 p.drawText(Point( 882 cx + l.paddingLeft.getPixels(oneEm, 1), 883 cy + l.paddingTop.getPixels(oneEm, 1) - scrollTop), 884 ele.value); 885 } 886 887 //p.fillColor = Color(255, 255, 255); 888 //p.drawRectangle(Point(cx, cy), cw, ch); // draw the content box 889 890 891 foreach(e; ele.childNodes) { 892 if(e.nodeType == 3) { 893 auto thisL = LayoutData.get(e); 894 p.outlineColor = LayoutData.get(e.parentNode).foregroundColor; 895 p.drawText(Point(thisL.offsetLeft, thisL.offsetTop - scrollTop), toAscii(LayoutData.get(e).textToRender)); 896 } else 897 drawElement(p, e, sx, sy); 898 } 899 900 l.repaintRequired = false; 901 } 902 903 904 string toAscii(string s) { 905 string ret; 906 foreach(dchar c; s) { 907 if(c < 128 && c > 0) 908 ret ~= cast(char) c; 909 else switch(c) { 910 case '\u00a0': // nbsp 911 ret ~= ' '; 912 break; 913 case '\u2018': 914 case '\u2019': 915 ret ~= "'"; 916 break; 917 case '\u201c': 918 case '\u201d': 919 ret ~= "\""; 920 break; 921 default: 922 // skip non-ascii 923 } 924 } 925 926 return ret; 927 } 928 929 930 class Event { 931 this(string eventName, Element target) { 932 this.eventName = eventName; 933 this.srcElement = target; 934 } 935 936 void preventDefault() { 937 defaultPrevented = true; 938 } 939 940 void stopPropagation() { 941 propagationStopped = true; 942 } 943 944 bool defaultPrevented; 945 bool propagationStopped; 946 string eventName; 947 948 Element srcElement; 949 alias srcElement target; 950 951 Element relatedTarget; 952 953 int clientX; 954 int clientY; 955 956 int button; 957 958 bool isBubbling; 959 960 void send() { 961 if(srcElement is null) 962 return; 963 964 auto e = LayoutData.get(srcElement); 965 966 if(eventName in e.bubblingEventHandlers) 967 foreach(handler; e.bubblingEventHandlers[eventName]) 968 handler(e.element, this); 969 970 if(!defaultPrevented) 971 if(eventName in e.defaultEventHandlers) 972 e.defaultEventHandlers[eventName](e.element, this); 973 } 974 975 void dispatch() { 976 if(srcElement is null) 977 return; 978 979 // first capture, then bubble 980 981 LayoutData[] chain; 982 Element curr = srcElement; 983 while(curr) { 984 auto l = LayoutData.get(curr); 985 chain ~= l; 986 curr = curr.parentNode; 987 988 } 989 990 isBubbling = false; 991 foreach(e; chain.retro) { 992 if(eventName in e.capturingEventHandlers) 993 foreach(handler; e.capturingEventHandlers[eventName]) 994 handler(e.element, this); 995 996 // the default on capture should really be to always do nothing 997 998 //if(!defaultPrevented) 999 // if(eventName in e.defaultEventHandlers) 1000 // e.defaultEventHandlers[eventName](e.element, this); 1001 1002 if(propagationStopped) 1003 break; 1004 } 1005 1006 isBubbling = true; 1007 if(!propagationStopped) 1008 foreach(e; chain) { 1009 if(eventName in e.bubblingEventHandlers) 1010 foreach(handler; e.bubblingEventHandlers[eventName]) 1011 handler(e.element, this); 1012 1013 if(!defaultPrevented) 1014 if(eventName in e.defaultEventHandlers) 1015 e.defaultEventHandlers[eventName](e.element, this); 1016 1017 if(propagationStopped) 1018 break; 1019 } 1020 1021 } 1022 } 1023 1024 void addEventListener(string event, Element what, EventHandler handler, bool bubble = true) { 1025 if(event.length > 2 && event[0..2] == "on") 1026 event = event[2 .. $]; 1027 1028 auto l = LayoutData.get(what); 1029 if(bubble) 1030 l.bubblingEventHandlers[event] ~= handler; 1031 else 1032 l.capturingEventHandlers[event] ~= handler; 1033 } 1034 1035 void addEventListener(string event, Element[] what, EventHandler handler, bool bubble = true) { 1036 foreach(w; what) 1037 addEventListener(event, w, handler, bubble); 1038 } 1039 1040 bool isAParentOf(Element a, Element b) { 1041 if(a is null || b is null) 1042 return false; 1043 1044 while(b !is null) { 1045 if(a is b) 1046 return true; 1047 b = b.parentNode; 1048 } 1049 1050 return false; 1051 } 1052 1053 void runHtmlWidget(SimpleWindow win, BrowsingContext context) { 1054 Element mouseLastOver; 1055 1056 win.eventLoop(0, 1057 (MouseEvent e) { 1058 auto ele = elementFromPoint(context.document, e.x, e.y + scrollTop); 1059 1060 if(mouseLastOver !is ele) { 1061 Event event; 1062 1063 if(ele !is null) { 1064 if(!isAParentOf(ele, mouseLastOver)) { 1065 //writeln("mouseenter on ", ele.tagName); 1066 1067 event = new Event("mouseenter", ele); 1068 event.relatedTarget = mouseLastOver; 1069 event.send(); 1070 } 1071 } 1072 1073 if(mouseLastOver !is null) { 1074 if(!isAParentOf(mouseLastOver, ele)) { 1075 event = new Event("mouseleave", mouseLastOver); 1076 event.relatedTarget = ele; 1077 event.send(); 1078 } 1079 } 1080 1081 if(ele !is null) { 1082 event = new Event("mouseover", ele); 1083 event.relatedTarget = mouseLastOver; 1084 event.dispatch(); 1085 } 1086 1087 if(mouseLastOver !is null) { 1088 event = new Event("mouseout", mouseLastOver); 1089 event.relatedTarget = ele; 1090 event.dispatch(); 1091 } 1092 1093 mouseLastOver = ele; 1094 } 1095 1096 if(ele !is null) { 1097 auto l = LayoutData.get(ele); 1098 auto event = new Event( 1099 e.type == 0 ? "mousemove" 1100 : e.type == 1 ? "mousedown" 1101 : e.type == 2 ? "mouseup" 1102 : impossible 1103 , ele); 1104 event.clientX = e.x; 1105 event.clientY = e.y; 1106 event.button = e.button; 1107 1108 event.dispatch(); 1109 1110 if(l.someRepaintRequired) { 1111 auto p = win.draw(); 1112 p.clear(); 1113 drawElement(p, context.document.mainBody, 0, 0); 1114 l.paintCompleted(); 1115 } 1116 } 1117 }, 1118 (dchar key) { 1119 auto s = scrollTop; 1120 if(key == 'j') 1121 scrollTop += 16; 1122 else if(key == 'k') 1123 scrollTop -= 16; 1124 if(key == 'n') 1125 scrollTop += 160; 1126 else if(key == 'm') 1127 scrollTop -= 160; 1128 1129 if(context.focusedElement !is null) { 1130 context.focusedElement.value = context.focusedElement.value ~ cast(char) key; 1131 auto p = win.draw(); 1132 drawElement(p, context.focusedElement, 0, 0); 1133 } 1134 1135 if(s != scrollTop) { 1136 auto p = win.draw(); 1137 p.clear(); 1138 drawElement(p, context.document.mainBody, 0, 0); 1139 } 1140 1141 if(key == 'q') 1142 win.close(); 1143 }); 1144 } 1145 1146 class BrowsingContext { 1147 string currentUrl; 1148 Document document; 1149 Element focusedElement; 1150 } 1151 1152 string absolutizeUrl(string url, string currentUrl) { 1153 if(url.length == 0) 1154 return null; 1155 1156 auto current = currentUrl; 1157 auto idx = current.lastIndexOf("/"); 1158 if(idx != -1 && idx > 7) 1159 current = current[0 .. idx + 1]; 1160 1161 if(url[0] == '/') { 1162 auto i = current[8 .. $].indexOf("/"); 1163 if(i != -1) 1164 current = current[0 .. i + 8]; 1165 } 1166 1167 if(url.length < 7 || url[0 .. 7] != "http://") 1168 url = current ~ url; 1169 1170 return url; 1171 } 1172 1173 BrowsingContext _contextHack; // FIXME: the images aren't done sanely 1174 1175 import arsd.curl; 1176 Document gotoSite(SimpleWindow win, BrowsingContext context, string url, string post = null) { 1177 _contextHack = context; 1178 1179 auto p = win.draw; 1180 p.fillColor = Color(255, 255, 255); 1181 p.outlineColor = Color(0, 0, 0); 1182 p.drawRectangle(Point(0, 0), 800, 800); 1183 1184 auto document = new Document(curl(url.absolutizeUrl(context.currentUrl), post)); 1185 context.document = document; 1186 1187 context.currentUrl = url.absolutizeUrl(context.currentUrl); 1188 1189 string styleSheetText = import("default.css"); 1190 1191 foreach(ele; document.querySelectorAll("head link[rel=stylesheet]")) { 1192 if(!ele.hasAttribute("media") || ele.media().indexOf("screen") != -1) 1193 styleSheetText ~= curl(ele.href.absolutizeUrl(context.currentUrl)); 1194 } 1195 1196 foreach(ele; document.getElementsByTagName("style")) 1197 styleSheetText ~= ele.innerHTML; 1198 1199 styleSheetText = styleSheetText.replace(`@import "/style_common.css";`, curl("http://arsdnet.net/style_common.css")); 1200 1201 auto styleSheet = new StyleSheet(styleSheetText); 1202 styleSheet.apply(document); 1203 1204 foreach(e; document.root.tree) 1205 LayoutData.get(e); // initializing the css here 1206 1207 return document; 1208 } 1209 1210 1211 string impossible() { 1212 assert(0); 1213 return null; 1214 } 1215