Read this to learn about the Layers language and syntax.
"Reusable, Reactive Data"
Layers is a declarative language for describing things and the relationships between them. A resolution engine manages the data and associated relationships.
The following is an example of how a set of game data might be structured in Layers.
// Base layer; inherit to create types of objects
Object:
Model: ""
Position.(X,Y,Z): 0 // Position.X, Position.Y, Position.Z
// Armor is a type of object, adding its own attributes
Armor << Object:
(Protection,Resistance): 0
// A specific piece of armor, overriding some inherited attributes.
Leather Hood << Armor:
Model: "armor-head-leatherhood"
Protection: 5
Resistance: 10
// Character is also a type of object
Character << Object:
(,Max) (Health,Stamina): 100 // Health, Stamina, Max Health, Max Stamina
// Humanoid is a type of character
Humanoid << Character:
Armor.(Head,Body,Arms,Legs) << Armor // The game would treat inheriting
// default `Armor` like empty slots.
// Player is a type of humanoid; it doesn't add anything of its own
Player << Humanoid
// Save/Hero is a specific player; attributes are overridden here to represent
// the current state
Save:
Hero << Player:
Model: "player-hero"
Max Health: 200
Armor.Head << Leather Hood
A comment is text intended for readers meant to annotate or convey information about the data and its usage.
To create a comment, put your comment text after two forward-slashes (//), either on its own line or at the end of a line. Comments cannot be created within a string.
// This whole line is a comment
String: "// This is not a comment" // This is a comment at the end of a line
A word is bare text with no quotations, constructed with alphanumeric ASCII characters (hyphen - and underscore _ are also valid). While words can contain numeric digits, they cannot begin with a number.
Open // 1 word
Open Button // 2 words
Open-Button // 1 word
3D-Model // `3` is a number, followed by the word `D-Model`
A number is at least one digit (0-9), optionally followed by more digits. Numbers can be preceded with a - negative sign, and trailed with . and more digits to compose decimal numbers.
3 // Single digit number
250 // Multiple digit number
0.25 // Decimal number
-5.5 // Negative number
A string is Unicode text within a pair of quotes (").
Strings support escaping to convert special characters into their literal form where a parser can treat it as normal text. The following escape characters are supported:
\n - Line end\t - Tab\" - " that doesn't terminate the string\\ - \ that isn't recognized as escape"This is a string" // 1 string
This is not a string // 5 words
"First\nSecond" // String with two lines
"\tIndented" // Produces ` Indented`
"\"Quoted\"" // Produces `"Quoted"`
"C:\\Windows\\System32" // Produces `C:\Windows\System32`
A color is composed of # followed by either 3, 4, 6, or 8 hexadecimal digits representing either RGB, ARGB, RRGGBB, or AARRGGBB. All forms are accepted and internally converted into the #AARRGGBB canonical form. If no alpha is supplied, its hex value is ff (opaque) by default.
#abc // Produces `#ffaabbcc`
#8f00 // Produces `#88ff0000`
#ed2b3f // Produces `#ffed2b3f`
#1adfe314 // Produces `#1adfe314`
#edxb3f // Invalid; `x` is not a hex digit
A bool is either true or false, lowercase-only.
true // bool
false // bool
True // word
FALSE // word
Both LF and CRLF line endings are accepted, emitted as NEWLINE. If no line end is present at the end of the file, the lexer emits NEWLINE for the parser.
The lexer emits ENDFILE at the end of the input.
Indentation is tracked through changes to leading spaces on each new line. The lexer emits INDENT when there's an increase in leading spaces, while DEDENT is emitted when a matching decrease occurs (same amount of spaces as the corresponding increase). Tabs are invalid in indentation.
First1
Second1 // +4; INDENT
Second2
Third1 // +4; INDENT
Second3 // -4; DEDENT
Invalid // -2; Invalid
A name is a sequence of words, separated by spaces.
Pane // Name; 1 word
Left Pane // Name; 2 words
Names can also contain expansions which the engine splits into multiple names. An expansion is a set of parenthesized words separated by commas (,). Expanded words combine with the other words in the name's sequence. Multiple expansions in a name combine as a cross-product.
When there are no words as part of an expansion (represented as a leading or trailing comma), that's an empty alternative that produces nothing. Space between an empty alternative and other words in the name is not significant.
Words in an expansion need to be unique; repeating words is invalid and leads to duplicate names.
(First,Last) // `First` and `Last`
(First,Last) Name // `First Name` and `Last Name`
(Top,Bottom) (Left,Right) // `Top Left`, `Top Right`,
// `Bottom Left`, and `Bottom Right`
(,Previous) Location // `Location` and `Previous Location`
(Max,Max) // Invalid; expands to duplicate names
A path is a special structure of names that fit into either a traversal, a group, or an item. All paths have at least a group or an item.
Employees/Elliot Alderson/Assignments.Upload Spec Document
⌊ traversal ⌋⌊ group ⌋⌊ item ⌋
A traversal is the optional set of forward-slash (/) separated path names at the start of the path. Only names that precede slashes are part of the traversal; names that follow the last slash are not traversal names.
Position // No traversal; path contains no slashes
Game/Player/Position // Traversal; { `Game`, `Player` }
A group is an optional path name that follows a traversal (if one is present), and precedes the item (also if present).
Left // No group; path contains no dot
Padding.Left // Group; `Padding` +
// Item; `Left`
Menu/Icon/Padding.Left // Traversal; { `Menu`, `Icon` } +
// Group; `Padding` +
// Item; `Left`
A path that contains a group without an item after is valid for group sets.
Slider/Handle/Margins. // Valid; `Margins` represents a group set
The last name in a path, unless the name happens to be a group representing a group set.
Button/Corner Radius // Traversal + Item
Button/Corner Radii. // Traversal + Group
A declaration is a path that identifies one or more items.
Expansions can appear anywhere in a declaration (traversal, group, or item names).
// Assuming the following are used where declarations are allowed
X // Declaration (Item)
Position.X // Declaration (Group + Item)
Enemy/Position.X // Declaration (Traversal + Group + Item)
Dialog/Margins.(Left,Top,Right,Bottom) // Valid; item can expand
Dialog/(Margins,Padding).Top // Valid; group can expand
(Window,Dialog)/Margins.Top // Valid; traversal can expand
On the other hand, a reference is an expressible path that identifies one specific item. Expansions are not allowed anywhere in a reference.
// Assuming the following are used where references are allowed
Gradient/Start.Color // Valid
Gradient/(Start,End).Color // Invalid; which gradient color?
A value is an expressible unit of data, either a boolean, number, string of text, or color.
An expression is either a value or a reference.
100 // Expression; number value
"USD" // Expression; string value
Balance // Expression; reference
An attribute is a declared expression. The declaration and expression are separated by a colon (:).
Status: "Connected" // Attribute; string value
Timeout: 3600 // Attribute; number value
Stay Online: true // Attribute; boolean value
Accent: #abbc34 // Attribute; color value
Button/Fill: Accent // Attribute; references another attribute
Conditions are one or more single words that, together, represent a state. A set of conditions is preceded by @ and separated by &.
Since conditions are single words, they cannot contain spaces. A multi-word condition can still be achieved using hyphenation.
@Selected // One condition; { `Selected` }
@Mobile&Narrow // Two conditions; { `Mobile`, `Narrow` }
@Desktop-Large // One condition; { `Desktop-Large` }
@Desktop Large // Invalid; conditions must be single word
A state is a set of conditions paired with an expression, separated by a colon (:).
// State could read as...
@Selected: 2 // "2 when selected"
@Narrow&Mobile: "Column" // "Column when narrow on mobile"
@Debug&Desktop: false // "False when debugging on desktop"
@Alert: Red // reference // "Red when alerting"
An attribute can optionally include states after its expression, either inline or within an indented block.
Thickness: 0 @Selected: 2 // Attribute with inline state
Text Color: #111 // Attribute with indented states
@Alert: Red
@Success: Green
Layers are declared things, described with attributes and other layers. A layer is, at least, a declaration.
None // Declaration with nothing following;
// `None` is just an empty layer
A layer can optionally have a definition: a colon (:) after the declaration and a set of statements (attributes, group sets, state sets, and other layers) within an indented block.
Dark Theme: // Declaration with definition;
Primary: #36393f // this layer describes a theme whose attributes
Secondary: #2f3136 // are intended to be referenced.
Tertiary: #25272b
Foreground: #e3e3e3
Inheritance produces a clone of a referenced base layer. A layer inherits a base by placing << after the declaration, followed by the base layer reference.
Layers that inherit can also optionally have a definition where attributes can override base layer attributes if they're declared with the same name.
Consider the following layer describing a box:
Box:
Fill: Theme/Primary
Size.(Width,Height): 0
Corner Radius: 0
(Padding,Margins).(Left,Top,Right,Bottom): 0
Layout.Direction: Horizontal
Layout.Spacing: 5
Declaring another box is as simple as inheriting and overriding:
Button << Box:
Fill: Theme/Tertiary // Override `Box/Fill`
Corner Radius: 5 // Override `Box/Corner Radius`
Icon:
Source: "" // Base doesn't set `Icon/Source`
Size.(Width,Height): 30
Label:
Text: "Button"
Color: Theme/Foreground
Font.Family: "Roboto"
Font.Size: 12
And you can continue to declare more specific buttons:
Confirm Button << Button:
Icon/Source: "check.svg"
Label/Text: "Confirm"
Browse Button << Button:
Icon/Source: "folder.svg"
Label/Text: "Browse"
A group set is a declaration (without an item) followed by a dot (.) and an indented block of related attribute declarations. When using a group set, the group name only needs to be typed once.
Consider these group attributes that repeat the Border group name in their declarations:
Border.Thickness: 8
Border.Fill: Theme/Gradient
Instead, a group set can represent the same attributes:
Border.
Thickness: 8 // Represents `Border.Thickness`
Fill: Theme/Gradient // Represents `Border.Fill`
Whether to use a group set or not is a personal preference.
A state set is a set of conditions (@-prefixed and &-separated words) followed by a colon (:) and indented block containing attributes and/or group sets.
The following attributes all have values for a Narrow condition:
Layout.
Direction: "Column" @Narrow: "Row"
Align: "Stretch" @Narrow: "Center"
Justify: "Start" @Narrow: "Center"
Size.Width: 200 @Narrow: 0
Corner Radii.
Top Left: 0 @Narrow: 20
Top Right: 5 @Narrow: 20
Bottom Left: 0
Bottom Right: 5 @Narrow: 0
When many attributes have states like this, they can be harder to read. Instead, a state set can isolate the state values to make them easier to read:
@Narrow:
Layout.
Direction: "Row"
(Align,Justify): "Center"
Size.Width: 0
Corner Radii.
Top (Left,Right): 20
Bottom Right: 0
Whether to use a state set or not is a personal preference.
Layers input is stored as plain-text .lyr files. The root (top-level) of a .lyr file can contain one or more statements (layers, attributes, group sets, and state sets). Files in a set load in path order, and when two files declare the same thing, the later path wins.
The syntax lets you declare various relationships, but the AST produced by the parser only declares the existence of those relationships; it doesn't make them true.
For example, Armor from the overview section inherits Object:
Object:
Model: ""
Position.(X,Y,Z): 0
Armor << Object:
(Protection,Resistance): 0
Since inheritance is supposed to produce a clone of the base layer, Armor should also have the Model and Position attributes.
This is not handled by the parser. Instead, Armor in the AST denotes Object as its base, meaning the relationship still needs to be resolved after parsing. A resolution engine converts the parsed AST into a live data model that maintains the relationships declared in the syntax.
Loading is the process of converting syntax into live layers and attributes. The following are the rules applied while loading a set of .lyr files.
When loading a set of files, file-level items can optionally be loaded into a namespace layer instead of directly under the root. Namespaces help reduce name clashing between loaded sets.
External files must use the namespace as part of the path when referencing earlier items loaded under one.
// From `button.lyr`, loads under `Vortex` namespace
Button << Box:
// ...
// ... later in a different set ...
My Button << Vortex/Button:
// ...
Expansion is handled during loading, where names containing expansions anywhere in a declaration (traversal segments, group names, and item names) expand to produce multiple targets (one per combination).
(Min,Max) Width: 0 // `Min Width`, `Max Width`
Padding.(Left,Right): 8 // `Padding.Left`, `Padding.Right`
(Open,Save) Dialog/Title: "" // `Open Dialog/Title`, `Save Dialog/Title`
(Open,Save) Dialog/Padding.(Left,Right): 8
// `Open Dialog/Padding.Left`, `Open Dialog/Padding.Right`,
// `Save Dialog/Padding.Left`, `Save Dialog/Padding.Right`
Traversal layers that don't exist are created.
// Assume no `Color` has been declared yet
Color/Transparent: #0000 // Creates `Color` first, then `Transparent` within it
Statements load in the order they're written. A layer's children keep the order they're first declared in, even when a child is first named by a path or within a state set.
Window:
Titlebar:
Height: 30
Sidebar:
Width: 200
@Narrow:
Sidebar/Width: 0
Status Bar/Height: 24 // Creates `Status Bar` after `Sidebar`
// `Window` children order: `Titlebar`, `Sidebar`, `Status Bar`
Declaring something that already exists updates it instead of creating a new copy, so the later declaration wins. Existing states stay intact.
Limit: 50
@Debug: 20
// ... later ...
Limit: 30 // Update `Limit` to `30`; `Debug` state remains
Values in a state set merge into their associated attributes. If an inline state and a state set target the same conditions, the inline one wins.
Banner:
Height: 60 @Compact: 40 // Inline state
Font.Size: 16
@Compact:
Height: 48 // Loses to the inline state
Font.Size: 12 // Merges into `Font.Size` as its `Compact` state
Within a file, the inline state wins regardless of declaration order, including when the state set is declared first or in a parent layer's block.
Both @Debug&Advance and @Advance&Debug condition the same state, so the order that they're written is irrelevant.
When a layer inherits another, a few merging rules apply to produce the resulting derived layer.
When a derived layer redeclares attributes that the base declared, its values win. Base-only attributes and child layers are copied.
Player:
(Health,Stamina): 100
Inventory/Gold: 0
Hero << Player:
Health: 200 // `Hero/Health` overrides `Player/Health`;
// `Hero/Stamina` and `Hero/Inventory` are copied from Player
If a base attribute declares states that an overriding derived attribute doesn't redeclare, it gets copied to the derived attribute.
Button:
Fill: #25272b
@Hover: #2f3136
@Pressed: #36393f
Danger Button << Button:
Fill: #a12d2f // Overrides the main value
@Hover: #b53739 // Overrides `Hover` state
// `Pressed` is copied from `Button/Fill`
Child order follows the order that they're declared from the base, with new children declared in the derived layer following them.
Dialog:
Titlebar
Content
Buttons
Settings Dialog << Dialog:
Sidebar // New child; goes after the base's children
Content: // Override; stays in the base's position
Padding: 16
// `Settings Dialog` children order: `Titlebar`, `Content`, `Buttons`, `Sidebar`
A child overridden in a derived layer that doesn't set its own base with << takes the inherited child as its base.
Game:
Hero << Player:
Health: 200
Harder Game << Game:
Hero: // `Harder Game/Hero` uses `Game/Hero` as its base
Health: 50
A derived layer can reach at-depth values by identifying inherited children down the hierarchy in a declaration path.
Button << Box:
Fill: Theme/Tertiary
// ...
Label:
Text: "Button"
// ...
Confirm Button << Button:
Label/Text: "Confirm" // Override `Text` from inherited `Button/Label`
Inheritance is handled one-time: bases are resolved and finalized once after each set of files is loaded, and never again. Changing a base at runtime has no impact since derived layers don't pick up on the change. On the other hand, copied attributes keep their links live. From the example above, Confirm Button/Fill stays linked to Theme/Tertiary.
A base must be in the same set of files or in a set loaded earlier. If a base can't be found when its set is resolved, nothing is merged.
When searching for a base reference (full path or single name), the derived layer's namespace is checked first, then it falls back to root.
// Assume the following is loaded under the same namespace
Settings Menu: // Creates `Settings Menu` in namespace root
// ...
// ... later in the same set ...
Main Window:
Settings Menu << Settings Menu // Lookup finds `Settings Menu` from namespace
An attribute that references another (the target) becomes linked. When an attribute is linked, that link is a live maintained relationship. The following are the rules applied to keep the relationship true.
Theme:
Tertiary: #25272b
// ...
Button << Box:
Fill: Theme/Tertiary // `Button/Fill` is linked to `Theme/Tertiary`
// ...
When reading attribute values, the read occurs at runtime, going through the link to obtain its value. The target attribute's value can be updated, and the attribute depending on it would see the new value when being reread. Attributes can also be relinked at runtime, getting their values from different attributes than the ones referenced by their declarations.
Attributes that have been linked to can, themselves, link to other attributes.
Theme/Tertiary: Dark Theme/Tertiary // Link to `Dark Theme/Tertiary`
// ... then `Button` unchanged from above ...
Button << Box:
Fill: Theme/Tertiary // `Button/Fill` is linked to `Theme/Tertiary`,
// ... // which, itself, is linked to `Dark Theme/Tertiary`
In the above, Button/Fill is indirectly linked to Dark Theme/Tertiary. This indirection pattern can be useful, since Theme's attributes can be linked to throughout, and later, relinked to a set of attributes from a different theme (such as Light Theme), and all of Theme's dependents would see the new values.
If a specific value is assigned to a linked attribute at runtime, instead of relinking it, then the value gets set and the existing link is broken.
For attribute references, single names are handled differently. When searching for a single-name reference, the searching attribute's own layer is first checked (its siblings), then each parent going up the hierarchy.
Attribute reference paths follow the same "namespace first, root fallback" lookup as base references.
A state is selected at runtime by passing a set of active conditions when reading an attribute's value. The winning state is the most specific one whose conditions are all considered active. When no states match, the attribute's main expression (value or link) is read.
A tie between two qualifying states is unspecified. Consider declaring a combined state when both are active.
A qualifying state's value wins over reading a link's value.
When an attribute has no state that qualifies the active conditions, the conditions carry to its link target, which might have a qualifying state, or pass them further down the chain.
Theme:
Tertiary: #1b3450
@Hover: #3a70b8
Button:
Fill: Theme/Tertiary // With `Hover` active, `Button/Fill` has no `Hover`
// state, so `Theme/Tertiary`'s `Hover` state is read
When a selected state is itself a link, its target is read without the active conditions.
Theme:
Muted: #545c64
@Hover: #5e6670 // Never read by `Button`
Button:
Fill: Theme/Tertiary
@Disabled: Theme/Muted // With `Disabled` and `Hover` active, the
// `Disabled` state wins and `Theme/Muted` is
// read without conditions, returning `#545c64`
The following are the rules when errors occur or input doesn't resolve correctly.
A file with lexer or parser errors is skipped entirely.
A derived layer whose base fails to resolve (or whose base would form a cycle) gets nothing merged into it.
An attribute with an unresolved link falls back to its own value, if it has one. Attributes declared only with references have no values of their own, so if their links don't resolve, they're just empty (the C++ runtime returns the requested type's default value for empty attributes). Links whose chain would form a cycle (including an attribute linking to itself, like Fill: Fill) are rejected.
Some specific rules apply to values in the engine as opposed to how they're declared.
Colors can be declared as non-string #RGB, #ARGB, #RRGGBB, or #AARRGGBB values, but they are converted to #AARRGGBB strings when loaded in the engine.
The engine doesn't distinguish between integers and decimals. All numerical values are considered a type of number.
Since Layers is schema-free, the meaning behind values loaded and maintained by the engine is entirely up to the host program using them.