Sky Style Language

THIS IS NOT UP TO DATE

DO NOT IMPLEMENT THIS YET

It has not been Dartified.

It has not been converted to have a sane element.style API.

It has not been converted to have layout be interruptible using Futures.

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:

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:

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 “@root”, optionally followed by a pseudo-element, as in:

@root::bar

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
}

callback ParserCallback = AbstractStyleValue (TokenSource tokens); // return if successful, throw if not

class StyleGrammar {
  constructor ();
  void addParser(ParserCallback parser);
  AbstractStyleValue 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)
}

/*
StyleNode
 |
 +-- Property
 |
 +-- AbstractStyleValue
     |   
     +-- NumericStyleValue
     |    |
     |    +-- AnimatableNumericStyleValue*
     |   
     +-- LengthStyleValue
     |    |
     |    +-- AnimatableLengthStyleValue*
     |    |
     |    +-- TransitionLengthStyleValue*
     |    |
     |    +-- PixelLengthStyleValue
     |    |
     |    +-- EmLengthStyleValue*
     |    |
     |    +-- VHLengthStyleValue*
     |    |
     |    +-- CalcLengthStyleValue*
     |    
     +-- ColorStyleValue
     |    |
     |    +-- RGBColorStyleValue
     |    |
     |    +-- AnimatableColorStyleValue*
     |    
     +-- AbstractOpaqueStyleValue
     |    |
     |    +-- IdentifierStyleValue
     |    |    |
     |    |    +-- AnimatableIdentifierStyleValue*
     |    |    
     |    +-- URLStyleValue*
     |    |    |
     |    |    +-- AnimatableURLStyleValue*
     |    |    
     |    +-- StringStyleValue*
     |    |    |
     |    |    +-- AnimatableStringStyleValue*
     |    |    
     |    +-- ObjectStyleValue
     |    
     +-- PrimitiveValuesListStyleValue*
*/

The types marked with * in the list above are not part of dart:sky, and are only shown here to illustrate what kinds of extensions are possible and where they would fit.

TODO(ianh): consider removing ‘StyleValue’ from these class names

abstract class StyleNode {
  abstract void markDirty();
}

dictionary StyleValueResolverSettingsSettings {
  Boolean firstTime = false;
  any state = null;
}

class StyleValueResolverSettings {
  // this is used as an "out" parameter for 'resolve()' below
  constructor(StyleValueResolverSettingsSettings initial);
  void reset(StyleValueResolverSettingsSettings initial);
    // sets firstTime and state to given values
    // sets layoutDependent to false
    // sets dependencies to empty set
    // sets lifetime to Infinity

  readonly attribute Boolean firstTime;
    // true if this is the first time this property is being resolved for this element,
    // or if the last time it was resolved, the value was a different object

  // attribute Boolean layoutDependent
  void setLayoutDependent();
    // call this if the value should be recomputed each time the ownerLayoutManager's dimensions change, rather than being cached
  Boolean getLayoutDependent();
    // returns true if setLayoutDependent has been called since the last reset()

  // attribute "BitField" dependencies; // defaults to no bits set
  void dependsOn(PropertyHandle property);
    // if the given property doesn't have a dependency bit assigned:
    //  - assign the next bit to the property
    //  - if there's no bits left, throw
    // set the bit on this StyleValueResolverSettings's dependencies bitfield
  Array<PropertyHandle> getDependencies();
    // returns an array of the PropertyHandle values for the bits that are set in dependencies

  // attribute (Float or Infinity) lifetime;
  void setLifetime(Float age);
    // if the new value is less than the current value of lifetime, update the current value
  (Float or Infinity) getLifetime();
    // return current value of lfietime

