| Write dvui's shape in jolt, on SDL3, with no shared object 109c7e4 Veronika Winters 9d ago | 1 | # glimmer-jvui |
| 2 | |
| 3 | The **jvui** backend for [glimmer](https://github.com/jolt-lang/glimmer), the |
| 4 | reactive GUI toolkit for [jolt](https://github.com/jolt-lang/jolt). |
| 5 | |
| 6 | glimmer owns the portable half — reactive cells, the component model, the |
| 7 | reconciler — and knows nothing about any toolkit. This project supplies the |
| 8 | other half out of [`../../jvui`](../../jvui), which is dvui's shape written in |
| 9 | jolt on SDL3. Requiring `glimmer-jvui.core` registers it, and components that |
| 10 | render as GTK widgets under |
| 11 | [glimmer-gtk](https://github.com/jolt-lang/glimmer-gtk), as egui under |
| 12 | [glimmer-vidya](../glimmer-vidya) and as text under |
| 13 | [glimmer-tui](../glimmer-tui) render here as jvui. |
| 14 | |
| 15 | ```clojure |
| 16 | (ns myapp |
| 17 | (:require [glimmer.ratom :as ra] |
| 18 | [glimmer.core :as ui] |
| 19 | [glimmer-jvui.core])) ; installs this backend |
| 20 | |
| 21 | (defn counter [] |
| 22 | (let [n (ra/atom 0)] |
| 23 | (fn [] |
| 24 | [:card {} |
| 25 | [:title {:label "Counter"}] |
| 26 | [:label {:label (str "Count: " (ra/deref n))}] |
| 27 | [:hbox {:spacing 8} |
| 28 | [:button {:label "- 1" :on-click #(ra/swap! n dec)}] |
| 29 | [:button {:label "+ 1" :kind :primary :on-click #(ra/swap! n inc)}]]]))) |
| 30 | |
| 31 | (defn -main [& _] (ui/run counter {:title "myapp" :max-width 420})) |
| 32 | ``` |
| 33 | |
| 34 | ```bash |
| 35 | jolt test # headless: no window, no SDL, no font, no display |
| 36 | jolt counter # a window |
| 37 | jolt counter --shot |
| 38 | ``` |
| 39 | |
| 40 | ## The smallest of the four backends |
| 41 | |
| 42 | A glimmer backend usually has to supply everything a toolkit would have done |
| 43 | for it. [glimmer-gfx](../glimmer-gfx) writes its own measure, place, paint and |
| 44 | hit test, because there is nothing underneath it. [glimmer-vidya](../glimmer-vidya) |
| 45 | keeps a node arena in Rust behind a second C ABI, because egui hands a |
| 46 | reconciler nothing to hold. |
| 47 | |
| 48 | Here none of that is needed, because jvui is a toolkit rather than an ABI. What |
| 49 | is left is the one thing an immediate-mode library does not have — somewhere to |
| 50 | put a widget between frames — and it is thirty lines of atoms: |
| 51 | |
| 52 | src/glimmer_jvui/core.clj the tree, the walk, the loop |
| 53 | |
| 54 | `create!` makes an atom, `append-child!` conjes onto a vector, and once a frame |
| 55 | `emit!` walks that tree and calls the jvui widget each node names. Layout, |
| 56 | clipping, focus, capture and painting are jvui's. |
| 57 | |
| 58 | ## Three things worth knowing |
| 59 | |
| 60 | **The walk is the closure.** glimmer-vidya's README explains why its tree lives |
| 61 | in Rust: egui's `ScrollArea` and `Frame` take an `FnOnce(&mut Ui)` and keep |
| 62 | their begin/end private, so a push/pop ABI cannot scroll a page. jvui's |
| 63 | containers take a body function for the same reason — and here the recursion |
| 64 | *is* that function. `emit!` on a container hands `emit-children!` over as the |
| 65 | body and the nesting takes care of itself, which is why there is no push/pop |
| 66 | anywhere in this file. |
| 67 | |
| 68 | **Every node carries a key.** jvui identifies a widget by its parent and its |
| 69 | index among its siblings, unless it is given a `:key`, which *replaces* the |
| 70 | index. A reconciler reorders children, and an identity built on the index would |
| 71 | hand every widget after the moved one the caret, the scroll offset and the drag |
| 72 | of whichever widget used to sit at its index. So each node takes a serial |
| 73 | number at creation and passes it as its key, and identity follows the node |
| 74 | rather than its position. That is the bug class |
| 75 | [zvui](../../zig/jolt-zvui)'s README describes from the backend side, closed |
| 76 | here at the other end — and `jolt test` reorders a list and checks it. |
| 77 | |
| 78 | **A re-render is queued, never inline.** A handler fires in the middle of the |
| 79 | walk, and a ratom change would have the reconciler patch the tree while it is |
| 80 | being walked — half the frame old, half new. `:schedule` queues the work and |
| 81 | jvui's `:before` hook drains it at the top of the next frame, before anything |
| 82 | is placed. |
| 83 | |
| 84 | ## What renders |
| 85 | |
| 86 | Containers: `:page` `:card` `:frame` `:vbox` `:box` `:hbox` `:scroll`. |
| 87 | Widgets: `:title` `:label` `:dim-label` `:button` `:checkbox` `:slider` |
| 88 | `:entry`/`:text-entry` `:progress` `:separator` `:spacer`/`:gap`. |
| 89 | |
| 90 | Props are the vocabulary the other backends share — `:label`/`:text`, |
| 91 | `:spacing`, `:padding`, `:margin`, `:orientation`, `:max-width`, `:kind`, |
| 92 | `:checked`, `:value`/`:min`/`:max`, `:placeholder`, `:dim`, `:size`, |
| 93 | `:on-click`, `:on-change`. |
| 94 | |
| 95 | An unknown tag is a container rather than an error, so a tree written against a |
| 96 | richer backend still shows its contents — the same bargain jolt-zvui makes. |
| 97 | |
| 98 | ## Not done |
| 99 | |
| 100 | Everything jvui has not got, and nothing else: one font style, no text |
| 101 | selection, vertical scrolling only, no animation clock, no menus or dialogs. |
| 102 | `:sensitive`, `:multiline` and `:fill-height` are accepted and ignored. |