16 Commits

Author SHA1 Message Date
89bc3eac0c add(example): WIP popup Element example implementation 2025-07-12 20:08:22 +02:00
df78c7d6eb mod: bump zg dependency version
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m19s
2025-07-11 22:38:47 +02:00
66b3a77805 feat(container): negative layout padding
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Has been cancelled
Negative paddings use the current size to calculate the padding from
the opposite orientation. For a given dimension (horizontal or vertical)
if the size is `30` a padding of `5` would be equivalent to a padding
of `-25`. This enables to describe the size of the container using
the padding property of the `Layout` and gives users more freedom to
describe different layouts.
2025-07-11 22:33:32 +02:00
b401b5ece8 lint(container): style
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m41s
2025-07-07 23:08:06 +02:00
9f33c902ee mod(container): cleanup and highlight points for improvement
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m42s
2025-07-06 00:42:00 +02:00
9f29ac6a77 feat(element/progress): Progress bar implementation as an Element
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 2m42s
2025-07-05 18:29:49 +02:00
f775a6ab2d formatting
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 13s
2025-06-30 22:53:05 +02:00
a39cee7ccb feat(element/button): add builtin Element implementation for buttons 2025-06-30 22:52:27 +02:00
7875db0aea feat(element/input): make accept event agnostic to u8 and u21 slices
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 50s
There are now comptime checks in place an the corresponding triggered
event will be automatically converted to the correct type to support
simple ascii strings (`[]u8`) or utf-8 strings (`[]u21`).
2025-06-30 22:03:59 +02:00
7595e3b5bb feat(element/input): text input element implementation
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 14s
Moved implementation from example/input as a standalone `Element`
implementation, which is directly used by the example instead.

The provided argument is the `App.Event`'s event that should be
triggered on acceptance for the contents of the Input `Element`.
Currently only `[]u21` strings are supported, but in the future also
`[]u8` strings shall be supported and automatically converted when
pushed as an `App.Event` into the app's queue.
2025-06-29 11:19:09 +02:00
2ba0ed85fb fix(app): remove exported Exec Element - it does not exist in this branch 2025-06-29 10:34:53 +02:00
439520d4fe mod(example/input): ellipse rendering with scrolling for text field contents
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 17s
2025-06-28 22:09:36 +02:00
ded1f2c17e mod(example/input): reorder input handling cases; add alt-b/f binding implementations 2025-06-28 22:08:19 +02:00
92ae8c9681 feat(example/input): readline shortcuts; better navigation;
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 58s
This will be the basis for a Textfield `Element` implementation to make
it easier to add user inputs into the container structures.
2025-06-28 13:04:00 +02:00
743cdca174 mod: bump zg dependency version
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 26s
As the dependency is still using version 0.14.0, it still causes an
error during testing. Corresponding examples are still working as
expected.
2025-06-24 20:48:32 +02:00
7d8e902ce2 chor: bump zig version and fix corresponding errors 2025-06-24 20:47:47 +02:00
10 changed files with 1050 additions and 122 deletions

View File