  attribute any state; // initially null, can be set to store value for this RenderNode/property pair
    // for example, TransitioningColorStyleValue would store
    //    {
    //      initial: /* color at time of transition */,
    //      target: /* color at end of transition */,
    //      start: /* time at start of transition */,
    //    }
    // ...which would enable it to update appropriately, and would also
    // let other transitions that come later know that you were half-way
    // through a transition so they can shorten their time accordingly
    //
    // best practices: if you're storing values on the state object,
    // then remove the values once they are no longer needed. For
    // example, when your transition ends, set the object to null.
    //
    // best practices: if you're a style value that contains multiple
    // style values, then before you call their resolve you should
    // replace the state with a state that is specific to them, and
    // when you get it back you should insert that value into your
    // state somehow. For example, in a resolve()r with two child
    // style values a and b:
    //    let ourState;
    //    if (settings.firstTime)
    //      ourState = { a: null, b: null };
    //    else
    //      ourState = settings.state;
    //    settings.state = ourState.a;
    //    let aResult = a.resolve(node, settings);
    //    ourState.a = settings.state;
    //    settings.state = ourState.b;
    //    let aResult = b.resolve(node, settings);
    //    ourState.b = settings.state;
    //    settings.state = ourState;
    //    return a + b; // or whatever
    //
    // best practices: if you're a style value that contains multiple
    // style values, and all those style values are storing null, then
    // store null yourself, instead of storing many nulls of your own.

  // attribute Boolean wasStateSet;
  Boolean getShouldSaveState();
    // returns true if state is not null, and either state was set
    // since the last reset, or firstTime is false.

}

class Property : StyleNode {
  constructor (AbstractStyleDeclaration parentNode, PropertyHandle property, AbstractStyleValue? initialValue = null);
  readonly attribute AbstractStyleDeclaration parentNode;
  readonly attribute PropertyHandle property;
  readonly attribute AbstractStyleValue value;

  void setValue(AbstractStyleValue? newValue);
    // updates value and calls markDirty()

  void markDirty();
    // call parentNode.markDirty(property);

  abstract any resolve(RenderNode node, StyleValueResolverSettings? settings = null);
    // if value is null, returns null
    // otherwise, returns value.resolve(property, node, settings)
}

abstract class AbstractStyleValue : StyleNode {
  abstract constructor(StyleNode? parentNode = null);
  attribute StyleNode? parentNode;

  void markDirty();
    // call this.parentNode.markDirty()

  abstract any resolve(PropertyHandle property, RenderNode node, StyleValueResolverSettings? settings = null);
}

abstract class LengthStyleValue : AbstractStyleValue {
  abstract Float resolve(PropertyHandle property, RenderNode node, StyleValueResolverSettings? settings = null);
}

class PixelLengthStyleValue : LengthStyleValue {
  constructor(Float number, StyleNode? parentNode = null);
  attribute Float value;
    // on setting, calls markDirty();
  Float resolve(PropertyHandle property, RenderNode node, StyleValueResolverSettings? settings = null);
    // return value
}

typedef RawColor Float; // TODO(ianh): figure out what Color should be
class ColorStyleValue : LengthStyleValue {
  constructor(Float red, Float green, Float blue, Float alpha, StyleNode? parentNode = null);
  // ... color API ...
  RawColor resolve(PropertyHandle property, RenderNode node, StyleValueResolverSettings? settings = null);
}

class AbstractOpaqueStyleValue : AbstractStyleValue {
  abstract constructor(any value, StyleNode? parentNode = null);
  attribute any value;
    // on setting, calls markDirty();
  any resolve(PropertyHandle property, RenderNode node, StyleValueResolverSettings? settings = null);
    // returns value
}

class IdentifierStyleValue : AbstractOpaqueStyleValue {
  constructor(String value, StyleNode? parentNode = null);
    // calls superclass constructor
}

/*
class AnimatableIdentifierStyleValue : AbstractOpaqueStyleValue {
  constructor(String value, String newValue, AnimationFunction player, StyleNode? parentNode = null);
  readonly attribute String newValue;
  readonly attribute AnimationFunction player;
  any resolve(PropertyHandle property, RenderNode node, StyleValueResolverSettings? settings = null);
}
*/

class ObjectStyleValue : AbstractOpaqueStyleValue {
  constructor(any value, StyleNode? parentNode = null);
    // calls superclass constructor
}

dictionary PropertySettings {
  String? name = null; // null if the property can't be set from a <style> block
  StyleGrammar? grammar = null; // must be non-null if name is non-null; must be null otherwise
  Boolean inherited = false;
  any initialValue = null;
  Boolean needsManager = false;
  Boolean needsLayout = false;
  Boolean needsPaint = false;
  // PropertyHandle propertyHandle; // assigned by registerProperty
  // Integer dependencyBit; // assigned by StyleValueResolverSettings.dependsOn()
}
typedef PropertyHandle Integer;
PropertyHandle registerProperty(PropertySettings propertySettings);
  // registers a property with the given settings, and returns an integer >= 0
  // that can be used to refer to this property

