Skip to content

Interfaces

The package includes a small framework for full-screen interfaces: menus, panels, inputs, gauges, and live textures, laid out in cells. You build a tree of elements, and the same tree runs two ways: drawn straight to the screen, or written as escape sequences through any connection.

To see every element, start the demo shell and type ui: a page with each one in a titled box, down a scrolling pane. Tab moves focus, Enter selects, the wheel scrolls, and Esc returns to the shell.

An interface is a TerminalUI with a root element. Containers take their children in a collection initializer:

using DarkEffekt.Terminal;
using DarkEffekt.Terminal.UI;
var log = new LogView();
var fuel = new ProgressBar { Value = 0.4f };
var ui = new TerminalUI();
ui.Root = new Dock
{
{ Edge.Top, new Label("ENGINE ROOM") { Align = Align.Center } },
{ Edge.Bottom, new TextInput { OnSubmit = text => log.Add("> " + text) } },
{ Edge.Left, new Border("Controls") { Width = 22, Child = new Stack
{
new Label("Fuel"), fuel,
new CheckBox("Pumps") { OnChanged = on => log.Add(on ? "Pumps on." : "Pumps off.") },
new Button("Close", ui.Close),
} } },
new Border("Log") { Child = log },
};
// Straight to the screen: the fast path, for anything that animates.
session.RunSurface(new UISurfaceProgram(ui));
// As escape sequences: through any connection, in any terminal.
session.SetConnection(new LocalProgramConnection(new UIAnsiProgram(ui)));
UISurfaceProgram UIAnsiProgram
Draws cells, directly escape sequences, changed cells only
Input events decoded from bytes
The terminal underneath kept, and back when it exits on the alternate screen
Textures in a TextureView shown the slot’s name
Key releases and hover reported hover with ReportHover; no releases

Use UISurfaceProgram in a game: it costs the least, shows textures, and leaves the terminal underneath as it was. Use UIAnsiProgram when the interface has to travel as bytes, to a terminal other than this session’s.

ui.Close() ends either one, and ui.Closed reports it. A surface program can also be ended from outside with session.ExitSurface(), and session.SurfaceClosed reports the end.

Element What it is
Stack Children in a row or a column, with optional Spacing
Dock Children against the edges, and one filling the rest
Grid Children in a table of Track.Fixed and Track.Star columns and rows
Border A box around one child, with a Title
ScrollView A viewport over one child larger than the room, with bars that scroll
ScrollBar A track and thumb for a region you scroll yourself
Label Text, one row per line, with Align, Attrs, and Wrap
BigText Text drawn large in a FIGlet font
Button Runs OnClick on Enter, Space, or a click
CheckBox Toggles Checked, and reports through OnChanged
TextInput One line of editable text; OnSubmit on Enter
ListView A scrolling list of Items with a selection and a bar; OnActivate on Enter
LogView Lines that scroll as you Add more, cut off at the edge or wrapped with Wrap; the wheel scrolls back
ProgressBar A bar filled to Value, from 0 to 1, in the accent color or along a Ramp
Sparkline A graph of the values you Push, newest at the right
Paint A region you draw yourself through a CellCanvas
TextureView Room for a texture, by name
Spacer Blank room
The arrangement Use
A title bar, a status line, a side panel, and a main area Dock
Items one after another, down or across Stack
Columns that line up across rows, such as a form or a dashboard Grid
A frame and a title around anything Border
More than fits: a long form, a document, a wide table ScrollView

Containers nest. The usual shape is a Dock at the root, with a Stack or a Grid in each region.

var form = new Grid
{
Columns = { Track.Fixed(12), Track.Star() },
Rows = { Track.Fixed(1), Track.Fixed(1) },
};
form.Add(0, 0, new Label("Callsign"));
form.Add(1, 0, new TextInput());
form.Add(0, 1, new Label("Channel"));
form.Add(1, 1, new TextInput());

Add takes the column, then the row.

