feat(model): implement Elm architecture
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m2s

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).
This commit is contained in:
2025-10-26 15:58:07 +01:00
parent 8f90f57f44
commit feae9fa1a4
22 changed files with 396 additions and 319 deletions

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.key => |key| { .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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -133,7 +133,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -214,7 +214,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -230,7 +230,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -242,6 +242,6 @@ const std = @import("std");
const time = std.time; const time = std.time;
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) { const App = zterm.App(struct {}, union(enum) {
accept: []u21, accept: []u21,
}); });

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -33,7 +33,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -168,7 +168,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // 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 = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -183,7 +183,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -195,4 +195,4 @@ const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const input = zterm.input; const input = zterm.input;
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -70,7 +70,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -85,7 +85,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -96,4 +96,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) { .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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -82,7 +82,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -119,7 +119,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -134,7 +134,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -145,7 +145,10 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) { const App = zterm.App(
click: [:0]const u8, struct {},
accept, union(enum) {
}); click: [:0]const u8,
accept,
},
);

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y }, .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)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
@@ -65,7 +65,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -133,7 +133,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -148,7 +148,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -161,6 +161,9 @@ const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const Color = zterm.Color; const Color = zterm.Color;
const App = zterm.App(union(enum) { const App = zterm.App(
accept: []u8, struct {},
}); union(enum) {
accept: []u8,
},
);

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -138,7 +138,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -154,7 +154,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -166,6 +166,9 @@ const std = @import("std");
const time = std.time; const time = std.time;
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) { const App = zterm.App(
progress: u8, struct {},
}); union(enum) {
progress: u8,
},
);

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -67,7 +67,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -82,7 +82,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -93,6 +93,6 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) { const App = zterm.App(struct {}, union(enum) {
accept, accept,
}); });

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -66,7 +66,7 @@ pub fn main() !void {
} }
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -169,7 +169,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -184,7 +184,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -196,4 +196,4 @@ const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const input = zterm.input; const input = zterm.input;
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -67,7 +67,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -82,7 +82,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -94,4 +94,4 @@ const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const Error = zterm.Error; const Error = zterm.Error;
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -31,7 +31,7 @@ const InfoText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); 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 } }; 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.key => |key| if (!key.isAscii()) return zterm.Error.TooSmall, .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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -97,7 +97,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -130,7 +130,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -145,7 +145,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -156,4 +156,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -86,7 +86,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // 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 = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -101,7 +101,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -112,4 +112,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -78,7 +78,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // 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 = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -93,7 +93,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -104,4 +104,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -94,7 +94,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // 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 = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -109,7 +109,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -120,4 +120,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -35,7 +35,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -77,7 +77,7 @@ pub fn main() !void {
} }
// NOTE returned errors should be propagated back to the application // 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 = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -92,7 +92,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -103,4 +103,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -32,7 +32,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -73,7 +73,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -88,7 +88,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -99,4 +99,4 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {}); const App = zterm.App(struct {}, union(enum) {});

View File

