nexium-gui

An immediate-mode GUI written in Nexium. Everything above the window is Nexium code: a software rasterizer drawing into a List(u32) framebuffer, a bitmap font, layout, and widgets. The only C is gui/platform.c, about 200 lines that open a window, queue input events, and blit the framebuffer.

import nexium_gui

fn main() {
    var ui = nexium_gui.Ui.open(@cstr("hello"), 400, 200).?
    var clicks: i32 = 0
    var dark = true
    while ui.begin_frame() {
        ui.heading("nexium-gui")
        if ui.button("click me") { clicks += 1 }
        ui.same_line()
        ui.label(format("{} clicks", .{clicks}))
        if ui.checkbox("dark theme", &mut dark) {
            ui.set_theme(if dark { nexium_gui.dark_theme() } else { nexium_gui.light_theme() })
        }
        ui.end_frame()
    }
}

Run the demo with nx run gui/demo.nx. Render it without a window with nx run gui/demo.nx -- --shot frame.ppm.

The model#

Widgets are function calls made every frame. They draw themselves and return what happened: button returns true on the frame it was clicked, checkbox and slider return true when the value changed and write the new value through the *mut you pass. There is no widget tree to build, no callbacks, and no closures; the application's state is the application's own variables. This is the egui model, and it suits a language with explicit ownership because nothing outlives the frame.

A frame is:

  1. begin_frame() clears the per-frame input flags, pumps the window's events, resets the layout cursor and widget ids, and clears the canvas. It returns false once the window was closed.
  2. Widget calls, top to bottom. Each takes the next id; hot (under the mouse), active (pressed), and focus (keyboard) are ids.
  3. Free drawing on ui.canvas if wanted.
  4. end_frame() presents the framebuffer and sleeps a few milliseconds.

API#

Ui

calleffect
Ui.open(title: *u8, w, h) -> ?Uiopen a window; null when the platform has no backend
Ui.headless(w, h) -> Uia Ui that draws to a canvas without a window (tests, offscreen rendering)
begin_frame() -> bool, end_frame(), close()the frame loop
set_theme(Theme)dark_theme() and light_theme() are provided
label(text), dim_label(text), heading(text)text
button(text) -> booltrue on the frame it is clicked
checkbox(text, *mut bool) -> booltrue when toggled
slider(text, *mut f32, min, max) -> booltrue while dragged
text_input(*mut String) -> boolclick to focus, type, backspace; true when changed
progress(frac), separator(), space(px), panel(h)layout pieces
same_line()the next widget goes to the right of the previous one
fields canvas, input, theme, time, dt, x, y, marginfor free drawing and custom widgets

Canvas

calleffect
Canvas.new(w, h), resize, clear(color)the framebuffer, 0x00RRGGBB
set, get, fill_rect, rect, fill_rounded, hline, vlineclipped primitives
text(x, y, text, color), Canvas.text_width(text), font_h()bitmap text, ASCII
save_ppm(path) -> !voidwrite the frame as a P6 image
count_color(color)pixel count, for tests

Colors: rgb(r, g, b), mix(a, b, t), hue_color(h).

Writing a widget#

A widget reserves a rectangle, asks interact what the mouse did to it, and draws. This is the whole of button:

pub fn button(self: *mut Self, text: []u8) -> bool {
    let w = Canvas.text_width(text) + 2 * self.pad
    let h = self.row_h
    let id = self.place(w, h)               // reserve, advance the cursor, take an id
    let x = self.last_x
    let y = self.last_y
    let clicked = self.interact(id, x, y, w, h)
    var fill = self.theme.panel
    if self.hot == id { fill = self.theme.hover }
    if self.active == id { fill = self.theme.active }
    self.canvas.fill_rounded(x, y, w, h, fill)
    self.canvas.rect(x, y, w, h, self.theme.border)
    self.canvas.text(x + self.pad, y + (h - font_h()) / 2, text, self.theme.text)
    return clicked
}

Testing without a window#

Ui.headless plus direct writes to ui.input drive widgets from a test block. Set the input after begin_frame(), which clears the per-frame flags:

test "button reports a click across frames" {
    var ui = Ui.headless(200, 100)
    expect(ui.begin_frame())
    ui.input.mouse_x = 20
    ui.input.mouse_y = 20
    ui.input.pressed = true
    ui.input.down = true
    expect(!ui.button("ok"))
    ui.end_frame()
    expect(ui.begin_frame())
    ui.input.released = true
    ui.input.down = false
    expect(ui.button("ok"))
    ui.end_frame()
}

nx test gui/nexium_gui.nx runs these; the test harness runs them too, on every platform, together with an offscreen render of the demo.

Platform layer#

gui/platform.h is eight functions: gp_open, gp_poll, gp_present, gp_width, gp_height, gp_close, gp_time, gp_sleep, and one event struct. gui/platform.c implements them with Win32 (GDI StretchDIBits for the blit). On other platforms it compiles to a stub whose gp_open returns 0, so the library, the tests, and --shot work and only the window is missing. An X11 or Cocoa backend is the same eight functions.

The library declares its own link line:

artifact link { c_sources = ["platform.c"], libs_windows = ["gdi32", "user32"] }

Limits#