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 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.

PropertyTypeDefaultBehaviour
itemsany[]|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
eachfun(item: any): NodeRequiredBuilds a node for every built item: all of them, or near the viewport in a keyed list with scroll
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 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.

ChangeRuns again
A write to items, or to a signal under a map or computed bound to itEvery 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 reloadEvery 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 hoverThe getters that read it, no each
A write to anything else, even on the same surfaceNothing

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…

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 items 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"),
    items = names,
    each = function(name)
        return text { content = name, width = "fill", padding = 6 }
    end,
}

Gotchas

TrapFix
A 2000-item list makes every update slowWithout 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 changedIts 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 updatingeach 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 refusedSet limit, or page the items
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.