nandi/jolt-nativepublic Fork 0
5ba95e0164dfaf9110357b5e041001f98d4f13a9
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

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

README.md · 158 lines · 7.4 KBmarkdown Blame HistoryRaw
Write dvui's shape in jolt, on SDL3, with no shared object 109c7e4 Veronika Winters 9d ago1# jvui
2
3An immediate-mode GUI toolkit in the shape of
4[dvui](https://github.com/david-vanderson/dvui), written in **jolt**, calling
5**SDL3** directly.
6
7There is no shared object here and nothing in this directory is compiled. The
8layout, the widget identity, the event routing and every widget are jolt;
9`jolt.ffi` binds libSDL3 and libSDL3_ttf the way [glimmer-gfx](../glimmer-backends/glimmer-gfx)
10binds Xlib.
11
12```clojure
13(ns myapp
14 (:require [jvui.app :as app] [jvui.widgets :as w]))
15
16(def n (atom 0))
17
18(defn render []
19 (w/page {:max-width 420}
20 (w/card {}
21 (w/title "Counter")
22 (w/label (str "Count: " @n))
23 (w/hbox {:spacing 8}
24 (when (w/button "- 1") (swap! n dec))
25 (when (w/button "+ 1" {:kind :primary}) (swap! n inc))))))
26
27(defn -main [& _] (app/run! render {:title "counter"}))
28```
29
30You call a widget and what it answers is what the person did. There is no
31widget object to hold, no handler to register and nothing to free — a button
32that is not called this frame is not on the screen, which is what the whole
33style is for.
34
35## Running
36
37```bash
38jolt test # headless: no window, no display, no SDL
39jolt counter # a window
40jolt showcase # every widget on one page
41jolt showcase --shot # three frames, then a BMP in /tmp
42```
43
44SDL3 and SDL3_ttf are *system* libraries here, not this repo's crates, and they
45must be ones the jolt binary can load — on a nix-built jolt the host
46`/usr/lib` copies are a different glibc and fail before `dlopen` returns. Any
47of the usual system faces is found automatically; `JVUI_FONT` names another.
48
49## Why this is not a fourth backend
50
51`crates/jolt-vidya`, `crates/jolt-tui` and `zig/jolt-zvui` all export the same
52retained-tree ABI: a tree the caller mutates between frames, one call that
53walks it, a queue of what the person did. That design puts the widget
54vocabulary *in* the native object, which is what makes those three
55interchangeable — and what means a new widget is a Rust or Zig change.
56
57jvui inverts it. jolt owns the tree, the layout and the widgets; SDL owns a
58window, an event pump, a rectangle and a glyph. A new widget is a function in
59[`src/jvui/widgets.clj`](src/jvui/widgets.clj), and nothing under it has to
60know.
61
62 src/jvui/sdl.clj SDL3 and SDL3_ttf; the entire foreign surface
63 src/jvui/paint.clj rectangles, rounded corners, text
64 src/jvui/font.clj one face, some sizes, two caches
65 src/jvui/core.clj identity, layout, event routing, the frame loop
66 src/jvui/widgets.clj the widgets
67 src/jvui/theme.clj colours and metrics, in one map
68 src/jvui/app.clj open a window and drive it
69
70## How a one-pass layout knows a size it has not measured yet
71
72An immediate-mode toolkit walks the tree once and must place a container before
73it has seen the container's children. dvui's answer, kept here, is to remember:
74every widget stores the size it turned out to need under a stable id, and the
75*next* frame uses that number. A leaf knows its own size at once — a label
76measures its string — so only containers lag, and they lag exactly one frame.
77
78`core/frame!` closes that gap before anything reaches the screen. When a stored
79size changes it walks again, up to three times, drawing nothing and delivering
80no events, and only the settled pass paints. So a new page appears laid out,
81not laid out on its second frame, and a click cannot be eaten by a pass whose
82output was thrown away. Once the layout stops moving the extra walks stop too,
83which is almost every frame.
84
85An id is a hash of its parent's id and the widget's index among its siblings —
86or of the parent and a `:key`, which *replaces* the index rather than joining
87it. No macro, no call-site capture. Replacing is the whole point: a list that
88reorders gives every widget after the moved one a new index, and a key that
89still carried the index would do nothing to stop each of them inheriting the
90caret, the scroll offset and the drag of whichever widget used to sit there.
91That is the bug class zvui's README describes from the other side, and
92`jolt test` reorders a list and checks it.
93
94## Two numbers that decided the design
95
96Both were measured here, and both sent the obvious approach back.
97
98**A foreign call costs 0.2 µs; `ffi/write-array` costs 0.6 µs per element.**
99The first draft of `paint.clj` batched everything into a packed `SDL_Vertex`
100buffer and issued one `SDL_RenderGeometry` per frame, which is how dvui's own
101backends do it. Filling that buffer from jolt costs six milliseconds for a few
102thousand vertices — the crossing was never the problem, the copy was. So the
103unit here is the call, not the vertex: a rectangle is one
104`SDL_RenderFillRect` (about 3 µs end to end) and text is one blit of a cached
105texture. A rounded rectangle is three rectangles plus four blits from a single
106antialiased white disc uploaded at startup, tinted by colour-mod — which is
107also why nothing here needs a tessellator.
108
109**`swap!` costs 4 µs.** The layout walk touches a box's running counters once
110per widget, and with those counters living in the context map a page of a
111hundred widgets spent most of a frame re-associng numbers no one else could
112see. They are now a five-slot `double-array` per box: written by one walker, in
113order, dead at the end of the frame. That one change took a label from 35 µs to
1145 µs and the showcase page from 8.7 ms a frame to 4.4.
115
116What is left is honest: about 5 µs per label and 20 µs per interactive widget,
117so a page of forty widgets is a millisecond or two of jolt per frame. The
118showcase, with a fifty-row scroll list, is 4.4 ms. That is a toolkit, not a
119slideshow, but it is also the ceiling — the remaining cost is `swap!` in the
120event and state paths, and it would come down the same way the counters did.
121
122## Using it under glimmer
123
124[`../glimmer-backends/glimmer-jvui`](../glimmer-backends/glimmer-jvui) is a
125glimmer backend over this: the reconciler's retained tree is thirty lines of
126atoms, and a walk over it calls the widgets below. It is the smallest of
127glimmer's four backends, because the toolkit it needs already exists here.
128
129## The vocabulary
130
131Containers, all macros over `core/box*`: `box` `vbox` `hbox` `card` `page`
132`scroll`.
133
134Widgets: `label` `title` `dim-label` `button` `checkbox` `slider` `progress`
135`text-entry` `separator` `spacer`.
136
137Options a container takes: `:dir` `:spacing` `:padding` `:margin` `:expand`
138`:gravity` `:key` `:fill` `:border` `:radius` `:min-size` `:clip?` `:offset`
139`:fixed`. `:expand` is `:none` `:horizontal` `:vertical` `:both` and
140`:gravity` is `[gx gy]`, each 0..1, placing a widget in whatever space it did
141not take.
142
143Events reaching a widget: hover, press, click, drag through capture, focus,
144Tab and shift-Tab, typed text, the editing keys, and the wheel.
145
146## Not done
147
148* **One font, one style.** No bold, no italic, no per-run font. `font.clj`
149 opens one face at whatever sizes are asked for.
150* **No text selection** and no clipboard in `text-entry`, though
151 `sdl/clipboard` and `sdl/clipboard!` are bound and waiting.
152* **Vertical scrolling only.** `scroll` clips both axes and scrolls one.
153* **No animation clock**, so no transitions and no cursor blink — the frame
154 loop runs at a fixed 16 ms and does not know what time it is.
155* **No menus, no dialogs, no floating layers.** Everything is in one
156 painter-ordered pass, and a popup needs a second one.
157* **`:gravity`'s cross axis is honoured; `:align` on text is `:left`
158 `:center` `:right` and nothing else.**