| Index: sky/specs/style.md
|
| diff --git a/sky/specs/style.md b/sky/specs/style.md
|
| index 21c84cd2e71f18ba5f5b90398fb0b058827ab3e6..93c9d4a7b3404478b9cc6c5d278a1377cb02835b 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).
|
|
|