raygui-jnk
  • Home
  • Docs
  • GitHub

raygui-jnk guide

24 raygui examples written in jank, a native Clojure dialect that compiles through C++ and LLVM rather than running on the JVM.

raygui was not available to jank before this. It is a header-only C library with no package anywhere, which is why the sibling b12n-raylib-jnk replaced raygui with keyboard controls in seventeen of its examples and wrote up the pattern. This repo builds raygui as an ordinary jank package, so those controls can be real again.

The suite is complete: 24 examples across 7 groups. bb examples prints the live count.

These pages are published at https://raygui-jnk.b12n.app and mirrored into b12n-wikis. Both come from docs/guide/ in the project repo, so edit the Markdown there.

Why raygui suits this

Nearly every raygui control has the same shape. Bounds in, application state through a pointer, an int out:

int GuiSlider(Rectangle bounds, const char *textLeft, const char *textRight,
              float *value, float minValue, float maxValue);

There is no callback machinery, no retained widget tree, and raygui keeps no per-control state at all. jank has one genuine gap in its C interop, which is that you cannot hand a jank function to a C API expecting a function pointer. raygui never asks for one, so that gap does not touch this repo.

Where the work actually went

Not into declaring functions. jank compiles through C++, so raygui.h is the binding and the count of hand-written signatures here is zero. The sibling raygui-jlt writes 61 defcfn forms to reach the same place through an FFI.

The work went into one rule instead: a native value cannot cross a jank function boundary. A Rectangle cannot be passed to or returned from a defn, so the wrapper is built so every native value is born and consumed inside the same function body, and only numbers, bools, strings and opaque boxes travel between namespaces. the-jank-shape.md is the full account.

The commands that matter

bb info                  # every task, grouped. Start here.
bb examples              # just the suite, grouped
bb check                 # compile every example headless, no window
bb lint                  # clj-kondo over every .jank file
bb basic-controls        # run one, windowed. Q quits.
bb run-all               # cycle all 24 as a demo reel
bb shot basic-controls   # run headless and screenshot

There is no separate build step for raygui. raygui-sys/jank-build.bb compiles it as part of the first lein invocation, against the same libraylib the raylib-sys package provides.

The pages

  • the-jank-shape.md: the boundary rule and everything that follows from it. Cells, text buffers, the two argument shapes that look identical in C, the array marshalling, and the one place where a rule the sibling repo states turns out to be narrower than it reads.
  • building-raygui.md: the jank package protocol, why linking against the right libraylib is load-bearing rather than incidental, how to use raygui-sys from another project, and a build cache that will hand you a stale library without saying so.
  • what-the-gates-do-not-catch.md: ten ways an example here can be wrong while every automated check passes. Each one happened during this port. Read it before adding an example.
  • example-catalog.md: all 24 with screenshots, plus the six vendored themes. Generated from the registry by bb scripts/gen_catalog.clj, as the README gallery is by bb readme:examples.

The screenshot is a gate

For a GUI library that is not a nicety. A control at the wrong bounds, a style that silently failed to load, a colour with red and blue swapped, or a frame captured before the render batch flushed all compile cleanly and pass bb check. Every example in this repo has had its PNG looked at by a person.

What no gate here covers is interaction. Synthetic clicks do not actuate a raylib or GLFW app at all, which b12n-raylib-jlt measured at 0 of 8 clicks delivered across every hold duration it tried. So the automated claim is that the suite renders correctly. Whether a dropdown opens or a slider drags has been checked by hand.

Demo GIFs are a stated non-goal

Same measurement, taken to its conclusion. raygui is mouse-driven by definition, so a recorded GIF could show hover and pointer motion but never a button press, a dropdown opening, or a slider being dragged. Twenty-four recordings of controls nobody touches would be worse than none, so the suite ships static screenshots instead.

Vendored revision

raygui-sys/vendor/raygui.h is pinned at 5.0-9-gfbf5d95, zlib licensed. That is nine commits past the 5.0 release and its API differs from the published 5.0 documentation in a few places, so write bindings from the header in vendor/. NOTICE carries the full attribution, including the four examples ported from raygui's own example tree.

