Skip to content

UI Overview

Mike Strobel edited this page Jun 26, 2026 · 1 revision

UI framework overview

Cursorial.UI is a retained-mode, WPF/Avalonia-style UI framework for the terminal. If you've built XAML apps before, almost everything here will feel familiar — a dependency-property system, a visual and logical element tree, measure/arrange layout, panels and controls, routed input, styling, data binding, resources and theming, windowing, and animation — all sitting on top of Cursorial.Drawing's scene compositor and, below that, Cursorial.Core's input/output and capability layers. Reach for it when you want to describe a UI and let the framework own layout, rendering, focus, and the frame loop, rather than driving a cell buffer by hand.

You can author a UI entirely in C# (this page) or declaratively in markup — see XAML.

Where it sits in the stack

Cursorial.UI            ← element tree, layout, controls, styling, binding, windowing (this layer)
Cursorial.Drawing       ← scenes, brushes, pens, the cached-raster compositor
Cursorial.Rendering     ← cell buffer + diffing frame renderer
Cursorial.Core          ← input parsing, output writers, capability negotiation, session

The framework never talks to the terminal directly. It composites element scenes through Cursorial.Drawing, flattens the result into a cell buffer, and hands successive frames to Cursorial.Rendering's diffing renderer — so only the cells that actually changed hit the wire each frame.

Hello, UI

A minimal runnable app: open a session, build a root element tree, and run the frame loop until the user quits.

using Cursorial.UI;
using Cursorial.UI.Controls;
using Cursorial.Drawing.Media;
using Cursorial.Output;

var app = UIApplication.CreateBuilder()
    .WithFrameRate(60)          // target FPS for the frame loop
    .UseAlternateScreen()       // run on the alt screen, restored on exit
    .Build();

await app.RunAsync(() =>
{
    var panel = new StackPanel { Orientation = Orientation.Vertical, Spacing = 1 };

    panel.Children.Add(new TextBlock
    {
        Text = "Counter demo",
        Foreground = new SolidColorBrush(Color.FromRgb(64, 224, 208)),
    });

    var count = 0;
    var label = new TextBlock { Text = "Clicked 0 times" };
    var button = new Button { Content = "_Increment" };
    button.Click += (_, _) => label.Text = $"Clicked {++count} times";

    panel.Children.Add(button);
    panel.Children.Add(label);
    return panel;
});

RunAsync takes either a Func<UIElement> factory (shown above — built on the UI thread once the app is wired) or an already-constructed UIElement, and returns the process exit code as a Task<int>. Call app.Shutdown(exitCode) from anywhere to end the loop.

The _ in "_Increment" is an access-key mnemonic — Alt+I activates the button on a terminal whose capabilities support key-up/down tracking. See Input and focus.

The application and builder

UIApplication owns the terminal session, the screen cell buffer, the frame renderer, the dispatcher, and the frame loop. Construct it through the fluent builder:

var app = UIApplication.CreateBuilder()
    .WithFrameRate(30)               // default 30
    .UseAlternateScreen(true)        // default true
    .WithSession(existingSession)    // bring your own TerminalSession (else stdio is opened for you)
    .WithSessionOptions(options)     // negotiation / escape-timeout options when the app opens stdio
    .WithTimeProvider(timeProvider)  // inject a clock (e.g. for tests)
    .WithClickOptions(clickOptions)  // multi-click timing
    .ExitOnUnhandledCtrlC(true)      // default true
    .Build();

If you don't supply a session, Build()/RunAsync() open platform stdio in raw mode for you and restore it on disposal. Useful members on the built UIApplication:

  • RootElement — the content currently shown (set for you by RunAsync).
  • Dispatcher — the UI-thread dispatcher; marshal work back onto the UI thread with it.
  • FocusManager, InputDispatcher, AccessKeys — the input/focus services (see Input and focus).
  • Styles, Resources, Theme — app-wide styling and theming (see Styling and themes).
  • WindowManager — windows, dialogs, and popups (see Windowing).
  • Capabilities — the realized TerminalCapabilities for the session.
  • RequestRender() — ask for a repaint; Shutdown(exitCode) — end the loop.
  • Current — the ambient application instance.

UIApplication.Current is set while the loop runs, so application-level services are reachable without threading a reference through your tree.

The frame loop, in one paragraph

Each frame the application drains queued dispatcher work and resize notifications, ticks animations against a clock frozen for the frame, runs the styling pass, runs layout (a measure/arrange fixpoint that converges before anything paints), repaints only the render zones that changed, updates pointer hover, and flushes the diffed bytes to the terminal. Clean frames — nothing changed — allocate nothing and emit nothing. The loop wakes on input, on an explicit RequestRender(), on a resize, or when an animation or timer is active, and otherwise idles; you never spin it yourself.

The dependency-property system

Just like WPF/Avalonia, element state lives in dependency properties rather than plain CLR fields. This is what makes styling, data binding, animation, and value inheritance possible — a single property can have a value contributed by several sources at once, and the strongest one wins.

