nandi/jolt-nativepublic Fork 0
d5dfd53e28ca47b0a4fdb550d5225ffa10aff7cf
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.

Paint the tree ABI a third way: Zig and dvui 7289263 · on d5dfd53e28ca47b0a4fdb550d5225ffa10aff7cf · nandi · 9d ago
lib.zig · 339 lines · 11.2 KBZig Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
//! `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;
}