//! 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 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 Elements = std.ArrayList(Element); 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, 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 == WidgetType) { elements.append(.{ .widget = child }) catch {}; continue; } if (ChildType == LayoutType) { elements.append(.{ .layout = child }) catch {}; continue; } @compileError("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.elements = elements; layout.events = Events.init(allocator); return layout; } 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(); 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; 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); const element_rows = @divTrunc(size.rows, len); var overflow = size.rows % len; var offset: u16 = 0; // adjust size according to the containing elements for (this.elements.items) |*element| { var rows = 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 { for (this.elements.items) |*element| { switch (element.*) { .layout => |*layout| { try layout.render(renderer); }, .widget => |*widget| { try widget.render(renderer); }, } } } }; }