vxfw: add FlexRow widget

This commit is contained in:
Tim Culverhouse 2024-10-30 15:14:54 -05:00
parent 6941cd831c
commit 147052b5f2
2 changed files with 160 additions and 0 deletions

159
src/vxfw/FlexRow.zig Normal file
View file

@ -0,0 +1,159 @@
const std = @import("std");
const vaxis = @import("../main.zig");
const vxfw = @import("vxfw.zig");
const Allocator = std.mem.Allocator;
const FlexRow = @This();
children: []const vxfw.FlexItem,
pub fn widget(self: *const FlexRow) vxfw.Widget {
return .{
.userdata = @constCast(self),
.eventHandler = vxfw.noopEventHandler,
.drawFn = typeErasedDrawFn,
};
}
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
const self: *const FlexRow = @ptrCast(@alignCast(ptr));
return self.draw(ctx);
}
pub fn draw(self: *const FlexRow, 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 = null, .height = ctx.max.height },
.arena = layout_arena.allocator(),
};
var first_pass_width: u16 = 0;
var total_flex: u16 = 0;
for (self.children, 0..) |child, i| {
const surf = try child.widget.draw(layout_ctx);
first_pass_width += surf.size.width;
total_flex += child.flex;
size_list[i] = surf.size.width;
}
// We are done with the layout arena
layout_arena.deinit();
// make our children list
var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena);
// Draw again, but with distributed widths
var second_pass_width: u16 = 0;
var max_height: u16 = 0;
const remaining_space = ctx.max.width.? - first_pass_width;
for (self.children, 1..) |child, i| {
const inherent_width = size_list[i - 1];
const child_width = if (child.flex == 0)
inherent_width
else if (i == self.children.len)
// If we are the last one, we just get the remainder
ctx.max.width.? - second_pass_width
else
inherent_width + (remaining_space * child.flex) / total_flex;
// Create a context for the child
const child_ctx = ctx.withConstraints(
.{ .width = child_width, .height = 0 },
.{ .width = child_width, .height = ctx.max.height.? },
);
const surf = try child.widget.draw(child_ctx);
try children.append(.{
.origin = .{ .col = second_pass_width, .row = 0 },
.surface = surf,
.z_index = 0,
});
max_height = @max(max_height, surf.size.height);
second_pass_width += surf.size.width;
}
const size = .{ .width = second_pass_width, .height = max_height };
return .{
.size = size,
.widget = self.widget(),
.buffer = &.{},
.children = children.items,
};
}
test FlexRow {
// 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 row
const flex_row: FlexRow = .{
.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();
const ucd = try vaxis.Unicode.init(arena.allocator());
vxfw.DrawContext.init(&ucd, .unicode);
const flex_widget = flex_row.widget();
const ctx: vxfw.DrawContext = .{
.arena = arena.allocator(),
.min = .{},
.max = .{ .width = 16, .height = 16 },
};
const surface = try flex_widget.draw(ctx);
// FlexRow expands to max width and tallest child
try std.testing.expectEqual(16, surface.size.width);
try std.testing.expectEqual(2, surface.size.height);
// We have four children
try std.testing.expectEqual(4, surface.children.len);
// We will track the column we are on to confirm the origins
var col: u16 = 0;
// First child has flex=0, it should be it's inherent width
try std.testing.expectEqual(3, surface.children[0].surface.size.width);
try std.testing.expectEqual(col, surface.children[0].origin.col);
// Add the child height each time
col += surface.children[0].surface.size.width;
// Let's do some math
// - We have 4 children to fit into 16 cols. All children will be 3 wide for a total width of 12
// - The first child is 3 cols and no flex. The rest of the width gets distributed evenly among
// the remaining 3 children. The remainder width is 16 - 12 = 4, so each child should get 4 /
// 3 = 1 extra cols, and the last will receive the remainder
try std.testing.expectEqual(1 + 3, surface.children[1].surface.size.width);
try std.testing.expectEqual(col, surface.children[1].origin.col);
col += surface.children[1].surface.size.width;
try std.testing.expectEqual(1 + 3, surface.children[2].surface.size.width);
try std.testing.expectEqual(col, surface.children[2].origin.col);
col += surface.children[2].surface.size.width;
try std.testing.expectEqual(1 + 3 + 1, surface.children[3].surface.size.width);
try std.testing.expectEqual(col, surface.children[3].origin.col);
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View file

@ -14,6 +14,7 @@ pub const App = @import("App.zig");
pub const Button = @import("Button.zig");
pub const Center = @import("Center.zig");
pub const FlexColumn = @import("FlexColumn.zig");
pub const FlexRow = @import("FlexRow.zig");
pub const ListView = @import("ListView.zig");
pub const RichText = @import("RichText.zig");
pub const Text = @import("Text.zig");