//! Margin layout for a nested `Layout`s 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_margin); 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 Events = std.ArrayList(Event); return struct { allocator: std.mem.Allocator = undefined, size: terminal.Size = undefined, require_render: bool = false, element: Element = undefined, events: Events = undefined, config: Config = undefined, const Config = struct { margin: ?u8 = undefined, left: u8 = 0, right: u8 = 0, top: u8 = 0, bottom: u8 = 0, }; pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) *@This() { if (config.margin) |margin| { std.debug.assert(margin <= 50); } else { std.debug.assert(config.left + config.right < 100); std.debug.assert(config.top + config.bottom < 100); } const layout = allocator.create(@This()) catch @panic("OOM"); layout.allocator = allocator; layout.config = config; layout.element = element; layout.events = Events.init(allocator); return layout; } pub fn deinit(this: *@This()) void { this.events.deinit(); switch ((&this.element).*) { .layout => |*layout| { layout.deinit(); }, .widget => |*widget| { widget.deinit(); }, } this.allocator.destroy(this); this.* = undefined; } pub fn handle(this: *@This(), event: Event) !*Events { this.events.clearRetainingCapacity(); // order is important switch (event) { .resize => |size| { this.size = size; this.require_render = true; log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ size.anchor.col, size.anchor.row, size.cols, size.rows, }); var sub_event: Event = undefined; if (this.config.margin) |margin| { // used overall margin const h_margin: u16 = @divTrunc(margin * size.cols, 100); const v_margin: u16 = @divFloor(margin * size.rows, 100); sub_event = .{ .resize = .{ .anchor = .{ .col = size.anchor.col + h_margin, .row = size.anchor.row + v_margin, }, .cols = size.cols -| (h_margin * 2), .rows = size.rows -| (v_margin * 2), }, }; } else { // use all for directions individually const left_margin: u16 = @divFloor(this.config.left * size.cols, 100); const right_margin: u16 = @divFloor(this.config.right * size.cols, 100); const top_margin: u16 = @divFloor(this.config.top * size.rows, 100); const bottom_margin: u16 = @divFloor(this.config.bottom * size.rows, 100); sub_event = .{ .resize = .{ .anchor = .{ .col = size.anchor.col + left_margin, .row = size.anchor.row + top_margin, }, .cols = size.cols -| left_margin -| right_margin, .rows = size.rows -| top_margin -| bottom_margin, }, }; } // adjust size according to the containing elements switch ((&this.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 => { switch ((&this.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 { if (this.require_render) { try renderer.clear(this.size); this.require_render = false; } switch ((&this.element).*) { .layout => |*layout| { try layout.render(renderer); }, .widget => |*widget| { try widget.render(renderer); }, } } }; }