arsd/htmlwidget.d

1220 lines
28 KiB
D

/**
My old toy html widget build out of my libraries. Not great, you probably don't want to use it.
This module has a lot of dependencies
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
-version=browser is important so dom.d has the extensibility hook this module uses.
The idea here is to be a quick html window, displayed using the simpledisplay.d
module.
Nothing fancy, the html+css support is spotty and it has some text layout bugs...
but it can work for a simple thing.
It has no javascript support, but you can (and must, for even links to work) add
event listeners in your D code.
*/
module arsd.htmlwidget;
public import arsd.simpledisplay;
import arsd.image;
public import arsd.dom;
import std.range;
import std.conv;
import std.stdio;
import std.string;
import std.algorithm : max, min;
alias void delegate(Element, Event) EventHandler;
struct CssSize {
string definition;
int getPixels(int oneEm, int oneHundredPercent)
// out (ret) { assert(ret >= 0, to!string(ret) ~ " " ~ definition); }
do {
if(definition.length == 0 || definition == "none")
return 0;
if(definition == "auto")
return 0;
if(isNumeric(definition))
return to!int(definition);
if(definition[$-1] == '%') {
if(oneHundredPercent < 0)
return 0;
return cast(int) (to!real(definition[0 .. $-1]) * oneHundredPercent);
}
if(definition[$-2 .. $] == "px")
return to!int(definition[0 .. $ - 2]);
if(definition[$-2 .. $] == "em")
return cast(int) (to!real(definition[0 .. $-2]) * oneEm);
// FIXME: other units of measure...
return 0;
}
}
Color readColor(string v) {
v = v.toLower;
switch(v) {
case "transparent":
return Color(0, 0, 0, 0);
case "red":
return Color(255, 0, 0);
case "green":
return Color(0, 255, 0);
case "blue":
return Color(0, 0, 255);
case "yellow":
return Color(255, 255, 0);
case "white":
return Color(255, 255, 255);
case "black":
return Color(0, 0, 0);
default:
if(v[0] == '#') {
return Color.fromString(v);
} else {
goto case "transparent";
}
}
}
enum TableDisplay : int {
table = 1,
row = 2,
cell = 3,
caption = 4
}
class LayoutData {
Element element;
this(Element parent) {
element = parent;
element.expansionHook = cast(void*) this;
parseStyle;
}
void parseStyle() {
// reset to defaults
renderInline = true;
outsideNormalFlow = false;
renderValueAsText = false;
doNotDraw = false;
if(element.nodeType != 1) {
return; // only tags get style
}
// legitimate attributes FIXME: do these belong here?
if(element.hasAttribute("colspan"))
tableColspan = to!int(element.attrs.colspan);
else
tableColspan = 1;
if(element.hasAttribute("rowspan"))
tableRowspan = to!int(element.attrs.rowspan);
else
tableRowspan = 1;
if(element.tagName == "img") {
try {
auto bytes = cast(ubyte[]) curl(absolutizeUrl(element.src, _contextHack.currentUrl));
auto i = loadImageFromMemory(bytes);
image = Image.fromMemoryImage(i);
width = CssSize(to!string(image.width) ~ "px");
height = CssSize(to!string(image.height) ~ "px");
} catch (Throwable t) {
writeln(t.toString);
image = null;
}
}
CssSize readSize(string v) {
return CssSize(v);
/*
if(v.indexOf("px") == -1)
return 0;
return to!int(v[0 .. $-2]);
*/
}
auto style = element.computedStyle;
//if(element.tagName == "a")
//assert(0, style.toString);
foreach(item; style.properties) {
string value = item.value;
Element curr = element;
while(value == "inherit" && curr.parentNode !is null) {
curr = curr.parentNode;
value = curr.computedStyle.getValue(item.name);
}
if(value == "inherit")
assert(0, item.name ~ " came in as inherit all the way up the chain");
switch(item.name) {
case "attribute-as-text":
renderValueAsText = true;
break;
case "margin-bottom":
marginBottom = readSize(value);
break;
case "margin-top":
marginTop = readSize(value);
break;
case "margin-left":
marginLeft = readSize(value);
break;
case "margin-right":
marginRight = readSize(value);
break;
case "padding-bottom":
paddingBottom = readSize(value);
break;
case "padding-top":
paddingTop = readSize(value);
break;
case "padding-left":
paddingLeft = readSize(value);
break;
case "padding-right":
paddingRight = readSize(value);
break;
case "visibility":
if(value == "hidden")
doNotDraw = true;
else
doNotDraw = false;
break;
case "width":
if(value == "auto")
width = CssSize();
else
width = readSize(value);
break;
case "height":
if(value == "auto")
height = CssSize();
else
height = readSize(value);
break;
case "display":
tableDisplay = 0;
switch(value) {
case "block":
renderInline = false;
break;
case "inline":
renderInline = true;
break;
case "none":
doNotRender = true;
break;
case "list-item":
renderInline = false;
// FIXME - show the list marker too
break;
case "inline-block":
renderInline = false; // FIXME
break;
case "table":
renderInline = false;
goto case;
case "inline-table":
tableDisplay = TableDisplay.table;
break;
case "table-row":
tableDisplay = TableDisplay.row;
break;
case "table-cell":
tableDisplay = TableDisplay.cell;
break;
case "table-caption":
tableDisplay = TableDisplay.caption;
break;
case "run-in":
/* do these even matter? */
case "table-header-group":
case "table-footer-group":
case "table-row-group":
case "table-column":
case "table-column-group":
default:
// FIXME
}
if(value == "table-row")
renderInline = false;
break;
case "position":
position = value;
if(position == "absolute" || position == "fixed")
outsideNormalFlow = true;
break;
case "top":
top = CssSize(value);
break;
case "bottom":
bottom = CssSize(value);
break;
case "right":
right = CssSize(value);
break;
case "left":
left = CssSize(value);
break;
case "color":
foregroundColor = readColor(value);
break;
case "background-color":
backgroundColor = readColor(value);
break;
case "float":
switch(value) {
case "none": cssFloat = 0; outsideNormalFlow = false; break;
case "left": cssFloat = 1; outsideNormalFlow = true; break;
case "right": cssFloat = 2; outsideNormalFlow = true; break;
default: assert(0);
}
break;
case "clear":
switch(value) {
case "none": floatClear = 0; break;
case "left": floatClear = 1; break;
case "right": floatClear = 2; break;
case "both": floatClear = 1; break; // FIXME
default: assert(0);
}
break;
case "border":
borderWidth = CssSize("1px");
break;
default:
}
}
// FIXME
if(tableDisplay == TableDisplay.row) {
renderInline = false;
} else if(tableDisplay == TableDisplay.cell)
renderInline = true;
}
static LayoutData get(Element e) {
if(e.expansionHook is null)
return new LayoutData(e);
return cast(LayoutData) e.expansionHook;
}
EventHandler[][string] bubblingEventHandlers;
EventHandler[][string] capturingEventHandlers;
EventHandler[string] defaultEventHandlers;
int absoluteLeft() {
int a = offsetLeft;
// FIXME: dead wrong
/*
auto p = offsetParent;
while(p) {
auto l = LayoutData.get(p);
a += l.offsetLeft;
p = l.offsetParent;
}*/
return a;
}
int absoluteTop() {
int a = offsetTop;
/*
auto p = offsetParent;
while(p) {
auto l = LayoutData.get(p);
a += l.offsetTop;
p = l.offsetParent;
}*/
return a;
}
int offsetWidth;
int offsetHeight;
int offsetLeft;
int offsetTop;
Element offsetParent;
CssSize borderWidth;
CssSize paddingLeft;
CssSize paddingRight;
CssSize paddingTop;
CssSize paddingBottom;
CssSize marginLeft;
CssSize marginRight;
CssSize marginTop;
CssSize marginBottom;
CssSize width;
CssSize height;
string position;
CssSize left;
CssSize top;
CssSize right;
CssSize bottom;
Color borderColor;
Color backgroundColor;
Color foregroundColor;
int zIndex;
/* pseudo classes */
bool hover;
bool active;
bool focus;
bool link;
bool visited;
bool selected;
bool checked;
/* done */
/* CSS styles */
bool doNotRender;
bool doNotDraw;
bool renderInline;
bool renderValueAsText;
int tableDisplay; // 1= table, 2 = table-row, 3 = table-cell, 4 = table-caption
int tableColspan;
int tableRowspan;
int cssFloat;
int floatClear;
string textToRender;
bool outsideNormalFlow;
/* Efficiency flags */
static bool someRepaintRequired;
bool repaintRequired;
void invalidate() {
repaintRequired = true;
someRepaintRequired = true;
}
void paintCompleted() {
repaintRequired = false;
someRepaintRequired = false; // FIXME
}
Image image;
}
Element elementFromPoint(Document document, int x, int y) {
int winningZIndex = int.min;
Element winner;
foreach(element; document.mainBody.tree) {
if(element.nodeType == 3) // do I want this?
continue;
auto e = LayoutData.get(element);
if(e.doNotRender)
continue;
if(
e.zIndex >= winningZIndex
&&
x >= e.absoluteLeft() && x < e.absoluteLeft() + e.offsetWidth
&&
y >= e.absoluteTop() && y < e.absoluteTop() + e.offsetHeight
) {
winner = e.element;
winningZIndex = e.zIndex;
}
}
return winner;
}
int longestLine(string a) {
int longest = 0;
foreach(l; a.split("\n"))
if(l.length > longest)
longest = cast(int) l.length;
return longest;
}
int getTableCells(Element row) {
int count;
foreach(c; row.childNodes) {
auto l = LayoutData.get(c);
if(l.tableDisplay == TableDisplay.cell)
count += l.tableColspan;
}
return count;
}
// returns: dom structure changed
bool layout(Element element, int containerWidth, int containerHeight, int cx, int cy, bool canWrap, int parentContainerWidth = 0) {
auto oneEm = 16;
if(element.tagName == "head")
return false;
auto l = LayoutData.get(element);
if(l.doNotRender)
return false;
if(element.nodeType == 3 && element.nodeValue.strip.length == 0) {
l.doNotRender = true;
return false;
}
if(!l.renderInline) {
cx += l.marginLeft.getPixels(oneEm, containerWidth); // FIXME: does this belong here?
//cy += l.marginTop.getPixels(oneEm, containerHeight);
containerWidth -= l.marginLeft.getPixels(oneEm, containerWidth) + l.marginRight.getPixels(oneEm, containerWidth);
//containerHeight -= l.marginTop.getPixels(oneEm, containerHeight) + l.marginBottom.getPixels(oneEm, containerHeight);
}
l.offsetLeft = cx;
l.offsetTop = cy;
//if(!l.renderInline) {
cx += l.paddingLeft.getPixels(oneEm, containerWidth);
cy += l.paddingTop.getPixels(oneEm, containerHeight);
containerWidth -= l.paddingLeft.getPixels(oneEm, containerWidth) + l.paddingRight.getPixels(oneEm, containerWidth);
containerHeight -= l.paddingTop.getPixels(oneEm, containerHeight) + l.paddingBottom.getPixels(oneEm, containerHeight);
//}
auto initialX = cx;
auto initialY = cy;
auto availableWidth = containerWidth;
auto availableHeight = containerHeight;
int fx; // current position for floats
int fy;
int boundingWidth;
int boundingHeight;
int biggestWidth;
int biggestHeight;
int lastMarginBottom;
int lastMarginApplied;
bool hasContentLeft;
int cssWidth = l.width.getPixels(oneEm, containerWidth);
int cssHeight = l.height.getPixels(oneEm, containerHeight);
bool widthSet = false;
if(l.tableDisplay == TableDisplay.cell && !widthSet) {
l.offsetWidth = l.tableColspan * parentContainerWidth / getTableCells(l.element.parentNode);
widthSet = true;
containerWidth = l.offsetWidth;
availableWidth = containerWidth;
}
int skip;
startAgain:
// now, we layout the children to collect all that info together
foreach(i, child; element.childNodes) {
if(skip) {
skip--;
continue;
}
auto childLayout = LayoutData.get(child);
if(!childLayout.outsideNormalFlow && !childLayout.renderInline && hasContentLeft) {
cx = initialX;
cy += biggestHeight;
availableWidth = containerWidth;
availableHeight -= biggestHeight;
hasContentLeft = false;
biggestHeight = 0;
}
if(childLayout.floatClear) {
cx = initialX;
if(max(fy, cy) != cy)
availableHeight -= fy - cy;
cy = max(fy, cy);
hasContentLeft = false;
biggestHeight = 0;
}
auto currentMargin = childLayout.marginTop.getPixels(oneEm, containerHeight);
currentMargin = max(currentMargin, lastMarginBottom) - lastMarginBottom;
if(currentMargin < 0)
currentMargin = 0;
if(!lastMarginApplied && max(currentMargin, lastMarginBottom) > 0)
currentMargin = max(currentMargin, lastMarginBottom);
lastMarginApplied = currentMargin;
cy += currentMargin;
containerHeight -= currentMargin;
bool changed = layout(child, availableWidth, availableHeight, cx, cy, !l.renderInline, containerWidth);
if(childLayout.cssFloat) {
childLayout.offsetTop += fy;
foreach(bele; child.tree) {
auto lolol = LayoutData.get(bele);
lolol.offsetTop += fy;
}
fx += childLayout.offsetWidth;
fy += childLayout.offsetHeight;
}
if(childLayout.doNotRender || childLayout.outsideNormalFlow)
continue;
//if(childLayout.offsetHeight < 0)
//childLayout.offsetHeight = 0;
//if(childLayout.offsetWidth < 0)
//childLayout.offsetWidth = 0;
assert(childLayout.offsetHeight >= 0);
assert(childLayout.offsetWidth >= 0);
// inline elements can't have blocks inside
//if(!childLayout.renderInline)
//l.renderInline = false;
lastMarginBottom = childLayout.marginBottom.getPixels(oneEm, containerHeight);
if(childLayout.offsetWidth > biggestWidth)
biggestWidth = childLayout.offsetWidth;
if(childLayout.offsetHeight > biggestHeight)
biggestHeight = childLayout.offsetHeight;
availableWidth -= childLayout.offsetWidth;
if(cx + childLayout.offsetWidth > boundingWidth)
boundingWidth = cx + childLayout.offsetWidth;
// if the dom was changed, it was to wrap...
if(changed || availableWidth <= 0) {
// gotta move to a new line
availableWidth = containerWidth;
cx = initialX;
cy += biggestHeight;
biggestHeight = 0;
availableHeight -= childLayout.offsetHeight;
hasContentLeft = false;
//writeln("new line now at ", cy);
} else {
// can still use this one
cx += childLayout.offsetWidth;
hasContentLeft = true;
}
if(changed) {
skip = cast(int) i;
writeln("dom changed");
goto startAgain;
}
}
if(hasContentLeft)
cy += biggestHeight; // line-height
boundingHeight = cy - initialY + l.paddingTop.getPixels(oneEm, containerHeight) + l.paddingBottom.getPixels(oneEm, containerHeight);
// And finally, layout this element itself
if(element.nodeType == 3) {
bool wrapIt;
if(element.computedStyle.getValue("white-space") == "pre") {
l.textToRender = element.nodeValue;
} else {
l.textToRender = replace(element.nodeValue,"\n", " ").replace("\t", " ").replace("\r", " ");//.squeeze(" "); // FIXME
wrapIt = true;
}
if(l.textToRender.length == 0) {
l.doNotRender = true;
return false;
}
if(wrapIt) {
auto lineWidth = containerWidth / 6;
bool startedWithSpace = l.textToRender[0] == ' ';
if(l.textToRender.length > lineWidth)
l.textToRender = wrap(l.textToRender, lineWidth);
if(l.textToRender[$-1] == '\n')
l.textToRender = l.textToRender[0 .. $-1];
if(startedWithSpace && l.textToRender[0] != ' ')
l.textToRender = " " ~ l.textToRender;
}
bool contentChanged = false;
// we can wrap so let's do it
/*
auto lineIdx = l.textToRender.indexOf("\n");
if(canWrap && lineIdx != -1) {
writeln("changing ***", l.textToRender, "***");
auto remaining = l.textToRender[lineIdx + 1 .. $];
l.textToRender = l.textToRender[0 .. lineIdx];
Element[] txt;
txt ~= new TextNode(element.parentDocument, l.textToRender);
txt ~= new TextNode(element.parentDocument, "\n");
txt ~= new TextNode(element.parentDocument, remaining);
element.parentNode.replaceChild(element, txt);
contentChanged = true;
}
*/
if(l.textToRender.length != 0) {
l.offsetHeight = cast(int) count(l.textToRender, "\n") * 16 + 16; // lines * line-height
l.offsetWidth = l.textToRender.longestLine * 6; // inline
} else {
l.offsetWidth = 0;
l.offsetHeight = 0;
}
l.renderInline = true;
//writefln("Text %s at (%s, %s) with size %sx%s", element.tagName, l.offsetLeft, l.offsetTop, l.offsetWidth, l.offsetHeight);
return contentChanged;
}
// images get special treatment too
if(l.image !is null) {
if(!widthSet)
l.offsetWidth = l.image.width;
l.offsetHeight = l.image.height;
//writefln("Image %s at (%s, %s) with size %sx%s", element.tagName, l.offsetLeft, l.offsetTop, l.offsetWidth, l.offsetHeight);
return false;
}
/*
// tables constrain floats...
if(l.tableDisplay == TableDisplay.cell) {
l.offsetHeight += fy;
}
*/
// layout an inline element...
if(l.renderInline) {
//if(l.tableDisplay == TableDisplay.cell) {
//auto ow = widthSet ? l.offsetWidth : 0;
//l.offsetWidth = min(ow, boundingWidth - initialX);
//if(l.offsetWidth < 0)
//l.offsetWidth = 0;
//} else
if(!widthSet) {
l.offsetWidth = boundingWidth - initialX; // FIXME: padding?
if(l.offsetWidth < 0)
l.offsetWidth = 0;
}
l.offsetHeight = max(boundingHeight, biggestHeight);
//writefln("Inline element %s at (%s, %s) with size %sx%s", element.tagName, l.offsetLeft, l.offsetTop, l.offsetWidth, l.offsetHeight);
// and layout a block element
} else {
l.offsetWidth = containerWidth;
l.offsetHeight = boundingHeight;
//writefln("Block element %s at (%s, %s) with size %sx%s", element.tagName, l.offsetLeft, l.offsetTop, l.offsetWidth, l.offsetHeight);
}
if(l.position == "absolute") {
l.offsetTop = l.top.getPixels(oneEm, containerHeight);
l.offsetLeft = l.left.getPixels(oneEm, containerWidth);
// l.offsetRight = l.right.getPixels(oneEm, containerWidth);
// l.offsetBottom = l.bottom.getPixels(oneEm, containerHeight);
} else if(l.position == "relative") {
l.offsetTop = l.top.getPixels(oneEm, containerHeight);
l.offsetLeft = l.left.getPixels(oneEm, containerWidth);
// l.offsetRight = l.right.getPixels(oneEm, containerWidth);
// l.offsetBottom = l.bottom.getPixels(oneEm, containerHeight);
}
// table cells need special treatment
if(!l.tableDisplay) {
if(cssWidth) {
l.offsetWidth = cssWidth;
containerWidth = min(containerWidth, cssWidth);
// not setting widthSet since this is just a hint
}
if(cssHeight) {
l.offsetHeight = cssHeight;
containerHeight = min(containerHeight, cssHeight);
}
}
/*
// table cell
if(l.tableDisplay == 2) {
l.offsetWidth = containerWidth;
}
*/
// a table row, and all it's cell children, have the same height
if(l.tableDisplay == TableDisplay.row) {
int maxHeight = 0;
foreach(e; element.childNodes) {
auto el = LayoutData.get(e);
if(el.tableDisplay == TableDisplay.cell) {
if(el.offsetHeight > maxHeight)
maxHeight = el.offsetHeight;
}
}
foreach(e; element.childNodes) {
auto el = LayoutData.get(e);
if(el.tableDisplay == TableDisplay.cell) {
el.offsetHeight = maxHeight;
}
}
l.offsetHeight = maxHeight;
}
// every column in a table has equal width
// assert(l.offsetHeight == 0 || l.offsetHeight > 10, format("%s on %s %s", l.offsetHeight, element.tagName, element.id ~ "." ~ element.className));
return false;
}
int scrollTop = 0;
void drawElement(ScreenPainter p, Element ele, int startingX, int startingY) {
auto oneEm = 1;
// margin is handled in the layout phase, but border, padding, and obviously, content are handled here
auto l = LayoutData.get(ele);
if(l.doNotDraw)
return;
if(l.doNotRender)
return;
startingX = 0; // FIXME
startingY = 0; // FIXME why does this fix things?
int cx = l.offsetLeft + startingX, cy = l.offsetTop + startingY, cw = l.offsetWidth, ch = l.offsetHeight;
if(l.image !is null) {
p.drawImage(Point(cx, cy - scrollTop), l.image);
}
//if(cw <= 0 || ch <= 0)
// return;
if(l.borderWidth.getPixels(oneEm, 1) > 0) {
p.fillColor = Color(0, 0, 0, 0);
p.outlineColor = l.borderColor;
// FIXME: handle actual widths by selecting a pen
p.drawRectangle(Point(cx, cy - scrollTop), cw, ch); // draws the border
}
int sx = cx, sy = cy;
cx += l.borderWidth.getPixels(oneEm, 1);
cy += l.borderWidth.getPixels(oneEm, 1);
cw -= l.borderWidth.getPixels(oneEm, 1) * 2;
ch -= l.borderWidth.getPixels(oneEm, 1) * 2;
p.fillColor = l.backgroundColor;
p.outlineColor = Color(0, 0, 0, 0);
if(ele.tagName == "body") { // HACK to make the body bg apply to the whole window
cx = 0;
cy = 0;
cw = p.window.width;
ch = p.window.height;
p.drawRectangle(Point(0, 0), p.window.width, p.window.height); // draw the padding box
} else
p.drawRectangle(Point(cx, cy - scrollTop), cw, ch); // draw the padding box
if(l.renderValueAsText && ele.value.length) {
p.outlineColor = l.foregroundColor;
p.drawText(Point(
cx + l.paddingLeft.getPixels(oneEm, 1),
cy + l.paddingTop.getPixels(oneEm, 1) - scrollTop),
ele.value);
}
//p.fillColor = Color(255, 255, 255);
//p.drawRectangle(Point(cx, cy), cw, ch); // draw the content box
foreach(e; ele.childNodes) {
if(e.nodeType == 3) {
auto thisL = LayoutData.get(e);
p.outlineColor = LayoutData.get(e.parentNode).foregroundColor;
p.drawText(Point(thisL.offsetLeft, thisL.offsetTop - scrollTop), toAscii(LayoutData.get(e).textToRender));
} else
drawElement(p, e, sx, sy);
}
l.repaintRequired = false;
}
string toAscii(string s) {
string ret;
foreach(dchar c; s) {
if(c < 128 && c > 0)
ret ~= cast(char) c;
else switch(c) {
case '\u00a0': // nbsp
ret ~= ' ';
break;
case '\u2018':
case '\u2019':
ret ~= "'";
break;
case '\u201c':
case '\u201d':
ret ~= "\"";
break;
default:
// skip non-ascii
}
}
return ret;
}
class Event {
this(string eventName, Element target) {
this.eventName = eventName;
this.srcElement = target;
}
void preventDefault() {
defaultPrevented = true;
}
void stopPropagation() {
propagationStopped = true;
}
bool defaultPrevented;
bool propagationStopped;
string eventName;
Element srcElement;
alias srcElement target;
Element relatedTarget;
int clientX;
int clientY;
int button;
bool isBubbling;
void send() {
if(srcElement is null)
return;
auto e = LayoutData.get(srcElement);
if(eventName in e.bubblingEventHandlers)
foreach(handler; e.bubblingEventHandlers[eventName])
handler(e.element, this);
if(!defaultPrevented)
if(eventName in e.defaultEventHandlers)
e.defaultEventHandlers[eventName](e.element, this);
}
void dispatch() {
if(srcElement is null)
return;
// first capture, then bubble
LayoutData[] chain;
Element curr = srcElement;
while(curr) {
auto l = LayoutData.get(curr);
chain ~= l;
curr = curr.parentNode;
}
isBubbling = false;
foreach(e; chain.retro) {
if(eventName in e.capturingEventHandlers)
foreach(handler; e.capturingEventHandlers[eventName])
handler(e.element, this);
// the default on capture should really be to always do nothing
//if(!defaultPrevented)
// if(eventName in e.defaultEventHandlers)
// e.defaultEventHandlers[eventName](e.element, this);
if(propagationStopped)
break;
}
isBubbling = true;
if(!propagationStopped)
foreach(e; chain) {
if(eventName in e.bubblingEventHandlers)
foreach(handler; e.bubblingEventHandlers[eventName])
handler(e.element, this);
if(!defaultPrevented)
if(eventName in e.defaultEventHandlers)
e.defaultEventHandlers[eventName](e.element, this);
if(propagationStopped)
break;
}
}
}
void addEventListener(string event, Element what, EventHandler handler, bool bubble = true) {
if(event.length > 2 && event[0..2] == "on")
event = event[2 .. $];
auto l = LayoutData.get(what);
if(bubble)
l.bubblingEventHandlers[event] ~= handler;
else
l.capturingEventHandlers[event] ~= handler;
}
void addEventListener(string event, Element[] what, EventHandler handler, bool bubble = true) {
foreach(w; what)
addEventListener(event, w, handler, bubble);
}
bool isAParentOf(Element a, Element b) {
if(a is null || b is null)
return false;
while(b !is null) {
if(a is b)
return true;
b = b.parentNode;
}
return false;
}
void runHtmlWidget(SimpleWindow win, BrowsingContext context) {
Element mouseLastOver;
win.eventLoop(0,
(MouseEvent e) {
auto ele = elementFromPoint(context.document, e.x, e.y + scrollTop);
if(mouseLastOver !is ele) {
Event event;
if(ele !is null) {
if(!isAParentOf(ele, mouseLastOver)) {
//writeln("mouseenter on ", ele.tagName);
event = new Event("mouseenter", ele);
event.relatedTarget = mouseLastOver;
event.send();
}
}
if(mouseLastOver !is null) {
if(!isAParentOf(mouseLastOver, ele)) {
event = new Event("mouseleave", mouseLastOver);
event.relatedTarget = ele;
event.send();
}
}
if(ele !is null) {
event = new Event("mouseover", ele);
event.relatedTarget = mouseLastOver;
event.dispatch();
}
if(mouseLastOver !is null) {
event = new Event("mouseout", mouseLastOver);
event.relatedTarget = ele;
event.dispatch();
}
mouseLastOver = ele;
}
if(ele !is null) {
auto l = LayoutData.get(ele);
auto event = new Event(
e.type == 0 ? "mousemove"
: e.type == 1 ? "mousedown"
: e.type == 2 ? "mouseup"
: impossible
, ele);
event.clientX = e.x;
event.clientY = e.y;
event.button = e.button;
event.dispatch();
if(l.someRepaintRequired) {
auto p = win.draw();
p.clear();
drawElement(p, context.document.mainBody, 0, 0);
l.paintCompleted();
}
}
},
(dchar key) {
auto s = scrollTop;
if(key == 'j')
scrollTop += 16;
else if(key == 'k')
scrollTop -= 16;
if(key == 'n')
scrollTop += 160;
else if(key == 'm')
scrollTop -= 160;
if(context.focusedElement !is null) {
context.focusedElement.value = context.focusedElement.value ~ cast(char) key;
auto p = win.draw();
drawElement(p, context.focusedElement, 0, 0);
}
if(s != scrollTop) {
auto p = win.draw();
p.clear();
drawElement(p, context.document.mainBody, 0, 0);
}
if(key == 'q')
win.close();
});
}
class BrowsingContext {
string currentUrl;
Document document;
Element focusedElement;
}
string absolutizeUrl(string url, string currentUrl) {
if(url.length == 0)
return null;
auto current = currentUrl;
auto idx = current.lastIndexOf("/");
if(idx != -1 && idx > 7)
current = current[0 .. idx + 1];
if(url[0] == '/') {
auto i = current[8 .. $].indexOf("/");
if(i != -1)
current = current[0 .. i + 8];
}
if(url.length < 7 || url[0 .. 7] != "http://")
url = current ~ url;
return url;
}
BrowsingContext _contextHack; // FIXME: the images aren't done sanely
import arsd.curl;
Document gotoSite(SimpleWindow win, BrowsingContext context, string url, string post = null) {
_contextHack = context;
auto p = win.draw;
p.fillColor = Color(255, 255, 255);
p.outlineColor = Color(0, 0, 0);
p.drawRectangle(Point(0, 0), 800, 800);
auto document = new Document(curl(url.absolutizeUrl(context.currentUrl), post));
context.document = document;
context.currentUrl = url.absolutizeUrl(context.currentUrl);
string styleSheetText = import("default.css");
foreach(ele; document.querySelectorAll("head link[rel=stylesheet]")) {
if(!ele.hasAttribute("media") || ele.attrs.media().indexOf("screen") != -1)
styleSheetText ~= curl(ele.href.absolutizeUrl(context.currentUrl));
}
foreach(ele; document.getElementsByTagName("style"))
styleSheetText ~= ele.innerHTML;
styleSheetText = styleSheetText.replace(`@import "/style_common.css";`, curl("http://arsdnet.net/style_common.css"));
auto styleSheet = new StyleSheet(styleSheetText);
styleSheet.apply(document);
foreach(e; document.root.tree)
LayoutData.get(e); // initializing the css here
return document;
}
string impossible() {
assert(0);
//return null;
}