lib/gui/src/paint/command.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const gui = @import("../root.zig");
4
5 const Allocator = std.mem.Allocator;
6 const Color = gui.model.UiColor;
7 const Gradient = gui.model.UiLinearGradient;
8 pub const Image = gui.model.UiImage;
9 pub const ImageSet = gui.model.UiImageSet;
10 const Point = gui.model.UiPoint;
11 const Rect = gui.layout.Rect;
12
13 pub const Kind = enum(u32) {
14 fill = 1,
15 stroke = 2,
16 image = 3,
17 shadow = 4,
18 glyph = 5,
19 linear_gradient = 6,
20 };
21
22 pub const Options = struct {
23 visual: gui.visual.Options = .{},
24 scale: f32 = 1,
25 };
26
27 pub const fragment_namespace_widget: u32 = 1;
28 pub const fragment_namespace_layer: u32 = 2;
29 pub const fragment_part_chrome: u32 = 1;
30 pub const fragment_part_text: u32 = 2;
31
32 pub const FragmentId = struct {
33 root_id: u64,
34 element_id: u64,
35 namespace: u32,
36 part: u32,
37 };
38
39 pub const Fragment = struct {
40 id: FragmentId,
41 command_start: u32,
42 command_count: u32,
43 bounds: Rect,
44 };
45
46 pub const Command = struct {
47 kind: Kind,
48 rect: Rect,
49 clip: Rect,
50 source: Rect = .{},
51 color: Color,
52 color_end: Color = .{},
53 gradient_start: Point = .{},
54 gradient_end: Point = .{},
55 radius: f32 = 0,
56 width: f32 = 0,
57 image_index: u32 = 0,
58 order: u32 = 0,
59 };
60
61 pub const CommandBuffer = struct {
62 pub const Error = Allocator.Error || error{
63 InvalidFragmentStart,
64 TooManyCommands,
65 };
66
67 allocator: Allocator,
68 commands: std.ArrayListUnmanaged(Command) = .empty,
69 fragments: std.ArrayListUnmanaged(Fragment) = .empty,
70
71 pub fn init(allocator: Allocator) CommandBuffer {
72 return .{ .allocator = allocator };
73 }
74
75 pub fn deinit(self: *CommandBuffer) void {
76 self.commands.deinit(self.allocator);
77 self.fragments.deinit(self.allocator);
78 self.* = undefined;
79 }
80
81 pub fn reset(self: *CommandBuffer) void {
82 self.commands.clearRetainingCapacity();
83 self.fragments.clearRetainingCapacity();
84 }
85
86 pub fn ensureCapacity(self: *CommandBuffer, capacity: usize) Allocator.Error!void {
87 try self.commands.ensureTotalCapacity(self.allocator, capacity);
88 }
89
90 pub fn ensureFragmentCapacity(
91 self: *CommandBuffer,
92 capacity: usize,
93 ) Allocator.Error!void {
94 try self.fragments.ensureTotalCapacity(self.allocator, capacity);
95 }
96
97 pub fn items(self: *const CommandBuffer) []const Command {
98 return self.commands.items;
99 }
100
101 pub fn fragmentItems(self: *const CommandBuffer) []const Fragment {
102 return self.fragments.items;
103 }
104
105 pub fn fragmentsComplete(self: *const CommandBuffer) bool {
106 return self.coveredCommandCount() == self.commands.items.len;
107 }
108
109 pub fn commitFragment(
110 self: *CommandBuffer,
111 id: FragmentId,
112 command_start: usize,
113 ) Error!void {
114 if (command_start != self.coveredCommandCount()) {
115 return error.InvalidFragmentStart;
116 }
117 if (command_start > self.commands.items.len) {
118 return error.InvalidFragmentStart;
119 }
120 const command_count = self.commands.items.len - command_start;
121 if (command_count == 0) return;
122 const start = std.math.cast(u32, command_start) orelse
123 return error.TooManyCommands;
124 const count = std.math.cast(u32, command_count) orelse
125 return error.TooManyCommands;
126 try self.fragments.append(self.allocator, .{
127 .id = id,
128 .command_start = start,
129 .command_count = count,
130 .bounds = fragmentBounds(self.commands.items[command_start..]),
131 });
132 }
133
134 pub fn appendFrame(
135 self: *CommandBuffer,
136 frame: gui.model.UiFrame,
137 root_id: u64,
138 target_width: u32,
139 target_height: u32,
140 options: Options,
141 ) Error!void {
142 const command_start = self.commands.items.len;
143 const fragment_start = self.fragments.items.len;
144 errdefer {
145 self.commands.shrinkRetainingCapacity(command_start);
146 self.fragments.shrinkRetainingCapacity(fragment_start);
147 }
148 var recorder = Recorder.init(self, .{
149 .x = 0,
150 .y = 0,
151 .width = @floatFromInt(target_width),
152 .height = @floatFromInt(target_height),
153 }, options.scale);
154 gui.visual.drawFrame(&recorder, frame, root_id, options.visual);
155 std.debug.assert(recorder.fragment_start == null);
156 if (recorder.failure) |failure| return failure;
157 }
158
159 pub fn append(self: *CommandBuffer, raw: Command) Allocator.Error!void {
160 if (raw.kind == .linear_gradient) {
161 if (raw.color.a == 0 and raw.color_end.a == 0) return;
162 } else if (raw.color.a == 0) return;
163 if (empty(raw.rect) or empty(raw.clip)) return;
164 var paint = raw;
165 paint.order = @intCast(self.commands.items.len);
166 try self.commands.append(self.allocator, paint);
167 }
168
169 pub fn rollback(self: *CommandBuffer, len: usize) void {
170 std.debug.assert(self.coveredCommandCount() <= len);
171 self.commands.shrinkRetainingCapacity(len);
172 }
173
174 fn coveredCommandCount(self: *const CommandBuffer) usize {
175 const last = if (self.fragments.items.len == 0)
176 return 0
177 else
178 self.fragments.items[self.fragments.items.len - 1];
179 return @as(usize, last.command_start) + last.command_count;
180 }
181 };
182
183 pub fn assignOrders(commands: []Command) void {
184 for (commands, 0..) |*paint, index| {
185 paint.order = @intCast(index);
186 }
187 }
188
189 pub fn bounds(paint: Command) Rect {
190 return switch (paint.kind) {
191 .shadow => inset(paint.rect, -@max(paint.width, 0)),
192 else => paint.rect,
193 };
194 }
195
196 pub fn visibleBounds(paint: Command) Rect {
197 return intersect(bounds(paint), paint.clip);
198 }
199
200 pub fn visuallyEqual(left: Command, right: Command) bool {
201 return left.kind == right.kind and
202 std.meta.eql(left.rect, right.rect) and
203 std.meta.eql(left.clip, right.clip) and
204 std.meta.eql(left.source, right.source) and
205 std.meta.eql(left.color, right.color) and
206 std.meta.eql(left.color_end, right.color_end) and
207 std.meta.eql(left.gradient_start, right.gradient_start) and
208 std.meta.eql(left.gradient_end, right.gradient_end) and
209 left.radius == right.radius and
210 left.width == right.width and
211 left.image_index == right.image_index;
212 }
213
214 const Recorder = struct {
215 buffer: *CommandBuffer,
216 clips: [2]Rect,
217 clip_depth: u8 = 1,
218 scale: f32 = 1,
219 failure: ?CommandBuffer.Error = null,
220 fragment_start: ?usize = null,
221 fragment_id: FragmentId = undefined,
222
223 fn init(buffer: *CommandBuffer, target: Rect, scale: f32) Recorder {
224 return .{
225 .buffer = buffer,
226 .clips = .{ target, undefined },
227 .scale = scale,
228 };
229 }
230
231 fn scaledRect(self: *const Recorder, rect: Rect) Rect {
232 return .{
233 .x = rect.x * self.scale,
234 .y = rect.y * self.scale,
235 .width = rect.width * self.scale,
236 .height = rect.height * self.scale,
237 };
238 }
239
240 fn scaledGradientPoint(self: *const Recorder, rect: Rect, point: Point, fallback: Point) Point {
241 const x_unit = finiteOr(point.x, fallback.x);
242 const y_unit = finiteOr(point.y, fallback.y);
243 const fallback_x = rect.x + rect.width * fallback.x;
244 const fallback_y = rect.y + rect.height * fallback.y;
245 return .{
246 .x = finiteOr(rect.x + rect.width * x_unit, fallback_x) * self.scale,
247 .y = finiteOr(rect.y + rect.height * y_unit, fallback_y) * self.scale,
248 };
249 }
250
251 pub fn beginClip(self: *Recorder, rect: Rect) void {
252 std.debug.assert(self.clip_depth < self.clips.len);
253 self.clips[self.clip_depth] = intersect(self.activeClip(), self.scaledRect(rect));
254 self.clip_depth += 1;
255 }
256
257 pub fn endClip(self: *Recorder) void {
258 if (self.clip_depth > 1) self.clip_depth -= 1;
259 }
260
261 pub fn beginWidgetFragment(
262 self: *Recorder,
263 root_id: u64,
264 widget_id: u64,
265 ) void {
266 if (self.failure != null) return;
267 std.debug.assert(self.fragment_start == null);
268 self.fragment_start = self.buffer.commands.items.len;
269 self.fragment_id = .{
270 .root_id = root_id,
271 .element_id = widget_id,
272 .namespace = fragment_namespace_widget,
273 .part = fragment_part_chrome,
274 };
275 }
276
277 pub fn endFragment(self: *Recorder) void {
278 if (self.failure != null) {
279 self.fragment_start = null;
280 return;
281 }
282 const start = self.fragment_start orelse unreachable;
283 self.fragment_start = null;
284 self.buffer.commitFragment(self.fragment_id, start) catch |failure| {
285 self.failure = failure;
286 };
287 }
288
289 pub fn fillRect(self: *Recorder, rect: Rect, color: Color) void {
290 self.append(.{
291 .kind = .fill,
292 .rect = self.scaledRect(rect),
293 .clip = self.activeClip(),
294 .color = color,
295 });
296 }
297
298 pub fn fillRoundedRect(self: *Recorder, rect: Rect, radius: f32, segments: u32, color: Color) void {
299 _ = segments;
300 self.append(.{
301 .kind = .fill,
302 .rect = self.scaledRect(rect),
303 .clip = self.activeClip(),
304 .color = color,
305 .radius = radius * self.scale,
306 });
307 }
308
309 pub fn fillLinearGradient(self: *Recorder, rect: Rect, radius: f32, segments: u32, gradient: Gradient) void {
310 _ = segments;
311 self.append(.{
312 .kind = .linear_gradient,
313 .rect = self.scaledRect(rect),
314 .clip = self.activeClip(),
315 .color = gradient.start_color,
316 .color_end = gradient.end_color,
317 .gradient_start = self.scaledGradientPoint(rect, gradient.start, .{}),
318 .gradient_end = self.scaledGradientPoint(rect, gradient.end, .{ .x = 1 }),
319 .radius = @max(radius, 0) * self.scale,
320 });
321 }
322
323 pub fn strokeRect(self: *Recorder, rect: Rect, width: f32, color: Color) void {
324 self.append(.{
325 .kind = .stroke,
326 .rect = self.scaledRect(rect),
327 .clip = self.activeClip(),
328 .color = color,
329 .width = width * self.scale,
330 });
331 }
332
333 pub fn strokeRoundedRect(self: *Recorder, rect: Rect, radius: f32, segments: u32, width: f32, color: Color) void {
334 _ = segments;
335 self.append(.{
336 .kind = .stroke,
337 .rect = self.scaledRect(rect),
338 .clip = self.activeClip(),
339 .color = color,
340 .radius = radius * self.scale,
341 .width = width * self.scale,
342 });
343 }
344
345 pub fn drawImage(self: *Recorder, rect: Rect, image_index: u32, source: Rect, opacity: u8) void {
346 self.append(.{
347 .kind = .image,
348 .rect = self.scaledRect(rect),
349 .clip = self.activeClip(),
350 .source = source,
351 .color = .{ .r = 255, .g = 255, .b = 255, .a = opacity },
352 .image_index = image_index,
353 });
354 }
355
356 pub fn drawGlyph(self: *Recorder, rect: Rect, image_index: u32, source: Rect, color: Color) void {
357 self.append(.{
358 .kind = .glyph,
359 .rect = self.scaledRect(rect),
360 .clip = self.activeClip(),
361 .source = source,
362 .color = color,
363 .image_index = image_index,
364 });
365 }
366
367 pub fn shadowRoundedRect(self: *Recorder, rect: Rect, radius: f32, blur_radius: f32, spread: f32, offset_x: f32, offset_y: f32, color: Color) void {
368 const expanded = @max(spread, 0);
369 self.append(.{
370 .kind = .shadow,
371 .rect = self.scaledRect(.{
372 .x = rect.x + offset_x - expanded,
373 .y = rect.y + offset_y - expanded,
374 .width = rect.width + expanded * 2,
375 .height = rect.height + expanded * 2,
376 }),
377 .clip = self.activeClip(),
378 .color = color,
379 .radius = @max(radius + expanded, 0) * self.scale,
380 .width = @max(blur_radius, 0) * self.scale,
381 });
382 }
383
384 fn append(self: *Recorder, raw: Command) void {
385 if (self.failure != null) return;
386 self.buffer.append(raw) catch |failure| {
387 self.failure = failure;
388 };
389 }
390
391 fn activeClip(self: *const Recorder) Rect {
392 return self.clips[self.clip_depth - 1];
393 }
394 };
395
396 fn finiteOr(value: f32, fallback: f32) f32 {
397 return if (std.math.isFinite(value)) value else fallback;
398 }
399
400 fn fragmentBounds(commands: []const Command) Rect {
401 std.debug.assert(commands.len != 0);
402 var result = visibleBounds(commands[0]);
403 for (commands[1..]) |paint| {
404 result = unionRect(result, visibleBounds(paint));
405 }
406 return result;
407 }
408
409 pub fn intersect(left: Rect, right: Rect) Rect {
410 const x0 = @max(left.x, right.x);
411 const y0 = @max(left.y, right.y);
412 const x1 = @min(left.x + left.width, right.x + right.width);
413 const y1 = @min(left.y + left.height, right.y + right.height);
414 return .{
415 .x = x0,
416 .y = y0,
417 .width = @max(x1 - x0, 0),
418 .height = @max(y1 - y0, 0),
419 };
420 }
421
422 fn unionRect(left: Rect, right: Rect) Rect {
423 if (empty(left)) return right;
424 if (empty(right)) return left;
425 const x0 = @min(left.x, right.x);
426 const y0 = @min(left.y, right.y);
427 const x1 = @max(left.x + left.width, right.x + right.width);
428 const y1 = @max(left.y + left.height, right.y + right.height);
429 return .{
430 .x = x0,
431 .y = y0,
432 .width = x1 - x0,
433 .height = y1 - y0,
434 };
435 }
436
437 pub fn empty(rect: Rect) bool {
438 return rect.width <= 0 or rect.height <= 0;
439 }
440
441 fn inset(rect: Rect, value: f32) Rect {
442 return .{
443 .x = rect.x + value,
444 .y = rect.y + value,
445 .width = @max(rect.width - value * 2, 0),
446 .height = @max(rect.height - value * 2, 0),
447 };
448 }
449
450 test "CommandBuffer captures visual widget chrome commands" {
451 const allocator = std.testing.allocator;
452 const child = [_]gui.model.UiNode{.{
453 .widget_id = 2,
454 .kind = .button,
455 .paint = .{
456 .background = .{ .r = 200, .g = 10, .b = 20, .a = 255 },
457 .border = .{ .r = 0, .g = 0, .b = 0, .a = 255 },
458 .border_width = 1,
459 .shadow = .{
460 .color = .{ .r = 0, .g = 0, .b = 0, .a = 80 },
461 .offset_y = 1,
462 .blur_radius = 2,
463 },
464 },
465 .size = .{ .width = 12, .height = 8 },
466 }};
467 const surface = gui.model.UiSurfaceTree{
468 .available_size = .{ .width = 32, .height = 16 },
469 .root = .{
470 .widget_id = 1,
471 .children = child[0..],
472 },
473 };
474 var frame_workspace = gui.frame.Workspace.init(allocator);
475 defer frame_workspace.deinit();
476 const frame = try frame_workspace.buildSurface(&surface, .{});
477
478 var commands = CommandBuffer.init(allocator);
479 defer commands.deinit();
480 try commands.appendFrame(frame, 1, 32, 16, .{});
481
482 try std.testing.expect(commands.items().len >= 3);
483 try std.testing.expectEqual(Kind.shadow, commands.items()[0].kind);
484 try std.testing.expectEqual(Kind.fill, commands.items()[1].kind);
485 try std.testing.expectEqual(@as(u32, 0), commands.items()[0].order);
486 try std.testing.expectEqual(@as(u32, 1), commands.items()[1].order);
487 try std.testing.expect(commands.fragmentsComplete());
488 try std.testing.expectEqual(@as(usize, 1), commands.fragmentItems().len);
489 const fragment = commands.fragmentItems()[0];
490 try std.testing.expectEqual(FragmentId{
491 .root_id = 1,
492 .element_id = 2,
493 .namespace = fragment_namespace_widget,
494 .part = fragment_part_chrome,
495 }, fragment.id);
496 try std.testing.expectEqual(
497 Rect{ .width = 14, .height = 11 },
498 fragment.bounds,
499 );
500 }
501
502 test "CommandBuffer scales chrome geometry to device pixels" {
503 const allocator = std.testing.allocator;
504 const child = [_]gui.model.UiNode{.{
505 .widget_id = 2,
506 .kind = .button,
507 .paint = .{
508 .background = .{ .r = 200, .g = 10, .b = 20, .a = 255 },
509 .border = .{ .r = 0, .g = 0, .b = 0, .a = 255 },
510 .border_width = 1,
511 .corner_roundness = 3,
512 },
513 .size = .{ .width = 12, .height = 8 },
514 }};
515 const surface = gui.model.UiSurfaceTree{
516 .available_size = .{ .width = 32, .height = 16 },
517 .root = .{
518 .widget_id = 1,
519 .children = child[0..],
520 },
521 };
522 var frame_workspace = gui.frame.Workspace.init(allocator);
523 defer frame_workspace.deinit();
524 const frame = try frame_workspace.buildSurface(&surface, .{});
525
526 var logical = CommandBuffer.init(allocator);
527 defer logical.deinit();
528 try logical.appendFrame(frame, 1, 32, 16, .{});
529 var device = CommandBuffer.init(allocator);
530 defer device.deinit();
531 try device.appendFrame(frame, 1, 64, 32, .{ .scale = 2 });
532
533 try std.testing.expectEqual(logical.items().len, device.items().len);
534 for (logical.items(), device.items()) |one, two| {
535 try std.testing.expectEqual(one.kind, two.kind);
536 try std.testing.expectEqual(one.rect.x * 2, two.rect.x);
537 try std.testing.expectEqual(one.rect.y * 2, two.rect.y);
538 try std.testing.expectEqual(one.rect.width * 2, two.rect.width);
539 try std.testing.expectEqual(one.rect.height * 2, two.rect.height);
540 try std.testing.expectEqual(one.radius * 2, two.radius);
541 try std.testing.expectEqual(one.width * 2, two.width);
542 try std.testing.expectEqual(one.source.x, two.source.x);
543 try std.testing.expectEqual(one.source.width, two.source.width);
544 }
545 }
546
547 test "CommandBuffer records normalized linear gradient endpoints in device pixels" {
548 const gradient = gui.model.UiLinearGradient{
549 .start = .{ .x = 0.25, .y = 0.5 },
550 .end = .{ .x = 0.75, .y = 1 },
551 .start_color = .{ .r = 12, .g = 24, .b = 48, .a = 220 },
552 .end_color = .{ .r = 80, .g = 120, .b = 220, .a = 160 },
553 };
554 const child = [_]gui.model.UiNode{.{
555 .widget_id = 2,
556 .kind = .container,
557 .paint = .{
558 .linear_gradient = gradient,
559 .corner_roundness = 3,
560 },
561 .size = .{ .width = 12, .height = 8 },
562 }};
563 const surface = gui.model.UiSurfaceTree{
564 .available_size = .{ .width = 32, .height = 16 },
565 .root = .{ .widget_id = 1, .children = &child },
566 };
567 var frame_workspace = gui.frame.Workspace.init(std.testing.allocator);
568 defer frame_workspace.deinit();
569 const frame = try frame_workspace.buildSurface(&surface, .{});
570 var logical = CommandBuffer.init(std.testing.allocator);
571 defer logical.deinit();
572 try logical.appendFrame(frame, 1, 32, 16, .{});
573 var device = CommandBuffer.init(std.testing.allocator);
574 defer device.deinit();
575 try device.appendFrame(frame, 1, 64, 32, .{ .scale = 2 });
576
577 try std.testing.expectEqual(@as(usize, 1), logical.items().len);
578 const one = logical.items()[0];
579 const two = device.items()[0];
580 try std.testing.expectEqual(Kind.linear_gradient, one.kind);
581 try std.testing.expectEqual(gradient.start_color, one.color);
582 try std.testing.expectEqual(gradient.end_color, one.color_end);
583 try std.testing.expectEqual(Point{ .x = 3, .y = 4 }, one.gradient_start);
584 try std.testing.expectEqual(Point{ .x = 9, .y = 8 }, one.gradient_end);
585 try std.testing.expectEqual(Point{ .x = 6, .y = 8 }, two.gradient_start);
586 try std.testing.expectEqual(Point{ .x = 18, .y = 16 }, two.gradient_end);
587 try std.testing.expectEqual(@as(f32, 6), two.radius);
588 }
589
590 test "CommandBuffer keeps a linear gradient with transparent start color" {
591 var commands = CommandBuffer.init(std.testing.allocator);
592 defer commands.deinit();
593 try commands.append(.{
594 .kind = .linear_gradient,
595 .rect = .{ .width = 8, .height = 8 },
596 .clip = .{ .width = 8, .height = 8 },
597 .color = .{ .a = 0 },
598 .color_end = .{ .r = 40, .g = 80, .b = 120, .a = 255 },
599 .gradient_end = .{ .x = 8 },
600 });
601 try std.testing.expectEqual(@as(usize, 1), commands.items().len);
602 }
603
604 test "assignOrders preserves painter order" {
605 var commands = [_]Command{
606 .{
607 .kind = .fill,
608 .rect = .{ .x = 0, .y = 0, .width = 8, .height = 8 },
609 .clip = .{ .x = 0, .y = 0, .width = 20, .height = 20 },
610 .color = .{ .a = 255 },
611 },
612 .{
613 .kind = .fill,
614 .rect = .{ .x = 10, .y = 0, .width = 4, .height = 4 },
615 .clip = .{ .x = 0, .y = 0, .width = 20, .height = 20 },
616 .color = .{ .a = 255 },
617 },
618 .{
619 .kind = .fill,
620 .rect = .{ .x = 1, .y = 1, .width = 3, .height = 3 },
621 .clip = .{ .x = 0, .y = 0, .width = 20, .height = 20 },
622 .color = .{ .a = 255 },
623 },
624 .{
625 .kind = .shadow,
626 .rect = .{ .x = 12, .y = 1, .width = 2, .height = 2 },
627 .clip = .{ .x = 0, .y = 0, .width = 20, .height = 20 },
628 .color = .{ .a = 255 },
629 .width = 2,
630 },
631 };
632
633 assignOrders(commands[0..]);
634
635 try std.testing.expectEqual(@as(u32, 0), commands[0].order);
636 try std.testing.expectEqual(@as(u32, 1), commands[1].order);
637 try std.testing.expectEqual(@as(u32, 2), commands[2].order);
638 try std.testing.expectEqual(@as(u32, 3), commands[3].order);
639 }
640
641 test "CommandBuffer warmed frames need no backing allocation" {
642 const child = [_]gui.model.UiNode{.{
643 .widget_id = 2,
644 .size = .{ .width = 8, .height = 8 },
645 .paint = .{ .linear_gradient = .{
646 .start = .{ .x = 0.125, .y = 0.25 },
647 .end = .{ .x = 0.875, .y = 0.75 },
648 .start_color = .{ .r = 20, .g = 30, .b = 40, .a = 255 },
649 .end_color = .{ .r = 70, .g = 90, .b = 120, .a = 255 },
650 } },
651 }};
652 const surface = gui.model.UiSurfaceTree{
653 .available_size = .{ .width = 16, .height = 16 },
654 .root = .{ .widget_id = 1, .children = child[0..] },
655 };
656 var frame_workspace = gui.frame.Workspace.init(std.testing.allocator);
657 defer frame_workspace.deinit();
658 const frame = try frame_workspace.buildSurface(&surface, .{});
659 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
660 var commands = CommandBuffer.init(failing.allocator());
661 defer commands.deinit();
662
663 try commands.appendFrame(frame, 1, 16, 16, .{});
664 const storage = commands.items().ptr;
665 const fragment_storage = commands.fragmentItems().ptr;
666 failing.fail_index = failing.alloc_index;
667 failing.resize_fail_index = failing.resize_index;
668 for (0..8) |_| {
669 commands.reset();
670 try commands.appendFrame(frame, 1, 16, 16, .{});
671 try std.testing.expectEqual(@as(usize, 1), commands.items().len);
672 try std.testing.expectEqual(@as(usize, 1), commands.fragmentItems().len);
673 try std.testing.expectEqual(storage, commands.items().ptr);
674 try std.testing.expectEqual(fragment_storage, commands.fragmentItems().ptr);
675 }
676 try std.testing.expect(!failing.has_induced_failure);
677 }
678
679 test "CommandBuffer reports allocation failure and rolls back" {
680 const child = [_]gui.model.UiNode{.{
681 .widget_id = 2,
682 .size = .{ .width = 8, .height = 8 },
683 .paint = .{ .background = .{ .r = 20, .g = 30, .b = 40, .a = 255 } },
684 }};
685 const surface = gui.model.UiSurfaceTree{
686 .available_size = .{ .width = 16, .height = 16 },
687 .root = .{ .widget_id = 1, .children = child[0..] },
688 };
689 var frame_workspace = gui.frame.Workspace.init(std.testing.allocator);
690 defer frame_workspace.deinit();
691 const frame = try frame_workspace.buildSurface(&surface, .{});
692 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
693 var commands = CommandBuffer.init(failing.allocator());
694 defer commands.deinit();
695
696 try std.testing.expectError(error.OutOfMemory, commands.appendFrame(frame, 1, 16, 16, .{}));
697 try std.testing.expectEqual(@as(usize, 0), commands.items().len);
698 try std.testing.expectEqual(@as(usize, 0), commands.fragmentItems().len);
699 failing.fail_index = std.math.maxInt(usize);
700 try commands.appendFrame(frame, 1, 16, 16, .{});
701 try std.testing.expectEqual(@as(usize, 1), commands.items().len);
702 try std.testing.expectEqual(@as(usize, 1), commands.fragmentItems().len);
703 }