// dart:sky exports a bunch of style grammars so that people can extend them
attribute StyleGrammar PositiveLengthOrInfinityStyleGrammar; // resolves to LengthStyleValue
attribute StyleGrammar PositiveLengthOrAutoStyleGrammar; // resolves to LengthStyleValue or IdentifierStyleValue (with value 'auto')
attribute StyleGrammar PositiveLengthStyleGrammar; // resolves to LengthStyleValue
attribute StyleGrammar NumberGrammar; // resolves to NumericStyleValue
attribute StyleGrammar ColorGrammar; // resolves to ColorStyleValue
attribute StyleGrammar DisplayStyleGrammar; // resolves to ObjectStyleValue

Inline Styles

abstract class AbstractStyleDeclarationList {
  void addStyles(StyleDeclaration styles, String pseudoElement = ''); // O(1)
  void removeStyles(StyleDeclaration styles, String pseudoElement = ''); // O(N) in number of declarations
  Array<StyleDeclaration> getDeclarations(String pseudoElement = ''); // O(N) in number of declarations
}

class ElementStyleDeclarationList : AbstractStyleDeclarationList {
  constructor (Element? element);
  readonly attribute Element? element;

  // there are two batches of styles in an ElementStyleDeclarationList.

  // the first batch is the per-frame styles; these get (conceptually)
  // 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 also add styles themselves, but they are
  // dropped after the next frame
  void addFrameStyles(StyleDeclaration styles, String pseudoElement = ''); // O(1)
  void clearFrameStyles();

  // the second batch is the persistent styles, which remain until removed;
  // they are accessed via the AbstractStyleDeclarationList accessors

  // as StyleDeclarations are added and removed, the ElementStyleDeclarationList
  // calls register(element) and unregister(element) respectively on those
  // StyleDeclaration objects, where element is the element that was passed
  // to the constructor, if not null
  // then, it calls element.renderNode.cascadedValueChanged
  // for each property on the object

  // the inherited getDeclarations() method returns all the frame
  // styles followed by all the persistent styles, in insertion order
}

class RenderNodeStyleDeclarationList : AbstractStyleDeclarationList {
  constructor (RenderNode? renderNode);
  readonly attribute RenderNode? renderNode;

  // as StyleDeclarations are added and removed, the RenderNodeStyleDeclarationList
  // calls register(renderNode) and unregister(renderNode) respectively on those
  // StyleDeclaration objects, where renderNode is the RenderNode that was passed
  // to the constructor, if not null
  // then, it calls renderNode.cascadedValueChanged
  // for each property on the object
}

class StyleDeclaration {
  constructor ();

  void markDirty(PropertyHandle property);
    // this indicates that the cascaded value of the property thinks
    // it will now have a different result (as opposed to the cascaded
    // value itself having changed)
    // invoke element.renderNode.cascadedValueDirty(property, pseudoElement); for each
    // currently registered consumer element/pseudoElement pair

  void register((Element or RenderNode) consumer, String pseudoElement = ''); // O(1)
  void unregister((Element or RenderNode) consumer, String pseudoElement = ''); // O(N)
    // registers an element/pseudoElement or renderNode/pseudoElement pair with
    // this StyleDeclaration so that a property/value on the style declaration
    // is marked dirty, the relevant render node is informed and can then update
    // its property cache accordingly


  getter AbstractStyleValue? (PropertyHandle property);
    // looks up the Property object for /property/, and returns its value
    // null if property is missing

  setter void (PropertyHandle property, AbstractStyleValue value);
    // verify that value.parentNode is null
    // if there is no Property object for /property/, creates one
    // else calls its update() method to change the value
    // update value's parentNode
    // invoke consumer.renderNode.cascadedValueChanged(property); for each
    // currently registered consumer

  void remove(PropertyHandle property);
    // drops the Property object for /property/ from this StyleDeclaration object
    // invoke consumer.renderNode.cascadedValueChanged(property); for each
    // currently registered consumer
}

Rule Matching

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 has started applying, or a Rule stopped applying, to an element, dart:sky calls thatElement.style.clearFrameStyles() and then, for each Rule that now applies, calls thatElement.style.addFrameStyles() with the relevant StyleDeclaration and pseudoElement from each such Rule.

Update the render tree

