From 4781e9ce3993bf3424074bb23d731044beba0f94 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sat, 15 Feb 2025 15:56:30 +0100 Subject: [PATCH] add(element): interface for injecting user behavior to containers Some additional refactoring and documentation updates have also been applied. --- examples/container.zig | 80 +++++++++++++++++++++++++++++++++--------- src/app.zig | 22 ++++++++---- src/container.zig | 45 ++++++++++++------------ src/element.zig | 23 ++++++++++++ src/zterm.zig | 3 ++ 5 files changed, 128 insertions(+), 45 deletions(-) diff --git a/examples/container.zig b/examples/container.zig index 5ad7c53..88d98d0 100644 --- a/examples/container.zig +++ b/examples/container.zig @@ -6,23 +6,65 @@ const Key = zterm.Key; const log = std.log.scoped(.example); +// TODO: maybe inlining the functions should be done as well to reduce the call +// stacks (might be important for more complex structures) + +pub const ExampleElement = struct { + pub fn element(this: *@This()) App.Element { + return .{ + .ptr = this, + .vtable = &.{ + .handle = handle, + .content = content, + }, + }; + } + + // example function to render contents for a `Container` + fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void { + _ = ctx; + std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows)); + + // NOTE: error should only be returned here in case an in-recoverable exception has occured + const row = size.rows / 2; + const col = size.cols / 2 -| 3; + + for (0..5) |c| { + cells[(row * size.cols) + col + c].style.fg = .black; + cells[(row * size.cols) + col + c].cp = '-'; + } + } + + // example function to handle events for a `Container` + fn handle(ctx: *anyopaque, event: App.Event) !void { + _ = ctx; + switch (event) { + .init => log.debug(".init event", .{}), + else => {}, + } + } +}; + pub fn main() !void { errdefer |err| log.err("Application Error: {any}", .{err}); // TODO: maybe create own allocator as some sort of arena allocator to have consistent memory usage - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer { const deinit_status = gpa.deinit(); if (deinit_status == .leak) { - log.err("memory lead", .{}); + log.err("memory leak", .{}); } } const allocator = gpa.allocator(); - var app: App = .{}; + var app: App = .init; var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); + var element_wrapper = ExampleElement{}; + const element = element_wrapper.element(); + var container = try App.Container.init(allocator, .{ .border = .{ .separator = .{ .enabled = true } }, .layout = .{ @@ -30,7 +72,7 @@ pub fn main() !void { .padding = .all(5), .direction = .horizontal, }, - }); + }, element); var box = try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, .layout = .{ @@ -38,29 +80,28 @@ pub fn main() !void { .direction = .vertical, .padding = .vertical(1), }, - }); + }, .{}); try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, - })); + }, element)); try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, - })); + }, .{})); try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_green }, - })); + }, element)); try container.append(box); try container.append(try App.Container.init(allocator, .{ .border = .{ .color = .light_blue, - .sides = .vertical(), + .sides = .vertical, }, - })); + }, .{})); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .blue }, - })); - defer container.deinit(); + }, .{})); + defer container.deinit(); // also de-initializes the children - // NOTE: should the min-size here be required? try app.start(); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); @@ -91,13 +132,18 @@ pub fn main() !void { }); } }, - .err => |err| { - log.err("Received {any} with message: {s}", .{ @errorName(err.err), err.msg }); - }, + // NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback + .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), else => {}, } - try container.handle(event); + // NOTE: returned errors should be propagated back to the application + container.handle(event) catch |err| app.postEvent(.{ + .err = .{ + .err = err, + .msg = "Container Event handling failed", + }, + }); try renderer.render(@TypeOf(container), &container); try renderer.flush(); } diff --git a/src/app.zig b/src/app.zig index 13a72d9..a8e1dd3 100644 --- a/src/app.zig +++ b/src/app.zig @@ -27,8 +27,8 @@ const log = std.log.scoped(.app); /// union(enum) {}, /// ); /// // later on create an `App` instance and start the event loop -/// var app: App = .{}; -/// try app.start(null); +/// var app: App = .init; +/// try app.start(); /// defer app.stop() catch unreachable; /// ``` pub fn App(comptime E: type) type { @@ -38,19 +38,29 @@ pub fn App(comptime E: type) type { return struct { pub const Event = mergeTaggedUnions(event.SystemEvent, E); pub const Container = @import("container.zig").Container(Event); + pub const Element = @import("element.zig").Element(Event); - queue: Queue(Event, 256) = .{}, - thread: ?std.Thread = null, - quit_event: std.Thread.ResetEvent = .{}, + queue: Queue(Event, 256), + thread: ?std.Thread, + quit_event: std.Thread.ResetEvent, termios: ?std.posix.termios = null, attached_handler: bool = false, - prev_size: Size = .{ .cols = 0, .rows = 0 }, + prev_size: Size, pub const SignalHandler = struct { context: *anyopaque, callback: *const fn (context: *anyopaque) void, }; + pub const init: @This() = .{ + .queue = .{}, + .thread = null, + .quit_event = .{}, + .termios = null, + .attached_handler = false, + .prev_size = .{}, + }; + pub fn start(this: *@This()) !void { if (this.thread) |_| return; diff --git a/src/container.zig b/src/container.zig index 9206ebf..d9af312 100644 --- a/src/container.zig +++ b/src/container.zig @@ -27,17 +27,12 @@ pub const Border = packed struct { left: bool = false, right: bool = false, - pub fn all() @This() { - return .{ .top = true, .bottom = true, .left = true, .right = true }; - } - - pub fn horizontal() @This() { - return .{ .left = true, .right = true }; - } - - pub fn vertical() @This() { - return .{ .top = true, .bottom = true }; - } + /// Enable border sides for all four sides + pub const all: @This() = .{ .top = true, .bottom = true, .left = true, .right = true }; + /// Enable border sides for the left and right sides + pub const horizontal: @This() = .{ .left = true, .right = true }; + /// Enable border sides for the top and bottom sides + pub const vertical: @This() = .{ .top = true, .bottom = true }; } = .{}, /// Configure separator borders between child element to added to the layout separator: packed struct { @@ -235,10 +230,12 @@ pub fn Container(comptime Event: type) type { if (!isTaggedUnion(Event)) { @compileError("Provided user event `Event` for `Container(comptime Event: type)`"); } + const Element = @import("element.zig").Element(Event); return struct { allocator: std.mem.Allocator, size: Size, properties: Properties, + element: Element, elements: std.ArrayList(@This()), /// Properties for each `Container` to configure their layout, @@ -251,11 +248,16 @@ pub fn Container(comptime Event: type) type { layout: Layout = .{}, }; - pub fn init(allocator: std.mem.Allocator, properties: Properties) !@This() { + pub fn init( + allocator: std.mem.Allocator, + properties: Properties, + element: Element, + ) !@This() { return .{ .allocator = allocator, .size = .{}, .properties = properties, + .element = element, .elements = std.ArrayList(@This()).init(allocator), }; } @@ -273,7 +275,6 @@ pub fn Container(comptime Event: type) type { pub fn handle(this: *@This(), event: Event) !void { switch (event) { - .init => log.debug(".init event", .{}), .resize => |size| resize: { log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ size.anchor.col, @@ -384,29 +385,29 @@ pub fn Container(comptime Event: type) type { } // border resizing - if (sides.top) { - element_size.anchor.row += 1; - } - if (sides.left) { - element_size.anchor.col += 1; - } - - // padding resizing + if (sides.top) element_size.anchor.row += 1; + if (sides.left) element_size.anchor.col += 1; try element.handle(.{ .resize = element_size }); } }, - else => for (this.elements.items) |*element| try element.handle(event), + else => { + try this.element.handle(event); + for (this.elements.items) |*element| try element.handle(event); + }, } } pub fn contents(this: *const @This()) ![]const Cell { const cells = try this.allocator.alloc(Cell, @as(usize, this.size.cols) * @as(usize, this.size.rows)); @memset(cells, .{}); + errdefer this.allocator.free(cells); this.properties.border.contents(cells, this.size, this.properties.layout, @truncate(this.elements.items.len)); this.properties.rectangle.contents(cells, this.size); + try this.element.content(cells, this.size); + return cells; } }; diff --git a/src/element.zig b/src/element.zig index e69de29..0b59394 100644 --- a/src/element.zig +++ b/src/element.zig @@ -0,0 +1,23 @@ +//! Interface for Element's which describe the contents of a `Container`. +const Cell = @import("cell.zig"); +const Size = @import("size.zig").Size; + +pub fn Element(Event: type) type { + return struct { + ptr: *anyopaque = undefined, + vtable: *const VTable = &.{}, + + pub const VTable = struct { + handle: ?*const fn (ctx: *anyopaque, event: Event) anyerror!void = null, + content: ?*const fn (ctx: *anyopaque, cells: []Cell, size: Size) anyerror!void = null, + }; + + pub inline fn handle(this: @This(), event: Event) !void { + if (this.vtable.handle) |handle_fn| try handle_fn(this.ptr, event); + } + + pub inline fn content(this: @This(), cells: []Cell, size: Size) !void { + if (this.vtable.content) |content_fn| try content_fn(this.ptr, cells, size); + } + }; +} diff --git a/src/zterm.zig b/src/zterm.zig index c30f339..725b18c 100644 --- a/src/zterm.zig +++ b/src/zterm.zig @@ -5,6 +5,9 @@ const size = @import("size.zig"); // public exports pub const App = @import("app.zig").App; +// App also exports further types once initialized with the user events at compile time: +// `App.Container` +// `App.Element` pub const Renderer = @import("render.zig"); // Container Configurations