layout
Work out where std:ui tags land on screen without opening a window, build nodes from code, and search and edit a node tree.
import "std:layout"std:layout is the layout engine behind ui and window, with no screen attached. Give it a tree of tags and a size, and it tells you the rectangle of every element. It follows the same rules as a web page: hug, fill and fixed sizes, gap, pad and nine-point align.
Use it to test that a view lays out the way you meant, to measure text before you draw it, to find what sits under a click, and to build or change trees in code. Text is measured with the UI's own built-in font, so every machine gets the same numbers.
Laying out a tree#
layout.boxes(node, {width, height}) lays a tree out in a screen of that size (800 by 600 if you leave it out) and returns one record per element, parents before their children. Each has tag, x, y, width, height, text and key. Positions count from 1 at the top-left corner, like everything else in OSL.
import "std:ui"
import "std:layout"
def Panel() ui.node (
return <column width=fill height=fill pad=20 gap=10>
<row width=fill>
<text>Title</text>
<spacer/>
<text>2 left</text>
</row>
<row gap=10 width=fill>
<input width=fill placeholder="Name"/>
<button>Add</button>
</row>
</column>
)
for box in layout.boxes(Panel(), {width: 400, height: 200}) (
log box.tag box.x box.y box.width box.height
)The outer column fills the 400 by 200 screen. Inside its 20 pixel padding the rows are 360 wide. In the first row the spacer takes all the room the two texts leave. In the second, the input fills what the button leaves.
Hug, fill and fixed#
Every size on every axis follows one of three rules. A tag's size is hug unless you say otherwise: as big as its content. width=fill takes the space left in the parent. A number is pixels.
import "std:ui"
import "std:layout"
def Rows() ui.node (
return <column width=300>
<row width=fill height=20>
<box width=50 height=20/>
<box width=fill height=20/>
<box width=fill height=20/>
</row>
</column>
)
for box in layout.boxes(Rows()) (
log box.tag box.x box.width
)The fixed box takes 50, and the two fill boxes split the remaining 250. Add maxWidth or minWidth to limit a size, and a fill that hits its limit hands its leftover space to its siblings.
Gap, pad and align#
gap goes between children, pad goes inside the edge, and align places children inside a container that is larger than they are. It names one of nine points (top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right). A single word such as top or left sets that edge; center centres both ways.
import "std:ui"
import "std:layout"
def Card() ui.node (
return <column width=200 height=100 pad=10 gap=4 align=bottom-right>
<box width=60 height=20/>
<box width=40 height=20/>
</column>
)
for box in layout.boxes(Card()) (
log box.tag box.x box.y box.width box.height
)The two boxes are stacked with a 4 pixel gap, and the stack sits in the bottom right corner, inside the padding.
Measuring#
layout.measure(node) is how big a node is when it hugs its content. Text wraps when you give it maxWidth:
import "std:ui"
import "std:layout"
ui.node note = <text>one two three four five six</text>
auto wide = layout.measure(note)
auto narrow = layout.measure(note, {maxWidth: 100})
log wide
log narrow
log narrow.height > wide.heightFinding what is at a point#
layout.at(node, x, y) returns the innermost element under a point, in the same coordinates as boxes, or null when nothing is there. Give a key prop to the elements you want to recognise:
import "std:ui"
import "std:layout"
def Toolbar() ui.node (
return <row gap=8 pad=8>
<button key="save">Save</button>
<button key="open">Open</button>
</row>
)
auto size = {width: 300, height: 100}
auto save = layout.at(Toolbar(), 15, 15, size)
log save.tag save.key
auto open = layout.at(Toolbar(), 90, 15, size)
log open.tag open.key
auto gap = layout.at(Toolbar(), 250, 80, size)
log gap.tag
log layout.at(Toolbar(), 500, 500, size) == nullThe first two points land on the buttons. The third is empty space in the row, so you get the outermost element, which always fills the screen. A point outside the screen finds nothing, and you get null.
Building nodes in code#
Every tag has a function with the same name: layout.column(props, children), layout.row, layout.text(props, text), layout.button, layout.input, layout.checkbox, layout.spacer, layout.stack, layout.grid, layout.scroll, layout.image, layout.link, layout.box and layout.absolute. They build the same nodes, which is useful when a tree comes from data. Prop names are strings or plain keys, and "fill" is the text of width=fill:
import "std:ui"
import "std:layout"
string[] names = ["Ada", "Grace", "Edsger"]
ui.node[] rows = []
for name in names (
rows.append(layout.text({}, name))
)
ui.node list = layout.column({gap: 6, pad: 10}, rows)
for box in layout.boxes(list) (
log box.tag box.y box.text
)Searching and editing a tree#
A node never changes after it is built. Its tag, props, children, key and text can be read, and you make a changed copy with node.with(props). layout.find(node, test) returns the first node (depth first) that the test accepts, layout.findAll returns all of them, and layout.replace(root, old, replacement) returns a copy of the tree with one node swapped:
import "std:ui"
import "std:layout"
def Panel() ui.node (
return <column pad=20>
<text>Title</text>
<button>Add</button>
<button>Remove</button>
</column>
)
ui.node panel = Panel()
log panel.tag panel.props.pad panel.children.len
auto buttons = layout.findAll(panel, n -> n.tag == "button")
log buttons.len
ui.node? first = layout.find(panel, n -> n.tag == "button")
if first != null (
ui.node original = first
ui.node red = original.with({bg: "red"})
log red.props.bg original.props.bg == null
ui.node changed = layout.replace(panel, original, layout.button({}, "Clear"))
log layout.findAll(changed, n -> n.text == "Clear").len
log layout.findAll(panel, n -> n.text == "Clear").len
)layout.replace finds the old node by identity, not by looking equal, and leaves the original tree alone. Setting a member on a node directly, as in n.tag = "row", is a compile error that points you to with.
Testing a layout#
Because boxes needs no window, a layout can be checked in a test. Pick the elements by key and check their rectangles:
import "std:ui"
import "std:layout"
def Footer() ui.node (
return <row width=fill pad=10>
<text>v1.2</text>
<spacer/>
<button key="ok">OK</button>
</row>
)
auto boxes = layout.boxes(Footer(), {width: 400, height: 100})
auto ok = boxes.find(b -> b.key == "ok")
log ok.x + ok.width
log ok.yThe OK button ends at the row's padded right edge. Positions count from 1, so on a screen 400 wide with 10 pixels of padding, that edge is 391.
layout.boxes reports a text node at x: 11. Where is it on screen?
Two even columns
Lay out a 400 pixel wide <row> holding two <box height=10 width=fill/>. Print the width of each box.
row 400 box 200 box 200
API reference#
20 members, generated from the std:layout source of OSL 9.3.0.
Functions
layout.absolute(object props, array children) ui.Nodeabsolute builds an <absolute>: children outside the parent's layout, at its pad values from the parent's edges.
layout.at(ui.Node node, number x, number y, object? options) {height: number, key: string, tag: string, text: string, width: number, x: number, y: number}?at is the innermost element at x, y, counted from 1, when node is laid out in a screen of opts.width by opts.height (800 by 600), as boxes lists it, or null when nothing is there.
layout.box(object props, array children) ui.Nodebox builds a <box>: a column that only groups its children.
layout.boxes(ui.Node node, object? options) {height: number, key: string, tag: string, text: string, width: number, x: number, y: number}[]boxes lays node out in a screen of opts.width by opts.height (800 by 600 when left out) and lists where every element lands, parents before their children: {tag, x, y, width, height, text, key}, with x and y counting from 1 at the screen's top-left corner. Text is measured with the UI's own font, as a window draws it. Hidden elements are left out. Throws ValueError for a size that isn't above 0.
layout.button(object props, string text) ui.Nodebutton builds a <button> labelled text; its on:click prop is what it runs.
layout.checkbox(object? options) ui.Nodecheckbox builds a <checkbox>, with a bind or checked prop.
layout.column(object props, array children) ui.Nodecolumn builds a <column>: children stacked top to bottom.
layout.column({gap: 8, pad: 16}, [layout.text({size: 20}, "Hi")])layout.find(ui.Node node, any test) anyfind is the first node in node's tree, node itself included, for which test returns true, going depth first, or null.
layout.findAll(ui.Node node, any test) ui.Node[]findAll lists the nodes in node's tree, node itself included, for which test returns true, depth first.
layout.grid(object props, array children) ui.Nodegrid builds a <grid>: children in rows of equal columns.
layout.image(object? options) ui.Nodeimage builds an <image> of the file or URL in its src prop.
layout.input(object? options) ui.Nodeinput builds an <input>, usually with a bind prop.
layout.link(object props, string text) ui.Nodelink builds a <link> labelled text, to its href prop or running its on:click prop.
layout.measure(ui.Node node, object? options) {height: number, width: number}measure is how big node is when it hugs its content: {width, height}, with text wrapped at opts.maxWidth when given. A fixed size is its size.
layout.replace(ui.Node root, ui.Node old, ui.Node replacement) ui.Nodereplace returns a copy of root with the node old, found by identity, swapped for replacement, everywhere it appears. root itself is unchanged.
layout.row(object props, array children) ui.Noderow builds a <row>: children side by side.
layout.scroll(object props, array children) ui.Nodescroll builds a <scroll>: a column whose children scroll when they don't fit.
layout.spacer(object? options) ui.Nodespacer builds a <spacer>, which takes the room left along its parent.
layout.stack(object props, array children) ui.Nodestack builds a <stack>: children on top of each other.
layout.text(object props, string text) ui.Nodetext builds a <text> showing text.