Header-only, multiplatform HTML/CSS UI for native OpenGL applications.
Luna UI parses HTML and CSS, lays out a lightweight DOM, and renders it directly with OpenGL. The same core is used by the built-in Windows, Linux, macOS, iOS, Android, and Web hosts.
luna-ui.h is the main public include. Define LUNA_UI_IMPLEMENTATION in exactly one translation unit and the appropriate native host is selected automatically.
This frame is rendered by the real examples/example.c application and the current Luna UI renderer.
The screenshot was captured from the real GLFW/OpenGL application with Luna UI's built-in F12 framebuffer capture path at 960 Γ 640. It replaces the older broken and stale example-state images.
These screenshots were captured by running the migrated demo applications in examples/ with the real GLFW/OpenGL host:
| Aurora dashboard | CSS/OpenGL showcase |
|---|---|
![]() |
![]() |
| Modal, backdrop and focus trap | Select menu and native controls |
|---|---|
![]() |
![]() |
The interaction shots above are not mockups: the demo was launched under the
GLFW/OpenGL host, the controls were clicked through Luna UI's input system, and
the resulting framebuffer was captured with the built-in F12 screenshot path.
| Minimal card sample | Profile studio and text fields |
|---|---|
![]() |
![]() |
The samples now bind F12 directly to Luna UI's PNG capture path. For an
automated, reproducible capture, set LUNA_SCREENSHOT to the output filename;
the sample renders three settling frames, writes the PNG, and exits. The images
above use the samples' real 800 Γ 500 and 860 Γ 620 framebuffers respectively.
Build and run the gallery demos locally with:
make -C examples
./examples/luna-ui --size 1024x700
./examples/opengl_gui --size 1024x700
./examples/sample_01
./examples/sample_02
./examples/tooltip
# or: LUNA_SCREENSHOT=sample.png ./examples/sample_01Luna UI supports dynamic pseudo-classes on ancestor selectors, so a tooltip can use the same nested HTML/CSS pattern as the web. Add keyboard focus selectors alongside hover so the description is not pointer-only:
<button class="tooltip">
Save
<span class="tooltip-text">Save the current document</span>
</button>.tooltip { position: relative; }
.tooltip-text {
position: absolute;
opacity: 0;
visibility: hidden;
}
.tooltip:hover .tooltip-text,
.tooltip:focus .tooltip-text,
.tooltip:focus-visible .tooltip-text {
opacity: 1;
visibility: visible;
}Build and run the complete example with make -C examples tooltip followed by
./examples/tooltip. It includes above/below placement and an arrow made with
::before.
#define LUNA_UI_IMPLEMENTATION
#include "luna-ui.h"
static const char* html =
"<body><main class=\"card\">"
"<h1>Hello, Luna UI</h1>"
"<p>HTML/CSS rendered by native OpenGL.</p>"
"</main></body>";
static const char* css =
"body{display:flex;align-items:center;justify-content:center;"
"background:#0f172a;color:white;}"
".card{padding:32px;border-radius:20px;background:#1e293b;}";
int main(void) {
LunaAppConfig app = {0};
app.title = "My Luna app";
app.width = 960;
app.height = 640;
app.resizable = 1;
app.vsync = 1;
app.html = html;
app.css = css;
return luna_app_run(&app);
}HTML/CSS can be supplied either as strings (html, css) or paths (html_path, css_path). examples/example.c also demonstrates registering a native C callback for an onclick handler and changing DOM text at runtime.
The repository currently consists of a shared UI/rendering core plus header-only platform hosts:
luna-ui.hβ DOM, CSS, layout, input, OpenGL renderer, public UI API, andLunaPlatformhost ABI v3.luna_windows.hβ Win32 + WGL/OpenGL host.luna_glfw.hβ GLFW + OpenGL Linux host (the default). Luna UI does not call X11, Wayland, GLX, or EGL directly from this backend.luna_kms.hβ bare-console Linux host: DRM/KMS + GBM + EGL + libinput, no display server. Selected with-DLUNA_UI_BACKEND_KMS.luna_macos.hβ Cocoa + OpenGL host.luna_ios.hβ UIKit + OpenGL ES host.luna_android.hβ NativeActivity + EGL/OpenGL ES host.luna_web.hβ Emscripten + WebGL 2 host.luna-window.hβ optional reusable window chrome, themes, dialogs, toast/notification UI, and desktop file-dialog/file-manager support.cssparser.h,stb_truetype.h,stb_image.h,stb_image_write.hβ bundled single-header dependencies.
There is no separate luna_platform.h. Platform selection is performed from luna-ui.h in the implementation translation unit.
The built-in hosts provide the native window/context lifecycle and connect platform services to the shared LunaPlatform ABI. The current v3 ABI includes hooks for:
- OpenGL procedure lookup and monotonic time
- resource and font loading
- clipboard access
- IME/text-input activation
- cursor changes and display scale
- close, minimize, maximize, title changes, move and resize requests
- redraw requests
- PNG saving
- system notifications
LunaPlatform includes both struct_size and api_version (LUNA_UI_API_VERSION is currently 0x00030000) so custom hosts can validate the host structure they provide.
Common helpers include:
void luna_clipboard_set(const char* utf8);
char* luna_clipboard_get(void);
void luna_clipboard_free(char* utf8);
float luna_platform_scale(void);LunaTouchEvent carries a stable contact ID, phase, logical coordinates,
pressure, contact radius, tool type (finger, stylus, or eraser), and stylus
tilt. Native hosts forward it with luna_touch(); applications that need raw
gestures can install LunaAppConfig.on_touch and consume selected events.
The built-in recognizer supports taps, control dragging, one-finger scrolling,
and multiple active contacts. iOS, Android, Web, and Windows (WM_POINTER)
provide native touch data. GLFW has no touch API, so the Linux GLFW host receives
the compatibility mouse events supplied by the window system. A Linux native
host can define LUNA_UI_NO_PLATFORM and feed libinput/Wayland contacts directly
to luna_touch() without adding those dependencies to Luna UI.
Applications that want consistent Luna-styled desktop chrome can additionally use luna-window.h:
#define LUNA_UI_IMPLEMENTATION
#include "luna-ui.h"
#define LUNA_WINDOW_IMPLEMENTATION
#include "luna-window.h"It provides standard titlebar/resize markup and CSS, light/dark/custom themes, alerts, confirmation and prompt dialogs, toast notifications, and notification fallback UI.
For the reusable desktop file dialog/file manager implementation, define LUNA_WINDOW_FILE_DIALOG_IMPLEMENTATION in the same translation unit as both implementation macros:
#define LUNA_UI_IMPLEMENTATION
#include "luna-ui.h"
#define LUNA_WINDOW_IMPLEMENTATION
#define LUNA_WINDOW_FILE_DIALOG_IMPLEMENTATION
#include "luna-window.h"The file-dialog API supports file manager, open file(s), select folder, and save file modes through LunaFileDialogConfig and luna_file_dialog_run().
The default Linux host uses GLFW for windowing/input and OpenGL for rendering:
cc -O2 -std=c11 examples/example.c -o luna-example \
-lGL -lm -lglfwNo direct -lX11 dependency is required by Luna UI itself. The GLFW package used by your system may of course depend on X11 or Wayland internally.
For a machine with no display server β a kiosk or a shell that starts right after boot β build the same source against the KMS host instead. It takes the DRM master's primary plane, page-flips scanout buffers straight out of EGL/GBM, draws the pointer on the hardware cursor plane, and reads the seat through libinput:
cc -O2 -std=c11 -DLUNA_UI_BACKEND_KMS examples/example.c -o luna-example \
$(pkg-config --cflags libdrm xkbcommon) \
-lEGL -lgbm -ldrm -linput -ludev -lxkbcommon -lmIt needs read/write access to /dev/dri/cardN (the video group, or root) and
a seat it can claim through seatd/logind. LUNA_DRM_DEVICE picks a specific
node and LUNA_SEAT a specific seat.
luna_app_run() owns the loop, which is what an application wants. A shell
usually cannot give that up: it has to wait on its own descriptors, decide its
own repaint cadence, and pick a backend at runtime. Such an embedder defines
LUNA_UI_NO_PLATFORM, includes the host headers itself with
LUNA_UI_HOST_PRELUDE / LUNA_UI_HOST_BODY, and drives LunaHostOps:
const LunaHostOps* host = use_kms ? luna_host_kms() : luna_host_glfw();
if (!host->start(&host_cfg)) return 1; /* installs LunaPlatform too */
luna_init(&(LunaInitConfig){ w, h, luna_platform_get_proc(), 0 });
while (!host->should_close()) {
host->wait_events(timeout_ms, my_fds, my_fd_count);
luna_render(w, h);
host->swap_buffers();
}wait_events() sleeps on host input and the caller's descriptors at once,
returning which of them became readable. The built-in luna_app_run() loops are
written on exactly these ops, so both paths share one implementation of each
backend. apps/luna-shell is the worked example.
cl /std:c11 /O2 examples\example.cThe Windows host uses Win32/WGL. With MinGW, link the normal Win32/OpenGL libraries (for example opengl32, gdi32, user32, imm32, and shell32).
Compile the implementation translation unit as Objective-C or Objective-C++:
clang -O2 -x objective-c examples/example.c -o luna-example \
-framework Cocoa -framework OpenGL -framework CoreTextDeclaration-only users of luna-ui.h can remain ordinary C/C++ translation units.
emcc examples/example.c -o examples/luna-example.js \
-sUSE_WEBGL2=1 -sFULL_ES3=1 -sALLOW_MEMORY_GROWTH=1 \
-sEXPORTED_FUNCTIONS=_main,_malloc,_free,_luna_web_commit_utf8 \
-sEXPORTED_RUNTIME_METHODS=UTF8ToString,stringToUTF8,lengthBytesUTF8Serve the examples directory over HTTP and open examples/index.html.
Add examples/example_ios.m and the Luna headers to an iOS application target. Link UIKit, QuartzCore, OpenGLES, and CoreText. The iOS host supplies the application/view lifecycle, EAGLContext, touch input, and text input bridge.
The Android host uses android.app.NativeActivity, EGL, and OpenGL ES 3. The implementation exports ANativeActivity_onCreate.
NativeActivity has no main(), so the host calls luna_app_config() when the application defines it. A portable app defines that one function, calls it from its own main() on the other platforms, and can compile the implementation straight from the header β no per-app implementation file:
cc -x c -DLUNA_UI_IMPLEMENTATION -c luna-ui.h -o luna-ui.o # -x objective-c on AppleAlternatively the config function can be named before including the implementation:
#define LUNA_ANDROID_APP_CONFIG luna_android_make_config
#define LUNA_UI_IMPLEMENTATION
#include "luna-ui.h"
LunaAppConfig luna_android_make_config(void) {
LunaAppConfig app = {0};
app.html_path = "ui/main.html";
app.css_path = "ui/main.css";
app.vsync = 1;
return app;
}Link android, EGL, GLESv3, dl, and log. Pack HTML, CSS, images, and bundled fonts in the APK assets as appropriate.
The renderer can be embedded in another window system without any built-in host:
#define LUNA_UI_NO_PLATFORM
#define LUNA_UI_IMPLEMENTATION
#include "luna-ui.h"Create a compatible OpenGL context, populate LunaPlatform, call luna_set_platform(), initialize Luna with luna_init(), forward input events, and drive luna_update() / luna_render() from your own loop.
This is also the intended extension point for alternative window systems and future rendering hosts without changing the DOM/CSS/layout API.
- Define
LUNA_UI_IMPLEMENTATIONin exactly one translation unit. - Keep the Luna headers and bundled single-header dependencies in the include tree.
- On macOS/iOS, compile the implementation translation unit as Objective-C/Objective-C++.
- Use
LUNA_UI_NO_PLATFORMonly when supplying your own host. - Define
LUNA_WINDOW_IMPLEMENTATIONonly if using the optional common window layer.
The current renderer is shared across the native OpenGL/OpenGL ES/WebGL hosts. macOS OpenGL and iOS OpenGL ES are legacy Apple APIs, but they keep the host layer compact while the higher-level DOM, CSS, layout, input, and application APIs remain portable.
This project is licensed under the Mozilla Public License 2.0 (MPL-2.0).
Copyright (c) Yuichiro Nakada, Berry OS / Luna Desktop contributors.
See LICENSE for the full license text. Bundled third-party single-header libraries retain their original licenses.






