| title | TabControl Specs |
|---|
This document captures design and implementation notes for TabControl.
Note
For end-user usage and examples, see TabControl.
- Status: Implemented
- Primary purpose: Display one tab page at a time, with a clickable header strip used for selection, closing, and overflow navigation.
- Key goals:
- lightweight tab UI for terminal apps
- tab headers are visuals (supports icons, counters, dynamic text, etc.)
- tab pages are bindable model objects that can mutate in place
- optional close buttons with state-aware styling
- single-line overflow handling via left/right navigation buttons
- additive styling support for both the legacy compact strip and the default attached-tab layout
- Primary implementation:
src/XenoAtom.Terminal.UI/Controls/TabControl.cssrc/XenoAtom.Terminal.UI/Controls/TabPage.cssrc/XenoAtom.Terminal.UI/Styling/TabControlStyle.cs
- Tests:
src/XenoAtom.Terminal.UI.Tests/TabControlInteractionTests.cssrc/XenoAtom.Terminal.UI.Tests/TabControlRenderingTests.cssrc/XenoAtom.Terminal.UI.Tests/TabControlFeatureTests.cs
- Demo:
samples/ControlsDemo/Demos/TabControlDemo.cs
TabControl : Visual(sealed)TabPage : record class, IVisualElementTabCloseReasonTabPageClosingEventArgsTabPageClosedEventArgsTabControlLayoutMode
Focusable = trueHorizontalAlignment = Align.StretchVerticalAlignment = Align.Stretch
SelectedIndex : int(bindable)- determines which tab content is shown
- keyboard navigation skips disabled tabs
FirstVisibleIndex : int(bindable)- preferred starting tab index for the visible header window when overflow buttons are active
Tabs : IReadOnlyList<TabPage>- read-only view over the bindable internal list
AddTab(Visual header, Visual content)AddTab(TabPage page)TryCloseTab(int index)TryCloseTab(TabPage page)
TabPage is a bindable state container:
Header : VisualContent : VisualIsEnabled : boolShowCloseButton : boolData : object?RequestClosingeventClosedevent
Because TabPage implements IVisualElement, page property changes participate in dependency tracking once the page is attached to a TabControl.
TabControl attaches:
- all tab headers as children (always attached while their page is present)
- a single content root as the final child (always attached once initialized)
Only the selected content is hosted at any given time:
- an internal
ContentVisualhost (TabContentHost) contains the selectedTabPage.Content - that host may be wrapped by
TabControlStyle.TabContentTemplateFactory - in attached layout, the wrapped content is hosted below an internal separator visual that cuts a gap under the selected tab
When a bound TabPage.Header changes while attached:
- the old header visual is detached
- the new header visual is attached
When a bound TabPage.Content changes while that page is selected:
- the content host switches to the new content visual immediately
PrepareChildren:
- resolves
TabControlStyleand ensures the current content host composition exists - clears content when there are no tabs
- otherwise hosts the selected page content using a clamped
SelectedIndex
Measurement considers:
- header desired size (headers + tab padding + optional close button reserve)
- selected content desired size
- the layout-specific header chrome reserve
Close button layout reserve:
- width =
GetRuneWidth(CloseButtonRune) + CloseButtonSpacing - only applied when
TabPage.ShowCloseButtonis true
Layout-specific header sizing:
Compact: header height matches the measured header heightAttached: header height adds one chrome row above the header visuals
Arrange computes:
_headerHeight- the visible header window
- hit ranges for:
- tab selection
- per-tab close buttons
- overflow previous/next buttons
Overflow behavior:
- headers are kept on a single row of tabs
- when total header width exceeds the arranged width, overflow buttons are reserved at the far left and far right
FirstVisibleIndexdetermines where the visible window starts- selection changes can adjust
FirstVisibleIndexto keep the selected tab visible
Attached layout:
- header visuals are arranged one row below the top tab outline
- content starts below a separator row with a gap beneath the selected tab
- the selected content is not boxed by default
Compact layout:
- headers render as a flat strip
- optional content chrome comes from
TabContentTemplateFactory
Render draws:
- the header strip background
- overflow button glyphs when overflow is active
- tab chrome for the currently visible tabs
- close button glyphs for visible closable tabs
- the attached separator line when attached layout is active
Header/content text is still rendered by child visuals.
State inputs:
- tab enabled:
TabPage.IsEnabled && TabPage.Header.IsEnabled && TabPage.Content.IsEnabled - tab focused:
TabControl.HasFocus - tab selected:
index == SelectedIndex - tab hovered/pressed: tracked per header part
- close button hovered/pressed: tracked separately from the tab body
- overflow buttons have their own hover/pressed/disabled state
When focused:
Left: select previous enabled tabRight: select next enabled tab
There is no wrap-around.
Mouse interaction targets the header strip only:
- tab body click selects the tab
- close button click requests closure via
TabPage.RequestClosing - overflow buttons move
FirstVisibleIndexbackward/forward by one tab
Close requests:
TabPage.RequestClosingis raised first and may setCancel = true- if not cancelled, the page is removed and
TabPage.Closedis raised TabControl.TryCloseTab(...)uses the same lifecycle
Relevant properties:
LayoutModeGlyphsBorderCellStyle,FocusedBorderCellStyleTabPaddingStripStyleTabStyle,TabHoveredStyle,TabPressedStyle,TabSelectedStyle,TabDisabledStyleCloseButtonRune,CloseButtonSpacingCloseButtonStyle,CloseButtonHoveredStyle,CloseButtonPressedStyle,CloseButtonDisabledStyleOverflowPreviousRune,OverflowNextRuneOverflowButtonStyle,OverflowButtonHoveredStyle,OverflowButtonPressedStyle,OverflowButtonDisabledStyleTabContentTemplateFactory
Predefined styles:
TabControlStyle.Default/TabControlStyle.AttachedRounded- attached rounded tabs above a separator line
- no extra content wrapper by default
TabControlStyle.Compact- attached single-line tabs with tighter padding
TabControlStyle.Legacy- restores the original flat strip + boxed content layout
TabControlStyle.Rounded,Single,Double,Heavy,Ascii,AsciiHeavy,Dashed- compact layout presets with the corresponding content wrapper border
Default close button behavior:
- normal: inherits the resolved tab style
- hovered: error-toned background (falling back to hover/surface colors)
- pressed: error-toned pressed background
- disabled: dimmed/disabled foreground
Default overflow button behavior:
- attached layout: glyph-only affordance over the strip background
- compact layout: tab/button surface styling
TabControlInteractionTestscovers:- keyboard selection
- mouse-based tab switching
- visual headers and arrange bounds
TabControlRenderingTestscovers:- compact pressed-state rendering
- attached default rendering
- close-button hover rendering
- content wrapper templating
TabControlFeatureTestscovers:- close-button lifecycle
- cancellation
- disabled-tab interaction
- in-place page mutation
- overflow scrolling
TabPageremains a record class for compatibility, but now behaves as an attached, bindable model object.Tabsstays read-only at the public API boundary; list mutation still flows throughAddTab(...)/TryCloseTab(...).