turbo-editors/turbo-corepublic Fork 0
v0.9.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

README.md · 78 lines · 7.1 KBmarkdown Blame HistoryRaw
🛟 Updated. 28d5985 k33g 17h ago1# ui
2
3The 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`.
4
5Nothing 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`.
6
7## Two conventions that explain the rest
8
9**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.
10
11**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.
12
13```go
14root := ui.NewPainter(screen) // the whole terminal
15body := root.Sub(window.InteriorBounds())
16body.Text(0, 0, "package main", style) // clipped to the window's interior
17```
18
19## The widget contract
20
21```go
22type Widget interface {
23 Bounds() Rect
24 SetBounds(r Rect)
25 Draw(p *Painter, th *theme.Theme)
26 HandleKey(ev *tcell.EventKey) bool
27 HandleMouse(ev *tcell.EventMouse) bool
28}
29```
30
31Handlers 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.
32
33The 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.
34
35## What is here
36
37| Type | What it is |
38| --- | --- |
39| `Rect` | Geometry: `Intersect`, `Inset`, `CenteredIn`, `ClampInto` |
40| `Painter` | Clipped drawing: cells, text, fills, lines, shading, cursor |
41| `Desktop` | Backdrop plus windows in back-to-front order; `Tile`, `Cascade`, `Next`, `FocusNumber`, `ToggleMaximize` |
42| `Window` | Frame, title, close box `[x]`, window number, maximise box `[■]`; movable and resizable with the mouse |
43| `MenuBar` / `Menu` / `MenuItem` | F10 and Alt-letter, arrow keys, shortcuts, separators, disabled items, one level of submenus, `Menu.OnOpen`, `MenuBar.SetMenus` |
44| `StatusBar` | Clickable `Fn` hints, a transient message, right-aligned extra text |
45| `Dialog` | Modal, with a focus ring: Tab, Shift-Tab, Escape, Enter presses the default button |
46| `Button` `InputLine` `CheckBox` `Label` `ListBox` | The controls dialogs are made of |
47
48`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.
49
50Free functions: `DrawFrame`, `DrawShadow`, `DrawLabel`, `DrawVScrollBar`, `DrawHScrollBar`, `SplitHotKey`, `MatchesHotKey`, `LabelWidth`, `PlainLabel`, `ButtonWidth`.
51
52## Turbo Vision details that are deliberate
53
54- The **active window has a double frame**, every other one a single frame, so the focused window is findable without any colour.
55- 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.
56- The window **number** in the top-right corner is what `Alt-1``Alt-9` select. 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.
57- 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.
58- **A window with nowhere to maximise into draws no maximise box.** `Desktop.Add` sets `Window.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.
59- **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.
60- **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 — `app` has a test that no two do.
61- **An item with `Items` opens 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 while `Escape` closes 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.
62- **`Menu.OnOpen` refills 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.
63- **`MenuBar.SetMenus` replaces 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.
64- A **modal dialog swallows every event**, including the ones none of its controls wants, so nothing behind it can be typed into or dragged.
65- 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.
66
67## What is where
68
69`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.
70
71## Tests
72
73Every 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.
74
75```sh
76make test
77go test ./ui/
78```