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