Skip to content

List

A container that scrolls, and tells you when the user reaches the end of what it holds. It is for the dialog surface: a deck tile is a fixed box, and a list in one would hide content behind a gesture the deck itself uses.

ui.list

new UiList
{
Key = "results",
Fill = true,
Gap = UiSize.FromBasis(0.015),
Events = [UiEventHandler.On(UiComponentEvents.Reveal, OnRevealed)],
Children =
[
new UiRepeat<CatalogItem>
{
Key = "rows",
Items = UiValue.From<IReadOnlyList<CatalogItem>>(() => items.Value.Take(window.Value).ToList()),
KeySelector = item => item.Id,
Template = (item, _) => Row(item),
},
],
}

A scrolling list of track rows, each with a cover, a title and an artist line; the last row is cut off at the bottom edge

A windowed result list, adapted from the built-in music picker: it starts with a few rows and grows as the user scrolls.

void OnRevealed(UiEventData data)
{
if (!data.TryGetDouble(out var index)) return;
var wanted = (int)index + 25;
if (wanted > window.Value) window.Value = wanted;
}

reveal carries the index of the furthest child the user has brought into view. Append children, or do not; there are no page sizes and no “has more” flag. When you run out, append nothing - the reader asks again only once the user goes further than before.

Replacing the rows, as a new search does, starts that over: once the list holds fewer children than before, or a different child sits where the furthest one reported was, the reader reports again from what is in view, even an index at or below one it sent earlier. Grow your window only when the new index asks for more, as above, so a lower index never takes rows away. If you shrink your window but the list still shows the same children, the reader cannot tell, and asks again only beyond the furthest index it already reported.

KeySelector = item => item.Id,

Key rows by the item’s own id, never its position. Rows are patched in place as the list grows or a filter narrows it, and a positional key would re-key everything below the change.

new UiWhen
{
Key = "emptyWhen",
Condition = () => items.Value.Count == 0,
Content = () => new UiTextRun { Key = "empty", Text = "Nothing found", Role = UiComponentTextRoles.Secondary },
},

“Loading” and “empty” are your own state, so express them as ordinary children of the list rather than properties.

new UiList
{
Key = "covers",
Direction = UiComponentDirections.Horizontal,
RequiredComponentVersion = 2,
Fallback = new UiStack { Key = "coversFallback", Children = [firstCover] },
Children = covers,
}

Horizontal needs component version 2. A version 1 reader ignores direction and scrolls vertically, so ask for version 2 and carry a ui.stack fallback.

new UiList
{
Key = "chat",
Fill = true,
Anchor = UiComponentListAnchors.End,
RequiredComponentVersion = 3,
Fallback = new UiList
{
Key = "chatNewestFirst",
Fill = true,
Children = [new UiRepeat<ChatMessage> { Key = "rows", Items = newestFirst, KeySelector = m => m.Id, Template = Row }],
},
Children = [new UiRepeat<ChatMessage> { Key = "rows", Items = oldestFirst, KeySelector = m => m.Id, Template = Row }],
}

A chat or a log appends at the bottom. With Anchor = End the reader keeps the view at the end while the user is there, so each new row scrolls into view. Once the user scrolls up to read, new rows and rows removed above the view, such as the oldest messages dropping off a capped history, leave the view where it is, and the reader offers a way back to the newest row. You send nothing and receive nothing for any of this: where the view is belongs to the reader.

Only a vertical list follows its end. End needs component version 3; an older reader ignores anchor and leaves the newest row below the fold, so ask for version 3 and carry a fallback that shows the newest row first, such as the same rows in reverse order.

Property Values Default Meaning
Direction (direction) UiComponentDirections.Vertical, .Horizontal (vertical, horizontal) vertical The scroll axis; horizontal needs component version 2.
Anchor (anchor) UiComponentListAnchors.Start, .End (start, end) start Which end the view holds on to; end follows new rows at the end of a vertical list and needs component version 3.
Gap (gap) length No gap The gap between children.
Padding (padding) length No padding Inner padding on every edge.
Background (background) #rrggbb Paints nothing behind its children The list’s own fill, a literal colour like every container fill - see Colours and text.
MainSize (mainSize), Fill (fill), Answer (answer) - - Shared with every container - see Stack and layer.

Declared only where Events names it - see Events.

Event When Payload
UiComponentEvents.Reveal (reveal) The user has reached the end of what the list currently holds The index of the furthest child brought into view, as a bare number

Any element, any number, one after another along the scroll axis.

The main axis - y for a vertical list, x for a horizontal one - is unbounded: children take their natural extent along it, and their mainSize/fill are ignored on that axis. Across it, each child takes the list’s inner extent. On its own parent’s main axis a list follows the ordinary container rule: mainSize or fill if declared, otherwise its content extent. See Sizing.

  • Send reveal only if the node declares it.
  • Send at most two reveal events a second, and only for an index beyond the furthest one already sent for the same list’s current content.
  • Start that over when the list holds fewer children than at its previous paint, or when the child at the furthest index sent is gone or has a different id: the content was replaced.
  • When the throttle holds back an index the user has reached, send it as soon as the throttle allows: a user already at the end of the list may never scroll again.
  • The payload is a child index, not a page; never assume a page size.
  • A version 1 reader ignores direction and scrolls vertically - negotiation catches unknown types, not unknown values, so producers pair horizontal with version 2 and a fallback.
  • Ignore children’s mainSize and fill on the scroll axis.
  • anchor absent, start or a value you do not know: leave the view where the user put it, as always. end on a vertical list:
    • While the view is at the end of the list, keep it there: after a paint that adds or removes children, and while children grow after one, scroll to the end.
    • Once the user has scrolled away from the end, never move the view for a content change. When children above the view are removed, keep the first visible child that is still there at the same offset from the top edge.
    • Deciding whether the view is at the end is up to the user’s own scrolling: a paint, a resized box or children that grew are not the user leaving the end.
    • When a child the list did not hold before arrives last while the user is away, offer a way back to the end, such as a control that scrolls there. Removing the last child is not new content. The control must not count as a press on any node, and it goes away once the view is back at the end. A reader that cannot keep it in view, such as an engine without sticky positioning, may leave it out.
    • A horizontal list ignores anchor.
    • None of this is reported to the producer.
  • Advertise ui.list version 3 only once you follow the end; a version 2 reader ignores anchor, so producers pair end with version 3 and a fallback.