Specs: Initial hack at extensible style/layout

Review URL: https://codereview.chromium.org/716013002
This commit is contained in:
Hixie 2014-11-13 14:00:46 -08:00
parent 586849aed1
commit 24b06d8998
6 changed files with 1103 additions and 41 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -151,17 +151,17 @@ module 'sky:core' {
// 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 @@ module 'sky:core' {
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 @@ module 'sky:core' {
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 @@ module 'sky:core' {
}
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 @@ The following types are available:
* ``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``)

View File

@ -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).