Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

list

A row or column whose children come from data: one itemfn(item) call per element of source. Reach for 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"),
    source = rows,
    key = function(pair) return table.concat(pair, "\n") end,
    itemfn = 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.

PropertyTypeDefaultBehaviour
sourceany[]|BoundEmptyArray; 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
itemfnfun(item: any): NodeRequiredBuilds a node for every built item, visible or not
keyfun(item: any): stringNoneUnique UTF-8 key per item; replaces the node’s id. Duplicates are refused. Without it items match by position
limitinteger|BoundNoneBuild 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
spacingnumber|Bound0Px between visible items along direction; negative values overlap them
scrollBoundNoneA 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 itemfn 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 read builds that item alone; a change the list itself read builds every item, scrolled out of view or not.

ChangeBuilds again
A write to source, or to a signal under a map or computed bound to itEvery item
A write to a signal key read with :get()Every item
A new source, itemfn or key value, a new limit, or a reloadEvery item
A write to a signal an item’s itemfn call read with :get(), or bound to a property of that item at any depth, such as its hoverThat item
A write to anything else, even on the same surfaceNothing

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 itemfn, key or item map that reads the clock, a mutable variable or a source 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.

How do I…

TaskAnswer
Lay out a gridThe example above: a list of rows, several items per row
Scroll a long listBelow
Keep items’ animations when the order changesGive key a stable per-element string (an id from the data)
Show only the top N matcheslimit = 50
Lay items out horizontallydirection = "Horizontal"
Filter as the user typesBind source to a map of the query, as the textfield example does
Show an empty stateA 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"),
    source = names,
    itemfn = function(name)
        return text { content = name, width = "Fill", padding = 6 }
    end,
}

Gotchas

TrapFix
A 2000-item list makes every update slowEvery item is laid out on every pass, and built whenever anything it read changes, visible or not. Cap it with limit, filter the source, or hide the list while it is closed
A list rebuilds though nothing it shows changedIts itemfn 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 updatingitemfn read the clock with os.time(). Bind the text to a map of mantle.system instead (what a node reads again)
A list of more than 10000 elements is refusedSet limit, or page the source
duplicate key errorkey must return a different string for every element
key returning a number is refusedReturn a string: tostring(item.id)
Items lose their state when one is added at the topWithout key they match by position. Add key
background on a list is refusedA list is not a box. Wrap it

See also: row and column, signals, input: scroll.

Source: vocabulary, list parser, layout as row or column.