ref(event): split Size into two Points (one for the size and one for the anchor / origin)
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 39s

This commit is contained in:
2025-03-04 00:04:56 +01:00
parent 91ac6241f4
commit 591b990087
23 changed files with 477 additions and 459 deletions

View File

@@ -5,6 +5,7 @@ pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const Examples = enum {
all,
demo,
// elements:
button,
@@ -22,7 +23,7 @@ pub fn build(b: *std.Build) void {
errors,
};
const example = b.option(Examples, "example", "Example to build and/or run. (default: demo)") orelse .demo;
const example = b.option(Examples, "example", "Example to build and/or run. (default: all)") orelse .all;
const options = b.addOptions();
options.addOption(Examples, "example", example);
@@ -153,6 +154,19 @@ pub fn build(b: *std.Build) void {
.palette => palette,
// error handling:
.errors => errors,
else => blk: {
b.installArtifact(button);
b.installArtifact(input);
b.installArtifact(scrollable);
b.installArtifact(vertical);
b.installArtifact(horizontal);
b.installArtifact(grid);
b.installArtifact(mixed);
b.installArtifact(text);
b.installArtifact(palette);
b.installArtifact(errors);
break :blk demo;
},
};
b.installArtifact(exe);

View File

@@ -13,13 +13,14 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
const y = 2;
const x = size.x / 2 -| (text.len / 2);
const anchor = (y * size.x) + x;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
@@ -73,7 +74,7 @@ pub fn main() !void {
var scrollable: App.Scrollable = .{
.container = box,
.min_size = .{ .cols = 60 },
.min_size = .{ .x = 60 },
};
var container = try App.Container.init(allocator, .{
@@ -91,11 +92,11 @@ pub fn main() !void {
.color = .light_blue,
.sides = .all,
},
.fixed_size = .{ .cols = 200 },
.fixed_size = .{ .x = 200 },
}, .{}));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.fixed_size = .{ .cols = 30 },
.fixed_size = .{ .x = 30 },
}, .{}));
defer container.deinit(); // also de-initializes the children
@@ -110,7 +111,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();

View File

