GUI (std/gui/)
A GUI toolkit written in SafeC from the ground up: windows and input on X11, Wayland, Win32, Cocoa, web pages (WebAssembly <canvas>) and bare-metal framebuffers, an anti-aliased vector canvas with TrueType text, and a retained widget toolkit with layouts, themes and 30+ widgets. Everything is rendered on the CPU into one RGBA buffer per window, so a UI looks the same, byte for byte, on every platform; getting that buffer onto the screen can go through the GPU (Platforms & GPU).
This page covers windows, events and the application loop. The other two parts have their own pages:
- GUI Graphics —
GuiCanvas(paths, gradients, strokes, clipping, blend modes, images) and TrueType text (GuiTypeface,GuiTextLayout). - GUI Widgets — the widget tree, layout, themes, focus, menus, dialogs and every widget.
- GUI Platforms & GPU — Wayland, the web (
<canvas>, WebGL, DOM interop,safeguard bundle --target wasm32) and GPU presentation (OpenGL ES/EGL, Metal, Direct3D 11).
#include <std/gui/gui.h> // windows, events, colors, clipboard, clock
#include <std/gui/gui_app.h> // GuiApp: several windows, timers, animation
#include <std/gui/gui_canvas.h> // the vector canvas (and gui_path.h, gui_text.h)
#include <std/gui/gui_widget.h> // widgets
#include <std/gui/gui_png.h> // GuiImage: PNG load/save, scaling
#include <std/gui/gui_headless.h> // the in-memory backend, for tests
std/prelude.h does not include the GUI headers — include them directly.
A first program
#include <std/gui/gui_app.h>
static void on_click(&Widget w, ?&Widget label) {
match (label) { case some(l): { l.set_text("Clicked!"); } case null: { } }
}
int main() {
struct GuiApp app = GuiApp::new();
?&GuiWindow ow = app.open_window("Hello", 360, 200);
match (ow) {
case null: { app.free(); return 1; } // no display
case some(win): {
&Widget root = Widget::vstack().set_padding(16);
&Widget label = Widget::label("Hello from SafeC");
root.add_child(label);
root.add_child(Widget::button("Click me").on_click(on_click, label));
app.set_root(win, root); // the window owns the tree now
app.run(); // until the window closes
}
}
app.free();
return 0;
}
examples/gui_hello is a small complete program, and examples/gui_gallery shows every widget and most of the canvas in two windows (safeguard run -- --snapshot DIR renders them to PNG files without a display).
Windows — gui.h
struct GuiWindowOptions {
const &char title;
int x, y; // GUI_POS_DEFAULT: let the system place it
int width, height; // client area, logical units
int minWidth, minHeight; // 0 = no limit
int maxWidth, maxHeight;
int resizable; // default 1
int decorated; // title bar and border; default 1
int visible; // default 1
int hiDpi; // a buffer at the display's scale; default 1
int topmost; // default 0
static struct GuiWindowOptions new(const &char title, int width, int height);
};
struct GuiWindow {
?&struct GuiWindowState platform; // backend state (gui_backend.h); null if creation failed
&heap unsigned char pixels; // width*height*4 bytes: premultiplied RGBA8888, R first
int width, height; // the buffer, in pixels
int shouldClose; // set by GUI_EVENT_CLOSE
double scale; // pixels per logical unit
unsigned int id; // unique, never 0
static struct GuiWindow create(const &char title, int width, int height);
static struct GuiWindow create_with(const &GuiWindowOptions opts);
static struct GuiWindow offscreen(int width, int height); // a buffer, no window
int poll_event(&GuiEvent out); // 1 = got one; never blocks
int wait_event(&GuiEvent out, int timeoutMs);
void present(); // show the buffer
void present_rect(int x, int y, int w, int h);
void destroy() consume;
void set_title(const &char title); // UTF-8
void set_size(int width, int height); // logical
void set_position(int x, int y);
int pos_x() const; int pos_y() const;
void set_min_size(int w, int h); void set_max_size(int w, int h);
void show(); void hide(); int is_visible() const;
void focus(); int is_focused() const;
void set_cursor(int cursor); // GUI_CURSOR_*
int logical_width() const; int logical_height() const;
void request_close(); // posts a GUI_EVENT_CLOSE
unsigned long long native_handle() const; // X11 Window / HWND / NSWindow*
void set_pixel(int x, int y, unsigned int rgba); // raw store, 0xRRGGBBAA
unsigned int get_pixel(int x, int y) const;
void clear(struct GuiColor c);
// and the integer drawing helpers below (fill_rect, draw_text, draw_svg, ...)
};
A window always has a buffer width × height pixels large. With hiDpi (the default) on a high-density display, the buffer is larger than the window's logical size by scale (2.0 on a Retina display, 1.25–2.0 on Windows and X11 depending on the system setting; SAFEC_GUI_SCALE overrides it). Mouse coordinates arrive in buffer pixels, so hit-testing needs no conversion; the widget toolkit and GuiCanvas::scale() take care of drawing at the right size.
create fails cleanly — platform is null and the size is 0 — when there is no display (a CI runner without X, say). Check before use, or use GuiApp::open_window, which returns null.
The buffer is reallocated when the window is resized: poll_event updates pixels, width and height before returning the GUI_EVENT_RESIZE.
Events
struct GuiEvent {
int kind; // GUI_EVENT_*
double x, y; // pointer position, buffer pixels
int button; // GUI_BUTTON_LEFT / RIGHT / MIDDLE
int clicks; // 1, 2 (double click), 3 ... on MOUSE_DOWN
int keycode; // GUI_KEY_*: named keys, 'A'..'Z', '0'..'9'
int modifiers; // GUI_MOD_SHIFT | CTRL | ALT | META
unsigned int codepoint; // the typed character (KEY_DOWN, CHAR)
double scrollDx, scrollDy;// in lines; +y = away from the user
int width, height; // RESIZE / SCALE: the new buffer size; MOVE: x, y
unsigned int windowId;
double timeMs; // gui_time_ms() when it happened
int defaultPrevented;
void prevent_default(); // see the widget toolkit's default actions
int has_mod(int mod) const;
int primary_mod() const; // Cmd on macOS, Ctrl elsewhere
};
| Event | When |
|---|---|
GUI_EVENT_CLOSE |
The close button (or Alt+F4, Cmd+W from the system). The window stays open until you destroy it. |
GUI_EVENT_RESIZE |
New size; the buffer is already reallocated. |
GUI_EVENT_MOVE |
The window moved (width/height hold the new x/y). |
GUI_EVENT_FOCUS_IN / _OUT |
The window gained / lost the keyboard focus. |
GUI_EVENT_MOUSE_DOWN / _UP / _MOVE |
Buttons and motion; the window captures the pointer while a button is held. |
GUI_EVENT_MOUSE_ENTER / _LEAVE |
The pointer entered / left the window. |
GUI_EVENT_SCROLL |
Wheel or touchpad. |
GUI_EVENT_KEY_DOWN / _UP |
Keys, with modifiers; KEY_DOWN carries the typed character. Repeats arrive as more KEY_DOWNs. |
GUI_EVENT_CHAR |
Text with no key behind it (an input method, compose sequences). |
GUI_EVENT_SCALE |
Moved to a display with another scale factor; the buffer is reallocated. |
GUI_EVENT_EXPOSE |
The system lost the window's contents: present again. |
Each backend translates native key codes, so programs only see GUI_KEY_*: arrows, Enter, Escape, Tab, Backspace, Space, Delete, Home/End, PageUp/PageDown, Insert, F1–F12, Shift/Control/Alt/Meta/Menu, and letters and digits as their uppercase ASCII codes ('A', '7'), whatever the keyboard layout produces as text. Text input goes through the platform's input method where it has one (XIM on X11, WM_CHAR with surrogate pairs on Windows, NSEvent.characters on macOS), so codepoint is a full Unicode scalar.
Clipboard, time, cursors
int gui_clipboard_set_text(const &char utf8); // 1 = ok
int gui_clipboard_get_text(&String out); // 1 = got text
double gui_time_ms(); // monotonic milliseconds
int gui_primary_mod(); // GUI_MOD_META on macOS, else GUI_MOD_CTRL
double gui_display_scale(); // the scale a hiDpi window would get
The clipboard is the system's: CLIPBOARD on X11 (served to other clients while the program runs; large INCR transfers aren't supported), CF_UNICODETEXT on Windows, NSPasteboard on macOS.
Cursors: GUI_CURSOR_ARROW, _IBEAM, _HAND, _CROSSHAIR, _RESIZE_EW, _RESIZE_NS, _RESIZE_NWSE, _RESIZE_NESW, _MOVE, _WAIT, _NOT_ALLOWED, _HIDDEN.
Colors
struct GuiColor {
unsigned char r, g, b, a; // straight (not premultiplied) alpha
static struct GuiColor rgba(r, g, b, a);
static struct GuiColor rgb(r, g, b);
static struct GuiColor hex(unsigned int rrggbb);
static struct GuiColor hexa(unsigned int rrggbbaa);
static struct GuiColor rgbaf(float r, float g, float b, float a); // 0..1
static struct GuiColor hsv(float h, float s, float v, float a); // h in degrees
struct GuiColor with_alpha(float f) const; // alpha multiplied by f (0..1)
struct GuiColor mix(struct GuiColor other, float t) const;
struct GuiColor shade(float amount) const; // > 0 lighter, < 0 darker
unsigned int packed() const; // 0xRRGGBBAA
void to_hsv(&float h, &float s, &float v) const;
};
The application — gui_app.h
GuiApp runs any number of windows from one event loop: it pumps the platform's events, routes each to the window it belongs to (its widget tree and/or your callback), fires timers and per-frame animation callbacks, repaints the windows that changed and presents them, and closes a window when its close button is clicked.
struct GuiApp {
static struct GuiApp new();
?&GuiWindow open_window(const &char title, int width, int height);
?&GuiWindow open(const &GuiWindowOptions opts);
void close_window(&GuiWindow win); // destroys it (and its widget tree) now
unsigned long window_count() const;
?&GuiWindow window_at(unsigned long i) const;
?&GuiWindow find_window(unsigned int id) const;
&GuiUi set_root(&GuiWindow win, &Widget root); // show a widget tree in the window
?&GuiUi ui_of(const &GuiWindow win) const;
void redraw(&GuiWindow win); // repaint on the next iteration
generic<T> void on_event(&GuiWindow win, fn int(&GuiApp, &GuiWindow, &GuiEvent, ?&T) cb, ?&T data);
generic<T> void on_draw(&GuiWindow win, fn void(&GuiApp, &GuiWindow, &GuiCanvas, ?&T) cb, ?&T data);
generic<T> void on_frame(&GuiWindow win, fn int(&GuiApp, &GuiWindow, double, ?&T) cb, ?&T data);
generic<T> unsigned int set_timeout(double ms, fn void(&GuiApp, unsigned int, ?&T) cb, ?&T data);
generic<T> unsigned int set_interval(double ms, fn void(&GuiApp, unsigned int, ?&T) cb, ?&T data);
void cancel_timer(unsigned int id);
int pump(int timeoutMs); // one iteration; 0 once the app should stop
int run(); // pump() until then; returns quit()'s code
void quit(int exitCode);
double now_ms() const;
void set_frame_interval(double ms); // default 1000/60
void free() consume; // closes every window
};
- Rendering. A window with a widget root is laid out and painted by its
GuiUi. A window with anon_drawcallback gets aGuiCanvasover its buffer — after the widgets, if it has both. A window with neither shows whatever you wrote intowin.pixels. Windows are repainted only when something changed: an input event a widget or youron_eventhandled (return 1), a resize, a timer, an animation frame, orapp.redraw(win). - Closing.
GUI_EVENT_CLOSEcloses the window after youron_eventcallback saw it — unless the callback calledev.prevent_default()(to ask "save changes?" first, say).run()returns when the last window is gone orquit()was called. - Animation.
on_framecallbacks run once per frame (set_frame_interval, 60 Hz by default) with the time in milliseconds, for as long as they return 1; the window is repainted after each. - Timers run on the loop thread, never concurrently with event handling, as close to their due time as the loop allows. Ids are never 0.
- Waiting.
run()sleeps in the platform's event wait (pollon the X connection,MsgWaitForMultipleObjects,nextEventMatchingMask) until the next event, timer or frame is due, so an idle app uses no CPU.
Driving the loop yourself
Call app.pump(0) once per iteration of your own loop (a game, an embedding host, a WebAssembly page driven by requestAnimationFrame): it does one iteration without waiting and returns 0 once the app should stop. app.pump(ms) waits up to ms milliseconds for the next event, timer or frame.
Example — two windows and a timer
#include <std/gui/gui_app.h>
struct State { int seconds; &Widget label; };
static void tick(&GuiApp app, unsigned int id, ?&State os) {
match (os) {
case some(s): {
s.seconds = s.seconds + 1;
struct String t = std::String::new();
t.push_int((long long)s.seconds);
t.push(" s");
s.label.set_text(t.as_ptr());
t.free();
}
case null: { }
}
}
static void draw(&GuiApp app, &GuiWindow win, &GuiCanvas c, ?&State os) {
c.clear(GuiColor::hex(0x202830U));
c.set_fill_color(GuiColor::hex(0x4DABF7U));
c.fill_circle((float)win.width / 2.0f, (float)win.height / 2.0f, 40.0f * (float)win.scale);
}
int main() {
struct GuiApp app = GuiApp::new();
?&GuiWindow oa = app.open_window("Widgets", 300, 120);
?&GuiWindow ob = app.open_window("Canvas", 300, 200);
if (oa.is_null() || ob.is_null()) { app.free(); return 1; }
&GuiWindow a; &GuiWindow b;
unsafe { a = (struct GuiWindow*)oa; b = (struct GuiWindow*)ob; }
struct State s;
s.seconds = 0;
s.label = Widget::label("0 s");
&Widget root = Widget::vstack().set_padding(16);
root.add_child(s.label);
app.set_root(a, root);
app.on_draw(b, draw, &s);
app.set_interval(1000.0, tick, &s);
int code = app.run();
app.free();
return code;
}
Backends
Everything platform-specific sits behind one table of function pointers, struct GuiBackend (gui_backend.h). The portable layer — windows, the clipboard, the clock, GuiApp — calls only through it, and the canvas, text and widget code only ever see a window's pixel buffer.
| Backend | Target | Tested |
|---|---|---|
gui_x11.sc |
Linux, BSD — Xlib, XPutImage, XIM text input, CLIPBOARD, EWMH |
Against Xvfb (tests/stdlib/x11/run.sh): pixels read back with XGetImage; keys, text, mouse, scroll, resize, close; two windows under one GuiApp; UTF-8 titles; the clipboard both ways with a second X client |
gui_wayland.sc |
Linux — Wayland: xdg-shell, wl_shm, xkbcommon, fractional scale (all dlopen()ed); chosen at run time when there is a compositor |
Under sway and Weston, headless (tests/stdlib/wayland/run.sh): frames read back with grim, virtual pointer and keyboard input, the clipboard with wl-copy/wl-paste, scale 1 and 1.5 |
gui_web_wasm.sc |
WebAssembly — <canvas> elements of a page, with std/gui/gui_web.js |
Under Node with a fake DOM (tests/stdlib/wasm_gui/run.sh) and in headless Chromium (browser.sh): the gallery's frame matches the native one |
gui_win32.sc |
Windows — wide-character Win32, StretchDIBits, per-monitor DPI |
Under Wine (tests/stdlib/win32/run.sh): pixels read back with GetPixel; posted input; two windows; the clipboard |
gui_cocoa.sc |
macOS — AppKit through the Objective-C runtime, a layer-backed NSView |
Compiled for arm64 and x86_64 macOS |
gui_fb.sc |
Bare metal — a linear framebuffer; input injected from your drivers | Compiled for every bare-metal target |
gui_headless.sc |
Any target — in memory, with a virtual clock | tests/stdlib/tests/test_gui_widgets.sc, test_gui_app.sc |
safeguard links the backend for the target by filename suffix (_x11 on Linux/BSD, _win32 on Windows, _cocoa on Apple, _wasm on WebAssembly, _fb on bare metal) and adds its system libraries to the link — nothing to list in Package.toml. SAFEC_GUI_BACKEND=headless runs any GUI program on the headless backend; on Linux, SAFEC_GUI_BACKEND=x11|wayland picks between the two (details).
| Backend | Libraries |
|---|---|
| Cocoa | -framework Cocoa -lobjc |
| Win32 | -luser32 -lgdi32 |
| X11 | -lX11 (and libx11-6 in a .deb's Depends) |
| Wayland, EGL | none: dlopen()ed (-ldl); libwayland-client0, libxkbcommon0, libwayland-cursor0, libegl1, libgles2 in a .deb's Recommends |
| Metal, Direct3D 11 | none: loaded at run time |
| WebAssembly | none: imports std/gui/gui_web.js provides |
Building without safeguard, pass them to the link yourself.
The headless backend
gui_use_headless() (from gui_headless.h, before the first window) swaps the platform backend for one that keeps windows in memory, with a virtual clock that only moves when the loop waits or you advance it. It drives tests: inject input and read the pixels back.
gui_use_headless();
struct GuiWindow win = GuiWindow::create("test", 200, 100);
gui_headless_click(&win, 50.0, 20.0); // MOUSE_DOWN + MOUSE_UP
gui_headless_key(&win, GUI_KEY_TAB, GUI_MOD_SHIFT, 0U);
gui_headless_type(&win, "héllo"); // one KEY_DOWN per character
gui_headless_scroll(&win, 10.0, 10.0, 0.0, -3.0);
gui_headless_resize(&win, 300, 120);
gui_headless_close(&win);
gui_headless_advance(600.0); // the clock, in ms
gui_headless_set_scale(2.0); // windows made from now on are 2x
int cur = gui_headless_cursor(&win); // GUI_CURSOR_* last asked for
unsigned long n = gui_headless_present_count(&win);
win.destroy();
Writing a backend
gui_backend.h documents the contract in full. In short, a backend is one .sc file that fills in a GuiBackend:
| Entry | Does |
|---|---|
init() |
Connect to the display server; 0 if there is none. |
window_create(st, opts) |
Create the native window; set st.key (native id), st.scale, st.pixelW/H, st.x/y. |
window_destroy, window_set_title/size/position/visible, window_focus, window_set_cursor, window_update_limits |
Apply a change. |
window_present(st, px, bufW, bufH, x, y, w, h) |
Show a rectangle of the premultiplied RGBA buffer, converting to the native pixel order. |
pump(timeoutMs) |
Read native events, translate them, gui_backend_post() them; wait up to timeoutMs if none was pending. |
wake() |
Make a blocked pump() return (any thread). |
now_ns, clipboard_set/get, display_scale, native_handle |
Clock, clipboard, scale, handles for GPU presenters. |
A new platform default is a suffix rule in safeguard's stdVariantMatches plus a gui_<...>_<suffix>.sc defining gui_platform_backend() (as gui_web_wasm.sc is for _wasm). An alternative next to an existing default defines its own table and setup call, as gui_wayland.sc does beside X11; a library it needs is best dlopen()ed and named with // safeguard: deb-recommends.
GPU presentation is a GuiPresenter attached per window — see Platforms & GPU for the ones that ship (OpenGL ES/EGL, Metal, Direct3D 11, WebGL) and how to write one.
Bare metal — gui_fb.h
There is no windowing system or input stack on bare metal. Tell the backend where the framebuffer is before creating the window, and feed it input from your own drivers.
#include <std/gui/gui_fb.h>
struct GuiFbConfig {
&volatile unsigned char base; // linear framebuffer memory
unsigned long pitch; // bytes per scanline (may include padding)
int width;
int height;
int bpp; // 32 (xRGB/xBGR) or 16 (RGB565)
int redShift; int greenShift; int blueShift; // channel bit positions, 32 bpp only
};
void gui_fb_configure(struct GuiFbConfig cfg); // once, before GuiWindow::create()
void gui_fb_inject_event(&GuiWindow win, struct GuiEvent ev); // from your keyboard/mouse/touch ISR
void gui_fb_set_clock(fn long long() nowNs); // a monotonic clock, for timers and animation
The values usually come from the bootloader, for example a Multiboot2 framebuffer tag or a VESA VBE mode. Without gui_fb_configure, window creation fails. Injected events go into a 32-entry ring (GUI_FB_QUEUE_CAP) that poll_event drains; events are dropped when it is full. present() converts the RGBA buffer to the framebuffer's pixel format as it copies. The canvas, text (with the built-in fallback font) and widgets all work unchanged.
See Bare-Metal for targets and linking.
Simple drawing, bitmap fonts, PNG and SVG
The original integer helpers are still there for quick pixel work (they write the buffer directly, without blending or anti-aliasing — use the canvas for anything else):
win.fill_rect(x, y, w, h, color);
win.draw_rect_border(x, y, w, h, color, thickness);
win.draw_line(x0, y0, x1, y1, color);
int n = win.draw_text(x, y, "text", color, scale); // built-in 8x8 font, ASCII
int ok = win.draw_svg(ox, oy, svgText); // an SVG subset, see below
int gui_text_width(const &char text, int scale); // gui_draw.h
int gui_text_height(int scale);
Bitmap fonts — gui_font.h. A GuiFont is a bit-packed glyph bitmap of any glyph size (GuiFont::new, GuiFont::default() for the built-in 8×8 font, get_bit/set_bit, save/load in the SCXF format, draw_text, text_width). A widget given one with set_font() draws its text with it. For real text, use TrueType fonts.
Images — gui_png.h.
struct GuiImage {
&heap unsigned char pixels; // width*height*4, RGBA8888
int width, height;
int premultiplied; // 1 for canvas-ready pixels
static struct GuiImage new(int width, int height); // transparent
static struct GuiImage from_window(const &GuiWindow win); // a copy of the buffer
int load_png([]const unsigned char data);
int load_png_file(const &char path);
int save_png(const &char path) const; // RGBA, stored deflate blocks
void premultiply(); void unpremultiply();
struct GuiImage copy() const;
struct GuiImage scaled(int width, int height) const; // area average shrinking, bilinear enlarging
unsigned int get_pixel(int x, int y) const; void set_pixel(int x, int y, unsigned int rgba);
void draw(&GuiWindow win, int x, int y) const; // a straight copy
void free() consume;
};
The PNG decoder (with its own DEFLATE inflater) reads 8-bit grayscale, RGB and RGBA, non-interlaced; palette images, 16-bit depth and Adam7 fail cleanly with 0. Draw images with blending, scaling and filtering through the canvas (draw_image*, image patterns).
SVG — draw_svg. Parses a <svg> document and draws <rect>, <circle>, <ellipse>, <line>, <polyline>, <polygon> and <path> (M/L/H/V/C/Z) with fill/stroke colors; no viewBox, transforms, gradients or text. For vector graphics with anti-aliasing, GuiPath::add_svg() parses full SVG path data (every command, arcs included) into a canvas path.