A layout is a description of 2-D content: boxes placed inside boxes, words, pictures, drawn shapes and controls a visitor can press. Handed to the layout(...) source (Images), it becomes an image at the size it ends up: over the picture as a HUD, on a sign or a screen in the world, on a wrist, in a speech bubble above someone's head. This page is the 2-D vocabulary: what a world can say about boxes, text, images, shapes, controls and styles, and how a layout is placed over a point in the 3-D world.
The vocabulary is drawn from what the browser's packages already do: uilayout (boxes, layout, controls, themes, the pointer), msdftext (letters sharp at any size, outline and shadow), richtext (paragraphs with styled runs, lists, pictures in the text), vector2d (paths, fills, strokes, gradients) and uipickers (number, color and choice controls). Where those packages lack something, this page says so. The names of the vocabulary are provisional: what is fixed is what each element means.
Terms used on this page. A node is one box of a layout: a panel, a line of text, a button. A container is a node that places its children by a rule (in a row, in a grid). Padding is space inside a box's edge, margin space outside it. A style is a named set of looks (colors, border, rounding) that many nodes share. A glyph is the drawn shape of one letter.
Units and coordinates
- Across and down. A layout's space runs x to the right and y down, with the origin at the top left, as on a page.
- On the screen, lengths are in logical pixels, written
px: apxis drawn as as many real pixels as the display's scale says, 1.5 at 150 %. So a panel 172 px wide is the same size to the eye on any display. - On a surface in the world, a layout is given a width in meters and its
pxare scaled to fit: a panel of so many pixels across is laid out over a rectangle so many meters wide. Text sizes may also be given in meters (size: 6 cm). - Fractions place a box relative to its parent:
(0, 0)is the parent's top left,(1, 1)its bottom right,(0.5, 1)the middle of its bottom edge. - Colors are written
rgba(r, g, b, a)with four numbers from 0 to 255 (Values defines the color type).
Boxes and layout
What it is
Every element of a layout is a box. A parent box hands each child a smallest and largest width and height it may take; the child answers with the size it wants within them; the parent then places its children by its container's rule. This is the layout model of Flutter and WPF, with CSS flexbox's single-line rows and columns. A layout is laid out again only where something changed: a change marks its box and the boxes above it, and a box of fixed size stops the marking from spreading further.
Containers
| Container | Places its children |
|---|---|
column([...], ...), row([...], ...) | one after another, down or across, as CSS flexbox's single line: each child may grow into spare room, shrink when room is short, and start from a basis size; the line is justified (start, center, end, space between, around or evenly) and children aligned across it (start, center, end, stretch), with a gap between |
stack([...]) | on top of each other, each aligned or filling |
grid([...], columns: [...], rows: [...]) | in cells of tracks that are fixed, a share of what is left, or as big as their content; a child may span several cells |
anchored([...]) | each child pinned to fractions of the parent, moved by offsets, about a pivot point of its own |
absolute([...]) | each child at a stated position |
dock([...]) | children against the edges in order, the last filling what remains |
split([first, second], ratio:, vertical:) | two panes with a divider the visitor can drag |
scroll([child], across:, down:) | a window onto a child larger than itself, with scrollbars |
Every container takes its children as one list, its first argument, and then its settings as named options. Any child of any container may be taken out of its parent's flow and placed by position instead.
Properties of every box
These are properties of any node, not of particular kinds, so that anchors, grid cells and flex settings work the same on a panel, a label or a button:
| Property | Meaning |
|---|---|
size: (w, h), minSize, maxSize | the size wanted, and its limits; auto for "as the content needs" |
padding:, margin: | one length all round, or four (left, top, right, bottom) |
grow:, shrink:, basis: | how a child of a row or column shares spare room or gives it up |
alignSelf: | this child's own alignment across its row or column |
cell: (col, row), span: (cols, rows) | its place in a grid |
anchor:, offset:, pivot: | its place in an anchored parent: two fractions pin it to a point of the parent; four (left, top, right, bottom) stretch it between two points, so anchor: (0, 0, 1, 1) fills the parent, as a dialogue's darkened backdrop does |
side: | its edge in a dock |
visible:, enabled: | shown or not; answers the pointer or not (a disabled button is shown dimmed) |
clip: | hides whatever of its children falls outside it |
style: | the style it takes its looks from (below) |
tooltip: | words shown when the pointer rests on it |
How you write it
Each container is a function: its children are its first argument, one list, and its settings are named options after it. A plain panel is box([], fill: ...), or box([child], style: ...) with children. A status panel, its first two lines and its corner place, written this way (the clock text is clockText):
part hud
derive(status, layout(
anchored([
column([
text(clockText(now(world), weather.state)), // "12:00 fair"
text(if connected { "connected " <- rttText } else { "single player" })],
gap: 3 px, style: styles.hudGlass, // top left, 14 px in from each edge, 172 px wide
anchor: (0, 0), pivot: (0, 0), offset: (14 px, 14 px), size: (172 px, auto))])))
A layout may make many children from a list with map(list, x => element), which gives the list of elements:
column(map(questions, q => button(questionText(q), does: ask(q, to: g))), // a talk panel's buttons, one per question
gap: 8 px)
What it means
A layout is a value: a pure description, recomputed by the engine whenever an input changes (here the clock and the weather), and laid out again only where it changed. It runs on the machine that draws it, each visitor's own. A layout may not act; what a control does when pressed is an action, performed by the visitor (below, "Controls").
Not yet supported. The uilayout package has no flex wrap (a row that wraps onto a second line), no grid auto-placement and no animation of properties of its own. The last is not missing from the language: a layout's numbers can be fields that animators drive (Animators).
Text
What it is
Words in a layout are kept as a description (the words, the font, the size, the color) until they are drawn, so they are sharp at any size and angle. The msdftext package reads TrueType fonts, keeps each glyph as a multi-channel signed distance field (each texel holds distances to the glyph's edges, from which an edge one screen pixel wide is drawn at any size), lays words out with kerning, breaks lines at a width and aligns them left, center or right. The richtext package builds documents on it: paragraphs and headings whose words carry styles, lists, and pictures in the flow of the text.
How you write it
text("Lanterns lit at dusk", size: 6 cm, font: inter, color: rgba(60, 40, 20, 255),
align: center, wrap: 1 m, lineHeight: 1.2,
outline: outline(rgba(0, 0, 0, 255), width: 2 px),
shadow: shadow(rgba(0, 0, 0, 160), offset: (2 px, 2 px), softness: 3 px)) // illustration
| Setting | Meaning |
|---|---|
size: | the em, the font's own size unit, in px or meters |
font: | a font the world has (below) |
color: | the fill, with transparency |
align: | left, center, right; in rich text also justify (lines widened to fill) |
wrap: (to be built) | the width at which lines break; none: only at line ends in the words. A label that wraps is (to be built) in uilayout; the dialogue on Overlays writes it |
lineHeight: | times the font's own line spacing |
kerning: | pair kerning (on by default) |
outline: | a color and a width round each glyph |
shadow: | a color, an offset and a soft edge's width, under each glyph |
Outline and shadow are read from the same distance field as the glyph, so they cost no extra pictures; they reach no further than half the field's range beyond the glyph.
Runs and rich text
A run is a stretch of words with one style. Rich text is a list of blocks (paragraphs, headings, list items), each a list of runs:
rich([
heading(1, "The labyrinth"), // illustration
paragraph([run("One of us "), run("never", italic: true), run(" lies.")]),
bullet("Ask one question."),
bullet("Then choose a door.", depth: 0),
paragraph([picture(labyrinthPlan, float: right, size: (120 px, 120 px), alt: "the plan"),
run("The middle lies beyond the two doors.")], align: justify),
paragraph([run("Read the "), run("rules", underline: true, link: 1), run(".")])])
A layout only describes; a pressed link is an event the world answers in a handler, like a pressed button (on(linkPressed(notice, 1), ...), an illustration of the trigger's name).
- A run's style: bold, italic, underline, size, color, and a link number the world gives meaning to.
- Blocks: paragraph, headings of three levels, bulleted and numbered list items with a depth for nesting; numbering is worked out from the blocks before.
- Pictures in text sit inline, as a large letter on the line, or float left or right with the words wrapping round them.
- Line breaking is greedy: a line is filled with words until the next does not fit, then broken at the last space, after a hyphen, or inside a word that alone is too wide.
- The pointer in text: the layout can say which link or which picture lies under a point, so a pressed link becomes an event.
Fonts
A font is a TrueType file. Bold and italic are separate faces of a family, up to four (regular, bold, italic, bold italic); a missing face falls back to the regular one, and there is no slanted imitation of italic, because the glyph quads cannot slant. When a font cannot be read, text falls back to a system font, or is not drawn. Which fonts a world may use, and whether a world may bring its own file, is not settled; the license of any font a world embeds is an open question of the text package.
What it means, and what is not yet supported
- Text is laid out where it is drawn, at its final size; a sign's words are laid out again as the visitor approaches.
- Not yet supported: shaping and right-to-left text. Layout is one glyph per letter, left to right. Scripts whose letters change shape with their neighbors (Arabic, the Indic scripts, Thai's stacked marks) and mixed-direction text need text shaping, which is not built. CFF fonts (cubic outlines) are refused.
- Not yet supported: wrapped labels (to be built). A
uilayoutlabel is one line; richtext wraps but is not yet auilayoutelement. A label that wraps, writtentext(words, wrap: 364 px, lines: 4)as the dialogue on Overlays writes it, is (to be built); until it is, wrapped words in a panel are broken by hand into fixed lines. - Not yet supported: text on a path and rotated text in
vector2d's drawing.
Images in a layout
picture(image, size: (w, h), tint: color, region: (u0, v0, u1, v1))
minimap(hud.map, size: 176 px, shape: rect, center: (u, v), zoom: 6,
markers: [marker((u, v), rgba(255, 255, 255, 255), 9 px)])
Any image (Images) may be placed in a layout, tinted and cut to a region. A minimap is a picture with a window that moves over it, a zoom, a rectangular or round frame, and markers placed on it in fractions of the picture; a press on it reports where, as fractions across and down.
Vector shapes
What it is
Drawn shapes: lines, curves, rounded panels, icons. The vector2d package draws them anti-aliased (with smooth edges) on the graphics card, by cutting each shape into triangles whose edges fade over one pixel.
How you write it
shape(path([move(0, 0), line(40, 0), quad(60, 0, 60, 20), cubic(60, 40, 30, 50, 0, 40), close]),
fill: linear((0, 0), rgba(240, 180, 60, 255), (0, 50), rgba(160, 90, 20, 255)),
stroke: stroke(width: 2 px, cap: round, join: round, dash: [6 px, 3 px])) // illustration
shape(roundRect(0, 0, 200 px, 80 px, radius: 10 px), fill: solid(rgba(50, 100, 200, 255)))
shape(circle(20 px, 20 px, 8 px), stroke: stroke(width: 1.5 px), arrows: none)
turned(0.3, shape(...)), moved((10 px, 0), shape(...)), scaled(2, shape(...))
| Part | What vector2d offers |
|---|---|
| Paths | move, line, quadratic and cubic curves, arcs, close; rectangles, rounded rectangles, ellipses, circles, polylines |
| Fill | a solid color or a linear gradient between two colors along a line |
| Stroke | width; caps butt, square, round; joins miter (with a limit), bevel, round; dashes with an offset |
| Arrowheads | triangle, open vee, diamond, circle, bar; solid or hollow |
| Transforms | translate, scale, rotate, with save and restore |
| Text | a line, a wrapped block, and text with a halo |
Rounded panels
A panel's look needs no path: any box's style gives it a fill, a border color and width, a corner radius and a drop shadow. A HUD's panels are typically drawn this way: dark glass with a rounded corner and a thin border.
Not yet supported
- No holes, no clipping, no self-crossing fills. Each outline is filled on its own; a ring is a thick stroked circle or two fills.
- No radial gradient and no image fill: paint is solid or linear only.
- Text is drawn over all shapes within one drawing; layers are separate drawings.
- Shapes inside a
uilayoutlayout.uilayout's own drawing list has rectangles, borders, rounded rectangles, images, lines, arcs, text and clips, but no paths, so an icon is drawn by hand as arcs and lines laid over its slot. Whether vector2d becomesuilayout's renderer is undecided.
Controls
What it is
A control is a box the visitor can press, drag or type into. In a world, a control does not run code of its own: it is one more way for a visitor to perform an action, beside a key, a gamepad button or a voice command. The world never sees keys, only actions (Visitors and their actions), and a control is bound to an action the same way.
The controls the packages offer
| Control | What the visitor does | What it reports |
|---|---|---|
button(label, does:) | presses it | a click |
toggle(label, shows:, does:), radio groups | turns it on or off; one of a group | toggled |
slider(range:, step:, shows:, does:), knob(...) | drags a value within a range | changed, with the value |
textField(shows:, placeholder:, does:) | types; caret, selection, word movement, clipboard | text changed; submitted with Enter |
choices(items, shows:, does:) | picks a row from a list | selected; activated |
tabs(...) | picks a page | tab chosen |
window(title, movable:, resizable:, closable:) | moves, resizes, closes, raises it | moved, resized, closed |
hotbar(slots), slot(does:, key:, cooldown:) | uses a slot by a key or a press; a slot cools down after use | used; busy (with the time left) |
inventory(container, columns:), bags | drags items between slots and bags | a drop done or refused |
numberDrag, vectorField, colorPicker, dropdown | drags a number sideways, sets two to four numbers, picks a color, picks from a list that opens in place | a change with its value before and after |
inspector(part) | edits a part's fields, each with the control its kind wants | a change per field |
(The package named controls is something else: it steers cameras from input, orbiting and flying; see Cameras.)
How you write it
button("Which door leads to the middle?", does: ask(middle, to: g))
button("Walk away", does: close(talk)) // the action's name is an illustration
slider(range: 0 ..= 1, step: 0.05, shows: visitorSettings.volume,
does: setVolume) // illustration: the action carries the value
hotbar([slot(does: paint, key: F1, cooldown: real(1.5 s), icon: icons.splat),
slot(does: zap, key: F2, cooldown: real(1.5 s), icon: icons.bolt),
slot(does: jig, key: F3, cooldown: real(1.5 s), icon: icons.dancer)])
Written as a world's layout, the slot and the action are one thing: the slot names the action it performs, and cools down after use.
does:names the action the visitor performs when the control is used. A control that shows a value (shows:) reads it from a field; it changes the field only by performing its action, which the world's rules answer like any other action (Events, Actions).key:on a slot is a suggestion. Keys belong to the person: a world suggests, the person's own binding wins every collision, and the reserved keys are the browser's (Visitors).- A control whose action is not offered at the moment is shown disabled, since an action's availability is known (Visitors).
What it means
- Events travel as in a web page. A press finds the box under the pointer, then travels from the root down to it (capture), at it (target), and back up (bubble); listeners on the way may stop it. A modal box, while set, is the only place presses are found; a press that hits no box goes on to the world. Overlays takes this further, into the 3-D world.
- Focus and Tab. One box has the keyboard's focus; Tab moves it by an explicit order first, then the tree's order.
- A control's presses are the visitor's acts, checked by the server like any action: a world cannot make a button do what its action is not allowed to.
Styles
What it is
A style is a named set of looks that a world sets once and many boxes share: the parchment of a medieval village's notices, the green-on-black of a ship's terminal, the dark glass of a HUD. Styles inherit from a parent style, and vary with a box's state: hovered, pressed, disabled, focused, checked, selected, a drop accepted or refused.
How you write it
part styles
field hudGlass: style = style(like: panel,
background: rgba(9, 13, 20, 170), border: rgba(150, 162, 184, 130), borderWidth: 1 px,
radius: 8 px, padding: 10 px, spacing: 3 px)
field hotbar: style = style(like: hotbarSlot,
background: rgba(9, 13, 20, 170), border: rgba(150, 162, 184, 150), borderWidth: 1.5 px,
radius: 6 px, size: 46 px,
hover: style(background: rgba(30, 36, 48, 190)))
field question: style = style(like: button,
background: rgba(52, 70, 104, 255), border: rgba(130, 146, 178, 255),
hover: style(background: rgba(70, 94, 140, 255)),
pressed: style(background: rgba(40, 54, 82, 255)),
disabled: style(background: rgba(36, 40, 50, 255)))
field parchment: style = style(like: panel, // illustration
background: rgba(236, 222, 186, 255), border: rgba(120, 90, 50, 255), borderWidth: 2 px,
radius: 4 px, textColor: rgba(60, 40, 20, 255), font: serifBook)
field terminal: style = style(like: panel, // illustration
background: rgba(0, 10, 0, 230), textColor: rgba(80, 255, 120, 255), font: mono, radius: 0 px)
// at the top of the world's text (the root), not inside a part:
field theme: theme = theme(panel: styles.hudGlass, hotbarSlot: styles.hotbar) // illustration
| Look | Varies with state? |
|---|---|
background, textColor, border, accent (a fill showing a value: a slider's, a selection), track (the groove behind it), shadow | yes |
borderWidth, radius | yes |
font, fontSize, padding, spacing, size | no: these change a box's size, and are read with no state, so hovering never moves anything |
What it means
- Which value wins, after CSS's cascade: of all the values for a look in the style and its parents whose state applies, the one for the most specific state wins; a tie goes to the style nearest the box, then to the value set last. So a "danger" button that sets only its own color still gets the ordinary button's hover color.
- A world's theme maps the vocabulary's own element kinds (panel, label, button, slider, hotbar slot, minimap...) to the world's styles, so a world restyles every box of a kind at once; a box may still name a style of its own.
- The browser's own controls keep the browser's style. A world's theme reaches only the world's layouts (Overlays).
- Styles are data and may change while the world runs: a terminal whose glass cracks takes a new style, and every box using it follows.
Placing a layout over a 3-D anchor
What it is
A bubble above a speaker, a name above a head, a marker over a quest-giver: a 2-D layout whose place on the screen follows a point in the 3-D world, drawn in each visitor's picture where that point falls in it.
How you write it
overlay(plate(nameOf(b)), // a name plate above: b, by: b.height + 0.45 m, // the point 0.45 m over the head pivot: (0.5, 1), // the plate's bottom middle sits on that point within: 80 m, // how far from the eye it is still drawn whenBlocked: fade(real(0.17 s)), // with no clear line from the eye to the point keepOnScreen: false) // hide a plate off the screen's edge
| Setting | Meaning |
|---|---|
above:, by: | the part and the height over its place where the anchor point is |
pivot: | which point of the layout sits on the anchor |
within: | how far from the eye it is still drawn |
whenBlocked: | what happens when something stands between the eye and the anchor |
keepOnScreen: | held inside the screen's edge when the anchor is off it, pointing toward it; with false a plate is drawn only when its point is in front of the eye and inside the screen |
| depth | hidden pixel by pixel by what stands in front: half a bubble behind a pillar's edge (not yet supported for overlays) |
An overlay is drawn over the picture, the same size on the screen however far away its anchor is. The other way to put words at a point is to place them in the world: msdftext can place text in the world fixed, or turned to face the camera (a billboard, either fully or only about the upright axis), lit and hidden by depth like any surface, and growing smaller with distance. The two are the spatial and the non-diegetic interface of Overlays. Text in the world that keeps a constant size on the screen is later work of the text package.
A layout on a surface
part noticeBoard, at: (...) // illustration
part board
field picture: image
derive(board.picture,
layout(column([text("Lanterns lit at dusk", size: 6 cm),
text(clockText(lanterns.nextLighting), size: 4 cm)],
style: styles.parchment, padding: 4 cm), width: 1.2 m))
The same layout that could sit over the picture is drawn into the board's image, at the resolution the board takes up on the screen (Images). Its controls are pressed by the ray from the eye: the ray is turned into the pixel it hits on the board's image, so hover, press and drag work as on the screen.
How it connects
- Images: the
layout(...)source; compositing. - Overlays, HUDs and speech: HUDs and dialogues written as layouts; who owns which overlay; the pointer.
- Visitors and their actions: what a control's
does:performs; keys as suggestions. - Subscriptions: a layout's words and numbers follow the world.
- Text and structured text: strings, formatting, dialogue lines.
- Animators: moving and fading a layout's numbers.
Open questions
- A layout over a list. A layout writes
map(list, x => element), a function giving the list of elements; Cone's own collection form,List[element]() <- each x in list yield ..., builds the same list. A layout is a value, so it needs a form that gives a value; whether both stay is open (A boss and its army; Control flow). - The vocabulary is to be shaped. The element names on this page are placeholders; which elements the browser's vocabulary offers, and how they are qualified, is open (the namespacing question of Packages).
- Text and layout meet at a closure. Whether the text package also answers caret and selection measures, and the height of a wrapped label for a given width.
- One renderer for 2-D. Whether vector2d becomes the renderer of
uilayout's drawing list. - Fonts. Which fonts a world may name, and the license of any font a world embeds.
- Accessibility. The layout tree has roles by kind, but nothing passes it to a screen reader; from the start, or later.
- Two inventories.
uilayout's inventory model andworldinteract's inventory are two models; one should own and the other show.