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/Controls/TreeNodeRightVisual.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].SelectedNode : TreeNode? (bindable, read-only)
SelectedIndex changes or the visible-row projection changes.Scroll : ScrollModel (non-bindable property from IScrollable)
ScrollViewer integration.Methods:
TrySelectNode(TreeNode node) : bool
IndexOfVisibleNode(TreeNode node) : int
-1 when the node is not visible.TreeNode : IVisualElement (sealed)Properties:
Header : Visual (required)Children : BindableList<TreeNode> (bindable getter)
RightVisuals : BindableList<TreeNodeRightVisual> (bindable getter)
Parent : TreeNode? (set automatically when added as a child)IsExpanded : bool (bindable)Icon : Rune? (bindable)
TreeViewStyle.ResolveIcon(...).IconStyle : Style? (bindable)
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.AddRightVisual(Visual visual)AddRightVisual(Visual visual, TreeNodeRightVisualVisibility visibility)TreeNodeRightVisual : IVisualElement (sealed)Properties:
Visual : Visual (required)Visibility : TreeNodeRightVisualVisibility (bindable)
Always: visible whenever the node row is visibleHover: visible only while the row is hoveredTreeView maintains:
_headers: a VisualList<Visual> containing the header visuals for all nodes currently in the tree_rightVisuals: a VisualList<Visual> containing right-aligned visuals for all nodes currently in the tree_visible: a list of visible rows derived from expanded nodesNode visuals are attached once and reused:
The visible row list is recomputed during PrepareChildren:
Roots recursively and includes children only when IsExpanded is trueExample:
[Root, ChildA, ChildB, Other], then SelectedIndex = 2 means ChildBRoot collapses, the visible rows become [Root, Other]To avoid allocating temporary sets when toggling visibility, PrepareChildren:
IsVisible = falseIsVisible = falseIsVisible = true for headers/right visuals that belong to rows in _visibleHover-only right visuals remain attached and measurable, but TreeView collapses them to zero-width rectangles when the row is not hovered so they stay out of rendering and hit testing without losing their measured size.
Important: 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 immediatelySelectedNode is synchronized from the selected visible row so code can react directly to the selected TreeNodeIntegration 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.Width + allRightVisualWidthsThe 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.OffsetXRight visuals are arranged against the viewport right edge for the row:
0TreeView 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>?IconStyle : Style?TreeNodeIcons.DocumentGlyph
TreeNodeIcons for the code pointsTreeNode.IconStyle overrides can further customize the rendered iconItem, SelectedFocused, SelectedUnfocused, DisabledResolveItemStyle(theme, enabled, selected, focused) provides theme-driven defaults when overrides are not set.TreeViewTests cover:
TreeViewScrollViewerTests cover:
SelectedNode / TrySelectNode(...) usage