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
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:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user