The fundamental building block is Visual:
Measure/Arrange)Controls (TextBox, Button, etc.) are Visual types. Containers (VStack, Grid, etc.) are also visuals.
A Visual instance can only live in one place in the visual tree.
If you try to reuse the same instance in two different containers, you'll get an exception similar to:
System.InvalidOperationException: The visual already has a parent.
If you need “the same UI twice”, create two visuals and bind them to the same state/model, or build a small factory method that creates a fresh visual tree each time.
Most controls provide fluent extension methods generated by the source generator:
new TextBox("Name").MinWidth(10)
Visual properties (and preferred fluent methods)Many layout/visibility-related properties are defined on Visual itself and are bindable. The source generator produces
fluent setters and binding helpers for them.
| Property | Typical fluent usage | Notes |
|---|---|---|
HorizontalAlignment |
.HorizontalAlignment(Align.Stretch) |
Start/Center/End/Stretch |
VerticalAlignment |
.VerticalAlignment(Align.Stretch) |
Start/Center/End/Stretch |
| Both alignments | .Stretch() |
Convenience for HorizontalAlignment(Stretch) + VerticalAlignment(Stretch) |
MinWidth / MinHeight |
.MinWidth(10) / .MinHeight(3) |
In cells |
MaxWidth / MaxHeight |
.MaxWidth(40) / .MaxHeight(10) |
In cells |
Margin |
.Margin(new Thickness(1)) |
Outer margin handled by the parent layout |
IsVisible |
.IsVisible(false) |
Hidden visuals don't render and don't participate in layout/input |
IsEnabled |
.IsEnabled(false) |
Usually affects input and disabled styling |
AutoFocus |
.AutoFocus(true) |
Lets TerminalApp pick an initial focus target |
The generated API also includes Bind* variants for connecting bindings directly:
var showDetails = new State<bool>(false);
var panel = new VStack()
.IsVisible(showDetails);
.BindBindable properties expose a typed Binding<T> through the generated .Bind view. This is the preferred way to connect
controls together:
var query = new State<string>("");
var box = new TextBox().Text(query);
var mirror = new TextBlock(query.Bind.Value);
See Binding for the full model.
IsEnabled and pointer hit testing are related but not identical:
IsVisible=false makes the visual effectively disappear (no layout, no render, no hit test).IsEnabled=false typically disables interaction and may affect styling, but it may still be present in the tree.IsHitTestVisible=false makes the visual transparent to pointer hit testing (useful for non-interactive overlays).There are also generic helpers:
VisualExtensions.Update(...) for dynamic updates (binding-registered callbacks)VisualExtensions.Add(...) to add children to a Panel fluentlyVisualExtensions.Style(...) to apply a style instanceXenoAtom.Terminal.UI supports retained-mode UI with optional computed subtrees:
ComputedVisual to build dynamic parts driven by state/bindings.ContentSwitcher when you want to switch between a set of pre-built visuals.When a binding changes, the framework re-runs only the required parts of:
ComputedVisualComputedVisual is a small “dynamic subtree” helper. It runs a Func<Visual?> during the prepare children pass and
tracks any binding reads performed while building the subtree. When those bindings change, only this part of the tree is
rebuilt.
This is a good fit for conditional UI, overlays, and “only show this panel when…” scenarios.
Example: a modal-like overlay driven by State<bool>:
var isOpen = new State<bool>(false);
var openButton = new Button("Open").OnClick(() => isOpen.Value = true);
var overlay = new ComputedVisual(() =>
{
if (!isOpen.Value)
{
return null;
}
return new Backdrop(
new Group
{
Content = new VStack(
new TextBlock("Hello!"),
new Button("Close").OnClick(() => isOpen.Value = false)
).Spacing(1).Pad(new Thickness(1))
});
});
var root = new VStack(openButton, overlay);
Important: ComputedVisual is responsible for attaching/detaching the visual it produces. Don’t reuse visuals across
parents (see “One visual, one parent”).
See also: