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