This document captures design and implementation notes for TreeView and TreeNode.
For end-user usage and examples, see TreeView.
Visual)SelectedIndex)IScrollable + ScrollModel (vertical and horizontal offsets)TreeNode structures)src/XenoAtom.Terminal.UI/Controls/TreeView.cssrc/XenoAtom.Terminal.UI/Controls/TreeNode.cssrc/XenoAtom.Terminal.UI/Styling/TreeViewStyle.cssrc/XenoAtom.Terminal.UI.Tests/TreeViewTests.cssrc/XenoAtom.Terminal.UI.Tests/TreeViewScrollViewerTests.cssamples/ControlsDemo/Demos/TreeViewDemo.csTreeView : Visual, IScrollable (sealed)Layout defaults:
Focusable = trueHorizontalAlignment = Align.StretchVerticalAlignment = Align.StretchProperties:
Roots : BindableList<TreeNode> (bindable getter)
onAdding/onRemoving hooks to attach/detach nodes.SelectedIndex : int (bindable)
-1 when the tree has no visible rows, otherwise clamped to [0 .. VisibleCount - 1].Scroll : ScrollModel (non-bindable property from IScrollable)
ScrollViewer integration.TreeNode : IVisualElement (sealed)Properties:
Header : Visual (required)Children : BindableList<TreeNode> (bindable getter)
Parent : TreeNode? (set automatically when added as a child)IsExpanded : bool (bindable)Icon : Rune? (bindable)
TreeViewStyle.ResolveIcon(...).Data : object? (bindable)
TreeViewStyle.IconResolver.Notes:
IVisualElement because it owns a BindableList<TreeNode>, but it is not itself a Visual. The
renderable surface is the Header visual.TreeView maintains:
_headers: a VisualList<Visual> containing the header visuals for all nodes currently in the tree_visible: a list of visible rows derived from expanded nodesNode headers are attached once and reused:
The visible row list is recomputed during PrepareChildren:
Roots recursively and includes children only when IsExpanded is trueTo avoid allocating temporary sets when toggling visibility, PrepareChildren:
IsVisible = falseIsVisible = true for headers that appear in _visibleImportant: this is not viewport virtualization. All expanded nodes are part of _visible, even if scrolled out of
view. Viewport virtualization could be a future improvement.
TreeView exposes a ScrollModel via IScrollable.Scroll.
TreeView updates scroll state during arrange:
Scroll.SetViewport(viewportWidth, viewportHeight)Scroll.SetExtent(extentWidth, extentHeight)Offsets:
Scroll.OffsetY selects which visible row is rendered at the top.Scroll.OffsetX is used to horizontally shift the row chrome and headers.Selection and scroll:
SelectedIndex changes, TreeView schedules "ensure selected visible"Scroll.ViewportHeight > 0), it updates the scroll offset immediatelyIntegration with ScrollViewer:
ScrollModel.Version is a bindable property that changes when viewport/extent/offset change.
TreeView both:
To avoid "read then write the same bindable in one tracking context" errors, TreeView uses the standard pattern:
PrepareChildren, copy Scroll.Version into a private bindable ScrollVersionArrangeCore and RenderOverride, read ScrollVersion (not Scroll.Version)This decouples the binding dependency read from the writes performed during arrange.
TreeView also stores MeasuredContentWidth as a private bindable so arrange can re-run when content width changes.
TreeView measures based on the visible row list and header desired widths:
FocusMarkerGlyph)SpaceBetweenGlyphAndText gapprefix + header.DesiredSize.WidthThe measured content width is stored in MeasuredContentWidth.
Height:
max(1, VisibleCount) (one row per visible node)Vertical scrollbar width reservation:
ScrollViewerStyle.ScrollBarThickness.Measure returns flexible hints (grow/shrink enabled) so it can be used as a resizable panel.
Arrange:
Each visible header is arranged to a 1-row rectangle at:
y = Bounds.Y + (rowIndex - Scroll.OffsetY)x = Bounds.X + prefixWidth - Scroll.OffsetXTreeView renders the row chrome (not the header content):
HierarchyLines (when enabled and indent size >= 2)Hierarchy line rendering uses a continuation mask to decide which ancestor levels draw a vertical connector.
TreeView does not register commands yet; interactions are implemented directly in key/pointer handlers.
There is no type-to-search in v1.
OffsetX).Mouse wheel scrolls the tree by 1 row without changing selection.
TreeViewStyle controls:
IndentSizeSpaceBetweenGlyphAndTextHierarchyLines : LineGlyphs? (default: Single; null disables)HierarchyLineStyle : Style? (default: theme border style with dim)NoLines, HeavyLines, DoubleLinesExpandedGlyph, CollapsedGlyph (non-ASCII defaults; override if needed)FocusMarkerGlyph (non-ASCII default; this is the selection marker)IconResolver : Func<object?, Rune?, Rune>?TreeNodeIcons.DocumentGlyph
TreeNodeIcons for the code pointsItem, SelectedFocused, SelectedUnfocused, DisabledResolveItemStyle(theme, enabled, selected, focused) provides theme-driven defaults when overrides are not set.TreeViewTests cover:
TreeViewScrollViewerTests cover: