nandi/jolt-nativepublic Fork 0
361b4dc4e78fd6c874f47af7a0144fb4accb1cba
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.zig · 251 lines · 9.4 KBZig Blame HistoryRaw
Paint the tree ABI a third way: Zig and dvui 7289263 nandi 9d ago1//! The painter: one walk of the retained tree per frame, in dvui widgets.
2//!
3//! The tree carries no sizes and no widget state of its own. Everything here
4//! reads props and answers events back into the tree, so the same tree could
5//! be walked by a different painter — which is the whole point of the shape
6//! `jolt-tui` already proves from the other side.
7
8const std = @import("std");
9const dvui = @import("dvui");
10
11const tree_mod = @import("tree.zig");
12const Tree = tree_mod.Tree;
13
14/// Every widget here is keyed by the tree's `(generation, index)` identity, so
15/// one `@src()` per tag is enough to keep dvui's ids apart.
16fn opts(t: *Tree, id: u32) dvui.Options {
17 var o: dvui.Options = .{ .id_extra = t.widgetId(id) };
18 if (t.getNum(id, "width-request") > 0) {
19 o.min_size_content = .{ .w = @floatCast(t.getNum(id, "width-request")), .h = 0 };
20 }
21 if (t.getNum(id, "margin") > 0) {
22 const m: f32 = @floatCast(t.getNum(id, "margin"));
23 o.margin = .{ .x = m, .y = m, .w = m, .h = m };
24 }
25 // `align` is the cross-axis pull a box gives a child: 0 start, 1 end.
26 const a = t.getStr(id, "align");
27 if (a.len != 0) {
28 if (std.mem.eql(u8, a, "center")) {
29 o.gravity_x = 0.5;
30 } else if (std.mem.eql(u8, a, "end")) {
31 o.gravity_x = 1.0;
32 } else if (std.mem.eql(u8, a, "fill")) {
33 o.expand = .horizontal;
34 }
35 }
36 if (t.getBool(id, "fill-height")) o.expand = .both;
37 return o;
38}
39
40fn direction(t: *Tree, id: u32) dvui.enums.Direction {
41 const tag = (t.get(id) orelse return .vertical).tag;
42 if (std.mem.eql(u8, tag, "hbox")) return .horizontal;
43 if (std.mem.eql(u8, tag, "vbox")) return .vertical;
44 const o = t.getStr(id, "orientation");
45 if (std.mem.eql(u8, o, "horizontal")) return .horizontal;
46 return .vertical;
47}
48
49/// The label a widget shows: `text` first, `label` second. Two names because
50/// the tags disagree about which one reads better, not because they differ.
51fn caption(t: *Tree, id: u32) []const u8 {
52 const text = t.getStr(id, "text");
53 if (text.len != 0) return text;
54 return t.getStr(id, "label");
55}
56
57pub fn walk(t: *Tree, id: u32) void {
58 const n = t.get(id) orelse return;
59 const tag = n.tag;
60
61 if (std.mem.eql(u8, tag, "window") or
62 std.mem.eql(u8, tag, "box") or
63 std.mem.eql(u8, tag, "hbox") or
64 std.mem.eql(u8, tag, "vbox"))
65 {
66 var o = opts(t, id);
67 if (o.expand == null) o.expand = .horizontal;
68 var b = dvui.box(@src(), .{ .dir = direction(t, id) }, o);
69 defer b.deinit();
70 children(t, id, @floatCast(t.getNum(id, "spacing")));
71 return;
72 }
73
74 if (std.mem.eql(u8, tag, "page")) {
75 // A page is a column with a reading width, centred in whatever it got.
76 var o = opts(t, id);
77 o.expand = .both;
78 var outer = dvui.box(@src(), .{ .dir = .horizontal }, o);
79 defer outer.deinit();
80 const max = t.getNum(id, "max-width");
81 var inner = dvui.box(@src(), .{ .dir = .vertical }, .{
82 .id_extra = t.widgetId(id),
83 .expand = .vertical,
84 .gravity_x = 0.5,
85 .min_size_content = .{ .w = if (max > 0) @floatCast(max) else 0, .h = 0 },
86 .max_size_content = if (max > 0) .width(@floatCast(max)) else null,
87 });
88 defer inner.deinit();
89 children(t, id, @floatCast(t.getNum(id, "spacing")));
90 return;
91 }
92
93 if (std.mem.eql(u8, tag, "card") or std.mem.eql(u8, tag, "frame")) {
94 var o = opts(t, id);
95 if (o.expand == null) o.expand = .horizontal;
96 o.background = true;
97 o.corners = dvui.CornerRect.all(6);
98 o.border = dvui.Rect.all(1);
99 o.padding = dvui.Rect.all(8);
100 var b = dvui.box(@src(), .{ .dir = direction(t, id) }, o);
101 defer b.deinit();
102 children(t, id, @floatCast(t.getNum(id, "spacing")));
103 return;
104 }
105
106 if (std.mem.eql(u8, tag, "scroll")) {
107 var o = opts(t, id);
108 o.expand = .both;
109 var s = dvui.scrollArea(@src(), .{}, o);
110 defer s.deinit();
111 children(t, id, @floatCast(t.getNum(id, "spacing")));
112 return;
113 }
114
115 if (std.mem.eql(u8, tag, "label") or
116 std.mem.eql(u8, tag, "title") or
117 std.mem.eql(u8, tag, "dim-label"))
118 {
119 var o = opts(t, id);
120 if (std.mem.eql(u8, tag, "title")) {
121 o.font = dvui.themeGet().font_title;
122 } else if (std.mem.eql(u8, tag, "dim-label")) {
123 o.color_text = .{ .color = dvui.themeGet().color(.window, .text).opacity(0.6) };
124 }
125 dvui.labelNoFmt(@src(), caption(t, id), .{ .align_x = o.gravity_x orelse 0 }, o);
126 return;
127 }
128
129 if (std.mem.eql(u8, tag, "separator")) {
130 var o = opts(t, id);
131 if (o.expand == null) o.expand = .horizontal;
132 o.min_size_content = .{ .w = 0, .h = 1 };
133 _ = dvui.separator(@src(), o);
134 return;
135 }
136
137 if (std.mem.eql(u8, tag, "spacer") or std.mem.eql(u8, tag, "gap")) {
138 var o = opts(t, id);
139 const size: f32 = @floatCast(t.getNum(id, "size"));
140 o.min_size_content = .{ .w = size, .h = size };
141 _ = dvui.spacer(@src(), o);
142 return;
143 }
144
145 if (std.mem.eql(u8, tag, "button")) {
146 var o = opts(t, id);
147 // `sensitive` is absent by default, and absent means enabled — an
148 // unset prop must not read as a dead button.
149 const enabled = t.get(id).?.props.contains("sensitive") == false or t.getBool(id, "sensitive");
150 if (!enabled) o.color_text = .{ .color = dvui.themeGet().color(.window, .text).opacity(0.4) };
151 const kind = t.getStr(id, "kind");
152 if (std.mem.eql(u8, kind, "primary")) {
153 o.color_fill = .{ .color = dvui.themeGet().color(.highlight, .fill) };
154 } else if (std.mem.eql(u8, kind, "destructive")) {
155 o.color_fill = .{ .color = dvui.themeGet().color(.err, .fill) };
156 }
157 if (dvui.button(@src(), caption(t, id), .{}, o) and enabled) {
158 t.emit(id, "click", "", 0);
159 }
160 return;
161 }
162
163 if (std.mem.eql(u8, tag, "checkbox") or std.mem.eql(u8, tag, "checkbutton")) {
164 // dvui writes through the target, so the prop is the store: read it in,
165 // let the widget edit it, write back what changed and say so.
166 var checked = t.getBool(id, "value") or t.getBool(id, "active");
167 const before = checked;
168 _ = dvui.checkbox(@src(), &checked, caption(t, id), opts(t, id));
169 if (checked != before) {
170 t.setBool(id, "value", checked);
171 t.setBool(id, "active", checked);
172 t.emit(id, "toggled", "", if (checked) 1 else 0);
173 }
174 return;
175 }
176
177 if (std.mem.eql(u8, tag, "entry")) {
178 entry(t, id);
179 return;
180 }
181
182 if (std.mem.eql(u8, tag, "progress")) {
183 var o = opts(t, id);
184 if (o.expand == null) o.expand = .horizontal;
185 o.min_size_content = .{ .w = 0, .h = 10 };
186 dvui.progress(@src(), .{ .percent = @floatCast(t.getNum(id, "value")) }, o);
187 return;
188 }
189
190 // An unknown tag is a container, not an error: a tree written against a
191 // richer painter should still show its contents here rather than vanish.
192 var b = dvui.box(@src(), .{ .dir = .vertical }, opts(t, id));
193 defer b.deinit();
194 children(t, id, @floatCast(t.getNum(id, "spacing")));
195}
196
197fn entry(t: *Tree, id: u32) void {
198 const n = t.get(id) orelse return;
199 if (n.entry == null) n.entry = .empty;
200 if (!n.entry_seeded) {
201 // The caller's `text` wins until the person typing changes it.
202 const want = t.getStr(id, "text");
203 n.entry.?.clearRetainingCapacity();
204 n.entry.?.appendSlice(t.gpa, want) catch {};
205 n.entry_seeded = true;
206 }
207
208 const placeholder = t.getStr(id, "placeholder");
209 var o = opts(t, id);
210 if (o.expand == null) o.expand = .horizontal;
211
212 var te = dvui.textEntry(@src(), .{
213 .text = .{ .array_list = .{ .backing = &n.entry.?, .allocator = t.gpa } },
214 .placeholder = if (placeholder.len != 0) placeholder else null,
215 .multiline = t.getBool(id, "multiline"),
216 }, o);
217 const changed = te.text_changed;
218 const entered = te.enter_pressed;
219 te.deinit();
220
221 // Read the buffer back only after deinit: the widget updates the list there.
222 const now = if (t.get(id)) |m| (if (m.entry) |buf| buf.items else "") else "";
223 if (changed) {
224 // Mirror into the prop so `zvui_node_get_str(node, "text")` answers what
225 // is on screen, then emit — without disturbing `entry_seeded`, which
226 // `setStr` would clear and so wipe the cursor position every keystroke.
227 const owned = t.gpa.dupe(u8, now) catch return;
228 defer t.gpa.free(owned);
229 t.setStr(id, "text", owned);
230 if (t.get(id)) |m| m.entry_seeded = true;
231 t.emit(id, "change", owned, 0);
232 }
233 if (entered) t.emit(id, "activate", now, 0);
234}
235
236fn children(t: *Tree, id: u32, spacing: f32) void {
237 const n = t.get(id) orelse return;
238 // Copy the child list before walking it: painting emits events, and a
239 // caller is free to restructure the tree between frames on the same thread.
240 const kids = t.gpa.dupe(u32, n.children.items) catch return;
241 defer t.gpa.free(kids);
242 for (kids, 0..) |c, i| {
243 if (spacing > 0 and i > 0) {
244 _ = dvui.spacer(@src(), .{
245 .id_extra = t.widgetId(c),
246 .min_size_content = .{ .w = spacing, .h = spacing },
247 });
248 }
249 walk(t, c);
250 }
251}