glimmer-gfx
glimmer's software backend: no toolkit, no shared object, no GPU. The
rasterizer, the layout and the font are jolt; the only foreign code is Xlib,
and Xlib draws nothing — it opens a window and takes a finished framebuffer.
It is glimmer-vidya with nothing underneath it. The
reconciler does not know what it is patching, so the same hiccup that egui
paints as a window and glimmer-tui paints as cells is painted
here, a pixel at a time, by code in this directory:
(ns myapp
(:require [glimmer.ratom :as ra]
[glimmer.core :as ui]
[glimmer-gfx.core])) ; installs this backend
(defn app []
[:card {:spacing 8}
[:title {:label "Counter"}]
[:label {:label (str "Count: " (ra/deref count))}]
[:button {:label "+ 1" :kind :primary :on-click #(ra/swap! count inc)}]])
(defn -main [& _] (ui/run app :title "myapp"))
Running
jolt test # headless: no window, no display
LD_LIBRARY_PATH=/path/to/libX11 jolt counter
Xlib is a system library here, not one of this repo's crates, and it must be
one the jolt binary can load — on a nix-built jolt, the host /usr/lib copy is
a different glibc and fails before dlopen returns.
What a backend has to do
Read this one first. It is the smallest complete backend in the repo, and the
only one where every part of the answer is visible rather than behind an ABI.
src/glimmer_gfx/raster.clj framebuffer, rect/line, a 3x5 font no FFI
src/glimmer_gfx/core.clj the backend: tree, layout, paint no FFI
src/glimmer_gfx/x11.clj the window, and nothing else
glimmer.backend's map is mostly trivial — create! allocates an atom,
append-child! conjes onto a vector. The work is what a toolkit would have
done for you:
- A tree to patch. The reconciler needs somewhere to hold widgets between
frames, which an immediate-mode painter has not got. Here it is atoms of
{:tag :props :children}, about 60 lines. glimmer-vidya needs the same thing
and keeps it in Rust behind a second C ABI, because egui hands the
reconciler nothing to hold. - Layout.
measure!sizes bottom-up,place!positions top-down. This is
most of the file.:spacing,:max-widthand container padding are
honoured;:alignand:groware not, so children keep their measured
width, left-aligned. - Hit testing. Deepest interactive node under the cursor, last match wins.
A press claims a widget and holds it until release, so a slider keeps
tracking when the pointer leaves its rect.
Tags: :page :card :vbox :hbox :title :label :button :checkbox
:slider :spacer. Props are the shared vocabulary — :label, :spacing,
:max-width, :kind, :checked, :value/:min/:max, :on-click,
:on-change.
Text
There is no font stack. raster.clj carries a 3x5 bitmap font for ASCII,
authored as art and editable in place:
"A.#.|#.#|###|#.#|#.#"
It scales by whole pixels, which is why the UI looks like it does. A real font
means FreeType, which means a crate — at which point this stops being the
backend that runs anywhere.
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 |
|