Simultaneously walk the tree rooted at the application‘s element tree’s root node, taking into account shadow trees and child distribution, and the tree rooted at that Root node's RenderNode.

If you come across a node that doesn‘t have an assigned RenderNode, then create one, placing it in the appropriate place in the RenderTree tree, after any nodes marked isGhost=true, with ownerLayoutManager pointing to the parent RenderNode’s layoutManager, if it has one, and, if it has one and autoreap is false on that layout manager, mark the new node “isNew”. (This means that when a node is marked isNew, the layout manager has already laid out at least one frame.)

For each element, if the node‘s needsManager is true, call getLayoutManager() on the element, and if that’s not null, and if the returned class isn‘t the same class as the current layoutManager, if any, construct the given class and assign it to the RenderNode’s layoutManager, then set all the child RenderNodes' ownerLayoutManager to that object; if it returns null, and that node already has a layoutManager, then set isGhost=true for that node and all its children (without changing the layoutManager). Otherwise, if it returned null and there's already no layoutManager, remove the node from the tree. Then, in any case, clear the needsManager bit.

When an Element or Text node is to be removed from its parent, and it has a renderNode, and that renderNode has an ownerLayoutManager with autoreap=false, then before actually removing the node, the node‘s renderNode should be marked isGhost=true, and all the StyleDeclarations in the relevant ElementStyleDeclarationList should be added to the RenderNode’s overrideStyles for use later (creating a RenderNodeStyleDeclarationList if necessary).

When an Element is to be removed from its parent, regardless of the above, the node's renderNode attribute should be nulled out.

When a RenderNode is added with isNew=true, call its parent RenderNode‘s LayoutManager’s childAdded() callback. When a a RenderNode has its isGhost property set to true, then call it‘s parent RenderNode’s LayoutManager's childRemoved() callback.

dictionary PropertySettings {
  String? name = null; // null if the property can't be set from a <style> block
  StyleGrammar? grammar = null; // must be non-null if name is non-null; must be null otherwise
  Boolean inherited = false;
  any initialValue = null;
  Boolean needsManager = false;
  Boolean needsLayout = false;
  Boolean needsPaint = false;
  // PropertyHandle propertyHandle; // assigned by registerProperty
  // Integer dependencyBit; // assigned by StyleValueResolverSettings.dependsOn()
}

dictionary GetPropertySettings {
  String pseudoElement = '';
  Boolean forceCache = false;
    // if set to true, will return the cached value if any, or null otherwise
    // this is used by transitions to figure out what to transition from
}

class RenderNode { // 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;

  // internal state:
  // - back pointer to backing Node, if we're not a ghost
  // - cache of resolved property values, mapping as follows:
  //    - pseudoElement, property => StyleValue object, resolved value, StyleValueResolverSettings, cascade dirty bit, value dirty bit
  // - property state map (initially empty), as follows:
  //    - pseudoElement, property => object

  any getProperty(PropertyHandle property, GetPropertySettings? settings = null);
     // looking at the cached data for the given pseudoElement:
     // if there's a cached value:
     //   if settings.forceCache is true, return the cached value
     //   if neither dirty bit is set, return the cached value
     //   if the cascade dirty bit is not set (value dirty is set) then
     //    - clear any pending lifetime-enforcing tasks for this
     //      property/pseudoElement pair on this render node
     //    - resolve the value using the same StyleValue object
     //       - with firstTime=false on the resolver settings
     //       - with the cached state object if any
     //       - jump to "cache" below
     // if settings.forceCache is true, return null
     // - clear any pending lifetime-enforcing tasks for this
     //   property/pseudoElement pair on this render node
     // - if there's an override declaration with the property (with
     //   the pseudo or without), then get the value object from there
     //   and jump to "resolve" below.
     // - if there's an element and it has a style declaration with
     //   the property (with the pseudo or without), then get the
     //   value object from there and jump to "resolve" below.
     // - if it's not an inherited property, or if there's no parent,
     //   then get the default value and jump to "resolve" below.
     // - call the parent render node's getProperty() with the same
     //   property but no settings, then cache that value as the value
     //   for this element with the given pseudoElement, with no
     //   StyleValue object, no resolver settings, and set the state
     //   to null.
     // resolve:
     //   - get a new resolver settings object (or reset an existing one)
     //     - if the obtained StyleValue object is different than the
     //       cached StyleValue object, or if there is no cached
     //       object, then set the resolver settings to
     //       firstTime=true, otherwise it's the same object and set
     //       firstTime=false.
     //     - set the resolver settings' state to the current state
     //       for this pseudoElement/property combination
     //   - using the obtained StyleValue object, call resolve(),
     //     passing it this node and the resolver settings object.
     //   - jump to "cache" below
     // cache:
     //   - update the cache with the obtained value and resolver
     //     settings
     //   - reset the dirty bits
     //   - if the resolver settings' getShouldSaveState() method
     //     returns false, then discard any cached state, otherwise,
     //     cache the new state
     //   - if the resolver settings' lifetime is not infinity, then
     //     queue a lifetime-enforcing task for the appropriate time
     //     in the future which calls cascadedValueDirty for this
     //     property/pseudoElement pair on this render node

