From feae9fa1a4b3f9eb20a986e04f2552f1f07e421a Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sun, 26 Oct 2025 15:58:07 +0100 Subject: [PATCH] feat(model): implement `Elm` architecture Now the `App` contains a state which is a user-defined `struct` which is passed to the `handle` and `contents` callbacks for `Container`'s and `Element`'s. Built-in `Element`'s shall not access the `App.Model` and should therefore never cause any side-effects. User-defined events shall be used to act as *messages* to cause potential side-effects for the model. This is the reason why only the `handle` callback has a non-const pointer to the `App.Model`. The `contents` callback can only access the `App.Model` read-only to use for generating the *view* (in context of the elm architecture). --- examples/continuous.zig | 16 +- examples/demo.zig | 10 +- examples/elements/alignment.zig | 10 +- examples/elements/button.zig | 23 ++- examples/elements/input.zig | 21 +- examples/elements/progress.zig | 17 +- examples/elements/radio-button.zig | 10 +- examples/elements/scrollable.zig | 12 +- examples/elements/selection.zig | 10 +- examples/errors.zig | 16 +- examples/layouts/grid.zig | 10 +- examples/layouts/horizontal.zig | 10 +- examples/layouts/mixed.zig | 10 +- examples/layouts/vertical.zig | 10 +- examples/styles/palette.zig | 10 +- examples/styles/text.zig | 13 +- src/app.zig | 46 +++-- src/container.zig | 109 +++++----- src/element.zig | 308 ++++++++++++++++------------- src/event.zig | 17 +- src/render.zig | 6 +- src/testing.zig | 21 +- 22 files changed, 396 insertions(+), 319 deletions(-) diff --git a/examples/continuous.zig b/examples/continuous.zig index bf6e620..123df86 100644 --- a/examples/continuous.zig +++ b/examples/continuous.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -41,7 +41,7 @@ const Spinner = struct { }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -83,7 +83,7 @@ const InputField = struct { }; } - fn handle(ctx: *anyopaque, event: App.Event) !void { + fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { .key => |key| { @@ -102,7 +102,7 @@ const InputField = struct { } } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -133,7 +133,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -214,7 +214,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -230,7 +230,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -242,6 +242,6 @@ const std = @import("std"); const time = std.time; const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) { +const App = zterm.App(struct {}, union(enum) { accept: []u21, }); diff --git a/examples/demo.zig b/examples/demo.zig index 88f987b..846f9a1 100644 --- a/examples/demo.zig +++ b/examples/demo.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + pub fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -33,7 +33,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -168,7 +168,7 @@ pub fn main() !void { } // NOTE returned errors should be propagated back to the application - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -183,7 +183,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -195,4 +195,4 @@ const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); const input = zterm.input; -const App = zterm.App(union(enum) {}); +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/elements/alignment.zig b/examples/elements/alignment.zig index f42dd9b..5580a98 100644 --- a/examples/elements/alignment.zig +++ b/examples/elements/alignment.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -70,7 +70,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -85,7 +85,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -96,4 +96,4 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) {}); +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/elements/button.zig b/examples/elements/button.zig index 45772ef..2921ec5 100644 --- a/examples/elements/button.zig +++ b/examples/elements/button.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -40,7 +40,7 @@ const Clickable = struct { }; } - fn handle(ctx: *anyopaque, event: App.Event) !void { + fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { .mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) { @@ -55,7 +55,7 @@ const Clickable = struct { } } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -82,7 +82,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -119,7 +119,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -134,7 +134,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -145,7 +145,10 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) { - click: [:0]const u8, - accept, -}); +const App = zterm.App( + struct {}, + union(enum) { + click: [:0]const u8, + accept, + }, +); diff --git a/examples/elements/input.zig b/examples/elements/input.zig index e13cb28..78ec150 100644 --- a/examples/elements/input.zig +++ b/examples/elements/input.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -37,7 +37,7 @@ const MouseDraw = struct { }; } - fn handle(ctx: *anyopaque, event: App.Event) !void { + fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { .mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y }, @@ -45,7 +45,7 @@ const MouseDraw = struct { } } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); const this: *@This() = @ptrCast(@alignCast(ctx)); @@ -65,7 +65,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -133,7 +133,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -148,7 +148,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -161,6 +161,9 @@ const assert = std.debug.assert; const zterm = @import("zterm"); const Color = zterm.Color; -const App = zterm.App(union(enum) { - accept: []u8, -}); +const App = zterm.App( + struct {}, + union(enum) { + accept: []u8, + }, +); diff --git a/examples/elements/progress.zig b/examples/elements/progress.zig index 49ef44d..349c251 100644 --- a/examples/elements/progress.zig +++ b/examples/elements/progress.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -138,7 +138,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -154,7 +154,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -166,6 +166,9 @@ const std = @import("std"); const time = std.time; const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) { - progress: u8, -}); +const App = zterm.App( + struct {}, + union(enum) { + progress: u8, + }, +); diff --git a/examples/elements/radio-button.zig b/examples/elements/radio-button.zig index 54c4c6e..96ce4fc 100644 --- a/examples/elements/radio-button.zig +++ b/examples/elements/radio-button.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -67,7 +67,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -82,7 +82,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -93,6 +93,6 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) { +const App = zterm.App(struct {}, union(enum) { accept, }); diff --git a/examples/elements/scrollable.zig b/examples/elements/scrollable.zig index 663070b..aaefe2d 100644 --- a/examples/elements/scrollable.zig +++ b/examples/elements/scrollable.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -34,7 +34,7 @@ const HelloWorldText = packed struct { }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -66,7 +66,7 @@ pub fn main() !void { } const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -169,7 +169,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -184,7 +184,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -196,4 +196,4 @@ const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); const input = zterm.input; -const App = zterm.App(union(enum) {}); +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/elements/selection.zig b/examples/elements/selection.zig index d1ef776..6227278 100644 --- a/examples/elements/selection.zig +++ b/examples/elements/selection.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -67,7 +67,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -82,7 +82,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -94,4 +94,4 @@ const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); const Error = zterm.Error; -const App = zterm.App(union(enum) {}); +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/errors.zig b/examples/errors.zig index e405493..81c53e3 100644 --- a/examples/errors.zig +++ b/examples/errors.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -31,7 +31,7 @@ const InfoText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -57,7 +57,7 @@ const ErrorNotification = struct { return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } }; } - fn handle(ctx: *anyopaque, event: App.Event) !void { + fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { .key => |key| if (!key.isAscii()) return zterm.Error.TooSmall, @@ -66,7 +66,7 @@ const ErrorNotification = struct { } } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -97,7 +97,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -130,7 +130,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -145,7 +145,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -156,4 +156,4 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) {}); +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/layouts/grid.zig b/examples/layouts/grid.zig index 13098c9..f7dde7b 100644 --- a/examples/layouts/grid.zig +++ b/examples/layouts/grid.zig @@ -8,7 +8,7 @@ const QuitText = struct { }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -35,7 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -86,7 +86,7 @@ pub fn main() !void { } // NOTE returned errors should be propagated back to the application - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -101,7 +101,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -112,4 +112,4 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) {}); +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/layouts/horizontal.zig b/examples/layouts/horizontal.zig index 0e77bb1..45b3368 100644 --- a/examples/layouts/horizontal.zig +++ b/examples/layouts/horizontal.zig @@ -8,7 +8,7 @@ const QuitText = struct { }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -35,7 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -78,7 +78,7 @@ pub fn main() !void { } // NOTE returned errors should be propagated back to the application - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -93,7 +93,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -104,4 +104,4 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) {}); +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/layouts/mixed.zig b/examples/layouts/mixed.zig index bd3ca37..58d7fa5 100644 --- a/examples/layouts/mixed.zig +++ b/examples/layouts/mixed.zig @@ -8,7 +8,7 @@ const QuitText = struct { }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -35,7 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -94,7 +94,7 @@ pub fn main() !void { } // NOTE returned errors should be propagated back to the application - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -109,7 +109,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -120,4 +120,4 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) {}); +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/layouts/vertical.zig b/examples/layouts/vertical.zig index e852582..7a16505 100644 --- a/examples/layouts/vertical.zig +++ b/examples/layouts/vertical.zig @@ -8,7 +8,7 @@ const QuitText = struct { }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -35,7 +35,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -77,7 +77,7 @@ pub fn main() !void { } // NOTE returned errors should be propagated back to the application - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -92,7 +92,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -103,4 +103,4 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) {}); +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/styles/palette.zig b/examples/styles/palette.zig index 2545be4..4ce9d25 100644 --- a/examples/styles/palette.zig +++ b/examples/styles/palette.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -32,7 +32,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -73,7 +73,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -88,7 +88,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -99,4 +99,4 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) {}); +const App = zterm.App(struct {}, union(enum) {}); diff --git a/examples/styles/text.zig b/examples/styles/text.zig index b809053..2dd178c 100644 --- a/examples/styles/text.zig +++ b/examples/styles/text.zig @@ -5,7 +5,7 @@ const QuitText = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -31,7 +31,7 @@ const TextStyles = struct { return .{ .ptr = this, .vtable = &.{ .content = content } }; } - fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { @setEvalBranchQuota(10000); _ = ctx; assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -83,7 +83,7 @@ pub fn main() !void { const allocator = gpa.allocator(); - var app: App = .init; + var app: App = .init(.{}); var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); @@ -128,7 +128,7 @@ pub fn main() !void { else => {}, } - container.handle(event) catch |err| app.postEvent(.{ + container.handle(&app.model, event) catch |err| app.postEvent(.{ .err = .{ .err = err, .msg = "Container Event handling failed", @@ -143,7 +143,7 @@ pub fn main() !void { container.resize(try renderer.resize()); container.reposition(.{}); - try renderer.render(@TypeOf(container), &container); + try renderer.render(@TypeOf(container), &container, App.Model, &app.model); try renderer.flush(); } } @@ -154,4 +154,5 @@ const log = std.log.scoped(.default); const std = @import("std"); const assert = std.debug.assert; const zterm = @import("zterm"); -const App = zterm.App(union(enum) {}); + +const App = zterm.App(struct {}, union(enum) {}); diff --git a/src/app.zig b/src/app.zig index 83ed900..249b3c2 100644 --- a/src/app.zig +++ b/src/app.zig @@ -12,18 +12,19 @@ /// ```zig /// const zterm = @import("zterm"); /// const App = zterm.App( -/// union(enum) {}, +/// struct {}, // empty model +/// union(enum) {}, // no additional user event's /// ); /// // later on create an `App` instance and start the event loop -/// var app: App = .init; +/// var app: App = .init(.{}); // provide instance of the model that shall be used /// try app.start(); -/// defer app.stop() catch unreachable; +/// defer app.stop() catch unreachable; // does not clean-up the resources used in the model /// ``` -pub fn App(comptime E: type) type { - if (!isTaggedUnion(E)) { - @compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`."); - } +pub fn App(comptime M: type, comptime E: type) type { + if (!isStruct(M)) @compileError("Provided model `M` for `App(comptime M: type, comptime E: type)` is not of type `struct`"); + if (!isTaggedUnion(E)) @compileError("Provided user event `E` for `App(comptime M: type, comptime E: type)` is not of type `union(enum)`."); return struct { + model: Model, queue: Queue, thread: ?Thread = null, quit_event: Thread.ResetEvent, @@ -40,10 +41,13 @@ pub fn App(comptime E: type) type { this.postEvent(.resize); } - pub const init: @This() = .{ - .queue = .{}, - .quit_event = .{}, - }; + pub fn init(model: Model) @This() { + return .{ + .model = model, + .queue = .{}, + .quit_event = .{}, + }; + } pub fn start(this: *@This()) !void { if (this.thread) |_| return; @@ -408,16 +412,17 @@ pub fn App(comptime E: type) type { } const element = @import("element.zig"); + pub const Model = M; pub const Event = mergeTaggedUnions(event.SystemEvent, E); - pub const Container = @import("container.zig").Container(Event); - pub const Element = element.Element(Event); - pub const Alignment = element.Alignment(Event); - pub const Button = element.Button(Event, Queue); - pub const Input = element.Input(Event, Queue); - pub const Progress = element.Progress(Event, Queue); - pub const RadioButton = element.RadioButton(Event); - pub const Scrollable = element.Scrollable(Event); - pub const Selection = element.Selection(Event); + pub const Container = @import("container.zig").Container(Model, Event); + pub const Element = element.Element(Model, Event); + pub const Alignment = element.Alignment(Model, Event); + pub const Button = element.Button(Model, Event, Queue); + pub const Input = element.Input(Model, Event, Queue); + pub const Progress = element.Progress(Model, Event, Queue); + pub const RadioButton = element.RadioButton(Model, Event); + pub const Scrollable = element.Scrollable(Model, Event); + pub const Selection = element.Selection(Model, Event); pub const Queue = queue.Queue(Event, 256); }; } @@ -436,6 +441,7 @@ const terminal = @import("terminal.zig"); const queue = @import("queue.zig"); const mergeTaggedUnions = event.mergeTaggedUnions; const isTaggedUnion = event.isTaggedUnion; +const isStruct = event.isStruct; const Mouse = input.Mouse; const Key = input.Key; const Point = @import("point.zig").Point; diff --git a/src/container.zig b/src/container.zig index dcd112a..1a37f8d 100644 --- a/src/container.zig +++ b/src/container.zig @@ -80,8 +80,9 @@ pub const Border = packed struct { test "all sides" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .border = .{ .color = .green, .sides = .all, @@ -92,14 +93,15 @@ pub const Border = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/border.all.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/border.all.zon")); } test "vertical sides" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .border = .{ .color = .green, .sides = .vertical, @@ -110,14 +112,15 @@ pub const Border = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/border.vertical.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/border.vertical.zon")); } test "horizontal sides" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .border = .{ .color = .green, .sides = .horizontal, @@ -128,7 +131,7 @@ pub const Border = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/border.horizontal.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/border.horizontal.zon")); } }; @@ -160,8 +163,9 @@ pub const Rectangle = packed struct { test "fill color overwrite parent fill" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .green }, }, .{}); try container.append(try .init(std.testing.allocator, .{ @@ -173,14 +177,15 @@ pub const Rectangle = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/rectangle_with_parent_fill_without_padding.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_fill_without_padding.zon")); } test "fill color padding to show parent fill" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .layout = .{ .padding = .all(2), }, @@ -195,14 +200,15 @@ pub const Rectangle = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/rectangle_with_parent_padding.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_padding.zon")); } test "fill color padding to show parent fill (negative padding)" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .layout = .{ .padding = .{ .top = -18, @@ -222,14 +228,15 @@ pub const Rectangle = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/rectangle_with_parent_padding.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_padding.zon")); } test "fill color spacer with padding" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .black, }, @@ -250,14 +257,15 @@ pub const Rectangle = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/rectangle_with_padding.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_padding.zon")); } test "fill color with gap" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .black, }, @@ -281,14 +289,15 @@ pub const Rectangle = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/rectangle_with_gap.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_gap.zon")); } test "fill color with separator" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .black, }, @@ -313,7 +322,7 @@ pub const Rectangle = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/rectangle_with_separator.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_separator.zon")); } }; @@ -403,8 +412,9 @@ pub const Layout = packed struct { test "separator without gaps" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .layout = .{ .separator = .{ .enabled = true, @@ -418,14 +428,15 @@ pub const Layout = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/separator_no_gaps.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/separator_no_gaps.zon")); } test "separator without gaps with padding" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .layout = .{ .padding = .all(1), .separator = .{ @@ -440,14 +451,15 @@ pub const Layout = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/separator_no_gaps_with_padding.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/separator_no_gaps_with_padding.zon")); } test "separator(2x) without gaps" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .layout = .{ .direction = .vertical, .separator = .{ @@ -464,14 +476,15 @@ pub const Layout = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/separator_2x_no_gaps.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps.zon")); } test "separator(2x) with border(all)" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .border = .{ .color = .red, .sides = .all, @@ -491,14 +504,15 @@ pub const Layout = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/separator_2x_no_gaps_with_border.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps_with_border.zon")); } test "separator(2x) with border(all) and padding(all(1))" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .border = .{ .color = .red, .sides = .all, @@ -519,14 +533,15 @@ pub const Layout = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/separator_2x_no_gaps_with_padding.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps_with_padding.zon")); } test "separator(2x) with border(all) and gap" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .border = .{ .color = .red, .sides = .all, @@ -547,14 +562,15 @@ pub const Layout = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/separator_2x_with_gaps_with_border.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/separator_2x_with_gaps_with_border.zon")); } test "separator(2x) with border(all) and gap and padding" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .border = .{ .color = .red, .sides = .all, @@ -576,7 +592,7 @@ pub const Layout = packed struct { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon")); } }; @@ -591,10 +607,10 @@ pub const Size = packed struct { } = .both, }; -pub fn Container(comptime Event: type) type { +pub fn Container(Model: type, Event: type) type { if (!isTaggedUnion(Event)) @compileError("Provided user event `Event` for `Container(comptime Event: type)`"); - const Element = @import("element.zig").Element(Event); + const Element = @import("element.zig").Element(Model, Event); return struct { allocator: Allocator, origin: Point, @@ -889,7 +905,7 @@ pub fn Container(comptime Event: type) type { this.grow_resize(this.size); } - pub fn handle(this: *const @This(), event: Event) !void { + pub fn handle(this: *const @This(), model: *Model, event: Event) !void { switch (event) { .mouse => |mouse| if (mouse.in(this.origin, this.size)) { // the element receives the mouse event with relative position @@ -897,17 +913,14 @@ pub fn Container(comptime Event: type) type { var relative_mouse: input.Mouse = mouse; relative_mouse.x -= this.origin.x; relative_mouse.y -= this.origin.y; - try this.element.handle(.{ .mouse = relative_mouse }); - for (this.elements.items) |*element| try element.handle(event); - }, - else => { - try this.element.handle(event); - for (this.elements.items) |*element| try element.handle(event); + try this.element.handle(model, .{ .mouse = relative_mouse }); }, + else => try this.element.handle(model, event), } + for (this.elements.items) |*element| try element.handle(model, event); } - pub fn content(this: *const @This()) ![]Cell { + pub fn content(this: *const @This(), model: *const Model) ![]Cell { if (this.size.x == 0 or this.size.y == 0) return Error.TooSmall; const cells = try this.allocator.alloc(Cell, @as(usize, this.size.x) * @as(usize, this.size.y)); @@ -918,7 +931,7 @@ pub fn Container(comptime Event: type) type { this.properties.border.content(cells, this.size); this.properties.rectangle.content(cells, this.size); - try this.element.content(cells, this.size); + try this.element.content(model, cells, this.size); // DEBUG render corresponding corners (except top left) of this `Container` *red* if (comptime build_options.debug) { @@ -962,8 +975,9 @@ test { test "Container Fixed and Grow Size Vertical" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ .layout = .{ .direction = .vertical }, }, .{}); try container.append(try .init(std.testing.allocator, .{ @@ -981,14 +995,15 @@ test "Container Fixed and Grow Size Vertical" { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/fixed_grow_vertical.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/fixed_grow_vertical.zon")); } test "Container Fixed and Grow Size Horizontal" { const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{}, .{}); + var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{}, .{}); try container.append(try .init(std.testing.allocator, .{ .size = .{ .dim = .{ .x = 5 }, @@ -1004,5 +1019,5 @@ test "Container Fixed and Grow Size Horizontal" { try testing.expectContainerScreen(.{ .y = 20, .x = 30, - }, &container, @import("test/container/fixed_grow_horizontal.zon")); + }, @TypeOf(container), &container, Model, @import("test/container/fixed_grow_horizontal.zon")); } diff --git a/src/element.zig b/src/element.zig index 72e8805..90b178e 100644 --- a/src/element.zig +++ b/src/element.zig @@ -9,7 +9,7 @@ // FIX known issues: // - hold fewer instances of the `Allocator` -pub fn Element(Event: type) type { +pub fn Element(Model: type, Event: type) type { return struct { ptr: *anyopaque = undefined, vtable: *const VTable = &.{}, @@ -17,8 +17,8 @@ pub fn Element(Event: type) type { pub const VTable = struct { resize: ?*const fn (ctx: *anyopaque, size: Point) void = null, reposition: ?*const fn (ctx: *anyopaque, origin: Point) void = null, - handle: ?*const fn (ctx: *anyopaque, event: Event) anyerror!void = null, - content: ?*const fn (ctx: *anyopaque, cells: []Cell, size: Point) anyerror!void = null, + handle: ?*const fn (ctx: *anyopaque, model: *Model, event: Event) anyerror!void = null, + content: ?*const fn (ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) anyerror!void = null, }; /// Resize the corresponding `Element` with the given *size*. @@ -34,15 +34,16 @@ pub fn Element(Event: type) type { } /// Handle the received event. The event is one of the user provided - /// events or a system event, with the exception of the `.size` - /// `Event` as every `Container` already handles that event. + /// events or a system event. The model can be updated through the + /// provided pointer. /// /// In case of user errors this function should return an error. This /// error may then be used by the application to display information - /// about the user error. - pub inline fn handle(this: @This(), event: Event) !void { + /// about the error to the user and is left intentionally up to the + /// implementation to decide. + pub inline fn handle(this: @This(), model: *Model, event: Event) !void { if (this.vtable.handle) |handle_fn| - try handle_fn(this.ptr, event); + try handle_fn(this.ptr, model, event); } /// Write content into the `cells` of the `Container`. The associated @@ -51,7 +52,7 @@ pub fn Element(Event: type) type { /// /// # Note /// - /// - Caller owns `cells` slice and ensures that the size usually by assertion: + /// - Caller owns `cells` slice and ensures the size usually by assertion: /// ```zig /// std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); /// ``` @@ -60,9 +61,21 @@ pub fn Element(Event: type) type { /// non-recoverable (i.e. an allocation error, system error, etc.). /// Otherwise user specific errors should be caught using the `handle` /// function before the rendering of the `Container` happens. - pub inline fn content(this: @This(), cells: []Cell, size: Point) !void { + /// + /// - The provided model may be used as a read-only reference to create + /// the contents for the `cells`. Changes should only be done through + /// the `handle` callback. + /// + /// - When facing layout problems it might help to enable the + /// build option for debug rendering, which renders corresponding + /// placeholders. + /// - **e** for `Element` + /// - **c** for `Container` + /// - **s** for `Separator` + /// - **r** for `Rectangle` + pub inline fn content(this: @This(), model: *const Model, cells: []Cell, size: Point) !void { if (this.vtable.content) |content_fn| { - try content_fn(this.ptr, cells, size); + try content_fn(this.ptr, model, cells, size); // DEBUG render corresponding top left corner of this `Element` *red* // - only rendered if the corresponding associated element renders contents into the `Container` @@ -74,16 +87,16 @@ pub fn Element(Event: type) type { } } }; -} // Element(Event: type) +} // Element(Model: type, Event: type) -pub fn Alignment(Event: type) type { +pub fn Alignment(Model: type, Event: type) type { return struct { /// `Size` of the actual contents that should be aligned. size: Point = .{}, /// Alignment Configuration to use for aligning the associated contents of this `Element`. configuration: Configuration, /// `Container` to render in alignment. It needs to use the sizing options accordingly to be effective. - container: Container(Event), + container: Container(Model, Event), /// Configuration for Alignment pub const Configuration = packed struct { @@ -105,14 +118,14 @@ pub fn Alignment(Event: type) type { pub const center: @This() = .{ .v = .center, .h = .center }; }; - pub fn init(container: Container(Event), configuration: Configuration) @This() { + pub fn init(container: Container(Model, Event), configuration: Configuration) @This() { return .{ .container = container, .configuration = configuration, }; } - pub fn element(this: *@This()) Element(Event) { + pub fn element(this: *@This()) Element(Model, Event) { return .{ .ptr = this, .vtable = &.{ @@ -146,18 +159,18 @@ pub fn Alignment(Event: type) type { this.container.reposition(origin); } - fn handle(ctx: *anyopaque, event: Event) !void { + fn handle(ctx: *anyopaque, model: *Model, event: Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); - try this.container.handle(event); + try this.container.handle(model, event); } - fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void { + fn content(ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); const origin = this.container.origin; const csize = this.container.size; - const container_cells = try this.container.content(); + const container_cells = try this.container.content(model); defer this.container.allocator.free(container_cells); outer: for (0..csize.y) |row| { @@ -171,9 +184,9 @@ pub fn Alignment(Event: type) type { } } }; -} // Alignment(Event: type) +} // Alignment(Model: type, Event: type) -pub fn Scrollable(Event: type) type { +pub fn Scrollable(Model: type, Event: type) type { return struct { /// `Size` of the actual contents where the anchor and the size is /// representing the size and location on screen. @@ -184,7 +197,7 @@ pub fn Scrollable(Event: type) type { /// Anchor of the viewport of the scrollable `Container`. anchor: Point = .{}, /// The actual `Container`, that is scrollable. - container: Container(Event), + container: Container(Model, Event), /// Whether the scrollable contents should show a scroll bar or not. /// The scroll bar will only be shown if required and enabled. With the /// corresponding provided color if to be shown. @@ -204,14 +217,14 @@ pub fn Scrollable(Event: type) type { } }; - pub fn init(container: Container(Event), configuration: Configuration) @This() { + pub fn init(container: Container(Model, Event), configuration: Configuration) @This() { return .{ .container = container, .configuration = configuration, }; } - pub fn element(this: *@This()) Element(Event) { + pub fn element(this: *@This()) Element(Model, Event) { return .{ .ptr = this, .vtable = &.{ @@ -255,7 +268,7 @@ pub fn Scrollable(Event: type) type { this.container.reposition(.{}); } - fn handle(ctx: *anyopaque, event: Event) !void { + fn handle(ctx: *anyopaque, model: *Model, event: Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { // TODO other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?) @@ -274,7 +287,7 @@ pub fn Scrollable(Event: type) type { const max_anchor_x = this.container_size.x -| this.size.x; this.anchor.x = @min(this.anchor.x + 1, max_anchor_x); }, - else => try this.container.handle(.{ + else => try this.container.handle(model, .{ .mouse = .{ .x = mouse.x + this.anchor.x, .y = mouse.y + this.anchor.y, @@ -283,14 +296,14 @@ pub fn Scrollable(Event: type) type { }, }), }, - else => try this.container.handle(event), + else => try this.container.handle(model, event), } } - fn render_container(container: Container(Event), cells: []Cell, container_size: Point) !void { + fn render_container(container: Container(Model, Event), model: *const Model, cells: []Cell, container_size: Point) !void { const size = container.size; const origin = container.origin; - const contents = try container.content(); + const contents = try container.content(model); const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x); @@ -306,10 +319,10 @@ pub fn Scrollable(Event: type) type { // free immediately container.allocator.free(contents); - for (container.elements.items) |child| try render_container(child, cells, size); + for (container.elements.items) |child| try render_container(child, model, cells, size); } - fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void { + fn content(ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, this.size.x) * @as(usize, this.size.y)); @@ -317,9 +330,9 @@ pub fn Scrollable(Event: type) type { const offset_y: usize = if (this.configuration.y_axis) 1 else 0; const container_size = this.container.size; - const container_cells = try this.container.content(); + const container_cells = try this.container.content(model); - for (this.container.elements.items) |child| try render_container(child, container_cells, container_size); + for (this.container.elements.items) |child| try render_container(child, model, container_cells, container_size); const anchor = (@as(usize, this.anchor.y) * @as(usize, container_size.x)) + @as(usize, this.anchor.x); for (0..size.y - offset_y) |row| { @@ -368,12 +381,12 @@ pub fn Scrollable(Event: type) type { this.container.allocator.free(container_cells); } }; -} // Scrollable(Event: type) +} // Scrollable(Model: type, Event: type) // TODO features // - clear input (with and without retaining of capacity) through an public api // - make handle / content functions public -pub fn Input(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { +pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { // NOTE the struct is necessary, as otherwise I cannot point to the function I want to return const input_struct = struct { pub fn input_fn(accept_event: meta.FieldEnum(Event)) type { @@ -430,7 +443,7 @@ pub fn Input(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { this.input.deinit(this.allocator); } - pub fn element(this: *@This()) Element(Event) { + pub fn element(this: *@This()) Element(Model, Event) { return .{ .ptr = this, .vtable = &.{ @@ -440,7 +453,7 @@ pub fn Input(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { }; } - fn handle(ctx: *anyopaque, event: Event) !void { + fn handle(ctx: *anyopaque, _: *Model, event: Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { .key => |key| { @@ -554,7 +567,7 @@ pub fn Input(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { } } - fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void { + fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -581,9 +594,9 @@ pub fn Input(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { } }; return input_struct.input_fn; -} // Input(Event: type, Queue: type) +} // Input(Model: type, Event: type, Queue: type) -pub fn Button(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { +pub fn Button(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { // NOTE the struct is necessary, as otherwise I cannot point to the function I want to return const button_struct = struct { pub fn button_fn(accept_event: meta.FieldEnum(Event)) type { @@ -618,7 +631,7 @@ pub fn Button(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { }; } - pub fn element(this: *@This()) Element(Event) { + pub fn element(this: *@This()) Element(Model, Event) { return .{ .ptr = this, .vtable = &.{ @@ -628,7 +641,7 @@ pub fn Button(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { }; } - fn handle(ctx: *anyopaque, event: Event) !void { + fn handle(ctx: *anyopaque, _: *Model, event: Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { // TODO should this also support key presses to accept? @@ -637,7 +650,7 @@ pub fn Button(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { } } - fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void { + fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -659,9 +672,9 @@ pub fn Button(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { } }; return button_struct.button_fn; -} // Button(Event: type, Queue: type) +} // Button(Model: type, Event: type, Queue: type) -pub fn RadioButton(Event: type) type { +pub fn RadioButton(Model: type, Event: type) type { return struct { configuration: Configuration, value: bool, @@ -683,7 +696,7 @@ pub fn RadioButton(Event: type) type { }; } - pub fn element(this: *@This()) Element(Event) { + pub fn element(this: *@This()) Element(Model, Event) { return .{ .ptr = this, .vtable = &.{ @@ -693,7 +706,7 @@ pub fn RadioButton(Event: type) type { }; } - fn handle(ctx: *anyopaque, event: Event) !void { + fn handle(ctx: *anyopaque, _: *Model, event: Event) !void { var this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { // TODO should this also support key presses to accept? @@ -704,7 +717,7 @@ pub fn RadioButton(Event: type) type { } } - fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void { + fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -723,9 +736,9 @@ pub fn RadioButton(Event: type) type { } } }; -} // RadioButton(Event: type) +} // RadioButton(Model: type, Event: type) -pub fn Selection(Event: type) fn (type) type { +pub fn Selection(Model: type, Event: type) fn (type) type { const selection_struct = struct { pub fn selection_fn(Enum: type) type { switch (@typeInfo(Enum)) { @@ -751,7 +764,7 @@ pub fn Selection(Event: type) fn (type) type { }; } - pub fn element(this: *@This()) Element(Event) { + pub fn element(this: *@This()) Element(Model, Event) { return .{ .ptr = this, .vtable = &.{ @@ -761,7 +774,7 @@ pub fn Selection(Event: type) fn (type) type { }; } - fn handle(ctx: *anyopaque, event: Event) !void { + fn handle(ctx: *anyopaque, _: *Model, event: Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { .mouse => |mouse| if (mouse.y == 0) { @@ -840,7 +853,7 @@ pub fn Selection(Event: type) fn (type) type { } } - fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void { + fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); if (this.configuration.label.len + 4 + this.width >= cells.len) return Error.TooSmall; @@ -865,9 +878,9 @@ pub fn Selection(Event: type) fn (type) type { } }; return selection_struct.selection_fn; -} // Selection(Enum: type) +} // Selection(Model: type, Event: type) -pub fn Progress(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { +pub fn Progress(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { // NOTE the struct is necessary, as otherwise I cannot point to the function I want to return const progress_struct = struct { pub fn progress_fn(progress_event: meta.FieldEnum(Event)) type { @@ -910,7 +923,7 @@ pub fn Progress(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { }; } - pub fn element(this: *@This()) Element(Event) { + pub fn element(this: *@This()) Element(Model, Event) { return .{ .ptr = this, .vtable = &.{ @@ -920,7 +933,7 @@ pub fn Progress(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { }; } - fn handle(ctx: *anyopaque, event: Event) !void { + fn handle(ctx: *anyopaque, _: *Model, event: Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); // TODO should this `Element` trigger a completion event? (I don't think that this is useful?) switch (event) { @@ -932,7 +945,7 @@ pub fn Progress(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { } } - fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void { + fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); @@ -965,7 +978,7 @@ pub fn Progress(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { } }; return progress_struct.progress_fn; -} // Progress(Event: type, Queue: type) +} // Progress(Model: type, Event: type, Queue: type) const std = @import("std"); const assert = std.debug.assert; @@ -990,8 +1003,10 @@ test "scrollable vertical" { .x = 30, .y = 20, }; + const Model = struct {}; + var model: Model = .{}; - var box: Container(event.SystemEvent) = try .init(allocator, .{ + var box: Container(Model, event.SystemEvent) = try .init(allocator, .{ .border = .{ .sides = .all, .color = .red, @@ -1016,9 +1031,9 @@ test "scrollable vertical" { }, .{})); defer box.deinit(); - var scrollable: Scrollable(event.SystemEvent) = .init(box, .disabled); + var scrollable: Scrollable(Model, event.SystemEvent) = .init(box, .disabled); - var container: Container(event.SystemEvent) = try .init(allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(allocator, .{ .border = .{ .color = .green, .sides = .vertical, @@ -1031,11 +1046,11 @@ test "scrollable vertical" { container.resize(size); container.reposition(.{}); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen); // scroll down 15 times (exactly to the end) - for (0..15) |_| try container.handle(.{ + for (0..15) |_| try container.handle(&model, .{ .mouse = .{ .button = .wheel_down, .kind = .press, @@ -1043,11 +1058,11 @@ test "scrollable vertical" { .y = 5, }, }); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen); // further scrolling down will not change anything - try container.handle(.{ + try container.handle(&model, .{ .mouse = .{ .button = .wheel_down, .kind = .press, @@ -1055,7 +1070,7 @@ test "scrollable vertical" { .y = 5, }, }); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen); } @@ -1068,8 +1083,10 @@ test "scrollable vertical with scrollbar" { .x = 30, .y = 20, }; + const Model = struct {}; + var model: Model = .{}; - var box: Container(event.SystemEvent) = try .init(allocator, .{ + var box: Container(Model, event.SystemEvent) = try .init(allocator, .{ .border = .{ .sides = .all, .color = .red, @@ -1094,9 +1111,9 @@ test "scrollable vertical with scrollbar" { }, .{})); defer box.deinit(); - var scrollable: Scrollable(event.SystemEvent) = .init(box, .enabled(.white)); + var scrollable: Scrollable(Model, event.SystemEvent) = .init(box, .enabled(.white)); - var container: Container(event.SystemEvent) = try .init(allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(allocator, .{ .border = .{ .color = .green, .sides = .vertical, @@ -1109,11 +1126,11 @@ test "scrollable vertical with scrollbar" { container.resize(size); container.reposition(.{}); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.top.zon"), renderer.screen); // scroll down 15 times (exactly to the end) - for (0..15) |_| try container.handle(.{ + for (0..15) |_| try container.handle(&model, .{ .mouse = .{ .button = .wheel_down, .kind = .press, @@ -1121,11 +1138,11 @@ test "scrollable vertical with scrollbar" { .y = 5, }, }); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.bottom.zon"), renderer.screen); // further scrolling down will not change anything - try container.handle(.{ + try container.handle(&model, .{ .mouse = .{ .button = .wheel_down, .kind = .press, @@ -1133,7 +1150,7 @@ test "scrollable vertical with scrollbar" { .y = 5, }, }); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.bottom.zon"), renderer.screen); } @@ -1146,8 +1163,10 @@ test "scrollable horizontal" { .x = 30, .y = 20, }; + const Model = struct {}; + var model: Model = .{}; - var box: Container(event.SystemEvent) = try .init(allocator, .{ + var box: Container(Model, event.SystemEvent) = try .init(allocator, .{ .border = .{ .sides = .all, .color = .red, @@ -1172,9 +1191,9 @@ test "scrollable horizontal" { }, .{})); defer box.deinit(); - var scrollable: Scrollable(event.SystemEvent) = .init(box, .disabled); + var scrollable: Scrollable(Model, event.SystemEvent) = .init(box, .disabled); - var container: Container(event.SystemEvent) = try .init(allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(allocator, .{ .border = .{ .color = .green, .sides = .horizontal, @@ -1187,11 +1206,11 @@ test "scrollable horizontal" { container.resize(size); container.reposition(.{}); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen); // scroll right 15 times (exactly to the end) - for (0..15) |_| try container.handle(.{ + for (0..15) |_| try container.handle(&model, .{ .mouse = .{ .button = .wheel_right, .kind = .press, @@ -1199,11 +1218,11 @@ test "scrollable horizontal" { .y = 5, }, }); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen); // further scrolling right will not change anything - try container.handle(.{ + try container.handle(&model, .{ .mouse = .{ .button = .wheel_right, .kind = .press, @@ -1211,7 +1230,7 @@ test "scrollable horizontal" { .y = 5, }, }); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen); } @@ -1224,8 +1243,10 @@ test "scrollable horizontal with scrollbar" { .x = 30, .y = 20, }; + const Model = struct {}; + var model: Model = .{}; - var box: Container(event.SystemEvent) = try .init(allocator, .{ + var box: Container(Model, event.SystemEvent) = try .init(allocator, .{ .border = .{ .sides = .all, .color = .red, @@ -1250,9 +1271,9 @@ test "scrollable horizontal with scrollbar" { }, .{})); defer box.deinit(); - var scrollable: Scrollable(event.SystemEvent) = .init(box, .enabled(.white)); + var scrollable: Scrollable(Model, event.SystemEvent) = .init(box, .enabled(.white)); - var container: Container(event.SystemEvent) = try .init(allocator, .{ + var container: Container(Model, event.SystemEvent) = try .init(allocator, .{ .border = .{ .color = .green, .sides = .horizontal, @@ -1265,11 +1286,11 @@ test "scrollable horizontal with scrollbar" { container.resize(size); container.reposition(.{}); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.left.zon"), renderer.screen); // scroll right 15 times (exactly to the end) - for (0..15) |_| try container.handle(.{ + for (0..15) |_| try container.handle(&model, .{ .mouse = .{ .button = .wheel_right, .kind = .press, @@ -1277,11 +1298,11 @@ test "scrollable horizontal with scrollbar" { .y = 5, }, }); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.right.zon"), renderer.screen); // further scrolling right will not change anything - try container.handle(.{ + try container.handle(&model, .{ .mouse = .{ .button = .wheel_right, .kind = .press, @@ -1289,7 +1310,7 @@ test "scrollable horizontal with scrollbar" { .y = 5, }, }); - try renderer.render(Container(event.SystemEvent), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.right.zon"), renderer.screen); } @@ -1297,11 +1318,12 @@ test "alignment center" { const allocator = std.testing.allocator; const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(allocator, .{}, .{}); + var container: Container(Model, event.SystemEvent) = try .init(allocator, .{}, .{}); defer container.deinit(); - var aligned_container: Container(event.SystemEvent) = try .init(allocator, .{ + var aligned_container: Container(Model, event.SystemEvent) = try .init(allocator, .{ .rectangle = .{ .fill = .green }, .size = .{ .dim = .{ .x = 12, .y = 5 }, @@ -1310,24 +1332,25 @@ test "alignment center" { }, .{}); defer aligned_container.deinit(); - var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .center); + var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .center); try container.append(try .init(allocator, .{}, alignment.element())); try testing.expectContainerScreen(.{ .x = 30, .y = 20, - }, &container, @import("test/element/alignment.center.zon")); + }, @TypeOf(container), &container, Model, @import("test/element/alignment.center.zon")); } test "alignment left" { const allocator = std.testing.allocator; const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(allocator, .{}, .{}); + var container: Container(Model, event.SystemEvent) = try .init(allocator, .{}, .{}); defer container.deinit(); - var aligned_container: Container(event.SystemEvent) = try .init(allocator, .{ + var aligned_container: Container(Model, event.SystemEvent) = try .init(allocator, .{ .rectangle = .{ .fill = .green }, .size = .{ .dim = .{ .x = 12, .y = 5 }, @@ -1336,7 +1359,7 @@ test "alignment left" { }, .{}); defer aligned_container.deinit(); - var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{ + var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{ .h = .start, .v = .center, }); @@ -1345,18 +1368,19 @@ test "alignment left" { try testing.expectContainerScreen(.{ .x = 30, .y = 20, - }, &container, @import("test/element/alignment.left.zon")); + }, @TypeOf(container), &container, Model, @import("test/element/alignment.left.zon")); } test "alignment right" { const allocator = std.testing.allocator; const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(allocator, .{}, .{}); + var container: Container(Model, event.SystemEvent) = try .init(allocator, .{}, .{}); defer container.deinit(); - var aligned_container: Container(event.SystemEvent) = try .init(allocator, .{ + var aligned_container: Container(Model, event.SystemEvent) = try .init(allocator, .{ .rectangle = .{ .fill = .green }, .size = .{ .dim = .{ .x = 12, .y = 5 }, @@ -1365,7 +1389,7 @@ test "alignment right" { }, .{}); defer aligned_container.deinit(); - var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{ + var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{ .h = .end, .v = .center, }); @@ -1374,18 +1398,19 @@ test "alignment right" { try testing.expectContainerScreen(.{ .x = 30, .y = 20, - }, &container, @import("test/element/alignment.right.zon")); + }, @TypeOf(container), &container, Model, @import("test/element/alignment.right.zon")); } test "alignment top" { const allocator = std.testing.allocator; const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(allocator, .{}, .{}); + var container: Container(Model, event.SystemEvent) = try .init(allocator, .{}, .{}); defer container.deinit(); - var aligned_container: Container(event.SystemEvent) = try .init(allocator, .{ + var aligned_container: Container(Model, event.SystemEvent) = try .init(allocator, .{ .rectangle = .{ .fill = .green }, .size = .{ .dim = .{ .x = 12, .y = 5 }, @@ -1394,7 +1419,7 @@ test "alignment top" { }, .{}); defer aligned_container.deinit(); - var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{ + var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{ .h = .center, .v = .start, }); @@ -1403,18 +1428,19 @@ test "alignment top" { try testing.expectContainerScreen(.{ .x = 30, .y = 20, - }, &container, @import("test/element/alignment.top.zon")); + }, @TypeOf(container), &container, Model, @import("test/element/alignment.top.zon")); } test "alignment bottom" { const allocator = std.testing.allocator; const event = @import("event.zig"); const testing = @import("testing.zig"); + const Model = struct {}; - var container: Container(event.SystemEvent) = try .init(allocator, .{}, .{}); + var container: Container(Model, event.SystemEvent) = try .init(allocator, .{}, .{}); defer container.deinit(); - var aligned_container: Container(event.SystemEvent) = try .init(allocator, .{ + var aligned_container: Container(Model, event.SystemEvent) = try .init(allocator, .{ .rectangle = .{ .fill = .green }, .size = .{ .dim = .{ .x = 12, .y = 5 }, @@ -1423,7 +1449,7 @@ test "alignment bottom" { }, .{}); defer aligned_container.deinit(); - var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{ + var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{ .h = .center, .v = .end, }); @@ -1432,7 +1458,7 @@ test "alignment bottom" { try testing.expectContainerScreen(.{ .x = 30, .y = 20, - }, &container, @import("test/element/alignment.bottom.zon")); + }, @TypeOf(container), &container, Model, @import("test/element/alignment.bottom.zon")); } test "input element" { @@ -1444,8 +1470,10 @@ test "input element" { }); const testing = @import("testing.zig"); const Queue = @import("queue.zig").Queue(Event, 256); + const Model = struct {}; + var model: Model = .{}; - var container: Container(Event) = try .init(allocator, .{}, .{}); + var container: Container(Model, Event) = try .init(allocator, .{}, .{}); defer container.deinit(); const size: Point = .{ @@ -1454,10 +1482,10 @@ test "input element" { }; var queue: Queue = .{}; - var input_element: Input(Event, Queue)(.accept) = .init(allocator, &queue, .init(.black)); + var input_element: Input(Model, Event, Queue)(.accept) = .init(allocator, &queue, .init(.black)); defer input_element.deinit(); - const input_container: Container(Event) = try .init(allocator, .{ + const input_container: Container(Model, Event) = try .init(allocator, .{ .rectangle = .{ .fill = .green }, .size = .{ .dim = .{ .x = 12, .y = 2 }, @@ -1472,25 +1500,25 @@ test "input element" { container.resize(size); container.reposition(.{}); - try renderer.render(Container(Event), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.without.text.zon"), renderer.screen); // press 'a' 15 times - for (0..15) |_| try container.handle(.{ + for (0..15) |_| try container.handle(&model, .{ .key = .{ .cp = 'a' }, }); - try renderer.render(Container(Event), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.zon"), renderer.screen); // press 'a' 15 times - for (0..15) |_| try container.handle(.{ + for (0..15) |_| try container.handle(&model, .{ .key = .{ .cp = 'a' }, }); - try renderer.render(Container(Event), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.overflow.zon"), renderer.screen); // test the accepting of the `Element` - try container.handle(.{ + try container.handle(&model, .{ .key = .{ .cp = input.Enter }, }); const accept_event = queue.pop(); @@ -1521,12 +1549,14 @@ test "button" { .y = 20, }; var queue: Queue = .{}; + const Model = struct {}; + var model: Model = .{}; - var container: Container(Event) = try .init(allocator, .{}, .{}); + var container: Container(Model, Event) = try .init(allocator, .{}, .{}); defer container.deinit(); - var button: Button(Event, Queue)(.accept) = .init(&queue, .init(.default, "Button")); - const button_container: Container(Event) = try .init(allocator, .{ + var button: Button(Model, Event, Queue)(.accept) = .init(&queue, .init(.default, "Button")); + const button_container: Container(Model, Event) = try .init(allocator, .{ .rectangle = .{ .fill = .blue }, }, button.element()); try container.append(button_container); @@ -1536,11 +1566,11 @@ test "button" { container.resize(size); container.reposition(.{}); - try renderer.render(Container(Event), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/button.zon"), renderer.screen); // test the accepting of the `Element` - try container.handle(.{ + try container.handle(&model, .{ .mouse = .{ .x = 5, .y = 3, @@ -1570,8 +1600,10 @@ test "progress" { .y = 20, }; var queue: Queue = .{}; + const Model = struct {}; + var model: Model = .{}; - var container: Container(Event) = try .init(allocator, .{ + var container: Container(Model, Event) = try .init(allocator, .{ .layout = .{ .padding = .all(1), }, @@ -1579,12 +1611,12 @@ test "progress" { }, .{}); defer container.deinit(); - var progress: Progress(Event, Queue)(.progress) = .init(&queue, .{ + var progress: Progress(Model, Event, Queue)(.progress) = .init(&queue, .{ .percent = .{ .enabled = true }, .fg = .green, .bg = .grey, }); - const progress_container: Container(Event) = try .init(allocator, .{}, progress.element()); + const progress_container: Container(Model, Event) = try .init(allocator, .{}, progress.element()); try container.append(progress_container); var renderer: testing.Renderer = .init(allocator, size); @@ -1592,42 +1624,42 @@ test "progress" { container.resize(size); container.reposition(.{}); - try renderer.render(Container(Event), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_zero.zon"), renderer.screen); // test the progress of the `Element` - try container.handle(.{ + try container.handle(&model, .{ .progress = 25, }); container.resize(size); container.reposition(.{}); - try renderer.render(Container(Event), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_quarter.zon"), renderer.screen); // test the progress of the `Element` - try container.handle(.{ + try container.handle(&model, .{ .progress = 50, }); container.resize(size); container.reposition(.{}); - try renderer.render(Container(Event), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_half.zon"), renderer.screen); // test the progress of the `Element` - try container.handle(.{ + try container.handle(&model, .{ .progress = 75, }); container.resize(size); container.reposition(.{}); - try renderer.render(Container(Event), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_three_quarter.zon"), renderer.screen); // test the progress of the `Element` - try container.handle(.{ + try container.handle(&model, .{ .progress = 100, }); container.resize(size); container.reposition(.{}); - try renderer.render(Container(Event), &container); + try renderer.render(@TypeOf(container), &container, Model, &.{}); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_hundred.zon"), renderer.screen); } diff --git a/src/event.zig b/src/event.zig index d6888bf..3675a25 100644 --- a/src/event.zig +++ b/src/event.zig @@ -21,14 +21,17 @@ pub const SystemEvent = union(enum) { key: Key, /// Mouse input event mouse: Mouse, - /// Focus event for mouse interaction - /// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for + /// Focus event indicating that the application has gained the focus of the user focus: bool, }; /// Merge the two provided `union(enum)` `A` and `B` to a tagged union containing all fields of both tagged unions in `comptime`. /// Declarations are not supported for `comptime` created types, see https://github.com/ziglang/zig/issues/6709 for details. pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type { + // TODO maybe it makes sense to have a nested tagged union type (i.e. system: union(enum) and event: union(enum)) + // - allows re-definition of system / built-in events + // - clearly shows which events are system / built-in ones and which are user defined events + // - the memory footprint for the nesting is not really harmful if (!isTaggedUnion(A) or !isTaggedUnion(B)) @compileError("Both types for merging tagged unions need to be of type `union(enum)`."); const a_fields = @typeInfo(A).@"union".fields; @@ -93,7 +96,7 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type { } }); } -/// Determine at whether the provided type `T` is a tagged union: `union(enum)`. +/// Determine whether the provided type `T` is a tagged union: `union(enum)`. pub fn isTaggedUnion(comptime T: type) bool { switch (@typeInfo(T)) { .@"union" => |u| if (u.tag_type) |_| {} else { @@ -104,6 +107,14 @@ pub fn isTaggedUnion(comptime T: type) bool { return true; } +/// Determine whether the provided type `T` is a `struct`. +pub fn isStruct(comptime T: type) bool { + return switch (@typeInfo(T)) { + .@"struct" => |_| true, + else => false, + }; +} + const std = @import("std"); const input = @import("input.zig"); const terminal = @import("terminal.zig"); diff --git a/src/render.zig b/src/render.zig index 333872e..20cebb8 100644 --- a/src/render.zig +++ b/src/render.zig @@ -58,10 +58,10 @@ pub const Buffered = struct { } /// Render provided cells at size (anchor and dimension) into the *virtual screen*. - pub fn render(this: *@This(), comptime T: type, container: *T) !void { + pub fn render(this: *@This(), comptime Container: type, container: *Container, comptime Model: type, model: *const Model) !void { const size: Point = container.size; const origin: Point = container.origin; - const cells: []const Cell = try container.content(); + const cells: []const Cell = try container.content(model); if (cells.len == 0) return; @@ -80,7 +80,7 @@ pub const Buffered = struct { // free immediately container.allocator.free(cells); - for (container.elements.items) |*element| try this.render(T, element); + for (container.elements.items) |*element| try this.render(Container, element, Model, model); } /// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop). diff --git a/src/testing.zig b/src/testing.zig index 651a0e2..399c2f8 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -34,10 +34,10 @@ pub const Renderer = struct { @memset(this.screen, .{}); } - pub fn render(this: *@This(), comptime T: type, container: *const T) !void { + pub fn render(this: *@This(), comptime T: type, container: *const T, comptime Model: type, model: *const Model) !void { const size: Point = container.size; const origin: Point = container.origin; - const cells: []const Cell = try container.content(); + const cells: []const Cell = try container.content(model); if (cells.len == 0) return; @@ -58,7 +58,7 @@ pub const Renderer = struct { // free immediately container.allocator.free(cells); - for (container.elements.items) |*element| try this.render(T, element); + for (container.elements.items) |*element| try this.render(T, element, Model, model); } pub fn save(this: @This(), writer: anytype) !void { @@ -74,6 +74,8 @@ pub const Renderer = struct { /// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method: /// /// ```zig +/// const Model = struct {}; +/// var model: Model = .{}; /// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true }); /// defer file.close(); /// @@ -81,8 +83,8 @@ pub const Renderer = struct { /// var renderer: testing.Renderer = .init(allocator, size); /// defer renderer.deinit(); /// -/// try container.handle(.{ .size = size }); -/// try renderer.render(Container(event.SystemEvent), &container); +/// try container.handle(&model, .{ .size = size }); +/// try renderer.render(@TypeOf(container), &container, Model, &.{}); /// try renderer.save(file.writer()); /// ``` /// @@ -91,7 +93,8 @@ pub const Renderer = struct { /// Then later load that .zon file at compile time and run your test against this `Cell` slice. /// /// ```zig -/// var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{ +/// const Model = struct {}; +/// var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{ /// .border = .{ /// .color = .green, /// .sides = .all, @@ -102,16 +105,16 @@ pub const Renderer = struct { /// try testing.expectContainerScreen(.{ /// .rows = 20, /// .cols = 30, -/// }, &container, @import("test/container/border.all.zon")); +/// }, @TypeOf(container), &container, Model, @import("test/container/border.all.zon")); /// ``` -pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEvent), expected: []const Cell) !void { +pub fn expectContainerScreen(size: Point, comptime T: type, container: *T, comptime Model: type, expected: []const Cell) !void { const allocator = testing.allocator; var renderer: Renderer = .init(allocator, size); defer renderer.deinit(); container.resize(size); container.reposition(.{}); - try renderer.render(Container(event.SystemEvent), container); + try renderer.render(T, container, Model, &.{}); try expectEqualCells(.{}, renderer.size, expected, renderer.screen); }