@@ -10,6 +10,8 @@ pub fn build(b: *std.Build) void {
alignment,
button,
input,
popup,
progress,
scrollable,
// layouts:
vertical,
@@ -60,6 +62,8 @@ pub fn build(b: *std.Build) void {
.alignment => "examples/elements/alignment.zig",
.button => "examples/elements/button.zig",
.input => "examples/elements/input.zig",
.popup => "examples/elements/popup.zig",
.progress => "examples/elements/progress.zig",
.scrollable => "examples/elements/scrollable.zig",
// layouts:
.vertical => "examples/layouts/vertical.zig",

View File

@@ -37,8 +37,8 @@
// internet connectivity.
.dependencies = .{
.zg = .{
.url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc",
.hash = "1220f3e29bc40856bfc06e0ee133f814b0011c76de987d8a6a458c2f34d82708899a",
.url = "git+https://codeberg.org/atman/zg#9427a9e53aaa29ee071f4dcb35b809a699d75aa9",
.hash = "zg-0.14.1-oGqU3IQ_tALZIiBN026_NTaPJqU-Upm8P_C7QED2Rzm8",
},
},
.paths = .{

View File

@@ -87,10 +87,6 @@ const InputField = struct {
.key => |key| {
if (key.isAscii()) try this.input.append(key.cp);
// TODO support arrow keys for navigation?
// TODO support readline keybindings (i.e. ctrl-k, ctrl-u, ctrl-b, ctrl-f, etc. and the equivalent alt bindings)
// create an own `Element` implementation from this
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
this.queue.push(.{ .accept = try this.input.toOwnedSlice() });

View File

@@ -89,6 +89,8 @@ pub fn main() !void {
var clickable: Clickable = .{ .queue = &app.queue };
const element = clickable.element();
var button: App.Button(.accept) = .init(&app.queue, .init(.default, "Button"));
var quit_text: QuitText = .{};
var container = try App.Container.init(allocator, .{
@@ -98,6 +100,7 @@ pub fn main() !void {
defer container.deinit();
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element()));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
@@ -110,9 +113,8 @@ pub fn main() !void {
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.click => |button| {
log.info("Clicked with mouse using Button: {s}", .{button});
},
.click => |b| log.info("Clicked with mouse using Button: {s}", .{b}),
.accept => log.info("Clicked built-in button using the mouse", .{}),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
@@ -145,4 +147,5 @@ const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
click: [:0]const u8,
accept,
});

View File

@@ -24,70 +24,6 @@ const QuitText = struct {
}
};
const InputField = struct {
input: std.ArrayList(u21),
queue: *App.Queue,
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
return .{
.input = .init(allocator),
.queue = queue,
};
}
pub fn deinit(this: @This()) void {
this.input.deinit();
}
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| {
if (key.isAscii()) try this.input.append(key.cp);
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
this.queue.push(.{ .accept = try this.input.toOwnedSlice() });
if (key.eql(.{ .cp = zterm.input.Backspace }) or key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete }))
_ = this.input.pop();
},
else => {},
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.input.items.len == 0) return;
const row = 1;
const col = 1;
const anchor = (row * size.x) + col;
for (this.input.items, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .black;
cells[anchor + idx].style.cursor = false;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
if (idx == this.input.items.len - 1) cells[anchor + idx + 1].style.cursor = true;
}
}
};
const MouseDraw = struct {
position: ?zterm.Point = null,
@@ -133,7 +69,7 @@ pub fn main() !void {
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var input_field: InputField = .init(allocator, &app.queue);
var input_field: App.Input(.accept) = .init(allocator, &app.queue, .init(.black));
defer input_field.deinit();
var mouse_draw: MouseDraw = .{};
@@ -153,7 +89,7 @@ pub fn main() !void {
.rectangle = .{ .fill = .light_grey },
.size = .{
.grow = .horizontal,
.dim = .{ .y = 10 },
.dim = .{ .y = 1 },
},
}, input_field.element()));
@@ -191,10 +127,7 @@ pub fn main() !void {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.accept => |input| {
defer allocator.free(input);
var string = try allocator.alloc(u8, input.len);
defer allocator.free(string);
for (0.., input) |i, char| string[i] = @intCast(char);
log.debug("Accepted input '{s}'", .{string});
log.debug("Accepted input '{s}'", .{input});
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
@@ -226,6 +159,8 @@ const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const Color = zterm.Color;
const App = zterm.App(union(enum) {
accept: []u21,
accept: []u8,
});

247
examples/elements/popup.zig Normal file
View File

@@ -0,0 +1,247 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const MouseDraw = struct {
position: ?zterm.Point = null,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y },
else => this.position = null,
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.position) |pos| {
const idx = @as(usize, size.x) * @as(usize, pos.y) + @as(usize, pos.x);
cells[idx].cp = 'x';
cells[idx].style.fg = .red;
}
}
};
const Popup = struct {
container: ?*App.Container = null,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.resize = resize,
.reposition = reposition,
.handle = handle,
.content = content,
},
};
}
fn resize(ctx: *anyopaque, size: zterm.Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.container) |container| container.resize(size);
}
fn reposition(ctx: *anyopaque, _: zterm.Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.container) |container| container.reposition(.{});
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
// TODO should the `Element` handle the pop_down element triggering (i.e. by defining a usual key for this?)
.pop_up => |optional| if (optional) |container| {
this.container = @ptrCast(@alignCast(container));
} else {
this.container = null;
},
else => if (this.container) |container| try container.handle(event),
}
}
fn render_container(container: App.Container, cells: []zterm.Cell, container_size: zterm.Point) !void {
const size = container.size;
const origin = container.origin;
const contents = try container.content();
const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x);
var idx: usize = 0;
blk: for (0..size.y) |row| {
for (0..size.x) |col| {
cells[anchor + (row * container_size.x) + col] = contents[idx];
idx += 1;
if (contents.len == idx) break :blk;
}
}
// free immediately
container.allocator.free(contents);
for (container.elements.items) |child| try render_container(child, cells, size);
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.container) |container| {
assert(cells.len == @as(usize, container.size.x) * @as(usize, container.size.y));
const popup_cells = try container.content();
for (container.elements.items) |child| try render_container(child, popup_cells, size);
assert(cells.len == popup_cells.len);
@memcpy(cells, popup_cells);
container.allocator.free(popup_cells);
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var popup: Popup = .{};
var quit_text: QuitText = .{};
// TODO
// - rendering of first Container contents will be overwritten by contents of the appended Container's (see the missing blue rectangle)
// - when rendering "nothing" it causes the below Container to be overwritten to "nothing" too!
// - not sure how this should be done?
// - provide the pop-up with a area where to draw? (i.e. somewhere to draw the provided `Container`)
// - This however will not suffice as the contents will be overwritten!
var container = try App.Container.init(allocator, .{
.rectangle = .{
.fill = .blue,
},
}, .{});
defer container.deinit();
var popup_root_container = try App.Container.init(allocator, .{
.layout = .{
.padding = .{
.top = -17,
.left = -40,
.right = 5,
.bottom = 5,
},
},
}, .{});
try popup_root_container.append(try App.Container.init(allocator, .{}, popup.element()));
try container.append(try .init(allocator, .{}, quit_text.element()));
try container.append(popup_root_container); // FIXME it should not be appended (as it would become part of the layout)
var mouse: MouseDraw = .{};
var popup_container: App.Container = try .init(allocator, .{
.rectangle = .{ .fill = .green },
.layout = .{
.padding = .{
.top = -4,
.bottom = 1,
.left = 3,
.right = 3,
},
},
}, .{});
// showcase that inner `Container`s handle `Element`s accordingly
try popup_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, mouse.element()));
defer popup_container.deinit();
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
if (key.eql(.{ .cp = zterm.input.Space })) app.postEvent(.{ .pop_up = &popup_container });
if (key.eql(.{ .cp = zterm.input.Escape })) app.postEvent(.{ .pop_up = null });
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
pop_up: ?*anyopaque,
});

