This document describes the remaining controls and enhancements planned for the “first complete release” of the library. It is written to be implementation-oriented: the intent is that an implementer can follow this document and add the controls with minimal design guesswork.
This document is kept for historical context. Some details may be outdated compared to the current implementation.
This spec assumes the current architecture:
Visual tree with binding dependency tracking (measure/arrange/render dependencies).Visual.GetStyle<T>() / Visual.Style(...) and record-based style types (IStyle<T>).BindableList<T> and VisualList<T> (with dependency tracking).Delegator<T> to keep fluent extension methods usable.TerminalApp.ShowWindow(...) with modal scoping via IModalVisual.Terminal.Live(...) and Terminal.Run(...) provide hosting scenarios (inline vs fullscreen).TreeViewStyle.SpaceBetweenGlyphAndText default from 2 → 1.
TreeView should be able to render classic tree “guide lines” (vertical continuation + branch joints), configurable through style.
Add to TreeViewStyle:
LineGlyphs? HierarchyLines { get; init; }
null means “no hierarchy lines”.LineGlyphs.Single (or theme.Lines, but style defaults should not depend on theme; prefer
LineGlyphs.Single and allow theme overrides via style environment).Style? HierarchyLineStyle { get; init; }
theme.BorderStyle(focused: false) or theme.MutedTextStyle() (implementation choice; keep consistent
with the library’s existing borders).Add convenience variants:
TreeViewStyle.NoLines (HierarchyLines = null)TreeViewStyle.HeavyLines (HierarchyLines = LineGlyphs.Heavy)TreeViewStyle.DoubleLines (HierarchyLines = LineGlyphs.Double)TreeView already maintains a flattened visible list. Extend the flattened list to also store enough information to draw lines without scanning siblings at render time.
Recommended representation for each visible row:
Depth : intContinuationMask : ulong (bit i = 1 means “draw a vertical line in indent column at depth i”)IsLastSibling : bool (for choosing BottomLeft vs TeeLeft)Compute these during the DFS used to build the visible list:
depth:
ContinuationMask is built from the stack (levels where ancestors have more siblings).IsLastSibling comes from “hasNextSibling at this depth”.Drawing rules (assuming IndentSize = 2):
< depth:
x = indentStart + level * IndentSize, draw lines.Vertical if ContinuationMask has bit level, else space.x+1, draw space (keeps the layout readable).depth:
x = indentStart + depth * IndentSize, draw lines.BottomLeft if IsLastSibling else lines.TeeLeft.x+1, draw lines.Horizontal.This produces the familiar output (example):
├─ Folder
│ ├─ File
│ └─ File
└─ Folder
The hierarchy lines consume the same horizontal space as indentation, so the public layout contract does not change:
IndentSize remains the single knob controlling the indentation width.IndentSize < 2 and lines are enabled, TreeView should clamp the “line indent width” to 2 internally.NoLines variant.Tooltips should be usable on any visual without attached properties (which the framework intentionally avoids).
TooltipHostIntroduce a wrapper control that owns a tooltip for a single child:
public sealed class TooltipHost : ContentVisual
{
[Bindable] public partial Visual? TooltipContent { get; set; }
[Bindable] public partial int ShowDelayMilliseconds { get; set; } = 500;
[Bindable] public partial PopupPlacement Placement { get; set; } = PopupPlacement.Below;
[Bindable] public partial int OffsetX { get; set; }
[Bindable] public partial int OffsetY { get; set; } = 1;
}
Notes:
IsEnabled = false so TerminalApp.DispatchMouseEvent will skip it and
deliver input to the underlying element.Add a VisualExtensions.Tooltip(...) convenience:
new Button("Delete").Tooltip(new Markup("[red]Permanently deletes the item[/]"));
// and/or
new Button("Delete").TooltipMarkup("[red]Permanently deletes the item[/]");
This should return a TooltipHost wrapping the target visual.
Tooltips should reuse the overlay infrastructure (window layer), but must not behave like popups/dialogs:
TooltipPopup visual that:
Popup (using the same placement rules).PopupSurface or dedicated TooltipStyle).IsEnabled = false).ShowDelay when Content is hovered (Visual.IsHovered becomes true).TooltipContent is null.TerminalApp or a static manager tied to the app).Introduce TooltipStyle:
BorderStyle patterns)Goal: show proportional parts of a whole (resource usage, KPI breakdown, category proportions), with interactivity.
public sealed class BreakdownChart : Visual
{
[Bindable] public BindableList<BreakdownSegment> Segments { get; }
[Bindable] public partial Visual? Title { get; set; }
[Bindable] public partial BreakdownLegendPlacement LegendPlacement { get; set; } = BreakdownLegendPlacement.Below;
[Bindable] public partial bool ShowPercentages { get; set; } = true;
[Bindable] public partial bool ShowValues { get; set; } = false;
// Interaction
[RoutedEvent(RoutingStrategy.Bubble)] private void OnSegmentClicked(BreakdownSegmentClickedEventArgs e) { }
}
public sealed class BreakdownSegment : IVisualElement
{
[Bindable] public partial double Value { get; set; }
[Bindable] public partial Visual? Label { get; set; }
[Bindable] public partial Color? Color { get; set; }
[Bindable] public partial Visual? Tooltip { get; set; }
}
Notes:
IVisualElement on the segment to avoid premature binding notifications while it is not attached (same pattern as
TreeNode / ProgressTask).Label and Tooltip should be visuals (avoid string-only APIs; allow composition/markup).In addition to the generator-provided fluent methods for bindable properties (e.g. Title(...), ShowValues(...)) and
the list replacement methods for Segments(...), provide a convenience helper that appends a segment in one call:
public static partial class BreakdownChartExtensions
{
public static BreakdownChart Segment(
this BreakdownChart breakdown,
double value,
Visual? label = null,
Color? color = null,
Visual? tooltip = null)
{
ArgumentNullException.ThrowIfNull(breakdown);
breakdown.Segments.Add(new BreakdownSegment
{
Value = value,
Label = label,
Color = color,
Tooltip = tooltip,
});
return breakdown;
}
}
Introduce BreakdownStyle (record):
Rune FillRune (default ' ')int SegmentGap (default 0 or 1)BreakdownLegendLayout LegendLayout (default: Compact)int LegendItemSpacing (spacing between items in Compact mode)Style? BarStyle (base style used for segments)Style? LegendStyle / Style? LegendMutedStyleIReadOnlyList<Color?>? DefaultSegmentColors (optional override; otherwise derived from scheme)
Primary/Success/Warning/Error then scheme accent colors.This control should use custom rendering for the bar portion (fast and simple), and optionally compose the legend:
OnPointerMoved to track hovered segment index (for hover style/tooltip).OnPointerPressed to click segments and raise SegmentClicked.Table or a VStack of HStacks; keep it simple (segment counts are typically small).Each segment can optionally expose a tooltip visual:
Tooltip is null:
Label + formatted value + percentage (based on style flags)The existing BarChart control is currently a useful but limited renderer (values-only). For v1, replace it entirely
with an enhanced chart that is immediately useful in real apps:
This is a deliberate breaking change (the library is not released yet). If an internal “values-only” renderer remains
useful, keep it internal (e.g. for sparklines or other primitives), but do not expose it as the public BarChart.
public sealed class BarChart : Visual
{
[Bindable] public BindableList<BarChartItem> Items { get; }
[Bindable] public partial Visual? Title { get; set; }
[Bindable] public partial ChartTitlePlacement TitlePlacement { get; set; } = ChartTitlePlacement.Above;
// Optional bounds. If null, derive from Items (min = 0, max = max(value)).
[Bindable] public partial double? Minimum { get; set; }
[Bindable] public partial double? Maximum { get; set; }
// Optional value display knobs.
[Bindable] public partial bool ShowValues { get; set; } = true;
[Bindable] public partial bool ShowPercentages { get; set; } = false;
}
public sealed class BarChartItem : IVisualElement
{
[Bindable] public partial Visual? Label { get; set; }
[Bindable] public partial double Value { get; set; }
// Optional override. If null and ShowValues is true, compute from Value using culture-aware formatting.
[Bindable] public partial Visual? ValueLabel { get; set; }
// Optional per-item bar color. If null, use BarChartStyle.DefaultBarColors (cycle) or Theme tones.
[Bindable] public partial Color? BarColor { get; set; }
}
Notes:
BarChartItem should be IVisualElement to avoid premature binding notifications while it is not attached (same
pattern as TreeNode / ProgressTask).Label/ValueLabel are visuals to allow composition and markup (avoid string-only APIs).Avoid custom text measurement and custom multi-line rendering. Compose existing controls:
Grid:
Label (Auto)Star(1))Auto) when ShowValues is trueProgressBar) where the normalized value is:
progress = (Value - min) / (max - min) clamped to [0..1]min/max resolved from bindables or derived from itemsShowPercentages is enabled, append the percentage to ValueLabel (or render as a separate column; keep v1
simple and consistent).Introduce BarChartStyle (record):
Thickness Paddingint RowSpacingStyle? LabelStyle, Style? ValueStyleProgressBarStyle BarStyle (or Style? BarStyle + glyphs if a dedicated bar renderer is used)IReadOnlyList<Color?>? DefaultBarColors (optional override; otherwise derived from theme tones/accent colors)Goal: provide a consistent, reusable validation/message surface that can be displayed above or below a control.
This is intended to replace one-off “error text under input” implementations (e.g. in NumberBox) and to be the shared
mechanism used by prompts, pickers, and any text input control.
Visual (wrapper/decorator; no attached properties).Visual (supports Markup, links, icons, etc.).Info, Warning, Error (theme-driven colors).public enum ValidationSeverity
{
Info,
Warning,
Error,
}
public enum ValidationPlacement
{
Above,
Below,
}
public readonly record struct ValidationMessage(ValidationSeverity Severity, Visual Content);
public sealed class ValidationPresenter : ContentVisual
{
[Bindable] public partial ValidationMessage? Message { get; set; }
[Bindable] public partial ValidationPlacement Placement { get; set; } = ValidationPlacement.Below;
}
Layout behavior:
Message is null, the presenter must not reserve space for the message (0 height).Provide convenience wrappers similar to Tooltip(...):
// Fixed message:
new TextBox("Port")
.Validation(new ValidationMessage(ValidationSeverity.Error, new TextBlock("Port is required.")));
// Dynamic message (recommended pattern):
var port = new State<string>("8080");
new TextBox().Text(port)
.Validate(
port.Bind.Value,
value =>
{
if (string.IsNullOrWhiteSpace(value))
{
return new(ValidationSeverity.Error, new TextBlock("Port is required."));
}
if (!int.TryParse(value, out var parsed) || parsed is < 1 or > 65535)
{
return new(ValidationSeverity.Error, new TextBlock("Port must be in [1..65535]."));
}
return null;
});
The .Validation(...) / .Validate(...) helpers should return a ValidationPresenter wrapping the original visual.
To make dynamic validation ergonomic without requiring users to manually create a derived state, provide helpers:
public static partial class ValidationExtensions
{
public static ValidationPresenter Validation(this Visual content, ValidationMessage? message);
public static ValidationPresenter Validation(this Visual content, Binding<ValidationMessage?> message);
public static ValidationPresenter Validation(this Visual content, State<ValidationMessage?> message);
// The validator is called as part of the binding system (dependency tracking).
public static ValidationPresenter Validate<T>(
this Visual content,
Binding<T> value,
Func<T, ValidationMessage?> validator,
ValidationPlacement placement = ValidationPlacement.Below);
}
Notes:
Validate<T>(...) helper can be implemented by binding Message to a computed value based on value.ValidationException and convert it to a ValidationMessage.Introduce ValidationStyle (record):
Style? InfoStyle, WarningStyle, ErrorStyle (foreground/background)Rune InfoGlyph, WarningGlyph, ErrorGlyph)If the wrapped control already has a border (e.g. BorderStyle), the validation line should not “double border” by
default; it should look like a compact status line aligned with the input.
ValidationPresenter (not ad-hoc text under the input).NumberBox<T>) should be refactored to use this shared
infrastructure in v1 by composing ValidationPresenter internally.
ValidationMessage? from its current value + validator and bind it to the presenter.TextBox) externally using .Validate(...).Goal: provide a high-level, Rich/Spectre-like prompt API for inline/live scenarios, built on top of the UI controls and binding system (not a separate prompt engine).
Terminal.Live) only.Terminal.Run instead.Provide both sync and async forms:
public static class TerminalPrompts
{
public static T Prompt<T>(TerminalPrompt<T> prompt);
public static ValueTask<T> PromptAsync<T>(TerminalPrompt<T> prompt, CancellationToken cancellationToken = default);
}
The prompt instances describe content + behavior; Prompt builds visuals and runs an inline host until completion.
TextPrompt → stringNumberPrompt<T> → T where T : struct, INumber<T> (reuse NumberBox<T> parsing and validation)MaskedPrompt → string (reuse MaskedInput)ConfirmationPrompt → bool (use Select<bool> or Switch)SelectionPrompt<T> → T (use Select<T> with template support)MultiSelectionPrompt<T> → IReadOnlyList<T> (use SelectionList<T> or SelectionList with templating)TerminalPrompt<T> should include:
Visual Message (typically markup)Visual? Help (displayed under input)Delegator<Func<T, string?>> (null means valid, else error message)TerminalLiveOptions knobs:
TerminalLoopResult.StopAndKeepVisual etc.Group/Border for framing and VStack for layout.ValidationPresenter) for “message under input” (info/warning/error).OperationCanceledException unless prompt provides a default).TerminalApp.BeginRun/EndRun and Tick() to simulate frames.Goal: select a Color (including RGB/RGBA) using a friendly UI:
ColorScheme (16 colors + derived accents)public sealed class ColorPicker : Visual
{
[Bindable] public partial Color Value { get; set; }
[Bindable] public partial bool AllowAlpha { get; set; } = true;
[Bindable] public partial bool ShowPalette { get; set; } = true;
// Optional palette override. If null and ShowPalette is true, derive from Theme.Scheme.
[Bindable] public partial IReadOnlyList<Color?>? Palette { get; set; }
}
Start with a layout that is achievable without heavy custom rendering:
Canvas or a Border with background).
Canvas) under the swatch color.Slider<int> (0..255)Slider<int> (0..255) only when AllowAlphaNumberBox<int>) for precise input#RRGGBB and #RRGGBBAA when alpha enabledGrid of clickable swatches (from Palette or theme scheme)Advanced visual pickers (wheel/field) can be v2. For v1, keep interactions straightforward but “modern” (good default styles, clear preview).
ColorPickerStyle:
Value, setting Value updates slidersThe library already supports async hosting (Terminal.LiveAsync/RunAsync) and installs a dispatcher-backed
SynchronizationContext, so await continuations naturally resume on the UI thread.
For v1, focus on practical async rather than introducing a separate async event system:
State<T> on the UI thread using Dispatcher.Invoke(...).Continue/Stop...).Optional small additions (if needed by prompts/tooltips):
TerminalApp-scoped scheduler:
Dispatcher.Post(Action) and Dispatcher.PostDelayed(TimeSpan, Action) (implemented using the app loop tick).Task.Delay in tests.If async event handlers are needed in v1, prefer adding explicit helpers (non-breaking, opt-in):
Button.Click(Func<Task>) extension that wraps the task and logs exceptions (optional).For each control/feature above:
site/docs/controls/ (or site/docs/ for non-control features).