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.
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.
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.
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-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.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.
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.
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.
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.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.
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.
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.
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.
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.
--disable-sandboxThere 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
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.
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.
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.
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.
| theme | theme | ||
|---|---|---|---|
ashes | ![]() | candy | ![]() |
cyber | ![]() | dark | ![]() |
sunny | ![]() | terminal | ![]() |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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].
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_int' within the global namespace. clear-background! does the whole conversion once so no example repeats it.
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.
cpp/int on a jank double throws at runtime with invalid object type (expected integer found small_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.Math/round, format and .indexOf are all absent. sliders.jank carries a hand-rolled fmt2, borrowed from b12n-raylib-jnk's.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.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.
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.
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.
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.
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").
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.
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.
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.
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.
nil where C wants a stringPassing 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.
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.
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.
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.