feat(model): implement Elm architecture
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m2s
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:
@@ -80,8 +80,9 @@ pub const Border = packed struct {
|
||||
test "all sides" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .green,
|
||||
.sides = .all,
|
||||
@@ -92,14 +93,15 @@ pub const Border = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/border.all.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/border.all.zon"));
|
||||
}
|
||||
|
||||
test "vertical sides" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .green,
|
||||
.sides = .vertical,
|
||||
@@ -110,14 +112,15 @@ pub const Border = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/border.vertical.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/border.vertical.zon"));
|
||||
}
|
||||
|
||||
test "horizontal sides" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .green,
|
||||
.sides = .horizontal,
|
||||
@@ -128,7 +131,7 @@ pub const Border = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/border.horizontal.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/border.horizontal.zon"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -160,8 +163,9 @@ pub const Rectangle = packed struct {
|
||||
test "fill color overwrite parent fill" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .green },
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
@@ -173,14 +177,15 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_parent_fill_without_padding.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_fill_without_padding.zon"));
|
||||
}
|
||||
|
||||
test "fill color padding to show parent fill" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.padding = .all(2),
|
||||
},
|
||||
@@ -195,14 +200,15 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_parent_padding.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_padding.zon"));
|
||||
}
|
||||
|
||||
test "fill color padding to show parent fill (negative padding)" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.padding = .{
|
||||
.top = -18,
|
||||
@@ -222,14 +228,15 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_parent_padding.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_parent_padding.zon"));
|
||||
}
|
||||
|
||||
test "fill color spacer with padding" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.rectangle = .{
|
||||
.fill = .black,
|
||||
},
|
||||
@@ -250,14 +257,15 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_padding.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_padding.zon"));
|
||||
}
|
||||
|
||||
test "fill color with gap" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.rectangle = .{
|
||||
.fill = .black,
|
||||
},
|
||||
@@ -281,14 +289,15 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_gap.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_gap.zon"));
|
||||
}
|
||||
|
||||
test "fill color with separator" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.rectangle = .{
|
||||
.fill = .black,
|
||||
},
|
||||
@@ -313,7 +322,7 @@ pub const Rectangle = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_separator.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/rectangle_with_separator.zon"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -403,8 +412,9 @@ pub const Layout = packed struct {
|
||||
test "separator without gaps" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
@@ -418,14 +428,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_no_gaps.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/separator_no_gaps.zon"));
|
||||
}
|
||||
|
||||
test "separator without gaps with padding" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.padding = .all(1),
|
||||
.separator = .{
|
||||
@@ -440,14 +451,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_no_gaps_with_padding.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/separator_no_gaps_with_padding.zon"));
|
||||
}
|
||||
|
||||
test "separator(2x) without gaps" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.direction = .vertical,
|
||||
.separator = .{
|
||||
@@ -464,14 +476,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_2x_no_gaps.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps.zon"));
|
||||
}
|
||||
|
||||
test "separator(2x) with border(all)" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
@@ -491,14 +504,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_2x_no_gaps_with_border.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps_with_border.zon"));
|
||||
}
|
||||
|
||||
test "separator(2x) with border(all) and padding(all(1))" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
@@ -519,14 +533,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_2x_no_gaps_with_padding.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/separator_2x_no_gaps_with_padding.zon"));
|
||||
}
|
||||
|
||||
test "separator(2x) with border(all) and gap" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
@@ -547,14 +562,15 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_2x_with_gaps_with_border.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/separator_2x_with_gaps_with_border.zon"));
|
||||
}
|
||||
|
||||
test "separator(2x) with border(all) and gap and padding" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
@@ -576,7 +592,7 @@ pub const Layout = packed struct {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -591,10 +607,10 @@ pub const Size = packed struct {
|
||||
} = .both,
|
||||
};
|
||||
|
||||
pub fn Container(comptime Event: type) type {
|
||||
pub fn Container(Model: type, Event: type) type {
|
||||
if (!isTaggedUnion(Event)) @compileError("Provided user event `Event` for `Container(comptime Event: type)`");
|
||||
|
||||
const Element = @import("element.zig").Element(Event);
|
||||
const Element = @import("element.zig").Element(Model, Event);
|
||||
return struct {
|
||||
allocator: Allocator,
|
||||
origin: Point,
|
||||
@@ -889,7 +905,7 @@ pub fn Container(comptime Event: type) type {
|
||||
this.grow_resize(this.size);
|
||||
}
|
||||
|
||||
pub fn handle(this: *const @This(), event: Event) !void {
|
||||
pub fn handle(this: *const @This(), model: *Model, event: Event) !void {
|
||||
switch (event) {
|
||||
.mouse => |mouse| if (mouse.in(this.origin, this.size)) {
|
||||
// the element receives the mouse event with relative position
|
||||
@@ -897,17 +913,14 @@ pub fn Container(comptime Event: type) type {
|
||||
var relative_mouse: input.Mouse = mouse;
|
||||
relative_mouse.x -= this.origin.x;
|
||||
relative_mouse.y -= this.origin.y;
|
||||
try this.element.handle(.{ .mouse = relative_mouse });
|
||||
for (this.elements.items) |*element| try element.handle(event);
|
||||
},
|
||||
else => {
|
||||
try this.element.handle(event);
|
||||
for (this.elements.items) |*element| try element.handle(event);
|
||||
try this.element.handle(model, .{ .mouse = relative_mouse });
|
||||
},
|
||||
else => try this.element.handle(model, event),
|
||||
}
|
||||
for (this.elements.items) |*element| try element.handle(model, event);
|
||||
}
|
||||
|
||||
pub fn content(this: *const @This()) ![]Cell {
|
||||
pub fn content(this: *const @This(), model: *const Model) ![]Cell {
|
||||
if (this.size.x == 0 or this.size.y == 0) return Error.TooSmall;
|
||||
|
||||
const cells = try this.allocator.alloc(Cell, @as(usize, this.size.x) * @as(usize, this.size.y));
|
||||
@@ -918,7 +931,7 @@ pub fn Container(comptime Event: type) type {
|
||||
this.properties.border.content(cells, this.size);
|
||||
this.properties.rectangle.content(cells, this.size);
|
||||
|
||||
try this.element.content(cells, this.size);
|
||||
try this.element.content(model, cells, this.size);
|
||||
|
||||
// DEBUG render corresponding corners (except top left) of this `Container` *red*
|
||||
if (comptime build_options.debug) {
|
||||
@@ -962,8 +975,9 @@ test {
|
||||
test "Container Fixed and Grow Size Vertical" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{ .direction = .vertical },
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
@@ -981,14 +995,15 @@ test "Container Fixed and Grow Size Vertical" {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/fixed_grow_vertical.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/fixed_grow_vertical.zon"));
|
||||
}
|
||||
|
||||
test "Container Fixed and Grow Size Horizontal" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
const Model = struct {};
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{}, .{});
|
||||
var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.size = .{
|
||||
.dim = .{ .x = 5 },
|
||||
@@ -1004,5 +1019,5 @@ test "Container Fixed and Grow Size Horizontal" {
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/fixed_grow_horizontal.zon"));
|
||||
}, @TypeOf(container), &container, Model, @import("test/container/fixed_grow_horizontal.zon"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user