User-facing documentation for raygui-jlt: 24 raygui examples written in jolt (native Clojure on Chez Scheme, no JVM), calling raygui directly over its C ABI through jolt.ffi. No wrapper library, no codegen: one shared bindings namespace and a suite of small example programs on top of it.
The suite is complete: 24 examples across 7 groups. bb info always prints the live count.
raygui is raylib's companion immediate-mode GUI library, and it turns out to be a very good fit for a small, direct FFI binding: its 61 functions almost all share one shape, a Rectangle passed by value, application state through a pointer, an int result. There is no callback machinery, no retained widget tree, and raygui keeps no per-control state of its own, which is exactly what makes it easy to bind and hard to leak. the-ffi-shape.md is the full account.
b12n-raylib-jltSame approach, different library. Both repos bind a real C library directly over its ABI with jolt.ffi, Chez foreign-procedure underneath, no wrapper and no codegen. Where they differ is how hard the library leans on structs passed by value, and that difference is much smaller here than it is there.
raylib's interesting surface is Image, Texture, Font, Model and the Camera2D / Camera3D pair, several of which need the pointer-trick workaround b12n-raylib-jlt documents. raygui's only by-value struct anywhere in its control surface is Rectangle, plus Color in one icon function and Font in the two font accessors this repo leaves unbound. One recurring 16-byte struct, one module-level scratch buffer, and the whole class of FFI lifetime bugs a bigger struct surface would invite never comes up.
bb lib:build # compile the vendored header into lib/libraygui.{dylib,so}
bb check # headless compile of all 24 examples, no window
bb <example-name> # run one, e.g. bb basic-controls (opens a window)
Example tasks build the native library automatically if it is missing, so a fresh clone runs bb basic-controls with no separate setup step.
building-libraygui.md: raygui ships no library. This repo builds its own from a vendored header, and the one detail that makes the build actually work is that it must link raylib dynamically. Read this first if you are wondering why there's a build step at all.the-ffi-shape.md: the recurring by-value-Rectangle-in, pointer-out, int-result signature; the scratch Rectangle and the second buffer GuiScrollPanel needs; cells and their eight types; the two char** functions; and what's verified versus assumed on non-AArch64 hardware.what-the-gates-do-not-catch.md: four ways an example can be wrong while passing every automated check, each one measured, not guessed. The most useful page here if you're about to add an example of your own.example-catalog.md: every example, its group, what it shows, and which raygui controls it exercises.Demo GIFs. Recording is a stated non-goal, not an oversight: the sibling repo measured that synthetic clicks do not actuate a raylib app at all (0 of 8 clicks delivered at every tested hold duration), and raygui is mouse-driven by definition, so a recorded GIF could never show a button being pressed, a dropdown opening, or a slider being dragged.
vendor/raygui.h is pinned at 5.0-9-gfbf5d95, zlib licensed. See NOTICE for the full attribution, including the four examples ported from raygui's own example tree.
jolt.ffi does all the binding work described on these pages.b12n-raylib-jlt's own docs/guide/: the sibling this repo's pattern follows, and the deeper read on struct-by-value FFI tricks this project mostly doesn't need.librayguiThe page with no equivalent in the sibling repo, and the one a reader most needs.
b12n-raylib-jlt loads a system-installed libraylib: brew install raylib and you're done. raygui has no such option. raygui is header-only: upstream ships a single raygui.h you #include with RAYGUI_IMPLEMENTATION defined once, and there is no libraygui.so, no Homebrew formula, and nothing on any distro's package index to install. jolt.ffi needs a shared library to load, so this repo builds its own.
vendor/ holdsvendor/raygui.h: a pinned, unmodified copy of upstream's header at revision 5.0-9-gfbf5d95.vendor/raygui_impl.c: the one compilation unit. Two lines: #define RAYGUI_IMPLEMENTATION then #include "raygui.h".bb lib:build doesIt runs a compiler invocation defined in bb.edn. On macOS:
cc -O2 -dynamiclib -fPIC -DBUILD_LIBTYPE_SHARED \
-I vendor \
-I /opt/homebrew/include \
-L /opt/homebrew/lib -lraylib \
-framework CoreVideo -framework IOKit -framework Cocoa -framework OpenGL \
-o lib/libraygui.dylib vendor/raygui_impl.c
The Homebrew include/lib paths are named explicitly because they aren't on the compiler's default search path. bb lib:check reports whether raylib is installed and whether libraygui is already built, with the fix for each; every example task runs bb lib:build automatically if the library is missing, so a fresh clone needs no separate setup step.
This is the load-bearing part of the whole build, and it fails silently if you get it wrong.
raygui's controls call raylib's own functions internally: GetMousePosition, DrawRectangle, MeasureTextEx, and more, every frame. Those calls must reach the same libraylib instance that the jolt process has already loaded, or raygui reads mouse position and draws geometry against a second, independent copy of raylib's global state, one that never receives input events and never gets flushed to the real framebuffer. The result: every control renders (raygui draws its own chrome) but nothing ever responds to input, because the click landed in the jolt process's libraylib instance while the control checked a different one.
A statically-linked raygui would compile and load without error, and every control would simply be inert. There is no crash, no warning, nothing in the log to point at. Dynamic linkage gives raygui and jolt the same libraylib instance for free, because both resolve the same shared library at load time; -lraylib in the build invocation above links dynamically by default, so getting this right on macOS is a matter of not accidentally adding a static-link flag, not adding one.
Two checks, both read-only:
nm -gU lib/libraygui.dylib | grep -c ' T _Gui'
# 61
otool -L lib/libraygui.dylib
# lib/libraygui.dylib:
# lib/libraygui.dylib (compatibility version 0.0.0, current version 0.0.0)
# /opt/homebrew/opt/raylib/lib/libraylib.600.dylib (compatibility version 600.0.0, current version 6.0.0)
# /System/Library/Frameworks/CoreVideo.framework/...
# /System/Library/Frameworks/IOKit.framework/...
# /System/Library/Frameworks/Cocoa.framework/...
# /System/Library/Frameworks/OpenGL.framework/...
# /usr/lib/libSystem.B.dylib
nm -gU reports 61 exported T _Gui* symbols, matching the 61 RAYGUIAPI declarations in vendor/raygui.h. otool -L is the dynamic-link check itself: the second line naming libraylib.600.dylib (not a static blob folded into the binary) is what makes the previous section true. If that line is missing, or points somewhere other than the raylib you expect, the build has silently gone wrong in exactly the way that produces inert controls.
cc -O2 -shared -fPIC -DBUILD_LIBTYPE_SHARED \
-I vendor \
-lraylib -lGL -lm -lpthread -ldl -lrt -lX11 \
-o lib/libraygui.so vendor/raygui_impl.c
These flags are written from the macOS invocation, using the distro-default include and library search paths instead of Homebrew's. They have not been run. Do not treat them as working until someone builds and screenshots at least one example on Linux.
vendor/raygui.h with the new revision.NOTICE.bb lib:build to recompile against it.bb check to confirm every example still compiles against any signature changes.scroll-panel, the example most exposed to a layout or style regression (it is the one control taking two by-value Rectangles in a single call), and any example whose control depends on the currently-loaded style once the styling group lands.the-ffi-shape.md: what this dynamically-linked library lets jolt call, and how.what-the-gates-do-not-catch.md: the byte-swapped style-color trap, one layer up from the build itself.A map of the whole suite. Each example is one namespace under src/net/b12n/raygui_jlt/, runnable with a friendly bb <name> task or the underlying jolt -M:<name> without babashka. scripts/examples_registry.clj is the single source of truth this page is generated from; bb info prints the same grouping live.
bb <name> # e.g. bb basic-controls (opens a window)
bb examples # flat list with descriptions, plus the count
bb info # the grouped cheat-sheet this page mirrors
24 examples across 7 groups. The suite is complete. Run bb info for the count that is actually true right now, not this page's memory of it.
Every preview is a real capture of the example actually running, not a mockup. Each was produced by running the example with RAYGUI_APP_AUTO_QUIT_MS set, so the window closes itself, and RAYGUI_APP_SHOT pointed at a path, so it screenshots frame 30 before it does: the same mechanism what-the-gates-do-not-catch.md describes as this project's actual gate.
They are static frames, not recordings, and that is a deliberate choice, not a shortcut. raygui is mouse-driven by definition, and synthetic input does not actuate a raylib/GLFW window at all: measured upstream, 0 of 8 synthetic clicks and 0 of 2 synthetic drags were delivered at any hold duration tried, against 3 of 3 for plain pointer motion, because a window like this ignores pid-routed button events. An animated recording could show a cursor drifting toward a button; it could never show that button actually being pressed, a dropdown opening, or a slider being dragged. A still frame does not claim more than it shows, which is the honest version of a preview here.
Four examples are direct ports of programs in raygui's own examples/ tree: scroll-panel, floating-window, custom-input-box and style-selector, all built and in the tables above. NOTICE carries the attribution for all four.
Five touchpoints, from AGENTS.md:
src/net/b12n/raygui_jlt/<name_underscored>.cljdeps.edn alias: :<name> {:main-opts ["-m" "net.b12n.raygui-jlt.<name>"]}check.clj require: alphabetically, so the compile gate covers it["<name>" "<alias>" "<group>" "<desc>"] in scripts/examples_registry.clj, the single source of truth this page is generated frombb.edn task: <name> {:doc "▶ <desc>" :task (run-example "<name>")}bb examples enforces the 49-character description cap and cross-checks the registry's description against bb.edn's :doc string, exiting non-zero on either mismatch, so a drifted description here is a build failure, not a stale doc page.
the-ffi-shape.md: the binding pattern every control in this catalog shares.what-the-gates-do-not-catch.md: why a new example needs a screenshot that was actually looked at, and in some cases a read of the vendored C source, before it is done.Why raygui suits jolt, for a reader who knows neither.
raygui's API is 61 functions, and nearly every control shares the same three-part shape: a bounding Rectangle by value, application state through a pointer, and an int result telling the caller what happened.
int GuiSlider(Rectangle bounds, const char *textLeft, const char *textRight,
float *value, float minValue, float maxValue);
The raw binding mirrors it directly:
(ffi/defcfn gui-slider "GuiSlider"
[[:by-value [:struct [[:x :float] [:y :float] [:width :float] [:height :float]]]]
:string :string :pointer :float :float] :int)
and the kwarg wrapper on top hides the bounds construction and turns the raw result into the shape a caller actually wants:
(defn slider!
[& {:keys [x y w h left right cell min max]
:or {x 0 y 0 w 200 h 20 left "" right "" min 0.0 max 1.0}}]
(pos? (gui-slider (bounds! x y w h) left right (ptr cell)
(double min) (double max))))
bounds! is the scratch Rectangle below; cell is the pointer out-param below; pos? turns raygui's RESULT_CHANGED (2) into a plain boolean, true on change. That's the whole pattern, repeated with minor variation across most of the 61 functions. There is no callback machinery to bind, no retained widget tree to model, and raygui keeps no per-control state of its own to worry about ownership of.
Every control takes Rectangle bounds by value, and jolt's [:by-value ...] wants a pointer to caller-owned native storage, copying the struct's bytes at call time rather than retaining the pointer. That last fact is what makes one shared buffer safe: since the callee never holds on to the pointer past the call, the caller can reuse the same 16 bytes for every control in the frame.
Verified, not assumed: four controls drawn at four different positions through one shared buffer render pixel-identical to four separate allocations. So the raygui namespace owns exactly one module-level scratch Rectangle, rewritten immediately before each control call:
(def ^:private scratch-rect (ffi/alloc (ffi/layout-size rect-layout)))
(defn bounds! [x y w h] (write-rect! scratch-rect x y w h))
The consequence is the point, not a micro-optimisation: no example allocates or frees a Rectangle inside a frame, which removes an entire class of FFI lifetime bugs from all 24 example programs. The only lifetimes an example manages are its cells, below.
GuiScrollPanelOne function breaks the one-buffer rule: GuiScrollPanel is the only raygui function that takes two by-value Rectangles in a single call, its own bounds and the size of the content behind it.
int GuiScrollPanel(Rectangle bounds, const char *text, Rectangle content,
Vector2 *scroll, Rectangle *view);
Clojure evaluates arguments left to right. Writing both rectangles through the same bounds! call would let the second write clobber the first before the FFI call ever happens, handing raygui the same rectangle for both parameters.
Measured, in the actual scroll-panel example: with content! not yet in place, a panel declared at 300×200 rendered at its 560×420 content size, covering the window, with both scrollbars live and no error anywhere, on screen or in a log. It looked like a working, if oddly large, scroll panel. Only the on-screen dimensions gave it away.
The fix is a second, dedicated scratch buffer:
(def ^:private scratch-content (ffi/alloc (ffi/layout-size rect-layout)))
(defn content! [x y w h] (write-rect! scratch-content x y w h))
(defn scroll-panel! [& {:keys [x y w h text content-w content-h scroll view] ...}]
(gui-scroll-panel (bounds! x y w h) text
(content! 0 0 content-w content-h)
(ptr scroll) (ptr view)))
There is no third case: no other raygui function in the control surface takes more than one by-value struct, so two buffers is the whole fix, not a pattern to extend.
raygui keeps no state of its own. The application owns every value a control reads or writes, and C wants a pointer to it. A cell is a typed native slot, allocated once outside the frame loop and freed once after it:
(let [vol (rg/cell :float 0.35)]
(rg/slider! :x 90 :y 130 :w 200 :h 20 :cell vol :min 0.0 :max 1.0)
(rg/value vol)) ; => 0.35, read with the right type
Eight cell types are supported: :float, :int, :bool, :color, :vector2, :vector3, :rect (all allocated through cell), and :text (allocated through the separate text-cell, because a text buffer's size isn't implied by its type the way the others' are). value reads a cell back typed; reset-cell! writes one; free-cell! releases it and is idempotent, since freeing an already-freed cell aborts the process outright rather than raising a catchable exception.
The raw gui-* bindings stay public underneath the kwarg layer, so an example can drop to the plain C shape where the wrapper gets in the way.
char** functionsMost list-shaped controls take their options as one semicolon-separated string and answer with an index in an :int cell, which keeps arrays off the FFI boundary entirely. Two functions break that pattern and take a real const char ** array: GuiListViewEx and GuiTabBarEx. Both are bound raw; only list-view-ex! wraps its Ex variant, built and freed inside one ffi/with-c-string-array so the array's member pointers never outlive the call that uses them:
(let [items (vec items) n (count items)]
(ffi/with-c-string-array [arr n] items
(pos? (gui-list-view-ex (bounds! x y w h) arr n
(ptr scroll) (ptr cell) (ptr focus)))))
tab-bar! deliberately calls the plain GuiTabBar (the semicolon-string variant) rather than the bound GuiTabBarEx, keeping the char** path to the one example that needs it.
Rectangle is 16 bytes of homogeneous float, small enough that AArch64 passes it in registers rather than indirectly through a pointer, a different path from b12n-raylib-jlt's pointer trick for its 24-byte Camera2D (too large for the register path, faked with a hand-built pointer instead). jolt 0.7.23's [:by-value [:struct ...]] handles the register-passed case without the caller knowing the difference.
This has been driven from jolt against a real window and verified by screenshot on AArch64 only. SysV x86-64 classifies a 16-byte homogeneous-float aggregate into a different register class, and nothing in this repo has run there. State what was tested; do not assume it ports.
Two functions, GuiSetFont and GuiGetFont, pass raylib's Font struct by value and are left unbound, not bound-and-unused. The vendored .rgs styles each carry and apply their own embedded font on load, so no example needs a font accessor, and binding one would add a Font marshalling layer nothing calls.
building-libraygui.md: the library these bindings load.what-the-gates-do-not-catch.md: four ways code built on this shape can be wrong and still pass every automated check.The most useful page in this guide. Four failure classes, each found by running something and being surprised, each of which passes at least one automated gate while rendering or behaving wrong. For a GUI toolkit, that combination is the normal case, not the exception, which is why the screenshot is treated as a required gate here rather than a nicety.
raylib defers batched geometry, DrawText, shapes, everything, until EndDrawing. Calling TakeScreenshot before that flush captures the framebuffer as it stood before the current frame's drawing landed: a perfectly blank, valid PNG from a program that just drew four controls.
maybe-screenshot! flushes first, which is why every example's screenshot works:
(ffi/defcfn ^:private flush-batch "rlDrawRenderBatchActive" [] :void)
(defn maybe-screenshot! [frame at]
(when (and shot-path (= frame at))
(flush-batch)
(take-screenshot shot-path)
...))
bb check does not catch this. It compiles every example headlessly and never draws a frame at all. The gate that catches it is looking at the PNG: a blank image means the flush is missing (or never ran), not that the example drew nothing by design.
Colorraygui stores style colours as 0xRRGGBBAA. raylib's Color packs little-endian as 0xAABBGGRR. Feed one straight into a function expecting the other and you get a plausible, wrong colour, not an error.
Measured on the cyber style's background:
GuiGetStyle(DEFAULT, LINE_COLOR) -> 0x81C0D0FF (R=129 G=192 B=208 A=255, light blue)
fed directly to ClearBackground -> renders salmon pink
via GetColor() -> renders light blue, correct
Nothing here throws. The window opens, the background paints some colour, and only comparing the render against the style's own declared value shows it's the wrong one, red and blue swapped into something that still looks like a deliberate choice. style-color wraps GetColor so this conversion happens once, correctly, and every example that themes itself from the loaded style goes through it rather than reading GuiGetStyle raw.
bb check does not catch this (no color comparison happens headlessly), and neither does a glance at the screenshot alone, since the wrong colour is still a colour. The gate is checking the rendered colour against the style's own declared value, not just confirming that something painted.
ffi/defcfn binds lazilyA jolt.ffi/defcfn form loads with no error even when the C symbol name it names doesn't exist. The failure only appears the first time the function is called:
Exception in foreign-procedure: no entry for "GuiGetStyleTYPO"
bb check compiles and requires every example namespace, which resolves macros and vars but never calls a single gui-* binding. A misspelled C symbol therefore compiles clean, passes bb check, and throws only at run time, inside whatever example first calls it, on whichever machine happens to run it next.
bb check does not catch this by construction: it is a require, not a call. The gate is either actually running the example (so the call happens and the missing symbol throws), or a static cross-check of every ffi/defcfn symbol name against nm -gU on the built library, confirming each named C symbol exists before anything tries to call it.
The sharpest of the four, because the first three are all visible-if-you-know-to-look: a blank frame, a wrong colour, a thrown exception. This one produces a screenshot that looks completely correct while the code behind it does the opposite of what it claims.
A screenshot shows a dialog open. It never shows what its buttons do.
The real example: GuiTextInputBox reports which button was pressed by writing an int result that is 0 for the window's own close X, and one-based for every button in the caller's semicolon list otherwise:
// raygui.h: btnActive = 0 for the window X, i+1 for the i-th button
With :buttons "Cancel;OK" that makes Cancel 1 and OK 2. text-input-box.clj first checked (= 1 result) to detect "OK pressed". Nothing about that crashes or even looks wrong on screen: the dialog opens, a button click closes it, some text appears. It just reports the wrong button every time, Cancel silently reporting success and OK silently reporting cancellation, with the on-screen result looking exactly as intended either way. Fixed by checking (= 2 result) for OK instead, per the header's actual encoding.
A second instance in the same suite: tab-bar.clj told a reader to click a tab's close x. GuiTabBarEx gates the entire close-button block on GuiGetStyle(TABBAR, TAB_CLOSE_BUTTON) (raygui.h:3938), and GuiLoadStyleDefault never sets that flag, leaving it 0. The close button the instructions pointed at was never drawn. The tab bar itself rendered fine, tabs selected correctly, and nothing about the screenshot showed a missing button, because there was nothing there to be missing from the image. Fixed with one line before the frame loop:
;; The close 'x' is off by default: GuiTabBarEx gates it on this style flag and
;; GuiLoadStyleDefault never sets it, so without this line the instruction
;; below points at a button that is never drawn.
(rg/gui-set-style rg/TABBAR rg/TAB-CLOSE-BUTTON 1)
No automated gate catches this class, and neither does looking at the PNG. The only thing that catches an outcome-logic bug is reading the vendored C source for any control whose result encodes more than a plain "pressed / not pressed": what each return value actually means, and which style flags gate which visible pieces.
| failure | bb check | screenshot | what actually catches it |
|---|---|---|---|
| missing batch flush (blank frame) | no | yes | look at the PNG |
| byte-swapped style colour | no | not alone | compare the rendered colour to the style's declared value |
misspelled C symbol (lazy defcfn) | no | no | run the example, or cross-check symbol names against nm -gU |
| wrong outcome/result encoding | no | no | read the vendored C source |
The first two are why the screenshot step exists at all for this suite. The last two are why it isn't sufficient by itself, and why every example task in this project's plan also asks whether the control's result meaning was checked against the header, not just whether the control renders.
building-libraygui.md: the batch-flush and byte-swap traps both sit downstream of a correctly built, dynamically-linked library.the-ffi-shape.md: the binding shapes these failures hide inside.