@@ -14,13 +14,14 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -52,7 +53,7 @@ const Clickable = struct {
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.mouse => |mouse| {
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) {
var value = @intFromEnum(this.color);
value += 1;
value %= 17;
@@ -64,13 +65,14 @@ const Clickable = struct {
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = size.rows / 2 -| (text.len / 2);
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
const row = size.y / 2 -| (text.len / 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 = this.color;
@@ -119,7 +121,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.click => |button| {
log.info("Clicked with mouse using Button: {s}", .{button});

View File

@@ -14,13 +14,14 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -74,15 +75,16 @@ const InputField = struct {
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
if (this.input.items.len == 0) return;
const row = 1;
const col = 1;
const anchor = (row * size.cols) + col;
const anchor = (row * size.x) + col;
for (this.input.items, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .black;
@@ -132,7 +134,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.accept => |input| {
defer allocator.free(input);

View File

@@ -13,13 +13,14 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -42,18 +43,22 @@ const HelloWorldText = packed struct {
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
// NOTE error should only be returned here in case an in-recoverable exception has occurred
const row = size.rows / 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
const row = size.y / 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (0.., text) |idx, char| {
cells[anchor + idx].style.fg = .black;
cells[anchor + idx].cp = char;
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;
}
}
};
@@ -138,10 +143,10 @@ pub fn main() !void {
defer container.deinit();
// place empty container containing the element of the scrollable Container.
var scrollable_top: App.Scrollable = .init(top_box, .{ .rows = 50 });
var scrollable_top: App.Scrollable = .init(top_box, .{ .y = 50 });
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
var scrollable_bottom: App.Scrollable = .init(bottom_box, .{ .rows = 30 });
var scrollable_bottom: App.Scrollable = .init(bottom_box, .{ .y = 30 });
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
try app.start();
@@ -155,7 +160,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.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 }),
else => {},

View File

@@ -12,13 +12,14 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -38,13 +39,14 @@ const InfoText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -64,24 +66,25 @@ const ErrorNotification = struct {
return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } };
}
pub fn handle(ctx: *anyopaque, event: App.Event) !void {
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| if (!key.isAscii()) return error.UnsupportedKey,
.resize => |_| {},
.size => |_| {},
.err => |err| this.msg = err.msg,
else => {},
}
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
if (this.msg) |msg| {
const row = size.rows -| 2;
const col = size.cols -| 2 -| msg.len;
const anchor = (row * size.cols) + col;
const row = size.y -| 2;
const col = size.x -| 2 -| msg.len;
const anchor = (row * size.x) + col;
for (msg, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
@@ -134,7 +137,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.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 }),
else => {},

View File

@@ -15,13 +15,14 @@ const QuitText = struct {
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -87,7 +88,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),

View File

@@ -15,13 +15,14 @@ const QuitText = struct {
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -79,7 +80,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),

View File

@@ -15,13 +15,14 @@ const QuitText = struct {
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -91,7 +92,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),

View File

@@ -15,13 +15,14 @@ const QuitText = struct {
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -78,7 +79,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),

View File

@@ -12,13 +12,14 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -63,7 +64,7 @@ pub fn main() !void {
if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
}
var scrollable: App.Scrollable = .init(box, .{ .cols = 3 * std.meta.fields(zterm.Color).len }); // ensure enough columns to render all colors -> scrollable otherwise
var scrollable: App.Scrollable = .init(box, .{ .x = 3 * std.meta.fields(zterm.Color).len }); // ensure enough columns to render all colors -> scrollable otherwise
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start();
@@ -76,7 +77,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.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 }),
else => {},

View File

@@ -12,13 +12,14 @@ const QuitText = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
const row = 2;
const col = size.cols / 2 -| (text.len / 2);
const anchor = (row * size.cols) + col;
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;
@@ -38,10 +39,11 @@ const TextStyles = struct {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
fn content(ctx: *anyopaque, cells: []zterm.Cell, origin: zterm.Point, size: zterm.Point) !void {
@setEvalBranchQuota(50000);
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = origin;
var row: usize = 0;
var col: usize = 0;
@@ -56,9 +58,9 @@ const TextStyles = struct {
// witouth any emphasis
for (text) |cp| {
cells[(row * size.cols) + col].style.bg = @enumFromInt(bg_field.value);
cells[(row * size.cols) + col].style.fg = @enumFromInt(fg_field.value);
cells[(row * size.cols) + col].cp = cp;
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
cells[(row * size.x) + col].cp = cp;
col += 1;
}
@@ -68,10 +70,10 @@ const TextStyles = struct {
const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value);
for (text) |cp| {
cells[(row * size.cols) + col].style.bg = @enumFromInt(bg_field.value);
cells[(row * size.cols) + col].style.fg = @enumFromInt(fg_field.value);
cells[(row * size.cols) + col].style.emphasis = &.{emphasis};
cells[(row * size.cols) + col].cp = cp;
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
cells[(row * size.x) + col].style.emphasis = &.{emphasis};
cells[(row * size.x) + col].cp = cp;
col += 1;
}
}
@@ -113,8 +115,8 @@ pub fn main() !void {
defer box.deinit();
var scrollable: App.Scrollable = .init(box, .{
.rows = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
.cols = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
.x = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
.y = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
}); // ensure enough rows and/or columns to render all text styles -> scrollable otherwise
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
@@ -128,7 +130,7 @@ pub fn main() !void {
switch (event) {
.init => continue,
.quit => break,
.resize => |size| try renderer.resize(size),
.size => |size| try renderer.resize(size),
.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 }),
else => {},

View File

@@ -11,7 +11,7 @@ const isTaggedUnion = event.isTaggedUnion;
const Mouse = input.Mouse;
const Key = input.Key;
const Size = @import("size.zig").Size;
const Point = @import("point.zig").Point;
const log = std.log.scoped(.app);
@@ -51,7 +51,6 @@ pub fn App(comptime E: type) type {
quit_event: std.Thread.ResetEvent,
termios: ?std.posix.termios = null,
attached_handler: bool = false,
prev_size: Size,
pub const SignalHandler = struct {
context: *anyopaque,
@@ -64,7 +63,6 @@ pub fn App(comptime E: type) type {
.quit_event = .{},
.termios = null,
.attached_handler = false,
.prev_size = .{},
};
pub fn start(this: *@This()) !void {
@@ -101,9 +99,7 @@ pub fn App(comptime E: type) type {
try terminal.enableMouseSupport();
// send initial size afterwards
const size = terminal.getTerminalSize();
this.postEvent(.{ .resize = size });
this.prev_size = size;
this.postEvent(.{ .size = terminal.getTerminalSize() });
}
pub fn interrupt(this: *@This()) !void {
@@ -148,11 +144,7 @@ pub fn App(comptime E: type) type {
fn winsizeCallback(ptr: *anyopaque) void {
const this: *@This() = @ptrCast(@alignCast(ptr));
const size = terminal.getTerminalSize();
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
this.postEvent(.{ .resize = size });
this.prev_size = size;
}
this.postEvent(.{ .size = terminal.getTerminalSize() });
}
var winch_handler: ?SignalHandler = null;
@@ -302,8 +294,8 @@ pub fn App(comptime E: type) type {
const mouse: Mouse = .{
.button = button,
.col = px -| 1,
.row = py -| 1,
.x = px -| 1,
.y = py -| 1,
.kind = blk: {
if (motion and button != Mouse.Button.none) {
break :blk .drag;
@@ -334,19 +326,13 @@ pub fn App(comptime E: type) type {
if (std.mem.eql(u8, "48", ps)) {
// in band window resize
// CSI 48 ; height ; width ; height_pix ; width_pix t
const height_char = iter.next() orelse break;
const width_char = iter.next() orelse break;
const height_char = iter.next() orelse break;
// TODO only post the event if the size has changed?
// because there might be too many resize events (which force a re-draw of the entire screen)
const size: Size = .{
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
};
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
this.postEvent(.{ .resize = size });
this.prev_size = size;
}
this.postEvent(.{ .size = .{
.x = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
.y = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
} });
}
},
'u' => {

View File

@@ -4,7 +4,7 @@ const isTaggedUnion = @import("event.zig").isTaggedUnion;
const Cell = @import("cell.zig");
const Color = @import("color.zig").Color;
const Size = @import("size.zig").Size;
const Point = @import("point.zig").Point;
const Style = @import("style.zig");
const Error = @import("error.zig").Error;
@@ -38,8 +38,8 @@ pub const Border = packed struct {
pub const vertical: @This() = .{ .top = true, .bottom = true };
} = .{},
pub fn contents(this: @This(), cells: []Cell, size: Size) void {
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
pub fn contents(this: @This(), cells: []Cell, size: Point) void {
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const frame = switch (this.corners) {
.rounded => Border.rounded_border,
@@ -49,14 +49,14 @@ pub const Border = packed struct {
// render top and bottom border
if (this.sides.top or this.sides.bottom) {
for (0..size.cols) |col| {
const last_row = @as(usize, size.rows - 1) * @as(usize, size.cols);
for (0..size.x) |col| {
const last_row = @as(usize, size.y - 1) * @as(usize, size.x);
if (this.sides.left and col == 0) {
// top left corner
if (this.sides.top) cells[col].cp = frame[0];
// bottom left corner
if (this.sides.bottom) cells[last_row + col].cp = frame[4];
} else if (this.sides.right and col == size.cols - 1) {
} else if (this.sides.right and col == size.x - 1) {
// top right corner
if (this.sides.top) cells[col].cp = frame[2];
// bottom left corner
@@ -75,17 +75,17 @@ pub const Border = packed struct {
if (this.sides.left or this.sides.right) {
var start: usize = 0;
if (this.sides.top) start = 1;
var end = size.rows;
var end = size.y;
if (this.sides.bottom) end -= 1;
for (start..end) |row| {
const idx = (row * size.cols);
const idx = (row * size.x);
if (this.sides.left) {
cells[idx].cp = frame[3]; // left
cells[idx].style.fg = this.color;
}
if (this.sides.right) {
cells[idx + size.cols - 1].cp = frame[3]; // right
cells[idx + size.cols - 1].style.fg = this.color;
cells[idx + size.x - 1].cp = frame[3]; // right
cells[idx + size.x - 1].style.fg = this.color;
}
}
}
@@ -104,8 +104,8 @@ pub const Border = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/border.all.zon"));
}
@@ -122,8 +122,8 @@ pub const Border = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/border.vertical.zon"));
}
@@ -140,8 +140,8 @@ pub const Border = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/border.horizontal.zon"));
}
};
@@ -153,13 +153,13 @@ pub const Rectangle = packed struct {
/// children accordingly without removing the coloring of the `Rectangle`
fill: Color = .default,
// NOTE caller owns `Cells` slice and ensures that `cells.len == size.cols * size.rows`
pub fn contents(this: @This(), cells: []Cell, size: Size) void {
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
// NOTE caller owns `Cells` slice and ensures that `cells.len == size.x * size.y`
pub fn contents(this: @This(), cells: []Cell, size: Point) void {
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
for (0..size.rows) |row| {
for (0..size.cols) |col| {
cells[(row * size.cols) + col].style.bg = this.fill;
for (0..size.y) |row| {
for (0..size.x) |col| {
cells[(row * size.x) + col].style.bg = this.fill;
}
}
}
@@ -178,8 +178,8 @@ pub const Rectangle = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/rectangle_with_parent_fill_without_padding.zon"));
}
@@ -200,8 +200,8 @@ pub const Rectangle = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/rectangle_with_parent_padding.zon"));
}
@@ -228,8 +228,8 @@ pub const Rectangle = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/rectangle_with_padding.zon"));
}
@@ -259,8 +259,8 @@ pub const Rectangle = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/rectangle_with_gap.zon"));
}
@@ -291,8 +291,8 @@ pub const Rectangle = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/rectangle_with_separator.zon"));
}
};
@@ -341,8 +341,8 @@ pub const Layout = packed struct {
} = .line,
} = .{},
pub fn contents(this: @This(), comptime C: type, cells: []Cell, size: Size, children: []const C) void {
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
pub fn contents(this: @This(), comptime C: type, cells: []Cell, origin: Point, size: Point, children: []const C) void {
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.separator.enabled and children.len > 1) {
const line_cps: [2]u21 = switch (this.separator.line) {
@@ -355,16 +355,16 @@ pub const Layout = packed struct {
for (0..children.len - 1) |idx| {
const child = children[idx];
const anchor = switch (this.direction) {
.horizontal => ((@as(usize, child.size.anchor.row) -| @as(usize, size.anchor.row)) * @as(usize, size.cols)) + @as(usize, child.size.anchor.col) + @as(usize, child.size.cols) + gap -| @as(usize, size.anchor.col),
.vertical => ((@as(usize, child.size.anchor.row) + @as(usize, child.size.rows) + gap -| @as(usize, size.anchor.row)) * @as(usize, size.cols)) + @as(usize, child.size.anchor.col) -| @as(usize, size.anchor.col),
.horizontal => ((@as(usize, child.origin.y) -| @as(usize, origin.y)) * @as(usize, size.x)) + @as(usize, child.origin.x) + @as(usize, child.size.x) + gap -| @as(usize, origin.x),
.vertical => ((@as(usize, child.origin.y) + @as(usize, child.size.y) + gap -| @as(usize, origin.y)) * @as(usize, size.x)) + @as(usize, child.origin.x) -| @as(usize, origin.x),
};
switch (this.direction) {
.horizontal => for (0..child.size.rows) |row| {
cells[anchor + row * size.cols].cp = line_cps[0];
cells[anchor + row * size.cols].style.fg = this.separator.color;
.horizontal => for (0..child.size.y) |row| {
cells[anchor + row * size.x].cp = line_cps[0];
cells[anchor + row * size.x].style.fg = this.separator.color;
},
.vertical => for (0..child.size.cols) |col| {
.vertical => for (0..child.size.x) |col| {
cells[anchor + col].cp = line_cps[1];
cells[anchor + col].style.fg = this.separator.color;
},
@@ -389,8 +389,8 @@ pub const Layout = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/separator_no_gaps.zon"));
}
@@ -411,8 +411,8 @@ pub const Layout = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/separator_no_gaps_with_padding.zon"));
}
@@ -435,8 +435,8 @@ pub const Layout = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/separator_2x_no_gaps.zon"));
}
@@ -462,8 +462,8 @@ pub const Layout = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/separator_2x_no_gaps_with_border.zon"));
}
@@ -490,8 +490,8 @@ pub const Layout = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/separator_2x_no_gaps_with_padding.zon"));
}
@@ -518,8 +518,8 @@ pub const Layout = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/separator_2x_with_gaps_with_border.zon"));
}
@@ -547,8 +547,8 @@ pub const Layout = packed struct {
defer container.deinit();
try testing.expectContainerScreen(.{
.rows = 20,
.cols = 30,
.y = 20,
.x = 30,
}, &container, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon"));
}
};
@@ -559,7 +559,8 @@ pub fn Container(comptime Event: type) type {
const Element = @import("element.zig").Element(Event);
return struct {
allocator: std.mem.Allocator,
size: Size,
origin: Point,
size: Point,
properties: Properties,
element: Element,
elements: std.ArrayList(@This()),
@@ -571,7 +572,7 @@ pub fn Container(comptime Event: type) type {
border: Border = .{},
rectangle: Rectangle = .{},
layout: Layout = .{},
fixed_size: Size = .{},
fixed_size: Point = .{},
};
pub fn init(
@@ -581,6 +582,7 @@ pub fn Container(comptime Event: type) type {
) !@This() {
return .{
.allocator = allocator,
.origin = .{},
.size = .{},
.properties = properties,
.element = element,
@@ -599,18 +601,18 @@ pub fn Container(comptime Event: type) type {
try this.elements.append(element);
}
pub fn position(this: *@This(), pos: Point) !void {
log.debug("pos: .{{ .x = {d}, .y = {d} }}", .{ pos.x, pos.y });
this.origin = pos;
}
pub fn handle(this: *@This(), event: Event) !void {
switch (event) {
.resize => |size| resize: {
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
.size => |size| resize: {
log.debug("Event .size: {{ .x = {d}, .y = {d} }}", .{ size.x, size.y });
this.size = size;
if (this.properties.fixed_size.cols > 0 and size.cols < this.properties.fixed_size.cols) return Error.TooSmall;
if (this.properties.fixed_size.rows > 0 and size.rows < this.properties.fixed_size.rows) return Error.TooSmall;
if (this.properties.fixed_size.x > 0 and size.x < this.properties.fixed_size.x) return Error.TooSmall;
if (this.properties.fixed_size.y > 0 and size.y < this.properties.fixed_size.y) return Error.TooSmall;
try this.element.handle(event);
@@ -618,13 +620,13 @@ pub fn Container(comptime Event: type) type {
const layout = this.properties.layout;
var fixed_size_elements: u16 = 0;
var fixed_size: Size = .{};
var fixed_size: Point = .{};
for (this.elements.items) |element| {
switch (layout.direction) {
.horizontal => if (element.properties.fixed_size.cols > 0) {
.horizontal => if (element.properties.fixed_size.x > 0) {
fixed_size_elements += 1;
},
.vertical => if (element.properties.fixed_size.rows > 0) {
.vertical => if (element.properties.fixed_size.y > 0) {
fixed_size_elements += 1;
},
}
@@ -632,8 +634,8 @@ pub fn Container(comptime Event: type) type {
}
// check if the available screen is large enough
switch (layout.direction) {
.horizontal => if (fixed_size.cols > size.cols) return Error.TooSmall,
.vertical => if (fixed_size.rows > size.rows) return Error.TooSmall,
.horizontal => if (fixed_size.x > size.x) return Error.TooSmall,
.vertical => if (fixed_size.y > size.y) return Error.TooSmall,
}
const sides = this.properties.border.sides;
const padding = layout.padding;
@@ -641,28 +643,28 @@ pub fn Container(comptime Event: type) type {
if (layout.separator.enabled) gap += 1;
const len: u16 = @truncate(this.elements.items.len);
const element_cols = blk: {
var cols = size.cols - fixed_size.cols - gap * (len - 1);
if (sides.left) cols -= 1;
if (sides.right) cols -= 1;
cols -= padding.left + padding.right;
const element_x = blk: {
var x = size.x - fixed_size.x - gap * (len - 1);
if (sides.left) x -= 1;
if (sides.right) x -= 1;
x -= padding.left + padding.right;
if (fixed_size_elements == len) break :blk 0;
if (fixed_size_elements == 0) {
break :blk @divTrunc(cols, len);
break :blk @divTrunc(x, len);
} else {
break :blk @divTrunc(cols, len - fixed_size_elements);
break :blk @divTrunc(x, len - fixed_size_elements);
}
};
const element_rows = blk: {
var rows = size.rows - fixed_size.rows - gap * (len - 1);
if (sides.top) rows -= 1;
if (sides.bottom) rows -= 1;
rows -= padding.top + padding.bottom;
const element_y = blk: {
var y = size.y - fixed_size.y - gap * (len - 1);
if (sides.top) y -= 1;
if (sides.bottom) y -= 1;
y -= padding.top + padding.bottom;
if (fixed_size_elements == len) break :blk 0;
if (fixed_size_elements == 0) {
break :blk @divTrunc(rows, len);
break :blk @divTrunc(y, len);
} else {
break :blk @divTrunc(rows, len - fixed_size_elements);
break :blk @divTrunc(y, len - fixed_size_elements);
}
};
var offset: u16 = switch (layout.direction) {
@@ -671,99 +673,102 @@ pub fn Container(comptime Event: type) type {
};
var overflow = switch (layout.direction) {
.horizontal => blk: {
var cols = size.cols - fixed_size.cols - gap * (len - 1);
if (sides.left) cols -= 1;
if (sides.right) cols -= 1;
cols -= padding.left + padding.right;
var x = size.x - fixed_size.x - gap * (len - 1);
if (sides.left) x -= 1;
if (sides.right) x -= 1;
x -= padding.left + padding.right;
if (fixed_size_elements == len) break :blk 0;
if (fixed_size_elements == 0) {
break :blk cols - element_cols * len;
break :blk x - element_x * len;
} else {
break :blk cols - element_cols * (len - fixed_size_elements);
break :blk x - element_x * (len - fixed_size_elements);
}
},
.vertical => blk: {
var rows = size.rows - fixed_size.rows - gap * (len - 1);
if (sides.top) rows -= 1;
if (sides.bottom) rows -= 1;
rows -= padding.top + padding.bottom;
var y = size.y - fixed_size.y - gap * (len - 1);
if (sides.top) y -= 1;
if (sides.bottom) y -= 1;
y -= padding.top + padding.bottom;
if (fixed_size_elements == len) break :blk 0;
if (fixed_size_elements == 0) {
break :blk rows - element_rows * len;
break :blk y - element_y * len;
} else {
break :blk rows - element_rows * (len - fixed_size_elements);
break :blk y - element_y * (len - fixed_size_elements);
}
},
};
for (this.elements.items) |*element| {
var element_size: Size = undefined;
var element_size: Point = undefined;
var element_origin: Point = undefined;
switch (layout.direction) {
.horizontal => {
// TODO this should not always be the max size property!
var cols = blk: {
if (element.properties.fixed_size.cols > 0) break :blk element.properties.fixed_size.cols;
break :blk element_cols;
var x = blk: {
if (element.properties.fixed_size.x > 0) break :blk element.properties.fixed_size.x;
break :blk element_x;
};
if (overflow > 0) {
overflow -|= 1;
cols += 1;
x += 1;
}
element_origin = .{
.x = this.origin.x + offset,
.y = this.origin.y,
};
element_size = .{
.anchor = .{
.col = this.size.anchor.col + offset,
.row = this.size.anchor.row,
},
.cols = cols,
.rows = size.rows,
.x = x,
.y = size.y,
};
// border
if (sides.top) element_size.rows -= 1;
if (sides.bottom) element_size.rows -= 1;
if (sides.top) element_size.y -= 1;
if (sides.bottom) element_size.y -= 1;
// padding
element_size.anchor.row += padding.top;
element_size.rows -= padding.top + padding.bottom;
element_origin.y += padding.top;
element_size.y -= padding.top + padding.bottom;
// gap
offset += gap;
offset += cols;
offset += x;
},
.vertical => {
var rows = blk: {
if (element.properties.fixed_size.rows > 0) break :blk element.properties.fixed_size.rows;
break :blk element_rows;
var y = blk: {
if (element.properties.fixed_size.y > 0) break :blk element.properties.fixed_size.y;
break :blk element_y;
};
if (overflow > 0) {
overflow -|= 1;
rows += 1;
y += 1;
}
element_origin = .{
.x = this.origin.x,
.y = this.origin.y + offset,
};
element_size = .{
.anchor = .{
.col = this.size.anchor.col,
.row = this.size.anchor.row + offset,
},
.cols = size.cols,
.rows = rows,
.x = size.x,
.y = y,
};
// border
if (sides.left) element_size.cols -= 1;
if (sides.right) element_size.cols -= 1;
if (sides.left) element_size.x -= 1;
if (sides.right) element_size.x -= 1;
// padding
element_size.anchor.col += padding.left;
element_size.cols -= padding.left + padding.right;
element_origin.x += padding.left;
element_size.x -= padding.left + padding.right;
// gap
offset += gap;
offset += rows;
offset += y;
},
}
// border resizing
if (sides.top) element_size.anchor.row += 1;
if (sides.left) element_size.anchor.col += 1;
if (sides.top) element_origin.y += 1;
if (sides.left) element_origin.x += 1;
try element.handle(.{ .resize = element_size });
// TODO tell the element its origin
try element.position(element_origin);
try element.handle(.{ .size = element_size });
}
},
.mouse => |mouse| if (mouse.in(this.size)) {
.mouse => |mouse| if (mouse.in(this.origin, this.size)) {
try this.element.handle(event);
for (this.elements.items) |*element| try element.handle(event);
},
@@ -775,15 +780,15 @@ pub fn Container(comptime Event: type) type {
}
pub fn contents(this: *const @This()) ![]const Cell {
const cells = try this.allocator.alloc(Cell, @as(usize, this.size.cols) * @as(usize, this.size.rows));
const cells = try this.allocator.alloc(Cell, @as(usize, this.size.x) * @as(usize, this.size.y));
@memset(cells, .{});
errdefer this.allocator.free(cells);
this.properties.layout.contents(@This(), cells, this.size, this.elements.items);
this.properties.layout.contents(@This(), cells, this.origin, this.size, this.elements.items);
this.properties.border.contents(cells, this.size);
this.properties.rectangle.contents(cells, this.size);
try this.element.content(cells, this.size);
try this.element.content(cells, this.origin, this.size);
return cells;
}

View File

@@ -1,13 +1,11 @@
//! Interface for Element's which describe the contents of a `Container`.
const std = @import("std");
const s = @import("size.zig");
const input = @import("input.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const Mouse = input.Mouse;
const Position = s.Position;
const Size = s.Size;
const Point = @import("point.zig").Point;
pub fn Element(Event: type) type {
return struct {
@@ -16,11 +14,11 @@ pub fn Element(Event: type) type {
pub const VTable = struct {
handle: ?*const fn (ctx: *anyopaque, event: Event) anyerror!void = null,
content: ?*const fn (ctx: *anyopaque, cells: []Cell, size: Size) anyerror!void = null,
content: ?*const fn (ctx: *anyopaque, cells: []Cell, origin: Point, size: Point) anyerror!void = null,
};
/// Handle the received event. The event is one of the user provided
/// events or a system event, with the exception of the `.resize`
/// events or a system event, with the exception of the `.size`
/// `Event` as every `Container` already handles that event.
///
/// In case of user errors this function should return an error. This
@@ -32,23 +30,23 @@ pub fn Element(Event: type) type {
}
/// Write content into the `cells` of the `Container`. The associated
/// `cells` slice has the size of (`size.cols * size.rows`). The
/// `cells` slice has the size of (`size.x * size.y`). The
/// renderer will know where to place the contents on the screen.
///
/// # Note
///
/// - Caller owns `cells` slice and ensures that the size usually by assertion:
/// ```zig
/// std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
/// std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
/// ```
///
/// - This function should only fail with an error if the error is
/// non-recoverable (i.e. an allocation error, system error, etc.).
/// Otherwise user specific errors should be caught using the `handle`
/// function before the rendering of the `Container` happens.
pub inline fn content(this: @This(), cells: []Cell, size: Size) !void {
pub inline fn content(this: @This(), cells: []Cell, origin: Point, size: Point) !void {
if (this.vtable.content) |content_fn|
try content_fn(this.ptr, cells, size);
try content_fn(this.ptr, cells, origin, size);
}
};
}
@@ -57,16 +55,17 @@ pub fn Scrollable(Event: type) type {
return struct {
/// `Size` of the actual contents where the anchor and the size is
/// representing the size and location on screen.
size: Size = .{},
size: Point = .{},
/// Minimal `Size` of the scrollable `Container` to be used. If the
/// actual scrollable container's size is larger it will be used instead
/// (no scrolling will be necessary for that screen size).
min_size: Size = .{},
min_size: Point = .{},
/// `Size` of the `Container` content that is scrollable and mapped to
/// the *size* of the `Scrollable` `Element`.
container_size: Size = .{},
container_size: Point = .{},
container_origin: Point = .{},
/// Anchor of the viewport of the scrollable `Container`.
anchor: Position = .{},
anchor: Point = .{},
/// The actual `Container`, that is scrollable.
container: Container(Event),
@@ -80,7 +79,7 @@ pub fn Scrollable(Event: type) type {
};
}
pub fn init(container: Container(Event), min_size: Size) @This() {
pub fn init(container: Container(Event), min_size: Point) @This() {
return .{
.container = container,
.min_size = min_size,
@@ -90,33 +89,33 @@ pub fn Scrollable(Event: type) type {
fn handle(ctx: *anyopaque, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.resize => |size| {
.size => |size| {
this.size = size;
// TODO scrollbar space - depending on configuration and only if necessary?
this.container_size = size.max(this.min_size);
this.container_size.anchor = size.anchor;
try this.container.handle(.{ .resize = this.container_size });
this.container_origin = size; // TODO the size should be a provided origin
try this.container.handle(.{ .size = this.container_size });
},
// TODO other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?)
.mouse => |mouse| switch (mouse.button) {
Mouse.Button.wheel_up => if (this.container_size.rows > this.size.rows) {
this.anchor.row -|= 1;
Mouse.Button.wheel_up => if (this.container_size.y > this.size.y) {
this.anchor.y -|= 1;
},
Mouse.Button.wheel_down => if (this.container_size.rows > this.size.rows) {
const max_anchor_row = this.container_size.rows -| this.size.rows;
this.anchor.row = @min(this.anchor.row + 1, max_anchor_row);
Mouse.Button.wheel_down => if (this.container_size.y > this.size.y) {
const max_origin_y = this.container_size.y -| this.size.y;
this.anchor.y = @min(this.anchor.y + 1, max_origin_y);
},
Mouse.Button.wheel_left => if (this.container_size.cols > this.size.cols) {
this.anchor.col -|= 1;
Mouse.Button.wheel_left => if (this.container_size.x > this.size.x) {
this.anchor.x -|= 1;
},
Mouse.Button.wheel_right => if (this.container_size.cols > this.size.cols) {
const max_anchor_col = this.container_size.cols -| this.size.cols;
this.anchor.col = @min(this.anchor.col + 1, max_anchor_col);
Mouse.Button.wheel_right => if (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);
},
else => try this.container.handle(.{
.mouse = .{
.col = mouse.col + this.anchor.col,
.row = mouse.row + this.anchor.row,
.x = mouse.x + this.anchor.x,
.y = mouse.y + this.anchor.y,
.button = mouse.button,
.kind = mouse.kind,
},
@@ -126,48 +125,50 @@ pub fn Scrollable(Event: type) type {
}
}
fn render_container(container: Container(Event), cells: []Cell, container_size: Size) !void {
fn render_container(container: Container(Event), cells: []Cell, container_origin: Point, container_size: Point) !void {
const size = container.size;
const origin = container.origin;
const contents = try container.contents();
defer container.allocator.free(contents);
const anchor = (@as(usize, size.anchor.row -| container_size.anchor.row) * @as(usize, container_size.cols)) +
@as(usize, size.anchor.col -| container_size.anchor.col);
const anchor = (@as(usize, origin.y -| container_origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x -| container_origin.x);
var idx: usize = 0;
blk: for (0..size.rows) |row| {
for (0..size.cols) |col| {
cells[anchor + (row * container_size.cols) + col] = contents[idx];
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;
}
}
for (container.elements.items) |child| try render_container(child, cells, size);
for (container.elements.items) |child| try render_container(child, cells, origin, size);
}
fn content(ctx: *anyopaque, cells: []Cell, size: Size) !void {
fn content(ctx: *anyopaque, cells: []Cell, origin: Point, size: Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, this.size.cols) * @as(usize, this.size.rows));
_ = origin; // this should be used
std.debug.assert(cells.len == @as(usize, this.size.x) * @as(usize, this.size.y));
const container_size = this.container.size;
const container_cells = try this.container.allocator.alloc(Cell, @as(usize, container_size.cols) * @as(usize, container_size.rows));
const container_origin = this.container.origin;
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.contents();
defer this.container.allocator.free(container_cells_const);
std.debug.assert(container_cells_const.len == @as(usize, container_size.cols) * @as(usize, container_size.rows));
std.debug.assert(container_cells_const.len == @as(usize, container_size.x) * @as(usize, container_size.y));
@memcpy(container_cells, container_cells_const);
}
// FIX this is not resolving the rendering recursively! This means that the content is only shown for the first children
for (this.container.elements.items) |child| try render_container(child, container_cells, container_size);
for (this.container.elements.items) |child| try render_container(child, container_cells, container_origin, container_size);
const anchor = (@as(usize, this.anchor.row) * @as(usize, container_size.cols)) + @as(usize, this.anchor.col);
const anchor = (@as(usize, this.anchor.y) * @as(usize, container_size.x)) + @as(usize, this.anchor.x);
// TODO render scrollbar according to configuration!
for (0..size.rows) |row| {
for (0..size.cols) |col| {
cells[(row * size.cols) + col] = container_cells[anchor + (row * container_size.cols) + col];
for (0..size.y) |row| {
for (0..size.x) |col| {
cells[(row * size.x) + col] = container_cells[anchor + (row * container_size.x) + col];
}
}
this.container.allocator.free(container_cells);
@@ -183,9 +184,9 @@ test "scrollable vertical" {
const testing = @import("testing.zig");
const allocator = std.testing.allocator;
const size: Size = .{
.rows = 20,
.cols = 30,
const size: Point = .{
.x = 30,
.y = 20,
};
var box: Container(event.SystemEvent) = try .init(allocator, .{
@@ -210,7 +211,7 @@ test "scrollable vertical" {
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .rows = size.rows + 15 });
var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .y = size.y + 15 });
var container: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
@@ -223,33 +224,33 @@ test "scrollable vertical" {
var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit();
try container.handle(.{ .resize = size });
try container.handle(.{ .size = size });
try renderer.render(Container(event.SystemEvent), &container);
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)
for (0..15) |_| try container.handle(.{
.mouse = .{
.button = .wheel_down,
.kind = .press,
.col = 5,
.row = 5,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
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
try container.handle(.{
.mouse = .{
.button = .wheel_down,
.kind = .press,
.col = 5,
.row = 5,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
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);
}
test "scrollable horizontal" {
@@ -257,9 +258,9 @@ test "scrollable horizontal" {
const testing = @import("testing.zig");
const allocator = std.testing.allocator;
const size: Size = .{
.rows = 20,
.cols = 30,
const size: Point = .{
.x = 30,
.y = 20,
};
var box: Container(event.SystemEvent) = try .init(allocator, .{
@@ -284,7 +285,7 @@ test "scrollable horizontal" {
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .cols = size.cols + 15 });
var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .x = size.x + 15 });
var container: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
@@ -297,31 +298,31 @@ test "scrollable horizontal" {
var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit();
try container.handle(.{ .resize = size });
try container.handle(.{ .size = size });
try renderer.render(Container(event.SystemEvent), &container);
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)
for (0..15) |_| try container.handle(.{
.mouse = .{
.button = .wheel_right,
.kind = .press,
.col = 5,
.row = 5,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
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
try container.handle(.{
.mouse = .{
.button = .wheel_right,
.kind = .press,
.col = 5,
.row = 5,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
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);
}

View File

@@ -6,7 +6,7 @@ const terminal = @import("terminal.zig");
const Key = input.Key;
const Mouse = input.Mouse;
const Size = @import("size.zig").Size;
const Point = @import("point.zig").Point;
/// System events available to every `zterm.App`
pub const SystemEvent = union(enum) {
@@ -21,8 +21,8 @@ pub const SystemEvent = union(enum) {
/// associated error message
msg: []const u8,
},
/// Resize event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in
resize: Size,
/// Size event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in
size: Point,
/// Input key event received from the user
key: Key,
/// Mouse input event

View File

@@ -1,11 +1,11 @@
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
const std = @import("std");
const Size = @import("size.zig").Size;
const Point = @import("point.zig").Point;
pub const Mouse = packed struct {
col: u16,
row: u16,
x: u16,
y: u16,
button: Button,
kind: Kind,
@@ -35,9 +35,9 @@ pub const Mouse = packed struct {
return std.meta.eql(this, other);
}
pub fn in(this: @This(), size: Size) bool {
return this.col >= size.anchor.col and this.col <= size.cols + size.anchor.col and
this.row >= size.anchor.row and this.row <= size.rows + size.anchor.row;
pub fn in(this: @This(), origin: Point, size: Point) bool {
return this.x >= origin.x and this.x <= size.x + origin.x and
this.y >= origin.y and this.y <= size.y + origin.y;
}
};

60
src/point.zig Normal file
View File

@@ -0,0 +1,60 @@
pub const Point = packed struct {
x: u16 = 0,
y: u16 = 0,
pub fn add(this: @This(), other: @This()) @This() {
return .{
.x = this.x + other.x,
.y = this.y + other.y,
};
}
pub fn max(this: @This(), other: @This()) @This() {
return .{
.x = @max(this.x, other.x),
.y = @max(this.y, other.y),
};
}
test "adding" {
const testing = @import("std").testing;
const a: @This() = .{
.x = 10,
.y = 20,
};
const b: @This() = .{
.x = 20,
.y = 10,
};
try testing.expectEqual(@This(){
.x = 30,
.y = 30,
}, a.add(b));
}
test "maximum" {
const testing = @import("std").testing;
const a: @This() = .{
.x = 10,
.y = 20,
};
const b: @This() = .{
.x = 20,
.y = 10,
};
try testing.expectEqual(@This(){
.x = 20,
.y = 20,
}, a.max(b));
}
};
test {
_ = Point;
}

View File

@@ -2,14 +2,13 @@ const std = @import("std");
const terminal = @import("terminal.zig");
const Cell = @import("cell.zig");
const Position = @import("size.zig").Position;
const Size = @import("size.zig").Size;
const Point = @import("point.zig").Point;
/// Double-buffered intermediate rendering pipeline
pub const Buffered = struct {
allocator: std.mem.Allocator,
created: bool,
size: Size,
size: Point,
screen: []Cell,
virtual_screen: []Cell,
@@ -30,9 +29,9 @@ pub const Buffered = struct {
}
}
pub fn resize(this: *@This(), size: Size) !void {
pub fn resize(this: *@This(), size: Point) !void {
this.size = size;
const n = @as(usize, size.cols) * @as(usize, size.rows);
const n = @as(usize, size.x) * @as(usize, size.y);
if (!this.created) {
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
@@ -60,18 +59,19 @@ pub const Buffered = struct {
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
const size: Size = container.size;
const size: Point = container.size;
const origin: Point = container.origin;
const cells: []const Cell = try container.contents();
if (cells.len == 0) return;
var idx: usize = 0;
var vs = this.virtual_screen;
const anchor: usize = (@as(usize, size.anchor.row) * @as(usize, this.size.cols)) + @as(usize, size.anchor.col);
const anchor: usize = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
blk: for (0..size.rows) |row| {
for (0..size.cols) |col| {
vs[anchor + (row * this.size.cols) + col] = cells[idx];
blk: for (0..size.y) |row| {
for (0..size.x) |col| {
vs[anchor + (row * this.size.x) + col] = cells[idx];
idx += 1;
if (cells.len == idx) break :blk;
@@ -89,15 +89,15 @@ pub const Buffered = struct {
const writer = terminal.writer();
const s = this.screen;
const vs = this.virtual_screen;
for (0..this.size.rows) |row| {
for (0..this.size.cols) |col| {
const idx = (row * this.size.cols) + col;
for (0..this.size.y) |row| {
for (0..this.size.x) |col| {
const idx = (row * this.size.x) + col;
const cs = s[idx];
const cvs = vs[idx];
if (cs.eql(cvs)) continue;
// render differences found in virtual screen
try terminal.setCursorPosition(.{ .row = @truncate(row + 1), .col = @truncate(col + 1) });
try terminal.setCursorPosition(.{ .y = @truncate(row + 1), .x = @truncate(col + 1) });
try cvs.value(writer);
// update screen to be the virtual screen for the next frame
s[idx] = vs[idx];

View File

@@ -1,66 +0,0 @@
pub const Size = packed struct {
anchor: Position = .{},
cols: u16 = 0,
rows: u16 = 0,
pub fn add(this: @This(), other: @This()) Size {
return .{
.cols = this.cols + other.cols,
.rows = this.rows + other.rows,
};
}
pub fn max(this: @This(), other: @This()) Size {
return .{
.cols = @max(this.cols, other.cols),
.rows = @max(this.rows, other.rows),
};
}
test "adding" {
const testing = @import("std").testing;
const a: @This() = .{
.anchor = .{ .col = 1, .row = 2 },
.cols = 10,
.rows = 20,
};
const b: @This() = .{
.anchor = .{ .col = 5, .row = 20 },
.cols = 20,
.rows = 10,
};
try testing.expectEqual(@This(){
.cols = 30,
.rows = 30,
}, a.add(b));
}
test "maximum" {
const testing = @import("std").testing;
const a: @This() = .{
.anchor = .{ .col = 1, .row = 2 },
.cols = 10,
.rows = 20,
};
const b: @This() = .{
.anchor = .{ .col = 5, .row = 20 },
.cols = 20,
.rows = 10,
};
try testing.expectEqual(@This(){
.cols = 20,
.rows = 20,
}, a.max(b));
}
};
pub const Position = packed struct {
col: u16 = 0,
row: u16 = 0,
};

View File

@@ -4,8 +4,8 @@ const ctlseqs = @import("ctlseqs.zig");
const input = @import("input.zig");
const Key = input.Key;
const Position = @import("size.zig").Position;
const Size = @import("size.zig").Size;
const Point = @import("point.zig").Point;
const Size = @import("point.zig").Point;
const Cell = @import("cell.zig");
const log = std.log.scoped(.terminal);
@@ -23,7 +23,7 @@ pub const ReportMode = enum {
pub fn getTerminalSize() Size {
var ws: std.posix.winsize = undefined;
_ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws));
return .{ .cols = ws.col, .rows = ws.row };
return .{ .x = ws.col, .y = ws.row };
}
pub fn saveScreen() !void {
@@ -89,9 +89,9 @@ pub fn writer() Writer {
return .{ .context = .{} };
}
pub fn setCursorPosition(pos: Position) !void {
pub fn setCursorPosition(pos: Point) !void {
var buf: [64]u8 = undefined;
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col });
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y, pos.x });
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
}
@@ -137,8 +137,8 @@ pub fn getCursorPosition() !Size.Position {
}
return .{
.row = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
.col = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
.x = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
.y = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
};
}

View File

@@ -5,8 +5,7 @@ const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const DisplayWidth = @import("DisplayWidth");
const Position = @import("size.zig").Position;
const Size = @import("size.zig").Size;
const Point = @import("point.zig").Point;
// TODO how would I describe the expected screens?
// - including styling?
@@ -15,11 +14,11 @@ const Size = @import("size.zig").Size;
/// Single-buffer test rendering pipeline for testing purposes.
pub const Renderer = struct {
allocator: std.mem.Allocator,
size: Size,
size: Point,
screen: []Cell,
pub fn init(allocator: std.mem.Allocator, size: Size) @This() {
const screen = allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("testing.zig: Out of memory.");
pub fn init(allocator: std.mem.Allocator, size: Point) @This() {
const screen = allocator.alloc(Cell, @as(usize, size.x) * @as(usize, size.y)) catch @panic("testing.zig: Out of memory.");
@memset(screen, .{});
return .{
@@ -33,9 +32,9 @@ pub const Renderer = struct {
this.allocator.free(this.screen);
}
pub fn resize(this: *@This(), size: Size) !void {
pub fn resize(this: *@This(), size: Point) !void {
this.size = size;
const n = @as(usize, size.cols) * @as(usize, size.rows);
const n = @as(usize, size.cols) * @as(usize, size.y);
this.allocator.free(this.screen);
this.screen = this.allocator.alloc(Cell, n) catch @panic("testing.zig: Out of memory.");
@@ -47,21 +46,22 @@ pub const Renderer = struct {
}
pub fn render(this: *@This(), comptime T: type, container: *const T) !void {
const size: Size = container.size;
const size: Point = container.size;
const origin: Point = container.origin;
const cells: []const Cell = try container.contents();
if (cells.len == 0) return;
var idx: usize = 0;
const anchor = (@as(usize, size.anchor.row) * @as(usize, this.size.cols)) + @as(usize, size.anchor.col);
const anchor = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
blk: for (0..size.rows) |row| {
for (0..size.cols) |col| {
blk: for (0..size.y) |row| {
for (0..size.x) |col| {
const cell = cells[idx];
idx += 1;
this.screen[anchor + (row * this.size.cols) + col].style = cell.style;
this.screen[anchor + (row * this.size.cols) + col].cp = cell.cp;
this.screen[anchor + (row * this.size.x) + col].style = cell.style;
this.screen[anchor + (row * this.size.x) + col].cp = cell.cp;
if (cells.len == idx) break :blk;
}
@@ -92,7 +92,7 @@ pub const Renderer = struct {
/// var renderer: testing.Renderer = .init(allocator, size);
/// defer renderer.deinit();
///
/// try container.handle(.{ .resize = size });
/// try container.handle(.{ .size = size });
/// try renderer.render(Container(event.SystemEvent), &container);
/// try renderer.save(file.writer());
/// ```
@@ -115,34 +115,34 @@ pub const Renderer = struct {
/// .cols = 30,
/// }, &container, @import("test/container/border.all.zon"));
/// ```
pub fn expectContainerScreen(size: Size, container: *Container(event.SystemEvent), expected: []const Cell) !void {
pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEvent), expected: []const Cell) !void {
const allocator = std.testing.allocator;
var renderer: Renderer = .init(allocator, size);
defer renderer.deinit();
try container.handle(.{ .resize = size });
try container.handle(.{ .size = size });
try renderer.render(Container(event.SystemEvent), container);
try expectEqualCells(renderer.size, expected, renderer.screen);
try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
}
/// This function is intended to be used only in tests. Test if the two
/// provided cell arrays are identical. Usually the `Cell` slices are
/// the contents of a given screen from the `zterm.testing.Renderer`. See
/// `zterm.testing.expectContainerScreen` for an example usage.
pub fn expectEqualCells(size: Size, expected: []const Cell, actual: []const Cell) !void {
pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actual: []const Cell) !void {
const allocator = std.testing.allocator;
try std.testing.expectEqual(expected.len, actual.len);
try std.testing.expectEqual(expected.len, @as(usize, size.rows) * @as(usize, size.cols));
try std.testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.cols -| size.anchor.col);
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer expected_cps.deinit();
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.cols -| size.anchor.col);
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer actual_cps.deinit();
var output = try std.ArrayList(u8).initCapacity(allocator, expected_cps.capacity * actual_cps.capacity + 5 * size.rows);
var output = try std.ArrayList(u8).initCapacity(allocator, expected_cps.capacity * actual_cps.capacity + 5 * size.y);
defer output.deinit();
var buffer = std.io.bufferedWriter(output.writer());
@@ -155,23 +155,22 @@ pub fn expectEqualCells(size: Size, expected: []const Cell, actual: []const Cell
defer dwd.deinit();
const dw: DisplayWidth = .{ .data = &dwd };
const expected_centered = try dw.center(allocator, "Expected Screen", size.cols, " ");
const expected_centered = try dw.center(allocator, "Expected Screen", size.x, " ");
defer allocator.free(expected_centered);
const actual_centered = try dw.center(allocator, "Actual Screen", size.cols, " ");
const actual_centered = try dw.center(allocator, "Actual Screen", size.x, " ");
defer allocator.free(actual_centered);
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
const anchor = (size.anchor.row * size.cols) + size.anchor.col;
for (size.anchor.row..size.rows) |row| {
for (origin.y..size.y) |row| {
defer {
expected_cps.clearRetainingCapacity();
actual_cps.clearRetainingCapacity();
}
for (size.anchor.col..size.cols) |col| {
const expected_cell = expected[anchor + (row * size.cols) + col];
const actual_cell = actual[anchor + (row * size.cols) + col];
for (origin.x..size.x) |col| {
const expected_cell = expected[(row * size.x) + col];
const actual_cell = actual[(row * size.x) + col];
if (!expected_cell.eql(actual_cell)) differ = true;

View File

@@ -1,7 +1,7 @@
// private imports
const container = @import("container.zig");
const color = @import("color.zig");
const size = @import("size.zig");
const size = @import("point.zig");
// public exports
pub const input = @import("input.zig");
@@ -24,8 +24,7 @@ pub const Cell = @import("cell.zig");
pub const Color = color.Color;
pub const Key = input.Key;
pub const Mouse = input.Mouse;
pub const Position = size.Position;
pub const Size = size.Size;
pub const Point = @import("point.zig").Point;
pub const Style = @import("style.zig");
test {
@@ -33,9 +32,9 @@ test {
_ = @import("container.zig");
_ = @import("queue.zig");
_ = @import("error.zig");
_ = @import("point.zig");
_ = color;
_ = size;
_ = Cell;
_ = Key;