Track.Fixed is a count of cells, and Track.Star a share of the room left, weighted by its argument.

The player Use
Reads a line or a paragraph Label; set Wrap for a paragraph
Reads a headline BigText
Reads a running history LogView; set Capacity to bound it
Reads a level ProgressBar; set Ramp to color it along its length
Reads a trend Sparkline; set Min, Max, and a Height for a taller graph
Triggers an action Button
Turns something on or off CheckBox
Picks one of many ListView; OnSelected follows the highlight, and OnActivate fires on Enter or a second click
Types a line TextInput; OnChanged follows each edit, and OnSubmit fires on Enter
Watches a camera or a picture TextureView; set Draw to mark it up
Watches something no element draws Paint

In a ListView, the arrows, Page Up, Page Down, Home, End, and the wheel move the selection. In a TextInput, the arrows, Home, End, Backspace, Delete, a click, and a paste all work, and Placeholder shows while it is empty.

A ScrollView shows one child through a viewport and scrolls it. The content extends down by default; set Horizontal for content wider than the view, and Vertical = false to keep the child at the view’s height:

var briefing = new ScrollView(new Label(text) { Wrap = TextWrap.Words });
var table = new ScrollView(grid) { Horizontal = true, Vertical = false };

A bar appears along an edge while the content overflows that way; set VerticalBar or HorizontalBar to Always or Hidden to decide for it. The wheel scrolls, Shift and the wheel scroll sideways, the thumb drags, and a click on the track pages. From the keyboard, the arrows, Page Up, Page Down, Home, and End scroll. A view that cannot move any further leaves the wheel or the key to the view around it, so views nest.

A view takes focus only when nothing inside it can: a pane of text is a Tab stop, and a pane of buttons adds none of its own. Moving focus, by Tab, by a click, or by ui.SetFocus, brings the focused element into view through every view around it; after that the player may scroll it away and it stays away. ScrollIntoView does the same for an element of your choice, and ScrollTo, ScrollBy, OffsetX, and OffsetY move the view from script.

An element that fills its room, such as a Dock, a Grid with Star rows, or a Paint, fills the whole scrollable extent; give it a Height or a Width inside a view.

A bar’s glyphs come from a ScrollBarStyle: the track, the thumb, and an arrow for each end, which takes a cell there and moves one cell on a click. ui.ScrollBarStyle sets every bar at once, and ScrollBarStyle on a view, a list, a log, or a bar sets that one:

ui.ScrollBarStyle = ScrollBarStyle.Arrows;
briefing.ScrollBarStyle = new ScrollBarStyle { VerticalTrack = '┃', Thumb = '◆', Up = '^', Down = 'v' };
ScrollBarStyle Draws
Default A shaded track and a block thumb
Arrows The default with a triangle at each end
Line A thin line for the track and a block thumb

The colors are the interface’s: the track in Border, the thumb and the arrows in the text color, or Accent while the element the bar belongs to has focus.

A ListView shows a bar while its items overflow, and a LogView scrolls back with the wheel and follows the newest line again at the end. Each has a ScrollBar mode: Auto, Always, or Hidden; the log’s starts hidden. A ScrollBar on its own, with Value, Viewport, and Maximum, scrolls a region you draw yourself.

A line too wide for its room is cut off at the edge. Set Wrap on a Label or a LogView to break it instead:

TextWrap Breaks a line
None Nowhere. The default
Words At spaces; a word wider than the room breaks where it has to
Anywhere Wherever the room runs out
var briefing = new Label(text) { Wrap = TextWrap.Words };

A wrapped label takes the width the layout gives it and as many rows as the text needs there, so what follows it in a Stack moves down. Widths count cells: a wide character is two, and a combining mark stays with its letter.

In a Paint, canvas.Text with a rectangle draws a block the same way and returns the rows the text takes, to place what comes next:

int rows = canvas.Text(rect, text, fg, bg, new TextOptions { Wrap = TextWrap.Words, Align = Align.Center });