See also

  • raygui, the upstream library.
  • raylib, which raygui draws through.
  • jank, and its book chapter on reaching into C++.
  • b12n-raylib-jnk's docs/guide/, for the general jank interop rules this repo builds on rather than restates.
  • raygui-jlt, the same 24 examples in jolt, for a side-by-side of FFI bindings against compiled C++ interop.

Building raygui

raygui ships no library. It is a single header where the declarations and the implementation live in the same file, and RAYGUI_IMPLEMENTATION has to be defined in exactly one translation unit. There is no Homebrew formula, no libraygui.so on any distro, and nothing to install.

jank has a native-package protocol that handles this, which is what the whole raygui-sys/ directory exists to use. The jolt port of the same library needs a bb lib:build task, a gitignored lib/ directory and build-if-missing logic in every example task. None of that appears here.

Every command on this page was run against this repo, and the output is pasted as it came back with $HOME shortened.

The protocol

A jank-build.bb at a project root is run by lein-jank with the build metadata on stdin, bound to *input*. Whatever it prints with a jank-build:: prefix becomes compiler flags:

jank-build::include-dir=<dir>
jank-build::link-dir=<dir>
jank-build::link-library=<name>
jank-build::define=K=V

raygui-sys/jank-build.bb compiles vendor/raygui_impl.c and emits three of those. Here is what it actually printed on the last build:

jank-build::include-dir=~/dev/raygui-jnk/raygui-examples/target/_cache/raygui-sys-0.1.0-SNAPSHOT-src-6d9a5e87.../vendor
jank-build::link-dir=~/dev/raygui-jnk/raygui-examples/target/_cache/raygui-sys-0.1.0-SNAPSHOT-out-efd2201e.../lib
jank-build::link-library=raygui

That include directory is why (:include "raygui.h") resolves as an angled include with no path juggling anywhere in the source.

Linking against the right raylib

The load-bearing detail, and the one that would fail silently if it were wrong.

raygui's controls call GetMousePosition, DrawRectangle and MeasureTextEx internally. Those have to resolve to the same libraylib the jank process has already loaded. Against a second copy, raygui reads input state from a set of globals nothing is updating, every control goes inert, and nothing raises.

The dependency graph supplies the right path. (:inputs *input*) maps each dependency to its own build output, so the build script compiles against raylib-sys rather than guessing at a system path:

(let [{:keys [src-dir out-dir inputs]} *input*
      rl (get inputs "org.jank-lang.commons/raylib-sys")]
  ...
  "-I" (str (fs/path rl "include"))
  "-L" (str (fs/path rl "lib"))
  "-lraylib")

The key is fully qualified. "raylib-sys" returns nil, and the resulting cc invocation fails with no such file or directory: 'lib', which does not point at the cause.

otool -L on the result shows the dynamic link landed:

libraygui.dylib:
	@rpath/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/...

and nm -gU ... | grep -c ' T _Gui' reports 61, matching the 61 RAYGUIAPI declarations in the vendored header.

Using raygui from another project

raygui-sys is an ordinary jank package. Install it:

bb sys:install

Then depend on it, and that is the whole setup:

:dependencies [[net.b12n/raygui-sys "0.1.0-SNAPSHOT"]]

The consuming project needs no vendor/ directory and no jank-build.bb of its own. raygui-examples/ is exactly that arrangement, and it is what makes restoring real raygui controls to b12n-raylib-jnk a one-line change rather than a second vendoring.

Two things make the package work, and both are easy to leave out:

:prep-tasks []                 ; or lein install aborts demanding bwrap
:verbatim-paths ["vendor"]     ; or the jar ships no C for a consumer to build

Leiningen's default :prep-tasks runs compile, which the jank middleware aliases to lein jank compile, which wants the bwrap sandbox. macOS has no bwrap, so the failure reads like a missing system dependency rather than a task-ordering problem. jank-build.bb itself rides along automatically; the middleware adds it to :verbatim-paths for you.

A cache that will hand you a stale library

Worth knowing before you edit any of the vendored C, because nothing warns you.

