nandi/jolt-nativepublic Fork 0
e39a374fff23521a6ce39b160b2fe55bbd1090e1
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 e39a374fff23521a6ce39b160b2fe55bbd1090e1 · nandi · 9d ago
paint.zig · 251 lines · 9.4 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
//! The painter: one walk of the retained tree per frame, in dvui widgets.
//!
//! The tree carries no sizes and no widget state of its own. Everything here
//! reads props and answers events back into the tree, so the same tree could
//! be walked by a different painter — which is the whole point of the shape
//! `jolt-tui` already proves from the other side.

const std = @import("std");
const dvui = @import("dvui");

const tree_mod = @import("tree.zig");
const Tree = tree_mod.Tree;

/// Every widget here is keyed by the tree's `(generation, index)` identity, so
/// one `@src()` per tag is enough to keep dvui's ids apart.
fn opts(t: *Tree, id: u32) dvui.Options {
    var o: dvui.Options = .{ .id_extra = t.widgetId(id) };
    if (t.getNum(id, "width-request") > 0) {
        o.min_size_content = .{ .w = @floatCast(t.getNum(id, "width-request")), .h = 0 };
    }
    if (t.getNum(id, "margin") > 0) {
        const m: f32 = @floatCast(t.getNum(id, "margin"));
        o.margin = .{ .x = m, .y = m, .w = m, .h = m };
    }
    // `align` is the cross-axis pull a box gives a child: 0 start, 1 end.
    const a = t.getStr(id, "align");
    if (a.len != 0) {
        if (std.mem.eql(u8, a, "center")) {
            o.gravity_x = 0.5;
        } else if (std.mem.eql(u8, a, "end")) {
            o.gravity_x = 1.0;
        } else if (std.mem.eql(u8, a, "fill")) {
            o.expand = .horizontal;
        }
    }
    if (t.getBool(id, "fill-height")) o.expand = .both;
    return o;
}

fn direction(t: *Tree, id: u32) dvui.enums.Direction {
    const tag = (t.get(id) orelse return .vertical).tag;
    if (std.mem.eql(u8, tag, "hbox")) return .horizontal;
    if (std.mem.eql(u8, tag, "vbox")) return .vertical;
    const o = t.getStr(id, "orientation");
    if (std.mem.eql(u8, o, "horizontal")) return .horizontal;
    return .vertical;
}

/// The label a widget shows: `text` first, `label` second. Two names because
/// the tags disagree about which one reads better, not because they differ.
fn caption(t: *Tree, id: u32) []const u8 {
    const text = t.getStr(id, "text");
    if (text.len != 0) return text;
    return t.getStr(id, "label");
}

