ui
The widget framework the editor is built from: a desktop of movable windows, a menu bar with drop-downs, modal dialogs, buttons, input lines and list boxes — the Turbo Vision furniture, drawn on tcell.
Nothing here knows about Go source, buffers or language servers. Widgets draw through a clipping Painter and answer key and mouse events; what those events mean is decided by app.
Two conventions that explain the rest
Bounds are absolute screen coordinates. Every widget's Bounds() is where it really is on the terminal, so hit-testing a mouse click is a plain rectangle test and no event ever needs translating. The cost is that containers place their children in screen space — which they do anyway, since they know where they are.
Painters clip, and clipping composes. p.Sub(r) returns a painter for the screen rectangle r, clipped to r and to whatever p was already clipped to. Drawing coordinates inside a painter start at (0, 0), so a widget's own drawing code never mentions where it sits. A child can never paint outside the box its parent was given, however wrong its arithmetic.
root := ui.NewPainter(screen) // the whole terminal
body := root.Sub(window.InteriorBounds())
body.Text(0, 0, "package main", style) // clipped to the window's interior
The widget contract
type Widget interface {
Bounds() Rect
SetBounds(r Rect)
Draw(p *Painter, th *theme.Theme)
HandleKey(ev *tcell.EventKey) bool
HandleMouse(ev *tcell.EventMouse) bool
}
Handlers return whether they consumed the event; an unconsumed event travels on to whatever is behind, which is how a click that misses every control lands on the window underneath. Embed Box to get bounds and no-op handlers, or FocusBox to add the focus flag a dialog control needs.
The terminal cursor is placed by whichever focused widget calls p.ShowCursor during its own Draw. Drawing runs back to front and only focused widgets call it, so the last one to speak is the right one.
What is here
| Type | What it is |
|---|---|
Rect |
Geometry: Intersect, Inset, CenteredIn, ClampInto |
Painter |
Clipped drawing: cells, text, fills, lines, shading, cursor |
Desktop |
Backdrop plus windows in back-to-front order; Tile, Cascade, Next, FocusNumber, ToggleMaximize |
Window |
Frame, title, close box [x], window number, maximise box [■]; movable and resizable with the mouse |
MenuBar / Menu / MenuItem |
F10 and Alt-letter, arrow keys, shortcuts, separators, disabled items, one level of submenus, Menu.OnOpen, MenuBar.SetMenus |
StatusBar |
Clickable Fn hints, a transient message, right-aligned extra text |
Dialog |
Modal, with a focus ring: Tab, Shift-Tab, Escape, Enter presses the default button |
Button InputLine CheckBox Label ListBox |
The controls dialogs are made of |
Window also carries Maximize(area), Restore() and Maximized(); Desktop.ToggleMaximize is the pair of them, and is what both the frame's box and Window ▸ Maximise go through so the two can never disagree.
Free functions: DrawFrame, DrawShadow, DrawLabel, DrawVScrollBar, DrawHScrollBar, SplitHotKey, MatchesHotKey, LabelWidth, PlainLabel, ButtonWidth.
Turbo Vision details that are deliberate
- The active window has a double frame, every other one a single frame, so the focused window is findable without any colour.
- Windows cast a shadow two cells right and one below — terminal cells are about twice as tall as they are wide, so that is what looks square.
- The window number in the top-right corner is what
Alt-1…Alt-9select. It is drawn after the title, so a title too long for the frame loses a character to it rather than the other way round — which is why the margin the title reserves is checked by a test across every width rather than trusted as arithmetic. - The top frame carries two boxes:
[x]at the left closes the window,[■]at the right fills the desktop and then reads[▬], so the box always says what pressing it will do rather than what the window currently is. - A window with nowhere to maximise into draws no maximise box.
Desktop.AddsetsWindow.OnMaximize, because the desktop is what a window is maximised into and is the only thing that knows the area. A window used on its own gets no button rather than one that would do nothing. - Tiling or cascading forgets that a window was maximised, so its box goes back to offering to maximise. The rectangle it would have restored to no longer means anything once the desktop has laid it out somewhere else.
- Hot keys are written in labels with tildes:
"~F~ile","Save ~A~s…". The bar answers the first match it finds, so two menus sharing a hot key silently make one of them unreachable —apphas a test that no two do. - An item with
Itemsopens a submenu, one level deep, marked▶.→opens it or moves to the next menu when there is none, so right always means "further in";←steps back out to the parent whileEscapecloses everything, because cancel should mean cancel from anywhere. The panel is drawn beside its parent, flipped to the left when there is no room, and capped to the screen width — flipping alone cannot fit a panel wider than the terminal. Menu.OnOpenrefills a menu just before it drops down. A menu built from a file, or from what the front window holds, has no start-up moment at which its contents exist.MenuBar.SetMenusreplaces the whole bar, for the rarer case where the set of menus changes rather than one menu's contents — a project file that names menus of its own. It closes first, because the open index refers to the old slice and keeping it would drop down whichever menu landed at that position.- A modal dialog swallows every event, including the ones none of its controls wants, so nothing behind it can be typed into or dragged.
- Windows have a grow mode, as Turbo Vision's did. A document window follows the desktop's right and bottom edges, so its top-left corner stays put while its far corner keeps pace with the terminal — which is what makes the editor still fill a window you have just made larger.
SetGrow(GrowNone)opts out; a window is then only moved back into view. No window may end up larger than the desktop, whatever its grow mode. A maximised window carries the bounds it would be restored to through a resize as well, or shrinking the terminal would leave it restoring to somewhere nobody can reach.
What is where
menu.go holds the types and the bar's state, menu_draw.go where the panels go and how they are painted, menu_events.go the keyboard and the mouse, and submenu.go the whole second level. The split is by what a reader is looking for, and it is what keeps any one of them measurably simple.
Tests
Every widget is exercised through tcell.SimulationScreen — a real Screen that draws into memory — so the assertions are made on the picture that a terminal would actually show, with no rendering stubbed out.
make test
go test ./ui/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
|