//! `libjoltzvui.so` — the retained-tree C ABI, painted by dvui. //! //! The same tree ABI `crates/jolt-vidya` exports on egui and `crates/jolt-tui` //! exports over terminal cells, with a third painter under it. The prefix is //! `zvui_` for the same reason jolt-tui's is `tui_`: a consumer names one //! object in `:jolt/native` and binds one set of symbols. //! //! # What this keeps of jolt-abi's rules, and what it cannot //! //! *Strings out* and *asking, not telling* are kept exactly: a returned string //! lives in one scratch slot until the next string-returning call, and nothing //! here calls back — events are queued and polled. //! //! *Unwinding* is the one rule Zig cannot keep the way `jolt_abi::guard` does. //! There is no `catch_unwind`: a Zig panic aborts the process, so a bug here is //! a dead client rather than a black tile. The answer is to not panic — every //! entry point below is total, allocation failure answers the same fallback a //! caught panic would, and an out-of-range node id is a miss rather than an //! index. Build ReleaseSafe (the default here) so an overflow traps loudly in //! this library instead of quietly corrupting the caller's heap. const std = @import("std"); const dvui = @import("dvui"); const SDLBackend = @import("sdl-backend"); const tree_mod = @import("tree.zig"); const paint = @import("paint.zig"); const Tree = tree_mod.Tree; const log = std.log.scoped(.zvui); var gpa_instance: std.heap.DebugAllocator(.{}) = .init; const gpa = gpa_instance.allocator(); var tree: ?Tree = null; var threaded: ?std.Io.Threaded = null; var backend: ?SDLBackend = null; var win: ?dvui.Window = null; var window_open: bool = false; var interrupted: bool = false; /// Backing store for every `const char *` this library returns. One slot, /// overwritten by the next call — the contract jolt-abi's `Scratch` states, and /// the one `vidya_tree_event_text` and friends already keep. var scratch: std.ArrayList(u8) = .empty; /// Copy `value` into the scratch slot and answer a pointer good until the next /// string-returning call. An interior NUL truncates rather than failing: these /// strings are shown to someone, not parsed. fn lend(value: []const u8) [*:0]const u8 { const cut = std.mem.indexOfScalar(u8, value, 0) orelse value.len; scratch.clearRetainingCapacity(); scratch.appendSlice(gpa, value[0..cut]) catch { scratch.clearRetainingCapacity(); }; scratch.append(gpa, 0) catch { // Out of memory for one byte. An empty C string still terminates. return ""; }; return @ptrCast(scratch.items.ptr); } /// Read a caller's string. Null is the empty string, as in `jolt_abi::borrowed`. fn borrowed(ptr: ?[*:0]const u8) []const u8 { const p = ptr orelse return ""; return std.mem.span(p); } /// The tree exists from first use, so a caller that only pushes nodes never /// pays to open a window. fn theTree() ?*Tree { if (tree == null) { tree = Tree.init(gpa) catch |e| { log.err("tree init failed: {t}", .{e}); return null; }; } return &tree.?; } // ── The window ────────────────────────────────────────────────────────────── export fn zvui_open(width: c_int, height: c_int, title: ?[*:0]const u8) c_int { if (win != null) return 1; // already open; opening twice is a no-op, not an error threaded = std.Io.Threaded.init(gpa, .{}); const io = threaded.?.io(); var buf: [256]u8 = undefined; const name = borrowed(title); const t = std.fmt.bufPrintSentinel(&buf, "{s}", .{name}, 0) catch "zvui"; backend = SDLBackend.initWindow(.{ .io = io, .size = .{ .w = @floatFromInt(width), .h = @floatFromInt(height) }, .vsync = true, .title = t, }) catch |e| { log.err("SDL window failed: {t}", .{e}); threaded.?.deinit(); threaded = null; backend = null; return 0; }; window_open = true; win = dvui.Window.init(@src(), gpa, backend.?.backend(), .{ .theme = switch (backend.?.preferredColorScheme() orelse .dark) { .light => dvui.Theme.builtin.adwaita_light, .dark => dvui.Theme.builtin.adwaita_dark, }, .open_flag = &window_open, }) catch |e| { log.err("dvui window failed: {t}", .{e}); backend.?.deinit(); backend = null; threaded.?.deinit(); threaded = null; window_open = false; return 0; }; return 1; } export fn zvui_close() void { if (win) |*w| w.deinit(); win = null; if (backend) |*b| b.deinit(); backend = null; if (threaded) |*t| t.deinit(); threaded = null; window_open = false; } /// 1 once the person has asked for the window to go away. Answers 1 with no /// window open, so a caller's loop terminates rather than spinning on nothing. export fn zvui_should_close() c_int { if (win == null) return 1; return if (window_open) 0 else 1; } export fn zvui_set_title(title: ?[*:0]const u8) void { const b = &(backend orelse return); const w = &(win orelse return); b.title(w, borrowed(title)); } /// The window's width in points, or 0 before the first frame. Points, not /// pixels: whoever asks is about to lay something out. export fn zvui_screen_width() f32 { const w = &(win orelse return 0); return w.data().rect.w; } export fn zvui_screen_height() f32 { const w = &(win orelse return 0); return w.data().rect.h; } /// Paint the whole tree as one frame and present it. Inert with no window open. export fn zvui_frame() void { const b = &(backend orelse return); const w = &(win orelse return); const t = theTree() orelse return; const nstime = w.beginWait(interrupted); w.begin(nstime) catch |e| { log.err("begin: {t}", .{e}); return; }; b.addAllEvents(w) catch |e| { log.err("events: {t}", .{e}); }; paint.walk(t, t.root()); const end_micros = w.end(.{}) catch |e| blk: { log.err("end: {t}", .{e}); break :blk null; }; const wait_micros = w.waitTime(end_micros); interrupted = b.waitEventTimeout(wait_micros) catch false; } // ── The tree ──────────────────────────────────────────────────────────────── export fn zvui_tree_root() c_int { const t = theTree() orelse return 0; return @intCast(t.root()); } export fn zvui_node_new(tag: ?[*:0]const u8) c_int { const t = theTree() orelse return 0; return @intCast(t.new(borrowed(tag))); } export fn zvui_node_free(node: c_int) void { const t = theTree() orelse return; t.free_node(idOf(node)); } export fn zvui_node_exists(node: c_int) c_int { const t = theTree() orelse return 0; return if (t.get(idOf(node)) != null) 1 else 0; } export fn zvui_node_tag(node: c_int) [*:0]const u8 { const t = theTree() orelse return lend(""); const n = t.get(idOf(node)) orelse return lend(""); return lend(n.tag); } export fn zvui_node_parent(node: c_int) c_int { const t = theTree() orelse return 0; const n = t.get(idOf(node)) orelse return 0; return @intCast(n.parent); } export fn zvui_node_set_str(node: c_int, key: ?[*:0]const u8, value: ?[*:0]const u8) void { const t = theTree() orelse return; t.setStr(idOf(node), borrowed(key), borrowed(value)); } export fn zvui_node_set_num(node: c_int, key: ?[*:0]const u8, value: f64) void { const t = theTree() orelse return; t.setNum(idOf(node), borrowed(key), value); } export fn zvui_node_set_bool(node: c_int, key: ?[*:0]const u8, value: c_int) void { const t = theTree() orelse return; t.setBool(idOf(node), borrowed(key), value != 0); } export fn zvui_node_clear_props(node: c_int) void { const t = theTree() orelse return; t.clear(idOf(node)); } export fn zvui_node_get_str(node: c_int, key: ?[*:0]const u8) [*:0]const u8 { const t = theTree() orelse return lend(""); return lend(t.getStr(idOf(node), borrowed(key))); } export fn zvui_node_get_num(node: c_int, key: ?[*:0]const u8) f64 { const t = theTree() orelse return 0; return t.getNum(idOf(node), borrowed(key)); } export fn zvui_node_get_bool(node: c_int, key: ?[*:0]const u8) c_int { const t = theTree() orelse return 0; return if (t.getBool(idOf(node), borrowed(key))) 1 else 0; } export fn zvui_node_child_count(node: c_int) c_int { const t = theTree() orelse return 0; return @intCast(t.childCount(idOf(node))); } export fn zvui_node_child_at(node: c_int, index: c_int) c_int { const t = theTree() orelse return 0; if (index < 0) return 0; return @intCast(t.childAt(idOf(node), @intCast(index))); } export fn zvui_node_append(parent: c_int, child: c_int) c_int { const t = theTree() orelse return 0; return if (t.append(idOf(parent), idOf(child))) 1 else 0; } export fn zvui_node_remove(parent: c_int, child: c_int) void { const t = theTree() orelse return; t.remove(idOf(parent), idOf(child)); } export fn zvui_node_insert_after(parent: c_int, child: c_int, sibling: c_int) c_int { const t = theTree() orelse return 0; return if (t.insertAfter(idOf(parent), idOf(child), idOf(sibling))) 1 else 0; } export fn zvui_node_replace(parent: c_int, old_child: c_int, new_child: c_int) c_int { const t = theTree() orelse return 0; return if (t.replace(idOf(parent), idOf(old_child), idOf(new_child))) 1 else 0; } /// The subtree under `node` as indented text. For a bug report or a test, the /// same way `vidya_tree_dump` is. export fn zvui_tree_dump(node: c_int) [*:0]const u8 { const t = theTree() orelse return lend(""); var out: std.ArrayList(u8) = .empty; defer out.deinit(gpa); t.dump(idOf(node), &out, 0); return lend(out.items); } // ── Events ────────────────────────────────────────────────────────────────── /// Dequeue one event, answering 1 while there was one. Its fields are read with /// the accessors below, which describe the most recently dequeued event. export fn zvui_tree_poll_event() c_int { const t = theTree() orelse return 0; return if (t.poll()) 1 else 0; } export fn zvui_tree_event_node() c_int { const t = theTree() orelse return 0; const e = t.current orelse return 0; return @intCast(e.node); } /// `click`, `change`, `toggled`, `activate` — or the empty string when nothing /// has been dequeued. export fn zvui_tree_event_name() [*:0]const u8 { const t = theTree() orelse return lend(""); const e = t.current orelse return lend(""); return lend(e.name); } export fn zvui_tree_event_text() [*:0]const u8 { const t = theTree() orelse return lend(""); const e = t.current orelse return lend(""); return lend(e.text); } export fn zvui_tree_event_num() f64 { const t = theTree() orelse return 0; const e = t.current orelse return 0; return e.num; } /// A negative id from C is not a node. Fold it to 0 — the null node — rather /// than let it become a huge index. fn idOf(node: c_int) u32 { return if (node <= 0) 0 else @intCast(node); } test { _ = tree_mod; }