lein-jank keys a dependency's output directory on (fingerprint subtree-ops), the fingerprint of its descendants' build steps, rather than its own source. Change raygui-sys's C and you get a new jar and a freshly extracted source directory, but the output directory is unchanged, is-already-built? finds its cache file, and jank-build.bb never runs.

Measured here on 2026-08-26: after a content edit to raygui_impl.c, the .dylib stayed thirteen minutes older than the source it was supposedly built from, and the build printed Extracting without ever printing Compiling.

bb sys:install drops the consumer's cached output for this package, which forces the rebuild. Editing the .jank sources is unaffected, since those are compiled from the re-extracted classpath every run. Only vendored C bites.

macOS needs --disable-sandbox

There is no bwrap on macOS, so lein-jank's build sandbox cannot work there at all. Every lein invocation in bb.edn already passes the flag. Running lein by hand needs it too:

cd raygui-examples && lein with-profile +basic-controls run --disable-sandbox

Linux

The .so branch of jank-build.bb swaps -dynamiclib for -shared and drops the four macOS frameworks. It has never been run. Nobody has built this repo on Linux, so treat those flags as a starting point rather than a tested path.

The vendored revision

raygui-sys/vendor/raygui.h is pinned at 5.0-9-gfbf5d95, zlib licensed, and committed unmodified. Pinning means the suite cannot break when upstream moves, and it also means fixes do not arrive on their own. NOTICE records the revision.

That revision is nine commits past the 5.0 release, and its API differs from what the 5.0 documentation describes in at least three places: GuiMessageBox and GuiTextInputBox carry an extra int *btnActive, and GuiTabBar takes a semicolon-separated string with an hscroll cell rather than an array with a count. Write bindings from the header in vendor/, never from the published docs or from the jolt port's Clojure.

The example catalog

Every example in the suite, grouped the way bb examples groups them. Each is one namespace under raygui-examples/src/net/b12n/raygui_jnk/.

bb <name>          # run one, windowed. Q quits.
bb run-all [secs]  # cycle all of them, a demo reel
bb shot <name>     # run headless and screenshot to /tmp/<name>.png
bb examples        # this grouping, printed live
bb info            # every task, grouped, not just these

scripts/examples_registry.clj is the single source of truth for the names and descriptions below, and bb examples fails if a bb.edn task drifts from it. Run that for the count that is true right now rather than this page's memory of it. At the time of writing there are 24 examples across 7 groups, and the suite is complete.

Screenshots are captured by the same bb shot task the gates use. None of them shows an interaction, because no gate here can drive a mouse.

basics

previewbb namewhat it shows
basic-controlsThe smallest complete program here: a button, a label and a click counter. It proves the package built, the bounds arrived intact, and an int cell round-tripped through raygui's pointer API.
icon-buttonsraygui carries 256 icons inside the header, so there is no image to load. GuiIconText composes one with a label for any control, GuiDrawIcon draws it directly. The scale is global and persists until it is set back.
labels-linesThe controls that draw and hold nothing: labels at two alignments, separators with and without a caption, a placeholder box, a status bar. Also the thing to watch about raygui styling, since TEXT_ALIGNMENT is global and has to be put back.
togglesThree controls that all hold a selection, with three different state shapes. GuiToggle owns a bool cell; GuiToggleGroup and GuiToggleSlider take their options as one semicolon-separated string and report an index through an int cell.

inputs

previewbb namewhat it shows
text-boxThe first mutable char buffer. Edit mode is a plain bool the caller owns rather than a cell, and the screenshot prints the buffer's length so a failed readback would be visible.
text-input-boxA whole dialog in one call, including an out-param that raygui 5.0's docs do not show. The masked field always draws sixteen asterisks whatever the buffer holds, because raygui masks with a fixed literal.
spinner-value-boxBoth hold an int cell and clamp to a range. The third spinner starts at 999 against a maximum of 50, so the screenshot shows the clamp holding rather than asserting that it does.
slidersGuiSlider has a draggable handle, GuiSliderBar fills from its left edge. Each value is drawn beside its control, so a cell that failed to round-trip would show up in the picture.
progress-barFilled from GetTime rather than a frame counter. The only example that does not capture on frame 0, since an empty bar proves nothing.

