-
Notifications
You must be signed in to change notification settings - Fork 0
UI 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.
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.
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.
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 byRunAsync). -
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 realizedTerminalCapabilitiesfor 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.
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.
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 classicGrid.Row/Canvas.Leftpattern).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.
Every property has one effective value, resolved from whichever contributing source has the highest priority.
From strongest to weakest (the BindingPriority order):
- Animation — a running animation handle.
-
LocalValue — a plain
SetValue(the default priority). - Style — a value from a matched style setter.
- Template — a default a control/data template author set on its parts.
- Inherited — a value flowing down the tree from an ancestor (only for properties registered to inherit).
- 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 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/VisualRootform the visual tree (what gets laid out and painted); the logical tree (LogicalParent) follows authored containment and is what resource lookup andDataContextinheritance walk. For most plain elements the two coincide; templated controls insert visual structure that isn't logical.TemplatedParentpoints back to the control that templated an element. -
Layout.
Margin, alignment, size constraints, and theMeasureOverride/ArrangeOverrideparticipation 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.
The folder/namespace split mirrors WPF's:
-
Cursorial.UI— the core:UIObject/UIElement, the property system (UIPropertyand friends), element-level layout enums (Visibility,HorizontalAlignment,VerticalAlignment), render integration, hosting (UIApplication, the builder, the dispatcher), styling, and theming. (TheSystem.Windowsanalog.) -
Cursorial.UI.Controls—Paneland the panels (StackPanel,DockPanel,Grid,Canvas,WrapPanel), presenters, and every control fromControldown (Button,TextBlock,CheckBox,ScrollViewer,TextBox,Menu, …), plus panel-facing enums likeOrientationandDock. (TheSystem.Windows.Controlsanalog.) -
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.
-
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.
Cursorial.Core
Cursorial.Rendering
Drawing & Animation
Cursorial.UI
- Overview
- Layout & panels
- Controls
- Styling & themes
- Data binding
- Input & focus
- Windowing
- Animation & transitions
Declarative