Compare commits
38 Commits
8c130a40d7
...
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 182dec6065 | |||
| 54af974c2b | |||
| dddc09b4ce | |||
| adda53c5a9 | |||
| 5c1d61eefd | |||
| 54c7e19939 | |||
| 5457e91b37 | |||
| 79016f39b2 | |||
| 2b9ab1e0fb | |||
| 315cd8d23e | |||
| e3551fa624 | |||
| 9ec335cad8 | |||
| 466e00c16c | |||
| fc72cf4abb | |||
| 65d7546efd | |||
| ec22e68e8c | |||
| 43cdc46853 | |||
| 591b990087 | |||
| 91ac6241f4 | |||
| edefc80759 | |||
| 4145ff497b | |||
| bec0cf2987 | |||
| e2fe884925 | |||
| caee008d50 | |||
| af443c6bbf | |||
| ae9cd08b15 | |||
| 91794a0197 | |||
| 8a7ce78aaf | |||
| 35ebe31008 | |||
| c28fcd26c1 | |||
| 3b6848f845 | |||
| 53b69f034c | |||
| 54ce697e91 | |||
| 8f16435f30 | |||
| ca14bc6106 | |||
| a293ef46da | |||
| c66401d941 | |||
| ad4186e1f8 |
@@ -10,7 +10,7 @@
|
|||||||
Clone this repository and run `zig build --help` to see the available examples. Run a given example as follows:
|
Clone this repository and run `zig build --help` to see the available examples. Run a given example as follows:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
zig build --release=safe -Dexample=input run
|
zig build --release=safe -Dexample=demo run
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
|
|||||||
16
build.zig
16
build.zig
@@ -5,6 +5,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
const Examples = enum {
|
const Examples = enum {
|
||||||
|
all,
|
||||||
demo,
|
demo,
|
||||||
// elements:
|
// elements:
|
||||||
button,
|
button,
|
||||||
@@ -22,7 +23,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
errors,
|
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();
|
const options = b.addOptions();
|
||||||
options.addOption(Examples, "example", example);
|
options.addOption(Examples, "example", example);
|
||||||
@@ -153,6 +154,19 @@ pub fn build(b: *std.Build) void {
|
|||||||
.palette => palette,
|
.palette => palette,
|
||||||
// error handling:
|
// error handling:
|
||||||
.errors => errors,
|
.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);
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,29 @@
|
|||||||
//
|
//
|
||||||
// It is redundant to include "zig" in this name because it is already
|
// It is redundant to include "zig" in this name because it is already
|
||||||
// within the Zig package namespace.
|
// within the Zig package namespace.
|
||||||
.name = "zterm",
|
.name = .zterm,
|
||||||
|
|
||||||
|
// Together with name, this represents a globally unique package
|
||||||
|
// identifier. This field is generated by the Zig toolchain when the
|
||||||
|
// package is first created, and then *never changes*. This allows
|
||||||
|
// unambiguous detection of one package being an updated version of
|
||||||
|
// another.
|
||||||
|
//
|
||||||
|
// When forking a Zig project, this id should be regenerated (delete the
|
||||||
|
// field and run `zig build`) if the upstream project is still maintained.
|
||||||
|
// Otherwise, the fork is *hostile*, attempting to take control over the
|
||||||
|
// original project's identity. Thus it is recommended to leave the comment
|
||||||
|
// on the following line intact, so that it shows up in code reviews that
|
||||||
|
// modify the field.
|
||||||
|
.fingerprint = 0xf10b37e210a619d7, // Changing this has security and trust implications.
|
||||||
|
|
||||||
// This is a [Semantic Version](https://semver.org/).
|
// This is a [Semantic Version](https://semver.org/).
|
||||||
// In a future version of Zig it will be used for package deduplication.
|
// In a future version of Zig it will be used for package deduplication.
|
||||||
.version = "0.0.0",
|
.version = "0.1.0",
|
||||||
|
|
||||||
// This field is optional.
|
// Tracks the earliest Zig version that the package considers to be a
|
||||||
// This is currently advisory only; Zig does not yet do anything
|
// supported use case.
|
||||||
// with this value.
|
.minimum_zig_version = "0.15.0-dev.56+d0911786c",
|
||||||
//.minimum_zig_version = "0.11.0",
|
|
||||||
|
|
||||||
// This field is optional.
|
// This field is optional.
|
||||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||||
@@ -29,11 +42,9 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
.paths = .{
|
.paths = .{
|
||||||
|
"LICENSE",
|
||||||
"build.zig",
|
"build.zig",
|
||||||
"build.zig.zon",
|
"build.zig.zon",
|
||||||
"src",
|
"src",
|
||||||
// For example...
|
|
||||||
//"LICENSE",
|
|
||||||
//"README.md",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,20 +13,20 @@ const QuitText = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
const row = 2;
|
const y = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const x = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (y * size.x) + x;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ const QuitText = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
// TODO: maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
@@ -47,15 +47,22 @@ pub fn main() !void {
|
|||||||
|
|
||||||
var quit_text: QuitText = .{};
|
var quit_text: QuitText = .{};
|
||||||
|
|
||||||
// TODO: what should the demo application do?
|
// TODO what should the demo application do?
|
||||||
// - some sort of chat? -> write messages and have them displayed in a scrollable array at the right hand side?
|
// - some sort of chat? -> write messages and have them displayed in a scrollable array at the right hand side?
|
||||||
// - on the left some buttons?
|
// - on the left some buttons?
|
||||||
var box = try App.Container.init(allocator, .{
|
var box = try App.Container.init(allocator, .{
|
||||||
|
.border = .{
|
||||||
|
.color = .blue,
|
||||||
|
.sides = .all,
|
||||||
|
},
|
||||||
.layout = .{
|
.layout = .{
|
||||||
.gap = 1,
|
.gap = 1,
|
||||||
.padding = .vertical(2),
|
.padding = .vertical(2),
|
||||||
|
.direction = .vertical,
|
||||||
|
},
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = 90 },
|
||||||
},
|
},
|
||||||
.min_size = .{ .cols = 50 },
|
|
||||||
}, .{});
|
}, .{});
|
||||||
try box.append(try App.Container.init(allocator, .{
|
try box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .light_green },
|
||||||
@@ -68,16 +75,12 @@ pub fn main() !void {
|
|||||||
}, .{}));
|
}, .{}));
|
||||||
defer box.deinit();
|
defer box.deinit();
|
||||||
|
|
||||||
var scrollable: App.Scrollable = .{
|
var scrollable: App.Scrollable = .{ .container = box };
|
||||||
.container = box,
|
|
||||||
};
|
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.border = .{
|
|
||||||
.separator = .{ .enabled = true },
|
|
||||||
},
|
|
||||||
.layout = .{
|
.layout = .{
|
||||||
.gap = 2,
|
.gap = 2,
|
||||||
|
.separator = .{ .enabled = true },
|
||||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||||
.direction = .horizontal,
|
.direction = .horizontal,
|
||||||
},
|
},
|
||||||
@@ -89,9 +92,15 @@ pub fn main() !void {
|
|||||||
.color = .light_blue,
|
.color = .light_blue,
|
||||||
.sides = .all,
|
.sides = .all,
|
||||||
},
|
},
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .x = 100 },
|
||||||
|
},
|
||||||
}, .{}));
|
}, .{}));
|
||||||
try container.append(try App.Container.init(allocator, .{
|
try container.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .blue },
|
.rectangle = .{ .fill = .blue },
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .x = 30 },
|
||||||
|
},
|
||||||
}, .{}));
|
}, .{}));
|
||||||
defer container.deinit(); // also de-initializes the children
|
defer container.deinit(); // also de-initializes the children
|
||||||
|
|
||||||
@@ -103,10 +112,8 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| {
|
.key => |key| {
|
||||||
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
|
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
|
||||||
|
|
||||||
@@ -123,18 +130,28 @@ pub fn main() !void {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
// 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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: returned errors should be propagated back to the application
|
// NOTE returned errors should be propagated back to the application
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,37 @@ const App = zterm.App(union(enum) {
|
|||||||
|
|
||||||
const log = std.log.scoped(.default);
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
pub const Clickable = struct {
|
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;
|
||||||
|
std.debug.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 Clickable = struct {
|
||||||
const text = "Press me";
|
const text = "Press me";
|
||||||
|
|
||||||
queue: *App.Queue,
|
queue: *App.Queue,
|
||||||
|
color: zterm.Color = .black,
|
||||||
|
|
||||||
pub fn element(this: *@This()) App.Element {
|
pub fn element(this: *@This()) App.Element {
|
||||||
return .{
|
return .{
|
||||||
@@ -25,24 +52,32 @@ pub const Clickable = struct {
|
|||||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.mouse => |mouse| this.queue.push(.{ .click = @tagName(mouse.button) }),
|
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) {
|
||||||
|
var value = @intFromEnum(this.color);
|
||||||
|
value += 1;
|
||||||
|
value %= 17;
|
||||||
|
if (value == 0) value = 1;
|
||||||
|
this.color = @enumFromInt(value);
|
||||||
|
this.queue.push(.{ .click = @tagName(mouse.button) });
|
||||||
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
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));
|
||||||
|
|
||||||
const row = size.rows / 2 -| (text.len / 2);
|
const row = size.y / 2 -| (text.len / 2);
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .black;
|
cells[anchor + idx].style.fg = this.color;
|
||||||
|
cells[anchor + idx].style.emphasis = &.{.bold};
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,10 +98,12 @@ pub fn main() !void {
|
|||||||
var clickable: Clickable = .{ .queue = &app.queue };
|
var clickable: Clickable = .{ .queue = &app.queue };
|
||||||
const element = clickable.element();
|
const element = clickable.element();
|
||||||
|
|
||||||
|
var quit_text: QuitText = .{};
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .grey },
|
.rectangle = .{ .fill = .grey },
|
||||||
.layout = .{ .padding = .all(5) },
|
.layout = .{ .padding = .all(5) },
|
||||||
}, .{});
|
}, quit_text.element());
|
||||||
defer container.deinit();
|
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 = .light_grey } }, element));
|
||||||
@@ -79,10 +116,8 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||||
.click => |button| {
|
.click => |button| {
|
||||||
log.info("Clicked with mouse using Button: {s}", .{button});
|
log.info("Clicked with mouse using Button: {s}", .{button});
|
||||||
@@ -98,6 +133,15 @@ pub fn main() !void {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,33 @@ const App = zterm.App(union(enum) {
|
|||||||
|
|
||||||
const log = std.log.scoped(.default);
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
pub const InputField = struct {
|
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;
|
||||||
|
std.debug.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 InputField = struct {
|
||||||
input: std.ArrayList(u21),
|
input: std.ArrayList(u21),
|
||||||
queue: *App.Queue,
|
queue: *App.Queue,
|
||||||
|
|
||||||
@@ -48,21 +74,21 @@ pub const InputField = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
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));
|
||||||
|
|
||||||
if (this.input.items.len == 0) return;
|
if (this.input.items.len == 0) return;
|
||||||
|
|
||||||
const row = 1;
|
const row = 1;
|
||||||
const col = 1;
|
const col = 1;
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (this.input.items, 0..) |cp, idx| {
|
for (this.input.items, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .black;
|
cells[anchor + idx].style.fg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,12 +109,14 @@ pub fn main() !void {
|
|||||||
var input_field: InputField = .init(allocator, &app.queue);
|
var input_field: InputField = .init(allocator, &app.queue);
|
||||||
defer input_field.deinit();
|
defer input_field.deinit();
|
||||||
|
|
||||||
|
var quit_text: QuitText = .{};
|
||||||
|
|
||||||
const element = input_field.element();
|
const element = input_field.element();
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .grey },
|
.rectangle = .{ .fill = .grey },
|
||||||
.layout = .{ .padding = .all(5) },
|
.layout = .{ .padding = .all(5) },
|
||||||
}, .{});
|
}, quit_text.element());
|
||||||
defer container.deinit();
|
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 = .light_grey } }, element));
|
||||||
@@ -101,10 +129,8 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||||
.accept => |input| {
|
.accept => |input| {
|
||||||
defer allocator.free(input);
|
defer allocator.free(input);
|
||||||
@@ -121,6 +147,15 @@ pub fn main() !void {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,33 @@ const App = zterm.App(union(enum) {});
|
|||||||
|
|
||||||
const log = std.log.scoped(.default);
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
pub const HelloWorldText = packed struct {
|
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;
|
||||||
|
std.debug.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 HelloWorldText = packed struct {
|
||||||
const text = "Hello World";
|
const text = "Hello World";
|
||||||
|
|
||||||
pub fn element(this: *@This()) App.Element {
|
pub fn element(this: *@This()) App.Element {
|
||||||
@@ -16,18 +42,21 @@ pub const HelloWorldText = packed struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
// NOTE: error should only be returned here in case an in-recoverable exception has occurred
|
const row = size.y / 2;
|
||||||
const row = size.rows / 2;
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const anchor = (row * size.x) + col;
|
||||||
const anchor = (row * size.cols) + col;
|
|
||||||
|
|
||||||
for (0.., text) |idx, char| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .black;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].cp = char;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -35,7 +64,7 @@ pub const HelloWorldText = packed struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
// TODO: maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||||
defer {
|
defer {
|
||||||
const deinit_status = gpa.deinit();
|
const deinit_status = gpa.deinit();
|
||||||
@@ -52,34 +81,55 @@ pub fn main() !void {
|
|||||||
var element_wrapper: HelloWorldText = .{};
|
var element_wrapper: HelloWorldText = .{};
|
||||||
const element = element_wrapper.element();
|
const element = element_wrapper.element();
|
||||||
|
|
||||||
|
var quit_text: QuitText = .{};
|
||||||
|
|
||||||
var top_box = try App.Container.init(allocator, .{
|
var top_box = try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .blue },
|
.rectangle = .{ .fill = .blue },
|
||||||
.layout = .{
|
.layout = .{
|
||||||
.gap = 1,
|
.gap = 2,
|
||||||
|
.separator = .{
|
||||||
|
.enabled = true,
|
||||||
|
},
|
||||||
.direction = .vertical,
|
.direction = .vertical,
|
||||||
.padding = .vertical(1),
|
.padding = .vertical(1),
|
||||||
},
|
},
|
||||||
.min_size = .{ .rows = 50 },
|
|
||||||
}, .{});
|
}, .{});
|
||||||
try top_box.append(try App.Container.init(allocator, .{
|
try top_box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .light_green },
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = 30 },
|
||||||
|
},
|
||||||
}, .{}));
|
}, .{}));
|
||||||
try top_box.append(try App.Container.init(allocator, .{
|
try top_box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .light_green },
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = 5 },
|
||||||
|
},
|
||||||
}, element));
|
}, element));
|
||||||
try top_box.append(try App.Container.init(allocator, .{
|
try top_box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .light_green },
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = 2 },
|
||||||
|
},
|
||||||
}, .{}));
|
}, .{}));
|
||||||
defer top_box.deinit();
|
defer top_box.deinit();
|
||||||
|
|
||||||
var bottom_box = try App.Container.init(allocator, .{
|
var bottom_box = try App.Container.init(allocator, .{
|
||||||
.border = .{ .separator = .{ .enabled = true } },
|
.border = .{
|
||||||
.rectangle = .{ .fill = .blue },
|
.sides = .all,
|
||||||
|
.color = .blue,
|
||||||
|
},
|
||||||
.layout = .{
|
.layout = .{
|
||||||
|
.separator = .{
|
||||||
|
.enabled = true,
|
||||||
|
.color = .red,
|
||||||
|
},
|
||||||
.direction = .vertical,
|
.direction = .vertical,
|
||||||
.padding = .vertical(1),
|
.padding = .vertical(1),
|
||||||
},
|
},
|
||||||
.min_size = .{ .rows = 30 },
|
.size = .{
|
||||||
|
.dim = .{ .y = 30 },
|
||||||
|
},
|
||||||
}, .{});
|
}, .{});
|
||||||
try bottom_box.append(try App.Container.init(allocator, .{
|
try bottom_box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .grey },
|
.rectangle = .{ .fill = .grey },
|
||||||
@@ -93,25 +143,23 @@ pub fn main() !void {
|
|||||||
defer bottom_box.deinit();
|
defer bottom_box.deinit();
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.border = .{
|
.layout = .{
|
||||||
|
.gap = 2,
|
||||||
.separator = .{
|
.separator = .{
|
||||||
.enabled = true,
|
.enabled = true,
|
||||||
.line = .double,
|
.line = .double,
|
||||||
},
|
},
|
||||||
},
|
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||||
.layout = .{
|
|
||||||
.gap = 2,
|
|
||||||
.padding = .all(5),
|
|
||||||
.direction = .vertical,
|
.direction = .vertical,
|
||||||
},
|
},
|
||||||
}, .{});
|
}, quit_text.element());
|
||||||
defer container.deinit();
|
defer container.deinit();
|
||||||
|
|
||||||
// place empty container containing the element of the scrollable Container.
|
// place empty container containing the element of the scrollable Container.
|
||||||
var scrollable_top: App.Scrollable = .init(top_box);
|
var scrollable_top: App.Scrollable = .{ .container = top_box };
|
||||||
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
|
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
|
||||||
|
|
||||||
var scrollable_bottom: App.Scrollable = .init(bottom_box);
|
var scrollable_bottom: App.Scrollable = .{ .container = bottom_box };
|
||||||
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
|
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
|
||||||
|
|
||||||
try app.start();
|
try app.start();
|
||||||
@@ -122,10 +170,8 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
@@ -137,6 +183,16 @@ pub fn main() !void {
|
|||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,20 @@ const QuitText = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,20 +38,20 @@ const InfoText = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
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, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,31 +64,30 @@ const ErrorNotification = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } };
|
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));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.key => |key| if (!key.isAscii()) return error.UnsupportedKey,
|
.key => |key| if (!key.isAscii()) return error.UnsupportedKey,
|
||||||
.resize => |_| {},
|
|
||||||
.err => |err| this.msg = err.msg,
|
.err => |err| this.msg = err.msg,
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
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));
|
||||||
|
|
||||||
if (this.msg) |msg| {
|
if (this.msg) |msg| {
|
||||||
const row = size.rows -| 2;
|
const row = size.y -| 2;
|
||||||
const col = size.cols -| 2 -| msg.len;
|
const col = size.x -| 2 -| msg.len;
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (msg, 0..) |cp, idx| {
|
for (msg, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,10 +130,8 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
@@ -146,6 +143,16 @@ pub fn main() !void {
|
|||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,20 @@ const QuitText = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,16 +50,16 @@ pub fn main() !void {
|
|||||||
const element = quit_text.element();
|
const element = quit_text.element();
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.border = .{ .separator = .{ .enabled = true } },
|
|
||||||
.layout = .{
|
.layout = .{
|
||||||
|
.separator = .{ .enabled = true },
|
||||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||||
.direction = .horizontal,
|
.direction = .horizontal,
|
||||||
},
|
},
|
||||||
}, element);
|
}, element);
|
||||||
for (0..3) |_| {
|
for (0..3) |_| {
|
||||||
var column = try App.Container.init(allocator, .{
|
var column = try App.Container.init(allocator, .{
|
||||||
.border = .{ .separator = .{ .enabled = true } },
|
|
||||||
.layout = .{
|
.layout = .{
|
||||||
|
.separator = .{ .enabled = true },
|
||||||
.direction = .vertical,
|
.direction = .vertical,
|
||||||
},
|
},
|
||||||
}, .{});
|
}, .{});
|
||||||
@@ -84,23 +84,31 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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
|
// 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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: returned errors should be propagated back to the application
|
// NOTE returned errors should be propagated back to the application
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,20 @@ const QuitText = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,23 +76,31 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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
|
// 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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: returned errors should be propagated back to the application
|
// NOTE returned errors should be propagated back to the application
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,20 @@ const QuitText = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,13 +53,12 @@ pub fn main() !void {
|
|||||||
.layout = .{
|
.layout = .{
|
||||||
.gap = 2,
|
.gap = 2,
|
||||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||||
.direction = .horizontal,
|
|
||||||
},
|
},
|
||||||
}, element);
|
}, element);
|
||||||
for (0..3) |i| {
|
for (0..3) |i| {
|
||||||
var column = try App.Container.init(allocator, .{
|
var column = try App.Container.init(allocator, .{
|
||||||
.border = .{ .separator = .{ .enabled = true } },
|
|
||||||
.layout = .{
|
.layout = .{
|
||||||
|
.separator = .{ .enabled = true },
|
||||||
.direction = if (i > 0) .vertical else .horizontal,
|
.direction = if (i > 0) .vertical else .horizontal,
|
||||||
},
|
},
|
||||||
}, .{});
|
}, .{});
|
||||||
@@ -71,7 +70,12 @@ pub fn main() !void {
|
|||||||
.rectangle = .{ .fill = .yellow },
|
.rectangle = .{ .fill = .yellow },
|
||||||
}, .{}));
|
}, .{}));
|
||||||
} else {
|
} else {
|
||||||
try column.append(try App.Container.init(allocator, .{}, .{}));
|
try column.append(try App.Container.init(allocator, .{
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = 4 },
|
||||||
|
.grow = .horizontal,
|
||||||
|
},
|
||||||
|
}, .{}));
|
||||||
}
|
}
|
||||||
try column.append(try App.Container.init(allocator, .{
|
try column.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .blue },
|
.rectangle = .{ .fill = .blue },
|
||||||
@@ -88,23 +92,31 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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
|
// 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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: returned errors should be propagated back to the application
|
// NOTE returned errors should be propagated back to the application
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,20 @@ const QuitText = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,23 +75,31 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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
|
// 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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: returned errors should be propagated back to the application
|
// NOTE returned errors should be propagated back to the application
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,20 @@ const QuitText = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,6 @@ pub fn main() !void {
|
|||||||
|
|
||||||
var box = try App.Container.init(allocator, .{
|
var box = try App.Container.init(allocator, .{
|
||||||
.layout = .{ .direction = .horizontal },
|
.layout = .{ .direction = .horizontal },
|
||||||
.min_size = .{ .cols = 3 * std.meta.fields(zterm.Color).len }, // ensure enough columns to render all colors -> scrollable otherwise
|
|
||||||
}, .{});
|
}, .{});
|
||||||
defer box.deinit();
|
defer box.deinit();
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ pub fn main() !void {
|
|||||||
if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||||
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
|
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
|
||||||
}
|
}
|
||||||
var scrollable: App.Scrollable = .init(box);
|
var scrollable: App.Scrollable = .{ .container = box };
|
||||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||||
|
|
||||||
try app.start();
|
try app.start();
|
||||||
@@ -74,10 +73,8 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
@@ -89,6 +86,16 @@ pub fn main() !void {
|
|||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,20 @@ const QuitText = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
if (anchor + idx == cells.len - 1) break;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,10 +38,10 @@ const TextStyles = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
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, size: zterm.Point) !void {
|
||||||
@setEvalBranchQuota(50000);
|
@setEvalBranchQuota(50000);
|
||||||
_ = ctx;
|
_ = 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));
|
||||||
|
|
||||||
var row: usize = 0;
|
var row: usize = 0;
|
||||||
var col: usize = 0;
|
var col: usize = 0;
|
||||||
@@ -56,9 +56,9 @@ const TextStyles = struct {
|
|||||||
|
|
||||||
// witouth any emphasis
|
// witouth any emphasis
|
||||||
for (text) |cp| {
|
for (text) |cp| {
|
||||||
cells[(row * size.cols) + col].style.bg = @enumFromInt(bg_field.value);
|
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
|
||||||
cells[(row * size.cols) + col].style.fg = @enumFromInt(fg_field.value);
|
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
|
||||||
cells[(row * size.cols) + col].cp = cp;
|
cells[(row * size.x) + col].cp = cp;
|
||||||
col += 1;
|
col += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +68,10 @@ const TextStyles = struct {
|
|||||||
const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value);
|
const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value);
|
||||||
|
|
||||||
for (text) |cp| {
|
for (text) |cp| {
|
||||||
cells[(row * size.cols) + col].style.bg = @enumFromInt(bg_field.value);
|
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
|
||||||
cells[(row * size.cols) + col].style.fg = @enumFromInt(fg_field.value);
|
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
|
||||||
cells[(row * size.cols) + col].style.emphasis = &.{emphasis};
|
cells[(row * size.x) + col].style.emphasis = &.{emphasis};
|
||||||
cells[(row * size.cols) + col].cp = cp;
|
cells[(row * size.x) + col].cp = cp;
|
||||||
col += 1;
|
col += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,14 +109,16 @@ pub fn main() !void {
|
|||||||
|
|
||||||
var box = try App.Container.init(allocator, .{
|
var box = try App.Container.init(allocator, .{
|
||||||
.layout = .{ .direction = .vertical },
|
.layout = .{ .direction = .vertical },
|
||||||
.min_size = .{
|
.size = .{
|
||||||
.rows = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
|
.dim = .{
|
||||||
.cols = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
|
.x = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
|
||||||
}, // ensure enough rows and/or columns to render all text styles -> scrollable otherwise
|
.y = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
|
||||||
|
},
|
||||||
|
},
|
||||||
}, text_styles.element());
|
}, text_styles.element());
|
||||||
defer box.deinit();
|
defer box.deinit();
|
||||||
|
|
||||||
var scrollable: App.Scrollable = .init(box);
|
var scrollable: App.Scrollable = .{ .container = box };
|
||||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||||
|
|
||||||
try app.start();
|
try app.start();
|
||||||
@@ -126,10 +128,8 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
@@ -141,6 +141,16 @@ pub fn main() !void {
|
|||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try renderer.resize();
|
||||||
|
container.resize(renderer.size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
try renderer.render(@TypeOf(container), &container);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/app.zig
48
src/app.zig
@@ -11,7 +11,7 @@ const isTaggedUnion = event.isTaggedUnion;
|
|||||||
|
|
||||||
const Mouse = input.Mouse;
|
const Mouse = input.Mouse;
|
||||||
const Key = input.Key;
|
const Key = input.Key;
|
||||||
const Size = @import("size.zig").Size;
|
const Point = @import("point.zig").Point;
|
||||||
|
|
||||||
const log = std.log.scoped(.app);
|
const log = std.log.scoped(.app);
|
||||||
|
|
||||||
@@ -51,7 +51,6 @@ pub fn App(comptime E: type) type {
|
|||||||
quit_event: std.Thread.ResetEvent,
|
quit_event: std.Thread.ResetEvent,
|
||||||
termios: ?std.posix.termios = null,
|
termios: ?std.posix.termios = null,
|
||||||
attached_handler: bool = false,
|
attached_handler: bool = false,
|
||||||
prev_size: Size,
|
|
||||||
|
|
||||||
pub const SignalHandler = struct {
|
pub const SignalHandler = struct {
|
||||||
context: *anyopaque,
|
context: *anyopaque,
|
||||||
@@ -64,7 +63,6 @@ pub fn App(comptime E: type) type {
|
|||||||
.quit_event = .{},
|
.quit_event = .{},
|
||||||
.termios = null,
|
.termios = null,
|
||||||
.attached_handler = false,
|
.attached_handler = false,
|
||||||
.prev_size = .{},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn start(this: *@This()) !void {
|
pub fn start(this: *@This()) !void {
|
||||||
@@ -99,11 +97,6 @@ pub fn App(comptime E: type) type {
|
|||||||
try terminal.enterAltScreen();
|
try terminal.enterAltScreen();
|
||||||
try terminal.hideCursor();
|
try terminal.hideCursor();
|
||||||
try terminal.enableMouseSupport();
|
try terminal.enableMouseSupport();
|
||||||
|
|
||||||
// send initial size afterwards
|
|
||||||
const size = terminal.getTerminalSize();
|
|
||||||
this.postEvent(.{ .resize = size });
|
|
||||||
this.prev_size = size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn interrupt(this: *@This()) !void {
|
pub fn interrupt(this: *@This()) !void {
|
||||||
@@ -148,11 +141,8 @@ pub fn App(comptime E: type) type {
|
|||||||
|
|
||||||
fn winsizeCallback(ptr: *anyopaque) void {
|
fn winsizeCallback(ptr: *anyopaque) void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ptr));
|
const this: *@This() = @ptrCast(@alignCast(ptr));
|
||||||
const size = terminal.getTerminalSize();
|
_ = this;
|
||||||
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
|
// this.postEvent(.{ .size = terminal.getTerminalSize() });
|
||||||
this.postEvent(.{ .resize = size });
|
|
||||||
this.prev_size = size;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var winch_handler: ?SignalHandler = null;
|
var winch_handler: ?SignalHandler = null;
|
||||||
@@ -174,11 +164,11 @@ pub fn App(comptime E: type) type {
|
|||||||
// thread to read user inputs
|
// thread to read user inputs
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
while (true) {
|
while (true) {
|
||||||
// FIX: I still think that there is a race condition (I'm just waiting 'long' enough)
|
// FIX I still think that there is a race condition (I'm just waiting 'long' enough)
|
||||||
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch {
|
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch {
|
||||||
// FIX: in case the queue is full -> the next user input should panic and quit the application? because something seems to clock up the event queue
|
// FIX in case the queue is full -> the next user input should panic and quit the application? because something seems to clock up the event queue
|
||||||
const read_bytes = try terminal.read(buf[0..]);
|
const read_bytes = try terminal.read(buf[0..]);
|
||||||
// TODO: `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
|
// TODO `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
|
||||||
// escape key presses
|
// escape key presses
|
||||||
if (buf[0] == 0x1b and read_bytes > 1) {
|
if (buf[0] == 0x1b and read_bytes > 1) {
|
||||||
switch (buf[1]) {
|
switch (buf[1]) {
|
||||||
@@ -273,7 +263,7 @@ pub fn App(comptime E: type) type {
|
|||||||
};
|
};
|
||||||
this.postEvent(.{ .key = key });
|
this.postEvent(.{ .key = key });
|
||||||
},
|
},
|
||||||
// TODO: focus usage? should this even be in the default event system?
|
// TODO focus usage? should this even be in the default event system?
|
||||||
'I' => this.postEvent(.{ .focus = true }),
|
'I' => this.postEvent(.{ .focus = true }),
|
||||||
'O' => this.postEvent(.{ .focus = false }),
|
'O' => this.postEvent(.{ .focus = false }),
|
||||||
'M', 'm' => {
|
'M', 'm' => {
|
||||||
@@ -302,8 +292,8 @@ pub fn App(comptime E: type) type {
|
|||||||
|
|
||||||
const mouse: Mouse = .{
|
const mouse: Mouse = .{
|
||||||
.button = button,
|
.button = button,
|
||||||
.col = px -| 1,
|
.x = px -| 1,
|
||||||
.row = py -| 1,
|
.y = py -| 1,
|
||||||
.kind = blk: {
|
.kind = blk: {
|
||||||
if (motion and button != Mouse.Button.none) {
|
if (motion and button != Mouse.Button.none) {
|
||||||
break :blk .drag;
|
break :blk .drag;
|
||||||
@@ -334,19 +324,15 @@ pub fn App(comptime E: type) type {
|
|||||||
if (std.mem.eql(u8, "48", ps)) {
|
if (std.mem.eql(u8, "48", ps)) {
|
||||||
// in band window resize
|
// in band window resize
|
||||||
// CSI 48 ; height ; width ; height_pix ; width_pix t
|
// CSI 48 ; height ; width ; height_pix ; width_pix t
|
||||||
const height_char = iter.next() orelse break;
|
|
||||||
const width_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?
|
_ = width_char;
|
||||||
// because there might be too many resize events (which force a re-draw of the entire screen)
|
_ = height_char;
|
||||||
const size: Size = .{
|
// this.postEvent(.{ .size = .{
|
||||||
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
|
// .x = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
|
||||||
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
|
// .y = std.fmt.parseUnsigned(u16, height_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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'u' => {
|
'u' => {
|
||||||
@@ -361,7 +347,7 @@ pub fn App(comptime E: type) type {
|
|||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// TODO: parse corresponding codes
|
// TODO parse corresponding codes
|
||||||
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
|
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const Style = @import("style.zig");
|
|||||||
pub const Cell = @This();
|
pub const Cell = @This();
|
||||||
|
|
||||||
style: Style = .{ .emphasis = &.{} },
|
style: Style = .{ .emphasis = &.{} },
|
||||||
// TODO: embrace `zg` dependency more due to utf-8 encoding
|
// TODO embrace `zg` dependency more due to utf-8 encoding
|
||||||
cp: u21 = ' ',
|
cp: u21 = ' ',
|
||||||
|
|
||||||
pub fn eql(this: Cell, other: Cell) bool {
|
pub fn eql(this: Cell, other: Cell) bool {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub const Color = enum(u8) {
|
|||||||
magenta,
|
magenta,
|
||||||
cyan,
|
cyan,
|
||||||
white,
|
white,
|
||||||
// TODO: add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
|
// TODO add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
|
||||||
|
|
||||||
pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void {
|
pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void {
|
||||||
if (this == .default) {
|
if (this == .default) {
|
||||||
|
|||||||
1036
src/container.zig
1036
src/container.zig
File diff suppressed because it is too large
Load Diff
@@ -92,7 +92,7 @@ pub const bg_rgb_legacy = "\x1b[48;2;{d};{d};{d}m";
|
|||||||
pub const ul_rgb_legacy = "\x1b[58;2;{d};{d};{d}m";
|
pub const ul_rgb_legacy = "\x1b[58;2;{d};{d};{d}m";
|
||||||
|
|
||||||
// Underlines
|
// Underlines
|
||||||
pub const ul_off = "\x1b[24m"; // NOTE: this could be \x1b[4:0m but is not as widely supported
|
pub const ul_off = "\x1b[24m"; // NOTE this could be \x1b[4:0m but is not as widely supported
|
||||||
pub const ul_single = "\x1b[4m";
|
pub const ul_single = "\x1b[4m";
|
||||||
pub const ul_double = "\x1b[4:2m";
|
pub const ul_double = "\x1b[4:2m";
|
||||||
pub const ul_curly = "\x1b[4:3m";
|
pub const ul_curly = "\x1b[4:3m";
|
||||||
|
|||||||
329
src/element.zig
329
src/element.zig
@@ -1,13 +1,11 @@
|
|||||||
//! Interface for Element's which describe the contents of a `Container`.
|
//! Interface for Element's which describe the contents of a `Container`.
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const s = @import("size.zig");
|
|
||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
|
|
||||||
const Container = @import("container.zig").Container;
|
const Container = @import("container.zig").Container;
|
||||||
const Cell = @import("cell.zig");
|
const Cell = @import("cell.zig");
|
||||||
const Mouse = input.Mouse;
|
const Mouse = input.Mouse;
|
||||||
const Position = s.Position;
|
const Point = @import("point.zig").Point;
|
||||||
const Size = s.Size;
|
|
||||||
|
|
||||||
pub fn Element(Event: type) type {
|
pub fn Element(Event: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
@@ -15,12 +13,26 @@ pub fn Element(Event: type) type {
|
|||||||
vtable: *const VTable = &.{},
|
vtable: *const VTable = &.{},
|
||||||
|
|
||||||
pub const VTable = struct {
|
pub const VTable = struct {
|
||||||
|
resize: ?*const fn (ctx: *anyopaque, size: Point) void = null,
|
||||||
|
reposition: ?*const fn (ctx: *anyopaque, origin: Point) void = null,
|
||||||
handle: ?*const fn (ctx: *anyopaque, event: Event) anyerror!void = null,
|
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, size: Point) anyerror!void = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Resize the corresponding `Element` with the given *size*.
|
||||||
|
pub fn resize(this: @This(), size: Point) void {
|
||||||
|
if (this.vtable.resize) |resize_fn|
|
||||||
|
resize_fn(this.ptr, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reposition the corresponding `Element` with the given *origin*.
|
||||||
|
pub fn reposition(this: @This(), origin: Point) void {
|
||||||
|
if (this.vtable.reposition) |reposition_fn|
|
||||||
|
reposition_fn(this.ptr, origin);
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle the received event. The event is one of the user provided
|
/// Handle the received event. The event is one of the user provided
|
||||||
/// events or a system event, with the exception of the `.resize`
|
/// events or a system event, with the exception of the `.size`
|
||||||
/// `Event` as every `Container` already handles that event.
|
/// `Event` as every `Container` already handles that event.
|
||||||
///
|
///
|
||||||
/// In case of user errors this function should return an error. This
|
/// In case of user errors this function should return an error. This
|
||||||
@@ -32,14 +44,21 @@ pub fn Element(Event: type) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Write content into the `cells` of the `Container`. The associated
|
/// 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.
|
/// renderer will know where to place the contents on the screen.
|
||||||
///
|
///
|
||||||
/// This function should only fail with an error if the error is
|
/// # Note
|
||||||
/// non-recoverable (i.e. an allocation error, system error, etc.).
|
///
|
||||||
/// Otherwise user specific errors should be caught using the `handle`
|
/// - Caller owns `cells` slice and ensures that the size usually by assertion:
|
||||||
/// function before the rendering of the `Container` happens.
|
/// ```zig
|
||||||
pub inline fn content(this: @This(), cells: []Cell, size: Size) !void {
|
/// 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: Point) !void {
|
||||||
if (this.vtable.content) |content_fn|
|
if (this.vtable.content) |content_fn|
|
||||||
try content_fn(this.ptr, cells, size);
|
try content_fn(this.ptr, cells, size);
|
||||||
}
|
}
|
||||||
@@ -50,63 +69,64 @@ pub fn Scrollable(Event: type) type {
|
|||||||
return struct {
|
return struct {
|
||||||
/// `Size` of the actual contents where the anchor and the size is
|
/// `Size` of the actual contents where the anchor and the size is
|
||||||
/// representing the size and location on screen.
|
/// representing the size and location on screen.
|
||||||
size: Size = .{},
|
size: Point = .{},
|
||||||
/// `Size` of the `Container` content that is scrollable and mapped to
|
/// `Size` of the `Container` content that is scrollable and mapped to
|
||||||
/// the *size* of the `Scrollable` `Element`.
|
/// the *size* of the `Scrollable` `Element`.
|
||||||
container_size: Size = .{},
|
container_size: Point = .{},
|
||||||
/// Anchor of the viewport of the scrollable `Container`.
|
/// Anchor of the viewport of the scrollable `Container`.
|
||||||
anchor: Position = .{},
|
anchor: Point = .{},
|
||||||
/// The actual container, that is scrollable.
|
/// The actual `Container`, that is scrollable.
|
||||||
container: Container(Event),
|
container: Container(Event),
|
||||||
|
|
||||||
pub fn element(this: *@This()) Element(Event) {
|
pub fn element(this: *@This()) Element(Event) {
|
||||||
return .{
|
return .{
|
||||||
.ptr = this,
|
.ptr = this,
|
||||||
.vtable = &.{
|
.vtable = &.{
|
||||||
|
.resize = resize,
|
||||||
|
.reposition = reposition,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
.content = content,
|
.content = content,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(container: Container(Event)) @This() {
|
fn resize(ctx: *anyopaque, size: Point) void {
|
||||||
return .{ .container = container };
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
this.size = size;
|
||||||
|
|
||||||
|
// TODO scrollbar space - depending on configuration and only if necessary?
|
||||||
|
this.container.resize(this.size);
|
||||||
|
this.container_size = this.container.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reposition(ctx: *anyopaque, _: Point) void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
this.container.reposition(.{});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle(ctx: *anyopaque, event: Event) !void {
|
fn handle(ctx: *anyopaque, event: Event) !void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.resize => |size| {
|
// TODO other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?)
|
||||||
this.size = size;
|
|
||||||
// TODO: scrollbar space - depending on configuration and only if necessary?
|
|
||||||
const min_size = this.container.minSize();
|
|
||||||
this.container_size = .{
|
|
||||||
.anchor = size.anchor,
|
|
||||||
.cols = @max(min_size.cols, size.cols),
|
|
||||||
.rows = @max(min_size.rows, size.rows),
|
|
||||||
};
|
|
||||||
try this.container.handle(.{ .resize = 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 => |mouse| switch (mouse.button) {
|
||||||
Mouse.Button.wheel_up => if (this.container_size.rows > this.size.rows) {
|
Mouse.Button.wheel_up => if (this.container_size.y > this.size.y) {
|
||||||
this.anchor.row -|= 1;
|
this.anchor.y -|= 1;
|
||||||
},
|
},
|
||||||
Mouse.Button.wheel_down => if (this.container_size.rows > this.size.rows) {
|
Mouse.Button.wheel_down => if (this.container_size.y > this.size.y) {
|
||||||
const max_anchor_row = this.container_size.rows -| this.size.rows;
|
const max_origin_y = this.container_size.y -| this.size.y;
|
||||||
this.anchor.row = @min(this.anchor.row + 1, max_anchor_row);
|
this.anchor.y = @min(this.anchor.y + 1, max_origin_y);
|
||||||
},
|
},
|
||||||
Mouse.Button.wheel_left => if (this.container_size.cols > this.size.cols) {
|
Mouse.Button.wheel_left => if (this.container_size.x > this.size.x) {
|
||||||
this.anchor.col -|= 1;
|
this.anchor.x -|= 1;
|
||||||
},
|
},
|
||||||
Mouse.Button.wheel_right => if (this.container_size.cols > this.size.cols) {
|
Mouse.Button.wheel_right => if (this.container_size.x > this.size.x) {
|
||||||
const max_anchor_col = this.container_size.cols -| this.size.cols;
|
const max_anchor_x = this.container_size.x -| this.size.x;
|
||||||
this.anchor.col = @min(this.anchor.col + 1, max_anchor_col);
|
this.anchor.x = @min(this.anchor.x + 1, max_anchor_x);
|
||||||
},
|
},
|
||||||
else => try this.container.handle(.{
|
else => try this.container.handle(.{
|
||||||
.mouse = .{
|
.mouse = .{
|
||||||
.col = mouse.col + this.anchor.col,
|
.x = mouse.x + this.anchor.x,
|
||||||
.row = mouse.row + this.anchor.row,
|
.y = mouse.y + this.anchor.y,
|
||||||
.button = mouse.button,
|
.button = mouse.button,
|
||||||
.kind = mouse.kind,
|
.kind = mouse.kind,
|
||||||
},
|
},
|
||||||
@@ -116,48 +136,209 @@ pub fn Scrollable(Event: type) type {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []Cell, size: Size) !void {
|
fn render_container(container: Container(Event), cells: []Cell, container_size: Point) !void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const size = container.size;
|
||||||
std.debug.assert(cells.len == @as(usize, this.size.cols) * @as(usize, this.size.rows));
|
const origin = container.origin;
|
||||||
|
const contents = try container.content();
|
||||||
|
defer container.allocator.free(contents);
|
||||||
|
|
||||||
const container_size = this.container.size;
|
const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x);
|
||||||
const container_cells = try this.container.allocator.alloc(Cell, @as(usize, container_size.cols) * @as(usize, container_size.rows));
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
@memcpy(container_cells, container_cells_const);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (this.container.elements.items) |*e| {
|
var idx: usize = 0;
|
||||||
const e_size = e.size;
|
blk: for (0..size.y) |row| {
|
||||||
const element_cells = try e.contents();
|
for (0..size.x) |col| {
|
||||||
defer e.allocator.free(element_cells);
|
cells[anchor + (row * container_size.x) + col] = contents[idx];
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
const anchor = (@as(usize, e_size.anchor.row -| container_size.anchor.row) * @as(usize, container_size.cols)) +
|
if (contents.len == idx) break :blk;
|
||||||
@as(usize, e_size.anchor.col -| container_size.anchor.col);
|
|
||||||
|
|
||||||
var idx: usize = 0;
|
|
||||||
blk: for (0..e_size.rows) |row| {
|
|
||||||
for (0..e_size.cols) |col| {
|
|
||||||
const cell = element_cells[idx];
|
|
||||||
idx += 1;
|
|
||||||
|
|
||||||
container_cells[anchor + (row * container_size.cols) + col] = cell;
|
|
||||||
|
|
||||||
if (element_cells.len == idx) break :blk;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchor = (@as(usize, this.anchor.row) * @as(usize, container_size.cols)) + @as(usize, this.anchor.col);
|
for (container.elements.items) |child| try render_container(child, cells, size);
|
||||||
// TODO: render scrollbar according to configuration!
|
}
|
||||||
for (0..size.rows) |row| {
|
|
||||||
for (0..size.cols) |col| {
|
fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void {
|
||||||
cells[(row * size.cols) + col] = container_cells[anchor + (row * container_size.cols) + col];
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
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.x) * @as(usize, container_size.y));
|
||||||
|
{
|
||||||
|
const container_cells_const = try this.container.content();
|
||||||
|
defer this.container.allocator.free(container_cells_const);
|
||||||
|
std.debug.assert(container_cells_const.len == @as(usize, container_size.x) * @as(usize, container_size.y));
|
||||||
|
@memcpy(container_cells, container_cells_const);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (this.container.elements.items) |child| try render_container(child, container_cells, container_size);
|
||||||
|
|
||||||
|
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.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);
|
this.container.allocator.free(container_cells);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO nested scrollable `Container`s?'
|
||||||
|
// TODO reaction only for when the event is actually pushed to the corresponding `Container` rendered container
|
||||||
|
|
||||||
|
test "scrollable vertical" {
|
||||||
|
const event = @import("event.zig");
|
||||||
|
const testing = @import("testing.zig");
|
||||||
|
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
const size: Point = .{
|
||||||
|
.x = 30,
|
||||||
|
.y = 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
var box: Container(event.SystemEvent) = try .init(allocator, .{
|
||||||
|
.border = .{
|
||||||
|
.sides = .all,
|
||||||
|
.color = .red,
|
||||||
|
},
|
||||||
|
.layout = .{
|
||||||
|
.separator = .{
|
||||||
|
.enabled = true,
|
||||||
|
.color = .red,
|
||||||
|
},
|
||||||
|
.direction = .vertical,
|
||||||
|
.padding = .all(1),
|
||||||
|
},
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = size.y + 15 },
|
||||||
|
},
|
||||||
|
}, .{});
|
||||||
|
try box.append(try .init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .grey },
|
||||||
|
}, .{}));
|
||||||
|
try box.append(try .init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .grey },
|
||||||
|
}, .{}));
|
||||||
|
defer box.deinit();
|
||||||
|
|
||||||
|
var scrollable: Scrollable(event.SystemEvent) = .{ .container = box };
|
||||||
|
|
||||||
|
var container: Container(event.SystemEvent) = try .init(allocator, .{
|
||||||
|
.border = .{
|
||||||
|
.color = .green,
|
||||||
|
.sides = .vertical,
|
||||||
|
},
|
||||||
|
}, scrollable.element());
|
||||||
|
defer container.deinit();
|
||||||
|
|
||||||
|
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/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,
|
||||||
|
.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);
|
||||||
|
|
||||||
|
// further scrolling down will not change anything
|
||||||
|
try container.handle(.{
|
||||||
|
.mouse = .{
|
||||||
|
.button = .wheel_down,
|
||||||
|
.kind = .press,
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "scrollable horizontal" {
|
||||||
|
const event = @import("event.zig");
|
||||||
|
const testing = @import("testing.zig");
|
||||||
|
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
const size: Point = .{
|
||||||
|
.x = 30,
|
||||||
|
.y = 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
var box: Container(event.SystemEvent) = try .init(allocator, .{
|
||||||
|
.border = .{
|
||||||
|
.sides = .all,
|
||||||
|
.color = .red,
|
||||||
|
},
|
||||||
|
.layout = .{
|
||||||
|
.separator = .{
|
||||||
|
.enabled = true,
|
||||||
|
.color = .red,
|
||||||
|
},
|
||||||
|
.direction = .horizontal,
|
||||||
|
.padding = .all(1),
|
||||||
|
},
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .x = size.x + 15 },
|
||||||
|
},
|
||||||
|
}, .{});
|
||||||
|
try box.append(try .init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .grey },
|
||||||
|
}, .{}));
|
||||||
|
try box.append(try .init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .grey },
|
||||||
|
}, .{}));
|
||||||
|
defer box.deinit();
|
||||||
|
|
||||||
|
var scrollable: Scrollable(event.SystemEvent) = .{ .container = box };
|
||||||
|
|
||||||
|
var container: Container(event.SystemEvent) = try .init(allocator, .{
|
||||||
|
.border = .{
|
||||||
|
.color = .green,
|
||||||
|
.sides = .horizontal,
|
||||||
|
},
|
||||||
|
}, scrollable.element());
|
||||||
|
defer container.deinit();
|
||||||
|
|
||||||
|
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/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,
|
||||||
|
.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);
|
||||||
|
|
||||||
|
// further scrolling right will not change anything
|
||||||
|
try container.handle(.{
|
||||||
|
.mouse = .{
|
||||||
|
.button = .wheel_right,
|
||||||
|
.kind = .press,
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|||||||
4
src/error.zig
Normal file
4
src/error.zig
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub const Error = error{
|
||||||
|
/// Thrown when a `Container` is too small to be rendered in the current screen part.
|
||||||
|
TooSmall,
|
||||||
|
};
|
||||||
@@ -6,12 +6,12 @@ const terminal = @import("terminal.zig");
|
|||||||
|
|
||||||
const Key = input.Key;
|
const Key = input.Key;
|
||||||
const Mouse = input.Mouse;
|
const Mouse = input.Mouse;
|
||||||
const Size = @import("size.zig").Size;
|
const Point = @import("point.zig").Point;
|
||||||
|
|
||||||
/// System events available to every `zterm.App`
|
/// System events available to every `zterm.App`
|
||||||
pub const SystemEvent = union(enum) {
|
pub const SystemEvent = union(enum) {
|
||||||
/// Initialize event, which is send once at the beginning of the event loop and before the first render loop
|
/// Initialize event, which is send once at the beginning of the event loop and before the first render loop
|
||||||
/// TODO: not sure if this is necessary or if there is an actual usecase for this - for now it will remain
|
/// TODO not sure if this is necessary or if there is an actual usecase for this - for now it will remain
|
||||||
init,
|
init,
|
||||||
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
|
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
|
||||||
quit,
|
quit,
|
||||||
@@ -21,14 +21,12 @@ pub const SystemEvent = union(enum) {
|
|||||||
/// associated error message
|
/// associated error message
|
||||||
msg: []const u8,
|
msg: []const u8,
|
||||||
},
|
},
|
||||||
/// Resize event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in
|
|
||||||
resize: Size,
|
|
||||||
/// Input key event received from the user
|
/// Input key event received from the user
|
||||||
key: Key,
|
key: Key,
|
||||||
/// Mouse input event
|
/// Mouse input event
|
||||||
mouse: Mouse,
|
mouse: Mouse,
|
||||||
/// Focus event for mouse interaction
|
/// Focus event for mouse interaction
|
||||||
/// TODO: this should instead be a union with a `Size` to derive which container / element the focus meant for
|
/// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for
|
||||||
focus: bool,
|
focus: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
|
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const Size = @import("size.zig").Size;
|
const Point = @import("point.zig").Point;
|
||||||
|
|
||||||
pub const Mouse = packed struct {
|
pub const Mouse = packed struct {
|
||||||
col: u16,
|
x: u16,
|
||||||
row: u16,
|
y: u16,
|
||||||
button: Button,
|
button: Button,
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
|
|
||||||
@@ -35,9 +35,9 @@ pub const Mouse = packed struct {
|
|||||||
return std.meta.eql(this, other);
|
return std.meta.eql(this, other);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn in(this: @This(), size: Size) bool {
|
pub fn in(this: @This(), origin: Point, size: Point) bool {
|
||||||
return this.col >= size.anchor.col and this.col <= size.cols + size.anchor.col and
|
return this.x >= origin.x and this.x < size.x + origin.x and
|
||||||
this.row >= size.anchor.row and this.row <= size.rows + size.anchor.row;
|
this.y >= origin.y and this.y < size.y + origin.y;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
60
src/point.zig
Normal file
60
src/point.zig
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
pub const Point = packed struct {
|
||||||
|
x: u16 = 0,
|
||||||
|
y: u16 = 0,
|
||||||
|
|
||||||
|
pub fn add(a: @This(), b: @This()) @This() {
|
||||||
|
return .{
|
||||||
|
.x = a.x + b.x,
|
||||||
|
.y = a.y + b.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max(a: @This(), b: @This()) @This() {
|
||||||
|
return .{
|
||||||
|
.x = @max(a.x, b.x),
|
||||||
|
.y = @max(a.y, b.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;
|
||||||
|
}
|
||||||
@@ -2,14 +2,13 @@ const std = @import("std");
|
|||||||
const terminal = @import("terminal.zig");
|
const terminal = @import("terminal.zig");
|
||||||
|
|
||||||
const Cell = @import("cell.zig");
|
const Cell = @import("cell.zig");
|
||||||
const Position = @import("size.zig").Position;
|
const Point = @import("point.zig").Point;
|
||||||
const Size = @import("size.zig").Size;
|
|
||||||
|
|
||||||
/// Double-buffered intermediate rendering pipeline
|
/// Double-buffered intermediate rendering pipeline
|
||||||
pub const Buffered = struct {
|
pub const Buffered = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
created: bool,
|
created: bool,
|
||||||
size: Size,
|
size: Point,
|
||||||
screen: []Cell,
|
screen: []Cell,
|
||||||
virtual_screen: []Cell,
|
virtual_screen: []Cell,
|
||||||
|
|
||||||
@@ -30,9 +29,12 @@ pub const Buffered = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(this: *@This(), size: Size) !void {
|
pub fn resize(this: *@This()) !void {
|
||||||
|
const size = terminal.getTerminalSize();
|
||||||
|
if (std.meta.eql(this.size, size)) return;
|
||||||
|
|
||||||
this.size = size;
|
this.size = size;
|
||||||
const n = @as(usize, size.cols) * @as(usize, size.rows);
|
const n = @as(usize, this.size.x) * @as(usize, this.size.y);
|
||||||
|
|
||||||
if (!this.created) {
|
if (!this.created) {
|
||||||
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
||||||
@@ -60,23 +62,21 @@ pub const Buffered = struct {
|
|||||||
|
|
||||||
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
|
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
|
||||||
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
|
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
|
||||||
const size: Size = container.size;
|
const size: Point = container.size;
|
||||||
const cells: []const Cell = try container.contents();
|
const origin: Point = container.origin;
|
||||||
|
const cells: []const Cell = try container.content();
|
||||||
|
|
||||||
if (cells.len == 0) return;
|
if (cells.len == 0) return;
|
||||||
|
|
||||||
var idx: usize = 0;
|
var idx: usize = 0;
|
||||||
var vs = this.virtual_screen;
|
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| {
|
blk: for (0..size.y) |row| {
|
||||||
for (0..size.cols) |col| {
|
for (0..size.x) |col| {
|
||||||
const cell = cells[idx];
|
vs[anchor + (row * this.size.x) + col] = cells[idx];
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
vs[anchor + (row * this.size.cols) + col].style = cell.style;
|
|
||||||
vs[anchor + (row * this.size.cols) + col].cp = cell.cp;
|
|
||||||
|
|
||||||
if (cells.len == idx) break :blk;
|
if (cells.len == idx) break :blk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,19 +88,19 @@ pub const Buffered = struct {
|
|||||||
|
|
||||||
/// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop).
|
/// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop).
|
||||||
pub fn flush(this: *@This()) !void {
|
pub fn flush(this: *@This()) !void {
|
||||||
// TODO: measure timings of rendered frames?
|
// TODO measure timings of rendered frames?
|
||||||
const writer = terminal.writer();
|
const writer = terminal.writer();
|
||||||
const s = this.screen;
|
const s = this.screen;
|
||||||
const vs = this.virtual_screen;
|
const vs = this.virtual_screen;
|
||||||
for (0..this.size.rows) |row| {
|
for (0..this.size.y) |row| {
|
||||||
for (0..this.size.cols) |col| {
|
for (0..this.size.x) |col| {
|
||||||
const idx = (row * this.size.cols) + col;
|
const idx = (row * this.size.x) + col;
|
||||||
const cs = s[idx];
|
const cs = s[idx];
|
||||||
const cvs = vs[idx];
|
const cvs = vs[idx];
|
||||||
if (cs.eql(cvs)) continue;
|
if (cs.eql(cvs)) continue;
|
||||||
|
|
||||||
// render differences found in virtual screen
|
// 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);
|
try cvs.value(writer);
|
||||||
// update screen to be the virtual screen for the next frame
|
// update screen to be the virtual screen for the next frame
|
||||||
s[idx] = vs[idx];
|
s[idx] = vs[idx];
|
||||||
|
|||||||
17
src/size.zig
17
src/size.zig
@@ -1,17 +0,0 @@
|
|||||||
pub const Size = packed struct {
|
|
||||||
anchor: Position = .{},
|
|
||||||
cols: u16 = 0,
|
|
||||||
rows: u16 = 0,
|
|
||||||
|
|
||||||
pub fn merge(this: @This(), other: @This()) Size {
|
|
||||||
return .{
|
|
||||||
.cols = this.cols + other.cols,
|
|
||||||
.rows = this.rows + other.rows,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Position = packed struct {
|
|
||||||
col: u16 = 0,
|
|
||||||
row: u16 = 0,
|
|
||||||
};
|
|
||||||
@@ -56,7 +56,7 @@ pub fn value(this: Style, writer: anytype, cp: u21) !void {
|
|||||||
try std.fmt.format(writer, ";", .{});
|
try std.fmt.format(writer, ";", .{});
|
||||||
try this.bg.write(writer, .bg);
|
try this.bg.write(writer, .bg);
|
||||||
// underline
|
// underline
|
||||||
// FIX: assert that if the underline property is set that the ul style and the attribute for underlining is available
|
// FIX assert that if the underline property is set that the ul style and the attribute for underlining is available
|
||||||
try std.fmt.format(writer, ";", .{});
|
try std.fmt.format(writer, ";", .{});
|
||||||
try this.ul.write(writer, .ul);
|
try this.ul.write(writer, .ul);
|
||||||
// append styles (aka attributes like bold, italic, strikethrough, etc.)
|
// append styles (aka attributes like bold, italic, strikethrough, etc.)
|
||||||
@@ -67,6 +67,6 @@ pub fn value(this: Style, writer: anytype, cp: u21) !void {
|
|||||||
try std.fmt.format(writer, "\x1b[0m", .{});
|
try std.fmt.format(writer, "\x1b[0m", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement helper functions for terminal capabilities:
|
// TODO implement helper functions for terminal capabilities:
|
||||||
// - links / url display (osc 8)
|
// - links / url display (osc 8)
|
||||||
// - show / hide cursor?
|
// - show / hide cursor?
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ const ctlseqs = @import("ctlseqs.zig");
|
|||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
|
|
||||||
const Key = input.Key;
|
const Key = input.Key;
|
||||||
const Position = @import("size.zig").Position;
|
const Point = @import("point.zig").Point;
|
||||||
const Size = @import("size.zig").Size;
|
const Size = @import("point.zig").Point;
|
||||||
const Cell = @import("cell.zig");
|
const Cell = @import("cell.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.terminal);
|
const log = std.log.scoped(.terminal);
|
||||||
@@ -23,7 +23,7 @@ pub const ReportMode = enum {
|
|||||||
pub fn getTerminalSize() Size {
|
pub fn getTerminalSize() Size {
|
||||||
var ws: std.posix.winsize = undefined;
|
var ws: std.posix.winsize = undefined;
|
||||||
_ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws));
|
_ = 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 {
|
pub fn saveScreen() !void {
|
||||||
@@ -89,9 +89,9 @@ pub fn writer() Writer {
|
|||||||
return .{ .context = .{} };
|
return .{ .context = .{} };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setCursorPosition(pos: Position) !void {
|
pub fn setCursorPosition(pos: Point) !void {
|
||||||
var buf: [64]u8 = undefined;
|
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);
|
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +137,8 @@ pub fn getCursorPosition() !Size.Position {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.row = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
|
.x = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
|
||||||
.col = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
|
.y = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
1
src/test/container/fixed_grow_horizontal.zon
Normal file
1
src/test/container/fixed_grow_horizontal.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/fixed_grow_vertical.zon
Normal file
1
src/test/container/fixed_grow_vertical.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/rectangle_with_gap.zon
Normal file
1
src/test/container/rectangle_with_gap.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/rectangle_with_padding.zon
Normal file
1
src/test/container/rectangle_with_padding.zon
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/test/container/rectangle_with_parent_padding.zon
Normal file
1
src/test/container/rectangle_with_parent_padding.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/rectangle_with_separator.zon
Normal file
1
src/test/container/rectangle_with_separator.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/separator_2x_no_gaps.zon
Normal file
1
src/test/container/separator_2x_no_gaps.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/separator_2x_no_gaps_with_border.zon
Normal file
1
src/test/container/separator_2x_no_gaps_with_border.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/separator_2x_no_gaps_with_padding.zon
Normal file
1
src/test/container/separator_2x_no_gaps_with_padding.zon
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/test/container/separator_no_gaps.zon
Normal file
1
src/test/container/separator_no_gaps.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/separator_no_gaps_with_padding.zon
Normal file
1
src/test/container/separator_no_gaps_with_padding.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/scrollable.horizontal.left.zon
Normal file
1
src/test/element/scrollable.horizontal.left.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/scrollable.horizontal.right.zon
Normal file
1
src/test/element/scrollable.horizontal.right.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/scrollable.vertical.bottom.zon
Normal file
1
src/test/element/scrollable.vertical.bottom.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/scrollable.vertical.top.zon
Normal file
1
src/test/element/scrollable.vertical.top.zon
Normal file
File diff suppressed because one or more lines are too long
@@ -5,21 +5,20 @@ const Container = @import("container.zig").Container;
|
|||||||
|
|
||||||
const Cell = @import("cell.zig");
|
const Cell = @import("cell.zig");
|
||||||
const DisplayWidth = @import("DisplayWidth");
|
const DisplayWidth = @import("DisplayWidth");
|
||||||
const Position = @import("size.zig").Position;
|
const Point = @import("point.zig").Point;
|
||||||
const Size = @import("size.zig").Size;
|
|
||||||
|
|
||||||
// TODO: how would I describe the expected screens?
|
// TODO how would I describe the expected screens?
|
||||||
// - including styling?
|
// - including styling?
|
||||||
// - compare generated strings instead? -> how would this be generated for the user?
|
// - compare generated strings instead? -> how would this be generated for the user?
|
||||||
|
|
||||||
/// Single-buffer test rendering pipeline for testing purposes.
|
/// Single-buffer test rendering pipeline for testing purposes.
|
||||||
pub const Renderer = struct {
|
pub const Renderer = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
size: Size,
|
size: Point,
|
||||||
screen: []Cell,
|
screen: []Cell,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, size: Size) @This() {
|
pub fn init(allocator: std.mem.Allocator, size: Point) @This() {
|
||||||
const screen = allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("testing.zig: Out of memory.");
|
const screen = allocator.alloc(Cell, @as(usize, size.x) * @as(usize, size.y)) catch @panic("testing.zig: Out of memory.");
|
||||||
@memset(screen, .{});
|
@memset(screen, .{});
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
@@ -33,9 +32,9 @@ pub const Renderer = struct {
|
|||||||
this.allocator.free(this.screen);
|
this.allocator.free(this.screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(this: *@This(), size: Size) !void {
|
pub fn resize(this: *@This(), size: Point) !void {
|
||||||
this.size = size;
|
this.size = size;
|
||||||
const n = @as(usize, size.cols) * @as(usize, size.rows);
|
const n = @as(usize, size.x) * @as(usize, size.y);
|
||||||
|
|
||||||
this.allocator.free(this.screen);
|
this.allocator.free(this.screen);
|
||||||
this.screen = this.allocator.alloc(Cell, n) catch @panic("testing.zig: Out of memory.");
|
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 {
|
pub fn render(this: *@This(), comptime T: type, container: *const T) !void {
|
||||||
const size: Size = container.size;
|
const size: Point = container.size;
|
||||||
const cells: []const Cell = try container.contents();
|
const origin: Point = container.origin;
|
||||||
|
const cells: []const Cell = try container.content();
|
||||||
|
|
||||||
if (cells.len == 0) return;
|
if (cells.len == 0) return;
|
||||||
|
|
||||||
var idx: usize = 0;
|
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| {
|
blk: for (0..size.y) |row| {
|
||||||
for (0..size.cols) |col| {
|
for (0..size.x) |col| {
|
||||||
const cell = cells[idx];
|
const cell = cells[idx];
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
this.screen[anchor + (row * this.size.cols) + col].style = cell.style;
|
this.screen[anchor + (row * this.size.x) + col].style = cell.style;
|
||||||
this.screen[anchor + (row * this.size.cols) + col].cp = cell.cp;
|
this.screen[anchor + (row * this.size.x) + col].cp = cell.cp;
|
||||||
|
|
||||||
if (cells.len == idx) break :blk;
|
if (cells.len == idx) break :blk;
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ pub const Renderer = struct {
|
|||||||
/// var renderer: testing.Renderer = .init(allocator, size);
|
/// var renderer: testing.Renderer = .init(allocator, size);
|
||||||
/// defer renderer.deinit();
|
/// defer renderer.deinit();
|
||||||
///
|
///
|
||||||
/// try container.handle(.{ .resize = size });
|
/// try container.handle(.{ .size = size });
|
||||||
/// try renderer.render(Container(event.SystemEvent), &container);
|
/// try renderer.render(Container(event.SystemEvent), &container);
|
||||||
/// try renderer.save(file.writer());
|
/// try renderer.save(file.writer());
|
||||||
/// ```
|
/// ```
|
||||||
@@ -115,34 +115,36 @@ pub const Renderer = struct {
|
|||||||
/// .cols = 30,
|
/// .cols = 30,
|
||||||
/// }, &container, @import("test/container/border.all.zon"));
|
/// }, &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;
|
const allocator = std.testing.allocator;
|
||||||
var renderer: Renderer = .init(allocator, size);
|
var renderer: Renderer = .init(allocator, size);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
try container.handle(.{ .resize = size });
|
try renderer.resize(size);
|
||||||
|
container.resize(size);
|
||||||
|
container.reposition(.{});
|
||||||
try renderer.render(Container(event.SystemEvent), container);
|
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
|
/// This function is intended to be used only in tests. Test if the two
|
||||||
/// provided cell arrays are identical. Usually the `Cell` slices are
|
/// provided cell arrays are identical. Usually the `Cell` slices are
|
||||||
/// the contents of a given screen from the `zterm.testing.Renderer`. See
|
/// the contents of a given screen from the `zterm.testing.Renderer`. See
|
||||||
/// `zterm.testing.expectContainerScreen` for an example usage.
|
/// `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;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
try std.testing.expectEqual(expected.len, actual.len);
|
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();
|
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();
|
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();
|
defer output.deinit();
|
||||||
|
|
||||||
var buffer = std.io.bufferedWriter(output.writer());
|
var buffer = std.io.bufferedWriter(output.writer());
|
||||||
@@ -155,23 +157,22 @@ pub fn expectEqualCells(size: Size, expected: []const Cell, actual: []const Cell
|
|||||||
defer dwd.deinit();
|
defer dwd.deinit();
|
||||||
const dw: DisplayWidth = .{ .data = &dwd };
|
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);
|
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);
|
defer allocator.free(actual_centered);
|
||||||
|
|
||||||
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, 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 (origin.y..size.y) |row| {
|
||||||
for (size.anchor.row..size.rows) |row| {
|
|
||||||
defer {
|
defer {
|
||||||
expected_cps.clearRetainingCapacity();
|
expected_cps.clearRetainingCapacity();
|
||||||
actual_cps.clearRetainingCapacity();
|
actual_cps.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
for (size.anchor.col..size.cols) |col| {
|
for (origin.x..size.x) |col| {
|
||||||
const expected_cell = expected[anchor + (row * size.cols) + col];
|
const expected_cell = expected[(row * size.x) + col];
|
||||||
const actual_cell = actual[anchor + (row * size.cols) + col];
|
const actual_cell = actual[(row * size.x) + col];
|
||||||
|
|
||||||
if (!expected_cell.eql(actual_cell)) differ = true;
|
if (!expected_cell.eql(actual_cell)) differ = true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// private imports
|
// private imports
|
||||||
const container = @import("container.zig");
|
const container = @import("container.zig");
|
||||||
const color = @import("color.zig");
|
const color = @import("color.zig");
|
||||||
const size = @import("size.zig");
|
const size = @import("point.zig");
|
||||||
|
|
||||||
// public exports
|
// public exports
|
||||||
pub const input = @import("input.zig");
|
pub const input = @import("input.zig");
|
||||||
pub const testing = @import("testing.zig");
|
pub const testing = @import("testing.zig");
|
||||||
|
|
||||||
pub const App = @import("app.zig").App;
|
pub const App = @import("app.zig").App;
|
||||||
|
pub const Error = @import("error.zig").Error;
|
||||||
// App also exports further types once initialized with the user events at compile time:
|
// App also exports further types once initialized with the user events at compile time:
|
||||||
// `App.Container`
|
// `App.Container`
|
||||||
// `App.Element`
|
// `App.Element`
|
||||||
@@ -23,17 +24,17 @@ pub const Cell = @import("cell.zig");
|
|||||||
pub const Color = color.Color;
|
pub const Color = color.Color;
|
||||||
pub const Key = input.Key;
|
pub const Key = input.Key;
|
||||||
pub const Mouse = input.Mouse;
|
pub const Mouse = input.Mouse;
|
||||||
pub const Position = size.Position;
|
pub const Point = @import("point.zig").Point;
|
||||||
pub const Size = size.Size;
|
|
||||||
pub const Style = @import("style.zig");
|
pub const Style = @import("style.zig");
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = @import("terminal.zig");
|
_ = @import("terminal.zig");
|
||||||
_ = @import("container.zig");
|
_ = @import("container.zig");
|
||||||
_ = @import("queue.zig");
|
_ = @import("queue.zig");
|
||||||
|
_ = @import("error.zig");
|
||||||
|
_ = @import("point.zig");
|
||||||
|
|
||||||
_ = color;
|
_ = color;
|
||||||
_ = size;
|
|
||||||
|
|
||||||
_ = Cell;
|
_ = Cell;
|
||||||
_ = Key;
|
_ = Key;
|
||||||
|
|||||||
Reference in New Issue
Block a user