Specs: Initial hack at extensible style/layout Review URL: https://codereview.chromium.org/716013002
diff --git a/sky/examples/style/block-layout.sky b/sky/examples/style/block-layout.sky new file mode 100644 index 0000000..b303db0 --- /dev/null +++ b/sky/examples/style/block-layout.sky
@@ -0,0 +1,74 @@ +SKY MODULE +<import src="sky:core" as="sky"/> +<!-- + ! this module provides trivial vertical block layout + ! no margins, padding, borders, etc + !--> +<script> + module.exports.BlockLayoutManager = class BlockLayoutManager extends sky.LayoutManager { + function layout(width, height) { + if (width == null) + width = this.getIntrinsicWidth().value; + let autoHeight = false; + if (height == null) { + height = 0; + autoHeight = true; + } + this.assumeDimensions(width, height); + let children = this.walkChildren(); + let loop = children.next(); + let y = 0; + while (!loop.done) { + let child = loop.value; + if (child.needsLayout) { + let dims = child.layoutManager.layout(width, null); + this.setChildSize(child, dims.width, dims.height); + } + this.setChildPosition(child, 0, y); + y += child.height; + loop = children.next(); + } + if (autoHeight) + height = y; + return { + width: width, + height: height, + } + } + function getIntrinsicWidth() { + let width = this.node.getProperty('width'); + if (typeof height != 'number') { + // e.g. width: auto + width = 0; + let children = this.walkChildren(); + let loop = children.next(); + while (!loop.done) { + let child = loop.value; + let childWidth = child.layoutManager.getIntrinsicWidth(); + if (width < childWidth.value) + width = childWidth.value; + loop = children.next(); + } + } + return super(width); // applies and provides our own min-width/max-width rules + } + function getIntrinsicHeight() { + let height = this.node.getProperty('height'); + if (typeof height != 'number') { + // e.g. height: auto + height = 0; + let children = this.walkChildren(); + let loop = children.next(); + while (!loop.done) { + let child = loop.value; + let childHeight = child.layoutManager.getIntrinsicHeight(); + if (height < childHeight.value) + height = childHeight.value; + loop = children.next(); + } + } + return super(height); // applies and provides our own min-width/max-width rules + } + } + sky.registerLayoutManager('block', module.exports.BlockLayoutManager); +</script>
diff --git a/sky/examples/style/hex-layout.sky b/sky/examples/style/hex-layout.sky new file mode 100644 index 0000000..ca286f0 --- /dev/null +++ b/sky/examples/style/hex-layout.sky
@@ -0,0 +1,146 @@ +#!mojo mojo:sky +<import src="sky:core" as="sky"/> +<script> + class BeehiveLayoutManager extends sky.LayoutManager { + function layout(width, height) { + if (width == null) + width = this.getIntrinsicWidth().value; + let autoHeight = false; + if (height == null) { + height = 0; + autoHeight = true; + } + this.assumeDimensions(width, height); + let cellCount = this.node.getProperty('beehive-count'); + let cellDim = width / cellCount; + let children = this.walkChildren(); + let loop = children.next(); + let x = 0; + let y = 0; + while (!loop.done) { + let child = loop.value; + if (child.needsLayout) { + child.layoutManager.layout(cellDim, cellDim); + // we ignore the size the child reported from layout(), and force it to the cell dimensions + this.setChildSize(child, cellDim, cellDim); + } + this.setChildPosition(child, x * cellDim + (y % 2) * cellDim/2, y * 3/4 * cellDim); + x += 1; + if (x > cellCount) { + y += 1; + x = 0; + } + loop = children.next(); + } + if (height == 0) + height = (1 + y * 3/4) * cellDim; + return { + width: width, + height: height, + } + } + function getIntrinsicWidth() { + // this is the logic that LayoutManager.getIntrinsicWidth() has by default + // shown here because I wrote it before realising it should be the default + let width = this.node.getProperty('width'); + if (typeof width != 'number') + width = 0; + let minWidth = this.node.getProperty('min-width'); + if (typeof width != 'number') + minWidth = 0; + let maxWidth = this.node.getProperty('max-width'); + if (typeof width != 'number') + maxWidth = Infinity; + if (maxWidth < minWidth) + maxWidth = minWidth; + if (width > maxWidth) + width = maxWidth; + if (width < minWidth) + width = minWidth; + return { + minimum: minWidth, + value: width, + maximum: maxWidth, + }; + } + function getIntrinsicHeight() { + let height = this.node.getProperty('height'); + if (typeof height != 'number') { + // e.g. height: auto + width = this.getIntrinsicWidth().value; + let cellCount = this.node.getProperty('beehive-count'); + let cellDim = width / cellCount; + let children = this.walkChildren(); + let loop = children.next(); + let childCount = 0; + while (!loop.done) { + childCount += 1; + loop.next(); + } + if (childCount > 0) + height = cellDim * (1/4 + Math.ceil(childCount / cellCount) * 3/4); + else + height = 0; + } + return super(height); // does the equivalent of getIntrinsicWidth() above, applying min-height etc + } + function paintChildren(RenderingSurface canvas) { + let width = this.node.width; + let cellCount = this.node.getProperty('beehive-count'); + let cellDim = width / cellCount; + let children = this.walkChildren(); + let loop = children.next(); + while (!loop.done) { + let child = loop.value; + if (child.needsPaint) { + canvas.save(); + try { + canvas.beginPath(); + canvas.moveTo(child.x, child.y + cellDim/4); + canvas.lineTo(child.x + cellDim/2, child.y); + canvas.lineTo(child.x + cellDim, child.y + cellDim/4); + canvas.lineTo(child.x + cellDim, child.y + 3*cellDim/4); + canvas.lineTo(child.x + cellDim/2, child.y + cellDim); + canvas.moveTo(child.x, child.y + 3*cellDim/4); + canvas.closePath(); + canvas.clip(); + child.paint(canvas); + } finally { + canvas.restore(); + } + } + loop = children.next(); + } + } + } + sky.registerLayoutManager('beehive', BeehiveLayoutManager); + let BeehiveCountStyleValueType = new StyleValueType(); + BeehiveCountStyleValueType.addParser((tokens) => { + let token = tokens.next(); + if (token.done) throw new Error(); + if (token.value.kind != 'number') throw new Error(); + if (token.value.value <= 0) throw new Error(); + if (Math.trunc(token.value.value) != token.value.value) throw new Error(); + let result = token.value.value; + if (!token.next().done) throw new Error(); + return result; + }); + sky.registerProperty({ + name: 'beehive-count', + type: BeehiveCountStyleValueType, + inherits: true, + initialValue: 5, + needsLayout: true, + }); +</script> +<style> + div { display: beehive; beehive-count: 3; } +</style> +<div> + <t>Hello</t> + <t>World</t> + <t>How</t> + <t>Are</t> + <t>You</t> + <t>Today?</t> +</div>
diff --git a/sky/examples/style/sky-core-styles.sky b/sky/examples/style/sky-core-styles.sky new file mode 100644 index 0000000..640236a --- /dev/null +++ b/sky/examples/style/sky-core-styles.sky
@@ -0,0 +1,143 @@ +SKY MODULE +<!-- this is part of sky:core --> +<script> + // "internals" is an object only made visible to this module that exports stuff implemented in C++ + module.exports.registerProperty = internals.registerProperty; + internals.registerLayoutManager('none', null); + module.exports.LayoutManager = internals.LayoutManager; + module.exports.InlineLayoutManager = internals.InlineLayoutManager; + internals.registerLayoutManager('inline', internals.InlineLayoutManager); + module.exports.ParagraphLayoutManager = internals.ParagraphLayoutManager; + internals.registerLayoutManager('paragraph', internals.ParagraphLayoutManager); + module.exports.BlockLayoutManager = internals.BlockLayoutManager; + internals.registerLayoutManager('block', internals.BlockLayoutManager); + + let displayTypes = new Map(); + module.exports.registerLayoutManager = function registerLayoutManager(displayValue, layoutManagerConstructor) { + // TODO(ianh): apply rules for type-checking displayValue is a String + // TODO(ianh): apply rules for type-checking layoutManagerConstructor implements the LayoutManagerConstructor interface (or is null) + if (displayTypes.has(displayValue)) + throw new Error(); + displayTypes.set(displayValue, layoutManagerConstructor); + }; + + module.exports.DisplayStyleValueType = new StyleValueType(); // value is null or a LayoutManagerConstructor + module.exports.DisplayStyleValueType.addParser((tokens) => { + let token = tokens.next(); + if (token.done) + throw new Error(); + if (token.value.kind != 'identifier') + throw new Error(); + if (!displayTypes.has(token.value.value)) + throw new Error(); + return { + value: displayTypes.get(token.value.value), + } + }); + + internals.registerProperty({ + name: 'display', + type: module.exports.DisplayStyleValueType, + inherits: false, + initialValue: internals.BlockLayoutManager, + needsLayout: true, + }); + + module.exports.PositiveLengthStyleValueType = new StyleValueType(); // value is a ParsedValue whose value (once resolved) is a number in 96dpi pixels, >=0 + module.exports.PositiveLengthStyleValueType.addParser((tokens) => { + // just handle "<number>px" + let token = tokens.next(); + if (token.done) + throw new Error(); + if (token.value.kind != 'dimension') + throw new Error(); + if (token.value.unit != 'px') + throw new Error(); + if (token.value.value < 0) + throw new Error(); + return { + value: token.value.value; + }; + }); + + internals.registerProperty({ + name: 'min-width', + type: module.exports.PositiveLengthStyleValueType, + inherits: false, + initialValue: 0, + needsLayout: true, + }); + internals.registerProperty({ + name: 'min-height', + type: module.exports.PositiveLengthStyleValueType, + inherits: false, + initialValue: 0, + needsLayout: true, + }); + + module.exports.PositiveLengthOrAutoStyleValueType = new StyleValueType(); // value is a ParsedValue whose value (once resolved) is either a number in 96dpi pixels (>=0) or null (meaning 'auto') + module.exports.PositiveLengthOrAutoStyleValueType.addParser((tokens) => { + // handle 'auto' + let token = tokens.next(); + if (token.done) + throw new Error(); + if (token.value.kind != 'identifier') + throw new Error(); + if (token.value.value != 'auto') + throw new Error(); + return { + value: null, + }; + }); + module.exports.PositiveLengthOrAutoStyleValueType.addParser((tokens) => { + return module.exports.PositiveLengthStyleValueType.parse(tokens); + }); + + internals.registerProperty({ + name: 'width', + type: module.exports.PositiveLengthOrAutoStyleValueType, + inherits: false, + initialValue: null, + needsLayout: true, + }); + internals.registerProperty({ + name: 'height', + type: module.exporets.PositiveLengthOrAutoStyleValueType, + inherits: false, + initialValue: null, + needsLayout: true, + }); + + module.exports.PositiveLengthOrInfinityStyleValueType = new StyleValueType(); // value is a ParsedValue whose value (once resolved) is either a number in 96dpi pixels (>=0) or Infinity + module.exports.PositiveLengthOrInfinityStyleValueType.addParser((tokens) => { + // handle 'infinity' + let token = tokens.next(); + if (token.done) + throw new Error(); + if (token.value.kind != 'identifier') + throw new Error(); + if (token.value.value != 'infinity') + throw new Error(); + return { + value: Infinity, + }; + }); + module.exports.PositiveLengthOrInfinityStyleValueType.addParser((tokens) => { + return module.exports.PositiveLengthStyleValueType.parse(tokens); + }); + + internals.registerProperty({ + name: 'width', + type: module.exports.PositiveLengthOrInfinityStyleValueType, + inherits: false, + initialValue: Infinity, + needsLayout: true, + }); + internals.registerProperty({ + name: 'height', + type: module.exporets.PositiveLengthOrInfinityStyleValueType, + inherits: false, + initialValue: Infinity, + needsLayout: true, + }); +</script>
diff --git a/sky/examples/style/toolbar-layout.sky b/sky/examples/style/toolbar-layout.sky new file mode 100644 index 0000000..a43ce3f --- /dev/null +++ b/sky/examples/style/toolbar-layout.sky
@@ -0,0 +1,208 @@ +SKY MODULE +<import src="sky:core" as="sky"/> +<script> + // display: toolbar; + // toolbar-spacing: <length> + // display: spring; // remaining space is split equally amongst the springs + // children are vertically centered, layout out left-to-right with toolbar-spacing space between them + // last child is hidden by default unless there's not enough room for the others, then it's shown last, right-aligned + module.exports.SpringLayoutManager = class SpringLayoutManager extends sky.LayoutManager { } + sky.registerLayoutManager('spring', module.exports.SpringLayoutManager); + sky.registerProperty({ + name: 'toolbar-spacing', + type: sky.LengthStyleValueType, + inherits: true, + initialValue: { value: 8, unit: 'px' }, + needsLayout: true, + }); + module.exports.ToolbarLayoutManager = class ToolbarLayoutManager extends sky.LayoutManager { + constructor (styleNode) { + super(styleNode); + this.showingOverflow = false; + this.firstSkippedChild = null; + this.overflowChild = null; + } + function layout(width, height) { + let children = null; + let loop = null; + if (height == null) + height = this.getIntrinsicHeight().value; + if (width == null) + this.assumeDimensions(0, height); + else + this.assumeDimensions(width, height); + let spacing = this.node.getProperty('toolbar-spacing'); + if (typeof spacing != 'number') + spacing = 0; + this.overflowChild = null; + this.firstSkippedChild = null; + + // layout children and figure out whether we need to truncate the child list and show the overflow child + let springCount = 0; + let minX = 0; + let overflowChildWidth = 0; + let pendingSpacing = 0; + children = this.walkChildren(); + loop = children.next(); + while (!loop.done) { + let child = loop.value; + let dims = null; + if (child.layoutManager instanceof module.exports.SpringLayoutManager) { + springCount += 1; + pendingSpacing = spacing; // not +=, because we only have one extra spacing per batch of springs + } else { + if (child.needsLayout) { + childHeight = child.layoutManager.getIntrinsicHeight(); + if (childHeight.value < height) + childHeight = childHeight.value; + else + childHeight = height; + dims = child.layoutManager.layout(width, height); + this.setChildSize(child, dims.width, dims.height); + } else { + dims = { + width: child.width, + height: child.height, + }; + } + loop = children.next(); + if (!loop.done) { + if (minX > 0) + minX += spacing + pendingSpacing; + minX += dims.width; + pendingSpacing = 0; + } else { + overflowChildWidth = spacing + dims.width; + this.overflowChild = child; + } + } + } + + // figure out the spacing + this.showingOverflow = false; + let springSize = 0; + if (width != null) { + if (minX <= width) { + if (springCount > 0) + springSize = (width - minX) / sprintCount; + } else { + this.showingOverflow = true; + } + } else { + width = minX; + } + + // position the children + // TODO(ianh): support rtl toolbars + let x = 0; + let lastWasNonSpring = false; + children = this.walkChildren(); + loop = children.next(); + while (!loop.done) { + let child = loop.value; + if (child.layoutManager instanceof module.exports.SpringLayoutManager) { + x += springSize; + if (lastWasNonSpring) + x += spacing; + lastWasNonSpring = false; + } else { + if (!loop.done) { + if (x + child.width + overflowChildWidth > width) { + this.firstSkippedChild = child; + break; // don't display any more children + } + this.setChildPosition(child, x, (height - child.height)/2); + x += child.width + spacing; + lastWasNonSpring = true; + } else { + // assert: this.showingOverflow == false + } + } + } + if (this.showingOverflow) + this.setChildPosition(this.overflowChild, width-this.overflowChild.width, (height - this.overflowChild.height)/2); + else + this.firstSkippedChild = this.overflowChild; + + return { + width: width, + height: height, + } + } + function getIntrinsicWidth() { + let width = this.node.getProperty('width'); + if (typeof height != 'number') { + let spacing = this.node.getProperty('toolbar-spacing'); + if (typeof spacing != 'number') + spacing = 0; + width = 0; + let children = this.walkChildren(); + let loop = children.next(); + // we exclude the last child because at our ideal width we wouldn't need it + let last1 = null; // last one + let last2 = null; // one before the last one + while (!loop.done) { + if (last1) + width += last1.layoutManager.getIntrinsicWidth().value; + if (last2) + width += spacing; + last2 = last1; + last1 = loop.value; + loop = children.next(); + } + } + return super(width); // applies and provides our own min-width/max-width rules + } + function getIntrinsicHeight() { + // we grow our minimum height to be no smaller than the children's + let result = super(); + let determineHeight = false; + let heightProperty = this.node.getProperty('height'); + if (typeof heightProperty != 'number') + determineHeight = true; + let children = this.walkChildren(); + let loop = children.next(); + // here we include the last child so that if it pops in our height doesn't change + while (!loop.done) { + let child = loop.value; + let childHeight = child.layoutManager.getIntrinsicHeight(); + if (determineHeight) { + if (result.value < childHeight.value) + result.value = childHeight.value; + } + if (result.minimum < childHeight.minimum) + result.minimum = childHeight.minimum; + loop = children.next(); + } + if (result.minimum > result.maximum) + result.maximum = result.minimum; + if (result.value > result.maximum) + result.value = result.maximum; + if (result.value < result.minimum) + result.value = result.minimum; + return result; + } + function paintChildren(canvas) { + let width = this.node.width; + let loop = children.next(); + while ((!loop.done) && (loop.value != this.firstSkippedChild)) + this.paintChild(loop.value, canvas); + if (this.showingOverflow) + this.paintChild(this.overflowChild, canvas); + } + function paintChild(child, canvas) { + if (child.needsPaint) { + canvas.save(); + try { + canvas.beginPath(); + canvas.rect(child.x, child.y, child.width, child.height); + canvas.clip(); + child.paint(canvas); + } finally { + canvas.restore(); + } + } + } + } + sky.registerLayoutManager('toolbar', module.exports.ToolbarLayoutManager); +</script>
diff --git a/sky/specs/apis.md b/sky/specs/apis.md index 2346960..b75b5aa 100644 --- a/sky/specs/apis.md +++ b/sky/specs/apis.md
@@ -151,17 +151,17 @@ // BUILT-IN ELEMENTS class ImportElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "import" constructor attribute Boolean shadow; // O(1) // false } class TemplateElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "template" constructor attribute Boolean shadow; // O(1) // false @@ -169,25 +169,25 @@ readonly attribute DocumentFragment content; // O(1) } class ScriptElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "script" constructor attribute Boolean shadow; // O(1) // false } class StyleElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "style" constructor attribute Boolean shadow; // O(1) // false } class ContentElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "content" constructor attribute Boolean shadow; // O(1) // false @@ -195,65 +195,65 @@ Array<Node> getDistributedNodes(); // O(N) in distributed nodes } class ImgElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "img" constructor attribute Boolean shadow; // O(1) // false } class DivElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "div" constructor attribute Boolean shadow; // O(1) // false } class SpanElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "span" constructor attribute Boolean shadow; // O(1) // false } class IframeElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "iframe" constructor attribute Boolean shadow; // O(1) // false } class TElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "t" constructor attribute Boolean shadow; // O(1) // false } class AElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "a" constructor attribute Boolean shadow; // O(1) // false } class TitleElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "title" constructor attribute Boolean shadow; // O(1) // false } class ErrorElement : Element { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; // O(1) // "error" constructor attribute Boolean shadow; // O(1) // false @@ -271,9 +271,9 @@ } interface ElementConstructor { - constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants + constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants constructor (ChildArguments... nodes); // shorthand - constructor (Dictionary attributes); // shorthand + constructor (Dictionary<String> attributes); // shorthand constructor (); // shorthand constructor attribute String tagName; @@ -452,14 +452,16 @@ * ``Integer`` - WebIDL ``long long`` * ``Float`` - WebIDL ``double`` +* ``Infinity`` - singleton type with value ``Infinity`` * ``String`` - WebIDL ``USVString`` * ``Boolean`` - WebIDL ``boolean`` # ``Object`` - WebIDL ``object`` (``ClassName`` can be used as a literal for this type) * ``ClassName`` - an instance of the class ClassName * ``DictionaryName`` - an instance of the dictionary DictionaryName * ``Promise<Type>`` - WebIDL ``Promise<T>`` +* ``Generator<Type>`` - An ECMAScript generator function that returns data of the given type * ``Array<Type>`` - WebIDL ``sequence<T>`` -* ``Dictionary`` - unordered set of name-value String-String pairs with no duplicate names +* ``Dictionary<Type>`` - unordered set of name-value String-Type pairs with no duplicate names * ``Type?`` - union of Type and the singleton type with value ``null`` (WebIDL nullable) * ``(Type1 or Type2)`` - union of Type1 and Type2 (WebIDL union) * ``any`` - union of all types (WebIDL ``any``)
diff --git a/sky/specs/style.md b/sky/specs/style.md index 21c84cd..93c9d4a 100644 --- a/sky/specs/style.md +++ b/sky/specs/style.md
@@ -1,21 +1,510 @@ Sky Style Language ================== -For now, the Sky style language is CSS with the following restrictions: - -- No combinators -- Only = and ~= attribute selectors -- Lots of other selectors removed // TODO(ianh): list them -- Floats removed -- Lots of other layout models removed // TODO(ianh): list them - - Planed changes -------------- Add //-to-end-of-line comments to be consistent with the script language. -Add a way to add new values, e.g. by default only support #RRGGBB -colours (or maybe only rgba() colours), but provide a way to enable -CSS4-like "color(red rgb(+ #004400))" stuff. + +Style Parser +------------ + +(this section is incomplete) + +### Tokenisation + + +#### Value parser + + +##### **Value** state + +If the current character is... + +* '``;``': Consume the character and exit the value parser + successfully. + +* '``@``': Consume the character and switch to the **at** + state. + +* '``#``': Consume the character and switch to the **hash** + state. + +* '``$``': Consume the character and switch to the **dollar** + state. + +* '``%``': Consume the character and switch to the **percent** + state. + +* '``&``': Consume the character and switch to the **ampersand** + state. + +* '``'``': Set _value_ to the empty string, consume the character, and + switch to the **single-quoted string** state. + +* '``"``': Set _value_ to the empty string, consume the character, and + switch to the **double-quoted string** state. + +* '``-``': Consume the character, and switch to the **negative + integer** state. + +* '``0``'-'``9``': Set _value_ to the decimal value of the current + character, consume the character, and switch to the **integer** + state. + +* '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to the current + character, consume the character, and switch to the **identifier** + state. + +* '``*``', '``^``', '``!``', '``?``', '``,``', '``/``', '``<``', + '``[``', '``)``', '``>``', '``]``', '``+``': Emit a symbol token + with the current character as the symbol, consume the character, and + stay in this state. + +* Anything else: Consume the character and switch to the **error** + state. + + +##### **At** state + +* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to + the current character, create a literal token with the unit set to + ``@``, consume the character, and switch to the **literal** state. + +* Anything else: Emit a symbol token with ``@`` as the symbol, and + switch to the **value** state without consuming the character. + + +##### **Hash** state + +* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to + the current character, create a literal token with the unit set to + ``@``, consume the character, and switch to the **literal** state. + +* Anything else: Emit a symbol token with ``#`` as the symbol, and + switch to the **value** state without consuming the character. + + +##### **Dollar** state + +* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to + the current character, create a literal token with the unit set to + ``@``, consume the character, and switch to the **literal** state. + +* Anything else: Emit a symbol token with ``$`` as the symbol, and + switch to the **value** state without consuming the character. + + +##### **Percent** state + +* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to + the current character, create a literal token with the unit set to + ``@``, consume the character, and switch to the **literal** state. + +* Anything else: Emit a symbol token with ``%`` as the symbol, and + switch to the **value** state without consuming the character. + + +##### **Ampersand** state + +* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to + the current character, create a literal token with the unit set to + ``@``, consume the character, and switch to the **literal** state. + +* Anything else: Emit a symbol token with ``&`` as the symbol, and + switch to the **value** state without consuming the character. + + +##### TODO(ianh): more states... + + +##### **Error** state + +If the current character is... + +* '``;``': Consume the character and exit the value parser in failure. + +* Anything else: Consume the character and stay in this state. + + + +Selectors +--------- + +Sky Style uses whatever SelectorQuery. Maybe one day we'll make +SelectorQuery support being extended to support arbitrary selectors, +but for now, it supports: + + tagname + #id + .class + [attrname] + [attrname=value] + :host + +These can be combined (without whitespace), with at most one tagname, +as in: + + tagname[attrname]#id:host.class.class[attrname=value] + +In debug mode, giving two IDs, or the same selector twice (e.g. the +same classname), or specifying other redundant or conflicting +selectors (e.g. [foo][foo=bar], or [foo=bar][foo=baz]) will be +flagged. + +Alternatively, a selector can be the following special value: + + @document + + +Value Parser +------------ + + class StyleToken { + constructor (String king, String value); + readonly attribute String kind; + // string + // identifier + // function (identifier + '(') + // number + // symbol (one of @#$%& if not immediately following numeric or preceding alphanumeric, or one of *^!?,/<[)>]+ or, if not followed by a digit, -) + // dimension (number + identifier or number + one of @#$%&) + // literal (one of @#$%& + alphanumeric) + readonly attribute String value; + readonly attribute String unit; // for 'dimension' type, this is the punctuation or identifier that follows the number, for 'literal' type, this is the punctuation that precedes it + } + + class TokenSource { + constructor (Array<StyleToken> tokens); + IteratorResult next(); + TokenSourceBookmark getBookmark(); + void rewind(TokenSourceBookmark bookmark); + } + class TokenSourceBookmark { + constructor (); + // TokenSource stores unforgeable state on this object using symbols or a weakmap or some such + } + + dictionary ParsedValue { + any value = null; + ValueResolver? resolver = null; + Boolean relativeDimension = false; // if true, e.g. for % lengths, the callback will be called again if an ancestor's dimensions change + Painter? painter = null; + } + + // best practice convention: if you're creating a property with needsPaint, you should + // create a new style value type for it so that it can set the paint callback right; + // you should never use such a style type when parsing another property + + callback any ParserCallback (TokenSource tokens); + + class StyleValueType { + constructor (); + void addParser(ParserCallback parser); + any parse(TokenSource tokens, Boolean root = false); + // for each parser callback that was registered, in reverse + // order (most recently registered first), run these steps: + // let bookmark = tokens.getBookmark(); + // try { + // let result = parser(tokens); + // if (root) { + // if (!tokens.next().done) + // throw new Error(); + // } + // } except { + // tokens.rewind(bookmark); + // } + // (root is set when you need to parse the entire token stream to be valid) + } + + // note: if you define a style value type that uses other style value types, e.g. a "length pair" that accepts two lengths, then + // if any of the subtypes have a resolver, you need to make sure you have a resolver that calls them to compute the final value + + dictionary PropertySettings { + String name; + StyleValueType type; // the output from the parser is coerced to a ParsedValue + Boolean inherits = false; + any initialValue = null; + Boolean needsLayout = false; + Boolean needsPaint = false; + } + + void registerProperty(PropertySettings propertySettings); + // when you register a new property, document the format that is expected to be cascaded + // (the output from the propertySettings.type parser's ParsedValue.value field after the resolver, if any, has been called) + + // sky:core exports a bunch of style value types so that people can + // extend them + attribute StyleValueType PositiveLengthOrInfinityStyleValueType; + attribute StyleValueType PositiveLengthOrAutoStyleValueType; + attribute StyleValueType PositiveLengthStyleValueType; + attribute StyleValueType DisplayStyleValueType; + + +Inline Styles +------------- + +partial class Element { + readonly attribute StyleDeclarationList style; +} + +class StyleDeclarationList { + constructor (); + void add(StyleDeclaration styles); // O(1) // in debug mode, throws if the dictionary has any properties that aren't registered + void remove(StyleDeclaration styles); // O(N) in number of declarations + Array<StyleDeclaration> getDeclarations(); // O(N) in number of declarations +} + +typedef StyleDeclaration Dictionary<ParsedValue>; + + +Rule Matching +------------- + +partial class StyleElement { + Array<Rule> getRules(); // O(N) in rules +} + +class Rule { + constructor (); + attribute SelectorQuery selector; // O(1) + attribute StyleDeclaration styles; // O(1) +} + +Each frame, at some defined point relative to requestAnimationFrame(): + - If a rule starts applying to an element, sky:core calls thatElement.style.add(rule.styles); + - If a rule stops applying to an element, sky:core calls thatElement.style.remove(rule.styles); + + +Cascade +------- + +For each Element, the StyleDeclarationList is conceptually flattened +so that only the last declaration mentioning a property is left. + +Create the flattened render tree as a tree of StyleNode objects +(described below). For each one, run the equivalent of the following +code: + + var display = node.getProperty('display'); + if (display) { + node.layoutManager = new display(node, ownerManager); + return true; + } + return false; + +If that code returns false, then that node an all its descendants must +be dropped from the render tree. + +If any node is removed in this pass relative to the previous pass, and +it has an ownerLayoutManager, then call + ``node.ownerLayoutManager.release(node)`` +...to notify the layout manager that the node went away, then set the +node's layoutManager and ownerLayoutManager attributes to null. + + callback any ValueResolver (any value, String propertyName, StyleNode node, Float containerWidth, Float containerHeight); + + class StyleNode { + // this is generated before layout + readonly attribute String text; + readonly attribute Node? parentNode; + readonly attribute Node? firstChild; + readonly attribute Node? nextSibling; + + // access to the results of the cascade + any getProperty(String name); + // if there's a cached value, return it + // otherwise, if there's an applicable ParsedValue, then + // if it has a resolver: + // call it + // cache the value + // if relativeDimension is true, then mark the value as provisional + // return the value + // otherwise use the ParsedValue's value; cache it; return it + // otherwise, if the property is inherited and there's a parent: + // get it from the parent; cache it; return it + // otherwise, get the default value; cache it; return it + + readonly attribute Boolean needsLayout; // means that a property with needsLayout:true has changed on this node or one of its descendants + readonly attribute LayoutManager layoutManager; + + readonly attribute LayoutManager ownerLayoutManager; // defaults to the parentNode.layoutManager + // if you are not the ownerLayoutManager, then ignore this StyleNode in layout() and paintChildren() + // using walkChildren() does this for you + + readonly attribute Boolean needsPaint; // means that either needsLayout is true or a property with needsPaint:true has changed on this node or one of its descendants + + // only the ownerLayoutManager can change these + readonly attribute Float x; + readonly attribute Float y; + readonly attribute Float width; + readonly attribute Float height; + } + +The flattened tree is represented as a hierarchy of Node objects. For +any element that only contains text node children, the "text" property +is set accordingly. For elements with mixed text node and non-text +node children, each run of text nodes is represented as a separate +Node with the "text" property set accordingly and the styles set as if +the Node inherited everything inheritable from its parent. + + +Layout +------ + +sky:core registers 'display' as follows: + + { + name: 'display', + type: sky.DisplayStyleValueType, + inherits: false, + initialValue: sky.BlockLayoutManager, + needsLayout: true, + } + +The following API is then used to add new layout manager types to 'display': + + void registerLayoutManager(String displayValue, LayoutManagerConstructor? layoutManager); + +sky:core by default registers: + + 'block': sky.BlockLayoutManager + 'paragraph': sky.ParagraphLayoutManager + 'inline': sky.InlineLayoutManager + 'none': null + + +Layout managers inherit from the following API: + + class LayoutManager { + readonly attribute StyleNode node; + constructor LayoutManager(StyleNode node); + + void take(StyleNode victim); // sets victim.ownerLayoutManager = this; + // assert: victim hasn't been take()n yet during this layout + // assert: victim.needsLayout == true + // assert: an ancestor of victim has needsLayout == this (aka, victim is a descendant of this.node) + + virtual void release(StyleNode victim); + // called when the StyleNode was removed from the tree + + void setChildPosition(child, x, y); // sets child.x, child.y + void setChildX(child, y); // sets child.x + void setChildY(child, y); // sets child.y + void setChildSize(child, width, height); // sets child.width, child.height + void setChildWidth(child, width); // sets child.width + void setChildHeight(child, height); // sets child.height + // these set needsPaint on the node and on any node impacted by this (?) + // for setChildSize/Width/Height: if the new dimension is different than the last assumed dimensions, and + // any StyleNodes with an ownerLayoutManager==this have cached values for getProperty() that are marked + // as provisional, clear them + + Generator<StyleNode> walkChildren(); + // returns a generator that iterates over the children, skipping any whose ownerLayoutManager is not this + + void paint(RenderingSurface canvas); + // set a clip rect on the canvas + // call the painter of each property, in order they were registered, which on this element has a painter + // call this.paintChildren() + // unset the clip + + virtual void paintChildren(RenderingSurface canvas); + // just calls paint() for each child returned by walkChildren() whose needsPaint is true + + void assumeDimensions(Float width, Float height); + // sets the assumed dimensions for calls to getProperty() on StyleNodes that have this as an ownerLayoutManager + // if the new dimension is different than the last assumed dimensions, and any StyleNodes with an + // ownerLayoutManager==this have cached values for getProperty() that are marked as provisional, clear them + // TODO(ianh): should we force this to match the input to layout(), when called from inside layout() and when + // layout() has a forced width and/or height? + + virtual LayoutValueRange getIntrinsicWidth(Float? defaultWidth = null); + // returns min-width, width, and max-width, normalised, defaulting to values given in LayoutValueRange + // if argument is provided, it overrides width + + virtual LayoutValueRange getIntrinsicHeight(Float? defaultHeight = null); + // returns min-height, height, and max-height, normalised, defaulting to values given in LayoutValueRange + // if argument is provided, it overrides height + + virtual Dimensions layout(Number? width, Number? height); + // returns { } + // the return value should include the final value for whichever of the width and height arguments that is null + // TODO(ianh): should we just grab the width and height from assumeDimensions()? + + } + + dictionary LayoutValueRange { + // negative values here should be treated as zero + Float minimum = 0; + Float value = 0; // ideal desired width; if it's not in the range minimum .. maximum then it overrides minimum and maximum + (Float or Infinity) maximum = Infinity; + } + + dictionary Dimensions { + Float width = 0; + Float height = 0; + } + + +Given a tree of StyleNode objects rooted at /node/, the application is +rendered as follows: + + node.layoutManager.layout(screen.width, screen.height); + node.layoutManager.paint(); + + + +Paint +----- + + callback void Painter (StyleNode node, RenderingSurface canvas); + + class RenderingSurface { + // ... + } + + +Default Styles +-------------- + +In the constructors for the default elements, they add to themselves +StyleDeclaration objects as follows: + +* ``import`` +* ``template`` +* ``style`` +* ``script`` +* ``content`` +* ``title`` + These all add to themselves the same declaration with value: + ``{ display: { value: null } }`` + +* ``img`` + This adds to itself the declaration with value: + ``{ display: { value: sky.ImageElementLayoutManager } }`` + +* ``span`` +* ``a`` + These all add to themselves the same declaration with value: + ``{ display: { value: sky.InlineLayoutManager } }`` + +* ``iframe`` + This adds to itself the declaration with value: + ``{ display: { value: sky.IFrameElementLayoutManager } }`` + +* ``t`` + This adds to itself the declaration with value: + ``{ display: { value: sky.ParagraphLayoutManager } }`` + +* ``error`` + This adds to itself the declaration with value: + ``{ display: { value: sky.ErrorLayoutManager } }`` + +The ``div`` element doesn't have any default styles. + +These declarations are all shared between all the elements (so e.g. if +you reach in and change the declaration that was added to a ``title`` +element, you're going to change the styles of all the other +default-hidden elements).