TextLayout.Break gives the lines themselves, and TextLayout.Measure their size, for layout of your own.

Sizes are whole cells. Every element sizes to its content unless you set Width or Height. In a Stack, an element with Grow takes a share of the room the others leave, in proportion to its value. Visible = false removes an element from layout and input, and Enabled = false dims it.

Foreground and Background set an element’s colors; an element with neither takes its parent’s. The defaults come from ui.Colors, which starts from the terminal’s own colors and palette, so an interface follows the session’s theme. Change its Text, Background, Dim, Border, Accent, Selection, and SelectionText to restyle every element at once:

ui.Colors.Accent = TermColor.Index(11);
ui.Colors.Border = TermColor.Rgb(90, 90, 90);

TermColor also builds and mixes colors: RgbUnit and FromHsv take channels from 0 to 1, Scale dims or brightens, and Lerp gives the color between two. A ColorRamp holds a row of colors to read by position, from 0 at the first to 1 at the last:

var heat = new ColorRamp(green, yellow, red) { Mix = ColorMix.Perceptual };
label.Foreground = heat.At(temperature);
fuel.Ramp = heat;

Stops are evenly spaced unless you pass positions: new ColorRamp(new[] { 0f, 0.8f, 1f }, new[] { green, green, red }) stays green for most of its length.

Mix Use it when
Srgb The default. The blend most tools show, and the cheapest
Linear Two bright colors should stay bright where they meet, as light does
Perceptual The steps should look even, or the hues are far apart, such as red to green or blue to yellow
Ease Between two stops, the color moves
Linear At an even rate
Smooth Slowly near each stop
In Slowly at first
Out Slowly at the end
Step In bands: each stop holds until the next

Tab and Shift+Tab move focus through the buttons, inputs, and lists in tree order, and a click focuses what it lands on; ui.SetFocus moves it from script. A key goes to the focused element and then up through its ancestors until one uses it. Set ui.UnhandledKey to take the keys nothing used:

ui.UnhandledKey = e =>
{
if (e.Key != TermKey.F5 || !e.IsPress) return false;
Refresh();
return true;
};

ui.CloseOnEscape = true closes the interface on Esc when nothing else used the key.

GalleryUI.Create() builds the page the demo shell’s ui command shows; its source uses every element.

ui.Tick runs every frame with the seconds since the last. Change what you like there; setting a property redraws, and only the rows that changed are drawn again.

ui.Tick += dt => fuel.Value = Mathf.PingPong(Time.time * 0.2f, 1f);

For anything the elements do not cover, draw it yourself with Paint, and call Invalidate() on it when the picture changes:

var gauge = new Paint((canvas, rect) =>
canvas.Text(rect.X, rect.Y, "RPM " + rpm, TermColor.Index(11), TermColor.Default));

The canvas draws text, fills, boxes, and bars. HBar fills a row from the left and VBar a column from the bottom, each to a fraction from 0 to 1 in eighths of a cell:

var meter = new Paint((canvas, rect) =>
canvas.VBar(rect.X, rect.Y, rect.Height, level, TermColor.Rgb(80, 220, 120), TermColor.Default));

Pass a ColorRamp in place of the color and each cell takes its color from the ramp along the bar’s whole length.

The Demo sample adds a demo command to the shell. Alone it opens a menu of full-screen interfaces; with a name it opens that one.

Command Shows
demo bridge A station dashboard: gauges, graphs, and an event log
demo radar A radar sweep with contacts to track
demo spectrum A spectrum analyzer, an oscilloscope, and level meters
demo starmap A star map with routes to plot and fly
demo dossier Two photographs in texture regions, with brackets, a scan line, and measurements drawn over them
demo boot A boot sequence with a FIGlet banner
demo rain Falling characters in truecolor

Their source is in the sample’s Showcase folder. Each builds a TerminalUI from the elements above and Paint regions, and animates in ui.Tick. ShowcaseCommand.AddTo(shell, session) adds the command to a shell of yours.

