Control Development Guide (Contributor/AI/Agent Checklist)

This document captures the most important project-specific rules and conventions for implementing new controls and framework features in XenoAtom.Terminal.UI.

It is intentionally concise and opinionated: follow it unless you have a strong reason not to.

Related docs (more detail):

Prefer the framework patterns (don't "do it manually")

Use [Bindable] for UI state

  • Control state that impacts measure/arrange/render/input should usually be a [Bindable] property so dependency tracking works.
  • Prefer auto-implemented bindable properties generated by the source generator (avoid hand-written fields + App?.RequestRender() patterns).
  • If you need custom getter/setter logic, still keep it bindable and let the generator produce the BindingAccessor/IBindings surface.

Use fluent APIs everywhere

  • User code should be readable and composable.
  • Prefer fluent helpers over .Update(...)/manual wiring in samples.
  • If a type needs fluent configuration but should not be bindable, use [Fluent].
  • Delegate-valued properties must use Delegator<TDelegate> so fluent methods can exist (C# property-invocation ambiguity).

Prefer Visual composition over string

  • Except for text-specific controls (TextBlock, TextBox, TextArea, Markup, etc.), avoid string properties for content.
  • Prefer Visual content (e.g. Label, Header, Content, etc.).
  • Prefer "xyz" where a Visual is expected (implicit conversions exist) rather than new TextBlock("xyz").

Non-visual models that participate in binding

  • If you have a model object that must participate in dependency tracking, it should implement IVisualElement so it can bind to an app context without being a Visual.
  • Typical examples: items/nodes/tasks used by list/tree/progress components.

Binding + dependency tracking: how to not break it

The UI invalidation system relies on tracking which bindable values were read during:

  • Visual build/composition
  • Layout passes (Measure/Arrange)
  • Rendering
  • Input handling / state transitions

Always read bindable properties via the property, not the backing field

Bad (bypasses tracking):

// _content is a field; reading it doesn't register a dependency.
var content = _content;

Good:

var content = Content;

This rule applies across the entire codebase. If a control uses backing fields directly, it will not update correctly when values change.

Avoid manual "render requests"

  • Don't call App?.RequestRender() from bindable setters.
  • Bindable setters should be "pure" state updates + change notification through BindingManager.

Lists are observable too

  • Use BindableList<T> / VisualList<T> for collections that affect UI.
  • If you need fluent population, prefer the generated Items(...)/Children(...) list-replacement fluent APIs.
  • Avoid appending repeatedly during "dynamic updates" or rebuild cycles; list population should be idempotent.

Layout: follow the protocol

All controls must implement the current layout protocol (Measure(in LayoutConstraints) returning SizeHints, Arrange(in Rectangle)), as described in:

Key rules:

  • Natural size in SizeHints must be finite. Never return infinity as "desired size".
  • "Stretch" is a parent responsibility during arrange (flex/allocation), not something a leaf expresses by returning max constraints in measure.
  • For scrollable content, distinguish extent vs viewport (see ScrollViewer spec/implementation).

Alignment defaults

  • Terminal UI defaults are generally Align.Start for both axes unless a specific control requires stretch semantics.
  • Don't overuse alignment in demos; keep examples minimal.

Rendering: correctness first, then performance

Render through the framework primitives

  • Render to CellBuffer / renderer infrastructure; avoid per-cell Terminal.Write(...) style output from controls.
  • Keep rendering deterministic: do not mutate control state during rendering.

Text width matters (graphemes)

  • Treat text in terms of text elements (grapheme clusters), not UTF-16 char count.
  • Use TerminalTextUtility (from XenoAtom.Terminal) for width and navigation utilities when dealing with cursor movement, trimming, wrapping, and selection.

Styles

  • Use the theme/style system (see Styling) rather than hardcoding colors/glyphs.
  • Control-specific style records should contain glyphs/colors and be overridden via Style(...).

Input + events

Use routed events for UI interactions

  • Terminal input events are "raw". Controls should expose routed events (preview/bubble) where appropriate.
  • When implementing interactions that depend on press/drag/release, ensure mouse capture rules are respected (drag shouldn't leak hover to other visuals).

RoutedEventAttribute: how it works (source-generated)

To expose a routed event from a control, declare a dispatch method and mark it with [RoutedEvent]. The source generator will produce:

  • A static ...Event identifier (RoutedEvent<TArgs>)
  • An instance event ...Routed that adds/removes handlers via Visual.AddHandler/RemoveHandler
  • Fluent extension methods to register handlers (e.g. button.Click(handler) and a lightweight button.Click(() => ...))
  • The dispatch wiring so the method is invoked during routing (before instance handlers)

Rules:

  • The containing type must be partial.
  • The method must return void and take exactly one parameter (the event args type).
  • The event name is derived from the method name:
    • OnClick(ClickEventArgs e) -> ClickEvent and ClickRouted.
  • Routing strategy defaults to Bubble if not specified.

Example (typical button click):

public partial class Button : ContentVisual
{
    [RoutedEvent(RoutingStrategy.Bubble)]
    protected virtual void OnClick(ClickEventArgs e) { }

    protected override void OnKeyDown(KeyEventArgs e)
    {
        if (e.Key is TerminalKey.Enter or TerminalKey.Space)
        {
            RaiseEvent(ClickEvent, new ClickEventArgs());
            e.Handled = true;
        }
    }
}

What gets generated (conceptual):

  • public static readonly RoutedEvent<ClickEventArgs> ClickEvent = RoutedEvent.Register<Button, ClickEventArgs>(...)
  • public event EventHandler<ClickEventArgs> ClickRouted { add => AddHandler(ClickEvent, value); remove => RemoveHandler(ClickEvent, value); }
  • Fluent: button.Click((sender, args) => ...) / button.Click(() => ...)

When the routed event is raised via RaiseEvent(...), the routing system:

  1. Dispatches to the generated method callback (invokes OnClick on the current routing node)
  2. Invokes any handlers registered via ...Routed
  3. Routes according to the RoutingStrategy (Preview then Bubble)

Ctrl keys and gestures

  • Use TerminalChar.* constants for control characters (e.g., TerminalChar.CtrlC).
  • Prefer KeyGesture parsing/printing helpers when dealing with configurable shortcuts.

Hosting: fullscreen vs inline/live are different

  • Fullscreen rendering can own the whole viewport/background.
  • Inline/live must behave like a flowing console: content should scroll, cursor must end up in the right place after the live region, and resize should not corrupt the screen.
  • Prefer using the existing hosting abstractions (Terminal.Live, Terminal.Run) rather than custom terminal manipulation in controls.

Source generator expectations

[Bindable]

  • Generates storage, BindingAccessor, IBindings, and fluent extension methods.
  • For delegate properties: declare as Delegator<TDelegate>; fluent methods expose the underlying delegate type.

[Fluent]

  • Generates fluent-only extension methods for non-bindable properties.
  • Same Delegator rule as [Bindable].

Tests and demos

Tests

  • Add unit tests for new controls and for regressions.
  • Prefer deterministic "tick" driven app/test harness patterns; avoid Task.Delay based tests.
  • Use MSTest recommended asserts where available.

Samples

  • Update samples to demonstrate new features.
  • Keep demos readable: minimal nesting, strong defaults, fluent configuration.

Documentation quality

  • New public APIs require XML docs (CS1591 must stay clean).
  • Add or update user-guide docs in site/docs/ when introducing new concepts/controls.
  • Specs live in site/docs/specs and should be kept aligned with implementation.