turbo-editors/turbo-corepublic Fork 0
v1.0.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.

🛟 Updated. 28d5985 · on v1.0.0 · k33g · 17h ago
README.md · 78 lines · 7.1 KBmarkdown
Blame HistoryOpen raw

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-1Alt-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.
  • 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.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.
  • 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 — app has a test that no two do.
  • 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.
  • 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.
  • 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.
  • 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
# 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.

```go
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

```go
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-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.
- 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.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.
- **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 — `app` has a test that no two do.
- **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.
- **`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.
- **`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.
- 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.

```sh
make test
go test ./ui/
```