fix(container): positioning; move separator options to layout struct
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 22s

Added corresponding test cases for padding, borders and corresponding
seperators.
This commit is contained in:
2025-02-26 18:21:55 +01:00
parent a293ef46da
commit ca14bc6106
13 changed files with 258 additions and 191 deletions

View File

@@ -73,11 +73,9 @@ pub fn main() !void {
};
var container = try App.Container.init(allocator, .{
.border = .{
.separator = .{ .enabled = true },
},
.layout = .{
.gap = 2,
.separator = .{ .enabled = true },
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
.direction = .horizontal,
},

View File

@@ -73,9 +73,9 @@ pub fn main() !void {
defer top_box.deinit();
var bottom_box = try App.Container.init(allocator, .{
.border = .{ .separator = .{ .enabled = true } },
.rectangle = .{ .fill = .blue },
.layout = .{
.separator = .{ .enabled = true },
.direction = .vertical,
.padding = .vertical(1),
},
@@ -93,14 +93,12 @@ pub fn main() !void {
defer bottom_box.deinit();
var container = try App.Container.init(allocator, .{
.border = .{
.layout = .{
.gap = 2,
.separator = .{
.enabled = true,
.line = .double,
},
},
.layout = .{
.gap = 2,
.padding = .all(5),
.direction = .vertical,
},

View File

@@ -50,16 +50,16 @@ pub fn main() !void {
const element = quit_text.element();
var container = try App.Container.init(allocator, .{
.border = .{ .separator = .{ .enabled = true } },
.layout = .{
.separator = .{ .enabled = true },
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
.direction = .horizontal,
},
}, element);
for (0..3) |_| {
var column = try App.Container.init(allocator, .{
.border = .{ .separator = .{ .enabled = true } },
.layout = .{
.separator = .{ .enabled = true },
.direction = .vertical,
},
}, .{});

View File

@@ -58,8 +58,8 @@ pub fn main() !void {
}, element);
for (0..3) |i| {
var column = try App.Container.init(allocator, .{
.border = .{ .separator = .{ .enabled = true } },
.layout = .{
.separator = .{ .enabled = true },
.direction = if (i > 0) .vertical else .horizontal,
},
}, .{});

View File

@@ -12,12 +12,8 @@ const log = std.log.scoped(.container);
/// Border configuration struct
pub const Border = packed struct {
// corners:
pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' };
pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' };
// separator.line:
pub const line: [2]u21 = .{ '│', '─' };
pub const dotted: [2]u21 = .{ '┆', '┄' };
pub const double: [2]u21 = .{ '║', '═' };
const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' };
const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' };
/// Color to use for the border
color: Color = .default,
@@ -40,19 +36,8 @@ pub const Border = packed struct {
/// Enable border sides for the top and bottom sides
pub const vertical: @This() = .{ .top = true, .bottom = true };
} = .{},
/// Configure separator borders between child element to added to the layout
separator: packed struct {
enabled: bool = false,
color: Color = .white,
line: enum(u2) {
line,
dotted,
double,
} = .line,
} = .{},
// NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows`
pub fn contents(this: @This(), cells: []Cell, size: Size, layout: Layout, len: u16) void {
pub fn contents(this: @This(), cells: []Cell, size: Size) void {
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
const frame = switch (this.corners) {
@@ -95,85 +80,6 @@ pub const Border = packed struct {
cells[idx + size.cols - 1].style.fg = this.color;
}
}
if (this.separator.enabled) {
// calculate where the separator would need to be
// TODO: use the childrens size to determine the location of the separator instead?
const gap = layout.gap + 1;
const element_cols = blk: {
var cols = size.cols - gap * (len - 1);
if (this.sides.left) cols -= 1;
if (this.sides.right) cols -= 1;
cols -= layout.padding.left + layout.padding.right;
break :blk @divTrunc(cols, len);
};
const element_rows = blk: {
var rows = size.rows - gap * (len - 1);
if (this.sides.top) rows -= 1;
if (this.sides.bottom) rows -= 1;
rows -= layout.padding.top + layout.padding.bottom;
break :blk @divTrunc(rows, len);
};
var offset: u16 = switch (layout.direction) {
.horizontal => layout.padding.left,
.vertical => layout.padding.top,
};
var overflow = switch (layout.direction) {
.horizontal => blk: {
var cols = size.cols - gap * (len - 1);
if (this.sides.left) cols -= 1;
if (this.sides.right) cols -= 1;
cols -= layout.padding.left + layout.padding.right;
break :blk cols - element_cols * len;
},
.vertical => blk: {
var rows = size.rows - gap * (len - 1);
if (this.sides.top) rows -= 1;
if (this.sides.bottom) rows -= 1;
rows -= layout.padding.top + layout.padding.bottom;
break :blk rows - element_rows * len;
},
};
const line_cps: [2]u21 = switch (this.separator.line) {
.line => line,
.dotted => dotted,
.double => double,
};
switch (layout.direction) {
.horizontal => {
offset += gap / 2;
for (0..len - 1) |_| {
var cols = element_cols;
if (overflow > 0) {
overflow -|= 1;
cols += 1;
}
offset += cols;
for (1..size.rows -| 1) |row| {
cells[row * size.cols + offset].cp = line_cps[0];
cells[row * size.cols + offset].style.fg = this.separator.color;
}
offset += gap;
}
},
.vertical => {
offset += gap / 2;
for (0..len - 1) |_| {
var rows = element_rows;
if (overflow > 0) {
overflow -|= 1;
rows += 1;
}
offset += rows;
for (1..size.cols -| 1) |col| {
cells[offset * size.cols + col].cp = line_cps[1];
cells[offset * size.cols + col].style.fg = this.separator.color;
}
offset += gap;
}
},
}
}
}
test "all sides" {
@@ -229,79 +135,6 @@ pub const Border = packed struct {
.cols = 30,
}, &container, @import("test/container/border.horizontal.zon"));
}
test "separator without gaps" {
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.separator = .{
.enabled = true,
},
},
}, .{});
try container.append(try .init(std.testing.allocator, .{}, .{}));
try container.append(try .init(std.testing.allocator, .{}, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
}, &container, @import("test/container/separator_no_gaps.zon"));
}
test "separator(2x) without gaps" {
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.separator = .{
.enabled = true,
.color = .red,
},
},
.layout = .{
.direction = .vertical,
},
}, .{});
try container.append(try .init(std.testing.allocator, .{}, .{}));
try container.append(try .init(std.testing.allocator, .{}, .{}));
try container.append(try .init(std.testing.allocator, .{}, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
}, &container, @import("test/container/separator_2x_no_gaps.zon"));
}
test "separator(2x) with border(all)" {
const event = @import("event.zig");
const testing = @import("testing.zig");
// FIXME: without a gap this does not work as expected!
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .red,
.sides = .all,
.separator = .{
.enabled = true,
.color = .red,
},
},
}, .{});
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
}, &container, @import("test/container/separator_no_gaps.zon"));
}
};
/// Rectangle configuration struct
@@ -325,6 +158,11 @@ pub const Rectangle = packed struct {
/// Layout configuration struct
pub const Layout = packed struct {
// separator.line:
const line: [2]u21 = .{ '│', '─' };
const dotted: [2]u21 = .{ '┆', '┄' };
const double: [2]u21 = .{ '║', '═' };
/// control the direction in which child elements are laid out
direction: enum(u1) { horizontal, vertical } = .horizontal,
/// Padding outside of the child elements
@@ -351,6 +189,227 @@ pub const Layout = packed struct {
} = .{},
/// Padding used in between child elements as gaps when laid out
gap: u16 = 0,
/// Configure separator borders between child element to added to the layout
separator: packed struct {
enabled: bool = false,
color: Color = .white,
line: enum(u2) {
line,
dotted,
double,
} = .line,
} = .{},
pub fn contents(this: @This(), comptime C: type, cells: []Cell, size: Size, children: []const C) void {
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
if (this.separator.enabled and children.len > 1) {
const line_cps: [2]u21 = switch (this.separator.line) {
.line => line,
.dotted => dotted,
.double => double,
};
const gap: u16 = (this.gap + 1) / 2;
for (0..children.len - 1) |idx| {
const child = children[idx];
const anchor = switch (this.direction) {
.horizontal => (child.size.anchor.row * size.cols) + child.size.anchor.col + child.size.cols + gap,
.vertical => ((child.size.anchor.row + child.size.rows + gap) * size.cols) + child.size.anchor.col,
};
switch (this.direction) {
.horizontal => for (0..child.size.rows) |row| {
cells[anchor + row * size.cols].cp = line_cps[0];
cells[anchor + row * size.cols].style.fg = this.separator.color;
},
.vertical => for (0..child.size.cols) |col| {
cells[anchor + col].cp = line_cps[1];
cells[anchor + col].style.fg = this.separator.color;
},
}
}
}
}
test "separator without gaps" {
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{
.separator = .{
.enabled = true,
},
},
}, .{});
try container.append(try .init(std.testing.allocator, .{}, .{}));
try container.append(try .init(std.testing.allocator, .{}, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
}, &container, @import("test/container/separator_no_gaps.zon"));
}
test "separator without gaps with padding" {
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{
.padding = .all(1),
.separator = .{
.enabled = true,
},
},
}, .{});
try container.append(try .init(std.testing.allocator, .{}, .{}));
try container.append(try .init(std.testing.allocator, .{}, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
}, &container, @import("test/container/separator_no_gaps_with_padding.zon"));
}
test "separator(2x) without gaps" {
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{
.direction = .vertical,
.separator = .{
.enabled = true,
.color = .red,
},
},
}, .{});
try container.append(try .init(std.testing.allocator, .{}, .{}));
try container.append(try .init(std.testing.allocator, .{}, .{}));
try container.append(try .init(std.testing.allocator, .{}, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
}, &container, @import("test/container/separator_2x_no_gaps.zon"));
}
test "separator(2x) with border(all)" {
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .red,
.sides = .all,
},
.layout = .{
.separator = .{
.enabled = true,
.color = .red,
},
},
}, .{});
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
}, &container, @import("test/container/separator_2x_no_gaps_with_border.zon"));
}
test "separator(2x) with border(all) and padding(all(1))" {
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .red,
.sides = .all,
},
.layout = .{
.padding = .all(1),
.separator = .{
.enabled = true,
.color = .red,
},
},
}, .{});
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
}, &container, @import("test/container/separator_2x_no_gaps_with_padding.zon"));
}
test "separator(2x) with border(all) and gap" {
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .red,
.sides = .all,
},
.layout = .{
.gap = 2,
.separator = .{
.enabled = true,
.color = .red,
},
},
}, .{});
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
}, &container, @import("test/container/separator_2x_with_gaps_with_border.zon"));
}
test "separator(2x) with border(all) and gap and padding" {
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .red,
.sides = .all,
},
.layout = .{
.gap = 2,
.padding = .all(1),
.separator = .{
.enabled = true,
.color = .red,
},
},
}, .{});
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
}, &container, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon"));
}
};
pub fn Container(comptime Event: type) type {
@@ -407,7 +466,7 @@ pub fn Container(comptime Event: type) type {
size = size.merge(element.minSize());
}
var gap = this.properties.layout.gap;
if (this.properties.border.separator.enabled) gap += 1;
if (this.properties.layout.separator.enabled) gap += 1;
switch (this.properties.layout.direction) {
.horizontal => size.cols += gap * (len - 1),
@@ -438,7 +497,7 @@ pub fn Container(comptime Event: type) type {
const sides = this.properties.border.sides;
const padding = layout.padding;
var gap = layout.gap;
if (this.properties.border.separator.enabled) gap += 1;
if (layout.separator.enabled) gap += 1;
const len: u16 = @truncate(this.elements.items.len);
const element_cols = blk: {
@@ -552,9 +611,9 @@ pub fn Container(comptime Event: type) type {
@memset(cells, .{});
errdefer this.allocator.free(cells);
this.properties.border.contents(cells, this.size, this.properties.layout, @truncate(this.elements.items.len));
this.properties.layout.contents(@This(), cells, this.size, this.elements.items);
this.properties.border.contents(cells, this.size);
this.properties.rectangle.contents(cells, this.size);
// NOTE: Layout has no contents to provide, hence no content method is called (or even exists in the `Layout` struct)
try this.element.content(cells, this.size);

View File

@@ -35,10 +35,17 @@ pub fn Element(Event: type) type {
/// `cells` slice has the size of (`size.cols * size.rows`). The
/// renderer will know where to place the contents on the screen.
///
/// This function should only fail with an error if the error is
/// non-recoverable (i.e. an allocation error, system error, etc.).
/// Otherwise user specific errors should be caught using the `handle`
/// function before the rendering of the `Container` happens.
/// # Note
///
/// - Caller owns `cells` slice and ensures that the size usually by assertion:
/// ```zig
/// std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
/// ```
///
/// - This function should only fail with an error if the error is
/// non-recoverable (i.e. an allocation error, system error, etc.).
/// Otherwise user specific errors should be caught using the `handle`
/// function before the rendering of the `Container` happens.
pub inline fn content(this: @This(), cells: []Cell, size: Size) !void {
if (this.vtable.content) |content_fn|
try content_fn(this.ptr, cells, size);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long