All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 36s
165 lines
6.8 KiB
Zig
165 lines
6.8 KiB
Zig
//! 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);
|
|
},
|
|
}
|
|
}
|
|
};
|
|
}
|