XenoAtom.Terminal.UI is a retained-mode UI framework built on the unified input event stream of XenoAtom.Terminal:
This page explains how input is dispatched to visuals, how to handle it in custom controls, and how the routed event system works.
The best way to learn the patterns is to read src/XenoAtom.Terminal.UI/Visual.cs (routing) and a few controls like src/XenoAtom.Terminal.UI/Controls/Button.cs.
At runtime, TerminalApp converts raw terminal events into routed events on the visual tree.
Keyboard:
TerminalKeyEvent is received by TerminalApp.TerminalApp raises Visual.KeyDownEvent on the focused element.Visual.TextInputEvent with TextInputEventArgsVisual.PasteEvent with PasteEventArgsMouse:
TerminalApp hit-tests the input root to find a target visual.
IsHitTestVisible is false.TerminalApp walks up to the nearest visible+enabled ancestor.Visual.PointerPressedEventVisual.PointerMovedEventVisual.PointerReleasedEventVisual.PointerWheelEventThe input root can be different when a modal overlay is active (for example, a dialog or popup).
Controls opt in to focus by setting Focusable = true.
TerminalApp.TerminalApp walks up from the hit target to the nearest focusable ancestor and focuses it.
If you implement a focusable control, use HasFocus / HasFocusWithin to render focus cues and to decide what keyboard behavior to enable.
Terminal UI uses routed events so that containers can intercept input (preview) or react to child interactions (bubble).
Each routed event has a RoutingStrategy:
Direct: only invokes handlers on the target.Preview: routes from root down to the target (parents first).Bubble: routes from the target up to the root (parents last).Most pointer events use Preview | Bubble so both patterns are possible.
Routed event args derive from RoutedEventArgs and include:
Handled: when set, routing stops.OriginalSource: the visual where the event started.Source: the current visual during routing.Containers should set Handled = true when they fully consume an input gesture.
Every Visual can participate in routed events by overriding the protected virtual methods declared on Visual:
OnKeyDown(KeyEventArgs e)OnTextInput(TextInputEventArgs e)OnPaste(PasteEventArgs e)OnPointerMoved(PointerEventArgs e)OnPointerPressed(PointerEventArgs e)OnPointerReleased(PointerEventArgs e)OnPointerWheel(PointerEventArgs e)These methods are themselves routed-event dispatch points (they are annotated with [RoutedEvent] and wired by the source generator).
Disabled visuals do not participate in input routing:
IsEnabled before invoking dispatch and user handlers.This is important for composition: for example, a ScrollViewer can still scroll when the pointer is over a disabled child.
From Button:
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key is TerminalKey.Enter or TerminalKey.Space)
{
RaiseEvent(Button.ClickEvent, new ClickEventArgs());
e.Handled = true;
}
}
Routed events enable patterns like:
They also make it easier to observe interactions from higher-level containers without tightly coupling controls.
To expose an interaction 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(...))Rules:
partialvoid and take exactly one parameter (the event args type)OnClick becomes ClickEvent)Example:
public sealed partial class FancyButton : ContentVisual
{
[RoutedEvent(RoutingStrategy.Bubble)]
protected virtual void OnClick(ClickEventArgs e) { }
protected override void OnPointerReleased(PointerEventArgs e)
{
if (e.Button == TerminalMouseButton.Left)
{
RaiseEvent(ClickEvent, new ClickEventArgs());
e.Handled = true;
}
}
}
The routed event system is also used for framework events like Visual.PointerPressedEvent. Overriding the virtual method is often the simplest way to handle input inside a control.
Pointer events are dispatched to a target visual chosen by hit testing:
Bounds (arranged rectangles in cell coordinates).IsHitTestVisible = false for visuals that should never receive pointer events (for example, non-interactive overlays such as tooltips).IsVisible and IsEnabled affect dispatch:
PointerEventArgs provides several coordinate spaces:
X / Y: terminal coordinatesUiX / UiY: UI root coordinates (important in inline/live hosting where the UI is offset inside the terminal)LocalX / LocalY: coordinates relative to the event target's BoundsMouse capture ensures that a pressed control continues to receive mouse events until release, even when the pointer moves outside.
TerminalApp captures the pointer automatically on left mouse down:
Controls typically do not request capture themselves. Instead, implement press/drag/release state as bindable properties.
While a pointer is captured, TerminalApp keeps hover on the captured element to avoid hover state leaking to other visuals during a drag.
The button uses:
IsPressed to track an active pressIsPressedInside to track whether the pointer is still inside while draggingThis produces correct behavior for click-cancel (press inside, drag out, release does not click).
In fullscreen hosting, TerminalApp can open a context menu on right click if the pointer press is not handled:
ContextMenuFactory in the hovered chain.CommandPresentation.ContextMenu.Controls that handle right click themselves should set Handled = true in OnPointerPressed to prevent the default context menu behavior.
[Bindable] properties so layout/render invalidation is automatic.
IsPressed, IsOpen, SelectedIndex, ScrollModel, etc.Handled = true when appropriate.CommandBar / CommandPalette.