@@ -5,7 +5,7 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -31,7 +31,7 @@ const TextStyles = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } }; 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); @setEvalBranchQuota(10000);
_ = ctx; _ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -83,7 +83,7 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .init; var app: App = .init(.{});
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
@@ -128,7 +128,7 @@ pub fn main() !void {
else => {}, else => {},
} }
container.handle(event) catch |err| app.postEvent(.{ container.handle(&app.model, event) catch |err| app.postEvent(.{
.err = .{ .err = .{
.err = err, .err = err,
.msg = "Container Event handling failed", .msg = "Container Event handling failed",
@@ -143,7 +143,7 @@ pub fn main() !void {
container.resize(try renderer.resize()); container.resize(try renderer.resize());
container.reposition(.{}); container.reposition(.{});
try renderer.render(@TypeOf(container), &container); try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
try renderer.flush(); try renderer.flush();
} }
} }
@@ -154,4 +154,5 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const App = zterm.App(struct {}, union(enum) {});

View File

@@ -12,18 +12,19 @@
/// ```zig /// ```zig
/// const zterm = @import("zterm"); /// const zterm = @import("zterm");
/// const App = zterm.App( /// 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 /// // 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(); /// 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 { pub fn App(comptime M: type, comptime E: type) type {
if (!isTaggedUnion(E)) { if (!isStruct(M)) @compileError("Provided model `M` for `App(comptime M: type, comptime E: type)` is not of type `struct`");
@compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`."); if (!isTaggedUnion(E)) @compileError("Provided user event `E` for `App(comptime M: type, comptime E: type)` is not of type `union(enum)`.");
}
return struct { return struct {
model: Model,
queue: Queue, queue: Queue,
thread: ?Thread = null, thread: ?Thread = null,
quit_event: Thread.ResetEvent, quit_event: Thread.ResetEvent,
@@ -40,10 +41,13 @@ pub fn App(comptime E: type) type {
this.postEvent(.resize); this.postEvent(.resize);
} }
pub const init: @This() = .{ pub fn init(model: Model) @This() {
.queue = .{}, return .{
.quit_event = .{}, .model = model,
}; .queue = .{},
.quit_event = .{},
};
}
pub fn start(this: *@This()) !void { pub fn start(this: *@This()) !void {
if (this.thread) |_| return; if (this.thread) |_| return;
@@ -408,16 +412,17 @@ pub fn App(comptime E: type) type {
} }
const element = @import("element.zig"); const element = @import("element.zig");
pub const Model = M;
pub const Event = mergeTaggedUnions(event.SystemEvent, E); pub const Event = mergeTaggedUnions(event.SystemEvent, E);
pub const Container = @import("container.zig").Container(Event); pub const Container = @import("container.zig").Container(Model, Event);
pub const Element = element.Element(Event); pub const Element = element.Element(Model, Event);
pub const Alignment = element.Alignment(Event); pub const Alignment = element.Alignment(Model, Event);
pub const Button = element.Button(Event, Queue); pub const Button = element.Button(Model, Event, Queue);
pub const Input = element.Input(Event, Queue); pub const Input = element.Input(Model, Event, Queue);
pub const Progress = element.Progress(Event, Queue); pub const Progress = element.Progress(Model, Event, Queue);
pub const RadioButton = element.RadioButton(Event); pub const RadioButton = element.RadioButton(Model, Event);
pub const Scrollable = element.Scrollable(Event); pub const Scrollable = element.Scrollable(Model, Event);
pub const Selection = element.Selection(Event); pub const Selection = element.Selection(Model, Event);
pub const Queue = queue.Queue(Event, 256); pub const Queue = queue.Queue(Event, 256);
}; };
} }
@@ -436,6 +441,7 @@ const terminal = @import("terminal.zig");
const queue = @import("queue.zig"); const queue = @import("queue.zig");
const mergeTaggedUnions = event.mergeTaggedUnions; const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion; const isTaggedUnion = event.isTaggedUnion;
const isStruct = event.isStruct;
const Mouse = input.Mouse; const Mouse = input.Mouse;
const Key = input.Key; const Key = input.Key;
const Point = @import("point.zig").Point; const Point = @import("point.zig").Point;

View File

@@ -80,8 +80,9 @@ pub const Border = packed struct {
test "all sides" { test "all sides" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .border = .{
.color = .green, .color = .green,
.sides = .all, .sides = .all,
@@ -92,14 +93,15 @@ pub const Border = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/border.all.zon")); }, @TypeOf(container), &container, Model, @import("test/container/border.all.zon"));
} }
test "vertical sides" { test "vertical sides" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .border = .{
.color = .green, .color = .green,
.sides = .vertical, .sides = .vertical,
@@ -110,14 +112,15 @@ pub const Border = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/border.vertical.zon")); }, @TypeOf(container), &container, Model, @import("test/container/border.vertical.zon"));
} }
test "horizontal sides" { test "horizontal sides" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .border = .{
.color = .green, .color = .green,
.sides = .horizontal, .sides = .horizontal,
@@ -128,7 +131,7 @@ pub const Border = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "fill color overwrite parent fill" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 }, .rectangle = .{ .fill = .green },
}, .{}); }, .{});
try container.append(try .init(std.testing.allocator, .{ try container.append(try .init(std.testing.allocator, .{
@@ -173,14 +177,15 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "fill color padding to show parent fill" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .layout = .{
.padding = .all(2), .padding = .all(2),
}, },
@@ -195,14 +200,15 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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)" { test "fill color padding to show parent fill (negative padding)" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .layout = .{
.padding = .{ .padding = .{
.top = -18, .top = -18,
@@ -222,14 +228,15 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "fill color spacer with padding" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .rectangle = .{
.fill = .black, .fill = .black,
}, },
@@ -250,14 +257,15 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "fill color with gap" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .rectangle = .{
.fill = .black, .fill = .black,
}, },
@@ -281,14 +289,15 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "fill color with separator" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .rectangle = .{
.fill = .black, .fill = .black,
}, },
@@ -313,7 +322,7 @@ pub const Rectangle = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "separator without gaps" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .layout = .{
.separator = .{ .separator = .{
.enabled = true, .enabled = true,
@@ -418,14 +428,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "separator without gaps with padding" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .layout = .{
.padding = .all(1), .padding = .all(1),
.separator = .{ .separator = .{
@@ -440,14 +451,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "separator(2x) without gaps" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .layout = .{
.direction = .vertical, .direction = .vertical,
.separator = .{ .separator = .{
@@ -464,14 +476,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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)" { test "separator(2x) with border(all)" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .border = .{
.color = .red, .color = .red,
.sides = .all, .sides = .all,
@@ -491,14 +504,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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))" { test "separator(2x) with border(all) and padding(all(1))" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .border = .{
.color = .red, .color = .red,
.sides = .all, .sides = .all,
@@ -519,14 +533,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "separator(2x) with border(all) and gap" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .border = .{
.color = .red, .color = .red,
.sides = .all, .sides = .all,
@@ -547,14 +562,15 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "separator(2x) with border(all) and gap and padding" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 = .{ .border = .{
.color = .red, .color = .red,
.sides = .all, .sides = .all,
@@ -576,7 +592,7 @@ pub const Layout = packed struct {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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, } = .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)`"); 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 { return struct {
allocator: Allocator, allocator: Allocator,
origin: Point, origin: Point,
@@ -889,7 +905,7 @@ pub fn Container(comptime Event: type) type {
this.grow_resize(this.size); 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) { switch (event) {
.mouse => |mouse| if (mouse.in(this.origin, this.size)) { .mouse => |mouse| if (mouse.in(this.origin, this.size)) {
// the element receives the mouse event with relative position // 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; var relative_mouse: input.Mouse = mouse;
relative_mouse.x -= this.origin.x; relative_mouse.x -= this.origin.x;
relative_mouse.y -= this.origin.y; relative_mouse.y -= this.origin.y;
try this.element.handle(.{ .mouse = relative_mouse }); try this.element.handle(model, .{ .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);
}, },
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; 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)); 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.border.content(cells, this.size);
this.properties.rectangle.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* // DEBUG render corresponding corners (except top left) of this `Container` *red*
if (comptime build_options.debug) { if (comptime build_options.debug) {
@@ -962,8 +975,9 @@ test {
test "Container Fixed and Grow Size Vertical" { test "Container Fixed and Grow Size Vertical" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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 }, .layout = .{ .direction = .vertical },
}, .{}); }, .{});
try container.append(try .init(std.testing.allocator, .{ try container.append(try .init(std.testing.allocator, .{
@@ -981,14 +995,15 @@ test "Container Fixed and Grow Size Vertical" {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .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" { test "Container Fixed and Grow Size Horizontal" {
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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, .{ try container.append(try .init(std.testing.allocator, .{
.size = .{ .size = .{
.dim = .{ .x = 5 }, .dim = .{ .x = 5 },
@@ -1004,5 +1019,5 @@ test "Container Fixed and Grow Size Horizontal" {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.y = 20, .y = 20,
.x = 30, .x = 30,
}, &container, @import("test/container/fixed_grow_horizontal.zon")); }, @TypeOf(container), &container, Model, @import("test/container/fixed_grow_horizontal.zon"));
} }

View File

@@ -9,7 +9,7 @@
// FIX known issues: // FIX known issues:
// - hold fewer instances of the `Allocator` // - hold fewer instances of the `Allocator`
pub fn Element(Event: type) type { pub fn Element(Model: type, Event: type) type {
return struct { return struct {
ptr: *anyopaque = undefined, ptr: *anyopaque = undefined,
vtable: *const VTable = &.{}, vtable: *const VTable = &.{},
@@ -17,8 +17,8 @@ pub fn Element(Event: type) type {
pub const VTable = struct { pub const VTable = struct {
resize: ?*const fn (ctx: *anyopaque, size: Point) void = null, resize: ?*const fn (ctx: *anyopaque, size: Point) void = null,
reposition: ?*const fn (ctx: *anyopaque, origin: Point) void = null, reposition: ?*const fn (ctx: *anyopaque, origin: Point) void = null,
handle: ?*const fn (ctx: *anyopaque, event: Event) anyerror!void = null, handle: ?*const fn (ctx: *anyopaque, model: *Model, event: Event) anyerror!void = null,
content: ?*const fn (ctx: *anyopaque, cells: []Cell, size: Point) 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*. /// 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 /// Handle the received event. The event is one of the user provided
/// events or a system event, with the exception of the `.size` /// events or a system event. The model can be updated through the
/// `Event` as every `Container` already handles that event. /// provided pointer.
/// ///
/// In case of user errors this function should return an error. This /// In case of user errors this function should return an error. This
/// error may then be used by the application to display information /// error may then be used by the application to display information
/// about the user error. /// about the error to the user and is left intentionally up to the
pub inline fn handle(this: @This(), event: Event) !void { /// implementation to decide.
pub inline fn handle(this: @This(), model: *Model, event: Event) !void {
if (this.vtable.handle) |handle_fn| 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 /// Write content into the `cells` of the `Container`. The associated
@@ -51,7 +52,7 @@ pub fn Element(Event: type) type {
/// ///
/// # Note /// # 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 /// ```zig
/// std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); /// 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.). /// non-recoverable (i.e. an allocation error, system error, etc.).
/// Otherwise user specific errors should be caught using the `handle` /// Otherwise user specific errors should be caught using the `handle`
/// function before the rendering of the `Container` happens. /// 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| { 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* // DEBUG render corresponding top left corner of this `Element` *red*
// - only rendered if the corresponding associated element renders contents into the `Container` // - 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 { return struct {
/// `Size` of the actual contents that should be aligned. /// `Size` of the actual contents that should be aligned.
size: Point = .{}, size: Point = .{},
/// Alignment Configuration to use for aligning the associated contents of this `Element`. /// Alignment Configuration to use for aligning the associated contents of this `Element`.
configuration: Configuration, configuration: Configuration,
/// `Container` to render in alignment. It needs to use the sizing options accordingly to be effective. /// `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 /// Configuration for Alignment
pub const Configuration = packed struct { pub const Configuration = packed struct {
@@ -105,14 +118,14 @@ pub fn Alignment(Event: type) type {
pub const center: @This() = .{ .v = .center, .h = .center }; 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 .{ return .{
.container = container, .container = container,
.configuration = configuration, .configuration = configuration,
}; };
} }
pub fn element(this: *@This()) Element(Event) { pub fn element(this: *@This()) Element(Model, Event) {
return .{ return .{
.ptr = this, .ptr = this,
.vtable = &.{ .vtable = &.{
@@ -146,18 +159,18 @@ pub fn Alignment(Event: type) type {
this.container.reposition(origin); 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)); 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const origin = this.container.origin; const origin = this.container.origin;
const csize = this.container.size; 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); defer this.container.allocator.free(container_cells);
outer: for (0..csize.y) |row| { 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 { return struct {
/// `Size` of the actual contents where the anchor and the size is /// `Size` of the actual contents where the anchor and the size is
/// representing the size and location on screen. /// 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 of the viewport of the scrollable `Container`.
anchor: Point = .{}, anchor: Point = .{},
/// The actual `Container`, that is scrollable. /// The actual `Container`, that is scrollable.
container: Container(Event), container: Container(Model, Event),
/// Whether the scrollable contents should show a scroll bar or not. /// Whether the scrollable contents should show a scroll bar or not.
/// The scroll bar will only be shown if required and enabled. With the /// The scroll bar will only be shown if required and enabled. With the
/// corresponding provided color if to be shown. /// 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 .{ return .{
.container = container, .container = container,
.configuration = configuration, .configuration = configuration,
}; };
} }
pub fn element(this: *@This()) Element(Event) { pub fn element(this: *@This()) Element(Model, Event) {
return .{ return .{
.ptr = this, .ptr = this,
.vtable = &.{ .vtable = &.{
@@ -255,7 +268,7 @@ pub fn Scrollable(Event: type) type {
this.container.reposition(.{}); 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
// TODO other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?) // 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; const max_anchor_x = this.container_size.x -| this.size.x;
this.anchor.x = @min(this.anchor.x + 1, max_anchor_x); this.anchor.x = @min(this.anchor.x + 1, max_anchor_x);
}, },
else => try this.container.handle(.{ else => try this.container.handle(model, .{
.mouse = .{ .mouse = .{
.x = mouse.x + this.anchor.x, .x = mouse.x + this.anchor.x,
.y = mouse.y + this.anchor.y, .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 size = container.size;
const origin = container.origin; 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); 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 // free immediately
container.allocator.free(contents); 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, this.size.x) * @as(usize, this.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 offset_y: usize = if (this.configuration.y_axis) 1 else 0;
const container_size = this.container.size; 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); const anchor = (@as(usize, this.anchor.y) * @as(usize, container_size.x)) + @as(usize, this.anchor.x);
for (0..size.y - offset_y) |row| { for (0..size.y - offset_y) |row| {
@@ -368,12 +381,12 @@ pub fn Scrollable(Event: type) type {
this.container.allocator.free(container_cells); this.container.allocator.free(container_cells);
} }
}; };
} // Scrollable(Event: type) } // Scrollable(Model: type, Event: type)
// TODO features // TODO features
// - clear input (with and without retaining of capacity) through an public api // - clear input (with and without retaining of capacity) through an public api
// - make handle / content functions public // - 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 // NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
const input_struct = struct { const input_struct = struct {
pub fn input_fn(accept_event: meta.FieldEnum(Event)) type { 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); this.input.deinit(this.allocator);
} }
pub fn element(this: *@This()) Element(Event) { pub fn element(this: *@This()) Element(Model, Event) {
return .{ return .{
.ptr = this, .ptr = this,
.vtable = &.{ .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)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.key => |key| { .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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); 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; 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 // NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
const button_struct = struct { const button_struct = struct {
pub fn button_fn(accept_event: meta.FieldEnum(Event)) type { 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 .{ return .{
.ptr = this, .ptr = this,
.vtable = &.{ .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)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
// TODO should this also support key presses to accept? // 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); 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; 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 { return struct {
configuration: Configuration, configuration: Configuration,
value: bool, 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 .{ return .{
.ptr = this, .ptr = this,
.vtable = &.{ .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)); var this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
// TODO should this also support key presses to accept? // 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); 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 { const selection_struct = struct {
pub fn selection_fn(Enum: type) type { pub fn selection_fn(Enum: type) type {
switch (@typeInfo(Enum)) { 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 .{ return .{
.ptr = this, .ptr = this,
.vtable = &.{ .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)); const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) { switch (event) {
.mouse => |mouse| if (mouse.y == 0) { .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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.configuration.label.len + 4 + this.width >= cells.len) return Error.TooSmall; 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; 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 // NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
const progress_struct = struct { const progress_struct = struct {
pub fn progress_fn(progress_event: meta.FieldEnum(Event)) type { 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 .{ return .{
.ptr = this, .ptr = this,
.vtable = &.{ .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)); const this: *@This() = @ptrCast(@alignCast(ctx));
// TODO should this `Element` trigger a completion event? (I don't think that this is useful?) // TODO should this `Element` trigger a completion event? (I don't think that this is useful?)
switch (event) { 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)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); 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; return progress_struct.progress_fn;
} // Progress(Event: type, Queue: type) } // Progress(Model: type, Event: type, Queue: type)
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
@@ -990,8 +1003,10 @@ test "scrollable vertical" {
.x = 30, .x = 30,
.y = 20, .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 = .{ .border = .{
.sides = .all, .sides = .all,
.color = .red, .color = .red,
@@ -1016,9 +1031,9 @@ test "scrollable vertical" {
}, .{})); }, .{}));
defer box.deinit(); 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 = .{ .border = .{
.color = .green, .color = .green,
.sides = .vertical, .sides = .vertical,
@@ -1031,11 +1046,11 @@ test "scrollable vertical" {
container.resize(size); container.resize(size);
container.reposition(.{}); 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); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen);
// scroll down 15 times (exactly to the end) // scroll down 15 times (exactly to the end)
for (0..15) |_| try container.handle(.{ for (0..15) |_| try container.handle(&model, .{
.mouse = .{ .mouse = .{
.button = .wheel_down, .button = .wheel_down,
.kind = .press, .kind = .press,
@@ -1043,11 +1058,11 @@ test "scrollable vertical" {
.y = 5, .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); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// further scrolling down will not change anything // further scrolling down will not change anything
try container.handle(.{ try container.handle(&model, .{
.mouse = .{ .mouse = .{
.button = .wheel_down, .button = .wheel_down,
.kind = .press, .kind = .press,
@@ -1055,7 +1070,7 @@ test "scrollable vertical" {
.y = 5, .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); 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, .x = 30,
.y = 20, .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 = .{ .border = .{
.sides = .all, .sides = .all,
.color = .red, .color = .red,
@@ -1094,9 +1111,9 @@ test "scrollable vertical with scrollbar" {
}, .{})); }, .{}));
defer box.deinit(); 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 = .{ .border = .{
.color = .green, .color = .green,
.sides = .vertical, .sides = .vertical,
@@ -1109,11 +1126,11 @@ test "scrollable vertical with scrollbar" {
container.resize(size); container.resize(size);
container.reposition(.{}); 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); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.top.zon"), renderer.screen);
// scroll down 15 times (exactly to the end) // scroll down 15 times (exactly to the end)
for (0..15) |_| try container.handle(.{ for (0..15) |_| try container.handle(&model, .{
.mouse = .{ .mouse = .{
.button = .wheel_down, .button = .wheel_down,
.kind = .press, .kind = .press,
@@ -1121,11 +1138,11 @@ test "scrollable vertical with scrollbar" {
.y = 5, .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); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.bottom.zon"), renderer.screen);
// further scrolling down will not change anything // further scrolling down will not change anything
try container.handle(.{ try container.handle(&model, .{
.mouse = .{ .mouse = .{
.button = .wheel_down, .button = .wheel_down,
.kind = .press, .kind = .press,
@@ -1133,7 +1150,7 @@ test "scrollable vertical with scrollbar" {
.y = 5, .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); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.bottom.zon"), renderer.screen);
} }
@@ -1146,8 +1163,10 @@ test "scrollable horizontal" {
.x = 30, .x = 30,
.y = 20, .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 = .{ .border = .{
.sides = .all, .sides = .all,
.color = .red, .color = .red,
@@ -1172,9 +1191,9 @@ test "scrollable horizontal" {
}, .{})); }, .{}));
defer box.deinit(); 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 = .{ .border = .{
.color = .green, .color = .green,
.sides = .horizontal, .sides = .horizontal,
@@ -1187,11 +1206,11 @@ test "scrollable horizontal" {
container.resize(size); container.resize(size);
container.reposition(.{}); 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); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen);
// scroll right 15 times (exactly to the end) // scroll right 15 times (exactly to the end)
for (0..15) |_| try container.handle(.{ for (0..15) |_| try container.handle(&model, .{
.mouse = .{ .mouse = .{
.button = .wheel_right, .button = .wheel_right,
.kind = .press, .kind = .press,
@@ -1199,11 +1218,11 @@ test "scrollable horizontal" {
.y = 5, .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); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
// further scrolling right will not change anything // further scrolling right will not change anything
try container.handle(.{ try container.handle(&model, .{
.mouse = .{ .mouse = .{
.button = .wheel_right, .button = .wheel_right,
.kind = .press, .kind = .press,
@@ -1211,7 +1230,7 @@ test "scrollable horizontal" {
.y = 5, .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); 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, .x = 30,
.y = 20, .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 = .{ .border = .{
.sides = .all, .sides = .all,
.color = .red, .color = .red,
@@ -1250,9 +1271,9 @@ test "scrollable horizontal with scrollbar" {
}, .{})); }, .{}));
defer box.deinit(); 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 = .{ .border = .{
.color = .green, .color = .green,
.sides = .horizontal, .sides = .horizontal,
@@ -1265,11 +1286,11 @@ test "scrollable horizontal with scrollbar" {
container.resize(size); container.resize(size);
container.reposition(.{}); 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); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.left.zon"), renderer.screen);
// scroll right 15 times (exactly to the end) // scroll right 15 times (exactly to the end)
for (0..15) |_| try container.handle(.{ for (0..15) |_| try container.handle(&model, .{
.mouse = .{ .mouse = .{
.button = .wheel_right, .button = .wheel_right,
.kind = .press, .kind = .press,
@@ -1277,11 +1298,11 @@ test "scrollable horizontal with scrollbar" {
.y = 5, .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); try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.right.zon"), renderer.screen);
// further scrolling right will not change anything // further scrolling right will not change anything
try container.handle(.{ try container.handle(&model, .{
.mouse = .{ .mouse = .{
.button = .wheel_right, .button = .wheel_right,
.kind = .press, .kind = .press,
@@ -1289,7 +1310,7 @@ test "scrollable horizontal with scrollbar" {
.y = 5, .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); 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 allocator = std.testing.allocator;
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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(); 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 }, .rectangle = .{ .fill = .green },
.size = .{ .size = .{
.dim = .{ .x = 12, .y = 5 }, .dim = .{ .x = 12, .y = 5 },
@@ -1310,24 +1332,25 @@ test "alignment center" {
}, .{}); }, .{});
defer aligned_container.deinit(); 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 container.append(try .init(allocator, .{}, alignment.element()));
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.x = 30, .x = 30,
.y = 20, .y = 20,
}, &container, @import("test/element/alignment.center.zon")); }, @TypeOf(container), &container, Model, @import("test/element/alignment.center.zon"));
} }
test "alignment left" { test "alignment left" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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(); 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 }, .rectangle = .{ .fill = .green },
.size = .{ .size = .{
.dim = .{ .x = 12, .y = 5 }, .dim = .{ .x = 12, .y = 5 },
@@ -1336,7 +1359,7 @@ test "alignment left" {
}, .{}); }, .{});
defer aligned_container.deinit(); defer aligned_container.deinit();
var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{ var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{
.h = .start, .h = .start,
.v = .center, .v = .center,
}); });
@@ -1345,18 +1368,19 @@ test "alignment left" {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.x = 30, .x = 30,
.y = 20, .y = 20,
}, &container, @import("test/element/alignment.left.zon")); }, @TypeOf(container), &container, Model, @import("test/element/alignment.left.zon"));
} }
test "alignment right" { test "alignment right" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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(); 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 }, .rectangle = .{ .fill = .green },
.size = .{ .size = .{
.dim = .{ .x = 12, .y = 5 }, .dim = .{ .x = 12, .y = 5 },
@@ -1365,7 +1389,7 @@ test "alignment right" {
}, .{}); }, .{});
defer aligned_container.deinit(); defer aligned_container.deinit();
var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{ var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{
.h = .end, .h = .end,
.v = .center, .v = .center,
}); });
@@ -1374,18 +1398,19 @@ test "alignment right" {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.x = 30, .x = 30,
.y = 20, .y = 20,
}, &container, @import("test/element/alignment.right.zon")); }, @TypeOf(container), &container, Model, @import("test/element/alignment.right.zon"));
} }
test "alignment top" { test "alignment top" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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(); 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 }, .rectangle = .{ .fill = .green },
.size = .{ .size = .{
.dim = .{ .x = 12, .y = 5 }, .dim = .{ .x = 12, .y = 5 },
@@ -1394,7 +1419,7 @@ test "alignment top" {
}, .{}); }, .{});
defer aligned_container.deinit(); defer aligned_container.deinit();
var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{ var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{
.h = .center, .h = .center,
.v = .start, .v = .start,
}); });
@@ -1403,18 +1428,19 @@ test "alignment top" {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.x = 30, .x = 30,
.y = 20, .y = 20,
}, &container, @import("test/element/alignment.top.zon")); }, @TypeOf(container), &container, Model, @import("test/element/alignment.top.zon"));
} }
test "alignment bottom" { test "alignment bottom" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const event = @import("event.zig"); const event = @import("event.zig");
const testing = @import("testing.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(); 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 }, .rectangle = .{ .fill = .green },
.size = .{ .size = .{
.dim = .{ .x = 12, .y = 5 }, .dim = .{ .x = 12, .y = 5 },
@@ -1423,7 +1449,7 @@ test "alignment bottom" {
}, .{}); }, .{});
defer aligned_container.deinit(); defer aligned_container.deinit();
var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{ var alignment: Alignment(Model, event.SystemEvent) = .init(aligned_container, .{
.h = .center, .h = .center,
.v = .end, .v = .end,
}); });
@@ -1432,7 +1458,7 @@ test "alignment bottom" {
try testing.expectContainerScreen(.{ try testing.expectContainerScreen(.{
.x = 30, .x = 30,
.y = 20, .y = 20,
}, &container, @import("test/element/alignment.bottom.zon")); }, @TypeOf(container), &container, Model, @import("test/element/alignment.bottom.zon"));
} }
test "input element" { test "input element" {
@@ -1444,8 +1470,10 @@ test "input element" {
}); });
const testing = @import("testing.zig"); const testing = @import("testing.zig");
const Queue = @import("queue.zig").Queue(Event, 256); 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(); defer container.deinit();
const size: Point = .{ const size: Point = .{
@@ -1454,10 +1482,10 @@ test "input element" {
}; };
var queue: Queue = .{}; 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(); defer input_element.deinit();
const input_container: Container(Event) = try .init(allocator, .{ const input_container: Container(Model, Event) = try .init(allocator, .{
.rectangle = .{ .fill = .green }, .rectangle = .{ .fill = .green },
.size = .{ .size = .{
.dim = .{ .x = 12, .y = 2 }, .dim = .{ .x = 12, .y = 2 },
@@ -1472,25 +1500,25 @@ test "input element" {
container.resize(size); container.resize(size);
container.reposition(.{}); 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); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.without.text.zon"), renderer.screen);
// press 'a' 15 times // press 'a' 15 times
for (0..15) |_| try container.handle(.{ for (0..15) |_| try container.handle(&model, .{
.key = .{ .cp = 'a' }, .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); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.zon"), renderer.screen);
// press 'a' 15 times // press 'a' 15 times
for (0..15) |_| try container.handle(.{ for (0..15) |_| try container.handle(&model, .{
.key = .{ .cp = 'a' }, .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); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.overflow.zon"), renderer.screen);
// test the accepting of the `Element` // test the accepting of the `Element`
try container.handle(.{ try container.handle(&model, .{
.key = .{ .cp = input.Enter }, .key = .{ .cp = input.Enter },
}); });
const accept_event = queue.pop(); const accept_event = queue.pop();
@@ -1521,12 +1549,14 @@ test "button" {
.y = 20, .y = 20,
}; };
var queue: Queue = .{}; 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(); defer container.deinit();
var button: Button(Event, Queue)(.accept) = .init(&queue, .init(.default, "Button")); var button: Button(Model, Event, Queue)(.accept) = .init(&queue, .init(.default, "Button"));
const button_container: Container(Event) = try .init(allocator, .{ const button_container: Container(Model, Event) = try .init(allocator, .{
.rectangle = .{ .fill = .blue }, .rectangle = .{ .fill = .blue },
}, button.element()); }, button.element());
try container.append(button_container); try container.append(button_container);
@@ -1536,11 +1566,11 @@ test "button" {
container.resize(size); container.resize(size);
container.reposition(.{}); 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); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/button.zon"), renderer.screen);
// test the accepting of the `Element` // test the accepting of the `Element`
try container.handle(.{ try container.handle(&model, .{
.mouse = .{ .mouse = .{
.x = 5, .x = 5,
.y = 3, .y = 3,
@@ -1570,8 +1600,10 @@ test "progress" {
.y = 20, .y = 20,
}; };
var queue: Queue = .{}; 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 = .{ .layout = .{
.padding = .all(1), .padding = .all(1),
}, },
@@ -1579,12 +1611,12 @@ test "progress" {
}, .{}); }, .{});
defer container.deinit(); defer container.deinit();
var progress: Progress(Event, Queue)(.progress) = .init(&queue, .{ var progress: Progress(Model, Event, Queue)(.progress) = .init(&queue, .{
.percent = .{ .enabled = true }, .percent = .{ .enabled = true },
.fg = .green, .fg = .green,
.bg = .grey, .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); try container.append(progress_container);
var renderer: testing.Renderer = .init(allocator, size); var renderer: testing.Renderer = .init(allocator, size);
@@ -1592,42 +1624,42 @@ test "progress" {
container.resize(size); container.resize(size);
container.reposition(.{}); 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); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_zero.zon"), renderer.screen);
// test the progress of the `Element` // test the progress of the `Element`
try container.handle(.{ try container.handle(&model, .{
.progress = 25, .progress = 25,
}); });
container.resize(size); container.resize(size);
container.reposition(.{}); 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); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_quarter.zon"), renderer.screen);
// test the progress of the `Element` // test the progress of the `Element`
try container.handle(.{ try container.handle(&model, .{
.progress = 50, .progress = 50,
}); });
container.resize(size); container.resize(size);
container.reposition(.{}); 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); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_half.zon"), renderer.screen);
// test the progress of the `Element` // test the progress of the `Element`
try container.handle(.{ try container.handle(&model, .{
.progress = 75, .progress = 75,
}); });
container.resize(size); container.resize(size);
container.reposition(.{}); 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); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_three_quarter.zon"), renderer.screen);
// test the progress of the `Element` // test the progress of the `Element`
try container.handle(.{ try container.handle(&model, .{
.progress = 100, .progress = 100,
}); });
container.resize(size); container.resize(size);
container.reposition(.{}); 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); // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_hundred.zon"), renderer.screen);
} }

View File

@@ -21,14 +21,17 @@ pub const SystemEvent = union(enum) {
key: Key, key: Key,
/// Mouse input event /// Mouse input event
mouse: Mouse, mouse: Mouse,
/// Focus event for mouse interaction /// Focus event indicating that the application has gained the focus of the user
/// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for
focus: bool, focus: bool,
}; };
/// Merge the two provided `union(enum)` `A` and `B` to a tagged union containing all fields of both tagged unions in `comptime`. /// 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. /// 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 { 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)`."); 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; 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 { pub fn isTaggedUnion(comptime T: type) bool {
switch (@typeInfo(T)) { switch (@typeInfo(T)) {
.@"union" => |u| if (u.tag_type) |_| {} else { .@"union" => |u| if (u.tag_type) |_| {} else {
@@ -104,6 +107,14 @@ pub fn isTaggedUnion(comptime T: type) bool {
return true; 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 std = @import("std");
const input = @import("input.zig"); const input = @import("input.zig");
const terminal = @import("terminal.zig"); const terminal = @import("terminal.zig");

View File

@@ -58,10 +58,10 @@ pub const Buffered = struct {
} }
/// Render provided cells at size (anchor and dimension) into the *virtual screen*. /// 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 size: Point = container.size;
const origin: Point = container.origin; 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; if (cells.len == 0) return;
@@ -80,7 +80,7 @@ pub const Buffered = struct {
// free immediately // free immediately
container.allocator.free(cells); 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). /// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop).

View File

@@ -34,10 +34,10 @@ pub const Renderer = struct {
@memset(this.screen, .{}); @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 size: Point = container.size;
const origin: Point = container.origin; 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; if (cells.len == 0) return;
@@ -58,7 +58,7 @@ pub const Renderer = struct {
// free immediately // free immediately
container.allocator.free(cells); 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 { 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: /// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method:
/// ///
/// ```zig /// ```zig
/// const Model = struct {};
/// var model: Model = .{};
/// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true }); /// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true });
/// defer file.close(); /// defer file.close();
/// ///
@@ -81,8 +83,8 @@ pub const Renderer = struct {
/// var renderer: testing.Renderer = .init(allocator, size); /// var renderer: testing.Renderer = .init(allocator, size);
/// defer renderer.deinit(); /// defer renderer.deinit();
/// ///
/// try container.handle(.{ .size = size }); /// try container.handle(&model, .{ .size = size });
/// try renderer.render(Container(event.SystemEvent), &container); /// try renderer.render(@TypeOf(container), &container, Model, &.{});
/// try renderer.save(file.writer()); /// 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. /// Then later load that .zon file at compile time and run your test against this `Cell` slice.
/// ///
/// ```zig /// ```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 = .{ /// .border = .{
/// .color = .green, /// .color = .green,
/// .sides = .all, /// .sides = .all,
@@ -102,16 +105,16 @@ pub const Renderer = struct {
/// try testing.expectContainerScreen(.{ /// try testing.expectContainerScreen(.{
/// .rows = 20, /// .rows = 20,
/// .cols = 30, /// .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; const allocator = testing.allocator;
var renderer: Renderer = .init(allocator, size); var renderer: Renderer = .init(allocator, size);
defer renderer.deinit(); defer renderer.deinit();
container.resize(size); container.resize(size);
container.reposition(.{}); container.reposition(.{});
try renderer.render(Container(event.SystemEvent), container); try renderer.render(T, container, Model, &.{});
try expectEqualCells(.{}, renderer.size, expected, renderer.screen); try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
} }