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