This document captures design and implementation notes for TabControl.
For end-user usage and examples, see TabControl.
src/XenoAtom.Terminal.UI/Controls/TabControl.cssrc/XenoAtom.Terminal.UI/Controls/TabPage.cssrc/XenoAtom.Terminal.UI/Styling/TabControlStyle.cssrc/XenoAtom.Terminal.UI.Tests/TabControlInteractionTests.cssrc/XenoAtom.Terminal.UI.Tests/TabControlRenderingTests.cssamples/ControlsDemo/Demos/TabControlDemo.csTabControl : Visual (sealed)TabPage : record class (header/content pair)Focusable = trueHorizontalAlignment = Align.StretchVerticalAlignment = Align.StretchSelectedIndex : int (bindable)
OnKeyDown clamps changes to [0 .. Tabs.Count - 1].PrepareChildren, tab content
selection uses a clamped index even if SelectedIndex is out of range.Tabs : IReadOnlyList<TabPage>
AddTab(Visual header, Visual content)
header.Parent and content.Parent are null (tab visuals must not already be attached elsewhere).AddTab(TabPage page)
TabPage is a simple pair:
Header : VisualContent : VisualStrings are commonly used as headers/contents via implicit conversion to Visual (a string becomes a TextBlock).
TabControl attaches:
Only the selected content is hosted at any given time:
ContentVisual host (TabContentHost) contains the selected TabPage.ContentTabControlStyle.TabContentTemplateFactory)Changing the content template factory replaces the content root visual:
Content is forced to be the internal content hostPrepareChildren:
TabControlStyle and ensures a content template exists (EnsureContentTemplate(style))Math.Clamp(SelectedIndex, 0, Tabs.Count - 1)Measurement considers:
Header measurement:
header.Natural.Width + TabPadding.HorizontalContent measurement:
constraints.MaxWidth (or infinite)constraints.MaxHeight - headerHeight (or infinite)The returned hints are:
min and natural are set to the computed (clamped) size (max(headerWidth, contentWidth), headerHeight + contentHeight)max is infinite in both dimensionsArrange computes:
_headerHeight as max header desired height, clamped to [1 .. finalRect.Height]_hitRanges for each arranged tab areaHeaders are arranged left-to-right:
header.DesiredSize.Width + TabPadding.Horizontal, clamped to remaining widthContent is arranged below the header strip:
Y + _headerHeightfinalRect.Height - _headerHeightRender draws only the header strip backgrounds:
ResolveStripStyle(theme)ResolveTabStyle(...)The actual header text and tab content are rendered by child visuals.
Tab style state inputs:
TabPage.Content.IsEnabledTabControl.HasFocusrange.Index == SelectedIndexrange.Index == HoveredIndexrange.Index == PressedIndex && IsPressedInsideTabControl currently implements interactions directly in pointer/key handlers (it does not register commands yet).
When focused:
Left: SelectedIndex = max(0, SelectedIndex - 1)Right: SelectedIndex = min(Tabs.Count - 1, SelectedIndex + 1)There is no wrap-around in v1.
Mouse interaction targets the header strip only (the first _headerHeight rows).
HoveredIndex based on hit testingIsPressedInside when dragging within the same pressed tab rangePressedIndex and IsPressedInside = trueIsPressedInside is trueKey properties:
TabPadding : Thickness (horizontal padding affects layout; vertical padding is currently not applied in arrange)StripStyle : Style? (tab strip background)TabStyle, TabHoveredStyle, TabPressedStyle, TabSelectedStyle, TabDisabledStyleTabContentTemplateFactory : Func<Visual, ContentVisual?>?
Border)Default style resolution:
StripStyle ?? theme.BaseTextStyle()TabStyle ?? theme.SurfaceStyle()theme.ControlFillHover ?? theme.SurfaceAlt (background) and boldtheme.Selection (background) and boldtheme.Accent foregroundtheme.FocusBorder foreground when availableThere are several predefined TabControlStyle variants that swap the content wrapper border style (e.g. rounded/single/double/ascii).
TabControlInteractionTests covers:
TabControlRenderingTests covers:
TabContentTemplateFactory (verifies the rounded border template renders below the strip)