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"); const log = std.log.scoped(.container); /// Border configuration struct pub const Border = struct { /// 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 = true, bottom: bool = true, left: bool = true, right: bool = true, } = .{}, /// Configure separator borders between child element to added to the layout separator: struct { enabled: bool = false, color: Color = .default, line: enum { line, dotted, // TODO: add more variations which could be used for the separator } = .line, } = .{}, }; /// Rectangle configuration struct pub const Rectangle = 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, /// Configure the corners of the `Rectangle` corners: enum(u1) { squared, rounded, } = .squared, }; /// Scroll configuration struct pub const Scroll = packed struct { /// Enable horizontal scrolling for this element horizontal: bool = false, /// Enable vertical scrolling for this element vertical: bool = false, }; /// Layout configuration struct pub const Layout = 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, // TODO: is there a way to make x / y type copied by the compiler at comptime instead? such that this only has to be defined once? /// Alignment of where the child elements are positioned relative to the parent container when laid out alignment: packed struct { x: enum(u2) { center, left, right } = .center, y: enum(u2) { center, left, right } = .center, } = .{}, // TODO: is there a way to make width / height type copied by the compiler at comptime instead? such that this only has to be defined once? // NOTE: `sizing` cannot be *packed* because of the tagged unions? is this necessary -> I would need to measure the size differences /// Sizing to be used for the width and height of this element to use sizing: struct { width: union(enum) { fit, grow, fixed: u16, percent: u16 } = .fit, height: union(enum) { fit, grow, fixed: u16, percent: u16 } = .fit, } = .{}, }; pub fn Container(comptime Event: type) type { if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Container(comptime Event: type)`"); } return struct { size: Size, properties: Properties, 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 = struct { border: Border = .{}, rectangle: Rectangle = .{}, scroll: Scroll = .{}, layout: Layout = .{}, }; pub fn init(allocator: std.mem.Allocator, properties: Properties) !@This() { return .{ .size = .{ .cols = 0, .rows = 0 }, .properties = properties, .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) ?Event { switch (event) { .init => log.debug(".init 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; const len: u16 = @truncate(this.elements.items.len); const element_cols = @divTrunc(size.cols, len); const element_rows = @divTrunc(size.rows, len); var offset: u16 = 0; for (this.elements.items) |*element| { var element_size: Size = undefined; switch (this.properties.layout.direction) { .horizontal => { var overflow = size.cols % len; // adjust size according to the containing elements var cols = element_cols; if (overflow > 0) { overflow -|= 1; cols += 1; } element_size = .{ .anchor = .{ .col = size.anchor.col + offset, .row = size.anchor.row, }, .cols = cols, .rows = size.rows, }; offset += cols; }, .vertical => { var overflow = size.rows % len; // adjust size according to the containing elements var rows = element_rows; if (overflow > 0) { overflow -|= 1; rows += 1; } element_size = .{ .anchor = .{ .col = size.anchor.col, .row = size.anchor.row + offset, }, .cols = size.cols, .rows = rows, }; offset += rows; }, } // TODO; adjust size according to the layout of the `Container` if (element.handle(.{ .resize = element_size })) |e| { _ = e; } } return null; }, else => {}, } for (this.elements.items) |*element| { if (element.handle(event)) |e| { // TODO: if only the top level container returns a single // event (i.e. as a reaction to a certain other event) what // should happen to potential other events? _ = e; } } return null; } pub fn contents(this: *const @This()) []const Cell { // TODO: use the size and the corresponding contents to determine what should be show in form of a `Cell` array _ = this; return &[0]Cell{}; } }; }