The base type is UIProperty, with three concrete flavors registered through static factory methods:

  • StyledProperty<T> — the common case: a settable property that participates in styling, binding, inheritance, and animation. UIProperty.Register<TOwner, T>(name, defaultValue).
  • AttachedProperty<T> — a property one type defines but is set on another (the classic Grid.Row / Canvas.Left pattern). UIProperty.RegisterAttached<TOwner, THost, T>(...).
  • DirectProperty<TOwner, T> — a lightweight property backed by a real CLR field via getter/setter delegates, for hot or high-frequency values that still need to notify and bind. UIProperty.RegisterDirect<TOwner, T>(...).

A typical registration plus its CLR accessor:

public static readonly StyledProperty<Orientation> OrientationProperty =
    UIProperty.Register<StackPanel, Orientation>(nameof(Orientation), defaultValue: Orientation.Vertical);

public Orientation Orientation
{
    get => GetValue(OrientationProperty);
    set => SetValue(OrientationProperty, value);
}

Read and write values through GetValue/SetValue on the owning object. Both have typed overloads (T GetValue<T>(StyledProperty<T>), SetValue<T>(StyledProperty<T>, T)) and untyped ones for the property base. A read-only property is registered with RegisterReadOnly and exposes a private write key; the public field is read-only to consumers.

Value precedence

Every property has one effective value, resolved from whichever contributing source has the highest priority. From strongest to weakest (the BindingPriority order):

  1. Animation — a running animation handle.
  2. LocalValue — a plain SetValue (the default priority).
  3. Style — a value from a matched style setter.
  4. Template — a default a control/data template author set on its parts.
  5. Inherited — a value flowing down the tree from an ancestor (only for properties registered to inherit).
  6. Default — the metadata default.

So a SetValue you make locally beats a style; a running animation beats your local value while it plays, then relinquishes. SetCurrentValue(property, value) is the special case that changes the effective value without becoming a local-value source — the way a control updates its own state (a slider's value, a checkbox's check) without clobbering a binding. ClearValue removes the local contribution; GetValueSource(property) tells you which lane currently wins, for diagnostics.

Property metadata carries the default value, inheritance flag, and effects — declaring that a property AffectsMeasure, AffectsArrange, or AffectsRender lets the framework invalidate exactly the right amount of work when it changes, instead of relaying out the world.

UIObject and UIElement

UIObject is the property-bearing base — it owns the value store and the GetValue/SetValue surface, and is the analog of WPF's DependencyObject. Non-visual types (brushes, styles, bindings) derive from it.

UIElement is the base of everything that participates in the tree, layout, rendering, and input — the analog of WPF's UIElement/FrameworkElement rolled together. It carries:

  • Two trees. VisualParent/VisualRoot form the visual tree (what gets laid out and painted); the logical tree (LogicalParent) follows authored containment and is what resource lookup and DataContext inheritance walk. For most plain elements the two coincide; templated controls insert visual structure that isn't logical. TemplatedParent points back to the control that templated an element.
  • Layout. Margin, alignment, size constraints, and the MeasureOverride/ArrangeOverride participation points. See Layout and panels.
  • Input and focus. Routed-event handlers, hit-testing, focus and capture, pointer state. See Input and focus.
  • DataContext. The inherited binding source — set it once on a container and descendants bind against it. See Data binding.

Namespaces

The folder/namespace split mirrors WPF's:

  • Cursorial.UI — the core: UIObject/UIElement, the property system (UIProperty and friends), element-level layout enums (Visibility, HorizontalAlignment, VerticalAlignment), render integration, hosting (UIApplication, the builder, the dispatcher), styling, and theming. (The System.Windows analog.)
  • Cursorial.UI.ControlsPanel and the panels (StackPanel, DockPanel, Grid, Canvas, WrapPanel), presenters, and every control from Control down (Button, TextBlock, CheckBox, ScrollViewer, TextBox, Menu, …), plus panel-facing enums like Orientation and Dock. (The System.Windows.Controls analog.)
  • Cursorial.UI.Input — routed events, the dispatcher's input side, FocusManager, gestures, and commands.

Brushes and pens (SolidColorBrush, gradients, Pen) live one layer down in Cursorial.Drawing.Media, and Color/Style/TextAttributes in Cursorial.Output — the same primitives the lower layers use.

The acronym UI is fully capitalized in type names: UIElement, UIProperty, UIObject, UIApplication — never UiElement.

Where to go next

  • Layout and panels — measure/arrange, StackPanel/Grid/DockPanel/Canvas, alignment and sizing.
  • UI controls — the control catalog: buttons, text, lists, menus, scrolling, pickers.
  • Styling and themes — CSS-like selectors, setters, pseudo-classes, resources, theme variants.
  • Data binding{Binding}, modes, converters, DataContext, compiled bindings.
  • XAML — author the same tree declaratively, at runtime or via the source generator.

See also

Clone this wiki locally