const std = @import("std"); const isTaggedUnion = @import("event.zig").isTaggedUnion; const Cell = @import("cell.zig"); const Color = @import("color.zig").Color; const Size = @import("size.zig").Size; const Style = @import("style.zig"); const log = std.log.scoped(.container); /// Border configuration struct pub const Border = packed struct { pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' }; pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' }; /// Color to use for the border color: Color = .default, /// Configure the corner type to be used for the border corners: enum(u1) { squared, rounded, } = .squared, /// Configure the sides where the borders shall be rendered sides: packed struct { top: bool = false, bottom: bool = false, left: bool = false, right: bool = false, /// Enable border sides for all four sides pub const all: @This() = .{ .top = true, .bottom = true, .left = true, .right = true }; /// Enable border sides for the left and right sides pub const horizontal: @This() = .{ .left = true, .right = true }; /// 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(u1) { line, dotted, // TODO: add more variations which could be used for the separator } = .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 { std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows)); const frame = switch (this.corners) { .rounded => Border.rounded_border, .squared => Border.squared_border, }; std.debug.assert(frame.len == 6); // render top and bottom border for (0..size.cols) |col| { const last_row = @as(usize, size.rows - 1) * @as(usize, size.cols); if (this.sides.left and col == 0) { // top left corner if (this.sides.top) cells[col].cp = frame[0]; // bottom left corner if (this.sides.bottom) cells[last_row + col].cp = frame[4]; } else if (this.sides.right and col == size.cols - 1) { // top right corner if (this.sides.top) cells[col].cp = frame[2]; // bottom left corner if (this.sides.bottom) cells[last_row + col].cp = frame[5]; } else { // top side if (this.sides.top) cells[col].cp = frame[1]; // bottom side if (this.sides.bottom) cells[last_row + col].cp = frame[1]; } if (this.sides.top) cells[col].style.fg = this.color; if (this.sides.bottom) cells[last_row + col].style.fg = this.color; } // render left and right border for (1..size.rows -| 1) |row| { const idx = (row * size.cols); if (this.sides.left) { cells[idx].cp = frame[3]; // left cells[idx].style.fg = this.color; } if (this.sides.right) { cells[idx + size.cols - 1].cp = frame[3]; // right cells[idx + size.cols - 1].style.fg = this.color; } } if (this.separator.enabled) { // calculate where the separator would need to be const element_cols = blk: { var cols = size.cols - layout.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 - layout.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 - layout.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 - layout.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; }, }; switch (layout.direction) { .horizontal => { offset += layout.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| { // TODO: support the line options cells[row * size.cols + offset].cp = frame[3]; cells[row * size.cols + offset].style.fg = this.separator.color; } offset += layout.gap; } }, .vertical => { offset += layout.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| { // TODO: support the line options cells[offset * size.cols + col].cp = frame[1]; cells[offset * size.cols + col].style.fg = this.separator.color; } offset += layout.gap; } }, } } } }; /// Rectangle configuration struct pub const Rectangle = packed struct { /// `Color` to use to fill the `Rectangle` with /// NOTE: used as background color when rendering! such that it renders the /// children accordingly without removing the coloring of the `Rectangle` fill: Color = .default, // NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows` pub fn contents(this: @This(), cells: []Cell, size: Size) void { std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows)); for (0..size.rows) |row| { for (0..size.cols) |col| { cells[(row * size.cols) + col].style.bg = this.fill; } } } }; /// Layout configuration struct pub const Layout = packed struct { /// control the direction in which child elements are laid out direction: enum(u1) { horizontal, vertical } = .horizontal, /// Padding outside of the child elements padding: packed struct { top: u16 = 0, bottom: u16 = 0, left: u16 = 0, right: u16 = 0, /// Create a padding with equivalent padding in all four directions. pub fn all(padding: u16) @This() { return .{ .top = padding, .bottom = padding, .left = padding, .right = padding }; } /// Create a padding with equivalent padding in the left and right directions; others directions remain the default value. pub fn horizontal(padding: u16) @This() { return .{ .left = padding, .right = padding }; } /// Create a padding with equivalent padding in the top and bottom directions; others directions remain the default value. pub fn vertical(padding: u16) @This() { return .{ .top = padding, .bottom = padding }; } } = .{}, /// Padding used in between child elements as gaps when laid out gap: u16 = 0, }; pub fn Container(comptime Event: type) type { if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Container(comptime Event: type)`"); } const Element = @import("element.zig").Element(Event); return struct { allocator: std.mem.Allocator, size: Size, properties: Properties, element: Element, elements: std.ArrayList(@This()), /// Properties for each `Container` to configure their layout, /// border, styling, etc. For details see the corresponding individual /// documentation of the members of this struct accordingly. pub const Properties = packed struct { border: Border = .{}, rectangle: Rectangle = .{}, layout: Layout = .{}, }; pub fn init( allocator: std.mem.Allocator, properties: Properties, element: Element, ) !@This() { return .{ .allocator = allocator, .size = .{}, .properties = properties, .element = element, .elements = std.ArrayList(@This()).init(allocator), }; } pub fn deinit(this: *@This()) void { for (this.elements.items) |*element| { element.deinit(); } this.elements.deinit(); } pub fn append(this: *@This(), element: @This()) !void { try this.elements.append(element); } pub fn handle(this: *@This(), event: Event) !void { switch (event) { .resize => |size| resize: { log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ size.anchor.col, size.anchor.row, size.cols, size.rows, }); this.size = size; if (this.elements.items.len == 0) break :resize; if (this.properties.border.separator.enabled) this.properties.layout.gap += 1; const layout = this.properties.layout; const sides = this.properties.border.sides; const padding = layout.padding; const gap = layout.gap; const len: u16 = @truncate(this.elements.items.len); const element_cols = blk: { var cols = size.cols - gap * (len - 1); if (sides.left) cols -= 1; if (sides.right) cols -= 1; cols -= padding.left + padding.right; break :blk @divTrunc(cols, len); }; const element_rows = blk: { var rows = size.rows - gap * (len - 1); if (sides.top) rows -= 1; if (sides.bottom) rows -= 1; rows -= padding.top + padding.bottom; break :blk @divTrunc(rows, len); }; var offset: u16 = switch (layout.direction) { .horizontal => padding.left, .vertical => padding.top, }; var overflow = switch (layout.direction) { .horizontal => blk: { var cols = size.cols - gap * (len - 1); if (sides.left) cols -= 1; if (sides.right) cols -= 1; cols -= padding.left + padding.right; break :blk cols - element_cols * len; }, .vertical => blk: { var rows = size.rows - gap * (len - 1); if (sides.top) rows -= 1; if (sides.bottom) rows -= 1; rows -= padding.top + padding.bottom; break :blk rows - element_rows * len; }, }; for (this.elements.items) |*element| { var element_size: Size = undefined; switch (layout.direction) { .horizontal => { var cols = element_cols; if (overflow > 0) { overflow -|= 1; cols += 1; } element_size = .{ .anchor = .{ .col = this.size.anchor.col + offset, .row = this.size.anchor.row, }, .cols = cols, .rows = size.rows, }; // border if (sides.top) element_size.rows -= 1; if (sides.bottom) element_size.rows -= 1; // padding element_size.anchor.row += padding.top; element_size.rows -= padding.top + padding.bottom; // gap offset += gap; offset += cols; }, .vertical => { var rows = element_rows; if (overflow > 0) { overflow -|= 1; rows += 1; } element_size = .{ .anchor = .{ .col = this.size.anchor.col, .row = this.size.anchor.row + offset, }, .cols = size.cols, .rows = rows, }; // border if (sides.left) element_size.cols -= 1; if (sides.right) element_size.cols -= 1; // padding element_size.anchor.col += padding.left; element_size.cols -= padding.left + padding.right; // gap offset += gap; offset += rows; }, } // border resizing if (sides.top) element_size.anchor.row += 1; if (sides.left) element_size.anchor.col += 1; try element.handle(.{ .resize = element_size }); } }, else => { try this.element.handle(event); for (this.elements.items) |*element| try element.handle(event); }, } } pub fn contents(this: *const @This()) ![]const Cell { const cells = try this.allocator.alloc(Cell, @as(usize, this.size.cols) * @as(usize, this.size.rows)); @memset(cells, .{}); errdefer this.allocator.free(cells); this.properties.border.contents(cells, this.size, this.properties.layout, @truncate(this.elements.items.len)); this.properties.rectangle.contents(cells, this.size); try this.element.content(cells, this.size); return cells; } }; }