diff --git a/src/layout.zig b/src/layout.zig index deefa91..8e0d9d9 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -85,7 +85,9 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { } // import and export of `Layout` implementations + pub const HContainer = @import("layout/HContainer.zig").Layout(Event, Element, Renderer); pub const HStack = @import("layout/HStack.zig").Layout(Event, Element, Renderer); + pub const VContainer = @import("layout/VContainer.zig").Layout(Event, Element, Renderer); pub const VStack = @import("layout/VStack.zig").Layout(Event, Element, Renderer); pub const Padding = @import("layout/Padding.zig").Layout(Event, Element, Renderer); pub const Margin = @import("layout/Margin.zig").Layout(Event, Element, Renderer); @@ -93,7 +95,9 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type { }; // test layout implementation satisfies the interface comptime Type.Interface.satisfiedBy(Type); + comptime Type.Interface.satisfiedBy(Type.HContainer); comptime Type.Interface.satisfiedBy(Type.HStack); + comptime Type.Interface.satisfiedBy(Type.VContainer); comptime Type.Interface.satisfiedBy(Type.VStack); comptime Type.Interface.satisfiedBy(Type.Padding); comptime Type.Interface.satisfiedBy(Type.Margin); diff --git a/src/layout/HContainer.zig b/src/layout/HContainer.zig new file mode 100644 index 0000000..f9eefc7 --- /dev/null +++ b/src/layout/HContainer.zig @@ -0,0 +1,184 @@ +//! 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`? + 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)); + } + return .{ + .containers = containers, + .events = Events.init(allocator), + }; + } + + 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(); + } + + 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); + }, + } + } + } + }; +} diff --git a/src/layout/VContainer.zig b/src/layout/VContainer.zig new file mode 100644 index 0000000..af1a6f5 --- /dev/null +++ b/src/layout/VContainer.zig @@ -0,0 +1,184 @@ +//! Vertical Container layout for nested `Layout`s and/or `Widget`s. +//! The contained elements are sized according to the provided configuration. +//! For an evenly spaced vertical stacking see the `VStack` 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_vcontainer); + +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`? + 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)); + } + return .{ + .containers = containers, + .events = Events.init(allocator), + }; + } + + 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(); + } + + 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 rows = @divTrunc(size.rows * container.container_size, 100); + const sub_event: Event = .{ + .resize = .{ + .anchor = .{ + .col = size.anchor.col, + .row = size.anchor.row + offset, + }, + .cols = size.cols, + .rows = rows, + }, + }; + offset += rows; + 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); + }, + } + } + } + }; +}