Rendering is done through an intermediate cell buffer and a diff renderer:
CellBufferThis makes rendering:
Terminals are stateful. If you write a line and then later move the cursor and overwrite a few characters, the terminal keeps the rest of the previous frame. That makes it hard to reason about rendering correctness, clipping, and focus effects.
XenoAtom.Terminal.UI instead treats each frame as a complete 2D buffer:
CellBufferThis is the same high-level approach used by many modern retained-mode UI systems: render to an intermediate representation, then efficiently update the output.
In a typical frame, the app performs:
SizeHints given LayoutConstraints.Rectangle and position children.CellBuffer.Rendering must be side-effect free. Don’t mutate bindable state in RenderOverride.
Fullscreen and inline rendering use synchronized output to reduce tearing:
The framework uses the terminal cursor as the caret for text controls:
CellBuffer is a 2D grid of cells. Each cell is a tuple of:
Rune (glyph),Style (foreground/background/text decorations),Controls render by calling:
buffer.SetCell(x, y, rune, style)buffer.WriteText(x, y, span, style)CellBuffer coordinates are integer cell coordinates:
x is the column (0..Width-1)y is the row (0..Height-1)Most controls use their Bounds (a Rectangle) as their render region:
var rect = Bounds;
for (var y = rect.Y; y < rect.Y + rect.Height; y++)
{
for (var x = rect.X; x < rect.X + rect.Width; x++)
{
buffer.SetCell(x, y, new Rune(' '), backgroundStyle);
}
}
The buffer is clipped by the visual tree while rendering. For performance, many controls start with a quick clip check:
if (!buffer.ClipIntersects(rect))
{
return;
}
Terminals render in cells, but Unicode characters do not map 1:1 to cells:
CellBuffer.WriteText handles this by iterating text elements and measuring their cell width with
TerminalTextUtility. If an element is wider than 1 cell, the buffer marks the subsequent cell(s) as continuations.
When writing custom text rendering, prefer buffer.WriteText(...) or measure using TerminalTextUtility so that your
control behaves correctly with wide/complex Unicode.
Controls often render in layers:
CellBuffer.SetCell merges the incoming style with the existing cell style using Style.MergeUnspecified(...).
That means:
Style.None.WithForeground(...) it keeps the previous backgroundStyle.None.WithBackground(...) it keeps the previous foreground (unless you override it)This pattern is used heavily across the library (e.g. list rows fill their row background so the item visuals can inherit that style).
After rendering a frame, the diff renderer compares the new CellBuffer to the previous frame and emits only the
necessary updates.
This is why many “live” UIs can run at 60 FPS without flicker: unchanged areas produce no output.
Some situations force a full repaint (for example after a resize, or when the host can’t safely preserve previous state).
Unlike many terminal UI libraries, XenoAtom.Terminal.UI supports alpha-aware colors via Color.RgbA(r,g,b,a).
This enables modern UI effects such as:
Internally, alpha colors are blended during rendering so that stacked overlays produce stable results. The final terminal output is still a concrete color per cell (terminals don’t have real alpha), but the blending makes layered UIs look consistent.
Alpha blending happens when a cell is written.
CellBuffer.SetCell:
ColorKind.RgbA, it blends the RGBA color over the resolved destination colorRules:
ColorKind.RgbA)Default, Basic16, Indexed256) are treated as opaque and never blendedColorKind.Rgb (because terminals can’t represent alpha in SGR)Blending is applied for:
If the destination background is not RGB (e.g. a palette color), alpha is ignored and the RGBA is treated as an opaque RGB color. This keeps output deterministic across terminals with different color capabilities.
The following screenshot is generated from the ControlsDemo and shows three translucent panels overlapping. The overlap
regions are computed by the CellBuffer blending algorithm:
You can reproduce it by running the ControlsDemo and opening the Rendering → Alpha blending page.
Prefer alpha overlays for “soft” elevation, but avoid stacking many translucent layers over large areas if your UI is extremely dynamic; it increases per-cell work.
The rendering path reuses internal buffers where possible to minimize per-frame allocations.
XenoAtom.Terminal.UI can export the rendered CellBuffer to SVG with high fidelity (glyphs + colors + text styles).
This is useful for:
All SVG screenshots in these docs are generated automatically from the ControlsDemo using the same rendering
pipeline as a real app. They are not hand-made images.
When you run a fullscreen app (or any TerminalApp), you can capture the last rendered frame:
// After the app has rendered at least one frame:
var svg = app.CaptureSvg();
File.WriteAllText("screenshot.svg", svg);
You can also capture a specific visual from the current frame buffer (cropped to its arranged bounds):
var svg = app.CaptureSvg(myControl, padding: new Thickness(1));
For automation (docs/tests), TerminalAppSnapshotRenderer renders a visual tree to an in-memory terminal backend and
returns the resulting SVG:
var svg = TerminalAppSnapshotRenderer.RenderSvg(
root,
width: 120,
height: 30,
theme: Theme.ElderberryDarkSoft);
At the lowest level, CellBufferSvgExporter converts any CellBuffer to SVG:
var svg = CellBufferSvgExporter.Export(buffer, new CellBufferSvgExportOptions
{
AutoCrop = true,
Padding = new Thickness(1),
FillBackground = true,
});
CellBufferSvgExportOptions supports cropping, padding, background fill, and SVG sizing parameters so you can produce
compact screenshots that still look great.
RenderOverrideControls typically follow a predictable pattern:
Example:
protected override void RenderOverride(CellBuffer buffer)
{
var rect = Bounds;
if (rect.Width <= 0 || rect.Height <= 0 || !buffer.ClipIntersects(rect))
{
return;
}
var theme = GetTheme();
var style = GetStyle<MyControlStyle>();
var background = style.ResolveBackground(theme, HasFocus);
for (var y = rect.Y; y < rect.Y + rect.Height; y++)
{
for (var x = rect.X; x < rect.X + rect.Width; x++)
{
buffer.SetCell(x, y, new Rune(' '), background);
}
}
// Render content (children usually render after this control returns).
buffer.WriteText(rect.X + 1, rect.Y, "Hello", style.ResolveText(theme));
// Draw a simple border.
var border = style.ResolveBorder(theme, HasFocus);
buffer.SetCell(rect.X, rect.Y, new Rune('['), border);
buffer.SetCell(rect.X + rect.Width - 1, rect.Y, new Rune(']'), border);
}
Guidelines:
TerminalTextUtility (handles grapheme width).Style.None can inherit from what you wrote before).Span<T>/ReadOnlySpan<T> APIs for text and avoid allocating substrings in render loops.