This document captures design and implementation notes for ListBox<T>.
For end-user usage and examples, see ListBox.
IScrollable with an internal ScrollModel, supporting both vertical and horizontal scrolling.DataTemplate<T> to create item visuals, with recycling via TryUpdate / Release to reduce allocations.ListBox<T> : Visual, IScrollable (sealed)Items : BindableList<T>
Items.Version for efficient rebuildsSelectedIndex : int
-1 when empty, otherwise [0..Items.Count-1]ItemTemplate : DataTemplate<T>
DataTemplates for role Display)Scroll : ScrollModel
ScrollModel is owned by the list box and updated from ArrangeScrollViewer) can read it to show scroll barsListBox<T> maintains:
_itemVisuals : BindableList<Visual> (children)_recyclePool : List<Visual> (detached visuals eligible for reuse)_lastItemsVersion and _lastResolvedTemplate (to avoid unnecessary rebuilds)Rebuild triggers:
Items.Version changesItemTemplate or DataTemplates fallback)Build rules per item:
T is already a Visual, it is used directly (no templating)new TextBlock(() => ToStringObject(value))DataTemplateValue<T>DataTemplate.TryUpdateDataTemplate.Release and creates a new visual from DataTemplate.DisplayPrepareChildren snapshots the scroll model version:
ScrollVersion = _scroll.VersionThis is a standard pattern in the library: Arrange/Render can write to the scroll model, so they read a separate bindable (ScrollVersion) to participate in dependency tracking without violating the “read then write in the same context” rule.
MaxWidth = Infinite, MaxHeight = 1 (one row per item).MeasuredContentWidth = maxItemWidth + 2 (marker + space).max(1, Items.Count).When the height is bounded and the list would overflow vertically, it reserves extra width for an expected vertical scroll bar:
reservedWidth = contentWidth + ScrollViewerStyle.ScrollBarThicknessThis prevents a common “bar causes overflow causes horizontal bar” feedback loop when embedded in a ScrollViewer.
Arrange updates the ScrollModel and positions each item visual:
_ = ScrollVersion (dependency)SetViewport(innerWidth, innerHeight)max(innerWidth, MeasuredContentWidth) so horizontal scroll can exist if neededmax(0, extentHeight - viewportHeight)x = innerLeft + 2 - Scroll.OffsetX (marker column + spacing)y = innerTop + (itemIndex - Scroll.OffsetY)width = extentWidth - 2height = 1RenderOverride paints the list background and selection chrome (items render themselves):
_ = ScrollVersion (dependency)Style.None get a consistent foreground/backgroundListBoxStyle.MarkerGlyph) in the marker column for the selected rowUp/Down: move selection by 1PageUp/PageDown: move by viewport heightHome/End: jump to first/last itemSelection changes set a flag so Arrange scrolls the selected row into view.
OffsetY + (UiY - Bounds.Y)Key knobs:
MarkerGlyph (default →)Item, SelectedFocused, SelectedUnfocused, Disabledsrc/XenoAtom.Terminal.UI.Tests/ListBoxInteractionTests.cssrc/XenoAtom.Terminal.UI.Tests/ListControlHorizontalScrollViewerTests.cs (ListBox inside ScrollViewer sizing rules)