View File

@@ -0,0 +1,137 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var progress_percent: u8 = 0;
var progress: App.Progress(.progress) = .init(&app.queue, .{
.percent = .{ .enabled = true },
.fg = .green,
.bg = .grey,
});
var quit_text: QuitText = .{};
var container = try App.Container.init(allocator, .{
.layout = .{ .padding = .all(5) },
}, quit_text.element());
defer container.deinit();
try container.append(try App.Container.init(allocator, .{}, progress.element()));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
var framerate: u64 = 60;
var tick_ms: u64 = @divFloor(time.ms_per_s, framerate);
var next_frame_ms: u64 = 0;
var increase_progress: u64 = 10;
// Continuous drawing
// draw loop
draw: while (true) {
const now_ms: u64 = @intCast(time.milliTimestamp());
if (now_ms >= next_frame_ms) {
next_frame_ms = now_ms + tick_ms;
} else {
time.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
next_frame_ms += tick_ms;
}
// NOTE time based progress increasion
increase_progress -= 1;
if (increase_progress == 0) {
increase_progress = 10;
progress_percent += 1;
if (progress_percent > 100) progress_percent = 0;
app.postEvent(.{ .progress = progress_percent });
}
const len = blk: {
app.queue.lock();
defer app.queue.unlock();
break :blk app.queue.len();
};
// handle events
for (0..len) |_| {
const event = app.queue.drain() orelse break;
log.debug("handling event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
.focus => |b| {
// NOTE reduce framerate in case the window is not focused and restore again when focused
framerate = if (b) 60 else 15;
tick_ms = @divFloor(time.ms_per_s, framerate);
},
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break :draw,
else => {},
}
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const time = std.time;
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
progress: u8,
});

View File

