//! Horizontal Container layout for nested `Layout`s and/or `Widget`s. //! The contained elements are sized according to the provided configuration. //! For an evenly spaced horizontal stacking see the `HStack` layout. //! //! # Example //! ... const std = @import("std"); const terminal = @import("../terminal.zig"); const isTaggedUnion = @import("../event.zig").isTaggedUnion; const Error = @import("../event.zig").Error; const Key = terminal.Key; const log = std.log.scoped(.layout_hcontainer); pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); } if (!isTaggedUnion(Element)) { @compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); } if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) { @compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name); } if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) { @compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[1].name); } const Container = struct { element: Element, container_size: u8, // 0 - 100 % }; const Containers = std.ArrayList(Container); const LayoutType = @typeInfo(Element).@"union".fields[0].type; const WidgetType = @typeInfo(Element).@"union".fields[1].type; const Events = std.ArrayList(Event); return struct { // TODO: current focused `Element`? allocator: std.mem.Allocator = undefined, size: terminal.Size = undefined, containers: Containers = undefined, events: Events = undefined, pub fn init(allocator: std.mem.Allocator, children: anytype) *@This() { const ArgsType = @TypeOf(children); const args_type_info = @typeInfo(ArgsType); if (args_type_info != .@"struct") { @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); } comptime var total_size = 0; const fields_info = args_type_info.@"struct".fields; var containers = Containers.initCapacity(allocator, fields_info.len) catch @panic("OOM"); inline for (comptime fields_info) |field| { const child = @field(children, field.name); const ChildType = @TypeOf(child); const child_type_info = @typeInfo(ChildType); if (child_type_info != .@"struct") { @compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType)); } const child_fields = child_type_info.@"struct".fields; if (child_fields.len != 2) { @compileError("expected nested tuple or struct to have exactly 2 fields, but found " ++ child_fields.len); } const element = @field(child, child_fields[0].name); const ElementType = @TypeOf(element); const element_size = @field(child, child_fields[1].name); const ElementSizeType = @TypeOf(element_size); if (ElementSizeType != u8 and ElementSizeType != comptime_int) { @compileError("expected an u8 or comptime_int as second argument of nested tuple or struct child, but found " ++ @typeName(ElementSizeType)); } total_size += element_size; if (total_size > 100) { @compileError("cannot place element: " ++ child_fields[0].name ++ " as total size of used container elements would overflow"); } if (ElementType == WidgetType) { containers.append(.{ .element = .{ .widget = element }, .container_size = element_size, }) catch {}; continue; } if (ElementType == LayoutType) { containers.append(.{ .element = .{ .layout = element }, .container_size = element_size, }) catch {}; continue; } @compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType)); } const layout = allocator.create(@This()) catch @panic("OOM"); layout.allocator = allocator; layout.containers = containers; layout.events = Events.init(allocator); return layout; } pub fn deinit(this: *@This()) void { this.events.deinit(); for (this.containers.items) |*container| { switch (container.element) { .layout => |*layout| { layout.deinit(); }, .widget => |*widget| { widget.deinit(); }, } } this.containers.deinit(); this.allocator.destroy(this); } pub fn handle(this: *@This(), event: Event) !*Events { this.events.clearRetainingCapacity(); // order is important switch (event) { .resize => |size| { this.size = size; // adjust size according to the containing elements log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ size.anchor.col, size.anchor.row, size.cols, size.rows, }); // adjust size according to the container size var offset: u16 = 0; for (this.containers.items) |*container| { const cols = @divTrunc(size.cols * container.container_size, 100); const sub_event: Event = .{ .resize = .{ .anchor = .{ .col = size.anchor.col + offset, .row = size.anchor.row, }, .cols = cols, .rows = size.rows, }, }; offset += cols; switch (container.element) { .layout => |*layout| { const events = try layout.handle(sub_event); try this.events.appendSlice(events.items); }, .widget => |*widget| { if (widget.handle(sub_event)) |e| { try this.events.append(e); } }, } } }, else => { for (this.containers.items) |*container| { switch (container.element) { .layout => |*layout| { const events = try layout.handle(event); try this.events.appendSlice(events.items); }, .widget => |*widget| { if (widget.handle(event)) |e| { try this.events.append(e); } }, } } }, } return &this.events; } pub fn render(this: *@This(), renderer: *Renderer) !void { for (this.containers.items) |*container| { switch (container.element) { .layout => |*layout| { try layout.render(renderer); }, .widget => |*widget| { try widget.render(renderer); }, } } } }; }