lib/gui/src/surface/survey.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const command = @import("command.zig");
3 const capacity = @import("capacity.zig");
4 const decode = @import("decode.zig");
5 const gui = @import("../root.zig");
6 const types = @import("types.zig");
7
8 const UiNode = gui.model.UiNode;
9 const UiImageSet = gui.model.UiImageSet;
10
11 pub const max_depth = gui.layout.max_depth;
12
13 pub const Error = error{
14 SurfaceTooDeep,
15 SurfaceFactsOverflow,
16 InvalidCommand,
17 };
18
19 pub fn surface(src: types.Surface) Error!capacity.Facts {
20 var facts = capacity.Facts{};
21 try surfaceImages(src.images, &facts);
22 try surfaceNode(&src.tree.root, &facts, 0, src.images.images.len);
23 return facts;
24 }
25
26 pub fn commandSurface(src: *const command.CommandUiSurface) Error!capacity.Facts {
27 const root = src.root_ptr orelse return error.InvalidCommand;
28 var facts = capacity.Facts{};
29 try commandImages(src, &facts);
30 try commandNode(root, &facts, 0, src.images_len);
31 return facts;
32 }
33
34 fn surfaceImages(images: UiImageSet, facts: *capacity.Facts) Error!void {
35 try addImages(facts, images.images.len);
36 for (images.images) |image| {
37 image.validate() catch return error.InvalidCommand;
38 const pixels = std.math.mul(usize, image.width, image.height) catch
39 return error.SurfaceFactsOverflow;
40 try addImagePixels(facts, pixels);
41 }
42 }
43
44 fn commandImages(src: *const command.CommandUiSurface, facts: *capacity.Facts) Error!void {
45 if (src.images_len > 0 and src.images_ptr == null) return error.InvalidCommand;
46 try addImages(facts, src.images_len);
47 const images = if (src.images_len == 0) &.{} else src.images_ptr.?[0..src.images_len];
48 for (images) |image| {
49 if (image.width == 0 or image.height == 0) return error.InvalidCommand;
50 const pixels = std.math.mul(usize, image.width, image.height) catch
51 return error.SurfaceFactsOverflow;
52 if (image.pixels_len != pixels or image.pixels_ptr == null) return error.InvalidCommand;
53 try addImagePixels(facts, pixels);
54 }
55 }
56
57 fn surfaceNode(
58 src: *const UiNode,
59 facts: *capacity.Facts,
60 depth: usize,
61 image_count: usize,
62 ) Error!void {
63 if (depth >= max_depth) return error.SurfaceTooDeep;
64 try addNode(facts);
65 if (src.paint.image) |image| {
66 @branchHint(.unlikely);
67 if (image.index >= image_count) return error.InvalidCommand;
68 }
69 if (src.text) |text| {
70 gui.model.validateText(text) catch return error.InvalidCommand;
71 try addRuns(facts, text.runs.len);
72 try addStyles(facts, text.styles.len);
73 try addSemantic(facts, text.content.len);
74 }
75 try addSemantic(facts, src.action.len);
76 try addSemantic(facts, src.role.len);
77 for (src.children) |*child| {
78 try surfaceNode(child, facts, depth + 1, image_count);
79 }
80 }
81
82 fn commandNode(
83 src: *const command.CommandUiNode,
84 facts: *capacity.Facts,
85 depth: usize,
86 image_count: usize,
87 ) Error!void {
88 if (depth >= max_depth) return error.SurfaceTooDeep;
89 try addNode(facts);
90 _ = decode.widgetKind(src.kind) catch return error.InvalidCommand;
91 _ = decode.commandStyle(src.style) catch return error.InvalidCommand;
92 _ = decode.commandScroll(src.style) catch return error.InvalidCommand;
93 if (src.paint.image.is_set != 0) {
94 @branchHint(.unlikely);
95 if (src.paint.image.index >= image_count) return error.InvalidCommand;
96 }
97
98 var text_len: usize = 0;
99 if (src.text_ptr) |text| {
100 if (text.content_len > 0 and text.content_ptr == null) return error.InvalidCommand;
101 if (text.runs_len > 0 and text.runs_ptr == null) return error.InvalidCommand;
102 if (text.styles_len > 0 and text.styles_ptr == null) return error.InvalidCommand;
103 if (text.styles_len > std.math.maxInt(u16)) return error.InvalidCommand;
104 _ = decode.textAlign(text.horizontal_align) catch return error.InvalidCommand;
105 _ = decode.textAlign(text.vertical_align) catch return error.InvalidCommand;
106 text_len = text.content_len;
107 const content = if (text_len == 0) &.{} else text.content_ptr.?[0..text_len];
108 const styles = if (text.styles_len == 0) &.{} else text.styles_ptr.?[0..text.styles_len];
109 for (styles) |style| {
110 if (!gui.model.validTextPointSize(style.point_size)) return error.InvalidCommand;
111 }
112 const base_style = gui.model.UiTextStyle{
113 .font_asset_id = text.font_asset_id,
114 .point_size = gui.model.normalizedTextPointSize(text.point_size),
115 };
116 var validator = gui.model.TextRunValidator{};
117 const runs = if (text.runs_len == 0) &.{} else text.runs_ptr.?[0..text.runs_len];
118 for (runs) |run| {
119 validator.validate(content, run.byte_start, run.byte_end) catch return error.InvalidCommand;
120 const style_index: usize = run.style_slot;
121 if (style_index > styles.len) return error.InvalidCommand;
122 }
123 for (runs, 0..) |run, index| {
124 const selected = commandTextStyle(base_style, styles, run.style_slot);
125 const before = if (index > 0 and runs[index - 1].byte_end == run.byte_start)
126 commandTextStyle(base_style, styles, runs[index - 1].style_slot)
127 else
128 base_style;
129 const after = if (index + 1 < runs.len and runs[index + 1].byte_start == run.byte_end)
130 commandTextStyle(base_style, styles, runs[index + 1].style_slot)
131 else
132 base_style;
133 if (!gui.model.textStylesEqual(before, selected) and
134 !gui.model.textGraphemeBoundary(content, run.byte_start))
135 {
136 return error.InvalidCommand;
137 }
138 if (!gui.model.textStylesEqual(selected, after) and
139 !gui.model.textGraphemeBoundary(content, run.byte_end))
140 {
141 return error.InvalidCommand;
142 }
143 }
144 try addRuns(facts, text.runs_len);
145 try addStyles(facts, text.styles_len);
146 try addSemantic(facts, text_len);
147 }
148 _ = decode.commandTextSelection(src.text_selection, text_len) catch return error.InvalidCommand;
149
150 if (src.action_len > 0 and src.action_ptr == null) return error.InvalidCommand;
151 if (src.role_len > 0 and src.role_ptr == null) return error.InvalidCommand;
152 try addSemantic(facts, src.action_len);
153 try addSemantic(facts, src.role_len);
154
155 if (src.children_len == 0) return;
156 const children = src.children_ptr orelse return error.InvalidCommand;
157 for (children[0..src.children_len]) |*child| {
158 try commandNode(child, facts, depth + 1, image_count);
159 }
160 }
161
162 fn commandTextStyle(
163 base: gui.model.UiTextStyle,
164 styles: []const command.CommandUiTextStyle,
165 style_slot: u16,
166 ) gui.model.UiTextStyle {
167 if (style_slot == 0) return base;
168 return decode.commandTextStyle(styles[style_slot - 1]);
169 }
170
171 fn addNode(facts: *capacity.Facts) Error!void {
172 facts.nodes = std.math.add(usize, facts.nodes, 1) catch return error.SurfaceFactsOverflow;
173 }
174
175 fn addRuns(facts: *capacity.Facts, count: usize) Error!void {
176 facts.runs = std.math.add(usize, facts.runs, count) catch return error.SurfaceFactsOverflow;
177 }
178
179 fn addStyles(facts: *capacity.Facts, count: usize) Error!void {
180 facts.styles = std.math.add(usize, facts.styles, count) catch return error.SurfaceFactsOverflow;
181 }
182
183 fn addImages(facts: *capacity.Facts, count: usize) Error!void {
184 facts.images = std.math.add(usize, facts.images, count) catch
185 return error.SurfaceFactsOverflow;
186 }
187
188 fn addImagePixels(facts: *capacity.Facts, count: usize) Error!void {
189 facts.image_pixels = std.math.add(usize, facts.image_pixels, count) catch
190 return error.SurfaceFactsOverflow;
191 }
192
193 fn addSemantic(facts: *capacity.Facts, bytes: usize) Error!void {
194 facts.semantic_bytes = std.math.add(usize, facts.semantic_bytes, bytes) catch
195 return error.SurfaceFactsOverflow;
196 }
197
198 test "surface survey counts nodes and semantic bytes exactly" {
199 comptime {
200 @stardustClaim(
201 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "gui_surface_clone_survey"),
202 null,
203 null,
204 null,
205 null,
206 null,
207 null,
208 );
209 }
210
211 const styles = [_]gui.model.UiTextStyle{
212 .{ .font_asset_id = 9, .point_size = 18 },
213 };
214 const runs = [_]gui.model.UiTextRun{.{
215 .byte_start = 0,
216 .byte_end = 3,
217 .style_slot = 1,
218 .foreground = .{ .r = 210, .g = 40, .b = 30 },
219 }};
220 const children = [_]UiNode{
221 .{
222 .widget_id = 2,
223 .text = .{ .content = "Run", .runs = &runs, .styles = &styles },
224 .action = "activate",
225 .role = "button",
226 },
227 .{ .widget_id = 3, .role = "group" },
228 };
229 const tree = gui.model.UiSurfaceTree{
230 .available_size = .{ .width = 10, .height = 10 },
231 .root = .{ .widget_id = 1, .children = &children },
232 };
233 try std.testing.expectEqual(capacity.Facts{
234 .nodes = 3,
235 .runs = 1,
236 .styles = 1,
237 .semantic_bytes = "Run".len + "activate".len + "button".len + "group".len,
238 }, try surface(.{ .tree = &tree }));
239 }
240
241 test "surface survey counts image descriptors and exact pixels" {
242 const pixels = [_]u32{ 0xff00_00ff, 0xff00_ff00, 0xffff_0000, 0xffff_ffff };
243 const images = [_]gui.model.UiImage{.{
244 .width = 2,
245 .height = 2,
246 .pixels = &pixels,
247 }};
248 const tree = gui.model.UiSurfaceTree{
249 .available_size = .{ .width = 8, .height = 8 },
250 .root = .{
251 .widget_id = 1,
252 .paint = .{ .image = .{ .index = 0 } },
253 },
254 };
255 try std.testing.expectEqual(capacity.Facts{
256 .nodes = 1,
257 .images = 1,
258 .image_pixels = 4,
259 }, try surface(.{ .tree = &tree, .images = .{ .images = &images } }));
260 try std.testing.expectError(error.InvalidCommand, surface(.{ .tree = &tree }));
261
262 var invalid = tree;
263 invalid.root.paint.image.?.index = 1;
264 try std.testing.expectError(
265 error.InvalidCommand,
266 surface(.{ .tree = &invalid, .images = .{ .images = &images } }),
267 );
268 const invalid_images = [_]gui.model.UiImage{.{
269 .width = 0,
270 .height = 2,
271 .pixels = &pixels,
272 }};
273 try std.testing.expectError(
274 error.InvalidCommand,
275 surface(.{ .tree = &tree, .images = .{ .images = &invalid_images } }),
276 );
277 const short_images = [_]gui.model.UiImage{.{
278 .width = 2,
279 .height = 2,
280 .pixels = pixels[0..3],
281 }};
282 try std.testing.expectError(
283 error.InvalidCommand,
284 surface(.{ .tree = &tree, .images = .{ .images = &short_images } }),
285 );
286 }
287
288 test "surface command survey rejects malformed image tables and references" {
289 const pixels = [_]u32{ 0xff00_00ff, 0xff00_ff00, 0xffff_0000, 0xffff_ffff };
290 var image = command.CommandUiImage{
291 .width = 2,
292 .height = 2,
293 .pixels_ptr = &pixels,
294 .pixels_len = pixels.len,
295 };
296 var root = std.mem.zeroes(command.CommandUiNode);
297 root.widget_id = 1;
298 root.paint.image.is_set = 1;
299 var source = command.CommandUiSurface{
300 .available_width = 8,
301 .available_height = 8,
302 .root_ptr = &root,
303 .images_ptr = @ptrCast(&image),
304 .images_len = 1,
305 };
306 try std.testing.expectEqual(capacity.Facts{
307 .nodes = 1,
308 .images = 1,
309 .image_pixels = 4,
310 }, try commandSurface(&source));
311
312 source.images_ptr = null;
313 try std.testing.expectError(error.InvalidCommand, commandSurface(&source));
314 source.images_ptr = @ptrCast(&image);
315 image.pixels_ptr = null;
316 try std.testing.expectError(error.InvalidCommand, commandSurface(&source));
317 image.pixels_ptr = &pixels;
318 image.pixels_len = 3;
319 try std.testing.expectError(error.InvalidCommand, commandSurface(&source));
320 image.pixels_len = pixels.len;
321 image.width = 0;
322 try std.testing.expectError(error.InvalidCommand, commandSurface(&source));
323 image.width = 2;
324 root.paint.image.index = 1;
325 try std.testing.expectError(error.InvalidCommand, commandSurface(&source));
326
327 source.images_ptr = null;
328 source.images_len = 0;
329 root.paint.image.index = 0;
330 try std.testing.expectError(error.InvalidCommand, commandSurface(&source));
331
332 const maximum_extent = std.math.maxInt(u32);
333 const maximum_pixels = @as(usize, maximum_extent) * @as(usize, maximum_extent);
334 const fake_pixels: [*]const u32 = @ptrFromInt(@alignOf(u32));
335 const overflowing_images = [_]command.CommandUiImage{
336 .{
337 .width = maximum_extent,
338 .height = maximum_extent,
339 .pixels_ptr = fake_pixels,
340 .pixels_len = maximum_pixels,
341 },
342 .{
343 .width = maximum_extent,
344 .height = maximum_extent,
345 .pixels_ptr = fake_pixels,
346 .pixels_len = maximum_pixels,
347 },
348 };
349 source.images_ptr = &overflowing_images;
350 source.images_len = overflowing_images.len;
351 root.paint.image.is_set = 0;
352 try std.testing.expectError(error.SurfaceFactsOverflow, commandSurface(&source));
353 }
354
355 test "surface survey rejects trees beyond the shared layout depth" {
356 var nodes: [max_depth + 1]UiNode = undefined;
357 var index: usize = nodes.len;
358 while (index > 0) {
359 index -= 1;
360 nodes[index] = .{
361 .widget_id = index,
362 .children = if (index + 1 < nodes.len) nodes[index + 1 ..][0..1] else &.{},
363 };
364 }
365 const tree = gui.model.UiSurfaceTree{
366 .available_size = .{ .width = 1, .height = 1 },
367 .root = nodes[0],
368 };
369 try std.testing.expectError(error.SurfaceTooDeep, surface(.{ .tree = &tree }));
370 }