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