| Sky Style Language |
| ================== |
| |
| Planed changes |
| -------------- |
| |
| Add //-to-end-of-line comments to be consistent with the script |
| language. |
| |
| |
| 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: |
| |
| ```css |
| tagname |
| #id |
| .class |
| [attrname] |
| [attrname=value] |
| :host ("host" string is fixed) |
| ::pseudo-element |
| ``` |
| |
| These can be combined (without whitespace), with at most one tagname |
| (must be first) and at most one pseudo-element (must be last) as in: |
| |
| ```css |
| tagname[attrname]#id:host.class.class[attrname=value]::foo |
| ``` |
| |
| 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 special value "@document", |
| optionally followed by a pseudo-element, as in: |
| |
| ```css |
| @document::bar |
| ``` |
| |
| |
| Value Parser |
| ------------ |
| |
| ```javascript |
| 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 |
| } |
| |
| // TODO(ianh): this is a non-starter, we need something better to handle units and custom painting |
| 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 |
| ------------- |
| |
| ```javascript |
| partial class Element { |
| readonly attribute StyleDeclarationList style; |
| } |
| |
| class StyleDeclarationList { |
| constructor (); |
| |
| // There are two batches of styles in a StyleDeclarationList. |
| |
| // The first batch is the per-frame styles. These get cleared each |
| // frame, after which all the matching rules in relevant <style> blocks |
| // get added back in, followed by all the animation-derived rules. |
| // Scripts can add styles themselves. |
| void addFrameStyles(StyleDeclaration styles, String? pseudoElement = null); // O(1) |
| void clearFrameStyles(); |
| |
| // The second batch is the persistent styles. |
| // Once added, they remain forever until removed. |
| void addPersistentStyles(StyleDeclaration styles, String? pseudoElement = null); // O(1) |
| void removePersistentStyles(StyleDeclaration styles, String? pseudoElement = null); // O(N) in number of declarations |
| |
| // This returns all the frame styles followed by all the persistent styles, in insertion order. |
| Array<StyleDeclaration> getDeclarations(String? pseudoElement = null); // O(N) in number of declarations |
| } |
| |
| class StyleDeclaration { |
| // TODO(ianh): define this |
| } |
| ``` |
| |
| Rule Matching |
| ------------- |
| |
| ```javascript |
| partial class StyleElement { |
| Array<Rule> getRules(); // O(N) in rules |
| } |
| |
| class Rule { |
| constructor (); |
| attribute SelectorQuery selector; // O(1) |
| attribute String? pseudoElement; // 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, rule.pseudoElement); |
| - If a rule stops applying to an element, sky:core calls thatElement.style.remove(rule.styles, rule.pseudoElement); |
| |
| TODO(ianh): fix the above so that rule order is maintained |
| |
| |
| 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: |
| |
| ```javascript |
| 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 |
| |
| ```javascript |
| node.ownerLayoutManager.release(node) |
| ``` |
| |
| ...to notify the layout manager that the node went away, then set the |
| node's ownerLayoutManager attribute to null. |
| |
| ```javascript |
| partial class Element { |
| readonly attribute StyleNode? layout; // TODO(ianh): come up with a better name (sadly "style" is taken) |
| // this will be null until the first time it is rendered |
| } |
| |
| callback any ValueResolver (any value, String propertyName, StyleNode node, Float containerWidth, Float containerHeight); |
| |
| class StyleNode { // implemented in C++ with no virtual tables |
| // 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 |
| // only works during layout and painting |
| any getProperty(String name, String? pseudoElement = null); |
| // throw if this isn't during layout or painting |
| // TODO(ianh): if the implementation of this does allow it to be queried the rest of the time too, relax this constraint |
| // looking at the declarations for the given pseudoElement: |
| // 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 a pseudo-element was specified, try again without one |
| // otherwise, if the property is inherited and there's a parent: |
| // get it from the parent (without pseudo); cache it; return it |
| // otherwise, get the default value; cache it; return it |
| |
| readonly attribute Boolean needsLayout; |
| // means that either needsLayout is true or a property with needsLayout:true has changed on this node |
| // needsLayout is set to false by the ownerLayoutManager's default layout() method |
| |
| readonly attribute Boolean descendantNeedsLayout; |
| // means that some child of this node has needsLayout set to true |
| // descendantNeedsLayout is set to false by the ownerLayoutManager's default layout() method |
| |
| 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 |
| |
| // only the ownerLayoutManager can change these |
| readonly attribute Float x; // relative to left edge of ownerLayoutManager |
| readonly attribute Float y; // relative to top edge of ownerLayoutManager |
| readonly attribute Float width; |
| readonly attribute Float height; |
| readonly attribute Boolean isNew; // node has just been added (and maybe you want to animate it in) |
| readonly attribute Boolean isGhost; // node has just been removed (and maybe you want to animate it away) |
| } |
| ``` |
| |
| 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: |
| |
| ```javascript |
| { |
| 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': |
| |
| ```javascript |
| 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: |
| |
| ```javascript |
| class LayoutManager : EventTarget { |
| readonly attribute StyleNode node; |
| constructor LayoutManager(StyleNode node); |
| |
| readonly attribute Boolean autoreap; |
| // defaults to true |
| // when true, any children that are isNew or isGhost are welcomed/reaped implicitly by default layout() |
| |
| virtual Array<EventTarget> getEventDispatchChain(); // O(N) in number of this.node's ancestors // implements EventTarget.getEventDispatchChain() |
| // let result = []; |
| // let node = this.node; |
| // while (node && node.layoutManager) { |
| // result.push(node.layoutManager); |
| // node = node.parentNode; |
| // } |
| // return result; |
| |
| 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 node.layoutManager == 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 |
| // 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 |
| void welcomeChild(child); // resets child.isNew |
| void reapChild(child); // resets child.isGhost |
| |
| Generator<StyleNode> walkChildren(); |
| // returns a generator that iterates over the children, skipping any whose ownerLayoutManager is not |this| |
| |
| Generator<StyleNode> walkChildrenBackwards(); |
| // returns a generator that iterates over the children backwards, skipping any whose ownerLayoutManager is not |this| |
| |
| 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); |
| /* |
| function getIntrinsicWidth(defaultWidth) { |
| if (defaultWidth == null) { |
| defaultWidth = this.node.getProperty('width'); |
| if (typeof defaultWidth != 'number') |
| defaultWidth = 0; |
| } |
| let minWidth = this.node.getProperty('min-width'); |
| if (typeof minWidth != 'number') |
| minWidth = 0; |
| let maxWidth = this.node.getProperty('max-width'); |
| if (typeof maxWidth != 'number') |
| maxWidth = Infinity; |
| if (maxWidth < minWidth) |
| maxWidth = minWidth; |
| if (defaultWidth > maxWidth) |
| defaultWidth = maxWidth; |
| if (defaultWidth < minWidth) |
| defaultWidth = minWidth; |
| return { |
| minimum: minWidth, |
| value: defaultWidth, |
| maximum: maxWidth, |
| }; |
| } |
| */ |
| |
| virtual LayoutValueRange getIntrinsicHeight(Float? defaultHeight = null); |
| /* |
| function getIntrinsicHeight(defaultHeight) { |
| if (defaultHeight == null) { |
| defaultHeight = this.node.getProperty('height'); |
| if (typeof defaultHeight != 'number') |
| defaultHeight = 0; |
| } |
| let minHeight = this.node.getProperty('min-height'); |
| if (typeof minHeight != 'number') |
| minHeight = 0; |
| let maxHeight = this.node.getProperty('max-height'); |
| if (typeof maxHeight != 'number') |
| maxHeight = Infinity; |
| if (maxHeight < minHeight) |
| maxHeight = minHeight; |
| if (defaultHeight > maxHeight) |
| defaultHeight = maxHeight; |
| if (defaultHeight < minHeight) |
| defaultHeight = minHeight; |
| return { |
| minimum: minHeight, |
| value: defaultHeight, |
| maximum: maxHeight, |
| }; |
| } |
| */ |
| |
| void markAsLaidOut(); // sets this.node.needsLayout and this.node.descendantNeedsLayout to false |
| virtual Dimensions layout(Number? width, Number? height); |
| // call markAsLaidOut(); |
| // if autoreap is true: use walkChildren() to call welcomeChild() and reapChild() on each child |
| // if width is null, set width to getIntrinsicWidth().value |
| // if height is null, set width height getIntrinsicHeight().value |
| // call this.assumeDimensions(width, height); |
| // call this.layoutChildren(width, height); |
| // return { width: width, height: height } |
| // - this should always call this.markAsLaidOut() to reset needsLayout |
| // - the return value should include the final value for whichever of the width and height arguments |
| // that is null |
| // - subclasses that want to make 'auto' values dependent on the children should override this |
| // entirely, rather than overriding layoutChildren |
| |
| virtual void layoutChildren(Number width, Number height); |
| // default implementation does nothing |
| // - override this if you want to lay out children but not have the children affect your dimensions |
| |
| virtual void paint(RenderingSurface canvas); |
| // set a clip rect on the canvas for rect(0,0,this.width,this.height) |
| // call the painter of each property, in order they were registered, which on this element has a painter |
| // call this.paintChildren(canvas) |
| // (the default implementation doesn't paint anything on top of the children) |
| // unset the clip |
| // - this gets called by the system if: |
| // - you are in your parent's current display list and it's in its parent's and so on up to the top, and |
| // - you haven't had paint() called since the last time you were dirtied |
| // - the following things make you dirty: |
| // - dimensions of your style node changed |
| // - one of your properties with needsLayout or needsPaint changed |
| |
| virtual void paintChildren(RenderingSurface canvas); |
| // for each child returned by walkChildren(): |
| // if child bounds intersects our bounds: |
| // call canvas.paintChild(child); |
| // - you should skip children that will be clipped out of yourself because they're outside your bounds |
| // - if you transform the canvas, you'll have to implement your own version of paintChildren() so |
| // that you don't skip the children that are visible in the new coordinate space but wouldn't be |
| // without the transform |
| |
| virtual StyleNode hitTest(Float x, Float y); |
| // default implementation uses the node's children nodes' x, y, |
| // width, and height, skipping any that have width=0 or height=0, or |
| // whose ownerLayoutManager is not |this| |
| // default implementation walks the tree backwards from its built-in order |
| // if no child is hit, then return this.node |
| // override this if you changed your children's z-order, or if you used take() to |
| // hoist some descendants up to be your responsibility, or if your children aren't |
| // rectangular (e.g. you lay them out in a hex grid) |
| // make sure to offset the value you pass your children: child.layoutManager.hitTest(x-child.x, y-child.y) |
| |
| } |
| |
| 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; |
| } |
| ``` |
| |
| |
| Paint |
| ----- |
| |
| ```javascript |
| callback void Painter (StyleNode node, RenderingSurface canvas); |
| |
| class RenderingSurface { |
| |
| // ... (API similar to <canvas>'s 2D API) |
| |
| void paintChild(StyleNode node); |
| // inserts a "paint this child" instruction in this canvas's display list. |
| // the child's display list, transformed by the child's x and y coordinates, will be inserted into this |
| // display list during painting. |
| } |
| ``` |
| |
| |
| 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: |
| ```javascript |
| { display: { value: null } } |
| ``` |
| |
| * ``img`` |
| This adds to itself the declaration with value: |
| ```javascript |
| { display: { value: sky.ImageElementLayoutManager } } |
| ``` |
| |
| * ``span`` |
| * ``a`` |
| These all add to themselves the same declaration with value: |
| ```javascript |
| { display: { value: sky.InlineLayoutManager } } |
| ``` |
| |
| * ``iframe`` |
| This adds to itself the declaration with value: |
| ```javascript |
| { display: { value: sky.IFrameElementLayoutManager } } |
| ``` |
| |
| * ``t`` |
| This adds to itself the declaration with value: |
| ```javascript |
| { display: { value: sky.ParagraphLayoutManager } } |
| ``` |
| |
| * ``error`` |
| This adds to itself the declaration with value: |
| ```javascript |
| { 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). |