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.
Building one
Section titled “Building one”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 },};Running it
Section titled “Running it”// 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.
Elements
Section titled “Elements”| 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 |
Choosing a layout
Section titled “Choosing a layout”| 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.
Choosing a control
Section titled “Choosing a control”| 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.
Scrolling
Section titled “Scrolling”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.
Text that wraps
Section titled “Text that wraps”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.
Layout
Section titled “Layout”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 |
Input and focus
Section titled “Input and focus”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.
Animation
Section titled “Animation”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.
Showcases
Section titled “Showcases”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.
Big text
Section titled “Big text”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.
Textures
Section titled “Textures”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.
A surface program of your own
Section titled “A surface program of your own”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.
Commands in the demo shell
Section titled “Commands in the demo shell”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.