pub fn walk(t: *Tree, id: u32) void {
    const n = t.get(id) orelse return;
    const tag = n.tag;

    if (std.mem.eql(u8, tag, "window") or
        std.mem.eql(u8, tag, "box") or
        std.mem.eql(u8, tag, "hbox") or
        std.mem.eql(u8, tag, "vbox"))
    {
        var o = opts(t, id);
        if (o.expand == null) o.expand = .horizontal;
        var b = dvui.box(@src(), .{ .dir = direction(t, id) }, o);
        defer b.deinit();
        children(t, id, @floatCast(t.getNum(id, "spacing")));
        return;
    }

    if (std.mem.eql(u8, tag, "page")) {
        // A page is a column with a reading width, centred in whatever it got.
        var o = opts(t, id);
        o.expand = .both;
        var outer = dvui.box(@src(), .{ .dir = .horizontal }, o);
        defer outer.deinit();
        const max = t.getNum(id, "max-width");
        var inner = dvui.box(@src(), .{ .dir = .vertical }, .{
            .id_extra = t.widgetId(id),
            .expand = .vertical,
            .gravity_x = 0.5,
            .min_size_content = .{ .w = if (max > 0) @floatCast(max) else 0, .h = 0 },
            .max_size_content = if (max > 0) .width(@floatCast(max)) else null,
        });
        defer inner.deinit();
        children(t, id, @floatCast(t.getNum(id, "spacing")));
        return;
    }

    if (std.mem.eql(u8, tag, "card") or std.mem.eql(u8, tag, "frame")) {
        var o = opts(t, id);
        if (o.expand == null) o.expand = .horizontal;
        o.background = true;
        o.corners = dvui.CornerRect.all(6);
        o.border = dvui.Rect.all(1);
        o.padding = dvui.Rect.all(8);
        var b = dvui.box(@src(), .{ .dir = direction(t, id) }, o);
        defer b.deinit();
        children(t, id, @floatCast(t.getNum(id, "spacing")));
        return;
    }

    if (std.mem.eql(u8, tag, "scroll")) {
        var o = opts(t, id);
        o.expand = .both;
        var s = dvui.scrollArea(@src(), .{}, o);
        defer s.deinit();
        children(t, id, @floatCast(t.getNum(id, "spacing")));
        return;
    }

    if (std.mem.eql(u8, tag, "label") or
        std.mem.eql(u8, tag, "title") or
        std.mem.eql(u8, tag, "dim-label"))
    {
        var o = opts(t, id);
        if (std.mem.eql(u8, tag, "title")) {
            o.font = dvui.themeGet().font_title;
        } else if (std.mem.eql(u8, tag, "dim-label")) {
            o.color_text = .{ .color = dvui.themeGet().color(.window, .text).opacity(0.6) };
        }
        dvui.labelNoFmt(@src(), caption(t, id), .{ .align_x = o.gravity_x orelse 0 }, o);
        return;
    }

    if (std.mem.eql(u8, tag, "separator")) {
        var o = opts(t, id);
        if (o.expand == null) o.expand = .horizontal;
        o.min_size_content = .{ .w = 0, .h = 1 };
        _ = dvui.separator(@src(), o);
        return;
    }

    if (std.mem.eql(u8, tag, "spacer") or std.mem.eql(u8, tag, "gap")) {
        var o = opts(t, id);
        const size: f32 = @floatCast(t.getNum(id, "size"));
        o.min_size_content = .{ .w = size, .h = size };
        _ = dvui.spacer(@src(), o);
        return;
    }

    if (std.mem.eql(u8, tag, "button")) {
        var o = opts(t, id);
        // `sensitive` is absent by default, and absent means enabled — an
        // unset prop must not read as a dead button.
        const enabled = t.get(id).?.props.contains("sensitive") == false or t.getBool(id, "sensitive");
        if (!enabled) o.color_text = .{ .color = dvui.themeGet().color(.window, .text).opacity(0.4) };
        const kind = t.getStr(id, "kind");
        if (std.mem.eql(u8, kind, "primary")) {
            o.color_fill = .{ .color = dvui.themeGet().color(.highlight, .fill) };
        } else if (std.mem.eql(u8, kind, "destructive")) {
            o.color_fill = .{ .color = dvui.themeGet().color(.err, .fill) };
        }
        if (dvui.button(@src(), caption(t, id), .{}, o) and enabled) {
            t.emit(id, "click", "", 0);
        }
        return;
    }

    if (std.mem.eql(u8, tag, "checkbox") or std.mem.eql(u8, tag, "checkbutton")) {
        // dvui writes through the target, so the prop is the store: read it in,
        // let the widget edit it, write back what changed and say so.
        var checked = t.getBool(id, "value") or t.getBool(id, "active");
        const before = checked;
        _ = dvui.checkbox(@src(), &checked, caption(t, id), opts(t, id));
        if (checked != before) {
            t.setBool(id, "value", checked);
            t.setBool(id, "active", checked);
            t.emit(id, "toggled", "", if (checked) 1 else 0);
        }
        return;
    }

    if (std.mem.eql(u8, tag, "entry")) {
        entry(t, id);
        return;
    }

    if (std.mem.eql(u8, tag, "progress")) {
        var o = opts(t, id);
        if (o.expand == null) o.expand = .horizontal;
        o.min_size_content = .{ .w = 0, .h = 10 };
        dvui.progress(@src(), .{ .percent = @floatCast(t.getNum(id, "value")) }, o);
        return;
    }

    // An unknown tag is a container, not an error: a tree written against a
    // richer painter should still show its contents here rather than vanish.
    var b = dvui.box(@src(), .{ .dir = .vertical }, opts(t, id));
    defer b.deinit();
    children(t, id, @floatCast(t.getNum(id, "spacing")));
}

fn entry(t: *Tree, id: u32) void {
    const n = t.get(id) orelse return;
    if (n.entry == null) n.entry = .empty;
    if (!n.entry_seeded) {
        // The caller's `text` wins until the person typing changes it.
        const want = t.getStr(id, "text");
        n.entry.?.clearRetainingCapacity();
        n.entry.?.appendSlice(t.gpa, want) catch {};
        n.entry_seeded = true;
    }

    const placeholder = t.getStr(id, "placeholder");
    var o = opts(t, id);
    if (o.expand == null) o.expand = .horizontal;

    var te = dvui.textEntry(@src(), .{
        .text = .{ .array_list = .{ .backing = &n.entry.?, .allocator = t.gpa } },
        .placeholder = if (placeholder.len != 0) placeholder else null,
        .multiline = t.getBool(id, "multiline"),
    }, o);
    const changed = te.text_changed;
    const entered = te.enter_pressed;
    te.deinit();

    // Read the buffer back only after deinit: the widget updates the list there.
    const now = if (t.get(id)) |m| (if (m.entry) |buf| buf.items else "") else "";
    if (changed) {
        // Mirror into the prop so `zvui_node_get_str(node, "text")` answers what
        // is on screen, then emit — without disturbing `entry_seeded`, which
        // `setStr` would clear and so wipe the cursor position every keystroke.
        const owned = t.gpa.dupe(u8, now) catch return;
        defer t.gpa.free(owned);
        t.setStr(id, "text", owned);
        if (t.get(id)) |m| m.entry_seeded = true;
        t.emit(id, "change", owned, 0);
    }
    if (entered) t.emit(id, "activate", now, 0);
}

fn children(t: *Tree, id: u32, spacing: f32) void {
    const n = t.get(id) orelse return;
    // Copy the child list before walking it: painting emits events, and a
    // caller is free to restructure the tree between frames on the same thread.
    const kids = t.gpa.dupe(u32, n.children.items) catch return;
    defer t.gpa.free(kids);
    for (kids, 0..) |c, i| {
        if (spacing > 0 and i > 0) {
            _ = dvui.spacer(@src(), .{
                .id_extra = t.widgetId(c),
                .min_size_content = .{ .w = spacing, .h = spacing },
            });
        }
        walk(t, c);
    }
}