list
A row or column whose children come from data: one each(item) call per element of items, up to limit.
Use it for anything with a count you do not know up front: workspaces, notifications, search
results, a thumbnail grid. For a fixed set of children, a row or column is
simpler.
A scrolling thumbnail grid: a vertical list of two-image rows, decoded off-thread.
local paths = state("wallpapers", { "/usr/share/backgrounds/a.jpg", "/usr/share/backgrounds/b.jpg",
"/usr/share/backgrounds/c.jpg", "/usr/share/backgrounds/d.jpg" })
-- Two per row: a vertical list of rows, keyed by the paths they hold.
local rows = paths:map(function(all)
local out = {}
for i = 1, #all, 2 do out[#out + 1] = { all[i], all[i + 1] } end
return out
end)
local grid = list {
width = 420,
height = 300,
spacing = 8,
scroll = scroll("thumbs"),
items = rows,
key = function(pair) return table.concat(pair, "\n") end,
each = function(pair)
local tiles = {}
for i, path in ipairs(pair) do
tiles[i] = image { source = path, async = true, fit = "cover", width = 206, height = 116 }
end
return row { spacing = 8, children = tiles }
end,
}
return grid
Keyed workspace buttons from a capability: workspaces cookbook.
Properties
list takes the common properties, plus the ones below. It takes no
box properties: wrap it in a rect or column for a background.
| Property | Type | Default | Behaviour |
|---|---|---|---|
items | any[]|Bound | Empty | Array; bind a signal to rebuild on change. Missing or nil (a capability before its first push) is an empty list; a nil hole ends it. More than 10000 items without limit is an error |
each | fun(item: any): Node | Required | Builds a node for every built item: all of them, or near the viewport in a keyed list with scroll |
key | fun(item: any): string | None | Unique UTF-8 key per item; replaces the node’s id. Duplicates are refused. Without it items match by position |
limit | integer|Bound | None | Build at most this many items; above 10000 acts as 10000, 0 builds none |
direction | "vertical"|"horizontal"|Bound | "vertical" | Lays out as a column or a row |
spacing | number|Bound | 0 | Px between visible items along direction; negative values overlap them |
scroll | Bound | None | A scroll(name) signal; makes the list a scrolling viewport along direction (scroll) |
A list packs and aligns exactly like the row or column its direction names: its own
align_v (vertical) or align_h (horizontal) packs the items.
When items rebuild
A list keeps the items it built until something that build read changes. A pass that finds nothing
changed calls no each and reads none of the items’ signals; it lays the kept items out again,
about a third of the cost of building them. A change one item’s each read builds that item
alone; a change the list itself read builds every item, or every item near the viewport in a
long scrolling list. A signal bound
to an item’s property is not the build’s: a write to it runs that property’s getter in each item
that binds it and calls no each.
| Change | Runs again |
|---|---|
A write to items, or to a signal under a map or computed bound to it | Every item’s each |
A write to a signal key read with :get() | Every item’s each |
A new items, each or key value, a new limit, or a reload | Every item’s each |
A write to a signal an item’s each call read with :get() | That item’s each |
A write to a signal bound to a property of an item at any depth, such as background = theme or its hover | The getters that read it, no each |
| A write to anything else, even on the same surface | Nothing |
So bind a shared signal such as a theme color as the property, background = theme.bg, rather than
reading it in each with theme.bg:get(): flipping the theme then costs one getter per row, not
one each per row. A selection highlight follows the same rule: derive it per row,
background = computed({ results, selected }, ...), rather than folding the selection into
items, which builds every row on each move.
key carries each item’s state (tweens, a held image, a text field’s draft) onto its rebuilt node,
and across reorders. Cap a long list with limit (a launcher’s top 50 matches), or hide it while
closed so it freezes.
The engine sees signal reads only. An each, key or item map that reads the clock, a mutable
variable or an items table changed in place keeps what it read until a signal it read is written.
A delay or pulse builds the items that read it again on every pass while one is pending or open. What to read
instead: what a node reads again.
Long scrolling lists
A list with both key and its own scroll builds only the items within a viewport of what it
shows. The rest cost a key call when items changes, and scrolling, clamping and reveal behave
as if every item were built. The first pass after the list appears builds everything once, to
measure. An item scrolled further away loses its node: no animate.exit plays, and its tweens,
keyboard focus and text field draft are dropped; when it comes back, each runs again but
animate.from does not replay. Keep state that must survive in a named state.
How do I…
| Task | Answer |
|---|---|
| Lay out a grid | The example above: a list of rows, several items per row |
| Scroll a long list | Below |
| Keep items’ animations when the order changes | Give key a stable per-element string (an id from the data) |
| Show only the top N matches | limit = 50 |
| Lay items out horizontally | direction = "horizontal" |
| Filter as the user types | Bind items to a map of the query, as the textfield example does |
| Show an empty state | A sibling with visible = items:map(function(all) return not all or #all == 0 end) |
Scroll a long list
Bound the size on the scrolling axis, then bind a scroll signal.
max_height lets the list shrink to fit a few items and scroll past 200 px.
local names = {}
for i = 1, 40 do names[i] = "Item " .. i end
local items = list {
width = 240,
max_height = 200, -- grows with its items up to 200 px, then scrolls
spacing = 2,
scroll = scroll("items"),
items = names,
each = function(name)
return text { content = name, width = "fill", padding = 6 }
end,
}
Gotchas
| Trap | Fix |
|---|---|
A 2000-item list makes every update slow | Without key and scroll, every item is laid out on every pass, and built whenever anything it read changes, visible or not. Give it both so it builds only near its viewport, or cap it with limit |
A list rebuilds though nothing it shows changed | Its each or key is a new function each time the builder around it runs, as inside a function child that reads a signal. Define them once, outside the builder |
| A relative time (“3 min ago”) in an item stops updating | each read the clock with os.time(). Bind the text to a map of mantle.clock instead (what a node reads again) |
A list of more than 10000 elements is refused | Set limit, or page the items |
duplicate key error | key must return a different string for every element |
key returning a number is refused | Return a string: tostring(item.id) |
| Items lose their state when one is added at the top | Without key they match by position. Add key |
background on a list is refused | A list is not a box. Wrap it |
See also: row and column, signals, input: scroll.
Source: vocabulary, list parser, layout as row or column.