  attribute RenderNodeStyleDeclarationList overrideStyles;
     // mutable; initially null
     // this is used when isGhost is true, and can also be used more generally to
     // override styles from the layout manager (e.g. to animate a new node into view)  
     // this is the only arbitrarily mutable state of a RenderNode object

  private void cascadedValueChanged(PropertyHandle property, String pseudoElement = '');
  private void cascadedValueDirty(PropertyHandle property, String pseudoElement = '');
    // - set the appropriate dirty bit on the cached data for this property/pseudoElement pair
    //    - cascade dirty for cascadedValueChanged
    //    - value dirty for cascadedValueDirty
    // - if the property is needsManager, set needsManager to true
    // - if the property is needsLayout, set needsLayout to true and walk up the
    //   tree setting descendantNeedsLayout
    // - if the property is needsPaint, add the node to the list of nodes that need painting
    // - if the property has a dependencyBit defined, then check the cache of all the
    //   properties on this RenderNode, and the cache for the property in all the child
    //   nodes and, if pseudoElement is '', the pseudoElements of this node, and,
    //   if any of them have the relevant dependency bit set, then call
    //     thatRenderNode.cascadedValueDirty(thatProperty, thatPseudoElement)
    // - if the property is inherited, then for each child node, and, if pseudoElement
    //   is '', the pseudoElements of this node, if the cached value for this property
    //   is present but has no StyleValue, call thatNode.cascadedValueChanged(property, thatPseudoElement)

  readonly attribute Boolean needsManager;
    // means that a property with needsManager:true has changed on this node

  readonly attribute Boolean needsLayout;
    // means that either needsManager 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 RenderNode 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

dart:sky registers ‘display’ as follows:

  {
    name: 'display',
    grammar: sky.DisplayStyleGrammar,
    inherited: false,
    initialValue: sky.BlockLayoutManager,
    needsManager: true,
  }

The following API is then used to add new layout manager types to ‘display’:

void registerLayoutManager(String displayValue, LayoutManagerConstructor? layoutManager);

dart:sky by default registers:

  • ‘block’: sky.BlockLayoutManager
  • ‘paragraph’: sky.ParagraphLayoutManager
  • ‘inline’: sky.InlineLayoutManager
  • ‘none’: null

Layout managers inherit from the following API:

callback LayoutManagerConstructor LayoutManager (RenderNode node);

class LayoutManager : EventTarget {
  readonly attribute RenderNode node;
  constructor LayoutManager(RenderNode node);
    // sets needsManager to false on the node

  readonly attribute Boolean autoreap;
    // defaults to true
    // when true, children that are added don't get set to isNew=true
    // when true, children that are removed don't get set to isGhost=true, they're just removed

  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 setProperty(RenderNode node, PropertyHandle property, any value, String pseudoElement = ''); // O(1)
    // replaces the value that getProperty() would return on that node with /value/
    // this also clears the dependency bits, dirty bits, and sets the property state to null
    // this also clears any relevant lifetime-enforcing tasks

  void take(RenderNode victim); // sets victim.ownerLayoutManager = this;
    // assert: victim hasn't been take()n yet during this layout
    // assert: an ancestor of victim has node.layoutManager == this (aka, victim is a descendant of this.node)

  virtual void release(RenderNode victim);
    // called when the RenderNode was removed from the tree

