//! Vertical Stacking layout for nested `Layout`s and/or `Widget`s. //! //! # 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_vstack); pub fn Layout(comptime Event: type, comptime Renderer: type) type { if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } const Widget = @import("../widget.zig").Widget(Event, Renderer); const Lay = @import("../layout.zig").Layout(Event, Renderer); const Element = union(enum) { layout: Lay, widget: Widget, }; const Elements = std.ArrayList(Element); const Events = std.ArrayList(Event); return struct { // TODO: current focused `Element`? // FIX: this should not be 'hardcoded' but dynamically be calculated and updated (i.e. through the event system) anchor: terminal.Position = .{ .col = 1, .row = 1 }, size: terminal.Size = undefined, element_rows: u16 = undefined, elements: Elements = 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)); } const fields_info = args_type_info.Struct.fields; var elements = Elements.initCapacity(allocator, fields_info.len) catch @panic("OOM"); inline for (comptime fields_info) |field| { const child = @field(children, field.name); const ChildType = @TypeOf(child); if (ChildType == Widget) { elements.append(.{ .widget = child }) catch {}; continue; } if (ChildType == Lay) { elements.append(.{ .layout = child }) catch {}; continue; } @compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(Lay) ++ " or " ++ @typeName(Widget) ++ " but " ++ @typeName(ChildType)); } return .{ .elements = elements, .events = Events.init(allocator), }; } pub fn deinit(this: *@This()) void { this.events.deinit(); for (this.elements.items) |*element| { switch (element.*) { .layout => |*layout| { layout.deinit(); }, .widget => |*widget| { widget.deinit(); }, } } this.elements.deinit(); } pub fn handle(this: *@This(), event: Event) !*Events { this.events.clearRetainingCapacity(); // order is important switch (event) { .resize => |size| { this.size = size; log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ size.anchor.col, size.anchor.row, size.cols, size.rows, }); const len: u16 = @truncate(this.elements.items.len); this.element_rows = @divTrunc(size.rows, len); var overflow = this.size.rows % len; var offset: u16 = 0; // adjust size according to the containing elements for (this.elements.items) |*element| { var rows = this.element_rows; if (overflow > 0) { overflow -|= 1; rows += 1; } const sub_event: Event = .{ .resize = .{ .anchor = .{ .col = size.anchor.col, .row = size.anchor.row + offset, }, .cols = size.cols, .rows = rows, }, }; offset += rows; switch (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.elements.items) |*element| { switch (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 { // FIX: renderer should clear only what is going to change! (i.e. the 'active' widget / layout) try renderer.clear(this.anchor, this.size); var overflow = this.size.rows % this.elements.items.len; for (this.elements.items, 0..) |*element, i| { const row_mul: u16 = @truncate(i); var row = row_mul * this.element_rows + 1; if (i > 0 and overflow > 0) { overflow -|= 1; row += 1; } // TODO: that's the anchor of each component (only necessary for each widget rendering) const pos: terminal.Position = .{ .col = 1, .row = row }; // TODO: do this using the renderer try terminal.setCursorPosition(pos); switch (element.*) { .layout => |*layout| { try layout.render(renderer); }, .widget => |*widget| { // TODO: clear per widget if necesary (i.e. can I query that?) try widget.render(renderer); }, } } } }; }