collections

previewbb namewhat it shows
dropdown-boxEdit mode is a plain bool and the caller flips it. While the list is open the rest of the panel is locked and the dropdown draws last, or clicks fall through to whatever sits underneath.
combo-boxThe simple one. No edit mode, no overlay, no locking: clicking advances through the options and writes an int cell.
list-viewTwo int cells, a scroll index and an active row, over a semicolon-separated string. The scroll index is seeded non-zero, because a zero scroll looks the same as an unwired one.
list-view-exThe only control taking an array of strings rather than a joined one, and it adds a third out-param for the row under the pointer. The array is built and freed inside the wrapper, so examples pass a plain vector.
tab-barReturns a result code rather than a tab index: the tab to close is the active one. Close buttons only appear when TAB_CLOSE_BUTTON is set on the tab bar.

containers

previewbb namewhat it shows
panel-group-boxNeither container clips its children or holds state. They draw a frame, and the caller places what goes inside.
scroll-panelThe suite's only Rectangle * out-param. raygui reports the visible view back, which the example uses to scissor its content: 386 by 221 inside a 400 by 260 panel, once the scrollbars are subtracted. Ported from raygui's own example.
window-boxDraws a titled frame and reports its close button, and manages nothing else. Visibility is a bool cell the caller keeps, along with a way back.
floating-windowDragging is not a raygui feature. The title-bar hit test, the offset and the movement are all the caller's, in float cells. Ported from raygui's own example. The drag itself is not covered by any gate here.

dialogs

previewbb namewhat it shows
message-boxReturns a result code and writes the button index into a cell: 0 for the window's own close button, then 1 upward in semicolon order. Modality is the caller's job, since raygui draws no scrim and blocks nothing.
custom-input-boxThe same dialog as text-input-box, assembled by hand from a panel, a label, a text box and two buttons. What that buys is validation: OK stays disabled while the field is empty. Ported from raygui's own example.

color

previewbb namewhat it shows
color-pickerA Color * cell, seeded a distinctive orange so a failed readback could not hide. The swatch is drawn from the cell's own components, and the alpha strip sits over a chequer so transparency is visible.
color-picker-hsvHolds a Vector3 of hue, saturation and value rather than a colour. The swatch converts back through ColorFromHSV, so both representations are on screen agreeing.

styling

previewbb namewhat it shows
style-selectorCycles the six vendored themes plus raygui's built-in default. Palette and embedded font both come from the .rgs. Shown here with cyber, since the default theme shows nothing about styling. Ported from raygui's own example.
gui-stateThe global-state API, which has no bounds of its own. The same three controls under each forced state, then alpha, then lock. All three are global and sticky, so every row resets immediately.

The six vendored themes

style-selector cycles these. Each .rgs carries a palette and its own embedded font, and the font changing is the clearest sign the file loaded.

themetheme
ashescandy
cyberdark
sunnyterminal

The jank shape

raygui's C API meets one jank rule that shapes everything else in this repo: a native value cannot cross a jank function boundary. Every design choice below follows from that, and each claim names the file that proves it.

If you have read b12n-raylib-jnk's native-value-lifetimes.md and cpp-interop-toolbox.md, you know the general rules already. This page covers what binding a GUI library added on top, including one place where a rule stated there turns out to be narrower than it sounds.

There is no binding layer

Worth saying first, because it is the biggest difference from the jolt port of the same library. jank compiles through C++, so raygui.h is the binding. There is no signature to declare, no struct layout to describe, no ABI to get wrong:

(ns net.b12n.raygui-jnk.raygui
  (:include "raylib.h" "raygui.h"))

(cpp/GuiButton bounds "Click me")   ; the real function, resolved by clang

The sibling raygui-jlt writes 61 defcfn forms to reach the same place. Here the count of hand-written signatures is zero. What replaces that work is the boundary rule.

The rule, and the one place it bends

A Rectangle, Color, Vector2 or Font has no conversion trait, so it cannot be passed to or returned from a defn. Integers, floats, bools and strings cross freely.

