gui.cssbox
A CSS box layout engine in pure Salam: block flow with margin collapsing, flexbox, grid, tables, absolute positioning and incremental relayout.
import gui.cssbox
This package computes where every box of a user interface goes, using the rules of CSS, so that a page drawn natively has the same geometry as the same page in a browser. You build a Tree of boxes, give each one a Style (display, size, margins, padding, border, flex properties and so on), and call Compute with the size of the window. Layout then gives each box its position, size, content box and scrollable area. The package draws nothing and knows nothing about fonts: text and images are leaves whose size comes from a measure callback you supply.
Block flow follows CSS 2 with margin collapsing between siblings, between a parent and its first or last child, and through empty boxes. Flexbox follows the CSS flexbox specification: flex base sizes, the grow and shrink loop with freezing, wrapping, every justify and align value, auto margins, baselines, reverse directions, order and right to left rows. Absolute and fixed boxes are placed against their containing block, including their static position when insets are auto. Floats, inline text flow and writing modes other than horizontal are not done.
Grid follows CSS Grid Level 1: track lists with pixels, percentages, fr, auto, min-content, max-content, fit-content(), minmax() and repeat() including auto-fill and auto-fit, named lines, grid-template-areas, implicit tracks from grid-auto-rows and grid-auto-columns, sparse and dense auto placement in either direction, spans and negative lines, the full track sizing algorithm, every justify and align value for items and content, baselines, and absolutely positioned items placed in their grid area. Subgrid is not done. Grid properties are strings in CSS syntax, such as grid_template_columns = "repeat(3, 1fr)"; the tree keeps its own copy, and IsTrackList and the other checks tell whether a value is valid.
Tables follow the CSS 2.1 table model the way Chrome lays it out: rows, row groups, header and footer groups (moved to the top and bottom), columns and column groups, captions on either side, anonymous rows and cells around misplaced children, col_span and row_span, automatic column widths from each cell's minimum and maximum content width (with percentage and fixed cells), fixed table layout, border-spacing, collapsed borders, extra height given to rows, and vertical-align in cells.
The block, flex and grid algorithms are ports of Taffy, and the package is tested against 1,527 of Taffy's fixtures measured in Chrome (616 of them with grid), in both box sizing models and both directions. Tables were written from the specification and checked against 53 cases measured in headless Chrome.
Things to know
- Lengths are already resolved: give pixels or percentages. Convert em, rem, vw and vh before building the style.
- Defaults are the CSS initial values:
displayis block,positionis static andbox_sizingis content-box. - Ids are plain integers in the order of
AddNode; the node you pass toComputeis the root. - Results are rounded to whole pixels the way Chrome rounds them. Turn this off with
SetRounding(false)to read fractions. - Each node caches its last results. After a style change only the changed node and its ancestors are laid out again; call
MarkDirtywhen the content behind a measure callback changes.
A toolbar with flexbox and text
A row with a growing title, a button and a badge moved first with order. The measure callback stands in for text: it answers how wide the text is with unlimited room, how narrow it can get, and how tall it is at a given width. Switching the direction to right to left mirrors the row.
import gui.cssbox
func label(chars: int, mi: cssbox.MeasureInput): cssbox.MeasureOutput:
full := (chars as f64) * 8.0
if mi.available_width.kind == cssbox.SpaceKind.MinContent:
ret cssbox.MeasuredWithBaseline(40.0, 32.0, 12.0)
end
if mi.available_width.kind == cssbox.SpaceKind.Definite and mi.available_width.value < full:
ret cssbox.MeasuredWithBaseline(mi.available_width.value, 32.0, 12.0)
end
ret cssbox.MeasuredWithBaseline(full, 16.0, 12.0)
end
func main:
mut t := cssbox.Tree {}
defer t.Free()
bar := t.AddNode(cssbox.Style { display = cssbox.Display.Flex, width = cssbox.Px(400.0), padding = cssbox.AllSides(cssbox.Px(8.0)), column_gap = cssbox.Px(8.0), align_items = cssbox.Align.Baseline, justify_content = cssbox.Align.SpaceBetween })
title := t.AddNode(cssbox.Style { flex_grow = 1.0, flex_shrink = 1.0, flex_basis = cssbox.Auto() })
t.SetMeasure(title, (mi: cssbox.MeasureInput): ret label(30, mi) end)
button := t.AddNode(cssbox.Style { padding = cssbox.SidesOf(cssbox.Px(4.0), cssbox.Px(12.0), cssbox.Px(4.0), cssbox.Px(12.0)), border = cssbox.AllSides(cssbox.Px(1.0)) })
t.SetMeasure(button, (mi: cssbox.MeasureInput): ret label(4, mi) end)
badge := t.AddNode(cssbox.Style { width = cssbox.Px(24.0), height = cssbox.Px(24.0), align_self = cssbox.Align.Center, order = -1 })
t.AddChild(bar, title)
t.AddChild(bar, button)
t.AddChild(bar, badge)
t.Compute(bar, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
each id in [bar, badge, title, button]:
l := t.Layout(id)
println id, l.x, l.y, l.width, l.height
end
rtl := t.GetStyle(bar)
mut s := rtl
s.direction = cssbox.Direction.Rtl
t.SetStyle(bar, s)
t.Compute(bar, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
println "rtl badge x", t.Layout(badge).x, "title x", t.Layout(title).x
endBlock flow and absolute positioning
The header's bottom margin and the card's top margin collapse into one 30 pixel gap, the card is centred with auto margins, and the close button is placed in the corner of the page, which is its containing block because it is positioned. A box with display none takes no space.
import gui.cssbox
func main:
mut t := cssbox.Tree {}
defer t.Free()
page := t.AddNode(cssbox.Style { width = cssbox.Px(320.0), position = cssbox.Position.Relative, border = cssbox.AllSides(cssbox.Px(2.0)) })
head := t.AddNode(cssbox.Style { height = cssbox.Px(40.0), margin = cssbox.SidesOf(cssbox.Px(10.0), cssbox.Px(0.0), cssbox.Px(20.0), cssbox.Px(0.0)) })
card := t.AddNode(cssbox.Style { width = cssbox.Percent(50.0), margin = cssbox.SidesOf(cssbox.Px(30.0), cssbox.Auto(), cssbox.Px(0.0), cssbox.Auto()), padding = cssbox.AllSides(cssbox.Px(10.0)), box_sizing = cssbox.BoxSizing.BorderBox, min_height = cssbox.Px(60.0) })
close := t.AddNode(cssbox.Style { position = cssbox.Position.Absolute, inset = cssbox.SidesOf(cssbox.Px(4.0), cssbox.Px(4.0), cssbox.Auto(), cssbox.Auto()), width = cssbox.Px(16.0), height = cssbox.Px(16.0) })
hidden := t.AddNode(cssbox.Style { display = cssbox.Display.None, height = cssbox.Px(500.0) })
t.AddChild(page, head)
t.AddChild(page, card)
t.AddChild(page, close)
t.AddChild(page, hidden)
t.Compute(page, cssbox.Definite(800.0), cssbox.Definite(600.0))
each id in [page, head, card, close, hidden]:
l := t.Layout(id)
println id, l.x, l.y, l.width, l.height, "margin", l.margin.top, l.margin.left
end
endChanging one box
The counters show the cache at work: a second Compute with nothing changed computes nothing, and changing one leaf recomputes that leaf and its ancestors. Laying out a 5,000 box page (50 wrapping flex rows of 100 text leaves) takes about 15 ms the first time on one desktop core, and about 0.7 ms after one leaf changes; most of that is rounding the whole tree.
import gui.cssbox
func main:
mut t := cssbox.Tree {}
defer t.Free()
list := t.AddNode(cssbox.Style { display = cssbox.Display.Flex, flex_direction = cssbox.FlexDirection.Column, row_gap = cssbox.Px(4.0) })
mut items := Vector {} as Vector<int>
defer items.free()
repeat 3 in i:
item := t.AddNode(cssbox.Style { height = cssbox.Px(20.0) })
t.SetMeasure(item, (_mi: cssbox.MeasureInput): ret cssbox.Measured(100.0 + (i as f64) * 10.0, 20.0) end)
items.push(item)
end
t.SetChildren(list, items)
println "nodes", t.NodeCount(), "children", t.ChildCount(list), "second", t.Child(list, 1), "parent", t.Parent(items.get(0))
t.Compute(list, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
println "size", t.Layout(list).width, t.Layout(list).height, "calls", t.LayoutCalls(), "measures", t.MeasureCalls()
t.ResetCounters()
t.Compute(list, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
println "again", t.LayoutCalls(), "dirty", t.IsDirty(list)
t.SetMeasure(items.get(2), (_mi: cssbox.MeasureInput): ret cssbox.Measured(200.0, 20.0) end)
println "dirty", t.IsDirty(list), t.IsDirty(items.get(0))
t.Compute(list, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
println "width", t.Layout(list).width, "calls", t.LayoutCalls()
t.ClearMeasure(items.get(2))
t.MarkDirty(items.get(1))
t.SetRounding(false)
t.Compute(list, cssbox.MinContentSpace(), cssbox.MaxContentSpace())
println "width", t.Layout(list).width
endA page grid with named areas
A header, sidebar, main area and footer are placed by area name; the main box is centred in its cell with justify_self. The second grid fills a 250 pixel row with as many 70 pixel columns as fit, then stretches them, and the fourth tile wraps to the next row. A 20 by 50 grid of measured text cells lays out in about 1 to 3.5 ms the first time and about 0.8 to 3.3 ms after one cell changes, on one desktop core.
import gui.cssbox
func main:
mut t := cssbox.Tree {}
defer t.Free()
page := t.AddNode(cssbox.Style { display = cssbox.Display.Grid, width = cssbox.Px(400.0), grid_template_columns = "120px 1fr", grid_template_rows = "40px auto 30px", grid_template_areas = `"head head" "side main" "foot foot"`, column_gap = cssbox.Px(10.0), row_gap = cssbox.Px(6.0) })
head := t.AddNode(cssbox.Style { grid_row_start = "head", grid_row_end = "head", grid_column_start = "head", grid_column_end = "head" })
side := t.AddNode(cssbox.Style { grid_row_start = "side", grid_row_end = "side", grid_column_start = "side", grid_column_end = "side", height = cssbox.Px(80.0) })
body := t.AddNode(cssbox.Style { grid_row_start = "main", grid_row_end = "main", grid_column_start = "main", grid_column_end = "main", justify_self = cssbox.Align.Center, width = cssbox.Px(100.0), height = cssbox.Px(50.0) })
foot := t.AddNode(cssbox.Style { grid_row_start = "foot", grid_row_end = "foot", grid_column_start = "foot", grid_column_end = "foot" })
tiles := t.AddNode(cssbox.Style { display = cssbox.Display.Grid, width = cssbox.Px(250.0), grid_template_columns = "repeat(auto-fill, minmax(70px, 1fr))", grid_auto_flow = cssbox.GridAutoFlow.Row, column_gap = cssbox.Px(4.0) })
each c in [head, side, body, foot]:
t.AddChild(page, c)
end
repeat 4:
tile := t.AddNode(cssbox.Style { height = cssbox.Px(20.0) })
t.AddChild(tiles, tile)
end
t.Compute(page, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
each id in [page, head, side, body, foot]:
l := t.Layout(id)
println id, l.x, l.y, l.width, l.height
end
t.Compute(tiles, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
repeat t.ChildCount(tiles) in i:
l := t.Layout(t.Child(tiles, i))
println "tile", i, l.x, l.y, l.width
end
println cssbox.IsTrackList("1fr [mid] minmax(40px, 2fr)"), cssbox.IsTrackSizes("50px auto"), cssbox.IsGridLine("span 2"), cssbox.IsGridAreas("'a b' 'a c'")
endA table with spans and a caption
Column widths come from the cells' content. The first cell spans two rows and is centred vertically, the last spans two columns, and the caption sits below. The table then switches to a fixed 300 pixel layout with collapsed borders: the four columns share the width equally and each cell keeps half of every shared border. A table of 200 rows and 5 measured columns takes about 0.7 to 1 ms the first time and about 0.4 to 0.5 ms after one cell changes.
import gui.cssbox
func main:
mut t := cssbox.Tree {}
defer t.Free()
table := t.AddNode(cssbox.Style { display = cssbox.Display.Table, border_spacing_x = 4.0, border_spacing_y = 2.0, border = cssbox.AllSides(cssbox.Px(1.0)) })
caption := t.AddNode(cssbox.Style { display = cssbox.Display.TableCaption, caption_side = cssbox.CaptionSide.Bottom, height = cssbox.Px(16.0) })
body := t.AddNode(cssbox.Style { display = cssbox.Display.TableRowGroup })
t.AddChild(table, caption)
t.AddChild(table, body)
widths := [30.0, 80.0, 50.0, 40.0, 120.0]
mut cells := Vector {} as Vector<int>
defer cells.free()
repeat 2 in r:
row := t.AddNode(cssbox.Style { display = cssbox.Display.TableRow })
t.AddChild(body, row)
repeat 3 - r in c:
mut st := cssbox.Style { display = cssbox.Display.TableCell, border = cssbox.AllSides(cssbox.Px(2.0)), padding = cssbox.AllSides(cssbox.Px(2.0)), vertical_align = cssbox.VerticalAlign.Middle }
if r == 1 and c == 1:
st.col_span = 2
end
if r == 0 and c == 0:
st.row_span = 2
end
cell := t.AddNode(st)
inner := t.AddNode(cssbox.Style { width = cssbox.Px(widths[r * 3 + c]), height = cssbox.Px(12.0 + (r as f64) * 10.0) })
t.AddChild(cell, inner)
t.AddChild(row, cell)
cells.push(cell)
end
end
t.Compute(table, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
tl := t.Layout(table)
println "table", tl.width, tl.height
each c in cells:
l := t.Layout(c)
println "cell", c, l.x, l.y, l.width, l.height
end
cl := t.Layout(caption)
println "caption", cl.y, cl.width
mut fixed := t.GetStyle(table)
fixed.table_layout = cssbox.TableLayout.Fixed
fixed.width = cssbox.Px(300.0)
fixed.border_collapse = cssbox.BorderCollapse.Collapse
t.SetStyle(table, fixed)
t.Compute(table, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
each c in cells:
l := t.Layout(c)
println "fixed", c, l.x, l.width, l.border.left
end
mut first := t.GetStyle(cells[0])
first.border_kinds = cssbox.BorderKinds { top = cssbox.BorderKind.Hidden, right = cssbox.BorderKind.Double, bottom = cssbox.BorderKind.Solid, left = cssbox.BorderKind.Dashed }
t.SetStyle(cells[0], first)
t.Compute(table, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
mut segs := t.CollapsedBorders(table)
defer segs.free()
println "segments", segs.len()
s0 := segs[0]
println "segment", s0.x, s0.y, s0.width, s0.height, s0.node, s0.side, s0.vertical
endTypes
LengthKind
enum LengthKindLength holds: auto, pixels, a percentage or a sizing keyword.| Name | Type | Description |
|---|---|---|
Auto | | The CSS auto value. |
Px | | A length in logical pixels. |
Percent | | A percentage of the containing block; value is the CSS number, so 50 means 50%. |
MinContent | | The min-content keyword: as narrow as the content allows. |
MaxContent | | The max-content keyword: as wide as the content wants without wrapping. |
FitContent | | The fit-content keyword: max-content, but never wider than the space available. |
Stretch | | The stretch keyword: fill the containing block minus margins. |
import gui.cssbox
func main:
mut st := cssbox.Style {}
println st.display == cssbox.Display.Block, st.position == cssbox.Position.Static, st.flex_shrink, st.width.kind == cssbox.LengthKind.Auto
st.width = cssbox.Percent(50.0)
st.max_width = cssbox.Px(300.0)
st.min_height = cssbox.MinContent()
st.height = cssbox.FitContent()
st.margin = cssbox.AllSides(cssbox.Auto())
st.padding = cssbox.SidesOf(cssbox.Px(1.0), cssbox.Px(2.0), cssbox.Px(3.0), cssbox.Px(4.0))
println st.width.value, st.padding.left.value, st.margin.top.kind == cssbox.LengthKind.Auto
m := cssbox.MaxContent()
s := cssbox.Stretch()
println m.kind == cssbox.LengthKind.MaxContent, s.kind == cssbox.LengthKind.Stretch
sp := cssbox.Definite(120.0)
println sp.kind == cssbox.SpaceKind.Definite, sp.value, cssbox.MinContentSpace().kind == cssbox.SpaceKind.MinContent
mut t := cssbox.Tree {}
defer t.Free()
root := t.AddNode(cssbox.Style { width = cssbox.Px(200.0), overflow_y = cssbox.Overflow.Scroll, scrollbar_width = 12.0, height = cssbox.Px(50.0), border = cssbox.AllSides(cssbox.Px(3.0)), padding = cssbox.AllSides(cssbox.Px(5.0)) })
child := t.AddNode(cssbox.Style { height = cssbox.Px(90.0) })
t.SetMeasure(child, (mi: cssbox.MeasureInput): ret cssbox.MeasuredWithBaseline(mi.known_width, 90.0, 14.0) end)
t.AddChild(root, child)
t.Compute(root, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
l := t.Layout(root)
println l.x, l.y, l.abs_x, l.abs_y, l.width, l.height
println l.content_x, l.content_y, l.content_width, l.content_height
println l.padding.top, l.padding.right, l.padding.bottom, l.padding.left
println l.border.top, l.border.right, l.border.bottom, l.border.left, l.margin.top
println l.scrollbar_width, l.scrollbar_height, l.scroll_width, l.scroll_height, l.has_baseline, l.baseline
endDisplay
enum Displaydisplay property.| Name | Type | Description |
|---|---|---|
Block | | A block box laid out with block flow and margin collapsing. |
InlineBlock | | Treated as a block that shrinks to fit its content and starts a new formatting context; inline flow is not done yet. |
FlowRoot | | A block that starts a new block formatting context, so its children's margins do not collapse through it. |
Flex | | A flex container laid out with the CSS flexbox algorithm. |
InlineFlex | | A flex container that shrinks to fit inside a block parent. |
Contents | | The box itself disappears and its children take its place in the parent. |
None | | The box and its subtree take no space and get a zero layout. |
Grid | | A grid container laid out with the CSS grid algorithm. |
InlineGrid | | A grid container that shrinks to fit inside a block parent. |
Table | | A table: its children are laid out with the CSS table model, and the box shrinks to fit its columns unless it has a width. |
InlineTable | | A table that shrinks to fit inside a block parent. |
TableRowGroup | | A group of rows, like tbody. |
TableHeaderGroup | | A group of rows drawn first, like thead. |
TableFooterGroup | | A group of rows drawn last, like tfoot. |
TableRow | | A table row; its children are its cells. |
TableCell | | A table cell, laid out inside like a block that starts a new formatting context. |
TableColumn | | A column, like col; its width and span apply to the table's columns. |
TableColumnGroup | | A group of columns, like colgroup. |
TableCaption | | A caption placed above or below the table, as wide as the table. |
import gui.cssbox
func main:
mut t := cssbox.Tree {}
defer t.Free()
page := t.AddNode(cssbox.Style { width = cssbox.Px(320.0), position = cssbox.Position.Relative, border = cssbox.AllSides(cssbox.Px(2.0)) })
head := t.AddNode(cssbox.Style { height = cssbox.Px(40.0), margin = cssbox.SidesOf(cssbox.Px(10.0), cssbox.Px(0.0), cssbox.Px(20.0), cssbox.Px(0.0)) })
card := t.AddNode(cssbox.Style { width = cssbox.Percent(50.0), margin = cssbox.SidesOf(cssbox.Px(30.0), cssbox.Auto(), cssbox.Px(0.0), cssbox.Auto()), padding = cssbox.AllSides(cssbox.Px(10.0)), box_sizing = cssbox.BoxSizing.BorderBox, min_height = cssbox.Px(60.0) })
close := t.AddNode(cssbox.Style { position = cssbox.Position.Absolute, inset = cssbox.SidesOf(cssbox.Px(4.0), cssbox.Px(4.0), cssbox.Auto(), cssbox.Auto()), width = cssbox.Px(16.0), height = cssbox.Px(16.0) })
hidden := t.AddNode(cssbox.Style { display = cssbox.Display.None, height = cssbox.Px(500.0) })
t.AddChild(page, head)
t.AddChild(page, card)
t.AddChild(page, close)
t.AddChild(page, hidden)
t.Compute(page, cssbox.Definite(800.0), cssbox.Definite(600.0))
each id in [page, head, card, close, hidden]:
l := t.Layout(id)
println id, l.x, l.y, l.width, l.height, "margin", l.margin.top, l.margin.left
end
endGridAutoFlow
enum GridAutoFlowgrid-auto-flow property: how items without a position are placed.| Name | Type | Description |
|---|---|---|
Row | | Fill each row, then start a new one. |
Column | | Fill each column, then start a new one. |
RowDense | | Like Row, but later items may fill earlier holes. |
ColumnDense | | Like Column, but later items may fill earlier holes. |
import gui.cssbox
func main:
mut t := cssbox.Tree {}
defer t.Free()
page := t.AddNode(cssbox.Style { display = cssbox.Display.Grid, width = cssbox.Px(400.0), grid_template_columns = "120px 1fr", grid_template_rows = "40px auto 30px", grid_template_areas = `"head head" "side main" "foot foot"`, column_gap = cssbox.Px(10.0), row_gap = cssbox.Px(6.0) })
head := t.AddNode(cssbox.Style { grid_row_start = "head", grid_row_end = "head", grid_column_start = "head", grid_column_end = "head" })
side := t.AddNode(cssbox.Style { grid_row_start = "side", grid_row_end = "side", grid_column_start = "side", grid_column_end = "side", height = cssbox.Px(80.0) })
body := t.AddNode(cssbox.Style { grid_row_start = "main", grid_row_end = "main", grid_column_start = "main", grid_column_end = "main", justify_self = cssbox.Align.Center, width = cssbox.Px(100.0), height = cssbox.Px(50.0) })
foot := t.AddNode(cssbox.Style { grid_row_start = "foot", grid_row_end = "foot", grid_column_start = "foot", grid_column_end = "foot" })
tiles := t.AddNode(cssbox.Style { display = cssbox.Display.Grid, width = cssbox.Px(250.0), grid_template_columns = "repeat(auto-fill, minmax(70px, 1fr))", grid_auto_flow = cssbox.GridAutoFlow.Row, column_gap = cssbox.Px(4.0) })
each c in [head, side, body, foot]:
t.AddChild(page, c)
end
repeat 4:
tile := t.AddNode(cssbox.Style { height = cssbox.Px(20.0) })
t.AddChild(tiles, tile)
end
t.Compute(page, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
each id in [page, head, side, body, foot]:
l := t.Layout(id)
println id, l.x, l.y, l.width, l.height
end
t.Compute(tiles, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
repeat t.ChildCount(tiles) in i:
l := t.Layout(t.Child(tiles, i))
println "tile", i, l.x, l.y, l.width
end
println cssbox.IsTrackList("1fr [mid] minmax(40px, 2fr)"), cssbox.IsTrackSizes("50px auto"), cssbox.IsGridLine("span 2"), cssbox.IsGridAreas("'a b' 'a c'")
endTableLayout
enum TableLayouttable-layout property.| Name | Type | Description |
|---|---|---|
Auto | | Column widths come from the content of all cells. |
Fixed | | Column widths come from the columns and the first row only; it applies when the table has a width. |
import gui.cssbox
func main:
mut t := cssbox.Tree {}
defer t.Free()
table := t.AddNode(cssbox.Style { display = cssbox.Display.Table, border_spacing_x = 4.0, border_spacing_y = 2.0, border = cssbox.AllSides(cssbox.Px(1.0)) })
caption := t.AddNode(cssbox.Style { display = cssbox.Display.TableCaption, caption_side = cssbox.CaptionSide.Bottom, height = cssbox.Px(16.0) })
body := t.AddNode(cssbox.Style { display = cssbox.Display.TableRowGroup })
t.AddChild(table, caption)
t.AddChild(table, body)
widths := [30.0, 80.0, 50.0, 40.0, 120.0]
mut cells := Vector {} as Vector<int>
defer cells.free()
repeat 2 in r:
row := t.AddNode(cssbox.Style { display = cssbox.Display.TableRow })
t.AddChild(body, row)
repeat 3 - r in c:
mut st := cssbox.Style { display = cssbox.Display.TableCell, border = cssbox.AllSides(cssbox.Px(2.0)), padding = cssbox.AllSides(cssbox.Px(2.0)), vertical_align = cssbox.VerticalAlign.Middle }
if r == 1 and c == 1:
st.col_span = 2
end
if r == 0 and c == 0:
st.row_span = 2
end
cell := t.AddNode(st)
inner := t.AddNode(cssbox.Style { width = cssbox.Px(widths[r * 3 + c]), height = cssbox.Px(12.0 + (r as f64) * 10.0) })
t.AddChild(cell, inner)
t.AddChild(row, cell)
cells.push(cell)
end
end
t.Compute(table, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
tl := t.Layout(table)
println "table", tl.width, tl.height
each c in cells:
l := t.Layout(c)
println "cell", c, l.x, l.y, l.width, l.height
end
cl := t.Layout(caption)
println "caption", cl.y, cl.width
mut fixed := t.GetStyle(table)
fixed.table_layout = cssbox.TableLayout.Fixed
fixed.width = cssbox.Px(300.0)
fixed.border_collapse = cssbox.BorderCollapse.Collapse
t.SetStyle(table, fixed)
t.Compute(table, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
each c in cells:
l := t.Layout(c)
println "fixed", c, l.x, l.width, l.border.left
end
mut first := t.GetStyle(cells[0])
first.border_kinds = cssbox.BorderKinds { top = cssbox.BorderKind.Hidden, right = cssbox.BorderKind.Double, bottom = cssbox.BorderKind.Solid, left = cssbox.BorderKind.Dashed }
t.SetStyle(cells[0], first)
t.Compute(table, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
mut segs := t.CollapsedBorders(table)
defer segs.free()
println "segments", segs.len()
s0 := segs[0]
println "segment", s0.x, s0.y, s0.width, s0.height, s0.node, s0.side, s0.vertical
endBorderCollapse
enum BorderCollapseborder-collapse property.| Name | Type | Description |
|---|---|---|
Separate | | Every cell keeps its own border and border-spacing separates them. |
Collapse | | Neighbouring cells, rows, row groups, columns, column groups and the table share one border per edge, chosen by the CSS conflict rules (see BorderKind); each cell's layout border is half of the winning width on each of its sides, and spacing is zero. |
The example for this item is shown with the ones above: gui-cssbox-table.salam
CaptionSide
enum CaptionSidecaption-side property.| Name | Type | Description |
|---|---|---|
Top | | The caption is placed above the table. |
Bottom | | The caption is placed below the table. |
The example for this item is shown with the ones above: gui-cssbox-table.salam
VerticalAlign
enum VerticalAlignvertical-align property as it applies to table cells.| Name | Type | Description |
|---|---|---|
Baseline | | The first line of the cell lines up with the other baseline cells of the row. |
Top | | The content sits at the top of the cell. |
Middle | | The content is centred vertically in the cell. |
Bottom | | The content sits at the bottom of the cell. |
The example for this item is shown with the ones above: gui-cssbox-table.salam
Position
enum Positionposition property.| Name | Type | Description |
|---|---|---|
Static | | Normal flow; insets are ignored. |
Relative | | Normal flow, then moved by its insets; it is a containing block for absolute descendants. |
Absolute | | Out of flow, placed against the nearest positioned ancestor, or the viewport when there is none. |
Fixed | | Out of flow, placed against the viewport given to Compute. |
Sticky | | Laid out like Relative for now; scrolling offsets are not applied. |
The example for this item is shown with the ones above: gui-cssbox-block.salam
BoxSizing
enum BoxSizingwidth and height include padding and border.| Name | Type | Description |
|---|---|---|
ContentBox | | Sizes are for the content box (the CSS default). |
BorderBox | | Sizes include padding and border. |
The example for this item is shown with the ones above: gui-cssbox-block.salam
Overflow
enum Overflowoverflow value for one axis.| Name | Type | Description |
|---|---|---|
Visible | | Content may overflow and counts toward the parent's scrollable overflow. |
Hidden | | A scroll container without a scrollbar; its automatic minimum size is zero. |
Clip | | Content is clipped, but the box is not a scroll container. |
Scroll | | A scroll container that always reserves scrollbar_width for its scrollbar. |
Auto | | A scroll container; layout treats it like Hidden and reserves no gutter. |
The example for this item is shown with the ones above: gui-cssbox-Style.salam
Direction
enum Directiondirection property.| Name | Type | Description |
|---|---|---|
Ltr | | Left to right. |
Rtl | | Right to left: block children start on the right, flex rows run from the right, and start and end swap. |
import gui.cssbox
func label(chars: int, mi: cssbox.MeasureInput): cssbox.MeasureOutput:
full := (chars as f64) * 8.0
if mi.available_width.kind == cssbox.SpaceKind.MinContent:
ret cssbox.MeasuredWithBaseline(40.0, 32.0, 12.0)
end
if mi.available_width.kind == cssbox.SpaceKind.Definite and mi.available_width.value < full:
ret cssbox.MeasuredWithBaseline(mi.available_width.value, 32.0, 12.0)
end
ret cssbox.MeasuredWithBaseline(full, 16.0, 12.0)
end
func main:
mut t := cssbox.Tree {}
defer t.Free()
bar := t.AddNode(cssbox.Style { display = cssbox.Display.Flex, width = cssbox.Px(400.0), padding = cssbox.AllSides(cssbox.Px(8.0)), column_gap = cssbox.Px(8.0), align_items = cssbox.Align.Baseline, justify_content = cssbox.Align.SpaceBetween })
title := t.AddNode(cssbox.Style { flex_grow = 1.0, flex_shrink = 1.0, flex_basis = cssbox.Auto() })
t.SetMeasure(title, (mi: cssbox.MeasureInput): ret label(30, mi) end)
button := t.AddNode(cssbox.Style { padding = cssbox.SidesOf(cssbox.Px(4.0), cssbox.Px(12.0), cssbox.Px(4.0), cssbox.Px(12.0)), border = cssbox.AllSides(cssbox.Px(1.0)) })
t.SetMeasure(button, (mi: cssbox.MeasureInput): ret label(4, mi) end)
badge := t.AddNode(cssbox.Style { width = cssbox.Px(24.0), height = cssbox.Px(24.0), align_self = cssbox.Align.Center, order = -1 })
t.AddChild(bar, title)
t.AddChild(bar, button)
t.AddChild(bar, badge)
t.Compute(bar, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
each id in [bar, badge, title, button]:
l := t.Layout(id)
println id, l.x, l.y, l.width, l.height
end
rtl := t.GetStyle(bar)
mut s := rtl
s.direction = cssbox.Direction.Rtl
t.SetStyle(bar, s)
t.Compute(bar, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
println "rtl badge x", t.Layout(badge).x, "title x", t.Layout(title).x
endFlexDirection
enum FlexDirectionflex-direction property.| Name | Type | Description |
|---|---|---|
Row | | Items run along the inline axis. |
RowReverse | | A row in reverse order. |
Column | | Items run top to bottom. |
ColumnReverse | | A column in reverse order. |
The example for this item is shown with the ones above: gui-cssbox-flex.salam
FlexWrap
enum FlexWrapflex-wrap property.| Name | Type | Description |
|---|---|---|
NoWrap | | All items stay on one line. |
Wrap | | Items move to a new line when the line is full. |
WrapReverse | | Like Wrap, with lines stacked in the reverse cross direction. |
The example for this item is shown with the ones above: gui-cssbox-flex.salam
Align
enum Alignjustify-content, align-items, align-self and align-content.| Name | Type | Description |
|---|---|---|
Auto | | For align_self: use the container's align_items. |
Normal | | The CSS normal value: stretch for items, flex-start for content. |
Start | | The start edge of the axis in the writing direction. |
End | | The end edge of the axis in the writing direction. |
FlexStart | | The start edge of the flex direction. |
FlexEnd | | The end edge of the flex direction. |
Center | | Centred. |
Baseline | | Items share the first baseline reported by their measure callbacks. |
Stretch | | Fill the line or the container. |
SpaceBetween | | Free space goes between items; falls back to the start when there is none. |
SpaceAround | | Free space goes around each item; falls back to the start when negative. |
SpaceEvenly | | Equal gaps between and around items; falls back to the start when negative. |
The example for this item is shown with the ones above: gui-cssbox-flex.salam
SpaceKind
enum SpaceKind| Name | Type | Description |
|---|---|---|
Definite | | A known number of pixels. |
MinContent | | Lay out as narrow as possible, to find the min-content size. |
MaxContent | | Lay out with unlimited room, to find the max-content size. |
The example for this item is shown with the ones above: gui-cssbox-Style.salam
BorderKind
enum BorderKindhidden wins, then the wider border, then the style in the order double, solid, dashed, dotted, ridge, outset, groove, inset, then the box (cell, row, row group, column, column group, table), then the start or top one.| Name | Type | Description |
|---|---|---|
Solid | | solid; the default, so a style that sets only widths behaves as before. |
None | | none: the side takes no part in collapsing. |
Hidden | | hidden: wins every conflict and removes the shared border. |
Inset | | inset; ranked as ridge when collapsing, as CSS says. |
Groove | | groove. |
Outset | | outset; ranked as groove when collapsing. |
Ridge | | ridge. |
Dotted | | dotted. |
Dashed | | dashed. |
Double | | double: the highest ranked style. |
The example for this item is shown with the ones above: gui-cssbox-table.salam
Length
struct Length| Name | Type | Description |
|---|---|---|
kind | LengthKind | What the length is. |
value | f64 = 0 | Pixels for Px, the CSS percentage for Percent, unused otherwise. |
The example for this item is shown with the ones above: gui-cssbox-Style.salam
Sides
struct Sides| Name | Type | Description |
|---|---|---|
top | Length | The top side. |
right | Length | The right side. |
bottom | Length | The bottom side. |
left | Length | The left side. |
The example for this item is shown with the ones above: gui-cssbox-Style.salam
Edges
struct EdgesBoxLayout.| Name | Type | Description |
|---|---|---|
top | f64 = 0 | The top edge in pixels. |
right | f64 = 0 | The right edge in pixels. |
bottom | f64 = 0 | The bottom edge in pixels. |
left | f64 = 0 | The left edge in pixels. |
The example for this item is shown with the ones above: gui-cssbox-block.salam
BorderKinds
struct BorderKinds| Name | Type | Description |
|---|---|---|
top | BorderKind | The top side. |
right | BorderKind | The right side. |
bottom | BorderKind | The bottom side. |
left | BorderKind | The left side. |
The example for this item is shown with the ones above: gui-cssbox-table.salam
CollapsedBorder
struct CollapsedBorder| Name | Type | Description |
|---|---|---|
x | f64 = 0 | Left edge of the segment. |
y | f64 = 0 | Top edge of the segment. |
width | f64 = 0 | Width of the segment. |
height | f64 = 0 | Height of the segment. |
node | int = -1 | The node (cell, row, row group, column, column group or table) whose border won. |
side | int = 0 | Which side of that node won: 0 top, 1 right, 2 bottom, 3 left. |
vertical | bool = false | Whether the segment runs along a column line. |
The example for this item is shown with the ones above: gui-cssbox-table.salam
Style
struct Style| Name | Type | Description |
|---|---|---|
display | Display | display, default block. |
position | Position | position, default static. |
inset | Sides | top, right, bottom and left, default auto. |
box_sizing | BoxSizing | box-sizing, default content-box. |
width | Length | width, default auto. |
height | Length | height, default auto. |
min_width | Length | min-width; auto means no minimum except the flex automatic minimum. |
min_height | Length | min-height. |
max_width | Length | max-width; auto means none. |
max_height | Length | max-height; auto means none. |
aspect_ratio | f64 = 0 | aspect-ratio as width divided by height; zero means none. |
margin | Sides | margin; auto margins centre blocks and absorb free space in flex. |
padding | Sides | padding. |
border | Sides | Border widths; percentages resolve against the containing block width. |
overflow_x | Overflow | overflow-x. |
overflow_y | Overflow | overflow-y. |
scrollbar_width | f64 = 0 | Width reserved for a scrollbar when overflow is Scroll, default zero. |
direction | Direction | direction, default left to right. |
flex_direction | FlexDirection | flex-direction. |
flex_wrap | FlexWrap | flex-wrap. |
flex_grow | f64 = 0 | flex-grow, default 0. |
flex_shrink | f64 = 1 | flex-shrink, default 1. |
flex_basis | Length | flex-basis, default auto. |
justify_content | Align | justify-content for flex containers. |
align_items | Align | align-items. |
align_self | Align | align-self; also aligns an absolute box between two vertical insets. |
align_content | Align | align-content for wrapped flex lines and for block containers. |
row_gap | Length | row-gap. |
column_gap | Length | column-gap. |
order | int = 0 | order of a flex item. |
justify_items | Align | justify-items of a grid container: the default inline alignment of its items. |
justify_self | Align | justify-self of a grid item, or of an absolutely positioned box. |
grid_template_columns | str = "" | grid-template-columns in CSS syntax, such as "100px [main] repeat(auto-fill, minmax(4em, 1fr))". Lengths must already be pixels or percentages. Empty means none. |
grid_template_rows | str = "" | grid-template-rows in CSS syntax. Empty means none. |
grid_template_areas | str = "" | grid-template-areas as quoted rows, such as "'head head' 'side main'". Each area makes the lines name-start and name-end. |
grid_auto_columns | str = "" | grid-auto-columns: the sizes of implicit columns, cycled. Empty means auto. |
grid_auto_rows | str = "" | grid-auto-rows: the sizes of implicit rows, cycled. Empty means auto. |
grid_auto_flow | GridAutoFlow | grid-auto-flow of a grid container. |
grid_row_start | str = "" | grid-row-start of a grid item: a line number, a negative number from the end, span 2, a line or area name, or empty for auto. |
grid_row_end | str = "" | grid-row-end of a grid item. |
grid_column_start | str = "" | grid-column-start of a grid item. |
grid_column_end | str = "" | grid-column-end of a grid item. |
table_layout | TableLayout | table-layout of a table. |
border_collapse | BorderCollapse | border-collapse of a table. |
border_kinds | BorderKinds | The line style of each border side, for resolving collapsed table borders. |
border_spacing_x | f64 = 0 | The horizontal border-spacing of a table, in pixels. |
border_spacing_y | f64 = 0 | The vertical border-spacing of a table, in pixels. |
caption_side | CaptionSide | caption-side of a table caption. |
vertical_align | VerticalAlign | vertical-align of a table cell. |
col_span | int = 1 | How many columns a cell spans (the HTML colspan), or how many columns a column element stands for (the HTML span). |
row_span | int = 1 | How many rows a cell spans (the HTML rowspan); 0 means to the end of its row group. |
The example for this item is shown with the ones above: gui-cssbox-Style.salam
Space
struct SpaceCompute and to measure callbacks.| Name | Type | Description |
|---|---|---|
kind | SpaceKind | Definite, min-content or max-content. |
value | f64 = 0 | The pixels when the kind is definite. |
The example for this item is shown with the ones above: gui-cssbox-Style.salam
MeasureInput
struct MeasureInput| Name | Type | Description |
|---|---|---|
node | int = 0 | The node being measured. |
known_width | f64 = 0 | The fixed width when has_known_width is true. |
has_known_width | bool = false | True when the width is already decided and the callback only needs the height. |
known_height | f64 = 0 | The fixed height when has_known_height is true. |
has_known_height | bool = false | True when the height is already decided. |
available_width | Space | The width available: definite, min-content or max-content. |
available_height | Space | The height available. |
The example for this item is shown with the ones above: gui-cssbox-flex.salam
MeasureOutput
struct MeasureOutput| Name | Type | Description |
|---|---|---|
width | f64 = 0 | The content width. |
height | f64 = 0 | The content height. |
baseline | f64 = 0 | The first baseline, measured from the top of the content box. |
has_baseline | bool = false | True when baseline is set. |
The example for this item is shown with the ones above: gui-cssbox-flex.salam
BoxLayout
struct BoxLayout| Name | Type | Description |
|---|---|---|
x | f64 = 0 | Border box left edge relative to the parent's border box. |
y | f64 = 0 | Border box top edge relative to the parent's border box. |
abs_x | f64 = 0 | Left edge relative to the root. |
abs_y | f64 = 0 | Top edge relative to the root. |
width | f64 = 0 | Border box width. |
height | f64 = 0 | Border box height. |
content_x | f64 = 0 | Content box left edge inside the border box. |
content_y | f64 = 0 | Content box top edge inside the border box. |
content_width | f64 = 0 | Content box width, after padding, border and scrollbar. |
content_height | f64 = 0 | Content box height. |
padding | Edges | Resolved padding. |
border | Edges | Resolved border widths. |
margin | Edges | Resolved margins, with auto margins filled in. |
scrollbar_width | f64 = 0 | Width taken by a vertical scrollbar. |
scrollbar_height | f64 = 0 | Height taken by a horizontal scrollbar. |
scroll_width | f64 = 0 | Width of the scrollable overflow, at least the padding box width. |
scroll_height | f64 = 0 | Height of the scrollable overflow. |
baseline | f64 = 0 | First baseline from the top of the border box. |
has_baseline | bool = false | True when the box has a baseline. |
The example for this item is shown with the ones above: gui-cssbox-Style.salam
Tree
struct Treeimport gui.cssbox
func main:
mut t := cssbox.Tree {}
defer t.Free()
list := t.AddNode(cssbox.Style { display = cssbox.Display.Flex, flex_direction = cssbox.FlexDirection.Column, row_gap = cssbox.Px(4.0) })
mut items := Vector {} as Vector<int>
defer items.free()
repeat 3 in i:
item := t.AddNode(cssbox.Style { height = cssbox.Px(20.0) })
t.SetMeasure(item, (_mi: cssbox.MeasureInput): ret cssbox.Measured(100.0 + (i as f64) * 10.0, 20.0) end)
items.push(item)
end
t.SetChildren(list, items)
println "nodes", t.NodeCount(), "children", t.ChildCount(list), "second", t.Child(list, 1), "parent", t.Parent(items.get(0))
t.Compute(list, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
println "size", t.Layout(list).width, t.Layout(list).height, "calls", t.LayoutCalls(), "measures", t.MeasureCalls()
t.ResetCounters()
t.Compute(list, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
println "again", t.LayoutCalls(), "dirty", t.IsDirty(list)
t.SetMeasure(items.get(2), (_mi: cssbox.MeasureInput): ret cssbox.Measured(200.0, 20.0) end)
println "dirty", t.IsDirty(list), t.IsDirty(items.get(0))
t.Compute(list, cssbox.MaxContentSpace(), cssbox.MaxContentSpace())
println "width", t.Layout(list).width, "calls", t.LayoutCalls()
t.ClearMeasure(items.get(2))
t.MarkDirty(items.get(1))
t.SetRounding(false)
t.Compute(list, cssbox.MinContentSpace(), cssbox.MaxContentSpace())
println "width", t.Layout(list).width
endAddNode
func AddNode(style: Style): intThe example for this item is shown with the ones above: gui-cssbox-Tree.salam
NodeCount
func NodeCount(): intThe example for this item is shown with the ones above: gui-cssbox-Tree.salam
SetChildren
func SetChildren(node: int, children: Vector<int>)The example for this item is shown with the ones above: gui-cssbox-Tree.salam
AddChild
func AddChild(node: int, child: int)The example for this item is shown with the ones above: gui-cssbox-Tree.salam
ChildCount
func ChildCount(node: int): intThe example for this item is shown with the ones above: gui-cssbox-Tree.salam
Child
func Child(node: int, index: int): intThe example for this item is shown with the ones above: gui-cssbox-Tree.salam
Parent
func Parent(node: int): intThe example for this item is shown with the ones above: gui-cssbox-Tree.salam
GetStyle
func GetStyle(node: int): StyleThe example for this item is shown with the ones above: gui-cssbox-Tree.salam
SetStyle
func SetStyle(node: int, style: Style)The example for this item is shown with the ones above: gui-cssbox-Tree.salam
SetMeasure
func SetMeasure(node: int, measure: func(MeasureInput): MeasureOutput)The example for this item is shown with the ones above: gui-cssbox-Tree.salam
ClearMeasure
func ClearMeasure(node: int)The example for this item is shown with the ones above: gui-cssbox-Tree.salam
MarkDirty
func MarkDirty(node: int)The example for this item is shown with the ones above: gui-cssbox-Tree.salam
IsDirty
func IsDirty(node: int): boolThe example for this item is shown with the ones above: gui-cssbox-Tree.salam
SetRounding
func SetRounding(enabled: bool)The example for this item is shown with the ones above: gui-cssbox-Tree.salam
MeasureCalls
func MeasureCalls(): intThe example for this item is shown with the ones above: gui-cssbox-Tree.salam
LayoutCalls
func LayoutCalls(): intThe example for this item is shown with the ones above: gui-cssbox-Tree.salam
ResetCounters
func ResetCounters()The example for this item is shown with the ones above: gui-cssbox-Tree.salam
Compute
func Compute(root: int, width: Space, height: Space)The example for this item is shown with the ones above: gui-cssbox-Tree.salam
Layout
func Layout(node: int): BoxLayoutCompute.The example for this item is shown with the ones above: gui-cssbox-Tree.salam
CollapsedBorders
func CollapsedBorders(table: int): Vector<CollapsedBorder>Compute, one per cell edge with a visible winner, with the joints given to the winning border the way Chromium paints them. The caller frees the vector; it is empty for a table with separate borders.The example for this item is shown with the ones above: gui-cssbox-table.salam
Free
func Free()The example for this item is shown with the ones above: gui-cssbox-Tree.salam
Functions
Px
func Px(v: f64): LengthThe example for this item is shown with the ones above: gui-cssbox-Style.salam
Percent
func Percent(v: f64): LengthPercent(50.0) is 50%.The example for this item is shown with the ones above: gui-cssbox-Style.salam
Auto
func Auto(): LengthThe example for this item is shown with the ones above: gui-cssbox-Style.salam
MinContent
func MinContent(): LengthThe example for this item is shown with the ones above: gui-cssbox-Style.salam
MaxContent
func MaxContent(): LengthThe example for this item is shown with the ones above: gui-cssbox-Style.salam
FitContent
func FitContent(): LengthThe example for this item is shown with the ones above: gui-cssbox-Style.salam
Stretch
func Stretch(): LengthThe example for this item is shown with the ones above: gui-cssbox-Style.salam
AllSides
func AllSides(v: Length): SidesThe example for this item is shown with the ones above: gui-cssbox-Style.salam
SidesOf
func SidesOf(top: Length, right: Length, bottom: Length, left: Length): SidesThe example for this item is shown with the ones above: gui-cssbox-Style.salam
Definite
func Definite(v: f64): SpaceThe example for this item is shown with the ones above: gui-cssbox-Style.salam
MinContentSpace
func MinContentSpace(): SpaceThe example for this item is shown with the ones above: gui-cssbox-Style.salam
MaxContentSpace
func MaxContentSpace(): SpaceThe example for this item is shown with the ones above: gui-cssbox-Style.salam
Measured
func Measured(width: f64, height: f64): MeasureOutputThe example for this item is shown with the ones above: gui-cssbox-flex.salam
MeasuredWithBaseline
func MeasuredWithBaseline(width: f64, height: f64, baseline: f64): MeasureOutputThe example for this item is shown with the ones above: gui-cssbox-flex.salam
IsTrackList
func IsTrackList(text: str): boolgrid-template-columns or grid-template-rows value.The example for this item is shown with the ones above: gui-cssbox-grid.salam
IsTrackSizes
func IsTrackSizes(text: str): boolgrid-auto-columns or grid-auto-rows value.The example for this item is shown with the ones above: gui-cssbox-grid.salam
IsGridLine
func IsGridLine(text: str): boolgrid-row-start style placement.The example for this item is shown with the ones above: gui-cssbox-grid.salam
IsGridAreas
func IsGridAreas(text: str): boolgrid-template-areas value with rectangular areas.The example for this item is shown with the ones above: gui-cssbox-grid.salam