@@ -55,7 +55,7 @@ pub fn App(comptime E: type) type {
handler_ctx = this;
var act = posix.Sigaction{
.handler = .{ .handler = handleWinch },
.mask = posix.empty_sigset,
.mask = posix.sigemptyset(),
.flags = 0,
};
posix.sigaction(posix.SIG.WINCH, &act, null);
@@ -288,12 +288,8 @@ pub fn App(comptime E: type) type {
.x = px -| 1,
.y = py -| 1,
.kind = blk: {
if (motion and button != Mouse.Button.none) {
break :blk .drag;
}
if (motion and button == Mouse.Button.none) {
break :blk .motion;
}
if (motion and button != Mouse.Button.none) break :blk .drag;
if (motion and button == Mouse.Button.none) break :blk .motion;
if (sequence[sequence.len - 1] == 'm') break :blk .release;
break :blk .press;
},
@@ -417,8 +413,10 @@ pub fn App(comptime E: type) type {
pub const Container = @import("container.zig").Container(Event);
pub const Element = element.Element(Event);
pub const Alignment = element.Alignment(Event);
pub const Button = element.Button(Event, Queue);
pub const Input = element.Input(Event, Queue);
pub const Progress = element.Progress(Event, Queue);
pub const Scrollable = element.Scrollable(Event);
pub const Exec = element.Exec(Event, Queue);
pub const Queue = queue.Queue(Event, 256);
};
}

View File

@@ -195,6 +195,33 @@ pub const Rectangle = packed struct {
}, &container, @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");
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.layout = .{
.padding = .{
.top = -18,
.bottom = -18,
.left = -28,
.right = -28,
},
},
.rectangle = .{ .fill = .green },
}, .{});
try container.append(try .init(std.testing.allocator, .{
.rectangle = .{ .fill = .white },
}, .{}));
try container.append(try .init(std.testing.allocator, .{}, .{}));
defer container.deinit();
try testing.expectContainerScreen(.{
.y = 20,
.x = 30,
}, &container, @import("test/container/rectangle_with_parent_padding.zon"));
}
test "fill color spacer with padding" {
const event = @import("event.zig");
const testing = @import("testing.zig");
@@ -293,23 +320,23 @@ pub const Layout = packed struct {
direction: enum(u1) { horizontal, vertical } = .horizontal,
/// Padding outside of the child elements
padding: packed struct {
top: u16 = 0,
bottom: u16 = 0,
left: u16 = 0,
right: u16 = 0,
top: i16 = 0,
bottom: i16 = 0,
left: i16 = 0,
right: i16 = 0,
/// Create a padding with equivalent padding in all four directions.
pub fn all(padding: u16) @This() {
pub fn all(padding: i16) @This() {
return .{ .top = padding, .bottom = padding, .left = padding, .right = padding };
}
/// Create a padding with equivalent padding in the left and right directions; others directions remain the default value.
pub fn horizontal(padding: u16) @This() {
pub fn horizontal(padding: i16) @This() {
return .{ .left = padding, .right = padding };
}
/// Create a padding with equivalent padding in the top and bottom directions; others directions remain the default value.
pub fn vertical(padding: u16) @This() {
pub fn vertical(padding: i16) @This() {
return .{ .top = padding, .bottom = padding };
}
} = .{},
@@ -326,6 +353,11 @@ pub const Layout = packed struct {
} = .line,
} = .{},
/// Calculate the absolute offset for the provided `padding` if it is negative to get the absolute padding for the given `size`.
pub fn getAbsolutePadding(padding: i16, size: u16) u16 {
return if (padding >= 0) @intCast(padding) else size -| @as(u16, @intCast(-padding));
}
pub fn content(this: @This(), comptime C: type, cells: []Cell, origin: Point, size: Point, children: []const C) void {
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -566,6 +598,7 @@ pub fn Container(comptime Event: type) type {
size: Point,
properties: Properties,
element: Element,
// TODO this should be renamed to `children`
elements: std.ArrayList(@This()),
/// Properties for each `Container` to configure their layout,
@@ -593,10 +626,8 @@ pub fn Container(comptime Event: type) type {
};
}
pub fn deinit(this: *@This()) void {
for (this.elements.items) |*element| {
element.deinit();
}
pub fn deinit(this: *const @This()) void {
for (this.elements.items) |*element| element.deinit();
this.elements.deinit();
}
@@ -610,8 +641,8 @@ pub fn Container(comptime Event: type) type {
this.element.reposition(origin);
var offset = origin.add(.{
.x = layout.padding.left,
.y = layout.padding.top,
.x = Layout.getAbsolutePadding(layout.padding.left, this.size.x),
.y = Layout.getAbsolutePadding(layout.padding.top, this.size.y),
});
const sides = this.properties.border.sides;
@@ -643,8 +674,8 @@ pub fn Container(comptime Event: type) type {
};
if (this.elements.items.len > 0) switch (layout.direction) {
.horizontal => size.x += layout.padding.left + layout.padding.right,
.vertical => size.y += layout.padding.top + layout.padding.bottom,
.horizontal => size.x += Layout.getAbsolutePadding(layout.padding.left, this.size.x) + Layout.getAbsolutePadding(layout.padding.right, this.size.x),
.vertical => size.y += Layout.getAbsolutePadding(layout.padding.top, this.size.y) + Layout.getAbsolutePadding(layout.padding.bottom, this.size.y),
};
const sides = this.properties.border.sides;
@@ -692,16 +723,16 @@ pub fn Container(comptime Event: type) type {
fn grow_resize(this: *@This(), max_size: Point) void {
const layout = this.properties.layout;
var remainder = switch (layout.direction) {
.horizontal => max_size.x -| (layout.padding.left + layout.padding.right),
.vertical => max_size.y -| (layout.padding.top + layout.padding.bottom),
.horizontal => max_size.x -| (Layout.getAbsolutePadding(layout.padding.left, this.size.x) + Layout.getAbsolutePadding(layout.padding.right, this.size.x)),
.vertical => max_size.y -| (Layout.getAbsolutePadding(layout.padding.top, this.size.y) + Layout.getAbsolutePadding(layout.padding.bottom, this.size.y)),
};
remainder -|= layout.gap * @as(u16, @truncate(this.elements.items.len -| 1));
if (layout.separator.enabled) remainder -|= @as(u16, @truncate(this.elements.items.len -| 1));
var available = switch (layout.direction) {
.horizontal => max_size.y -| (layout.padding.top + layout.padding.bottom),
.vertical => max_size.x -| (layout.padding.left + layout.padding.right),
.horizontal => max_size.y -| (Layout.getAbsolutePadding(layout.padding.top, this.size.y) + Layout.getAbsolutePadding(layout.padding.bottom, this.size.y)),
.vertical => max_size.x -| (Layout.getAbsolutePadding(layout.padding.left, this.size.x) + Layout.getAbsolutePadding(layout.padding.right, this.size.x)),
};
const sides = this.properties.border.sides;
@@ -773,6 +804,7 @@ pub fn Container(comptime Event: type) type {
for (this.elements.items) |child| {
if (child.properties.size.grow == .fixed) continue;
switch (layout.direction) {
.horizontal => if (child.properties.size.grow == .vertical) continue,
.vertical => if (child.properties.size.grow == .horizontal) continue,
@@ -794,10 +826,8 @@ pub fn Container(comptime Event: type) type {
size_to_correct = @min(size_to_correct, remainder / growable_children);
var overflow: u16 = 0;
if (size_to_correct == 0 and remainder > 0) {
// there is some overflow
overflow = remainder;
}
if (size_to_correct == 0 and remainder > 0) overflow = remainder;
for (this.elements.items) |*child| {
const child_size = switch (layout.direction) {
.horizontal => child.size.x,
@@ -838,6 +868,7 @@ pub fn Container(comptime Event: type) type {
pub fn resize(this: *@This(), size: Point) void {
// NOTE assume that this function is only called for the root `Container`
this.size = size;
const fit_size = this.fit_resize();
// if (fit_size.y > size.y or fit_size.x > size.x) @panic("error: cannot render in available space");
switch (this.properties.size.grow) {
@@ -855,7 +886,7 @@ pub fn Container(comptime Event: type) type {
this.grow_resize(this.size);
}
pub fn handle(this: *@This(), event: Event) !void {
pub fn handle(this: *const @This(), event: Event) !void {
switch (event) {
.mouse => |mouse| if (mouse.in(this.origin, this.size)) {
// the element receives the mouse event with relative position
@@ -873,11 +904,12 @@ pub fn Container(comptime Event: type) type {
}
}
pub fn content(this: *const @This()) ![]const Cell {
pub fn content(this: *const @This()) ![]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));
@memset(cells, .{});
errdefer this.allocator.free(cells);
@memset(cells, .{});
this.properties.layout.content(@This(), cells, this.origin, this.size, this.elements.items);
this.properties.border.content(cells, this.size);

View File

@@ -182,6 +182,7 @@ pub fn Scrollable(Event: type) type {
configuration: Configuration,
pub const Configuration = packed struct {
// TODO the scrollbar bool and the color should be in their own struct using `enabled` and `color` inside of the struct to be more consistent with other `Configuration` structs
scrollbar: bool,
color: Color = .default,
x_axis: bool = false,
@@ -281,7 +282,6 @@ pub fn Scrollable(Event: type) type {
const size = container.size;
const origin = container.origin;
const contents = try container.content();
defer container.allocator.free(contents);
const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x);
@@ -294,6 +294,8 @@ pub fn Scrollable(Event: type) type {
if (contents.len == idx) break :blk;
}
}
// free immediately
container.allocator.free(contents);
for (container.elements.items) |child| try render_container(child, cells, size);
}
@@ -306,13 +308,7 @@ pub fn Scrollable(Event: type) type {
const offset_y: usize = if (this.configuration.y_axis) 1 else 0;
const container_size = this.container.size;
const container_cells = try this.container.allocator.alloc(Cell, @as(usize, container_size.x) * @as(usize, container_size.y));
{
const container_cells_const = try this.container.content();
defer this.container.allocator.free(container_cells_const);
assert(container_cells_const.len == @as(usize, container_size.x) * @as(usize, container_size.y));
@memcpy(container_cells, container_cells_const);
}
const container_cells = try this.container.content();
for (this.container.elements.items) |child| try render_container(child, container_cells, container_size);
@@ -365,8 +361,395 @@ pub fn Scrollable(Event: type) type {
};
}
pub fn Input(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type {
// NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
const input_struct = struct {
pub fn input_fn(accept_event: meta.FieldEnum(Event)) type {
const event_type: enum { ascii, utf8 } = blk: { // check for type correctness and the associated type to use for the passed `accept_event`
const err_msg = "Unexpected type for the associated input completion event to trigger. Only `[]u8` or `[]u21` are allowed.";
switch (@typeInfo(@FieldType(Event, @tagName(accept_event)))) {
.pointer => |pointer| {
if (pointer.size != .slice) @compileError(err_msg);
switch (@typeInfo(pointer.child)) {
.int => |num| {
if (num.signedness != .unsigned) @compileError(err_msg);
switch (num.bits) {
8 => break :blk .ascii,
21 => break :blk .utf8,
else => @compileError(err_msg),
}
},
else => @compileError(err_msg),
}
},
else => @compileError(err_msg),
}
};
return struct {
/// Offset from the end describing the current position of the cursor.
cursor_offset: usize = 0,
/// Configuration for the InputField.
configuration: Configuration,
/// Array holding the value of the input.
input: std.ArrayList(u21),
/// Reference to the app's queue to issue the associated event to trigger when completing the input.
queue: *Queue,
/// Configuration for InputField's.
pub const Configuration = packed struct {
color: Color,
pub fn init(color: Color) @This() {
return .{ .color = color };
}
};
pub fn init(allocator: std.mem.Allocator, queue: *Queue, configuration: Configuration) @This() {
return .{
.configuration = configuration,
.input = .init(allocator),
.queue = queue,
};
}
pub fn deinit(this: @This()) void {
this.input.deinit();
}
pub fn element(this: *@This()) Element(Event) {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| {
assert(this.cursor_offset >= 0 and this.cursor_offset <= this.input.items.len);
// readline commands
if (key.eql(.{ .cp = input.Left }) or key.eql(.{ .cp = input.KpLeft }) or key.eql(.{ .cp = 'b', .mod = .{ .ctrl = true } })) {
if (this.cursor_offset < this.input.items.len) this.cursor_offset += 1;
}
if (key.eql(.{ .cp = input.Right }) or key.eql(.{ .cp = input.KpRight }) or key.eql(.{ .cp = 'f', .mod = .{ .ctrl = true } }))
this.cursor_offset -|= 1;
if (key.eql(.{ .cp = 'e', .mod = .{ .ctrl = true } })) this.cursor_offset = 0;
if (key.eql(.{ .cp = 'a', .mod = .{ .ctrl = true } })) this.cursor_offset = this.input.items.len;
if (key.eql(.{ .cp = 'k', .mod = .{ .ctrl = true } })) {
while (this.cursor_offset > 0) {
_ = this.input.pop();
this.cursor_offset -= 1;
}
}
if (key.eql(.{ .cp = 'u', .mod = .{ .ctrl = true } })) {
const len = this.input.items.len - this.cursor_offset;
for (0..len) |_| _ = this.input.orderedRemove(0);
this.cursor_offset = this.input.items.len;
}
if (key.eql(.{ .cp = 'w', .mod = .{ .ctrl = true } })) {
var non_whitespace = false;
while (this.cursor_offset < this.input.items.len) {
if (non_whitespace and this.input.items[this.input.items.len - this.cursor_offset - 1] == ' ') break;
// see backspace
const removed = if (this.cursor_offset == 0) this.input.pop() else this.input.orderedRemove(this.input.items.len - this.cursor_offset - 1);
if (removed != ' ') non_whitespace = true;
}
}
if (key.eql(.{ .cp = 'b', .mod = .{ .alt = true } }) or key.eql(.{ .cp = input.Left, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = input.KpLeft, .mod = .{ .ctrl = true } })) {
var non_whitespace = false;
while (this.cursor_offset < this.input.items.len) {
if (non_whitespace and this.input.items[this.input.items.len - this.cursor_offset - 1] == ' ') break;
// see backspace
this.cursor_offset += 1;
if (this.cursor_offset == this.input.items.len) break;
const next = this.input.items[this.input.items.len - this.cursor_offset - 1];
if (next != ' ') non_whitespace = true;
}
}
if (key.eql(.{ .cp = 'f', .mod = .{ .alt = true } }) or key.eql(.{ .cp = input.Right, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = input.KpRight, .mod = .{ .ctrl = true } })) {
var non_whitespace = false;
while (this.cursor_offset > 0) {
if (non_whitespace and this.input.items[this.input.items.len - this.cursor_offset - 1] == ' ') {
this.cursor_offset += 1; // correct cursor position back again to make sure the cursor is not on the whitespace, but at the end of the jumped word
break;
}
// see backspace
this.cursor_offset -= 1;
const next = this.input.items[this.input.items.len - this.cursor_offset - 1];
if (next != ' ') non_whitespace = true;
}
}
// usual input keys
if (key.isAscii()) try this.input.insert(this.input.items.len - this.cursor_offset, key.cp);
if (key.eql(.{ .cp = input.Backspace })) {
if (this.cursor_offset < this.input.items.len) {
_ = if (this.cursor_offset == 0) this.input.pop() else this.input.orderedRemove(this.input.items.len - this.cursor_offset - 1);
}
}
if (key.eql(.{ .cp = input.Delete }) or key.eql(.{ .cp = input.KpDelete })) {
if (this.cursor_offset > 0) {
_ = this.input.orderedRemove(this.input.items.len - this.cursor_offset);
this.cursor_offset -= 1;
}
}
// TODO enter to accept?
// - shift+enter is not recognized by the input reader of `zterm`, so currently it is not possible to add newlines into the text box?
if (key.eql(.{ .cp = input.Enter }) or key.eql(.{ .cp = input.KpEnter })) {
switch (event_type) {
.ascii => {
// NOTE convert unicode characters to ascii characters; if non ascii characters are found this is will fail!
var slice = try this.input.allocator.alloc(u8, this.input.items.len);
for (0.., this.input.items) |i, c| slice[i] = @intCast(c);
this.input.clearAndFree();
this.queue.push(@unionInit(
Event,
@tagName(accept_event),
slice,
));
},
.utf8 => this.queue.push(@unionInit(
Event,
@tagName(accept_event),
try this.input.toOwnedSlice(),
)),
}
this.cursor_offset = 0;
}
},
else => {},
}
}
fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const offset = if (this.input.items.len - this.cursor_offset + 1 >= cells.len) this.input.items.len + 1 - cells.len else 0;
for (this.input.items[offset..], 0..) |cp, idx| {
cells[idx].style.fg = this.configuration.color;
cells[idx].cp = cp;
// display ellipse at the beginning
if (offset > 0 and idx == 0) cells[idx].cp = '…';
// NOTE do not write over the contents of this `Container`'s `Size`
if (idx == cells.len - 1) {
// display ellipse at the end
if (this.input.items.len >= cells.len and this.cursor_offset > 0) cells[idx].cp = '…';
break;
}
}
if (this.input.items.len < cells.len)
cells[this.input.items.len - this.cursor_offset].style.cursor = true
else
cells[this.input.items.len - offset - this.cursor_offset].style.cursor = true;
}
};
}
};
return input_struct.input_fn;
}
pub fn Button(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type {
// NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
const button_struct = struct {
pub fn button_fn(accept_event: meta.FieldEnum(Event)) type {
{ // check for type correctness and the associated type to use for the passed `accept_event`
const err_msg = "Unexpected type for the associated input completion event to trigger. Only `void` is allowed.";
switch (@typeInfo(@FieldType(Event, @tagName(accept_event)))) {
.void => |_| {},
else => @compileError(err_msg),
}
}
return struct {
queue: *Queue,
configuration: Configuration,
/// Configuration for InputField's.
pub const Configuration = struct {
color: Color,
text: []const u8,
pub fn init(color: Color, text: []const u8) @This() {
return .{
.color = color,
.text = text,
};
}
};
pub fn init(queue: *Queue, configuration: Configuration) @This() {
return .{
.queue = queue,
.configuration = configuration,
};
}
pub fn element(this: *@This()) Element(Event) {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
// TODO should this also support key presses to accept?
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) this.queue.push(accept_event),
else => {},
}
}
fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
// NOTE center text in the middle of the available cell slice
const row = size.y / 2 -| (this.configuration.text.len / 2);
const col = size.x / 2 -| (this.configuration.text.len / 2);
const anchor = (row * size.x) + col;
for (0.., this.configuration.text) |idx, cp| {
cells[anchor + idx].style.fg = this.configuration.color;
cells[anchor + idx].style.emphasis = &.{.bold};
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
}
};
return button_struct.button_fn;
}
pub fn Progress(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type {
// NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
const progress_struct = struct {
pub fn progress_fn(progress_event: meta.FieldEnum(Event)) type {
{ // check for type correctness and the associated type to use for the passed `progress_event`
const err_msg = "Unexpected type for the associated input completion event to trigger. Only `u8` is allowed.";
switch (@typeInfo(@FieldType(Event, @tagName(progress_event)))) {
.int => |num| {
if (num.signedness != .unsigned) @compileError(err_msg);
switch (num.bits) {
8 => {},
else => @compileError(err_msg),
}
},
else => @compileError(err_msg),
}
}
return struct {
configuration: Configuration,
progress: u8 = 0,
queue: *Queue,
pub const Configuration = packed struct {
/// Control whether the percentage of the progress be rendered.
percent: packed struct {
enabled: bool = false,
alignment: enum(u2) { left, middle, right } = .middle,
} = .{},
/// Foreground color to use for the progress bar.
fg: Color,
/// Background color to use for the progress bar.
bg: Color,
/// Code point to use for rendering the progress bar with.
cp: u21 = '━',
};
pub fn init(queue: *Queue, configuration: Configuration) @This() {
return .{
.configuration = configuration,
.queue = queue,
};
}
pub fn element(this: *@This()) Element(Event) {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
// TODO should this `Element` trigger a completion event? (I don't think that this is useful?)
switch (event) {
progress_event => |value| {
assert(value >= 0 and value <= 100);
this.progress = value;
},
else => {},
}
}
fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const ratio: f32 = @as(f32, @floatFromInt(size.x)) / 100.0;
const scrollbar_end_pos: usize = @intFromFloat(ratio * @as(f32, @floatFromInt(this.progress)));
// NOTE remain `undefined` in case percentage rendering is not enabled
var percent_buf: [6]u8 = undefined;
var percent_buf_len: usize = undefined;
if (this.configuration.percent.enabled) percent_buf_len = (try std.fmt.bufPrint(&percent_buf, "[{d: >3}%]", .{this.progress})).len;
for (0..size.x) |idx| {
// NOTE the progress bar is rendered at the bottom row of the `Container` taking its entire columns
cells[((size.y - 1) * size.x) + idx] = .{
.style = .{
.fg = if (scrollbar_end_pos > 0 and idx <= scrollbar_end_pos) this.configuration.fg else this.configuration.bg,
.emphasis = &.{},
},
.cp = if (this.configuration.percent.enabled) switch (this.configuration.percent.alignment) {
.left => if (idx < percent_buf_len) percent_buf[idx] else this.configuration.cp,
.middle => if (idx >= ((size.x - percent_buf_len) / 2) and idx < ((size.x - percent_buf_len) / 2) + percent_buf_len) percent_buf[percent_buf_len - (((size.x - percent_buf_len) / 2) + percent_buf_len - idx - 1) - 1] else this.configuration.cp,
.right => if (idx >= size.x - percent_buf_len) percent_buf[percent_buf_len - ((size.x - idx + percent_buf_len - 1) % percent_buf_len) - 1] else this.configuration.cp,
} else this.configuration.cp,
};
// NOTE do not write over the contents of this `Container`'s `Size`
if (idx == cells.len - 1) break;
}
}
};
}
};
return progress_struct.progress_fn;
}
const std = @import("std");
const assert = std.debug.assert;
const meta = std.meta;
const build_options = @import("build_options");
const input = @import("input.zig");
const Container = @import("container.zig").Container;
@@ -820,3 +1203,196 @@ test "alignment bottom" {
.y = 20,
}, &container, @import("test/element/alignment.bottom.zon"));
}
test "input element" {
// FIX correctly generate the `.zon` files for the cell equivalence test (see below)
const allocator = std.testing.allocator;
const event = @import("event.zig");
const Event = event.mergeTaggedUnions(event.SystemEvent, union(enum) {
accept: []u21,
});
const testing = @import("testing.zig");
const Queue = @import("queue").Queue(Event, 256);
var container: Container(Event) = try .init(allocator, .{}, .{});
defer container.deinit();
const size: Point = .{
.x = 30,
.y = 20,
};
var queue: Queue = .{};
const input_container: Container(Event) = try .init(allocator, .{
.rectangle = .{ .fill = .green },
.size = .{
.dim = .{ .x = 12, .y = 2 },
.grow = .fixed,
},
}, .{});
var input_element: Input(Event, Queue)(.accept) = .init(input_container, &queue, .{.black});
defer input_element.deinit();
try container.append(try .init(allocator, .{}, input_element.element()));
var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit();
container.resize(size);
container.reposition(.{});
try renderer.render(Container(event.SystemEvent), &container);
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.without.text.zon"), renderer.screen);
// press 'a' 15 times
for (0..15) |_| try container.handle(.{
.key = .{ .cp = 'a' },
});
try renderer.render(Container(event.SystemEvent), &container);
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.zon"), renderer.screen);
// press 'a' 15 times
for (0..15) |_| try container.handle(.{
.key = .{ .cp = 'a' },
});
try renderer.render(Container(event.SystemEvent), &container);
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/input.with.text.overflow.zon"), renderer.screen);
// test the accepting of the `Element`
try container.handle(.{
.key = .{ .cp = input.Enter },
});
const accept_event = queue.pop();
try std.testing.expectEqual(.accept, std.meta.activeTag(accept_event));
try std.testing.expectEqual(30, switch (accept_event) {
.accept => |input_content| input_content.len,
else => unreachable,
});
// free allocated resources
switch (accept_event) {
.accept => |slice| allocator.free(slice),
else => unreachable,
}
}
test "button" {
const allocator = std.testing.allocator;
const event = @import("event.zig");
const Event = event.mergeTaggedUnions(event.SystemEvent, union(enum) {
accept,
});
const testing = @import("testing.zig");
const Queue = @import("queue").Queue(Event, 256);
const size: Point = .{
.x = 30,
.y = 20,
};
var queue: Queue = .{};
var container: Container(Event) = try .init(allocator, .{}, .{});
defer container.deinit();
var button: Button(Event, Queue)(.accept) = .init(&queue, .init(.default, "Button"));
const button_container: Container(Event) = try .init(allocator, .{
.rectangle = .{ .fill = .blue },
}, button.element());
try container.append(button_container);
var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit();
container.resize(size);
container.reposition(.{});
try renderer.render(Container(Event), &container);
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/button.zon"), renderer.screen);
// test the accepting of the `Element`
try container.handle(.{
.mouse = .{
.x = 5,
.y = 3,
.button = .left,
.kind = .release,
},
});
try std.testing.expect(switch (queue.pop()) {
.accept => true,
else => false,
});
}
test "progress" {
const allocator = std.testing.allocator;
const event = @import("event.zig");
const Event = event.mergeTaggedUnions(event.SystemEvent, union(enum) {
progress: u8,
});
const testing = @import("testing.zig");
const Queue = @import("queue").Queue(Event, 256);
const size: Point = .{
.x = 30,
.y = 20,
};
var container: Container(Event) = try .init(allocator, .{
.layout = .{
.padding = .all(1),
},
.rectangle = .{ .fill = .white },
}, .{});
defer container.deinit();
var progress: Progress(Event, Queue)(.progress) = .init(&.{}, .{
.percent = .{ .enabled = true },
.fg = .green,
.bg = .grey,
});
const progress_container: Container(Event) = try .init(allocator, .{}, progress.element());
try container.append(progress_container);
var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit();
container.resize(size);
container.reposition(.{});
try renderer.render(Container(Event), &container);
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_zero.zon"), renderer.screen);
// test the progress of the `Element`
try container.handle(.{
.progress = 25,
});
container.resize(size);
container.reposition(.{});
try renderer.render(Container(Event), &container);
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_quarter.zon"), renderer.screen);
// test the progress of the `Element`
try container.handle(.{
.progress = 50,
});
container.resize(size);
container.reposition(.{});
try renderer.render(Container(Event), &container);
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_half.zon"), renderer.screen);
// test the progress of the `Element`
try container.handle(.{
.progress = 75,
});
container.resize(size);
container.reposition(.{});
try renderer.render(Container(Event), &container);
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_three_quarter.zon"), renderer.screen);
// test the progress of the `Element`
try container.handle(.{
.progress = 100,
});
container.resize(size);
container.reposition(.{});
try renderer.render(Container(Event), &container);
// try testing.expectEqualCells(.{}, renderer.size, @import("test/element/progress_one_hundred.zon"), renderer.screen);
}