this repo has no description
1const std = @import("std");
2const vaxis = @import("../main.zig");
3
4const Allocator = std.mem.Allocator;
5
6const vxfw = @import("vxfw.zig");
7
8const FlexColumn = @This();
9
10children: []const vxfw.FlexItem,
11
12pub fn widget(self: *const FlexColumn) vxfw.Widget {
13 return .{
14 .userdata = @constCast(self),
15 .drawFn = typeErasedDrawFn,
16 };
17}
18
19fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
20 const self: *const FlexColumn = @ptrCast(@alignCast(ptr));
21 return self.draw(ctx);
22}
23
24pub fn draw(self: *const FlexColumn, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
25 std.debug.assert(ctx.max.height != null);
26 std.debug.assert(ctx.max.width != null);
27 if (self.children.len == 0) return vxfw.Surface.init(ctx.arena, self.widget(), ctx.min);
28
29 // Store the inherent size of each widget
30 const size_list = try ctx.arena.alloc(u16, self.children.len);
31
32 var layout_arena = std.heap.ArenaAllocator.init(ctx.arena);
33
34 const layout_ctx: vxfw.DrawContext = .{
35 .min = .{ .width = 0, .height = 0 },
36 .max = .{ .width = ctx.max.width, .height = null },
37 .arena = layout_arena.allocator(),
38 .cell_size = ctx.cell_size,
39 };
40
41 // Store the inherent size of each widget
42 var first_pass_height: u16 = 0;
43 var total_flex: u16 = 0;
44 for (self.children, 0..) |child, i| {
45 const surf = try child.widget.draw(layout_ctx);
46 first_pass_height += surf.size.height;
47 total_flex += child.flex;
48 size_list[i] = surf.size.height;
49 }
50
51 // We are done with the layout arena
52 layout_arena.deinit();
53
54 // make our children list
55 var children: std.ArrayList(vxfw.SubSurface) = .empty;
56
57 // Draw again, but with distributed heights
58 var second_pass_height: u16 = 0;
59 var max_width: u16 = 0;
60 const remaining_space = ctx.max.height.? - first_pass_height;
61 for (self.children, 1..) |child, i| {
62 const inherent_height = size_list[i - 1];
63 const child_height = if (child.flex == 0)
64 inherent_height
65 else if (i == self.children.len)
66 // If we are the last one, we just get the remainder
67 ctx.max.height.? - second_pass_height
68 else
69 inherent_height + (remaining_space * child.flex) / total_flex;
70
71 // Create a context for the child
72 const child_ctx = ctx.withConstraints(
73 .{ .width = 0, .height = child_height },
74 .{ .width = ctx.max.width.?, .height = child_height },
75 );
76 const surf = try child.widget.draw(child_ctx);
77
78 try children.append(ctx.arena, .{
79 .origin = .{ .col = 0, .row = second_pass_height },
80 .surface = surf,
81 .z_index = 0,
82 });
83 max_width = @max(max_width, surf.size.width);
84 second_pass_height += surf.size.height;
85 }
86
87 const size: vxfw.Size = .{ .width = max_width, .height = second_pass_height };
88 return .{
89 .size = size,
90 .widget = self.widget(),
91 .buffer = &.{},
92 .children = children.items,
93 };
94}
95
96test FlexColumn {
97 // Create child widgets
98 const Text = @import("Text.zig");
99 // Will be height=1, width=3
100 const abc: Text = .{ .text = "abc" };
101 const def: Text = .{ .text = "def" };
102 const ghi: Text = .{ .text = "ghi" };
103 const jklmno: Text = .{ .text = "jkl\nmno" };
104
105 // Create the flex column
106 const flex_column: FlexColumn = .{
107 .children = &.{
108 .{ .widget = abc.widget(), .flex = 0 }, // flex=0 means we are our inherent size
109 .{ .widget = def.widget(), .flex = 1 },
110 .{ .widget = ghi.widget(), .flex = 1 },
111 .{ .widget = jklmno.widget(), .flex = 1 },
112 },
113 };
114
115 // Boiler plate draw context
116 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
117 defer arena.deinit();
118 vxfw.DrawContext.init(.unicode);
119
120 const flex_widget = flex_column.widget();
121 const ctx: vxfw.DrawContext = .{
122 .arena = arena.allocator(),
123 .min = .{},
124 .max = .{ .width = 16, .height = 16 },
125 .cell_size = .{ .width = 10, .height = 20 },
126 };
127
128 const surface = try flex_widget.draw(ctx);
129 // FlexColumn expands to max height and widest child
130 try std.testing.expectEqual(16, surface.size.height);
131 try std.testing.expectEqual(3, surface.size.width);
132 // We have four children
133 try std.testing.expectEqual(4, surface.children.len);
134
135 // We will track the row we are on to confirm the origins
136 var row: u16 = 0;
137 // First child has flex=0, it should be it's inherent height
138 try std.testing.expectEqual(1, surface.children[0].surface.size.height);
139 try std.testing.expectEqual(row, surface.children[0].origin.row);
140 // Add the child height each time
141 row += surface.children[0].surface.size.height;
142 // Let's do some math
143 // - We have 4 children to fit into 16 rows. 3 children will be 1 row tall, one will be 2 rows
144 // tall for a total height of 5 rows.
145 // - The first child is 1 row and no flex. The rest of the height gets distributed evenly among
146 // the remaining 3 children. The remainder height is 16 - 5 = 11, so each child should get 11 /
147 // 3 = 3 extra rows, and the last will receive the remainder
148 try std.testing.expectEqual(1 + 3, surface.children[1].surface.size.height);
149 try std.testing.expectEqual(row, surface.children[1].origin.row);
150 row += surface.children[1].surface.size.height;
151
152 try std.testing.expectEqual(1 + 3, surface.children[2].surface.size.height);
153 try std.testing.expectEqual(row, surface.children[2].origin.row);
154 row += surface.children[2].surface.size.height;
155
156 try std.testing.expectEqual(2 + 3 + 2, surface.children[3].surface.size.height);
157 try std.testing.expectEqual(row, surface.children[3].origin.row);
158}
159
160test "refAllDecls" {
161 std.testing.refAllDecls(@This());
162}