nandi/jolt-nativepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

lib.zig · 339 lines · 11.2 KBZig Blame HistoryRaw
Paint the tree ABI a third way: Zig and dvui 7289263 nandi 9d ago1//! `libjoltzvui.so` — the retained-tree C ABI, painted by dvui.
2//!
3//! The same tree ABI `crates/jolt-vidya` exports on egui and `crates/jolt-tui`
4//! exports over terminal cells, with a third painter under it. The prefix is
5//! `zvui_` for the same reason jolt-tui's is `tui_`: a consumer names one
6//! object in `:jolt/native` and binds one set of symbols.
7//!
8//! # What this keeps of jolt-abi's rules, and what it cannot
9//!
10//! *Strings out* and *asking, not telling* are kept exactly: a returned string
11//! lives in one scratch slot until the next string-returning call, and nothing
12//! here calls back — events are queued and polled.
13//!
14//! *Unwinding* is the one rule Zig cannot keep the way `jolt_abi::guard` does.
15//! There is no `catch_unwind`: a Zig panic aborts the process, so a bug here is
16//! a dead client rather than a black tile. The answer is to not panic — every
17//! entry point below is total, allocation failure answers the same fallback a
18//! caught panic would, and an out-of-range node id is a miss rather than an
19//! index. Build ReleaseSafe (the default here) so an overflow traps loudly in
20//! this library instead of quietly corrupting the caller's heap.
21
22const std = @import("std");
23const dvui = @import("dvui");
24const SDLBackend = @import("sdl-backend");
25
26const tree_mod = @import("tree.zig");
27const paint = @import("paint.zig");
28const Tree = tree_mod.Tree;
29
30const log = std.log.scoped(.zvui);
31
32var gpa_instance: std.heap.DebugAllocator(.{}) = .init;
33const gpa = gpa_instance.allocator();
34
35var tree: ?Tree = null;
36
37var threaded: ?std.Io.Threaded = null;
38var backend: ?SDLBackend = null;
39var win: ?dvui.Window = null;
40var window_open: bool = false;
41var interrupted: bool = false;
42
43/// Backing store for every `const char *` this library returns. One slot,
44/// overwritten by the next call — the contract jolt-abi's `Scratch` states, and
45/// the one `vidya_tree_event_text` and friends already keep.
46var scratch: std.ArrayList(u8) = .empty;
47
48/// Copy `value` into the scratch slot and answer a pointer good until the next
49/// string-returning call. An interior NUL truncates rather than failing: these
50/// strings are shown to someone, not parsed.
51fn lend(value: []const u8) [*:0]const u8 {
52 const cut = std.mem.indexOfScalar(u8, value, 0) orelse value.len;
53 scratch.clearRetainingCapacity();
54 scratch.appendSlice(gpa, value[0..cut]) catch {
55 scratch.clearRetainingCapacity();
56 };
57 scratch.append(gpa, 0) catch {
58 // Out of memory for one byte. An empty C string still terminates.
59 return "";
60 };
61 return @ptrCast(scratch.items.ptr);
62}
63
64/// Read a caller's string. Null is the empty string, as in `jolt_abi::borrowed`.
65fn borrowed(ptr: ?[*:0]const u8) []const u8 {
66 const p = ptr orelse return "";
67 return std.mem.span(p);
68}
69
70/// The tree exists from first use, so a caller that only pushes nodes never
71/// pays to open a window.
72fn theTree() ?*Tree {
73 if (tree == null) {
74 tree = Tree.init(gpa) catch |e| {
75 log.err("tree init failed: {t}", .{e});
76 return null;
77 };
78 }
79 return &tree.?;
80}
81
82// ── The window ──────────────────────────────────────────────────────────────
83
84export fn zvui_open(width: c_int, height: c_int, title: ?[*:0]const u8) c_int {
85 if (win != null) return 1; // already open; opening twice is a no-op, not an error
86
87 threaded = std.Io.Threaded.init(gpa, .{});
88 const io = threaded.?.io();
89
90 var buf: [256]u8 = undefined;
91 const name = borrowed(title);
92 const t = std.fmt.bufPrintSentinel(&buf, "{s}", .{name}, 0) catch "zvui";
93
94 backend = SDLBackend.initWindow(.{
95 .io = io,
96 .size = .{ .w = @floatFromInt(width), .h = @floatFromInt(height) },
97 .vsync = true,
98 .title = t,
99 }) catch |e| {
100 log.err("SDL window failed: {t}", .{e});
101 threaded.?.deinit();
102 threaded = null;
103 backend = null;
104 return 0;
105 };
106
107 window_open = true;
108 win = dvui.Window.init(@src(), gpa, backend.?.backend(), .{
109 .theme = switch (backend.?.preferredColorScheme() orelse .dark) {
110 .light => dvui.Theme.builtin.adwaita_light,
111 .dark => dvui.Theme.builtin.adwaita_dark,
112 },
113 .open_flag = &window_open,
114 }) catch |e| {
115 log.err("dvui window failed: {t}", .{e});
116 backend.?.deinit();
117 backend = null;
118 threaded.?.deinit();
119 threaded = null;
120 window_open = false;
121 return 0;
122 };
123 return 1;
124}
125
126export fn zvui_close() void {
127 if (win) |*w| w.deinit();
128 win = null;
129 if (backend) |*b| b.deinit();
130 backend = null;
131 if (threaded) |*t| t.deinit();
132 threaded = null;
133 window_open = false;
134}
135
136/// 1 once the person has asked for the window to go away. Answers 1 with no
137/// window open, so a caller's loop terminates rather than spinning on nothing.
138export fn zvui_should_close() c_int {
139 if (win == null) return 1;
140 return if (window_open) 0 else 1;
141}
142
143export fn zvui_set_title(title: ?[*:0]const u8) void {
144 const b = &(backend orelse return);
145 const w = &(win orelse return);
146 b.title(w, borrowed(title));
147}
148
149/// The window's width in points, or 0 before the first frame. Points, not
150/// pixels: whoever asks is about to lay something out.
151export fn zvui_screen_width() f32 {
152 const w = &(win orelse return 0);
153 return w.data().rect.w;
154}
155
156export fn zvui_screen_height() f32 {
157 const w = &(win orelse return 0);
158 return w.data().rect.h;
159}
160
161/// Paint the whole tree as one frame and present it. Inert with no window open.
162export fn zvui_frame() void {
163 const b = &(backend orelse return);
164 const w = &(win orelse return);
165 const t = theTree() orelse return;
166
167 const nstime = w.beginWait(interrupted);
168 w.begin(nstime) catch |e| {
169 log.err("begin: {t}", .{e});
170 return;
171 };
172 b.addAllEvents(w) catch |e| {
173 log.err("events: {t}", .{e});
174 };
175
176 paint.walk(t, t.root());
177
178 const end_micros = w.end(.{}) catch |e| blk: {
179 log.err("end: {t}", .{e});
180 break :blk null;
181 };
182 const wait_micros = w.waitTime(end_micros);
183 interrupted = b.waitEventTimeout(wait_micros) catch false;
184}
185
186// ── The tree ────────────────────────────────────────────────────────────────
187
188export fn zvui_tree_root() c_int {
189 const t = theTree() orelse return 0;
190 return @intCast(t.root());
191}
192
193export fn zvui_node_new(tag: ?[*:0]const u8) c_int {
194 const t = theTree() orelse return 0;
195 return @intCast(t.new(borrowed(tag)));
196}
197
198export fn zvui_node_free(node: c_int) void {
199 const t = theTree() orelse return;
200 t.free_node(idOf(node));
201}
202
203export fn zvui_node_exists(node: c_int) c_int {
204 const t = theTree() orelse return 0;
205 return if (t.get(idOf(node)) != null) 1 else 0;
206}
207
208export fn zvui_node_tag(node: c_int) [*:0]const u8 {
209 const t = theTree() orelse return lend("");
210 const n = t.get(idOf(node)) orelse return lend("");
211 return lend(n.tag);
212}
213
214export fn zvui_node_parent(node: c_int) c_int {
215 const t = theTree() orelse return 0;
216 const n = t.get(idOf(node)) orelse return 0;
217 return @intCast(n.parent);
218}
219
220export fn zvui_node_set_str(node: c_int, key: ?[*:0]const u8, value: ?[*:0]const u8) void {
221 const t = theTree() orelse return;
222 t.setStr(idOf(node), borrowed(key), borrowed(value));
223}
224
225export fn zvui_node_set_num(node: c_int, key: ?[*:0]const u8, value: f64) void {
226 const t = theTree() orelse return;
227 t.setNum(idOf(node), borrowed(key), value);
228}
229
230export fn zvui_node_set_bool(node: c_int, key: ?[*:0]const u8, value: c_int) void {
231 const t = theTree() orelse return;
232 t.setBool(idOf(node), borrowed(key), value != 0);
233}
234
235export fn zvui_node_clear_props(node: c_int) void {
236 const t = theTree() orelse return;
237 t.clear(idOf(node));
238}
239
240export fn zvui_node_get_str(node: c_int, key: ?[*:0]const u8) [*:0]const u8 {
241 const t = theTree() orelse return lend("");
242 return lend(t.getStr(idOf(node), borrowed(key)));
243}
244
245export fn zvui_node_get_num(node: c_int, key: ?[*:0]const u8) f64 {
246 const t = theTree() orelse return 0;
247 return t.getNum(idOf(node), borrowed(key));
248}
249
250export fn zvui_node_get_bool(node: c_int, key: ?[*:0]const u8) c_int {
251 const t = theTree() orelse return 0;
252 return if (t.getBool(idOf(node), borrowed(key))) 1 else 0;
253}
254
255export fn zvui_node_child_count(node: c_int) c_int {
256 const t = theTree() orelse return 0;
257 return @intCast(t.childCount(idOf(node)));
258}
259
260export fn zvui_node_child_at(node: c_int, index: c_int) c_int {
261 const t = theTree() orelse return 0;
262 if (index < 0) return 0;
263 return @intCast(t.childAt(idOf(node), @intCast(index)));
264}
265
266export fn zvui_node_append(parent: c_int, child: c_int) c_int {
267 const t = theTree() orelse return 0;
268 return if (t.append(idOf(parent), idOf(child))) 1 else 0;
269}
270
271export fn zvui_node_remove(parent: c_int, child: c_int) void {
272 const t = theTree() orelse return;
273 t.remove(idOf(parent), idOf(child));
274}
275
276export fn zvui_node_insert_after(parent: c_int, child: c_int, sibling: c_int) c_int {
277 const t = theTree() orelse return 0;
278 return if (t.insertAfter(idOf(parent), idOf(child), idOf(sibling))) 1 else 0;
279}
280
281export fn zvui_node_replace(parent: c_int, old_child: c_int, new_child: c_int) c_int {
282 const t = theTree() orelse return 0;
283 return if (t.replace(idOf(parent), idOf(old_child), idOf(new_child))) 1 else 0;
284}
285
286/// The subtree under `node` as indented text. For a bug report or a test, the
287/// same way `vidya_tree_dump` is.
288export fn zvui_tree_dump(node: c_int) [*:0]const u8 {
289 const t = theTree() orelse return lend("");
290 var out: std.ArrayList(u8) = .empty;
291 defer out.deinit(gpa);
292 t.dump(idOf(node), &out, 0);
293 return lend(out.items);
294}
295
296// ── Events ──────────────────────────────────────────────────────────────────
297
298/// Dequeue one event, answering 1 while there was one. Its fields are read with
299/// the accessors below, which describe the most recently dequeued event.
300export fn zvui_tree_poll_event() c_int {
301 const t = theTree() orelse return 0;
302 return if (t.poll()) 1 else 0;
303}
304
305export fn zvui_tree_event_node() c_int {
306 const t = theTree() orelse return 0;
307 const e = t.current orelse return 0;
308 return @intCast(e.node);
309}
310
311/// `click`, `change`, `toggled`, `activate` — or the empty string when nothing
312/// has been dequeued.
313export fn zvui_tree_event_name() [*:0]const u8 {
314 const t = theTree() orelse return lend("");
315 const e = t.current orelse return lend("");
316 return lend(e.name);
317}
318
319export fn zvui_tree_event_text() [*:0]const u8 {
320 const t = theTree() orelse return lend("");
321 const e = t.current orelse return lend("");
322 return lend(e.text);
323}
324
325export fn zvui_tree_event_num() f64 {
326 const t = theTree() orelse return 0;
327 const e = t.current orelse return 0;
328 return e.num;
329}
330
331/// A negative id from C is not a node. Fold it to 0 — the null node — rather
332/// than let it become a huge index.
333fn idOf(node: c_int) u32 {
334 return if (node <= 0) 0 else @intCast(node);
335}
336
337test {
338 _ = tree_mod;
339}