The dossier’s photographs are Dossier Front.png and Dossier Profile.png in the sample’s Showcase/Resources/DarkEffektTerminalDemo folder. Replace them with pictures of your own at 3 by 4. They import at 192 by 256 with point sampling, for the blocky look of a scan from the period; raise Max Size on the textures for more detail.

BigText draws its text in a FIGlet font, the large lettering made of ordinary characters. Put a .flf font file in the project and it imports as a FigletFontAsset; a file that is not a FIGlet font logs why in the Console. Assign the asset to a field and give its font to the element:

[SerializeField] FigletFontAsset titleFont;
var title = new BigText { Text = "ALERT", Font = titleFont.Font, Align = Align.Center };

FigletFont and FigletText are part of the emulator core and work with no interface: titleFont.Render("ALERT") gives the rows of a banner to print into a shell, and new FigletText(font) adds layout, direction, and wrapping options. For a font read at run time, FigletFont.Parse takes the file’s text and FigletFont.Load takes a stream. Zip-compressed fonts need unzipping first.

The package bundles the Standard FIGlet font. The demo shell’s figlet command and the sample interface’s title use it: FigletFontAsset.LoadBundled() returns the first font found in a Resources/DarkEffektTerminal folder.

A TextureView reserves a rectangle of cells for a texture and names it. Bind a texture to that name on the session, and it shows there, under any text drawn over it and through the screen type’s effect like the rest of the picture:

ui.Root = new Dock { { Edge.Top, new Label("CAM 2") }, new TextureView("camera") };
session.BindTexture("camera", securityCamera.targetTexture);
session.RunSurface(new UISurfaceProgram(ui));

session.BindTexture("camera", null) removes it.

Set Draw on the view to mark up the picture. Characters show over the texture, so a label, a bracket, or a reticle sits on top of it:

var feed = new TextureView("camera")
{
Draw = (canvas, rect) => canvas.Text(rect.X + 1, rect.Y, "REC", TermColor.Index(9), TermColor.Default),
};

demo dossier in the Demo sample does this over two photographs.

The framework sits on ISurfaceProgram, which you can implement directly for a display that needs no elements: a game of life, a waveform, a map. Inside an interface, a Paint element does the same job for one region, so reach for a surface program when the whole screen is one drawing. Draw into the surface each frame, then call Invalidate. Implement ITerminalInput as well to receive keys, text, and the pointer as events.

public sealed class Rain : ISurfaceProgram
{
ISurfaceHost _host;
readonly System.Random _random = new System.Random();
public void Start(ISurfaceHost host) => _host = host;
public void OnResize(int cols, int rows) { }
public void Stop() { }
public void Tick(float deltaSeconds)
{
var surface = _host.Surface;
int x = _random.Next(surface.Cols), y = _random.Next(surface.Rows);
surface.Put(x, y, '|', TermColor.Index(12), TermColor.Default);
surface.Invalidate();
}
}
session.RunSurface(new Rain());

A surface has no cursor, scrollback, or selection. The terminal and its connection keep running underneath, and show again when the program exits.

DemoShell.AddCommand adds a command of your own, which help then lists:

var shell = new DemoShell();
shell.AddCommand("map", "the station map", _ => session.RunSurface(new UISurfaceProgram(BuildMap())));
session.SetConnection(new LocalProgramConnection(shell));

A usage string replaces the name in help, arguments in capitals and optional ones in brackets: shell.AddCommand("map", "the station map", run, "map [DECK]"). AddGroup puts commands under one word, as the screen commands sit under screen and signal: the word alone lists them, help lists the group as one line, and each runs as station map 2:

var station = shell.AddGroup("station", "the station: map, crew, cargo");
station.AddCommand("map", "the station map", run, "map [DECK]");

Confirm prints a briefing and runs its action on Enter, with any other key returning to the prompt, for a command that should say what it is about to do first.