Guide
Getting started
This guide takes you from an empty folder to a working window, then explains the ideas you need to build real screens. When the window is up, the tutorial builds a small complete app in it.
Get CUI
CUI is a regular CMake project with no external dependencies. You need a C99 compiler and CMake 3.14 or newer:
| System | Install |
|---|---|
| Windows | Visual Studio 2019 or newer with "Desktop development with C++" (it includes CMake), or MinGW-w64 and CMake |
| macOS | xcode-select --install, then CMake (brew install cmake) |
| Debian, Ubuntu | sudo apt install build-essential cmake libx11-dev |
| Fedora | sudo dnf install gcc cmake libX11-devel |
| Arch | sudo pacman -S base-devel cmake libx11 |
To check that everything works, build CUI and run the smallest example:
git clone https://github.com/yappologistic/CUI.git
cd CUI
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/examples/hello # Windows with Visual Studio: build\examples\Release\hello.exe
build/examples/ also holds the gallery, which shows every component, and complete apps (mail, notes, music, a dashboard). To look before you build, each release has these apps ready to run for Windows, macOS and Linux. If something fails, see Troubleshooting.
With CMake (recommended)
cmake_minimum_required(VERSION 3.14)
project(my_app C)
add_subdirectory(CUI)
add_executable(my_app main.c)
target_link_libraries(my_app PRIVATE cui::cui)
cui_app_target(my_app)
Or fetch a release at configure time:
include(FetchContent)
FetchContent_Declare(cui GIT_REPOSITORY https://github.com/yappologistic/CUI.git GIT_TAG v1.3.1)
FetchContent_MakeAvailable(cui)
target_link_libraries(my_app PRIVATE cui::cui)
cui_app_target(my_app)
GIT_TAG names a release, so your build does not change under you. master works too and is the newest code.
cui_app_target sets up the executable the way a window app wants it. On Windows no console window opens and int main(void) stays the entry point. In release builds the linker keeps only the library code your app calls. That last part is what makes apps small: the first window below is about 250 KB with -DCMAKE_BUILD_TYPE=MinSizeRel and about 320 KB with Release (Linux, GCC, stripped), against about 470 KB without the call.
An installed CUI (cmake --install build) works the same way through find_package(cui REQUIRED).
Without CMake
Compile every .c file in src/, plus src/render/*.c, src/platform/cui_app.c, src/platform/cui_dialogs.c and the backend for your platform:
| Platform | Backend file | Link with |
|---|---|---|
| Windows | src/platform/cui_app_win32.c | user32 gdi32 opengl32 shell32 imm32 dwmapi comdlg32 ole32 |
| macOS | src/platform/cui_app_cocoa.m (compile with -fobjc-arc) | -framework Cocoa -framework QuartzCore -framework OpenGL |
| Linux | src/platform/cui_app_x11.c | -lX11 -ldl -lm -lpthread |
cc -std=c99 -O2 -Iinclude main.c src/*.c src/render/*.c \
src/platform/cui_app.c src/platform/cui_dialogs.c src/platform/cui_app_x11.c \
-lX11 -ldl -lm -lpthread -o my_app
One file
For a build system other than CMake, or to vendor CUI into a large codebase, use the single-file distribution, attached to every release or made from a checkout with python3 tools/amalgamate.py (it writes build/amalgamation/). It comes in two forms:
cui.handcui.c: addcui.cto your build and includecui.h, as with SQLite.cui_single.h: one header in the style of the stb libraries. Include it anywhere; in exactly one.cfile,#define CUI_IMPLEMENTATIONfirst, and include it before any system header there.
The includes differ. This guide writes #include <cui/cui.h> and #include <cui/cui_app.h>. With the single-file distribution both are one header beside your code, so write #include "cui.h" (or "cui_single.h") in their place and leave the second line out.
cui.c is the whole library, about 74,000 lines, and takes around half a minute to compile. Compile it once and link the object file; your own files then build in a moment:
cc -std=c99 -Os -ffunction-sections -fdata-sections -c cui.c -o cui.o # once
cc -std=c99 -Os main.c cui.o -Wl,--gc-sections -s -lX11 -ldl -lm -lpthread -o my_app # Linux; other systems link as in the table above
Those flags are what keep the app small. -ffunction-sections -fdata-sections with -Wl,--gc-sections lets the linker drop the components you do not use, and -s strips symbols: the first window below comes to about 265 KB. Compiling and linking everything in one plain cc -O2 main.c cui.c works too, and gives about 1.6 MB, because every component stays in. On macOS use -Wl,-dead_strip instead of -Wl,--gc-sections; with MSVC, /Gy and /link /OPT:REF /OPT:ICF.
The window layer for your system is picked by the preprocessor. Before the implementation, CUI_NO_APP leaves the window layer out (bring your own window and renderer), CUI_PLATFORM_SDL2 or CUI_PLATFORM_GLFW uses SDL2 or GLFW instead, CUI_NO_DEFAULT_FONTS and CUI_NO_STB_IMAGE leave out the fonts and image loading, and CUI_TEXT_SHAPING adds Arabic-script shaping. On macOS the window layer is Objective-C: compile cui.c with -x objective-c -fobjc-arc, or define CUI_NO_APP.
Options
| CMake option | Default | Meaning |
|---|---|---|
CUI_PLATFORM | auto | win32, cocoa, x11, sdl2 or glfw |
CUI_WITH_APP | ON | Build the window layer. Turn off to use only the core in your engine |
CUI_DEFAULT_FONTS | ON | Embed Inter, Geist and Lucide (about 180 KB) |
CUI_STB_IMAGE | ON | PNG and JPEG loading for cui_image_load |
CUI_BUILD_SHARED | OFF | Build a shared library |
CUI_BUILD_EXAMPLES, CUI_BUILD_TESTS | ON at top level |
Cross compiling for Windows from Linux works with the included toolchain file:
cmake -B build-win -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64.cmakeYour first window
#include <cui/cui.h>
#include <cui/cui_app.h>
int main(void) {
cui_app *app = cui_app_create(&(cui_app_desc){ .title = "My app", .width = 900, .height = 600 });
if (!app) return 1;
while (cui_app_frame(app)) {
cui_ctx *ui = cui_app_ui(app);
cui_column(ui, .pad = 24, .gap = 12) {
cui_heading(ui, "Welcome", 2);
cui_text(ui, "This text wraps to the width of the window.");
}
}
cui_app_destroy(app);
return 0;
}
cui_app_frame presents the previous frame, sleeps until something happens (input, an animation, a timer) and starts the next frame. Everything between two calls describes the screen.
If you prefer callbacks, cui_app_run does the loop for you and also gives smooth redraws while the user resizes the window on Windows and macOS:
static void frame(cui_ctx *ui, void *user) { /* ... */ }
int main(void) {
return cui_app_run(&(cui_app_desc){ .title = "My app" }, frame, NULL);
}How immediate mode works
You do not create widgets and keep pointers to them. You call a function every frame, and it returns what happened since the last frame:
static bool dark = false;
static float volume = 40;
static char name[64] = "";
if (cui_button(ui, "Save", .variant = CUI_BUTTON_PRIMARY)) save(); /* true once, when clicked */
cui_switch(ui, "Dark mode", &dark); /* toggles your bool */
cui_slider(ui, "Volume", &volume, 0, 100, .show_value = true); /* writes your float */
if (cui_input(ui, "name", name, sizeof name, .label = "Name") & CUI_CHANGED) validate(name);
Your variables are the source of truth. There is nothing to synchronise, and conditional UI is just an if.
CUI keeps small amounts of state for you behind the scenes (hover animations, scroll offsets, caret positions), keyed by an id that comes from the label. Two components with the same label in the same scope share an id. If you need two buttons called "Edit", give them different keys with ##:
cui_button(ui, "Edit##profile"); /* shows "Edit" */
cui_button(ui, "Edit##billing"); /* shows "Edit" */
Or wrap repeated items in an id scope, which is what you want in loops:
for (int i = 0; i < count; i++) {
cui_push_id_int(ui, i);
if (cui_button(ui, "Delete")) remove_item(i);
cui_pop_id(ui);
}Named arguments
Every component that has options takes them as named arguments after the required ones. Anything you leave out has a sensible default.
cui_button(ui, "Export", .icon = CUI_ICON_DOWNLOAD, .size = CUI_SIZE_SM, .tooltip = "Export as CSV");
Under the hood this is a C99 compound literal: cui_button(ui, label, ...) expands to cui_button_ex(ui, label, &(cui_button_opts){ ... }). If you want to build options at runtime, call the _ex function with a filled struct. C++ always does: the named arguments are C99, so from C++ you call the _ex functions (see From C++).
Containers
Containers take a block. The block runs once, between the container's begin and end:
cui_card(ui, .gap = 12) {
cui_label(ui, "Plan");
cui_row(ui, .gap = 8) {
cui_badge(ui, "Pro", .tone = CUI_TONE_ACCENT);
cui_spacer(ui);
cui_button(ui, "Change plan", .size = CUI_SIZE_SM);
}
}
Do not return, break or goto out of a block: the end would not run. If you need early exits, use the explicit functions (cui_card_begin and cui_card_end). Overlays use the same shape and only run their block while visible:
static bool open;
if (cui_button(ui, "Delete")) open = true;
cui_dialog(ui, "Delete project?", &open, .description = "This cannot be undone.") {
cui_dialog_footer(ui) {
if (cui_button(ui, "Cancel")) open = false;
if (cui_button(ui, "Delete", .variant = CUI_BUTTON_DANGER)) { delete_project(); open = false; }
}
}
Read Layout next. It explains sizes, alignment and scrolling in detail.
Keyboard shortcuts
if (!cui_wants_keyboard(ui)) { /* skip while the user types in a field */
if (cui_shortcut(ui, CUI_MOD_PRIMARY, CUI_KEY_K)) palette_open = true; /* Ctrl+K, Cmd+K on macOS */
}Files and links
char path[1024];
if (cui_app_file_dialog(app, &(cui_file_dialog_opts){ .kind = CUI_FILE_OPEN, .filter_name = "Images", .filter = "png;jpg" },
path, sizeof path))
open_image(path);
cui_app_open_url("https://example.com");
Native dialogs on Windows and macOS; on Linux zenity or kdialog when installed. Kinds: CUI_FILE_OPEN, CUI_FILE_SAVE, CUI_FILE_FOLDER. Inside a cui_app_run callback, pass NULL for the app.
Screenshots and tests
Any CUI app can save a screenshot and exit, which is handy for docs and visual tests:
CUI_SCREENSHOT=out.png CUI_SCREENSHOT_FRAMES=60 ./my_app
For unit tests, skip the window entirely: create a context with the software renderer, feed input with cui_input_* and render into memory. tests/test_main.c shows the pattern.
Run in the browser
CUI compiles to WebAssembly with plain clang: no Emscripten and no downloads. The live docs are built this way, and your app can be too, unchanged, as long as its main is int main(void) and hands its frame function to cui_app_run (as in the example above) instead of running its own loop. You need clang with the wasm32 target, wasm-ld, llvm-ar and python3 (on Debian or Ubuntu: sudo apt install clang lld llvm python3).
web/tools/build.sh app path/to/my_app.c # more .c files may follow
web/tools/serve.sh # then open http://localhost:8765/#/app/my_app
The app runs on the docs site's page, beside its source, with the theme, renderer and width controls. web/site/ is plain static files, so any web server can host it. The page draws with WebGL2, or with CUI's software renderer. Two things of the app layer do nothing in the browser yet: cui_app_file_dialog (it returns false) and cui_app_open_url. web/README.md explains the pieces.
Android and iOS are not supported yet. Each needs its own platform layer (an android_native_app_glue or UIKit window with touch input and an OpenGL ES or Metal renderer). The core already handles touch-sized layouts, phone widths and the software renderer, so it is a contained piece of work; until then, Integration shows how to drive CUI from a window and renderer you provide.
Troubleshooting
| Problem | Fix |
|---|---|
CMake says Could NOT find X11 | Install the X11 headers: libx11-dev (Debian, Ubuntu), libX11-devel (Fedora), libx11 (Arch) |
undefined reference to XOpenDisplay (building without CMake) | Put -lX11 -ldl -lm -lpthread after the source files on the command line |
| No window on Wayland without XWayland | Configure with -DCUI_PLATFORM=sdl2 or -DCUI_PLATFORM=glfw (install libsdl2-dev or libglfw3-dev) |
| Black or empty window, OpenGL errors (virtual machines, remote desktop, old drivers) | CUI falls back to its software renderer when OpenGL 3.3 cannot start; to force it, run with CUI_RENDERER=software |
| Everything looks too small or too large | Run with CUI_SCALE=1.5 (or any scale) to override what the display reports |
| A console window opens next to the app (Windows) | Call cui_app_target(my_app) in your CMakeLists.txt. Without CMake: -mwindows (MinGW), or /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup (MSVC) |
cui/cui.h: No such file or directory with the single-file distribution | Write #include "cui.h" in place of <cui/cui.h> and <cui/cui_app.h> (see One file) |
| The app is over 1 MB | The linker kept every component. Call cui_app_target(my_app) and build Release or MinSizeRel; without CMake see the flags under One file |
cui.c takes half a minute to compile | Compile it once to cui.o and link that (see One file) |
cui_app_cocoa.m fails to compile (macOS, without CMake) | Compile it with -fobjc-arc and link the Cocoa, QuartzCore and OpenGL frameworks |
| Text is missing | With CUI_DEFAULT_FONTS=OFF there are no built-in fonts: load your own (see Theming) |
Still stuck? Open an issue with your system, compiler, the CMake command and its full output.
Environment variables
| Variable | Effect |
|---|---|
CUI_RENDERER=software | Use the software renderer even when OpenGL is available |
CUI_RENDERER=opengl | Keep OpenGL even when it is drawn on the CPU (without it, X11 and Win32 apps on a machine with no GPU switch to the software renderer, which costs several times less there) |
CUI_SCALE=2 | Override the display scale |
CUI_THEME=dark | Start in the dark (or light) theme, whatever the app asks for |
CUI_ACCENT=blue | Start with another accent: neutral, violet, blue, green, amber, orange, coral, rose |
CUI_SCREENSHOT=file.png | Save a screenshot after CUI_SCREENSHOT_FRAMES frames (default 90) and quit |