raddy — Nuklear immediate-mode GUI for naylib/raylib (desktop + PS Vita)
raddy is a Nim wrapper around the Nuklear immediate-mode GUI library. It targets both desktop (via naylib/raylib with OpenGL) and PS Vita (via raylib_console), sharing a single core that emits no platform-specific imports.
Nuklear builds a draw-command queue each frame — a list of NK_COMMAND_* records describing lines, rectangles, text, and so on. raddy translates that queue into raylib draw calls at render time. This is command-queue rendering, not vertex-buffer rendering. The distinction matters: the same Nuklear core runs on every platform; only the final dispatch step (raylib on desktop, raylib_console on Vita) differs.
The import raddy surface is backend-free. Rendering and input plumbing live in separate backend modules that consumers import explicitly. This keeps the core linkable on Vita without pulling in any desktop rendering machinery.
nimble install "https://github.com/birbparty/raddy"Desktop usage also requires naylib:
nimble install naylibMinimal desktop example:
import raylib
import raddy
import raddy/backend/ctx_bundle
import raddy/backend/render
import raddy/backend/pump_naylib
import raddy/backend/raylib_api
var gFont: Font
var clickCount = 0
proc buildUI(ctx: ptr nk_context) =
let bounds = nk_rect(x: 20, y: 20, w: 300, h: 200)
let flags = NK_WINDOW_BORDER.nk_flags or NK_WINDOW_TITLE.nk_flags
if not raddyBegin(ctx, "demo", bounds, flags):
raddyEnd(ctx); return
raddyLayoutRowDynamic(ctx, height=30, cols=2)
if raddyButton(ctx, "Click"):
inc clickCount
raddyLabel(ctx, "Count: " & $clickCount, NK_TEXT_LEFT)
raddyEnd(ctx)
proc main() =
initWindow(800, 600, "raddy demo")
setTargetFPS(60)
gFont = getFontDefault()
let bundle = raddyBundleCreate(cast[ptr RFont](addr gFont), float32(gFont.baseSize))
let ctx = raddyBundleCtx(bundle)
let rt = loadRenderTexture(800, 600)
while not windowShouldClose():
beginTextureMode(rt)
clearBackground(Color(r:30,g:30,b:30,a:255))
raddyNaylibPump(ctx)
buildUI(ctx)
var overflow = false
raddyRender(ctx, rt.texture.height, overflow)
endTextureMode()
beginDrawing()
drawTexture(rt.texture,
source=Rectangle(x:0,y:0,width:float32(rt.texture.width),height:float32(-rt.texture.height)),
dest=Rectangle(x:0,y:0,width:800,height:600),
origin=Vector2(x:0,y:0),rotation=0,tint=White)
endDrawing()
raddyBundleFree(bundle)
closeWindow()
main()raddy renders at multiple font sizes in one UI by switching the active Nuklear
font between widget groups. Bake one Font per pixel size (do not scale a
single atlas — it blurs), wrap each in a caller-owned RaddyFont, and switch
with setRaddyFont (handle form) or raddyBundleSetFont (bundle form — see
below):
import raddy
import raddy/backend/font # RaddyFont, raddyMakeFont, raddyFontHandle
import raddy/backend/raylib_api # RFont
# Module scope — each Font and its RaddyFont must outlive every frame it is
# active, at a STABLE address (both pointers escape into Nuklear).
var small, large: Font
var smallRf, largeRf: RaddyFont
proc loadFonts() = # call after initWindow (baking needs a GL context)
small = loadFont("my.ttf", 16, 0)
large = loadFont("my.ttf", 32, 0)
# RFont is raddy's separate Nim view of raylib's Font (same C struct, distinct
# Nim identity) — bridge the ptr with cast[ptr RFont]. Pass the bake ppem
# (baseSize) as the pixel height: it MUST equal the size the font was baked at,
# or measure and draw diverge and text clips/overflows.
smallRf = raddyMakeFont(cast[ptr RFont](addr small), float32(small.baseSize))
largeRf = raddyMakeFont(cast[ptr RFont](addr large), float32(large.baseSize))
# Per frame, inside raddyBegin/raddyEnd. `ctx` is raddyBundleCtx(bundle) from the
# Quickstart above.
setRaddyFont(ctx, raddyFontHandle(smallRf)) # smallRf active; everything next is 16 px
raddyLabel(ctx, "small")
setRaddyFont(ctx, raddyFontHandle(largeRf)) # mid-frame switch is supported → 32 px
raddyLabel(ctx, "BIG")If you hold the bundle (not a raw ctx), the bundle form switches the same way
but takes the RaddyFont value rather than a handle:
raddyBundleSetFont(bundle, largeRf) # equivalent to setRaddyFont(raddyBundleCtx(bundle), raddyFontHandle(largeRf))The switch is forward-only (affects only widgets emitted after it) and
persists across frames (the per-frame clear, nk_clear, does not reset it —
re-set each frame if you want a deterministic starting font). A single-size app
just bakes one font; the machinery is additive. See
docs/prompts/font-contract.md for the full
lifetime contract (the fonts and RaddyFonts must stay at a stable address —
no frame-loop locals, no reallocating seq storage — until after raddyRender).
Do not use nimble build — the srcDir flatten causes import path issues. Compile directly:
nim c \
--mm:orc \
--path:src \
--path:"$(nimble path naylib)" \
--passC:"-I$(nimble path naylib)/raylib" \
examples/demo.nimnimble test # unit/spec suite — stubs raylib, no GL context required
nimble acceptance # real-raylib acceptance spec — needs a GL/window contextnimble test links a stubbed raylib (no window, no GPU) and runs every
tests/test_*.nim. The acceptance task is separate: it links real
raylib (naylib) and opens a hidden window to exercise the font pipeline against
the real LoadFont/MeasureTextEx — it runs tests/acceptance_smoke.nim
(deliberately named without test_ so the stubbed suite never picks it up). On
a headless CI host, wrap it with a virtual framebuffer:
xvfb-run -a nimble acceptance.
tests/assets/unscii-16.ttf is the bundled acceptance-test font —
Unscii 16, public domain / CC-0. It is a test
asset only: raddy's library bundles no font, and consumers supply their own.
Provenance and the CC-0 terms (and the GPL carve-out that this non-full
variant avoids) are recorded in tests/assets/LICENSE.txt.
Nuklear exposes two rendering backends: vertex-buffer (fills a GPU-ready buffer) and command-queue (emits a linked list of draw commands). raddy uses the command-queue path.
Why:
- Backend-agnostic dispatch. Nuklear produces
NK_COMMAND_RECT,NK_COMMAND_TEXT, etc. — abstract draw intents.raddyRenderiterates them withnk__begin/nk__nextand maps each to a raylib call. The mapping is the same whether raylib is naylib on desktop or raylib_console on Vita. - No geometry shaders required. Vertex-buffer rendering assumes a shader pipeline for atlas-based glyph rendering. raylib_console on Vita does not expose that pipeline. Command-queue sidesteps it entirely.
- Fits in 64 KiB fixed memory. The Vita build uses
nk_init_fixedwith a 64 KiB stack buffer (-d:vitaor-d:raddyFixed). A vertex buffer would overflow that budget. The command queue is compact by design.
Desktop uses nk_init_default (heap allocation) unless -d:raddyFixed is set.
| Widget | Proc |
|---|---|
| Window | raddyBegin / raddyEnd |
| Label | raddyLabel |
| Button | raddyButton |
| Checkbox | raddyCheckbox |
| Slider | raddySlider |
| Text edit | raddyEdit |
| Combo box | raddyCombo |
| Group | raddyGroupBegin / raddyGroupEnd |
| Property | raddyProperty |
| Spacing | raddySpacing |
Layout rows: raddyLayoutRowDynamic, raddyLayoutRowStatic, raddyLayoutRowBegin/Push/End.
See docs/api/widgets.md for full signatures and usage.
| Platform | Renderer | Input pump |
|---|---|---|
| Desktop | naylib (OpenGL via GLFW) | raddy/backend/pump_naylib |
| PS Vita | raylib_console | raddy/backend/pump_vita |
import raddy has no naylib or raylib_console imports. Backend modules are pulled in explicitly by the application. The core compiles cleanly on Vita without any desktop rendering headers in scope.
See docs/command-coverage.md for the full dispatch matrix, including provisional items (ARC, TRIANGLE winding).
raddy is MIT licensed. Nuklear is vendored from https://github.com/Immediate-Mode-UI/Nuklear under the Unlicense (public domain). See src/raddy/vendor/nuklear.h for the full license.
The bundled test font tests/assets/unscii-16.ttf is Unscii — public domain / CC-0 (the non-full variant; the GPL unscii-full/Unifont-derived files are deliberately not vendored). See tests/assets/LICENSE.txt.