Every control wrapper is built so the native value is born and consumed inside the same function body:

(defn button! [x y w h text]
  (= 1 (int (cpp/GuiButton (cpp/Rectangle (cpp/float x) (cpp/float y)
                                          (cpp/float w) (cpp/float h))
                           text))))

x y w h arrive as ordinary jank numbers. The Rectangle never leaves that body. An int comes back. Examples in this repo never see a Rectangle at all, which lands in the same place the jolt port reached for a different reason: there it avoided a per-frame allocation, here the type simply cannot travel.

Closures count as boundaries too, so dotimes and doseq are out wherever a native pointer is in scope. loop/recur is inline and works. Several examples iterate that way for no other reason.

Where the sibling repo's rule is narrower than it reads

native-value-lifetimes.md says boxing cannot be wrapped in a function, and gives this as the failing case:

(defn box-it [s] (cpp/box (cpp/new cpp/Shader s)))   ; fails

That holds when the argument is native. By the time the body runs, s has already arrived as an object_ref, and there is nothing left to box. It does not hold when the argument is trait-convertible, because then no boxing has been lost on the way in:

(defn fcell [v] (cpp/box (cpp/new cpp/float (cpp/float v))))   ; works

(cpp/float v) on a jank number is a conversion rather than a re-box. Every cell constructor in raygui.jank relies on this. Reading the broader claim alone would have pushed all of them into macros for no benefit, so the distinction is worth carrying: the rule is about native arguments, not about defn as such.

Cells

raygui keeps no state. The application owns it and C wants a pointer to it, so this repo allocates a typed native slot once, outside the frame loop, and hands raygui its address:

(let [volume (g/fcell 0.35)]
  (loop [frame 0]
    (g/slider! 20.0 40.0 200.0 24.0 nil nil volume 0.0 1.0)
    (g/fvalue volume)))                     ; => 0.3499999940395355

Six types cover the whole suite: fcell, icell, bcell, ccell for a Color, v2cell and v3cell. Each has a matching reader and a reset!. Text is the seventh and behaves differently, so it gets its own section below.

A pointer passed as a jank function parameter arrives as an object_ref, which is why cells are boxed rather than raw. Allocate them before the loop, never inside it. A cell allocated per frame leaks at sixty a second, and for text cells that exhausts the heap in a long run.

Two argument shapes the compiler will not separate for you

This is the sharpest trap in the whole binding, because both shapes are spelled the same in C and only one of them takes a cell.

Controls with a bool * out-param, like GuiToggle and GuiCheckBox, take a cell. Controls with a plain bool editMode, like GuiTextBox, GuiDropdownBox, GuiSpinner and GuiValueBox, take an ordinary jank boolean, and the caller owns the mode:

(when (pos? (g/text-box! 20.0 40.0 200.0 30.0 buf 64 (g/bvalue editing?)))
  (g/breset! editing? (not (g/bvalue editing?))))

Pass a cell where the plain bool belongs and it compiles cleanly, because C++ converts a pointer to true. The control then sits permanently in edit mode. The spike behind this repo made exactly that mistake and did not notice, because the text box still rendered and still accepted text. Only reading the header showed the argument was never a pointer.

Text buffers

GuiTextBox wants a mutable char *. cpp/new does not make arrays, so the buffer comes from cpp/MemAlloc with a cast, seeded through raylib's own TextCopy:

(defn tcell [s cap]
  (let [p (cpp/unsafe-cast (:* cpp/char) (cpp/MemAlloc (cpp/uint32_t cap)))]
    (cpp/TextCopy p s)
    (cpp/box p)))

Reading one back needs a cast, not TextFormat. jank cannot call variadic C functions at all, so raylib's usual string helper is unavailable here:

(defn tvalue [c]
  (str (cpp/cast (:* (:const cpp/char)) (cpp/unbox (:* cpp/char) c))))

text-box.jank prints the buffer's length beside the field for this reason. A readback that silently failed would show up in the picture rather than only in a log.

Optional captions need two call sites

Many raygui controls accept NULL for their caption. jank cannot express that by passing nil, and the failure arrives late:

