Files
zterm/src/layout/HStack.zig
Yves Biener 90af887d77
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 36s
fix: destructor should not change pointer to undefined
2024-11-16 20:12:02 +01:00

163 lines
7.0 KiB
Zig

//! Horizontal 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_hstack);
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;
// 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,
});
const len: u16 = @truncate(this.elements.items.len);
const element_cols = @divTrunc(size.cols, len);
var overflow = size.cols % len;
var offset: u16 = 0;
// adjust size according to the containing elements
for (this.elements.items) |*element| {
var cols = element_cols;
if (overflow > 0) {
overflow -|= 1;
cols += 1;
}
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + offset,
.row = size.anchor.row,
},
.cols = cols,
.rows = size.rows,
},
};
offset += cols;
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);
},
}
}
}
};
}