Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 29s
The .resize `Event` has been adapted to include an _anchor_, which provide the full necessary information for each widget where to render on the screen with what requested size. Each Widget can then dynamically decide how and what to render (i.e. provide placeholder text in case the size is too small, etc.).
171 lines
7.1 KiB
Zig
171 lines
7.1 KiB
Zig
//! 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);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|