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