const std = @import("std"); const vaxis = @import("../main.zig"); const Allocator = std.mem.Allocator; const vxfw = @import("vxfw.zig"); const FlexColumn = @This(); children: []const vxfw.FlexItem, pub fn widget(self: *const FlexColumn) vxfw.Widget { return .{ .userdata = @constCast(self), .drawFn = typeErasedDrawFn, }; } fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { const self: *const FlexColumn = @ptrCast(@alignCast(ptr)); return self.draw(ctx); } pub fn draw(self: *const FlexColumn, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { std.debug.assert(ctx.max.height != null); std.debug.assert(ctx.max.width != null); if (self.children.len == 0) return vxfw.Surface.init(ctx.arena, self.widget(), ctx.min); // Store the inherent size of each widget const size_list = try ctx.arena.alloc(u16, self.children.len); var layout_arena = std.heap.ArenaAllocator.init(ctx.arena); const layout_ctx: vxfw.DrawContext = .{ .min = .{ .width = 0, .height = 0 }, .max = .{ .width = ctx.max.width, .height = null }, .arena = layout_arena.allocator(), .cell_size = ctx.cell_size, }; // Store the inherent size of each widget var first_pass_height: u16 = 0; var total_flex: u16 = 0; for (self.children, 0..) |child, i| { const surf = try child.widget.draw(layout_ctx); first_pass_height += surf.size.height; total_flex += child.flex; size_list[i] = surf.size.height; } // We are done with the layout arena layout_arena.deinit(); // make our children list var children: std.ArrayList(vxfw.SubSurface) = .empty; // Draw again, but with distributed heights var second_pass_height: u16 = 0; var max_width: u16 = 0; const remaining_space = ctx.max.height.? - first_pass_height; for (self.children, 1..) |child, i| { const inherent_height = size_list[i - 1]; const child_height = if (child.flex == 0) inherent_height else if (i == self.children.len) // If we are the last one, we just get the remainder ctx.max.height.? - second_pass_height else inherent_height + (remaining_space * child.flex) / total_flex; // Create a context for the child const child_ctx = ctx.withConstraints( .{ .width = 0, .height = child_height }, .{ .width = ctx.max.width.?, .height = child_height }, ); const surf = try child.widget.draw(child_ctx); try children.append(ctx.arena, .{ .origin = .{ .col = 0, .row = second_pass_height }, .surface = surf, .z_index = 0, }); max_width = @max(max_width, surf.size.width); second_pass_height += surf.size.height; } const size: vxfw.Size = .{ .width = max_width, .height = second_pass_height }; return .{ .size = size, .widget = self.widget(), .buffer = &.{}, .children = children.items, }; } test FlexColumn { // Create child widgets const Text = @import("Text.zig"); // Will be height=1, width=3 const abc: Text = .{ .text = "abc" }; const def: Text = .{ .text = "def" }; const ghi: Text = .{ .text = "ghi" }; const jklmno: Text = .{ .text = "jkl\nmno" }; // Create the flex column const flex_column: FlexColumn = .{ .children = &.{ .{ .widget = abc.widget(), .flex = 0 }, // flex=0 means we are our inherent size .{ .widget = def.widget(), .flex = 1 }, .{ .widget = ghi.widget(), .flex = 1 }, .{ .widget = jklmno.widget(), .flex = 1 }, }, }; // Boiler plate draw context var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); vxfw.DrawContext.init(.unicode); const flex_widget = flex_column.widget(); const ctx: vxfw.DrawContext = .{ .arena = arena.allocator(), .min = .{}, .max = .{ .width = 16, .height = 16 }, .cell_size = .{ .width = 10, .height = 20 }, }; const surface = try flex_widget.draw(ctx); // FlexColumn expands to max height and widest child try std.testing.expectEqual(16, surface.size.height); try std.testing.expectEqual(3, surface.size.width); // We have four children try std.testing.expectEqual(4, surface.children.len); // We will track the row we are on to confirm the origins var row: u16 = 0; // First child has flex=0, it should be it's inherent height try std.testing.expectEqual(1, surface.children[0].surface.size.height); try std.testing.expectEqual(row, surface.children[0].origin.row); // Add the child height each time row += surface.children[0].surface.size.height; // Let's do some math // - We have 4 children to fit into 16 rows. 3 children will be 1 row tall, one will be 2 rows // tall for a total height of 5 rows. // - The first child is 1 row and no flex. The rest of the height gets distributed evenly among // the remaining 3 children. The remainder height is 16 - 5 = 11, so each child should get 11 / // 3 = 3 extra rows, and the last will receive the remainder try std.testing.expectEqual(1 + 3, surface.children[1].surface.size.height); try std.testing.expectEqual(row, surface.children[1].origin.row); row += surface.children[1].surface.size.height; try std.testing.expectEqual(1 + 3, surface.children[2].surface.size.height); try std.testing.expectEqual(row, surface.children[2].origin.row); row += surface.children[2].surface.size.height; try std.testing.expectEqual(2 + 3 + 2, surface.children[3].surface.size.height); try std.testing.expectEqual(row, surface.children[3].origin.row); } test "refAllDecls" { std.testing.refAllDecls(@This()); }