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 }