  virtual void childAdded(RenderNode child);
  virtual void childRemoved(RenderNode child);
    // called when a child has its isNew or isGhost attributes set respectively

  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
    // assert: child.ownerLayoutManager == this
    // for setChildSize/Width/Height: if the new dimension is different than the last assumed dimensions, and
    // any RenderNodes with an ownerLayoutManager==this have cached values for getProperty() that are marked
    // as layout-dependent, mark them as dirty with cascadedValueDirty()

  void assumeDimensions(Float width, Float height);
    // sets the assumed dimensions for calls to getProperty() on RenderNodes that have this as an ownerLayoutManager,
    // by updating renderNode width/height;
    // if the new dimension is different than the last assumed dimensions, and any RenderNodes with an
    // ownerLayoutManager==this have cached values for getProperty() that are marked as layout-dependent, mark them
    // as dirty with cascadedValueDirty()

  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 welcomeChild(child);
    // assert: this == child.ownerLayoutManager
    // assert: child.isNew is true
    // resets child.isNew
  void reapChild(child);
    // assert: this == child.ownerLayoutManager
    // assert: child.isGhost is true
    // removes the RenderNode from its parent if isGhost is true

  Generator<RenderNode> walkChildren();
    // returns a generator that iterates over the children, skipping any whose ownerLayoutManager is not |this|

  Generator<RenderNode> walkChildrenBackwards();
    // returns a generator that iterates over the children backwards, skipping any whose ownerLayoutManager is not |this|

  void markAsLaidOut(); // sets this.node.needsLayout and this.node.descendantNeedsLayout to false
  virtual Dimensions layout(Float? width, Float? height);
    // if width is null, set width to getIntrinsicWidth().value
    // if height is null, set height to getIntrinsicHeight().value
    // call this.assumeDimensions(width, height);
    // call this.layoutChildren(width, height);
    // call markAsLaidOut();
    // return { width: width, height: height }
    // - this should always call this.markAsLaidOut() to reset
    //   needsLayout and descendantNeedsLayout
    // - 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; but see layoutChildren()'s notes
    //   for how to do this

  virtual void layoutChildren(Float width, Float height);
    // default implementation does nothing
    // - override only this (and not layout()) if you want to lay out
    //   children but not have the children affect your dimensions
    // - always call setChildSize() and setChildPosition() after
    //   calling a child's layout() method
    // - if the child has needsLayout or if you need to have it
    //   autosize, call its ownerLayoutManager's layout() method
    // - otherwise if the child has needs descendantNeedsLayout, call
    //   layoutDescendants()

  virtual Dimensions layoutDescendants();
    // assert: node.needsLayout is false, node.descendantNeedsLayout is true
    // walk children:
    //  - if it has needsLayout, call its layout() method with its
    //    current width and height, then call setChildSize() with
    //    those same dimensions
    //  - else, if it has descendantNeedsLayout, call its
    //    layoutDescendants() method
    // call markAsLaidOut();
    // - override this if you use take() to control more children, in
    //   which case you should call their methods too
    // - this should always call this.markAsLaidOut() to reset
    //   needsLayout and descendantNeedsLayout

  virtual void paint(RenderingSurface canvas);
    // set a clip rect on the canvas for rect(0,0,this.width,this.height)
    //   (? we don't really have to do this; consider shadows...)
    // 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 RenderNode 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 RenderNode 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

Sky has a list of RenderNodes that need painting. When a RenderNode is created, it's added to this list. (See also needsPaint for another time it is added to the list.)

callback void Painter (RenderNode node, RenderingSurface canvas);

class RenderingSurface {

  // ... (API similar to <canvas>'s 2D API)

  void paintChild(RenderNode 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.
}

The default framework provides global hooks for extending the painting of:

  • borders
  • backgrounds

These are called during the default framework‘s layout managers’ paint() functions. They are also made available so that other people can call them from their paint() functions.

Default Styles

In the constructors for the default elements, they add to themselves StyleDeclaration objects as follows:

  • span
  • a These all add to themselves the same declaration as follows:
let d = new StyleDeclaration();
d[pDisplay] = new ObjectStyleValue(InlineLayoutManager);
this.style.addStyles(d);
  • t This adds to itself the declaration as follows:
let d = new StyleDeclaration();
d[pDisplay] = new ObjectStyleValue(ParagraphLayoutManager);
this.style.addStyles(d);

The other elements don'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 span element, you're going to change the styles of all the other default-hidden elements). (In other words, in the code snippets above, the d variable is initialised in shared code, and only the addStyles() call is per-element.)