This document captures the design and implementation details of OptionList<T>.
For end-user usage and examples, see OptionList.
IScrollable via an owned ScrollModel (vertical and horizontal offsets, no built-in scrollbars).DataTemplate<T> (display role) to build item visuals, with recycling support.public sealed partial class OptionList<T> : Visual, IScrollableItems : BindableList<T>SelectedIndex : intActivateOnClick : boolItemTemplate : DataTemplate<T>ItemIsEnabled : Delegator<Func<T, bool>>ItemSearchText : Delegator<Func<T, string?>>SelectionChanged (bubble): raised when selection changes (OldIndex/NewIndex)ItemActivated (bubble): raised when an item is activated (click/keyboard depending on configuration)Scroll : ScrollModel (owned scroll model)OptionList<T> maintains a child Visual for each item:
T is already a Visual, it is used directly.ItemTemplate (or the environment DataTemplates) in DataTemplateRole.Display.TextBlock that calls ToString() on the item.When the list rebuilds its item visuals (items version or resolved template change), removed visuals are added to a recycle pool. During rebuild, the list may reuse pooled visuals when:
DataTemplate<T>.TryUpdate is provided and returns true.DataTemplate<T>.Release (if provided) and a new visual is created.The library provides OptionListItem : Visual as a convenient row visual for menus and command-like lists:
Content : Visual? - main label/content (line 1)Shortcut : Visual? - optional shortcut area (line 1, right-aligned)Description : Visual? - optional second line, indentedSearchText : string? - optional search text for type-to-jumpOptionListItem measures:
Content width (plus optional Shortcut + gap)DescriptionIndent + Description width1 when Description is null, otherwise 2.PrepareChildren snapshots scroll state:
ScrollVersion = _scroll.VersionArrange and Render read ScrollVersion so scroll changes trigger layout/render updates without causing "read then write"
dependency loops (a common pattern for IScrollable controls in this codebase).
Measure:
markerWidth = runeWidth(OptionListStyle.MarkerGlyph) (minimum 1)prefixWidth = markerWidth + OptionListStyle.SpaceBetweenGlyphAndTextMeasuredItemHeight = the maximum item DesiredSize.Height (minimum 1)MeasuredContentWidth = prefixWidth + maxItemWidthitems.Count * MeasuredItemHeight (minimum 1).Important: the list uses a uniform item height (max across items). If you mix 1-row and 2-row items, all rows will reserve 2 rows of height.
When the available height is bounded and the content is taller than the available height, measure reserves additional width
equal to ScrollViewerStyle.ScrollBarThickness. This avoids a common loop where adding a vertical scrollbar reduces width,
which would introduce horizontal overflow.
Arrange:
MeasuredItemHeight so scrolling stays row-aligned.ViewportWidth = bounds.WidthViewportHeight = viewportItems * itemHeightExtentHeight = itemCount * itemHeightExtentWidth = max(viewportWidth, MeasuredContentWidth)OffsetY clamped and aligned to full item rows.When selection changes, the list sets a flag and scrolls in arrange so the selected row is visible:
Keyboard navigation:
ItemActivated)HoveredIndex used for hover styling.ActivateOnClick is true and the press/release happened on the selected row.OptionList uses OptionListStyle (src/XenoAtom.Terminal.UI/Styling/OptionListStyle.cs):
MarkerGlyph and spacing:
SpaceBetweenGlyphAndTextSpaceBetweenContentAndShortcut (used by OptionListItem)DescriptionIndent (used by OptionListItem)Item, SelectedFocused, SelectedUnfocused, Hovered, DisabledThe default style resolution uses theme colors (accent and hover fill) when available.
src/XenoAtom.Terminal.UI.Tests/OptionListTests.cssrc/XenoAtom.Terminal.UI.Tests/ListControlHorizontalScrollViewerTests.cs (OptionList inside ScrollViewer sizing rules)samples/ControlsDemo/Demos/OptionListDemo.cs