GitHub - duanebester/gooey: Gooey is a hybrid immediate/retained mode UI framework designed for building fast, GPU-rendered applications on macOS/Metal, WebAssembly/WebGPU, and Wayland/Vulkan
Pangram verdict · v3.3
We believe that this document is mainly AI-generated, with some AI-assisted and human-written content
AI likelihood · overall
AIArticle text · 1,554 words · 7 segments analyzed
A GPU-accelerated UI framework for Zig, targeting macOS (Metal), Linux (Vulkan/Wayland), and Browser (WASM/WebGPU). Join the Gooey discord Early Development: API is evolving. Example app built with Gooey — chat-zig, an Anthropic Claude client using the Zig 0.16 std.Io stack for async HTTP: Features GPU Rendering - Metal (macOS), Vulkan (Linux) with MSAA anti-aliasing (WebGPU/WASM is blocked upstream on Zig 0.16 — see WASM) Declarative UI - Component-based layout with ui.* primitives and flexbox-style system Cx/UI Separation - Cx for state, handlers, and focus; ui.* for layout primitives Pure State Pattern - Testable state methods with automatic re-rendering Animation System - Built-in animations with easing, animateOn triggers Entity System - Dynamic entity creation/deletion with auto-cleanup Retained Widgets - TextInput, TextArea, Checkbox, Scroll containers Text Rendering - CoreText (macOS), FreeType/HarfBuzz (Linux), Canvas (WASM) Custom Shaders - Drop in your own Metal/GLSL shaders Drag & Drop - Type-safe drag sources and drop targets with pointer_events control Liquid Glass - macOS 26.0+ Tahoe transparent window effects Actions & Keybindings - Contextual action system with keymap Theming - Built-in light/dark mode support Images & SVG - Load images and render SVG icons with styling File Dialogs - Native file open/save dialogs (macOS, Linux, WASM) Clipboard - Native clipboard support on all platforms IME Support - Input method editor for international text input Accessibility - Built-in screen reader support (VoiceOver, Orca, ARIA) with semantic roles and live regions Zero Dependencies - No external Zig packages; builds against system frameworks/libraries only (the Objective-C runtime bindings are vendored in-tree) Quick Start Requirements: Zig 0.16.0+ Dependencies: None. Gooey has zero external Zig package dependencies — build.zig.zon lists no dependencies. It links only against platform system frameworks/libraries (see platform notes below).
macOS: macOS 12.0+ Linux: Wayland compositor, Vulkan drivers, FreeType, HarfBuzz, Fontconfig, libpng, D-Bus zig build run # Showcase demo zig build run-counter # Counter example zig build run-todo # Todo app (state, handlers, TextInput, lists) zig build run-animation # Animation demo zig build run-pomodoro # Pomodoro timer zig build run-glass # Liquid glass effect zig build run-spaceship # Space dashboard with shader zig build run-dynamic-counters # Entity system demo zig build run-layout # Flexbox, shrink, text wrapping zig build run-actions # Keybindings demo zig build run-select # Dropdown select component zig build run-tooltip # Tooltip component zig build run-modal # Modal dialogs zig build run-images # Image loading and styling zig build run-file-dialog # Native file dialogs zig build run-uniform-list # Virtualized list (10k items) zig build run-virtual-list # Variable-height list zig build run-data-table # Virtualized table (10k rows) zig build run-code-editor # Code editor with syntax highlighting zig build test # Run tests Example A small todo app that touches a representative slice of the API: a pure, UI-free state model; cx.update / cx.updateWith / cx.command handlers; a bound TextInput; Checkbox and Button; list iteration; and unit tests that exercise the state with no UI in play. The full, runnable source lives in src/examples/todo.zig (zig build run-todo). Its state model is covered by the tests shown at the bottom, which run as part of zig build test. const std = @import("std"); const gooey = @import("gooey"); const ui = gooey.ui; const Cx = gooey.Cx; const Button = gooey.components.Button; const Checkbox = gooey.components.Checkbox; const TextInput = gooey.components.TextInput; const MAX_TODOS = 64; const TEXT_CAP = 128; const draft_input_id = "new-todo"; // State is pure — no UI knowledge, fully testable.
const Todo = struct { buf: [TEXT_CAP]u8 = [_]u8{0} ** TEXT_CAP, len: usize = 0, done: bool = false, fn text(self: *const Todo) []const u8 { return self.buf[0..self.len]; } }; const Filter = enum { all, active, done }; const AppState = struct { todos: [MAX_TODOS]Todo = [_]Todo{.{}} ** MAX_TODOS, count: usize = 0, draft: []const u8 = "", // two-way bound to the TextInput filter: Filter = .all, // Pure logic — what the tests below drive. fn pushTodo(self: *AppState, value: []const u8) void { const trimmed = std.mem.trim(u8, value, " \t\r\n"); if (trimmed.len == 0) return; if (self.count >= MAX_TODOS) return; const slot = &self.todos[self.count]; const n = @min(trimmed.len, TEXT_CAP); @memcpy(slot.buf[0..n], trimmed[0..n]); slot.len = n; slot.done = false; self.count += 1; } pub fn toggle(self: *AppState, index: usize) void { if (index >= self.count) return; self.todos[index].done = !self.todos[index].done; } pub fn remove(self: *AppState, index: usize) void { if (index >= self.count) return; var i = index; while (i + 1 < self.count) : (i += 1) self.todos[i] = self.todos[i + 1]; self.count -= 1; } pub fn setFilter(self: *AppState, filter: Filter) void { self.filter = filter; } pub fn clearCompleted(self: *AppState) void { var write: usize = 0; var read: usize = 0; while (read < self.count) : (read += 1) { if (!
self.todos[read].done) { self.todos[write] = self.todos[read]; write += 1; } } self.count = write; } fn remaining(self: *const AppState) u32 { var n: u32 = 0; for (self.todos[0..self.count]) |*t| { if (!t.done) n += 1; } return n; } fn visible(self: *const AppState, t: *const Todo) bool { return switch (self.filter) { .all => true, .active => !t.done, .done => t.done, }; } // Command — needs framework access (the binding only flows widget -> state, // so we reach the retained input widget to clear it after adding). pub fn addTodo(self: *AppState, g: *gooey.Window) void { self.pushTodo(self.draft); self.draft = ""; if (g.widgetState(gooey.widgets.TextInputState, draft_input_id)) |input| { input.clear(); } } }; var state = AppState{}; const App = gooey.App(AppState, &state, render, .{ .title = "Todos", .width = 480, .height = 560, }); comptime { _ = App; // Force analysis (also wires @export on WASM). }
pub fn main(init: std.process.Init) !void { return App.main(init); } fn render(cx: *Cx) void { const s = cx.state(AppState); const size = cx.windowSize(); cx.render(ui.box(.{ .width = size.width, .height = size.height, .direction = .column, .padding = .{ .all = 24 }, .gap = 16, .background = ui.Color.rgb(0.96, 0.96, 0.97), }, .{ ui.text("Todos", .{ .size = 28 }), // Input row: TextInput binds to state.draft; Add is a command. ui.hstack(.{ .gap = 8, .alignment = .center }, .{ TextInput{ .id = draft_input_id, .placeholder = "What needs doing?", .bind = &s.draft, .fill_width = true }, Button{ .label = "Add", .on_click_handler = cx.command(AppState.addTodo) }, }), // Filters: each button packs its enum value into the handler arg. ui.hstack(.{ .gap = 8 }, .{ FilterButton{ .label = "All", .filter = .all, .active = s.filter == .all }, FilterButton{ .label = "Active", .filter = .active, .active = s.filter == .active }, FilterButton{ .label = "Done", .filter = .done, .active = s.filter == .done }, }), // The list, or an empty-state hint. ui.when(s.count == 0, .{ ui.text("Nothing yet — add your first todo above.", .{ .size = 14 }), }), TodoItems{}, ui.spacer(), ui.hstack(.{ .gap = 12, .alignment = .center }, .{ ui.textFmt("{d} left", .{s.remaining()}, .{ .size = 14 }), ui.spacer(), Button{ .label = "Clear completed", .variant = .secondary, .size = .small, .on_click_handler = cx.update(AppState.clearCompleted) }, }), })); } // Iteration lives in a component because each row needs `cx` for its handlers.
const TodoItems = struct { pub fn render(_: @This(), cx: *Cx) void { const s = cx.state(AppState); for (s.todos[0..s.count], 0..) |*todo, index| { if (!s.visible(todo)) continue; cx.render(TodoRow{ .index = index, .done = todo.done, .label = todo.text() }); } } }; const TodoRow = struct { index: usize, done: bool, label: []const u8, pub fn render(self: @This(), cx: *Cx) void { // A background + cross-axis centering means this is a `box` (with // `.direction = .row`), not an `hstack` — stacks carry only gap/ // alignment/padding. cx.render(ui.box(.{ .direction = .row, .gap = 12, .alignment = .{ .cross = .center }, .padding = .{ .all = 10 }, .background = ui.Color.white, .corner_radius = 8, }, .{ Checkbox{ .checked = self.done, .on_click_handler = cx.updateWith(self.index, AppState.toggle) }, ui.text(self.label, .{ .size = 16 }), ui.spacer(), Button{ .label = "Delete", .variant = .danger, .size = .small, .on_click_handler = cx.updateWith(self.index, AppState.remove) }, })); } }; const FilterButton = struct { label: []const u8, filter: Filter, active: bool, pub fn render(self: @This(), cx: *Cx) void { cx.render(Button{ .label = self.label, .size = .small, .variant = if (self.active) .primary else .secondary, .on_click_handler = cx.updateWith(self.filter, AppState.setFilter), }); } }; // State is testable without UI.
test "remove keeps the list contiguous" { var s = AppState{}; s.pushTodo("a"); s.pushTodo("b"); s.pushTodo("c"); s.remove(1); // drop "b" try std.testing.expectEqual(@as(usize, 2), s.count); try std.testing.expectEqualStrings("a", s.todos[0].text()); try std.testing.expectEqualStrings("c", s.todos[1].text()); } test "remaining and clearCompleted" { var s = AppState{}; s.pushTodo("a"); s.pushTodo("b"); s.toggle(0); try std.testing.expectEqual(@as(u32, 1), s.remaining()); s.clearCompleted(); try std.testing.expectEqual(@as(usize, 1), s.count); try std.testing.expectEqualStrings("b", s.todos[0].text()); } API Pattern Gooey separates concerns between Cx (context) and ui (layout primitives): Module Purpose Examples cx.* State, handlers, animations, focus cx.state(), cx.update(), cx.animate(), cx.changed(), cx.render() ui.* Layout containers and primitives ui.box(), ui.rect(), ui.hstack(), ui.vstack(), ui.text(), ui.when() fn render(cx: *Cx) void { const s = cx.state(AppState); cx.render(ui.box(.{ .width = 100 }, .{ ui.text("Hello", .{}), // Conditional rendering ui.when(s.show_extra, .{ ui.text("Extra content", .{}), }), // Iterate over items ui.each(&s.items, struct { fn render(item: Item, _: usize) @TypeOf(ui.text("", .{})) { return ui.text(item.name, .{}); } }.render), })); } Key primitives: ui.box() - Container with flexbox layout ui.rect() - Childless box (dividers, spacers, colored blocks) ui.hstack() / ui.vstack() - Horizontal/vertical stacks ui.text() / ui.textFmt() - Text rendering ui.when(cond, children) - Conditional rendering ui.maybe(optional, fn) - Render if optional has value ui.each(items, fn) - Render for each item ui.scroll(id, style, children) - Scrollable container ui.spacer() - Flexible space Handler Types Method Signature Use Case cx.update()