FTUI is a tiny immediate-mode GUI for C++.
It is a single header, keeps the baseline loop small, and aims for utility-app workflows where you want native windows and straightforward widget code without bringing in a retained UI framework.
- Windows backend: Win32 + Direct2D + DirectWrite
- Linux backend: X11 + Cairo
- Distribution model: single header, no CMake required
| Showcase overview | Drawer and progress masks |
|---|---|
![]() |
![]() |
| Controls and colors | Ghostty-style theme and toasts |
|---|---|
![]() |
![]() |
- Keep the default path tiny:
create_window() -> pump() -> begin() -> widgets -> end() -> shutdown() - Add power through new functions and additive flags, not new required setup
- Avoid builders, registries, or retained widget trees
- If you do not use a feature, it should not make the rest of the API feel heavier
In exactly one .cpp file:
#define FTUI_IMPLEMENTATION
#include "ftui.hpp"
int main() {
ftui::Config cfg;
cfg.title = "My App";
cfg.width = 960;
cfg.height = 640;
cfg.fps_limit = 60; // default; use 0 for uncapped
if (!ftui::create_window(cfg)) return 1;
char name[64] = "";
while (ftui::pump()) {
ftui::begin();
ftui::text("Hello");
ftui::input("Name", name, sizeof(name));
if (ftui::button("Go")) {
// handle click
}
ftui::end();
}
ftui::shutdown();
return 0;
}Every other translation unit should include ftui.hpp without FTUI_IMPLEMENTATION.
FTUI caps rendering at Config::fps_limit, which defaults to 60.
ftui::Config cfg;
cfg.fps_limit = 144; // or 0 for uncapped renderingYou can also change it at runtime:
ftui::set_fps_limit(30);FTUI sleeps while idle, then redraws on native input/window events, active interaction, active debug/effect overlays, or an explicit request:
ftui::request_redraw();The Linux backend renders into an X11 pixmap back buffer and swaps it to the window.
Effects backdrops can use the default blur-style panel on Windows, or a cached theme-colored soft dither texture on both Windows and Linux:
ftui::Config cfg;
cfg.backdrop_effect = ftui::BackdropEffect::BayerDither;
cfg.dither_size = 5;Window-level transparency is separate:
cfg.window_transparency = ftui::WindowTransparency::Plain;
cfg.window_opacity = 0.88f;Windows supports Opaque, Plain, BayerDither, and Blur. Linux supports Opaque, Plain, and BayerDither; the BayerDither compatibility name now renders FTUI's softer cached dither texture. Plain opacity uses the compositor _NET_WM_WINDOW_OPACITY property.
Windows with clang++:
clang++ main.cpp -o app.exe -std=c++17Windows with MSVC or MSVC-targeting clang++:
ftui.hppalready includes the required#pragma comment(lib, ...)lines.
Linux:
g++ main.cpp -o app -DFTUI_IMPLEMENTATION $(pkg-config --cflags --libs cairo x11) -std=c++17Linux note:
open_file_dialog()useszenitywhen available.- The Linux backend intentionally keeps a simpler rendering path than Windows in the current release.
Example builds from the repo root:
clang++ demo.cpp -o demo.exe -std=c++17
clang++ examples/benchmark.cpp -o benchmark.exe -std=c++17ftui.hpp: the single-header librarydemo.cpp: compatibility entrypoint that builds the showcase exampleexamples/showcase.cpp: broad widget and layout tourexamples/control_panel.cpp: settings-heavy utility app exampleexamples/log_viewer.cpp: read-only output and operator-notes exampleexamples/benchmark.cpp: synthetic workload / FPS stress exampleftui.svg: built-in icon source used by the project
FTUI 1.x treats the common surface as additive-first.
- Core config types like
Config.titlestay stable through 1.x. - Existing calls like
input(),text_area(),tabs(),row(int, ...), andopen_child_window()remain valid. - New capability is added through new helpers, new widgets, and additive flags rather than changing the shape of existing calls.
New windows start with the style configured near the top of ftui.hpp:
#ifndef FTUI_DEFAULT_STYLE
#define FTUI_DEFAULT_STYLE ftui::one_dark_style
#endifIf the application developer does not define FTUI_DEFAULT_STYLE, FTUI also checks FTUI_THEME at startup. Vim-style theme commands such as :to, :tn, and :th update FTUI_THEME for the current process, so child windows and later windows can reuse the selected theme. A developer-defined FTUI_DEFAULT_STYLE always takes precedence.
Vim-style commands:
:tddefault dark:tcCatppuccin Mocha:tnNord:tgGruvbox dark:toOne Dark:thGhostty green:fps60set the frame limit; use any integer, or:fps0to disable limiting:fx,:fx0,:fx1toggle, disable, or enable animations/effects:df,:dl,:di,:dwtoggle FPS, layout rects, widget IDs, or widget logging:dit,:plain,:opaque,:blurswitch window transparency mode:op85set window opacity as a percentage:ds6set dither motif size:bb,:bdswitch backdrop blur/dither style:rrrequest redraw:ctclear toasts:q,:quitquit
Built-in presets:
ftui::set_style(ftui::default_dark_style());
ftui::set_style(ftui::catppuccin_mocha_style());
ftui::set_style(ftui::nord_style());
ftui::set_style(ftui::gruvbox_dark_style());
ftui::set_style(ftui::one_dark_style());
ftui::set_style(ftui::ghostty_green_style());The built-in presets are the library’s named color packs. Each one fully initializes the semantic color roles used by FTUI, including accent, warning, and success.
Hex colors are supported directly:
ftui::Color red = ftui::color_from_hex("#ff4d4f");
ftui::Color translucent = ftui::color_from_hex("#3b82f680");Manual overrides stay immediate-mode and scoped:
ftui::set_next_color(ftui::ColorRole::Text, ftui::color_from_hex("#ffffff"));
ftui::set_next_color(ftui::ColorRole::Button, ftui::color_from_hex("#c93c3c"));
ftui::button("One-shot red");
ftui::push_color(ftui::ColorRole::Button, ftui::color_from_hex("#2f855a"));
ftui::button("Scoped green");
ftui::button("Still green");
ftui::pop_color();Semantic roles also work directly at the callsite:
ftui::button("Primary", ftui::ColorRole::Accent);
ftui::button("Delete", ftui::ColorRole::Warning);
ftui::button("Healthy", ftui::ColorRole::Success);
ftui::button("Custom", ftui::color_from_hex("#7c3aed"));On Windows, the titlebar color follows the active FTUI theme automatically.
Use the built-in icon variants:
ftui::set_window_icon_builtin(ftui::BuiltinIcon::Symbol);
ftui::set_window_icon_builtin(ftui::BuiltinIcon::SymbolWithText);Or pass a native icon handle directly on Windows:
ftui::set_window_icon(native_hicon);ftui::text("Section title");
ftui::text_wrapped("Longer help text that should wrap inside the current content width.");
ftui::separator();
ftui::spacing(12.0f);if (ftui::button("Run")) {
// fired on mouse-up inside the button
}Optional tint overloads keep one-off emphasis straightforward without changing the surrounding theme:
ftui::button("Promote", ftui::ColorRole::Accent);
ftui::button("Danger", ftui::ColorRole::Warning);
ftui::button("Success", ftui::color_from_hex("#22c55e"));Visible labels can be reused by suffixing an internal ID:
ftui::button("Open##file");
ftui::button("Open##folder");Drawer-style navigation:
static const char* pages[] = {"Dashboard", "Network", "Services", "Logs", "Settings"};
int page = 0;
ftui::side_menu_drawer("Navigation", pages, 5, &page);
if (page == 0) ftui::text("Dashboard");
else if (page == 1) ftui::text("Network");Embedded sidebar navigation:
static const char* pages[] = {"Dashboard", "Network", "Services", "Logs", "Settings"};
int page = 0;
ftui::side_layout(220.0f, [&]() {
ftui::side_menu("Navigation", pages, 5, &page);
ftui::content([&]() {
if (page == 0) ftui::text("Dashboard");
else if (page == 1) ftui::text("Network");
});
});Use side_menu_drawer(...) when you want an app-style slide-out sidebar. Use side_layout(...) or split({220.0f, 1.0f}, ...) directly when you want a persistent sidebar. Values >= 16 are fixed pixel columns; smaller values are flexible weights.
ftui::toast("Saved");
ftui::toast_success("Configuration saved");
ftui::toast_warning("High CPU usage");
ftui::toast_error("Connection failed");For custom timing:
ftui::Toast t;
t.message = "Relay restarted";
t.duration_ms = 5000;
t.dismissible = true;
t.type = ftui::ToastType::Success;
ftui::toast(t);float progress = 0.73f;
ftui::progress_bar(progress);
ftui::progress_bar(progress, "Loading assets");Masked progress bars fill the white/opaque area of an SVG, PNG, or image from left to right. PNG masks can also be dark alpha silhouettes; if no bright pixels are present, FTUI uses alpha as the fillable area:
ftui::ProgressStyle style;
style.mask_path = "battery.png";
style.wave_front = true;
style.glint = true;
ftui::progress_bar(progress, style);For single-binary examples, embed SVG text directly:
ftui::ProgressStyle battery;
battery.mask_svg = "<svg width='160' height='64' viewBox='0 0 160 64'>"
"<rect x='4' y='12' width='132' height='40' fill='white'/>"
"<rect x='140' y='24' width='16' height='16' fill='white'/>"
"</svg>";
ftui::progress_bar(progress, battery);Built-in masks are available by name:
ftui::progress_bar(progress, "battery");
ftui::ProgressStyle pill;
pill.mask_shape = "pill"; // battery, tank, pill, circle, logo
ftui::progress_bar(progress, pill);char username[128] = "";
char password[128] = "";
bool submitted = false;
ftui::input("Username", username, sizeof(username));
ftui::input("Password", password, sizeof(password), ftui::InputFlags::Password, &submitted);Additive filtering flags:
char port[16] = "8080";
char code[32] = "";
ftui::input("Port", port, sizeof(port), ftui::InputFlags::CharsDecimal);
ftui::input("Code", code, sizeof(code),
ftui::InputFlags::CharsUppercase | ftui::InputFlags::CharsNoBlank);Read-only input:
ftui::input("Token", code, sizeof(code), ftui::InputFlags::ReadOnly);Simple multiline editor:
char notes[2048] = "";
ftui::text_area("Notes", notes, sizeof(notes), 8);Extended multiline editor:
ftui::text_area_ex("Notes", notes, sizeof(notes), 8,
ftui::TextAreaFlags::WordWrap);Read-only / wrapped variants:
ftui::text_area_ex("Preview", notes, sizeof(notes), 6,
ftui::TextAreaFlags::ReadOnly | ftui::TextAreaFlags::WordWrap);const char* log_text =
"[09:14] Boot complete.\n"
"[09:15] Waiting for input.\n";
ftui::log_view("Output", log_text, 10,
ftui::LogViewFlags::WordWrap | ftui::LogViewFlags::AutoScrollBottom);log_view() is intended for logs, transcripts, debug output, and read-only history panes.
bool enabled = false;
float blend = 0.5f;
ftui::checkbox("Enable option", &enabled);
ftui::slider_float("Blend", &blend, 0.0f, 1.0f);static const char* tabs[] = { "Login", "Notes", "Settings" };
int selected_tab = 0;
ftui::tabs(tabs, 3, &selected_tab);Dropdown:
static const char* envs[] = { "Local", "Staging", "Production" };
int env = 0;
ftui::dropdown("Environment", envs, 3, &env);Dropdown popups render as top-layer overlays, flip upward when needed to stay in bounds, and use the lightweight frosted popup treatment on Windows when effects are enabled.
Listbox:
static const char* roles[] = { "Admin", "Observer", "Maintainer" };
int role = 0;
ftui::listbox("Role", roles, 3, &role, 4);Radio group:
static const char* shells[] = { "Powershell", "Bash", "Cmd" };
int shell = 0;
ftui::radio_group("Shell", shells, 3, &shell, 1);bool advanced_open = true;
if (ftui::collapsing_header("Advanced", &advanced_open)) {
ftui::checkbox("Enable tracing", &enabled);
}With the Windows effects path enabled, collapsible sections reveal and hide their body gradually and the widgets below them ease into their new positions instead of snapping.
Equal row:
ftui::row(3, [&]() {
ftui::button("Left");
ftui::button("Center");
ftui::button("Right");
});Weighted row:
ftui::row({2.0f, 1.0f}, [&]() {
ftui::button("Wide");
ftui::button("Narrow");
});One-shot width helpers:
ftui::set_next_width(220.0f);
ftui::button("Fixed width");
ftui::set_next_percent(0.60f);
ftui::button("60 percent");
ftui::set_next_fill();
ftui::button("Fill width");
ftui::set_next_percent(0.75f);
ftui::set_next_limits(180.0f, 280.0f);
ftui::set_next_align(ftui::Align::End);
ftui::button("Aligned and clamped");ftui::scroll_area("History", 220.0f, [&]() {
for (int i = 0; i < 20; ++i) {
char line[64];
snprintf(line, sizeof(line), "Event %02d", i + 1);
ftui::text(line);
}
});Scroll areas keep their own scroll state and consume wheel input when hovered.
Disabled scope:
ftui::begin_disabled();
ftui::button("Disabled button");
ftui::end_disabled();Tooltips:
ftui::button("Hover me");
ftui::tooltip("Shown after a short steady hover.");Tooltips fade in and out, wait for a steady cursor hover, and back off more aggressively if the user repeatedly moves away right after they appear.
Request focus:
if (ftui::button("Focus username")) {
ftui::request_focus("Username");
}Text measurement:
float w = ftui::calc_text_width("Status");
float h = ftui::calc_text_height(long_text, 280.0f);Modal:
if (ftui::button("Reset")) {
ftui::open_modal("Confirm reset");
}
ftui::modal("Confirm reset", [&]() {
ftui::text_wrapped("This blocks background interaction until closed.");
if (ftui::button("Cancel")) ftui::close_modal();
});Child window:
ftui::Config child;
child.title = "Details";
child.width = 640;
child.height = 360;
ftui::open_child_window(child, [&]() {
ftui::text("Child window content");
});ftui::ImageHandle* img = ftui::load_image("photo.png");
ftui::image(img, 200.0f, 150.0f);
ftui::free_image(img);static const ftui::FileFilter filters[] = {
{ "Images", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tiff" },
{ "All Files", "*.*" },
};
std::string path = ftui::open_file_dialog("Open Image", filters, 2);Windows can enable a lightweight effects layer with:
- smooth window and widget scrolling
- hover and press easing
- tab underline motion
- tab content slide motion
- collapsible section reveal and layout easing
- frosted dropdown popups on Windows
Runtime opt-out:
ftui::Config cfg;
cfg.enable_effects = false;Compile-time strip:
#define FTUI_DISABLE_EFFECTS
#define FTUI_IMPLEMENTATION
#include "ftui.hpp"Linux keeps the same API and behavior model, but currently uses a flatter rendering path without the Windows-only effects layer.1
Tab/Shift+Tabcycles interactive widgetsEnter/Spaceactivate focused buttons and picker widgets- Arrow keys navigate tabs, radios, listboxes, and dropdown selections
Enterinserts newline in multiline textCtrl+C/Ctrl+Vwork in text widgets where applicableCtrl+Qquits by default:opens command mode when no text widget is focused
Disable the built-in quit shortcut:
ftui::set_quit_on_ctrl_q(false);demo.cpp remains the easiest root-level build target and now simply includes the showcase example so older commands still work.
examples/showcase.cpp: broad FTUI tour covering forms, text areas, log output, scroll areas, themes, modals, and child windowsexamples/control_panel.cpp: a settings-heavy control panel with collapsible sections, confirmation flow, and activity outputexamples/log_viewer.cpp: a focused log/transcript app usinglog_view(), multiline notes, filters, and a nested history scrollerexamples/benchmark.cpp: a synthetic benchmark app for row-count, workload, and frame-pacing checks with the FPS overlay enabled
Footnotes
-
Linux support is first-class at the API level and actively maintained, but the Windows backend currently receives the richer visual effects path first so the Linux renderer can stay lean and dependency-light. ↩



