nandi/jolt-nativepublic Fork 0
807f68183d34c39f2c251c5ad1ec50bb79005211
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 · 250 lines · 10.1 KBmarkdown Blame HistoryRaw
Bring vidya in cfd3e36 nandi 19d ago1# glimmer-vidya
2
3The **Vidya/egui** backend for [glimmer](https://github.com/jolt-lang/glimmer),
4the reactive GUI toolkit for [jolt](https://github.com/jolt-lang/jolt).
5
6glimmer owns the portable half — reactive cells, the component model, the
7reconciler — and knows nothing about any toolkit. This project supplies the
8other half for a GPU window: Vidya's widgets and theme, painted by egui through
9[`../ffi`](../ffi/README.md). Requiring `glimmer-vidya.core` registers it, and
10components that render as GTK widgets under
11[glimmer-gtk](https://github.com/jolt-lang/glimmer-gtk), and as text under
12[glimmer-tui](https://github.com/jolt-lang/glimmer-tui), render here as Vidya.
13
14```clojure
15(ns myapp
16 (:require [glimmer.ratom :as r :refer [atom]]
17 [glimmer.core :as ui]
18 [glimmer-vidya.core])) ; installs this backend
19
20(defn counter []
21 (let [count (atom 0)]
22 (fn []
23 [:page {:max-width 420}
24 [:card {}
25 [:title {:label "Counter"}]
26 [:label {:label (str "Count: " @count)}]
27 [:hbox {:spacing 8}
28 [:button {:label "- 1" :on-click #(swap! count dec)}]
29 [:button {:label "+ 1" :kind :primary :on-click #(swap! count inc)}]
30 [:button {:label "reset" :on-click #(reset! count 0)}]]]])))
31
32(defn -main [& _] (ui/run counter :title "counter" :width 480 :height 320))
33```
34
35Components, reactive state and reconciliation are documented in glimmer's
36README. What follows is the Vidya-specific part.
37
38## How an immediate-mode toolkit holds still
39
40egui has no widgets. It has calls you make every frame, and a reconciler has
41nothing to reconcile against — no pointer to patch, nothing to append a child
42to. GTK hands glimmer a `GtkButton`; egui hands it nothing at all.
43
44So the widget tree lives one layer down, in Rust. `libvidya` keeps a node arena
45behind a second C ABI ([`../ffi/include/vidya_tree.h`](../ffi/include/vidya_tree.h)):
46nodes are integer handles, and this backend's `create!` / `apply-props!` /
47`append-child!` mutate them. Nothing is painted by those calls. Once a frame,
48`vidya_tree_frame` walks the whole tree and emits the egui calls it describes.
49
50Two things follow from putting the tree there rather than here:
51
52* **FFI traffic tracks edits, not frames.** A static UI costs no crossings per
53 frame; only what the reconciler actually changed is sent. The alternative —
54 keeping the tree in jolt and walking it over the FFI 60 times a second —
55 would put the frame rate at the mercy of the reconciler's thread.
56* **The closure-shaped parts of egui work.** `ScrollArea` and `Frame` take an
57 `FnOnce(&mut Ui)` and keep their `begin`/`end` private, which is why Vidya's
58 original push/pop ABI could not scroll a page. Painting from a tree already
59 in hand means the recursion *is* the closure.
60
61**Handlers do not cross the boundary.** A jolt closure cannot be a callback in
62a library painting at 60fps, so identity travels instead: a node reports that it
63was clicked, `glimmer-vidya.core` looks up whose `:on-click` that was, and calls
64it on the loop thread. Handlers are held on the jolt side and never sent.
65
66## Requirements
67
68`libvidya` built from [`../ffi`](../ffi), the Rust/egui implementation:
69
70```sh
71just ffi # buck2 → ../build/libvidya.so
72```
73
74Then put it on the search path when running anything here:
75
76```sh
77LD_LIBRARY_PATH=../build jolt counter
78```
79
80`just ffi-android` cross-compiles the same library to
81`../build/android/arm64-v8a/libvidya.so` for a 64-bit device — both ABIs this
82backend binds are exported there too.
83
84On macOS use `DYLD_LIBRARY_PATH`. Note that this is the one place in the repo
85where the two `libvidya` builds are **not** interchangeable: `../raylib`
86implements `vidya.h` only, and this backend binds the tree ABI, which is the
87Rust build's alone.
88
89## Running
90
91```sh
92jolt test # the suite, headless: no window, display or GPU needed
93jolt counter # the counter above
94jolt showcase # every tag, a keyed task list, an entry, a disabled subtree
95jolt smoke # non-interactive: reconciles under paint, then quits
96```
97
98## Seeing what was rendered
99
100`dump` reads the tree back out of the library as hiccup — what is actually
101mounted, after the reconciler has had its way with it, rather than what a
102component returned:
103
104```clojure
105(require '[glimmer-vidya.core :as backend])
106
107(backend/dump!) ; print the whole window
108(backend/dump! node) ; or one subtree
109(backend/dump) ; the same as hiccup data, for a test
110(backend/dump-str) ; as text, to paste into a bug report
111```
112
113```clojure
114[:window {}
115 [:card {}
116 [:title {:label "Counter"}]
117 [:label {:label "Count: 3"}]
118 [:box {:orientation "horizontal" :spacing 8}
119 [:button {:label "- 1"}]
120 [:button {:kind "primary" :label "+ 1"}]]]]
121```
122
123Two things to expect when reading one, both of them the boundary showing
124through. `:hbox` and `:vbox` are one node down there, so both dump as `:box`
125with the orientation in the props; and no `:on-*` appears, because handlers are
126held on this side and never sent. Props are sorted, so two dumps of the same
127tree compare as text.
128
129It needs no window — the tree is only painted by `vidya_tree_frame` — so a
130headless test can mount a component and assert on `(dump root)` directly.
131
132## Hiccup reference
133
134Elements are `[:tag props? & children]`, as everywhere in glimmer. Strings and
135numbers become labels, `nil` children are skipped, seqs are spliced.
136
137**Containers**
138
139| tag | holds | notes |
140|---|---|---|
141| `:window` | many | the root; you rarely name it |
142| `:box` / `:hbox` / `:vbox` | many | `:orientation :horizontal\|:vertical`, implied by the tag |
143| `:page` | many | scrolling column with page padding; `:max-width` centres it |
144| `:scroll` | many | `:orientation :vertical\|:horizontal\|:both`, `:scroll-key` |
145| `:card` | many | Vidya's raised surface |
146| `:frame` | many | a card with a `:label` as its heading |
147
148**Widgets**
149
150| tag | shows |
151|---|---|
152| `:label` | body text |
153| `:title` / `:title-2` / `:dim-label` | the type scale's other roles |
154| `:button` | `:kind :default\|:primary\|:destructive` |
155| `:checkbutton` | Vidya's themed checkbox (`:checkbox` is an alias) |
156| `:entry` | a text field; `:multiline true` with `:rows` for a box |
157| `:separator` | a rule |
158| `:spacer` | blank space of `:size` points |
159| `:progress` | a bar, `:value` 0.0–1.0, with an optional `:label` |
160| `:spinner` | an indeterminate spinner |
161| `:status` | a live/offline dot beside a label |
162| `:image` | a picture from a file; `:max-height` bounds it, `:fit true` fills and centres it in the space it is given (the one case that scales up) |
163
164**Common props**
165
166- `:sensitive false` — dims the widget *and its whole subtree*, and takes it out
167 of egui's interaction
168- `:width-request` — a fixed width. Worth more here than it sounds: immediate
169 mode has no natural width for a field, so an `:entry` asks for whatever is
170 left and takes the row it shares with a button. This is how you say otherwise.
171- `:margin`, `:spacing` — on containers
172
173**Events**
174
175- `:on-click` — button pressed. No args.
176- `:on-toggled` — checkbutton clicked. No args.
177- `:on-change` — entry text changed. Receives the new text.
178- `:on-activate` — Enter pressed in an entry. No args.
179- `:on-paste-empty` — Ctrl+V in an entry with no text on the clipboard, which
180 is what a copied picture looks like from there. No args; ask
181 `clipboard-image-png!` what is actually on it.
182
183As in the other backends, a handler owns the state: `:on-toggled` flips the cell
184the component reads, and `:active` comes back down as a prop. A control that
185ignores its own event still works — the library wrote the new state into the
186node, and the next render either confirms it or overwrites it.
187
188An unrecognized tag is kept rather than refused: it paints as a vertical box, so
189a component written against a tag this backend has not grown yet still shows its
190children.
191
192## Options for `ui/run`
193
194On top of glimmer's own `:title`, `:width`, `:height` and `:auto-quit-ms`:
195
196| option | |
197|---|---|
198| `:fps` | frame rate cap, default 60 |
199| `:mode` | `:dark` (the default) or `:light` |
200| `:font` | path to a TTF/OTF for UI text |
201
202## Timers
203
204The loop wakes every frame anyway, so a timer is a due time and a thunk. Both
205run on the loop thread, the only one allowed to touch nodes:
206
207```clojure
208(vidya/every! 80 #(swap! tick inc)) ; a spinner, a clock, a progress bar
209(vidya/after! 500 #(reset! ready true))
210(vidya/cancel! id)
211```
212
213`(vidya/quit!)` stops the loop and closes the window.
214
215## Threads
216
217Every node call belongs to the thread that opened the window — the library
218enforces it, keeping its state in thread-local storage. glimmer already knows
219this: while the loop runs it marshals each component's re-render through the
220backend's `schedule`, which queues the work for the next tick. So a `swap!` from
221an nREPL worker is safe, and `glimmer.core/on-gui` is there for code that wants
222to touch the tree directly.
223
224## Testing
225
226The suite is headless. Node calls need no GL surface — only `vidya_tree_frame`
227does — so `jolt test` mounts real components, reconciles them, and reads the
228resulting tree back through the same ABI the backend writes it with. It asserts
229what was created, what was patched in place, what was replaced, and that a keyed
230list reorders its widgets rather than rebuilding them. No window is opened and
231no display is required, which is also what makes it run in CI.
232
233Painting is checked separately, by `jolt smoke` under `VIDYA_CAPTURE`:
234
235```sh
236VIDYA_CAPTURE=/tmp/frame.ppm LD_LIBRARY_PATH=../build jolt smoke
237```
238
239## Limits
240
241* **X11 is preferred on Linux**, for the reason in
242 [`../ffi/README.md`](../ffi/README.md): the caller owns the frame loop, and a
243 native Wayland surface driven that way stops receiving frame callbacks. Under
244 XWayland everything works; fractional scaling follows XWayland's rules.
245* **No focus or keyboard navigation of your own.** egui owns focus, tabbing and
246 hit testing, so there is nothing here like glimmer-tui's `:keys`, `:on-key` or
247 `:autofocus` — and nothing to configure.
248* **No `:listbox`, `:table`, `:overlay` or `:paginator` yet.** A list is a
249 keyed `:vbox` for now. These are widget-layer work in `../ffi/src/tree.rs`
250 plus a tag; nothing in the design is in the way.