(g/line! 20.0 90.0 380.0 12.0 nil)
;; compiles fine, then at runtime:
;; invalid object type (expected persistent_string found nil)

Coercing at the argument position does not work either. (if text text cpp/nullptr) fails during code generation, because the two branches have incompatible C++ types:

error: assigning to 'std::nullptr_t' from incompatible type
       'jank::runtime::oref<jank::runtime::object>'

What works is two complete calls, one per type, which a macro emits so the duplication stays in one place:

(defmacro ^:private opt-text [f x y w h text & more]
  `(if ~text
     (~f (cpp/Rectangle (cpp/float ~x) (cpp/float ~y) (cpp/float ~w) (cpp/float ~h))
         ~text ~@more)
     (~f (cpp/Rectangle (cpp/float ~x) (cpp/float ~y) (cpp/float ~w) (cpp/float ~h))
         cpp/nullptr ~@more)))

A macro rather than a function, since neither a Rectangle nor a const char * could cross a function boundary to get there.

The slider family is the exception. GuiSlider, GuiSliderBar and GuiProgressBar each take two optional captions, and covering both positions would need four call sites. They substitute "" instead, which is safe for a specific reason worth checking rather than assuming: raygui guards each caption with if (textLeft != NULL) around a draw whose width is GuiGetTextWidth(textLeft), and that function returns 0 for an empty string because its scan loop exits on the terminator immediately. An empty caption reserves no space and draws nothing.

Arrays

GuiListViewEx is the only control in the suite taking char **. The array is built, used and freed inside one function body, since neither it nor its elements can travel:

(let [arr (cpp/unsafe-cast (:* (:* cpp/char)) (cpp/MemAlloc (cpp/uint32_t (* 8 n))))]
  (loop [i 0] ...)                       ; fill with loop/recur, not doseq
  (cpp/GuiListViewEx bounds arr (cpp/int n) ...)
  (loop [i 0] ...)                       ; free each element
  (cpp/MemFree arr))

Examples pass a plain jank vector of strings and never see the array. list-view-ex.jank is the one that uses it. The pointer width is hardcoded to 8, because jank has no sizeof and every platform this repo has run on is 64-bit.

Out-params that are structs

GuiScrollPanel reports the visible view back through a Rectangle *. A native struct cannot be returned across a boundary, so the wrapper reads its four floats and hands back numbers:

(defn scroll-panel! [x y w h text cx cy cw ch scroll-cell]
  (let [view (cpp/new cpp/Rectangle)]
    (cpp/GuiScrollPanel ... view)
    [(+ 0.0 (.-x (cpp/* view))) (+ 0.0 (.-y (cpp/* view)))
     (+ 0.0 (.-width (cpp/* view))) (+ 0.0 (.-height (cpp/* view)))]))

scroll-panel.jank scissors its content to that rectangle, which is what shows the value is live. raygui reports 386 by 221 inside a 400 by 260 panel, having subtracted its own scrollbars.

ColorFromHSV has the same problem in the other direction: it returns a Color by value. hsv->rgb consumes it inside the wrapper and returns [r g b].

Style colours are byte-swapped and signed

raygui stores style colours as 0xRRGGBBAA. raylib's Color packs little-endian as 0xAABBGGRR. Feeding one straight to the other produces a plausible wrong colour rather than an error, and GetColor is the conversion raygui's own README uses.

jank adds a second half. GuiGetStyle returns unsigned int, and jank boxes it signed:

GuiGetStyle(DEFAULT, BACKGROUND_COLOR)     ->  -168430081     (0xF5F5F5FF)
GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL)  -> -2088532993     (0x838383FF)

Both correct, both negative. Widening needs cpp/uint32_t, since cpp/unsigned_int does not exist and the compiler answers Unable to find 'unsigned&#95;int' within the global namespace. clear-background! does the whole conversion once so no example repeats it.

Constants come from the wrapper, not from cpp/

Examples do not include raygui.h. For a while they still reached cpp/DEFAULT and it worked, because the wrapper's own include had made the enum visible in the shared C++ session. That is an accident to rely on, and the failure mode when an index is wrong gives you nothing: a style set on the wrong control does nothing at all, silently.

That happened here. A guessed TABBAR of 20, when the real value is 11, meant the tab close buttons simply never appeared. Nothing raised, nothing logged.

raygui.jank now exports the enums as jank vars, each read from the C enum so none can drift from the vendored header:

(def TABBAR (int (cpp/cast cpp/int cpp/TABBAR)))

The cast is required. (int cpp/TABBAR) on its own throws Can't convert GuiControl to integer.

Small things that cost time

  • cpp/int on a jank double throws at runtime with invalid object type (expected integer found small&#95;real). Box first: (cpp/int (int x)).
  • case will not compile a clause list whose results are C string literals. jank raises no viable overloaded '=' from inside clojure/core.jank, naming neither your form nor your file. cond works. message-box.jank uses it.
  • There is no JVM, so Math/round, format and .indexOf are all absent. sliders.jank carries a hand-rolled fmt2, borrowed from b12n-raylib-jnk's.
  • Several native values in one println fails during code generation with member reference base type 'i64' is not a structure or union. Print one per call.
  • rlDrawRenderBatchActive lives in rlgl.h, so any namespace taking a screenshot needs it in :include.

What is not verified

No control in this repo has ever been clicked by a test, and none can be. The sibling b12n-raylib-jlt measured that synthetic clicks do not actuate a raylib or GLFW app at all: 0 of 8 clicks and 0 of 2 drags delivered at every hold duration up to 300ms, against 3 of 3 for pointer motion. Every control's behaviour here has been checked by a person moving a real mouse, or not at all. The automated claim is that the suite renders correctly.

x86-64 and Linux are both untested. Unlike the jolt port this carries no ABI risk of its own, since clang handles the struct passing, but nobody has run it.

What the gates do not catch

The most useful page here if you are about to add an example.

bb check compiles. bb lint lints. Neither looks at a pixel, and a GUI library fails in ways that survive both. Every item below happened during this port, was caught by looking at a screenshot or reading the C, and would have shipped otherwise.

1. A screenshot that was never written, with a zero exit code

TakeScreenshot resolves its argument against CORE.Storage.basePath, fixed at InitWindow. Not the live working directory, so ChangeDirectory cannot redirect it. From rcore.c:1844:

strncpy(path, TextFormat("%s/%s", CORE.Storage.basePath, fileName), MAX_FILEPATH_LENGTH - 1);

Hand it /tmp/x.png and it builds <basePath>//tmp/x.png, which cannot be created. raylib logs this and moves on:

WARNING: SYSTEM: [.../raygui-examples//tmp/basic-controls.png] Screenshot could not be saved

The process still exits 0. A gate that only checks the exit code passes with no PNG at all. maybe-screenshot! captures by basename, where basePath resolves cleanly, then renames the result. bb shot checks the file exists afterwards and fails loudly when it does not.

2. A screenshot with three quarters of the frame missing

FLAG_WINDOW_HIGHDPI makes raylib double-count the DPI scale inside TakeScreenshot. The first capture from this repo came back 1680 by 880 from a 420 by 220 window, with the content drawn correctly into the bottom-left quadrant and the rest black.

No example here sets that flag. The sibling b12n-raylib-jnk does set it and can, because it records through screen capture rather than TakeScreenshot.

3. A blank frame from the missing batch flush

raylib defers batched geometry until EndDrawing, so a mid-frame capture without rlDrawRenderBatchActive writes a valid, perfectly blank PNG from a program that just drew a screen full of controls.

This one is inherited rather than rediscovered. The jolt port hit it first and maybe-screenshot! here flushes for the same reason. The symbol lives in rlgl.h, which is why every example that screenshots carries (:include "raylib.h" "rlgl.h").

4. A lint gate that linted nothing

clj-kondo does not discover .jank files. Point it at a directory and it scans for .clj, .cljs and .cljc, finds none, and reports success very fast:

linting took 51ms, errors: 0, warnings: 0

That was this repo's lint gate for a phase and a half, over four real source files. bb lint now globs every file explicitly and prints the count it is about to check, so a number that stops climbing is visible.

b12n-raylib-jnk's .clj-kondo/config.edn documents the same thing and this repo adopted it wholesale.

5. A gate that reported problems and passed anyway

clj-kondo exits 2 on warnings and 3 on errors. bb lint discarded that exit code, so the pre-commit hook printed two unresolved-namespace warnings and let the commit through regardless.

Both now propagate. The fix was checked with a deliberate broken symbol rather than by reading the change, which matters: the first version of this same gate looked correct and was not.

6. A style that never loaded, under a label claiming it did

GuiLoadStyleFromMemory is fed by LoadFileData, which resolves against the process working directory. A bare "style_cyber.rgs" works when run from the repo root and finds nothing anywhere else. raylib logs a warning, raygui keeps whatever style was already loaded, and the on-screen label goes on naming the one you asked for.

Every path goes through rl/styles-path, which anchors against the vendored directory and honours RAYGUI_STYLES_DIR.

Verifying this properly means checking each theme against the palette it claims, not confirming that a screenshot exists. All seven were captured separately and looked at. cyber is navy and amber, terminal green on black, candy cream and salmon, sunny gold, ashes and dark two distinct greys. Each also carries its own embedded font, and the font changing is the clearest single sign that the .rgs actually loaded.

7. A colour that is wrong but plausible

raygui stores style colours as 0xRRGGBBAA and raylib's Color packs 0xAABBGGRR. Feed one to the other and red and blue swap into something that still looks deliberate. The jolt port measured this on the cyber background: 0x81C0D0FF is a light blue, and passing it straight to ClearBackground renders salmon pink.

jank adds the sign. GuiGetStyle returns unsigned int, jank boxes it signed, and the widening cast is cpp/uint32_t, since cpp/unsigned_int does not exist. clear-background! does the conversion centrally so no example can get it half right.

A glance at the screenshot will not catch this on its own, because a wrong colour is still a colour. Comparing the render against the style's declared value will.

8. nil where C wants a string

Passing nil for an optional caption compiles. It throws when that line actually runs:

error: invalid object type (expected persistent_string found nil)

bb check compiles every example and never draws a frame, so it cannot see this. The screenshot gate caught it three separate times here, in line!, panel! and then across all six colour controls.

The wrapper handles it now, so an example can pass nil freely. If you add a control that takes an optional caption, route it through opt-text or you will meet this again.

9. A style property set on the wrong control

Nothing at all happens. No error, no log line, no visual difference except the feature you wanted quietly not being there.

A guessed TABBAR index of 20, when the real value is 11, meant TAB_CLOSE_BUTTON was set on a control that has no such property, and the tab close buttons never appeared. The example compiled, ran, screenshotted, and looked fine unless you knew what was supposed to be there.

raygui.jank exports every raygui enum as a jank var, each read from the C enum. Use g/TABBAR, never a literal and never cpp/TABBAR. Examples do not include raygui.h, so the cpp/ form only resolves for them by accident.

10. Mouse interaction, all of it

Nothing in this repo has been clicked by a test, and nothing can be. b12n-raylib-jlt measured that synthetic clicks do not actuate a raylib or GLFW app at all: 0 of 8 clicks and 0 of 2 drags delivered at every hold duration up to 300ms, against 3 of 3 for pointer motion.

So the automated claim this suite makes is that it renders correctly. Whether a dropdown opens, a slider drags, or a tab closes has been checked by a person with a real mouse, or not at all. floating-window.jank says so in its own docstring, because its whole subject is a drag that no gate here exercises.

The habit that catches the rest

Build each example so its own screenshot cross-checks its state. Print the cell value next to the control, seed a scroll index to something other than zero, seed a spinner above its own maximum so the clamp has to show.

spinner-value-box.jank starts its third spinner at 999 against a range ending at 50. The screenshot reads 50. That is a working clamp, rather than a claim that the clamp works.

raygui-jnk: raygui examples in jank. raylib's immediate-mode GUI library, ported to native Clojure on C++/LLVM, with raygui built as an ordinary jank package.