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):
[Bindable] for UI state[Bindable] property so dependency tracking works.App?.RequestRender() patterns).BindingAccessor/IBindings surface..Update(...)/manual wiring in samples.[Fluent].Delegator<TDelegate> so fluent methods can exist (C# property-invocation ambiguity).Visual composition over stringTextBlock, TextBox, TextArea, Markup, etc.), avoid string properties for content.Visual content (e.g. Label, Header, Content, etc.)."xyz" where a Visual is expected (implicit conversions exist) rather than new TextBlock("xyz").IVisualElement so it can bind to an app context without being a Visual.The UI invalidation system relies on tracking which bindable values were read during:
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.
App?.RequestRender() from bindable setters.BindingManager.BindableList<T> / VisualList<T> for collections that affect UI.Items(...)/Children(...) list-replacement fluent APIs.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".Align.Start for both axes unless a specific control requires stretch semantics.CellBuffer / renderer infrastructure; avoid per-cell Terminal.Write(...) style output from controls.char count.TerminalTextUtility (from XenoAtom.Terminal) for width and navigation utilities when dealing with cursor movement, trimming, wrapping, and selection.Style(...).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:
...Event identifier (RoutedEvent<TArgs>)...Routed that adds/removes handlers via Visual.AddHandler/RemoveHandlerbutton.Click(handler) and a lightweight button.Click(() => ...))Rules:
partial.void and take exactly one parameter (the event args type).OnClick(ClickEventArgs e) -> ClickEvent and ClickRouted.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); }button.Click((sender, args) => ...) / button.Click(() => ...)When the routed event is raised via RaiseEvent(...), the routing system:
OnClick on the current routing node)...RoutedRoutingStrategy (Preview then Bubble)TerminalChar.* constants for control characters (e.g., TerminalChar.CtrlC).KeyGesture parsing/printing helpers when dealing with configurable shortcuts.Terminal.Live, Terminal.Run) rather than custom terminal manipulation in controls.[Bindable]BindingAccessor, IBindings, and fluent extension methods.Delegator<TDelegate>; fluent methods expose the underlying delegate type.[Fluent][Bindable].Task.Delay based tests.samples to demonstrate new features.site/docs/ when introducing new concepts/controls.site/docs/specs and should be kept aligned with implementation.