Compare commits
133 Commits
aeac4bdc83
...
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 | |||
| 8c130a40d7 | |||
| 9a3bc3dbf7 | |||
| 9d5a661b4e | |||
| 4234c9ad0c | |||
| a588e2ef21 | |||
| 8519d204f3 | |||
| 33262c9638 | |||
| 5c5c59cbfc | |||
| c022d1d9e2 | |||
| 12497e92f8 | |||
| d10f738c75 | |||
| 140f27216a | |||
| 04ba88c68b | |||
| 6ccab74c94 | |||
| c634e1affc | |||
| dab486a2c1 | |||
| 9b0dd3c52f | |||
| f45e722578 | |||
| 7b005ea4b1 | |||
| c0c0590bb9 | |||
| 16724f6a52 | |||
| 44e92735cf | |||
| 8cc047c1fa | |||
| 8fbc958ca1 | |||
| b980703350 | |||
| eb89f7f98b | |||
| cc847b7035 | |||
| 9dc1a4b95a | |||
| 69c1600eb9 | |||
| c4639bf4bb | |||
| 07e932741c | |||
| e4ff240839 | |||
| 96375e3b72 | |||
| 9322785ca0 | |||
| cc831a5cdf | |||
| 86b3e7d4ed | |||
| f55d71a7cb | |||
| f66a870223 | |||
| e2f9408850 | |||
| c2080ab40f | |||
| a9f48bfb6a | |||
| 7891af6c6f | |||
| 7b690d387b | |||
| 5c929479b2 | |||
| d8a9e72b67 | |||
| d951906b2b | |||
| 01eb14f1bd | |||
| 1041b0a955 | |||
| 4781e9ce39 | |||
| 5c148e1aa5 | |||
| a6aa6e5150 | |||
| 26d31a38de | |||
| 01d121ef87 | |||
| abaea968a6 | |||
| 73a7f740c9 | |||
| c2a03e95c1 | |||
| 8998afd9d6 | |||
| 4cda202873 | |||
| bbe6f4741e | |||
| 98031dbd1a | |||
| ef950809a6 | |||
| c72d76470a | |||
| 29ae75adf5 | |||
| d326deac97 | |||
| 11531e9d4a | |||
| 8586a05508 | |||
| 009d2129b6 | |||
| 9c06ced658 | |||
| 2bfacc0e98 | |||
| 0bf79dc236 | |||
| 1293cb065d | |||
| bdbe05c996 | |||
| 3decc541a9 | |||
| c83ceff925 | |||
| 1ea410354f | |||
| e9be9a1c35 | |||
| f4a01f227e | |||
| 2efbb5feb1 | |||
| 3048b59e84 | |||
| 817056cf8b | |||
| 0015bac9ff | |||
| c2c3f41ff3 | |||
| 04e1ca087f | |||
| 2ef59ca9ea | |||
| 3a989321fc | |||
| f7cd61d619 | |||
| 3947c2b5af | |||
| bbb360e417 | |||
| 05277a226a | |||
| 8d68945100 | |||
| c0c7b9f925 | |||
| 6cd78d0418 | |||
| f4adf53067 | |||
| 4ef9e077cb | |||
| 7c9038fbda |
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup zig installation
|
||||
uses: mlugg/setup-zig@v1
|
||||
with:
|
||||
version: 0.13.0
|
||||
version: master
|
||||
- name: Run tests
|
||||
run: zig build --release=fast
|
||||
- name: Release build artifacts
|
||||
|
||||
@@ -16,9 +16,9 @@ jobs:
|
||||
- name: Setup zig installation
|
||||
uses: mlugg/setup-zig@v1
|
||||
with:
|
||||
version: 0.13.0
|
||||
version: master
|
||||
- name: Lint check
|
||||
run: zig fmt --check .
|
||||
run: zig fmt --check --exclude src/test .
|
||||
- name: Spell checking
|
||||
uses: crate-ci/typos@v1.25.0
|
||||
with:
|
||||
|
||||
31
README.md
31
README.md
@@ -1,9 +1,22 @@
|
||||
# zterm Terminal User Interface Library
|
||||
# zterm TUI Library
|
||||
|
||||
`zterm` is a terminal user interface library to implement terminal (fullscreen or inline) applications.
|
||||
`zterm` is a terminal user interface library (*tui*) to implement terminal (fullscreen or inline) applications.
|
||||
|
||||
> [!NOTE]
|
||||
> Currently version `0.13.0` is officially supported. Builds using the master version might not work.
|
||||
> [!CAUTION]
|
||||
> Only builds using the zig master version are tested to work.
|
||||
|
||||
## Demo
|
||||
|
||||
Clone this repository and run `zig build --help` to see the available examples. Run a given example as follows:
|
||||
|
||||
```sh
|
||||
zig build --release=safe -Dexample=demo run
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Every example application can be quit using `ctrl+c`.
|
||||
|
||||
See the [wiki](https://gitea.yves-biener.de/yves-biener/zterm/wiki) for a showcase of the examples and the further details.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -13,7 +26,7 @@ To add or update `zterm` as a dependency in your project run the following comma
|
||||
zig fetch --save git+https://gitea.yves-biener.de/yves-biener/zterm
|
||||
```
|
||||
|
||||
Add the dependency to your module as follows in your _build.zig_:
|
||||
Add the dependency to your module as follows in your *build.zig*:
|
||||
|
||||
```zig
|
||||
const zterm: *Dependency = b.dependency("zterm", .{
|
||||
@@ -24,4 +37,10 @@ const zterm: *Dependency = b.dependency("zterm", .{
|
||||
exe.root_module.addImport("zterm", zterm.module("zterm"));
|
||||
```
|
||||
|
||||
For an example you can take a look at [build.zig](build.zig) for an example.
|
||||
### Documentation
|
||||
|
||||
A wiki should be created containing a bright overview of the structure and usage
|
||||
of the library. For details it should refer to the examples. The documentation
|
||||
should be minimal in terms of updateability in case the library changes. Maybe
|
||||
some documentation could be derived from the code documentation (there is a tool
|
||||
for this if I recall correctly).
|
||||
|
||||
198
build.zig
198
build.zig
@@ -1,92 +1,198 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Although this function looks imperative, note that its job is to
|
||||
// declaratively construct a build graph that will be executed by an external
|
||||
// runner.
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const Examples = enum {
|
||||
all,
|
||||
demo,
|
||||
// elements:
|
||||
button,
|
||||
input,
|
||||
scrollable,
|
||||
// layouts:
|
||||
vertical,
|
||||
horizontal,
|
||||
grid,
|
||||
mixed,
|
||||
// styles:
|
||||
text,
|
||||
palette,
|
||||
// error handling
|
||||
errors,
|
||||
};
|
||||
|
||||
const example = b.option(Examples, "example", "Example to build and/or run. (default: all)") orelse .all;
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption(Examples, "example", example);
|
||||
|
||||
// dependencies
|
||||
const zg = b.dependency("zg", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const interface = b.dependency("interface", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// library
|
||||
const lib = b.addModule("zterm", .{
|
||||
.root_source_file = b.path("src/zterm.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
lib.addImport("interface", interface.module("interface"));
|
||||
lib.addImport("code_point", zg.module("code_point"));
|
||||
|
||||
// example executables
|
||||
const stack_example = b.addExecutable(.{
|
||||
.name = "stack",
|
||||
.root_source_file = b.path("examples/stack.zig"),
|
||||
//--- Examples ---
|
||||
|
||||
// demo:
|
||||
const demo = b.addExecutable(.{
|
||||
.name = "demo",
|
||||
.root_source_file = b.path("examples/demo.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
stack_example.root_module.addImport("zterm", lib);
|
||||
demo.root_module.addImport("zterm", lib);
|
||||
|
||||
const container_example = b.addExecutable(.{
|
||||
.name = "container",
|
||||
.root_source_file = b.path("examples/container.zig"),
|
||||
// elements:
|
||||
const button = b.addExecutable(.{
|
||||
.name = "button",
|
||||
.root_source_file = b.path("examples/elements/button.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
container_example.root_module.addImport("zterm", lib);
|
||||
button.root_module.addImport("zterm", lib);
|
||||
|
||||
const padding_example = b.addExecutable(.{
|
||||
.name = "padding",
|
||||
.root_source_file = b.path("examples/padding.zig"),
|
||||
const input = b.addExecutable(.{
|
||||
.name = "input",
|
||||
.root_source_file = b.path("examples/elements/input.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
padding_example.root_module.addImport("zterm", lib);
|
||||
input.root_module.addImport("zterm", lib);
|
||||
|
||||
const exec_example = b.addExecutable(.{
|
||||
.name = "exec",
|
||||
.root_source_file = b.path("examples/exec.zig"),
|
||||
const scrollable = b.addExecutable(.{
|
||||
.name = "scrollable",
|
||||
.root_source_file = b.path("examples/elements/scrollable.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exec_example.root_module.addImport("zterm", lib);
|
||||
scrollable.root_module.addImport("zterm", lib);
|
||||
|
||||
// This declares intent for the executable to be installed into the
|
||||
// standard location when the user invokes the "install" step (the default
|
||||
// step when running `zig build`).
|
||||
b.installArtifact(stack_example);
|
||||
b.installArtifact(container_example);
|
||||
b.installArtifact(padding_example);
|
||||
b.installArtifact(exec_example);
|
||||
// layouts:
|
||||
const vertical = b.addExecutable(.{
|
||||
.name = "vertical",
|
||||
.root_source_file = b.path("examples/layouts/vertical.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
vertical.root_module.addImport("zterm", lib);
|
||||
|
||||
// Creates a step for unit testing. This only builds the test executable
|
||||
// but does not run it.
|
||||
const horizontal = b.addExecutable(.{
|
||||
.name = "horizontal",
|
||||
.root_source_file = b.path("examples/layouts/horizontal.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
horizontal.root_module.addImport("zterm", lib);
|
||||
|
||||
const grid = b.addExecutable(.{
|
||||
.name = "grid",
|
||||
.root_source_file = b.path("examples/layouts/grid.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
grid.root_module.addImport("zterm", lib);
|
||||
|
||||
const mixed = b.addExecutable(.{
|
||||
.name = "mixed",
|
||||
.root_source_file = b.path("examples/layouts/mixed.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
mixed.root_module.addImport("zterm", lib);
|
||||
|
||||
// styles:
|
||||
const palette = b.addExecutable(.{
|
||||
.name = "palette",
|
||||
.root_source_file = b.path("examples/styles/palette.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
palette.root_module.addImport("zterm", lib);
|
||||
|
||||
const text = b.addExecutable(.{
|
||||
.name = "text",
|
||||
.root_source_file = b.path("examples/styles/text.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
text.root_module.addImport("zterm", lib);
|
||||
|
||||
// error handling:
|
||||
const errors = b.addExecutable(.{
|
||||
.name = "errors",
|
||||
.root_source_file = b.path("examples/errors.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
errors.root_module.addImport("zterm", lib);
|
||||
|
||||
// mapping of user selected example to compile step
|
||||
const exe = switch (example) {
|
||||
.demo => demo,
|
||||
// elements:
|
||||
.button => button,
|
||||
.input => input,
|
||||
.scrollable => scrollable,
|
||||
// layouts:
|
||||
.vertical => vertical,
|
||||
.horizontal => horizontal,
|
||||
.grid => grid,
|
||||
.mixed => mixed,
|
||||
// styles:
|
||||
.text => text,
|
||||
.palette => palette,
|
||||
// error handling:
|
||||
.errors => errors,
|
||||
else => blk: {
|
||||
b.installArtifact(button);
|
||||
b.installArtifact(input);
|
||||
b.installArtifact(scrollable);
|
||||
b.installArtifact(vertical);
|
||||
b.installArtifact(horizontal);
|
||||
b.installArtifact(grid);
|
||||
b.installArtifact(mixed);
|
||||
b.installArtifact(text);
|
||||
b.installArtifact(palette);
|
||||
b.installArtifact(errors);
|
||||
break :blk demo;
|
||||
},
|
||||
};
|
||||
b.installArtifact(exe);
|
||||
|
||||
// zig build run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
// Allow additional arguments, like this: `zig build run -- arg1 arg2 etc`
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
|
||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||
// and can be selected like this: `zig build run`
|
||||
// This will evaluate the `run` step rather than the default, which is "install".
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// zig build test
|
||||
const lib_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/zterm.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
lib_unit_tests.root_module.addImport("zg", zg.module("code_point"));
|
||||
lib_unit_tests.root_module.addImport("code_point", zg.module("code_point"));
|
||||
lib_unit_tests.root_module.addImport("DisplayWidth", zg.module("DisplayWidth"));
|
||||
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
|
||||
// Similar to creating the run step earlier, this exposes a `test` step to
|
||||
// the `zig build --help` menu, providing a way for the user to request
|
||||
// running the unit tests.
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_lib_unit_tests.step);
|
||||
}
|
||||
|
||||
@@ -6,16 +6,29 @@
|
||||
//
|
||||
// It is redundant to include "zig" in this name because it is already
|
||||
// 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/).
|
||||
// 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.
|
||||
// This is currently advisory only; Zig does not yet do anything
|
||||
// with this value.
|
||||
//.minimum_zig_version = "0.11.0",
|
||||
// Tracks the earliest Zig version that the package considers to be a
|
||||
// supported use case.
|
||||
.minimum_zig_version = "0.15.0-dev.56+d0911786c",
|
||||
|
||||
// This field is optional.
|
||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||
@@ -24,20 +37,14 @@
|
||||
// internet connectivity.
|
||||
.dependencies = .{
|
||||
.zg = .{
|
||||
.url = "https://codeberg.org/dude_the_builder/zg/archive/v0.13.2.tar.gz",
|
||||
.hash = "122055beff332830a391e9895c044d33b15ea21063779557024b46169fb1984c6e40",
|
||||
},
|
||||
.interface = .{
|
||||
.url = "git+https://github.com/nilslice/zig-interface#c6ca205de75969fdcf04542f48d813d529196594",
|
||||
.hash = "1220401627a97a7b429acd084bd3447fe5838122d71bbca57061906a2c3baf1c2e98",
|
||||
.url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc",
|
||||
.hash = "1220f3e29bc40856bfc06e0ee133f814b0011c76de987d8a6a458c2f34d82708899a",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"LICENSE",
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
// For example...
|
||||
//"LICENSE",
|
||||
//"README.md",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Direct,
|
||||
true,
|
||||
);
|
||||
const Key = zterm.Key;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
|
||||
const log = std.log.scoped(.container);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// fail test; can't try in defer as defer is executed after we return
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer: App.Renderer = .{};
|
||||
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
|
||||
// -> size hint how much should it use?
|
||||
|
||||
var layout = Layout.createFrom(layout: {
|
||||
var stack = Layout.HContainer.init(allocator, .{
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
15,
|
||||
},
|
||||
.{
|
||||
Layout.createFrom(container: {
|
||||
var container = Layout.VContainer.init(allocator, .{
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
25,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./src/app.zig", .{});
|
||||
defer file.close();
|
||||
var widget = Widget.RawText.init(allocator, file);
|
||||
break :blk &widget;
|
||||
}),
|
||||
50,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
25,
|
||||
},
|
||||
});
|
||||
break :container &container;
|
||||
}),
|
||||
70,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
15,
|
||||
},
|
||||
});
|
||||
break :layout &stack;
|
||||
});
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch unreachable;
|
||||
|
||||
// App.Event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
.resize => |size| {
|
||||
renderer.resize(size);
|
||||
},
|
||||
.key => |key| {
|
||||
// ctrl+c to quit
|
||||
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
|
||||
},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
158
examples/demo.zig
Normal file
158
examples/demo.zig
Normal file
@@ -0,0 +1,158 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const input = zterm.input;
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
const QuitText = struct {
|
||||
const text = "Press ctrl+c to quit. Press ctrl+n to launch helix.";
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||
}
|
||||
|
||||
pub 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 y = 2;
|
||||
const x = size.x / 2 -| (text.len / 2);
|
||||
const anchor = (y * size.x) + x;
|
||||
|
||||
for (text, 0..) |cp, idx| {
|
||||
cells[anchor + idx].style.fg = .white;
|
||||
cells[anchor + idx].style.bg = .black;
|
||||
cells[anchor + idx].cp = cp;
|
||||
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
|
||||
// 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?
|
||||
// - on the left some buttons?
|
||||
var box = try App.Container.init(allocator, .{
|
||||
.border = .{
|
||||
.color = .blue,
|
||||
.sides = .all,
|
||||
},
|
||||
.layout = .{
|
||||
.gap = 1,
|
||||
.padding = .vertical(2),
|
||||
.direction = .vertical,
|
||||
},
|
||||
.size = .{
|
||||
.dim = .{ .y = 90 },
|
||||
},
|
||||
}, .{});
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, .{}));
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, .{}));
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, .{}));
|
||||
defer box.deinit();
|
||||
|
||||
var scrollable: App.Scrollable = .{ .container = box };
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.separator = .{ .enabled = true },
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
.direction = .horizontal,
|
||||
},
|
||||
}, quit_text.element());
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.border = .{
|
||||
.color = .light_blue,
|
||||
.sides = .all,
|
||||
},
|
||||
.size = .{
|
||||
.dim = .{ .x = 100 },
|
||||
},
|
||||
}, .{}));
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
.size = .{
|
||||
.dim = .{ .x = 30 },
|
||||
},
|
||||
}, .{}));
|
||||
defer container.deinit(); // also de-initializes the children
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
// event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| {
|
||||
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
|
||||
|
||||
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
||||
try app.interrupt();
|
||||
defer app.start() catch @panic("could not start app event loop");
|
||||
var child = std.process.Child.init(&.{"hx"}, allocator);
|
||||
_ = child.spawnAndWait() catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Spawning $EDITOR failed",
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
},
|
||||
// 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 }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.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.flush();
|
||||
}
|
||||
}
|
||||
148
examples/elements/button.zig
Normal file
148
examples/elements/button.zig
Normal file
@@ -0,0 +1,148 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {
|
||||
click: [:0]const u8,
|
||||
});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
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";
|
||||
|
||||
queue: *App.Queue,
|
||||
color: zterm.Color = .black,
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.handle = handle,
|
||||
.content = content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
.mouse => |mouse| 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 => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = size.y / 2 -| (text.len / 2);
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.x) + col;
|
||||
|
||||
for (text, 0..) |cp, idx| {
|
||||
cells[anchor + idx].style.fg = this.color;
|
||||
cells[anchor + idx].style.emphasis = &.{.bold};
|
||||
cells[anchor + idx].cp = cp;
|
||||
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var clickable: Clickable = .{ .queue = &app.queue };
|
||||
const element = clickable.element();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .grey },
|
||||
.layout = .{ .padding = .all(5) },
|
||||
}, quit_text.element());
|
||||
defer container.deinit();
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
// event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
.click => |button| {
|
||||
log.info("Clicked with mouse using Button: {s}", .{button});
|
||||
},
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
162
examples/elements/input.zig
Normal file
162
examples/elements/input.zig
Normal file
@@ -0,0 +1,162 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {
|
||||
accept: []u21,
|
||||
});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
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),
|
||||
queue: *App.Queue,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
|
||||
return .{
|
||||
.input = .init(allocator),
|
||||
.queue = queue,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: @This()) void {
|
||||
this.input.deinit();
|
||||
}
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.handle = handle,
|
||||
.content = content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
.key => |key| {
|
||||
if (key.isAscii()) try this.input.append(key.cp);
|
||||
|
||||
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
|
||||
this.queue.push(.{ .accept = try this.input.toOwnedSlice() });
|
||||
|
||||
if (key.eql(.{ .cp = zterm.input.Backspace }) or key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete }))
|
||||
_ = this.input.pop();
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
if (this.input.items.len == 0) return;
|
||||
|
||||
const row = 1;
|
||||
const col = 1;
|
||||
const anchor = (row * size.x) + col;
|
||||
|
||||
for (this.input.items, 0..) |cp, idx| {
|
||||
cells[anchor + idx].style.fg = .black;
|
||||
cells[anchor + idx].cp = cp;
|
||||
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var input_field: InputField = .init(allocator, &app.queue);
|
||||
defer input_field.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
|
||||
const element = input_field.element();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .grey },
|
||||
.layout = .{ .padding = .all(5) },
|
||||
}, quit_text.element());
|
||||
defer container.deinit();
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
// event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
.accept => |input| {
|
||||
defer allocator.free(input);
|
||||
log.info("Accepted input {any}", .{input});
|
||||
},
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
199
examples/elements/scrollable.zig
Normal file
199
examples/elements/scrollable.zig
Normal file
@@ -0,0 +1,199 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const input = zterm.input;
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
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";
|
||||
|
||||
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 = size.y / 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.x) + col;
|
||||
|
||||
for (text, 0..) |cp, idx| {
|
||||
cells[anchor + idx].style.fg = .white;
|
||||
cells[anchor + idx].style.bg = .black;
|
||||
cells[anchor + idx].cp = cp;
|
||||
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var element_wrapper: HelloWorldText = .{};
|
||||
const element = element_wrapper.element();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
|
||||
var top_box = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
},
|
||||
.direction = .vertical,
|
||||
.padding = .vertical(1),
|
||||
},
|
||||
}, .{});
|
||||
try top_box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
.size = .{
|
||||
.dim = .{ .y = 30 },
|
||||
},
|
||||
}, .{}));
|
||||
try top_box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
.size = .{
|
||||
.dim = .{ .y = 5 },
|
||||
},
|
||||
}, element));
|
||||
try top_box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
.size = .{
|
||||
.dim = .{ .y = 2 },
|
||||
},
|
||||
}, .{}));
|
||||
defer top_box.deinit();
|
||||
|
||||
var bottom_box = try App.Container.init(allocator, .{
|
||||
.border = .{
|
||||
.sides = .all,
|
||||
.color = .blue,
|
||||
},
|
||||
.layout = .{
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.color = .red,
|
||||
},
|
||||
.direction = .vertical,
|
||||
.padding = .vertical(1),
|
||||
},
|
||||
.size = .{
|
||||
.dim = .{ .y = 30 },
|
||||
},
|
||||
}, .{});
|
||||
try bottom_box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .grey },
|
||||
}, .{}));
|
||||
try bottom_box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .grey },
|
||||
}, element));
|
||||
try bottom_box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .grey },
|
||||
}, .{}));
|
||||
defer bottom_box.deinit();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.line = .double,
|
||||
},
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
.direction = .vertical,
|
||||
},
|
||||
}, quit_text.element());
|
||||
defer container.deinit();
|
||||
|
||||
// place empty container containing the element of the scrollable Container.
|
||||
var scrollable_top: App.Scrollable = .{ .container = top_box };
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
|
||||
|
||||
var scrollable_bottom: App.Scrollable = .{ .container = bottom_box };
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
// event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
159
examples/errors.zig
Normal file
159
examples/errors.zig
Normal file
@@ -0,0 +1,159 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
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 InfoText = struct {
|
||||
const text = "Press any key; Non-Ascii inputs (i.e. `Enter` or `Backspace`) to trigger an exception";
|
||||
|
||||
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 ErrorNotification = struct {
|
||||
msg: ?[]const u8 = null,
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } };
|
||||
}
|
||||
|
||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
.key => |key| if (!key.isAscii()) return error.UnsupportedKey,
|
||||
.err => |err| this.msg = err.msg,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
if (this.msg) |msg| {
|
||||
const row = size.y -| 2;
|
||||
const col = size.x -| 2 -| msg.len;
|
||||
const anchor = (row * size.x) + col;
|
||||
|
||||
for (msg, 0..) |cp, idx| {
|
||||
cells[anchor + idx].style.fg = .white;
|
||||
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;
|
||||
}
|
||||
|
||||
this.msg = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
var info_text: InfoText = .{};
|
||||
var error_notification: ErrorNotification = .{};
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
},
|
||||
}, quit_text.element());
|
||||
defer container.deinit();
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{}, info_text.element()));
|
||||
try container.append(try App.Container.init(allocator, .{}, error_notification.element()));
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Direct,
|
||||
true,
|
||||
);
|
||||
const Key = zterm.Key;
|
||||
const Cell = zterm.Cell;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
|
||||
const log = std.log.scoped(.exec);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// fail test; can't try in defer as defer is executed after we return
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer: App.Renderer = .{};
|
||||
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
|
||||
// -> size hint how much should it use?
|
||||
|
||||
var layout = Layout.createFrom(layout: {
|
||||
var container = Layout.VContainer.init(allocator, .{
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
45,
|
||||
},
|
||||
.{
|
||||
Layout.createFrom(framing: {
|
||||
var framing = Layout.Framing.init(allocator, .{}, .{
|
||||
.widget = Widget.createFrom(blk: {
|
||||
var widget = Widget.Text.init(&[_]Cell{
|
||||
.{ .content = "Press " },
|
||||
.{ .content = "Ctrl+n", .style = .{ .fg = .{ .index = 6 } } },
|
||||
.{ .content = " to launch $EDITOR" },
|
||||
});
|
||||
break :blk &widget;
|
||||
}),
|
||||
});
|
||||
break :framing &framing;
|
||||
}),
|
||||
10,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
45,
|
||||
},
|
||||
});
|
||||
break :layout &container;
|
||||
});
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch unreachable;
|
||||
|
||||
// App.Event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
.resize => |size| {
|
||||
renderer.resize(size);
|
||||
},
|
||||
.key => |key| {
|
||||
// ctrl+c to quit
|
||||
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
app.quit();
|
||||
}
|
||||
if (Key.matches(key, .{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
||||
try app.interrupt();
|
||||
defer app.start() catch @panic("could not start app event loop");
|
||||
// TODO: parse environment variables to extract the value of $EDITOR and use it here instead
|
||||
var child = std.process.Child.init(&.{"hx"}, allocator);
|
||||
_ = child.spawnAndWait() catch |err| {
|
||||
app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Spawning $EDITOR failed",
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
|
||||
},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
115
examples/layouts/grid.zig
Normal file
115
examples/layouts/grid.zig
Normal file
@@ -0,0 +1,115 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
const element = quit_text.element();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.separator = .{ .enabled = true },
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
.direction = .horizontal,
|
||||
},
|
||||
}, element);
|
||||
for (0..3) |_| {
|
||||
var column = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.separator = .{ .enabled = true },
|
||||
.direction = .vertical,
|
||||
},
|
||||
}, .{});
|
||||
try column.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
try column.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
try column.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
try container.append(column);
|
||||
}
|
||||
defer container.deinit(); // also de-initializes the children
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
// event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
// 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 }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.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.flush();
|
||||
}
|
||||
}
|
||||
107
examples/layouts/horizontal.zig
Normal file
107
examples/layouts/horizontal.zig
Normal file
@@ -0,0 +1,107 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
const element = quit_text.element();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.border = .{},
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
.direction = .horizontal,
|
||||
},
|
||||
}, element);
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
defer container.deinit(); // also de-initializes the children
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
// event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
// 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 }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.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.flush();
|
||||
}
|
||||
}
|
||||
123
examples/layouts/mixed.zig
Normal file
123
examples/layouts/mixed.zig
Normal file
@@ -0,0 +1,123 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
const element = quit_text.element();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
},
|
||||
}, element);
|
||||
for (0..3) |i| {
|
||||
var column = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.separator = .{ .enabled = true },
|
||||
.direction = if (i > 0) .vertical else .horizontal,
|
||||
},
|
||||
}, .{});
|
||||
if (i != 1) {
|
||||
try column.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .green },
|
||||
}, .{}));
|
||||
try column.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .yellow },
|
||||
}, .{}));
|
||||
} else {
|
||||
try column.append(try App.Container.init(allocator, .{
|
||||
.size = .{
|
||||
.dim = .{ .y = 4 },
|
||||
.grow = .horizontal,
|
||||
},
|
||||
}, .{}));
|
||||
}
|
||||
try column.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
try container.append(column);
|
||||
}
|
||||
defer container.deinit(); // also de-initializes the children
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
// event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
// 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 }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.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.flush();
|
||||
}
|
||||
}
|
||||
106
examples/layouts/vertical.zig
Normal file
106
examples/layouts/vertical.zig
Normal file
@@ -0,0 +1,106 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
const element = quit_text.element();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
.direction = .vertical,
|
||||
},
|
||||
}, element);
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
defer container.deinit(); // also de-initializes the children
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
// event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
// 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 }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.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.flush();
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Direct,
|
||||
true,
|
||||
);
|
||||
const Key = zterm.Key;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
|
||||
const log = std.log.scoped(.padding);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// fail test; can't try in defer as defer is executed after we return
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer: App.Renderer = .{};
|
||||
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
|
||||
// -> size hint how much should it use?
|
||||
|
||||
var layout = Layout.createFrom(layout: {
|
||||
var padding = Layout.Padding.init(allocator, .{
|
||||
.padding = 15,
|
||||
}, .{
|
||||
.layout = Layout.createFrom(framing: {
|
||||
var framing = Layout.Framing.init(
|
||||
allocator,
|
||||
.{
|
||||
.style = .{
|
||||
.fg = .{
|
||||
.index = 6,
|
||||
},
|
||||
},
|
||||
.frame = .round,
|
||||
.title = .{
|
||||
.str = "Content in Margin",
|
||||
.style = .{
|
||||
.ul_style = .single,
|
||||
.ul = .{ .index = 6 },
|
||||
.bold = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
.layout = Layout.createFrom(margin: {
|
||||
var margin = Layout.Margin.init(
|
||||
allocator,
|
||||
.{
|
||||
.margin = 10,
|
||||
},
|
||||
.{
|
||||
.widget = Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./examples/padding.zig", .{});
|
||||
defer file.close();
|
||||
var widget = Widget.RawText.init(allocator, file);
|
||||
break :blk &widget;
|
||||
}),
|
||||
},
|
||||
);
|
||||
break :margin &margin;
|
||||
}),
|
||||
},
|
||||
);
|
||||
break :framing &framing;
|
||||
}),
|
||||
});
|
||||
break :layout &padding;
|
||||
});
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch unreachable;
|
||||
|
||||
// App.Event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
.resize => |size| {
|
||||
renderer.resize(size);
|
||||
},
|
||||
.key => |key| {
|
||||
// ctrl+c to quit
|
||||
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
|
||||
},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Direct,
|
||||
true,
|
||||
);
|
||||
const Key = zterm.Key;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
|
||||
const log = std.log.scoped(.stack);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// fail test; can't try in defer as defer is executed after we return
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer: App.Renderer = .{};
|
||||
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
|
||||
// -> size hint how much should it use?
|
||||
|
||||
var layout = Layout.createFrom(layout: {
|
||||
var framing = Layout.Framing.init(allocator, .{
|
||||
.style = .{
|
||||
.fg = .{
|
||||
.index = 6,
|
||||
},
|
||||
},
|
||||
.frame = .round,
|
||||
.title = .{
|
||||
.str = "HStack",
|
||||
.style = .{
|
||||
.ul_style = .single,
|
||||
.ul = .{ .index = 6 },
|
||||
.bold = true,
|
||||
},
|
||||
},
|
||||
}, .{
|
||||
.layout = Layout.createFrom(hstack: {
|
||||
var hstack = Layout.HStack.init(allocator, .{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
Layout.createFrom(framing: {
|
||||
var framing = Layout.Framing.init(
|
||||
allocator,
|
||||
.{
|
||||
.style = .{
|
||||
.fg = .{
|
||||
.index = 6,
|
||||
},
|
||||
},
|
||||
.frame = .round,
|
||||
.title = .{
|
||||
.str = "VStack",
|
||||
.style = .{
|
||||
.ul_style = .single,
|
||||
.ul = .{ .index = 6 },
|
||||
.bold = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
.layout = Layout.createFrom(
|
||||
padding: {
|
||||
var padding = Layout.Margin.init(
|
||||
allocator,
|
||||
.{
|
||||
.margin = 10,
|
||||
},
|
||||
.{
|
||||
.layout = Layout.createFrom(vstack: {
|
||||
var vstack = Layout.VStack.init(allocator, .{
|
||||
Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./examples/stack.zig", .{});
|
||||
defer file.close();
|
||||
var widget = Widget.RawText.init(allocator, file);
|
||||
break :blk &widget;
|
||||
}),
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./examples/stack.zig", .{});
|
||||
defer file.close();
|
||||
var widget = Widget.RawText.init(allocator, file);
|
||||
break :blk &widget;
|
||||
}),
|
||||
});
|
||||
break :vstack &vstack;
|
||||
}),
|
||||
},
|
||||
);
|
||||
break :padding &padding;
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
break :framing &framing;
|
||||
}),
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
});
|
||||
break :hstack &hstack;
|
||||
}),
|
||||
});
|
||||
break :layout &framing;
|
||||
});
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch unreachable;
|
||||
|
||||
// App.Event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
.resize => |size| {
|
||||
renderer.resize(size);
|
||||
},
|
||||
.key => |key| {
|
||||
// ctrl+c to quit
|
||||
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
|
||||
},
|
||||
}
|
||||
// NOTE: this currently re-renders the screen for every key-press -> which might be a bit of an overkill
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
102
examples/styles/palette.zig
Normal file
102
examples/styles/palette.zig
Normal file
@@ -0,0 +1,102 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
const element = quit_text.element();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
},
|
||||
}, element);
|
||||
defer container.deinit();
|
||||
|
||||
var box = try App.Container.init(allocator, .{
|
||||
.layout = .{ .direction = .horizontal },
|
||||
}, .{});
|
||||
defer box.deinit();
|
||||
|
||||
inline for (std.meta.fields(zterm.Color)) |field| {
|
||||
if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
|
||||
}
|
||||
var scrollable: App.Scrollable = .{ .container = box };
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
157
examples/styles/text.zig
Normal file
157
examples/styles/text.zig
Normal file
@@ -0,0 +1,157 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
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 TextStyles = struct {
|
||||
const text = "Example";
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
@setEvalBranchQuota(50000);
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
var row: usize = 0;
|
||||
var col: usize = 0;
|
||||
|
||||
// Color
|
||||
inline for (std.meta.fields(zterm.Color)) |bg_field| {
|
||||
if (comptime bg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||
|
||||
inline for (std.meta.fields(zterm.Color)) |fg_field| {
|
||||
if (comptime fg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||
if (comptime fg_field.value == bg_field.value) continue;
|
||||
|
||||
// witouth any emphasis
|
||||
for (text) |cp| {
|
||||
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
|
||||
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
|
||||
cells[(row * size.x) + col].cp = cp;
|
||||
col += 1;
|
||||
}
|
||||
|
||||
// emphasis (no combinations)
|
||||
inline for (std.meta.fields(zterm.Style.Emphasis)) |emp_field| {
|
||||
if (comptime emp_field.value == 0) continue; // zterm.Style.Emphasis.reset == 0 -> skip
|
||||
const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value);
|
||||
|
||||
for (text) |cp| {
|
||||
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
|
||||
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
|
||||
cells[(row * size.x) + col].style.emphasis = &.{emphasis};
|
||||
cells[(row * size.x) + col].cp = cp;
|
||||
col += 1;
|
||||
}
|
||||
}
|
||||
row += 1;
|
||||
col = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
const element = quit_text.element();
|
||||
|
||||
var text_styles: TextStyles = .{};
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
},
|
||||
}, element);
|
||||
defer container.deinit();
|
||||
|
||||
var box = try App.Container.init(allocator, .{
|
||||
.layout = .{ .direction = .vertical },
|
||||
.size = .{
|
||||
.dim = .{
|
||||
.x = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
|
||||
.y = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
|
||||
},
|
||||
},
|
||||
}, text_styles.element());
|
||||
defer box.deinit();
|
||||
|
||||
var scrollable: App.Scrollable = .{ .container = box };
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
292
src/app.zig
292
src/app.zig
@@ -1,13 +1,17 @@
|
||||
//! Application type for TUI-applications
|
||||
const std = @import("std");
|
||||
const terminal = @import("terminal.zig");
|
||||
const code_point = @import("code_point");
|
||||
const event = @import("event.zig");
|
||||
const input = @import("input.zig");
|
||||
const terminal = @import("terminal.zig");
|
||||
const queue = @import("queue.zig");
|
||||
|
||||
const mergeTaggedUnions = event.mergeTaggedUnions;
|
||||
const isTaggedUnion = event.isTaggedUnion;
|
||||
|
||||
const Key = terminal.Key;
|
||||
const Queue = @import("queue.zig").Queue;
|
||||
const Mouse = input.Mouse;
|
||||
const Key = input.Key;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
@@ -15,14 +19,6 @@ const log = std.log.scoped(.app);
|
||||
/// an tagged union for all the user events that can be send through the
|
||||
/// applications event loop.
|
||||
///
|
||||
/// _R_ is the type function for the `Renderer` to use. The parameter boolean
|
||||
/// will be set to the _fullscreen_ value at compile time. The corresponding
|
||||
/// `Renderer` type is accessible through the generated type of this function.
|
||||
///
|
||||
/// _fullscreen_ will be used to configure the `App` and the `Renderer` to
|
||||
/// respect the corresponding configuration whether to render a fullscreen tui
|
||||
/// or an inline tui.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Create an `App` which renders using the `PlainRenderer` in fullscreen with
|
||||
@@ -32,26 +28,27 @@ const log = std.log.scoped(.app);
|
||||
/// const zterm = @import("zterm");
|
||||
/// const App = zterm.App(
|
||||
/// union(enum) {},
|
||||
/// zterm.Renderer.Direct,
|
||||
/// true,
|
||||
/// );
|
||||
/// // later on use
|
||||
/// var app: App = .{};
|
||||
/// var renderer: App.Renderer = .{};
|
||||
/// // later on create an `App` instance and start the event loop
|
||||
/// var app: App = .init;
|
||||
/// try app.start();
|
||||
/// defer app.stop() catch unreachable;
|
||||
/// ```
|
||||
pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fullscreen: bool) type {
|
||||
pub fn App(comptime E: type) type {
|
||||
if (!isTaggedUnion(E)) {
|
||||
@compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
return struct {
|
||||
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
|
||||
pub const Renderer = R(fullscreen);
|
||||
pub const Layout = @import("layout.zig").Layout(Event, Renderer);
|
||||
pub const Widget = @import("widget.zig").Widget(Event, Renderer);
|
||||
pub const Container = @import("container.zig").Container(Event);
|
||||
const element = @import("element.zig");
|
||||
pub const Element = element.Element(Event);
|
||||
pub const Scrollable = element.Scrollable(Event);
|
||||
pub const Queue = queue.Queue(Event, 256);
|
||||
|
||||
queue: Queue(Event, 256) = .{},
|
||||
thread: ?std.Thread = null,
|
||||
quit_event: std.Thread.ResetEvent = .{},
|
||||
queue: Queue,
|
||||
thread: ?std.Thread,
|
||||
quit_event: std.Thread.ResetEvent,
|
||||
termios: ?std.posix.termios = null,
|
||||
attached_handler: bool = false,
|
||||
|
||||
@@ -60,6 +57,14 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
callback: *const fn (context: *anyopaque) void,
|
||||
};
|
||||
|
||||
pub const init: @This() = .{
|
||||
.queue = .{},
|
||||
.thread = null,
|
||||
.quit_event = .{},
|
||||
.termios = null,
|
||||
.attached_handler = false,
|
||||
};
|
||||
|
||||
pub fn start(this: *@This()) !void {
|
||||
if (this.thread) |_| return;
|
||||
|
||||
@@ -69,13 +74,16 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
.mask = std.posix.empty_sigset,
|
||||
.flags = 0,
|
||||
};
|
||||
std.posix.sigaction(std.posix.SIG.WINCH, &winch_act, null) catch @panic("could not attach signal WINCH");
|
||||
std.posix.sigaction(std.posix.SIG.WINCH, &winch_act, null);
|
||||
|
||||
try registerWinch(.{
|
||||
.context = this,
|
||||
.callback = @This().winsizeCallback,
|
||||
});
|
||||
this.attached_handler = true;
|
||||
|
||||
// post init event (as the very first element to be in the queue - event loop)
|
||||
this.postEvent(.init);
|
||||
}
|
||||
|
||||
this.quit_event.reset();
|
||||
@@ -83,22 +91,19 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
|
||||
var termios: std.posix.termios = undefined;
|
||||
try terminal.enableRawMode(&termios);
|
||||
if (this.termios) |_| {} else {
|
||||
this.termios = termios;
|
||||
}
|
||||
if (fullscreen) {
|
||||
try terminal.saveScreen();
|
||||
try terminal.enterAltScreen();
|
||||
try terminal.hideCursor();
|
||||
}
|
||||
if (this.termios) |_| {} else this.termios = termios;
|
||||
|
||||
try terminal.saveScreen();
|
||||
try terminal.enterAltScreen();
|
||||
try terminal.hideCursor();
|
||||
try terminal.enableMouseSupport();
|
||||
}
|
||||
|
||||
pub fn interrupt(this: *@This()) !void {
|
||||
this.quit_event.set();
|
||||
if (fullscreen) {
|
||||
try terminal.existAltScreen();
|
||||
try terminal.restoreScreen();
|
||||
}
|
||||
try terminal.disableMouseSupport();
|
||||
try terminal.exitAltScreen();
|
||||
try terminal.restoreScreen();
|
||||
if (this.thread) |thread| {
|
||||
thread.join();
|
||||
this.thread = null;
|
||||
@@ -108,12 +113,11 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
pub fn stop(this: *@This()) !void {
|
||||
try this.interrupt();
|
||||
if (this.termios) |*termios| {
|
||||
try terminal.disableMouseSupport();
|
||||
try terminal.showCursor();
|
||||
try terminal.exitAltScreen();
|
||||
try terminal.disableRawMode(termios);
|
||||
if (fullscreen) {
|
||||
try terminal.showCursor();
|
||||
try terminal.existAltScreen();
|
||||
try terminal.restoreScreen();
|
||||
}
|
||||
try terminal.restoreScreen();
|
||||
}
|
||||
this.termios = null;
|
||||
}
|
||||
@@ -137,7 +141,8 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
|
||||
fn winsizeCallback(ptr: *anyopaque) void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ptr));
|
||||
this.postEvent(.{ .resize = terminal.getTerminalSize() });
|
||||
_ = this;
|
||||
// this.postEvent(.{ .size = terminal.getTerminalSize() });
|
||||
}
|
||||
|
||||
var winch_handler: ?SignalHandler = null;
|
||||
@@ -156,41 +161,212 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
}
|
||||
|
||||
fn run(this: *@This()) !void {
|
||||
// send initial terminal size
|
||||
// changes are handled by the winch signal handler
|
||||
// see `App.start` and `App.registerWinch` for details
|
||||
this.postEvent(.{ .resize = terminal.getTerminalSize() });
|
||||
|
||||
// thread to read user inputs
|
||||
var buf: [256]u8 = undefined;
|
||||
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 {
|
||||
// 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..]);
|
||||
// TODO `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
|
||||
// escape key presses
|
||||
if (buf[0] == 0x1b and read_bytes > 1) {
|
||||
switch (buf[1]) {
|
||||
// TODO: parse corresponding codes
|
||||
0x4F => { // ss3
|
||||
if (read_bytes < 3) continue;
|
||||
|
||||
const key: Key = switch (buf[2]) {
|
||||
'A' => .{ .cp = input.Up },
|
||||
'B' => .{ .cp = input.Down },
|
||||
'C' => .{ .cp = input.Right },
|
||||
'D' => .{ .cp = input.Left },
|
||||
'E' => .{ .cp = input.KpBegin },
|
||||
'F' => .{ .cp = input.End },
|
||||
'H' => .{ .cp = input.Home },
|
||||
'P' => .{ .cp = input.F1 },
|
||||
'Q' => .{ .cp = input.F2 },
|
||||
'R' => .{ .cp = input.F3 },
|
||||
'S' => .{ .cp = input.F4 },
|
||||
else => continue,
|
||||
};
|
||||
this.postEvent(.{ .key = key });
|
||||
},
|
||||
0x5B => { // csi
|
||||
if (read_bytes < 3) continue;
|
||||
|
||||
// We start iterating at index 2 to get past the '['
|
||||
const sequence = for (buf[2..], 2..) |b, i| {
|
||||
switch (b) {
|
||||
0x40...0xFF => break buf[0 .. i + 1],
|
||||
else => continue,
|
||||
}
|
||||
} else continue;
|
||||
|
||||
const final = sequence[sequence.len - 1];
|
||||
switch (final) {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'R', 'S' => {
|
||||
// Legacy keys
|
||||
// CSI {ABCDEFHPQS}
|
||||
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
|
||||
const key: Key = .{
|
||||
.cp = switch (final) {
|
||||
'A' => input.Up,
|
||||
'B' => input.Down,
|
||||
'C' => input.Right,
|
||||
'D' => input.Left,
|
||||
'E' => input.KpBegin,
|
||||
'F' => input.End,
|
||||
'H' => input.Home,
|
||||
'P' => input.F1,
|
||||
'Q' => input.F2,
|
||||
'R' => input.F3,
|
||||
'S' => input.F4,
|
||||
else => unreachable, // switch case prevents in this case form ever happening
|
||||
},
|
||||
};
|
||||
this.postEvent(.{ .key = key });
|
||||
},
|
||||
'~' => {
|
||||
// Legacy keys
|
||||
// CSI number ~
|
||||
// CSI number ; modifier ~
|
||||
// CSI number ; modifier:event_type ; text_as_codepoint ~
|
||||
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||
const number_buf = field_iter.next() orelse unreachable; // always will have one field
|
||||
const number = std.fmt.parseUnsigned(u16, number_buf, 10) catch break;
|
||||
|
||||
const key: Key = .{
|
||||
.cp = switch (number) {
|
||||
2 => input.Insert,
|
||||
3 => input.Delete,
|
||||
5 => input.PageUp,
|
||||
6 => input.PageDown,
|
||||
7 => input.Home,
|
||||
8 => input.End,
|
||||
11 => input.F1,
|
||||
12 => input.F2,
|
||||
13 => input.F3,
|
||||
14 => input.F4,
|
||||
15 => input.F5,
|
||||
17 => input.F6,
|
||||
18 => input.F7,
|
||||
19 => input.F8,
|
||||
20 => input.F9,
|
||||
21 => input.F10,
|
||||
23 => input.F11,
|
||||
24 => input.F12,
|
||||
// 200 => return .{ .event = .paste_start, .n = sequence.len },
|
||||
// 201 => return .{ .event = .paste_end, .n = sequence.len },
|
||||
57427 => input.KpBegin,
|
||||
else => unreachable,
|
||||
},
|
||||
};
|
||||
this.postEvent(.{ .key = key });
|
||||
},
|
||||
// TODO focus usage? should this even be in the default event system?
|
||||
'I' => this.postEvent(.{ .focus = true }),
|
||||
'O' => this.postEvent(.{ .focus = false }),
|
||||
'M', 'm' => {
|
||||
std.debug.assert(sequence.len >= 4);
|
||||
if (sequence[2] != '<') break;
|
||||
|
||||
const delim1 = std.mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
|
||||
const button_mask = std.fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
|
||||
const delim2 = std.mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
|
||||
const px = std.fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
|
||||
const py = std.fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
|
||||
|
||||
const mouse_bits = packed struct {
|
||||
const motion: u8 = 0b00100000;
|
||||
const buttons: u8 = 0b11000011;
|
||||
const shift: u8 = 0b00000100;
|
||||
const alt: u8 = 0b00001000;
|
||||
const ctrl: u8 = 0b00010000;
|
||||
};
|
||||
|
||||
const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
|
||||
const motion = button_mask & mouse_bits.motion > 0;
|
||||
// const shift = button_mask & mouse_bits.shift > 0;
|
||||
// const alt = button_mask & mouse_bits.alt > 0;
|
||||
// const ctrl = button_mask & mouse_bits.ctrl > 0;
|
||||
|
||||
const mouse: Mouse = .{
|
||||
.button = button,
|
||||
.x = px -| 1,
|
||||
.y = py -| 1,
|
||||
.kind = blk: {
|
||||
if (motion and button != Mouse.Button.none) {
|
||||
break :blk .drag;
|
||||
}
|
||||
if (motion and button == Mouse.Button.none) {
|
||||
break :blk .motion;
|
||||
}
|
||||
if (sequence[sequence.len - 1] == 'm') break :blk .release;
|
||||
break :blk .press;
|
||||
},
|
||||
};
|
||||
this.postEvent(.{ .mouse = mouse });
|
||||
},
|
||||
'c' => {
|
||||
// Primary DA (CSI ? Pm c)
|
||||
},
|
||||
'n' => {
|
||||
// Device Status Report
|
||||
// CSI Ps n
|
||||
// CSI ? Ps n
|
||||
std.debug.assert(sequence.len >= 3);
|
||||
},
|
||||
't' => {
|
||||
// XTWINOPS
|
||||
// Split first into fields delimited by ';'
|
||||
var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||
const ps = iter.first();
|
||||
if (std.mem.eql(u8, "48", ps)) {
|
||||
// in band window resize
|
||||
// CSI 48 ; height ; width ; height_pix ; width_pix t
|
||||
const width_char = iter.next() orelse break;
|
||||
const height_char = iter.next() orelse break;
|
||||
|
||||
_ = width_char;
|
||||
_ = height_char;
|
||||
// this.postEvent(.{ .size = .{
|
||||
// .x = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
|
||||
// .y = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
|
||||
// } });
|
||||
}
|
||||
},
|
||||
'u' => {
|
||||
// Kitty keyboard
|
||||
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
|
||||
// Not all fields will be present. Only unicode-key-code is
|
||||
// mandatory
|
||||
},
|
||||
'y' => {
|
||||
// DECRPM (CSI ? Ps ; Pm $ y)
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
// TODO parse corresponding codes
|
||||
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
|
||||
else => {},
|
||||
}
|
||||
} else {
|
||||
const b = buf[0];
|
||||
const key: Key = switch (b) {
|
||||
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
|
||||
0x08 => .{ .cp = Key.backspace },
|
||||
0x09 => .{ .cp = Key.tab },
|
||||
0x0a, 0x0d => .{ .cp = Key.enter },
|
||||
0x08 => .{ .cp = input.Backspace },
|
||||
0x09 => .{ .cp = input.Tab },
|
||||
0x0a, 0x0d => .{ .cp = input.Enter },
|
||||
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
|
||||
0x1b => escape: {
|
||||
std.debug.assert(read_bytes == 1);
|
||||
break :escape .{ .cp = Key.escape };
|
||||
break :escape .{ .cp = input.Escape };
|
||||
},
|
||||
0x7f => .{ .cp = Key.backspace },
|
||||
0x7f => .{ .cp = input.Backspace },
|
||||
else => {
|
||||
var iter = terminal.code_point.Iterator{ .bytes = buf[0..read_bytes] };
|
||||
while (iter.next()) |cp| {
|
||||
this.postEvent(.{ .key = .{ .cp = cp.code } });
|
||||
}
|
||||
var iter = code_point.Iterator{ .bytes = buf[0..read_bytes] };
|
||||
while (iter.next()) |cp| this.postEvent(.{ .key = .{ .cp = cp.code } });
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
65
src/cell.zig
Normal file
65
src/cell.zig
Normal file
@@ -0,0 +1,65 @@
|
||||
const std = @import("std");
|
||||
const Style = @import("style.zig");
|
||||
|
||||
pub const Cell = @This();
|
||||
|
||||
style: Style = .{ .emphasis = &.{} },
|
||||
// TODO embrace `zg` dependency more due to utf-8 encoding
|
||||
cp: u21 = ' ',
|
||||
|
||||
pub fn eql(this: Cell, other: Cell) bool {
|
||||
return this.cp == other.cp and this.style.eql(other.style);
|
||||
}
|
||||
|
||||
pub fn reset(this: *Cell) void {
|
||||
this.style = .{ .emphasis = &.{} };
|
||||
this.cp = ' ';
|
||||
}
|
||||
|
||||
pub fn value(this: Cell, writer: anytype) !void {
|
||||
try this.style.value(writer, this.cp);
|
||||
}
|
||||
|
||||
test "ascii styled text" {
|
||||
const cells: [4]Cell = .{
|
||||
.{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
|
||||
.{ .cp = 'v', .style = .{ .emphasis = &.{ .bold, .underline } } },
|
||||
.{ .cp = 'e', .style = .{ .emphasis = &.{.italic} } },
|
||||
.{ .cp = 's', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
|
||||
};
|
||||
|
||||
var string = std.ArrayList(u8).init(std.testing.allocator);
|
||||
defer string.deinit();
|
||||
|
||||
const writer = string.writer();
|
||||
for (cells) |cell| {
|
||||
try cell.value(writer);
|
||||
}
|
||||
try std.testing.expectEqualSlices(
|
||||
u8,
|
||||
"\x1b[38;5;10;48;5;8;59mY\x1b[0m\x1b[39;49;59;1;4mv\x1b[0m\x1b[39;49;59;3me\x1b[0m\x1b[38;5;2;48;5;16;59;4ms\x1b[0m",
|
||||
string.items,
|
||||
);
|
||||
}
|
||||
|
||||
test "utf-8 styled text" {
|
||||
const cells: [4]Cell = .{
|
||||
.{ .cp = '╭', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
|
||||
.{ .cp = '─', .style = .{ .emphasis = &.{} } },
|
||||
.{ .cp = '┄', .style = .{ .emphasis = &.{} } },
|
||||
.{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
|
||||
};
|
||||
|
||||
var string = std.ArrayList(u8).init(std.testing.allocator);
|
||||
defer string.deinit();
|
||||
|
||||
const writer = string.writer();
|
||||
for (cells) |cell| {
|
||||
try cell.value(writer);
|
||||
}
|
||||
try std.testing.expectEqualSlices(
|
||||
u8,
|
||||
"\x1b[38;5;10;48;5;8;59m╭\x1b[0m\x1b[39;49;59m─\x1b[0m\x1b[39;49;59m┄\x1b[0m\x1b[38;5;2;48;5;16;59;4m┘\x1b[0m",
|
||||
string.items,
|
||||
);
|
||||
}
|
||||
38
src/color.zig
Normal file
38
src/color.zig
Normal file
@@ -0,0 +1,38 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Color = enum(u8) {
|
||||
default = 0,
|
||||
black = 16,
|
||||
light_red = 1,
|
||||
light_green,
|
||||
light_yellow,
|
||||
light_blue,
|
||||
light_magenta,
|
||||
light_cyan,
|
||||
light_grey,
|
||||
grey,
|
||||
red,
|
||||
green,
|
||||
yellow,
|
||||
blue,
|
||||
magenta,
|
||||
cyan,
|
||||
white,
|
||||
// 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 {
|
||||
if (this == .default) {
|
||||
switch (coloring) {
|
||||
.fg => try std.fmt.format(writer, "39", .{}),
|
||||
.bg => try std.fmt.format(writer, "49", .{}),
|
||||
.ul => try std.fmt.format(writer, "59", .{}),
|
||||
}
|
||||
} else {
|
||||
switch (coloring) {
|
||||
.fg => try std.fmt.format(writer, "38;5;{d}", .{@intFromEnum(this)}),
|
||||
.bg => try std.fmt.format(writer, "48;5;{d}", .{@intFromEnum(this)}),
|
||||
.ul => try std.fmt.format(writer, "58;5;{d}", .{@intFromEnum(this)}),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
953
src/container.zig
Normal file
953
src/container.zig
Normal file
@@ -0,0 +1,953 @@
|
||||
const std = @import("std");
|
||||
|
||||
const isTaggedUnion = @import("event.zig").isTaggedUnion;
|
||||
|
||||
const Cell = @import("cell.zig");
|
||||
const Color = @import("color.zig").Color;
|
||||
const Point = @import("point.zig").Point;
|
||||
const Style = @import("style.zig");
|
||||
const Error = @import("error.zig").Error;
|
||||
|
||||
const log = std.log.scoped(.container);
|
||||
|
||||
/// Border configuration struct
|
||||
pub const Border = packed struct {
|
||||
// corners:
|
||||
const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' };
|
||||
const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' };
|
||||
|
||||
/// Color to use for the border
|
||||
color: Color = .default,
|
||||
/// Configure the corner type to be used for the border
|
||||
corners: enum(u1) {
|
||||
squared,
|
||||
rounded,
|
||||
} = .squared,
|
||||
/// Configure the sides where the borders shall be rendered
|
||||
sides: packed struct {
|
||||
top: bool = false,
|
||||
bottom: bool = false,
|
||||
left: bool = false,
|
||||
right: bool = false,
|
||||
|
||||
/// Enable border sides for all four sides
|
||||
pub const all: @This() = .{ .top = true, .bottom = true, .left = true, .right = true };
|
||||
/// Enable border sides for the left and right sides
|
||||
pub const horizontal: @This() = .{ .left = true, .right = true };
|
||||
/// Enable border sides for the top and bottom sides
|
||||
pub const vertical: @This() = .{ .top = true, .bottom = true };
|
||||
} = .{},
|
||||
|
||||
pub fn content(this: @This(), cells: []Cell, size: Point) void {
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const frame = switch (this.corners) {
|
||||
.rounded => Border.rounded_border,
|
||||
.squared => Border.squared_border,
|
||||
};
|
||||
std.debug.assert(frame.len == 6);
|
||||
|
||||
// render top and bottom border
|
||||
if (this.sides.top or this.sides.bottom) {
|
||||
for (0..size.x) |col| {
|
||||
const last_row = @as(usize, size.y - 1) * @as(usize, size.x);
|
||||
if (this.sides.left and col == 0) {
|
||||
// top left corner
|
||||
if (this.sides.top) cells[col].cp = frame[0];
|
||||
// bottom left corner
|
||||
if (this.sides.bottom) cells[last_row + col].cp = frame[4];
|
||||
} else if (this.sides.right and col == size.x - 1) {
|
||||
// top right corner
|
||||
if (this.sides.top) cells[col].cp = frame[2];
|
||||
// bottom left corner
|
||||
if (this.sides.bottom) cells[last_row + col].cp = frame[5];
|
||||
} else {
|
||||
// top side
|
||||
if (this.sides.top) cells[col].cp = frame[1];
|
||||
// bottom side
|
||||
if (this.sides.bottom) cells[last_row + col].cp = frame[1];
|
||||
}
|
||||
if (this.sides.top) cells[col].style.fg = this.color;
|
||||
if (this.sides.bottom) cells[last_row + col].style.fg = this.color;
|
||||
}
|
||||
}
|
||||
// render left and right border
|
||||
if (this.sides.left or this.sides.right) {
|
||||
var start: usize = 0;
|
||||
if (this.sides.top) start = 1;
|
||||
var end = size.y;
|
||||
if (this.sides.bottom) end -= 1;
|
||||
for (start..end) |row| {
|
||||
const idx = (row * size.x);
|
||||
if (this.sides.left) {
|
||||
cells[idx].cp = frame[3]; // left
|
||||
cells[idx].style.fg = this.color;
|
||||
}
|
||||
if (this.sides.right) {
|
||||
cells[idx + size.x - 1].cp = frame[3]; // right
|
||||
cells[idx + size.x - 1].style.fg = this.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "all sides" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .green,
|
||||
.sides = .all,
|
||||
},
|
||||
}, .{});
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/border.all.zon"));
|
||||
}
|
||||
|
||||
test "vertical sides" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .green,
|
||||
.sides = .vertical,
|
||||
},
|
||||
}, .{});
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/border.vertical.zon"));
|
||||
}
|
||||
|
||||
test "horizontal sides" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .green,
|
||||
.sides = .horizontal,
|
||||
},
|
||||
}, .{});
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/border.horizontal.zon"));
|
||||
}
|
||||
};
|
||||
|
||||
/// Rectangle configuration struct
|
||||
pub const Rectangle = packed struct {
|
||||
/// `Color` to use to fill the `Rectangle` with
|
||||
/// NOTE used as background color when rendering! such that it renders the
|
||||
/// children accordingly without removing the coloring of the `Rectangle`
|
||||
fill: Color = .default,
|
||||
|
||||
// NOTE caller owns `Cells` slice and ensures that `cells.len == size.x * size.y`
|
||||
pub fn content(this: @This(), cells: []Cell, size: Point) void {
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
for (0..size.y) |row| {
|
||||
for (0..size.x) |col| {
|
||||
cells[(row * size.x) + col].style.bg = this.fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "fill color overwrite parent fill" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .green },
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .white },
|
||||
}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_parent_fill_without_padding.zon"));
|
||||
}
|
||||
|
||||
test "fill color padding to show parent fill" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.padding = .all(2),
|
||||
},
|
||||
.rectangle = .{ .fill = .green },
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .white },
|
||||
}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_parent_padding.zon"));
|
||||
}
|
||||
|
||||
test "fill color spacer with padding" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.rectangle = .{
|
||||
.fill = .black,
|
||||
},
|
||||
.layout = .{
|
||||
.padding = .vertical(2),
|
||||
.direction = .vertical,
|
||||
},
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .white },
|
||||
}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .white },
|
||||
}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_padding.zon"));
|
||||
}
|
||||
|
||||
test "fill color with gap" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.rectangle = .{
|
||||
.fill = .black,
|
||||
},
|
||||
.layout = .{
|
||||
.padding = .vertical(1),
|
||||
.gap = 1,
|
||||
.direction = .vertical,
|
||||
},
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .white },
|
||||
}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .white },
|
||||
}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_gap.zon"));
|
||||
}
|
||||
|
||||
test "fill color with separator" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.rectangle = .{
|
||||
.fill = .black,
|
||||
},
|
||||
.layout = .{
|
||||
.padding = .vertical(1),
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .white },
|
||||
}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .white },
|
||||
}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/rectangle_with_separator.zon"));
|
||||
}
|
||||
};
|
||||
|
||||
/// Layout configuration struct
|
||||
pub const Layout = packed struct {
|
||||
// separator.line:
|
||||
const line: [2]u21 = .{ '│', '─' };
|
||||
const dotted: [2]u21 = .{ '┆', '┄' };
|
||||
const double: [2]u21 = .{ '║', '═' };
|
||||
|
||||
/// control the direction in which child elements are laid out
|
||||
direction: enum(u1) { horizontal, vertical } = .horizontal,
|
||||
/// Padding outside of the child elements
|
||||
padding: packed struct {
|
||||
top: u16 = 0,
|
||||
bottom: u16 = 0,
|
||||
left: u16 = 0,
|
||||
right: u16 = 0,
|
||||
|
||||
/// Create a padding with equivalent padding in all four directions.
|
||||
pub fn all(padding: u16) @This() {
|
||||
return .{ .top = padding, .bottom = padding, .left = padding, .right = padding };
|
||||
}
|
||||
|
||||
/// Create a padding with equivalent padding in the left and right directions; others directions remain the default value.
|
||||
pub fn horizontal(padding: u16) @This() {
|
||||
return .{ .left = padding, .right = padding };
|
||||
}
|
||||
|
||||
/// Create a padding with equivalent padding in the top and bottom directions; others directions remain the default value.
|
||||
pub fn vertical(padding: u16) @This() {
|
||||
return .{ .top = padding, .bottom = padding };
|
||||
}
|
||||
} = .{},
|
||||
/// Padding used in between child elements as gaps when laid out
|
||||
gap: u16 = 0,
|
||||
/// Configure separator borders between child element to added to the layout
|
||||
separator: packed struct {
|
||||
enabled: bool = false,
|
||||
color: Color = .white,
|
||||
line: enum(u2) {
|
||||
line,
|
||||
dotted,
|
||||
double,
|
||||
} = .line,
|
||||
} = .{},
|
||||
|
||||
pub fn content(this: @This(), comptime C: type, cells: []Cell, origin: Point, size: Point, children: []const C) void {
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
if (this.separator.enabled and children.len > 1) {
|
||||
const line_cps: [2]u21 = switch (this.separator.line) {
|
||||
.line => line,
|
||||
.dotted => dotted,
|
||||
.double => double,
|
||||
};
|
||||
const gap: u16 = (this.gap + 1) / 2;
|
||||
|
||||
for (0..children.len - 1) |idx| {
|
||||
const child = children[idx];
|
||||
const anchor = switch (this.direction) {
|
||||
.horizontal => ((@as(usize, child.origin.y) - @as(usize, origin.y)) * @as(usize, size.x)) + @as(usize, child.origin.x) + @as(usize, child.size.x) + gap - @as(usize, origin.x),
|
||||
.vertical => ((@as(usize, child.origin.y) + @as(usize, child.size.y) + gap - @as(usize, origin.y)) * @as(usize, size.x)) + @as(usize, child.origin.x) - @as(usize, origin.x),
|
||||
};
|
||||
|
||||
switch (this.direction) {
|
||||
.horizontal => for (0..child.size.y) |row| {
|
||||
cells[anchor + row * size.x].cp = line_cps[0];
|
||||
cells[anchor + row * size.x].style.fg = this.separator.color;
|
||||
},
|
||||
.vertical => for (0..child.size.x) |col| {
|
||||
cells[anchor + col].cp = line_cps[1];
|
||||
cells[anchor + col].style.fg = this.separator.color;
|
||||
},
|
||||
}
|
||||
|
||||
// DEBUG render corresponding beginning of the separator for this `Container` *red*
|
||||
// cells[anchor].style.fg = .red;
|
||||
// cells[anchor].style.bg = .red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "separator without gaps" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_no_gaps.zon"));
|
||||
}
|
||||
|
||||
test "separator without gaps with padding" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.padding = .all(1),
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_no_gaps_with_padding.zon"));
|
||||
}
|
||||
|
||||
test "separator(2x) without gaps" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{
|
||||
.direction = .vertical,
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.color = .red,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_2x_no_gaps.zon"));
|
||||
}
|
||||
|
||||
test "separator(2x) with border(all)" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
},
|
||||
.layout = .{
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.color = .red,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_2x_no_gaps_with_border.zon"));
|
||||
}
|
||||
|
||||
test "separator(2x) with border(all) and padding(all(1))" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
},
|
||||
.layout = .{
|
||||
.padding = .all(1),
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.color = .red,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_2x_no_gaps_with_padding.zon"));
|
||||
}
|
||||
|
||||
test "separator(2x) with border(all) and gap" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
},
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.color = .red,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_2x_with_gaps_with_border.zon"));
|
||||
}
|
||||
|
||||
test "separator(2x) with border(all) and gap and padding" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.border = .{
|
||||
.color = .red,
|
||||
.sides = .all,
|
||||
},
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .all(1),
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.color = .red,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{ .rectangle = .{ .fill = .white } }, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/separator_2x_with_gaps_with_border_with_padding.zon"));
|
||||
}
|
||||
};
|
||||
|
||||
/// Sizing options which should be used by the `Container`
|
||||
pub const Size = packed struct {
|
||||
dim: Point = .{},
|
||||
grow: enum(u2) {
|
||||
both,
|
||||
fixed,
|
||||
vertical,
|
||||
horizontal,
|
||||
} = .both,
|
||||
};
|
||||
|
||||
pub fn Container(comptime Event: type) type {
|
||||
if (!isTaggedUnion(Event)) @compileError("Provided user event `Event` for `Container(comptime Event: type)`");
|
||||
|
||||
const Element = @import("element.zig").Element(Event);
|
||||
return struct {
|
||||
allocator: std.mem.Allocator,
|
||||
origin: Point,
|
||||
size: Point,
|
||||
properties: Properties,
|
||||
element: Element,
|
||||
elements: std.ArrayList(@This()),
|
||||
|
||||
/// Properties for each `Container` to configure their layout,
|
||||
/// border, styling, etc. For details see the corresponding individual
|
||||
/// documentation of the members of this struct accordingly.
|
||||
pub const Properties = packed struct {
|
||||
border: Border = .{},
|
||||
rectangle: Rectangle = .{},
|
||||
layout: Layout = .{},
|
||||
size: Size = .{},
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
properties: Properties,
|
||||
element: Element,
|
||||
) !@This() {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.origin = .{},
|
||||
.size = .{},
|
||||
.properties = properties,
|
||||
.element = element,
|
||||
.elements = std.ArrayList(@This()).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
for (this.elements.items) |*element| {
|
||||
element.deinit();
|
||||
}
|
||||
this.elements.deinit();
|
||||
}
|
||||
|
||||
pub fn append(this: *@This(), element: @This()) !void {
|
||||
try this.elements.append(element);
|
||||
}
|
||||
|
||||
pub fn reposition(this: *@This(), origin: Point) void {
|
||||
const layout = this.properties.layout;
|
||||
this.origin = origin;
|
||||
this.element.reposition(origin);
|
||||
|
||||
var offset = origin.add(.{
|
||||
.x = layout.padding.left,
|
||||
.y = layout.padding.top,
|
||||
});
|
||||
|
||||
const sides = this.properties.border.sides;
|
||||
if (sides.left) offset.x += 1;
|
||||
if (sides.top) offset.y += 1;
|
||||
|
||||
for (this.elements.items) |*child| {
|
||||
child.reposition(offset);
|
||||
|
||||
switch (layout.direction) {
|
||||
.horizontal => offset.x += child.size.x + layout.gap,
|
||||
.vertical => offset.y += child.size.y + layout.gap,
|
||||
}
|
||||
|
||||
if (layout.separator.enabled) switch (layout.direction) {
|
||||
.horizontal => offset.x += 1,
|
||||
.vertical => offset.y += 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize all fit sized `Containers` to the necessary size required by its child elements.
|
||||
fn fit_resize(this: *@This()) Point {
|
||||
// NOTE this is supposed to be a simple and easy to understand algorithm, there are currently no optimizations done
|
||||
const layout = this.properties.layout;
|
||||
var size: Point = switch (layout.direction) {
|
||||
.horizontal => .{ .x = layout.gap * @as(u16, @truncate(this.elements.items.len -| 1)) },
|
||||
.vertical => .{ .y = layout.gap * @as(u16, @truncate(this.elements.items.len -| 1)) },
|
||||
};
|
||||
|
||||
if (this.elements.items.len > 0) switch (layout.direction) {
|
||||
.horizontal => size.x += layout.padding.left + layout.padding.right,
|
||||
.vertical => size.y += layout.padding.top + layout.padding.bottom,
|
||||
};
|
||||
|
||||
const sides = this.properties.border.sides;
|
||||
if (sides.left) size.x += 1;
|
||||
if (sides.right) size.x += 1;
|
||||
if (sides.top) size.y += 1;
|
||||
if (sides.bottom) size.y += 1;
|
||||
|
||||
if (layout.separator.enabled) switch (layout.direction) {
|
||||
.horizontal => size.x += @as(u16, @truncate(this.elements.items.len -| 1)),
|
||||
.vertical => size.y += @as(u16, @truncate(this.elements.items.len -| 1)),
|
||||
};
|
||||
|
||||
for (this.elements.items) |*child| {
|
||||
const child_size = child.fit_resize();
|
||||
switch (layout.direction) {
|
||||
.horizontal => {
|
||||
size.x += child_size.x;
|
||||
size.y = @max(size.y, child_size.y);
|
||||
},
|
||||
.vertical => {
|
||||
size.x = @max(size.x, child_size.x);
|
||||
size.y += child_size.y;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// assign currently calculated size
|
||||
this.size = switch (this.properties.size.grow) {
|
||||
.both => Point.max(size, this.properties.size.dim),
|
||||
.fixed => this.properties.size.dim,
|
||||
.horizontal => .{
|
||||
.x = @max(size.x, this.properties.size.dim.x),
|
||||
.y = this.properties.size.dim.y,
|
||||
},
|
||||
.vertical => .{
|
||||
.x = this.properties.size.dim.x,
|
||||
.y = @max(size.y, this.properties.size.dim.y),
|
||||
},
|
||||
};
|
||||
log.debug("fit_size returning: {any}", .{this.size});
|
||||
return this.size;
|
||||
}
|
||||
|
||||
/// growable implicitly requires the root `Container` to have a set a size property to the size of the available terminal screen
|
||||
fn grow_resize(this: *@This(), max_size: Point) void {
|
||||
log.debug("grow_size: {any}", .{this.size});
|
||||
const layout = this.properties.layout;
|
||||
var remainder = switch (layout.direction) {
|
||||
.horizontal => max_size.x -| (layout.padding.left + layout.padding.right),
|
||||
.vertical => max_size.y -| (layout.padding.top + layout.padding.bottom),
|
||||
};
|
||||
remainder -|= layout.gap * @as(u16, @truncate(this.elements.items.len -| 1));
|
||||
|
||||
if (layout.separator.enabled) remainder -|= @as(u16, @truncate(this.elements.items.len -| 1));
|
||||
|
||||
var available = switch (layout.direction) {
|
||||
.horizontal => max_size.y -| (layout.padding.top + layout.padding.bottom),
|
||||
.vertical => max_size.x -| (layout.padding.left + layout.padding.right),
|
||||
};
|
||||
|
||||
const sides = this.properties.border.sides;
|
||||
switch (layout.direction) {
|
||||
.horizontal => {
|
||||
if (sides.top) {
|
||||
available -|= 1;
|
||||
remainder -|= 1;
|
||||
}
|
||||
if (sides.bottom) {
|
||||
available -|= 1;
|
||||
remainder -|= 1;
|
||||
}
|
||||
},
|
||||
.vertical => {
|
||||
if (sides.left) {
|
||||
available -|= 1;
|
||||
remainder -|= 1;
|
||||
}
|
||||
if (sides.right) {
|
||||
available -|= 1;
|
||||
remainder -|= 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for (this.elements.items) |child| remainder -|= switch (layout.direction) {
|
||||
.horizontal => child.size.x,
|
||||
.vertical => child.size.y,
|
||||
};
|
||||
|
||||
var growable_children: usize = 0;
|
||||
var first_growable_child: *@This() = undefined;
|
||||
for (this.elements.items) |*child| {
|
||||
// layout direction side growth
|
||||
switch (child.properties.size.grow) {
|
||||
.fixed => continue,
|
||||
.both => {
|
||||
if (growable_children == 0) first_growable_child = child;
|
||||
growable_children += 1;
|
||||
},
|
||||
.horizontal => if (layout.direction == .horizontal) {
|
||||
if (growable_children == 0) first_growable_child = child;
|
||||
growable_children += 1;
|
||||
},
|
||||
.vertical => if (layout.direction == .vertical) {
|
||||
if (growable_children == 0) first_growable_child = child;
|
||||
growable_children += 1;
|
||||
},
|
||||
}
|
||||
// non layout direction side growth
|
||||
switch (layout.direction) {
|
||||
.horizontal => if (child.properties.size.grow == .vertical or child.properties.size.grow == .both) {
|
||||
child.size.y = available;
|
||||
},
|
||||
.vertical => if (child.properties.size.grow == .horizontal or child.properties.size.grow == .both) {
|
||||
child.size.x = available;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
while (growable_children > 0 and remainder > 0) {
|
||||
var smallest_size = switch (layout.direction) {
|
||||
.horizontal => first_growable_child.size.x,
|
||||
.vertical => first_growable_child.size.y,
|
||||
};
|
||||
var second_smallest_size: u16 = std.math.maxInt(u16);
|
||||
var size_to_correct = remainder;
|
||||
|
||||
for (this.elements.items) |child| {
|
||||
if (child.properties.size.grow == .fixed) continue;
|
||||
|
||||
const size = switch (layout.direction) {
|
||||
.horizontal => child.size.x,
|
||||
.vertical => child.size.y,
|
||||
};
|
||||
|
||||
if (size < smallest_size) {
|
||||
second_smallest_size = smallest_size;
|
||||
smallest_size = size;
|
||||
} else if (size > smallest_size) {
|
||||
second_smallest_size = @min(size, second_smallest_size);
|
||||
size_to_correct = second_smallest_size -| smallest_size;
|
||||
}
|
||||
}
|
||||
|
||||
size_to_correct = @min(size_to_correct, remainder / growable_children);
|
||||
var overflow: u16 = 0;
|
||||
if (size_to_correct == 0 and remainder > 0) {
|
||||
// there is some overflow
|
||||
overflow = remainder;
|
||||
}
|
||||
for (this.elements.items) |*child| {
|
||||
const child_size = switch (layout.direction) {
|
||||
.horizontal => child.size.x,
|
||||
.vertical => child.size.y,
|
||||
};
|
||||
if (child.properties.size.grow != .fixed and child_size == smallest_size) {
|
||||
switch (layout.direction) {
|
||||
.horizontal => if (child.properties.size.grow != .vertical) {
|
||||
child.size.x += size_to_correct;
|
||||
},
|
||||
.vertical => if (child.properties.size.grow != .horizontal) {
|
||||
child.size.y += size_to_correct;
|
||||
},
|
||||
}
|
||||
if (overflow > 0) {
|
||||
switch (layout.direction) {
|
||||
.horizontal => if (child.properties.size.grow != .vertical) {
|
||||
child.size.x += 1;
|
||||
overflow -|= 1;
|
||||
remainder -|= 1;
|
||||
},
|
||||
.vertical => if (child.properties.size.grow != .horizontal) {
|
||||
child.size.y += 1;
|
||||
overflow -|= 1;
|
||||
remainder -|= 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
remainder -|= size_to_correct;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.element.resize(this.size);
|
||||
for (this.elements.items) |*child| child.grow_resize(child.size);
|
||||
}
|
||||
|
||||
pub fn resize(this: *@This(), size: Point) void {
|
||||
// NOTE assume that this function is only called for the root `Container`
|
||||
const fit_size = this.fit_resize();
|
||||
// if (fit_size.y > size.y or fit_size.x > size.x) @panic("error: cannot render in available space");
|
||||
switch (this.properties.size.grow) {
|
||||
.both => this.size = Point.max(size, fit_size),
|
||||
.fixed => {},
|
||||
.horizontal => this.size = .{
|
||||
.x = @max(size.x, fit_size.x),
|
||||
.y = size.y,
|
||||
},
|
||||
.vertical => this.size = .{
|
||||
.x = size.x,
|
||||
.y = @max(size.y, fit_size.y),
|
||||
},
|
||||
}
|
||||
this.grow_resize(this.size);
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !void {
|
||||
switch (event) {
|
||||
.mouse => |mouse| if (mouse.in(this.origin, this.size)) {
|
||||
try this.element.handle(event);
|
||||
for (this.elements.items) |*element| try element.handle(event);
|
||||
},
|
||||
else => {
|
||||
try this.element.handle(event);
|
||||
for (this.elements.items) |*element| try element.handle(event);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content(this: *const @This()) ![]const Cell {
|
||||
if (this.size.x == 0 or this.size.y == 0) return Error.TooSmall;
|
||||
const cells = try this.allocator.alloc(Cell, @as(usize, this.size.x) * @as(usize, this.size.y));
|
||||
@memset(cells, .{});
|
||||
errdefer this.allocator.free(cells);
|
||||
|
||||
this.properties.layout.content(@This(), cells, this.origin, this.size, this.elements.items);
|
||||
this.properties.border.content(cells, this.size);
|
||||
this.properties.rectangle.content(cells, this.size);
|
||||
|
||||
try this.element.content(cells, this.size);
|
||||
|
||||
// DEBUG render corresponding top left corner of this `Container` *red*
|
||||
// cells[0].style.fg = .red;
|
||||
// cells[0].style.bg = .red;
|
||||
|
||||
return cells;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
_ = Border;
|
||||
_ = Layout;
|
||||
_ = Rectangle;
|
||||
}
|
||||
|
||||
test "Container Fixed and Grow Size Vertical" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
.layout = .{ .direction = .vertical },
|
||||
}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.size = .{
|
||||
.dim = .{ .y = 5 },
|
||||
.grow = .horizontal,
|
||||
},
|
||||
.rectangle = .{ .fill = .grey },
|
||||
}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .red },
|
||||
}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/fixed_grow_vertical.zon"));
|
||||
}
|
||||
|
||||
test "Container Fixed and Grow Size Horizontal" {
|
||||
const event = @import("event.zig");
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{}, .{});
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.size = .{
|
||||
.dim = .{ .x = 5 },
|
||||
.grow = .vertical,
|
||||
},
|
||||
.rectangle = .{ .fill = .grey },
|
||||
}, .{}));
|
||||
try container.append(try .init(std.testing.allocator, .{
|
||||
.rectangle = .{ .fill = .red },
|
||||
}, .{}));
|
||||
defer container.deinit();
|
||||
|
||||
try testing.expectContainerScreen(.{
|
||||
.y = 20,
|
||||
.x = 30,
|
||||
}, &container, @import("test/container/fixed_grow_horizontal.zon"));
|
||||
}
|
||||
@@ -58,8 +58,11 @@ pub const cub = "\x1b[{d}D";
|
||||
|
||||
// Erase
|
||||
pub const erase_below_cursor = "\x1b[J";
|
||||
pub const clear_screen = "\x1b[2J";
|
||||
|
||||
// alt screen
|
||||
pub const save_screen = "\x1b[?47h";
|
||||
pub const restore_screen = "\x1b[?47l";
|
||||
pub const smcup = "\x1b[?1049h";
|
||||
pub const rmcup = "\x1b[?1049l";
|
||||
|
||||
@@ -89,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";
|
||||
|
||||
// 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_double = "\x1b[4:2m";
|
||||
pub const ul_curly = "\x1b[4:3m";
|
||||
344
src/element.zig
Normal file
344
src/element.zig
Normal file
@@ -0,0 +1,344 @@
|
||||
//! Interface for Element's which describe the contents of a `Container`.
|
||||
const std = @import("std");
|
||||
const input = @import("input.zig");
|
||||
|
||||
const Container = @import("container.zig").Container;
|
||||
const Cell = @import("cell.zig");
|
||||
const Mouse = input.Mouse;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
pub fn Element(Event: type) type {
|
||||
return struct {
|
||||
ptr: *anyopaque = undefined,
|
||||
vtable: *const VTable = &.{},
|
||||
|
||||
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,
|
||||
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
|
||||
/// events or a system event, with the exception of the `.size`
|
||||
/// `Event` as every `Container` already handles that event.
|
||||
///
|
||||
/// In case of user errors this function should return an error. This
|
||||
/// error may then be used by the application to display information
|
||||
/// about the user error.
|
||||
pub inline fn handle(this: @This(), event: Event) !void {
|
||||
if (this.vtable.handle) |handle_fn|
|
||||
try handle_fn(this.ptr, event);
|
||||
}
|
||||
|
||||
/// Write content into the `cells` of the `Container`. The associated
|
||||
/// `cells` slice has the size of (`size.x * size.y`). The
|
||||
/// renderer will know where to place the contents on the screen.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// - Caller owns `cells` slice and ensures that the size usually by assertion:
|
||||
/// ```zig
|
||||
/// std.debug.assert(cells.len == @as(usize, size.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|
|
||||
try content_fn(this.ptr, cells, size);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn Scrollable(Event: type) type {
|
||||
return struct {
|
||||
/// `Size` of the actual contents where the anchor and the size is
|
||||
/// representing the size and location on screen.
|
||||
size: Point = .{},
|
||||
/// `Size` of the `Container` content that is scrollable and mapped to
|
||||
/// the *size* of the `Scrollable` `Element`.
|
||||
container_size: Point = .{},
|
||||
/// Anchor of the viewport of the scrollable `Container`.
|
||||
anchor: Point = .{},
|
||||
/// The actual `Container`, that is scrollable.
|
||||
container: Container(Event),
|
||||
|
||||
pub fn element(this: *@This()) Element(Event) {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.resize = resize,
|
||||
.reposition = reposition,
|
||||
.handle = handle,
|
||||
.content = content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn resize(ctx: *anyopaque, size: Point) void {
|
||||
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 {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
// TODO other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?)
|
||||
.mouse => |mouse| switch (mouse.button) {
|
||||
Mouse.Button.wheel_up => if (this.container_size.y > this.size.y) {
|
||||
this.anchor.y -|= 1;
|
||||
},
|
||||
Mouse.Button.wheel_down => if (this.container_size.y > this.size.y) {
|
||||
const max_origin_y = this.container_size.y -| this.size.y;
|
||||
this.anchor.y = @min(this.anchor.y + 1, max_origin_y);
|
||||
},
|
||||
Mouse.Button.wheel_left => if (this.container_size.x > this.size.x) {
|
||||
this.anchor.x -|= 1;
|
||||
},
|
||||
Mouse.Button.wheel_right => if (this.container_size.x > this.size.x) {
|
||||
const max_anchor_x = this.container_size.x -| this.size.x;
|
||||
this.anchor.x = @min(this.anchor.x + 1, max_anchor_x);
|
||||
},
|
||||
else => try this.container.handle(.{
|
||||
.mouse = .{
|
||||
.x = mouse.x + this.anchor.x,
|
||||
.y = mouse.y + this.anchor.y,
|
||||
.button = mouse.button,
|
||||
.kind = mouse.kind,
|
||||
},
|
||||
}),
|
||||
},
|
||||
else => try this.container.handle(event),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_container(container: Container(Event), cells: []Cell, container_size: Point) !void {
|
||||
const size = container.size;
|
||||
const origin = container.origin;
|
||||
const contents = try container.content();
|
||||
defer container.allocator.free(contents);
|
||||
|
||||
const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x);
|
||||
|
||||
var idx: usize = 0;
|
||||
blk: for (0..size.y) |row| {
|
||||
for (0..size.x) |col| {
|
||||
cells[anchor + (row * container_size.x) + col] = contents[idx];
|
||||
idx += 1;
|
||||
|
||||
if (contents.len == idx) break :blk;
|
||||
}
|
||||
}
|
||||
|
||||
for (container.elements.items) |child| try render_container(child, cells, size);
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void {
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 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,
|
||||
};
|
||||
@@ -1,34 +1,45 @@
|
||||
//! Events which are defined by the library. They might be extended by user
|
||||
//! events. See `App` for more details about user defined events.
|
||||
const std = @import("std");
|
||||
const input = @import("input.zig");
|
||||
const terminal = @import("terminal.zig");
|
||||
|
||||
const Size = terminal.Size;
|
||||
const Key = terminal.Key;
|
||||
const Key = input.Key;
|
||||
const Mouse = input.Mouse;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
pub const Error = struct {
|
||||
err: anyerror,
|
||||
msg: []const u8,
|
||||
};
|
||||
|
||||
// System events available to every application.
|
||||
/// System events available to every `zterm.App`
|
||||
pub const SystemEvent = union(enum) {
|
||||
/// 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
|
||||
init,
|
||||
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
|
||||
quit,
|
||||
err: Error,
|
||||
resize: Size,
|
||||
/// Error event to notify other containers about a recoverable error
|
||||
err: struct {
|
||||
err: anyerror,
|
||||
/// associated error message
|
||||
msg: []const u8,
|
||||
},
|
||||
/// Input key event received from the user
|
||||
key: Key,
|
||||
/// Mouse input event
|
||||
mouse: Mouse,
|
||||
/// Focus event for mouse interaction
|
||||
/// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for
|
||||
focus: bool,
|
||||
};
|
||||
|
||||
pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
|
||||
if (!isTaggedUnion(A) or !isTaggedUnion(B)) {
|
||||
@compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
|
||||
}
|
||||
const a_fields = @typeInfo(A).Union.fields;
|
||||
const a_fields_tag = @typeInfo(A).Union.tag_type.?;
|
||||
const a_enum_fields = @typeInfo(a_fields_tag).Enum.fields;
|
||||
const b_fields = @typeInfo(B).Union.fields;
|
||||
const b_fields_tag = @typeInfo(B).Union.tag_type.?;
|
||||
const b_enum_fields = @typeInfo(b_fields_tag).Enum.fields;
|
||||
const a_fields = @typeInfo(A).@"union".fields;
|
||||
const a_fields_tag = @typeInfo(A).@"union".tag_type.?;
|
||||
const a_enum_fields = @typeInfo(a_fields_tag).@"enum".fields;
|
||||
const b_fields = @typeInfo(B).@"union".fields;
|
||||
const b_fields_tag = @typeInfo(B).@"union".tag_type.?;
|
||||
const b_enum_fields = @typeInfo(b_fields_tag).@"enum".fields;
|
||||
var fields: [a_fields.len + b_fields.len]std.builtin.Type.UnionField = undefined;
|
||||
var enum_fields: [a_fields.len + b_fields.len]std.builtin.Type.EnumField = undefined;
|
||||
var i: usize = 0;
|
||||
@@ -49,19 +60,19 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
|
||||
|
||||
const log2_i = @bitSizeOf(@TypeOf(i)) - @clz(i);
|
||||
|
||||
const EventType = @Type(.{ .Int = .{
|
||||
const EventType = @Type(.{ .int = .{
|
||||
.signedness = .unsigned,
|
||||
.bits = log2_i,
|
||||
} });
|
||||
|
||||
const Event = @Type(.{ .Enum = .{
|
||||
const Event = @Type(.{ .@"enum" = .{
|
||||
.tag_type = EventType,
|
||||
.fields = enum_fields[0..],
|
||||
.decls = &.{},
|
||||
.is_exhaustive = true,
|
||||
} });
|
||||
|
||||
return @Type(.{ .Union = .{
|
||||
return @Type(.{ .@"union" = .{
|
||||
.layout = .auto,
|
||||
.tag_type = Event,
|
||||
.fields = fields[0..],
|
||||
@@ -72,7 +83,7 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
|
||||
// Determine at `comptime` whether the provided type `E` is an `union(enum)`.
|
||||
pub fn isTaggedUnion(comptime E: type) bool {
|
||||
switch (@typeInfo(E)) {
|
||||
.Union => |u| {
|
||||
.@"union" => |u| {
|
||||
if (u.tag_type) |_| {} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
225
src/input.zig
Normal file
225
src/input.zig
Normal file
@@ -0,0 +1,225 @@
|
||||
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
|
||||
const std = @import("std");
|
||||
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
pub const Mouse = packed struct {
|
||||
x: u16,
|
||||
y: u16,
|
||||
button: Button,
|
||||
kind: Kind,
|
||||
|
||||
pub const Button = enum(u8) {
|
||||
left,
|
||||
middle,
|
||||
right,
|
||||
none,
|
||||
wheel_up = 64,
|
||||
wheel_down = 65,
|
||||
wheel_right = 66,
|
||||
wheel_left = 67,
|
||||
button_8 = 128,
|
||||
button_9 = 129,
|
||||
button_10 = 130,
|
||||
button_11 = 131,
|
||||
};
|
||||
|
||||
pub const Kind = enum(u2) {
|
||||
press,
|
||||
release,
|
||||
motion,
|
||||
drag,
|
||||
};
|
||||
|
||||
pub fn eql(this: @This(), other: @This()) bool {
|
||||
return std.meta.eql(this, other);
|
||||
}
|
||||
|
||||
pub fn in(this: @This(), origin: Point, size: Point) bool {
|
||||
return this.x >= origin.x and this.x < size.x + origin.x and
|
||||
this.y >= origin.y and this.y < size.y + origin.y;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Key = packed struct {
|
||||
cp: u21,
|
||||
mod: Modifier = .{},
|
||||
|
||||
pub const Modifier = packed struct {
|
||||
shift: bool = false,
|
||||
alt: bool = false,
|
||||
ctrl: bool = false,
|
||||
};
|
||||
|
||||
/// Compare _this_ `Key` with an _other_ `Key`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Configure `ctrl+c` to quit the application (done in main event loop of the application):
|
||||
///
|
||||
/// ```zig
|
||||
/// switch (event) {
|
||||
/// .quit => break,
|
||||
/// .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit.set(),
|
||||
/// else => {},
|
||||
/// }
|
||||
/// ```
|
||||
pub fn eql(this: @This(), other: @This()) bool {
|
||||
return std.meta.eql(this, other);
|
||||
}
|
||||
|
||||
/// Determine if the `Key` is an ascii character that can be printed to
|
||||
/// the screen. This means that the code point of the `Key` is an ascii
|
||||
/// character between 32 - 255 (with the exception of 127 = Delete) and no
|
||||
/// modifiers (alt and/or ctrl) are used.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Get user input's from the .key event from the application event loop:
|
||||
///
|
||||
/// ```zig
|
||||
/// switch (event) {
|
||||
/// .key => |key| if (key.isAscii()) try this.input.append(key.cp),
|
||||
/// else => {},
|
||||
/// }
|
||||
/// ```
|
||||
pub fn isAscii(this: @This()) bool {
|
||||
return this.mod.alt == false and this.mod.ctrl == false and // no modifier keys
|
||||
(this.cp >= 32 and this.cp <= 126 or // ascii printable characters (except for input.Delete)
|
||||
this.cp >= 128 and this.cp <= 255); // extended ascii codes
|
||||
}
|
||||
|
||||
test "isAscii with ascii character" {
|
||||
try std.testing.expectEqual(true, isAscii(.{ .cp = 'c' }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .ctrl = true } }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true } }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true, .ctrl = true } }));
|
||||
}
|
||||
|
||||
test "isAscii with non-ascii character" {
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Escape }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Enter }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Enter, .mod = .{ .alt = true } }));
|
||||
}
|
||||
|
||||
test "isAscii with excluded input.Delete" {
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Delete }));
|
||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Delete, .mod = .{ .alt = false, .ctrl = false } }));
|
||||
}
|
||||
};
|
||||
|
||||
// codepoints for keys
|
||||
pub const Tab: u21 = 0x09;
|
||||
pub const Enter: u21 = 0x0D;
|
||||
pub const Escape: u21 = 0x1B;
|
||||
pub const Space: u21 = 0x20;
|
||||
pub const Backspace: u21 = 0x7F;
|
||||
|
||||
// kitty key encodings (re-used here)
|
||||
pub const Insert: u21 = 57348;
|
||||
pub const Delete: u21 = 57349;
|
||||
pub const Left: u21 = 57350;
|
||||
pub const Right: u21 = 57351;
|
||||
pub const Up: u21 = 57352;
|
||||
pub const Down: u21 = 57353;
|
||||
pub const PageUp: u21 = 57354;
|
||||
pub const PageDown: u21 = 57355;
|
||||
pub const Home: u21 = 57356;
|
||||
pub const End: u21 = 57357;
|
||||
pub const CapsLock: u21 = 57358;
|
||||
pub const ScrollLock: u21 = 57359;
|
||||
pub const NumLock: u21 = 57360;
|
||||
pub const PrintScreen: u21 = 57361;
|
||||
pub const Pause: u21 = 57362;
|
||||
pub const Menu: u21 = 57363;
|
||||
pub const F1: u21 = 57364;
|
||||
pub const F2: u21 = 57365;
|
||||
pub const F3: u21 = 57366;
|
||||
pub const F4: u21 = 57367;
|
||||
pub const F5: u21 = 57368;
|
||||
pub const F6: u21 = 57369;
|
||||
pub const F7: u21 = 57370;
|
||||
pub const F8: u21 = 57371;
|
||||
pub const F9: u21 = 57372;
|
||||
pub const F10: u21 = 57373;
|
||||
pub const F11: u21 = 57374;
|
||||
pub const F12: u21 = 57375;
|
||||
pub const F13: u21 = 57376;
|
||||
pub const F14: u21 = 57377;
|
||||
pub const F15: u21 = 57378;
|
||||
pub const F16: u21 = 57379;
|
||||
pub const F17: u21 = 57380;
|
||||
pub const F18: u21 = 57381;
|
||||
pub const F19: u21 = 57382;
|
||||
pub const F20: u21 = 57383;
|
||||
pub const F21: u21 = 57384;
|
||||
pub const F22: u21 = 57385;
|
||||
pub const F23: u21 = 57386;
|
||||
pub const F24: u21 = 57387;
|
||||
pub const F25: u21 = 57388;
|
||||
pub const F26: u21 = 57389;
|
||||
pub const F27: u21 = 57390;
|
||||
pub const F28: u21 = 57391;
|
||||
pub const F29: u21 = 57392;
|
||||
pub const F30: u21 = 57393;
|
||||
pub const F31: u21 = 57394;
|
||||
pub const F32: u21 = 57395;
|
||||
pub const F33: u21 = 57396;
|
||||
pub const F34: u21 = 57397;
|
||||
pub const F35: u21 = 57398;
|
||||
pub const Kp0: u21 = 57399;
|
||||
pub const Kp1: u21 = 57400;
|
||||
pub const Kp2: u21 = 57401;
|
||||
pub const Kp3: u21 = 57402;
|
||||
pub const Kp4: u21 = 57403;
|
||||
pub const Kp5: u21 = 57404;
|
||||
pub const Kp6: u21 = 57405;
|
||||
pub const Kp7: u21 = 57406;
|
||||
pub const Kp8: u21 = 57407;
|
||||
pub const Kp9: u21 = 57408;
|
||||
pub const KpDecimal: u21 = 57409;
|
||||
pub const KpDivide: u21 = 57410;
|
||||
pub const KpMultiply: u21 = 57411;
|
||||
pub const KpSubtract: u21 = 57412;
|
||||
pub const KpAdd: u21 = 57413;
|
||||
pub const KpEnter: u21 = 57414;
|
||||
pub const KpEqual: u21 = 57415;
|
||||
pub const KpSeparator: u21 = 57416;
|
||||
pub const KpLeft: u21 = 57417;
|
||||
pub const KpRight: u21 = 57418;
|
||||
pub const KpUp: u21 = 57419;
|
||||
pub const KpDown: u21 = 57420;
|
||||
pub const KpPageUp: u21 = 57421;
|
||||
pub const KpPageDown: u21 = 57422;
|
||||
pub const KpHome: u21 = 57423;
|
||||
pub const KpEnd: u21 = 57424;
|
||||
pub const KpInsert: u21 = 57425;
|
||||
pub const KpDelete: u21 = 57426;
|
||||
pub const KpBegin: u21 = 57427;
|
||||
pub const MediaPlay: u21 = 57428;
|
||||
pub const MediaPause: u21 = 57429;
|
||||
pub const MediaPlayPause: u21 = 57430;
|
||||
pub const MediaReverse: u21 = 57431;
|
||||
pub const MediaStop: u21 = 57432;
|
||||
pub const MediaFastForward: u21 = 57433;
|
||||
pub const MediaRewind: u21 = 57434;
|
||||
pub const MediaTrackNext: u21 = 57435;
|
||||
pub const MediaTrackPrevious: u21 = 57436;
|
||||
pub const MediaRecord: u21 = 57437;
|
||||
pub const LowerVolume: u21 = 57438;
|
||||
pub const RaiseVolume: u21 = 57439;
|
||||
pub const MuteVolume: u21 = 57440;
|
||||
pub const LeftShift: u21 = 57441;
|
||||
pub const LeftControl: u21 = 57442;
|
||||
pub const LeftAlt: u21 = 57443;
|
||||
pub const LeftSuper: u21 = 57444;
|
||||
pub const LeftHyper: u21 = 57445;
|
||||
pub const LeftMeta: u21 = 57446;
|
||||
pub const RightShift: u21 = 57447;
|
||||
pub const RightControl: u21 = 57448;
|
||||
pub const RightAlt: u21 = 57449;
|
||||
pub const RightSuper: u21 = 57450;
|
||||
pub const RightHyper: u21 = 57451;
|
||||
pub const RightMeta: u21 = 57452;
|
||||
pub const IsoLevel3Shift: u21 = 57453;
|
||||
pub const IsoLevel5Shift: u21 = 57454;
|
||||
106
src/layout.zig
106
src/layout.zig
@@ -1,106 +0,0 @@
|
||||
//! Dynamic dispatch for layout implementations. Each `Layout` has to implement
|
||||
//! the `Layout.Interface`.
|
||||
//!
|
||||
//! Create a `Layout` using `createFrom(object: anytype)` and use them through
|
||||
//! the defined `Layout.Interface`. The layout will take care of calling the
|
||||
//! correct implementation of the corresponding underlying type.
|
||||
//!
|
||||
//! Each `Layout` is responsible for clearing the allocated memory of the used
|
||||
//! `Element`s (union of `Layout` or `Widget`) when deallocated. This means
|
||||
//! that `deinit()` will also deallocate every used `Element` too.
|
||||
//!
|
||||
//! When `Layout.render` is called the provided `Renderer` type is expected
|
||||
//! which handles how contents are rendered for a given layout.
|
||||
const std = @import("std");
|
||||
const isTaggedUnion = @import("event.zig").isTaggedUnion;
|
||||
|
||||
pub fn Layout(comptime Event: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
const Events = std.ArrayList(Event);
|
||||
const Type = struct {
|
||||
const LayoutType = @This();
|
||||
const Element = union(enum) {
|
||||
layout: LayoutType,
|
||||
widget: @import("widget.zig").Widget(Event, Renderer),
|
||||
};
|
||||
const Ptr = usize;
|
||||
pub const Interface = @import("interface").Interface(.{
|
||||
.handle = fn (anytype, Event) anyerror!*Events,
|
||||
.render = fn (anytype, *Renderer) anyerror!void,
|
||||
.deinit = fn (anytype) void,
|
||||
}, .{});
|
||||
|
||||
const VTable = struct {
|
||||
handle: *const fn (this: *LayoutType, event: Event) anyerror!*Events,
|
||||
render: *const fn (this: *LayoutType, renderer: *Renderer) anyerror!void,
|
||||
deinit: *const fn (this: *LayoutType) void,
|
||||
};
|
||||
|
||||
object: Ptr = undefined,
|
||||
vtable: *const VTable = undefined,
|
||||
|
||||
// Handle the provided `Event` for this `Layout`.
|
||||
pub fn handle(this: *LayoutType, event: Event) !*Events {
|
||||
return try this.vtable.handle(this, event);
|
||||
}
|
||||
|
||||
// Render this `Layout` completely. This will render contained sub-elements too.
|
||||
pub fn render(this: *LayoutType, renderer: *Renderer) !void {
|
||||
return try this.vtable.render(this, renderer);
|
||||
}
|
||||
|
||||
pub fn deinit(this: *LayoutType) void {
|
||||
this.vtable.deinit(this);
|
||||
this.* = undefined;
|
||||
}
|
||||
|
||||
pub fn createFrom(object: anytype) LayoutType {
|
||||
return LayoutType{
|
||||
.object = @intFromPtr(object),
|
||||
.vtable = &.{
|
||||
.handle = struct {
|
||||
// Handle the provided `Event` for this `Layout`.
|
||||
fn handle(this: *LayoutType, event: Event) !*Events {
|
||||
const layout: @TypeOf(object) = @ptrFromInt(this.object);
|
||||
return try layout.handle(event);
|
||||
}
|
||||
}.handle,
|
||||
.render = struct {
|
||||
// Render the contents of this `Layout`.
|
||||
fn render(this: *LayoutType, renderer: *Renderer) !void {
|
||||
const layout: @TypeOf(object) = @ptrFromInt(this.object);
|
||||
try layout.render(renderer);
|
||||
}
|
||||
}.render,
|
||||
.deinit = struct {
|
||||
fn deinit(this: *LayoutType) void {
|
||||
const layout: @TypeOf(object) = @ptrFromInt(this.object);
|
||||
layout.deinit();
|
||||
}
|
||||
}.deinit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// import and export of `Layout` implementations
|
||||
pub const HContainer = @import("layout/HContainer.zig").Layout(Event, Element, Renderer);
|
||||
pub const HStack = @import("layout/HStack.zig").Layout(Event, Element, Renderer);
|
||||
pub const VContainer = @import("layout/VContainer.zig").Layout(Event, Element, Renderer);
|
||||
pub const VStack = @import("layout/VStack.zig").Layout(Event, Element, Renderer);
|
||||
pub const Padding = @import("layout/Padding.zig").Layout(Event, Element, Renderer);
|
||||
pub const Margin = @import("layout/Margin.zig").Layout(Event, Element, Renderer);
|
||||
pub const Framing = @import("layout/Framing.zig").Layout(Event, Element, Renderer);
|
||||
};
|
||||
// test layout implementation satisfies the interface
|
||||
comptime Type.Interface.satisfiedBy(Type);
|
||||
comptime Type.Interface.satisfiedBy(Type.HContainer);
|
||||
comptime Type.Interface.satisfiedBy(Type.HStack);
|
||||
comptime Type.Interface.satisfiedBy(Type.VContainer);
|
||||
comptime Type.Interface.satisfiedBy(Type.VStack);
|
||||
comptime Type.Interface.satisfiedBy(Type.Padding);
|
||||
comptime Type.Interface.satisfiedBy(Type.Margin);
|
||||
comptime Type.Interface.satisfiedBy(Type.Framing);
|
||||
return Type;
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
//! Framing layout for a nested `Layout`s or `Widget`s.
|
||||
//!
|
||||
//! # Example
|
||||
//! ...
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Key = terminal.Key;
|
||||
const Style = terminal.Cell.Style;
|
||||
|
||||
const log = std.log.scoped(.layout_framing);
|
||||
|
||||
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!isTaggedUnion(Element)) {
|
||||
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) {
|
||||
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) {
|
||||
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[1].name);
|
||||
}
|
||||
const Events = std.ArrayList(Event);
|
||||
return struct {
|
||||
size: terminal.Size = undefined,
|
||||
require_render: bool = true,
|
||||
element: Element = undefined,
|
||||
events: Events = undefined,
|
||||
config: Config = undefined,
|
||||
|
||||
const Config = struct {
|
||||
style: Style = .{ .fg = .default },
|
||||
frame: Frame = .round,
|
||||
title: Title = .{},
|
||||
|
||||
const Title = struct {
|
||||
str: []const u8 = &.{},
|
||||
style: Style = .{ .fg = .default },
|
||||
};
|
||||
|
||||
const Frame = enum {
|
||||
round,
|
||||
square,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) @This() {
|
||||
return .{
|
||||
.config = config,
|
||||
.element = element,
|
||||
.events = Events.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !*Events {
|
||||
this.events.clearRetainingCapacity();
|
||||
// order is important
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
this.require_render = true;
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
// adjust size according to the containing elements
|
||||
const sub_event: Event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col + 1,
|
||||
.row = size.anchor.row + 1,
|
||||
},
|
||||
.cols = size.cols -| 2,
|
||||
.rows = size.rows -| 2,
|
||||
},
|
||||
};
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(sub_event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(sub_event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
return &this.events;
|
||||
}
|
||||
|
||||
const round_frame = .{ "╭", "─", "╮", "│", "╰", "╯" };
|
||||
const square_frame = .{ "┌", "─", "┐", "│", "└", "┘" };
|
||||
|
||||
fn renderFrame(this: *@This(), renderer: *Renderer) !void {
|
||||
// FIXME: use renderer instead!
|
||||
_ = renderer;
|
||||
const frame = switch (this.config.frame) {
|
||||
.round => round_frame,
|
||||
.square => square_frame,
|
||||
};
|
||||
std.debug.assert(frame.len == 6);
|
||||
// render top: +---+
|
||||
try terminal.setCursorPosition(this.size.anchor);
|
||||
const writer = terminal.writer();
|
||||
try this.config.style.value(writer, frame[0]);
|
||||
if (this.config.title.str.len > 0) {
|
||||
try this.config.title.style.value(writer, this.config.title.str);
|
||||
}
|
||||
for (0..this.size.cols -| 2 -| this.config.title.str.len) |_| {
|
||||
try this.config.style.value(writer, frame[1]);
|
||||
}
|
||||
try this.config.style.value(writer, frame[2]);
|
||||
// render left: |
|
||||
for (1..this.size.rows -| 1) |r| {
|
||||
const row: u16 = @truncate(r);
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = this.size.anchor.col,
|
||||
.row = this.size.anchor.row + row,
|
||||
});
|
||||
try this.config.style.value(writer, frame[3]);
|
||||
}
|
||||
// render right: |
|
||||
for (1..this.size.rows -| 1) |r| {
|
||||
const row: u16 = @truncate(r);
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = this.size.anchor.col + this.size.cols -| 1,
|
||||
.row = this.size.anchor.row + row,
|
||||
});
|
||||
try this.config.style.value(writer, frame[3]);
|
||||
}
|
||||
// render bottom: +---+
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = this.size.anchor.col,
|
||||
.row = this.size.anchor.row + this.size.rows - 1,
|
||||
});
|
||||
try this.config.style.value(writer, frame[4]);
|
||||
for (0..this.size.cols -| 2) |_| {
|
||||
try this.config.style.value(writer, frame[1]);
|
||||
}
|
||||
try this.config.style.value(writer, frame[5]);
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
if (this.require_render) {
|
||||
try renderer.clear(this.size);
|
||||
try this.renderFrame(renderer);
|
||||
this.require_render = false;
|
||||
}
|
||||
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
//! Horizontal Container layout for nested `Layout`s and/or `Widget`s.
|
||||
//! The contained elements are sized according to the provided configuration.
|
||||
//! For an evenly spaced horizontal stacking see the `HStack` layout.
|
||||
//!
|
||||
//! # Example
|
||||
//! ...
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Key = terminal.Key;
|
||||
|
||||
const log = std.log.scoped(.layout_hcontainer);
|
||||
|
||||
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!isTaggedUnion(Element)) {
|
||||
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) {
|
||||
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) {
|
||||
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[1].name);
|
||||
}
|
||||
const Container = struct {
|
||||
element: Element,
|
||||
container_size: u8, // 0 - 100 %
|
||||
};
|
||||
const Containers = std.ArrayList(Container);
|
||||
const LayoutType = @typeInfo(Element).Union.fields[0].type;
|
||||
const WidgetType = @typeInfo(Element).Union.fields[1].type;
|
||||
const Events = std.ArrayList(Event);
|
||||
return struct {
|
||||
// TODO: current focused `Element`?
|
||||
size: terminal.Size = undefined,
|
||||
containers: Containers = undefined,
|
||||
events: Events = undefined,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, children: anytype) @This() {
|
||||
const ArgsType = @TypeOf(children);
|
||||
const args_type_info = @typeInfo(ArgsType);
|
||||
if (args_type_info != .Struct) {
|
||||
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
|
||||
}
|
||||
comptime var total_size = 0;
|
||||
const fields_info = args_type_info.Struct.fields;
|
||||
var containers = Containers.initCapacity(allocator, fields_info.len) catch @panic("OOM");
|
||||
inline for (comptime fields_info) |field| {
|
||||
const child = @field(children, field.name);
|
||||
const ChildType = @TypeOf(child);
|
||||
const child_type_info = @typeInfo(ChildType);
|
||||
if (child_type_info != .Struct) {
|
||||
@compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType));
|
||||
}
|
||||
const child_fields = child_type_info.Struct.fields;
|
||||
if (child_fields.len != 2) {
|
||||
@compileError("expected nested tuple or struct to have exactly 2 fields, but found " ++ child_fields.len);
|
||||
}
|
||||
const element = @field(child, child_fields[0].name);
|
||||
const ElementType = @TypeOf(element);
|
||||
const element_size = @field(child, child_fields[1].name);
|
||||
const ElementSizeType = @TypeOf(element_size);
|
||||
if (ElementSizeType != u8 and ElementSizeType != comptime_int) {
|
||||
@compileError("expected an u8 or comptime_int as second argument of nested tuple or struct child, but found " ++ @typeName(ElementSizeType));
|
||||
}
|
||||
total_size += element_size;
|
||||
if (total_size > 100) {
|
||||
@compileError("cannot place element: " ++ child_fields[0].name ++ " as total size of used container elements would overflow");
|
||||
}
|
||||
if (ElementType == WidgetType) {
|
||||
containers.append(.{
|
||||
.element = .{ .widget = element },
|
||||
.container_size = element_size,
|
||||
}) catch {};
|
||||
continue;
|
||||
}
|
||||
if (ElementType == LayoutType) {
|
||||
containers.append(.{
|
||||
.element = .{ .layout = element },
|
||||
.container_size = element_size,
|
||||
}) catch {};
|
||||
continue;
|
||||
}
|
||||
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
|
||||
}
|
||||
return .{
|
||||
.containers = containers,
|
||||
.events = Events.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
this.containers.deinit();
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !*Events {
|
||||
this.events.clearRetainingCapacity();
|
||||
// order is important
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
// adjust size according to the containing elements
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
// adjust size according to the container size
|
||||
var offset: u16 = 0;
|
||||
for (this.containers.items) |*container| {
|
||||
const cols = @divTrunc(size.cols * container.container_size, 100);
|
||||
const sub_event: Event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col + offset,
|
||||
.row = size.anchor.row,
|
||||
},
|
||||
.cols = cols,
|
||||
.rows = size.rows,
|
||||
},
|
||||
};
|
||||
offset += cols;
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(sub_event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(sub_event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return &this.events;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
//! Horizontal Stacking layout for nested `Layout`s and/or `Widget`s.
|
||||
//!
|
||||
//! # Example
|
||||
//! ...
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Key = terminal.Key;
|
||||
|
||||
const log = std.log.scoped(.layout_hstack);
|
||||
|
||||
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!isTaggedUnion(Element)) {
|
||||
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) {
|
||||
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) {
|
||||
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[1].name);
|
||||
}
|
||||
const Elements = std.ArrayList(Element);
|
||||
const LayoutType = @typeInfo(Element).Union.fields[0].type;
|
||||
const WidgetType = @typeInfo(Element).Union.fields[1].type;
|
||||
const Events = std.ArrayList(Event);
|
||||
return struct {
|
||||
// TODO: current focused `Element`?
|
||||
size: terminal.Size = undefined,
|
||||
elements: Elements = undefined,
|
||||
events: Events = undefined,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, children: anytype) @This() {
|
||||
const ArgsType = @TypeOf(children);
|
||||
const args_type_info = @typeInfo(ArgsType);
|
||||
if (args_type_info != .Struct) {
|
||||
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
|
||||
}
|
||||
const fields_info = args_type_info.Struct.fields;
|
||||
var elements = Elements.initCapacity(allocator, fields_info.len) catch @panic("OOM");
|
||||
inline for (comptime fields_info) |field| {
|
||||
const child = @field(children, field.name);
|
||||
const ChildType = @TypeOf(child);
|
||||
if (ChildType == WidgetType) {
|
||||
elements.append(.{ .widget = child }) catch {};
|
||||
continue;
|
||||
}
|
||||
if (ChildType == LayoutType) {
|
||||
elements.append(.{ .layout = child }) catch {};
|
||||
continue;
|
||||
}
|
||||
@compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
|
||||
}
|
||||
return .{
|
||||
.elements = elements,
|
||||
.events = Events.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
for (this.elements.items) |*element| {
|
||||
switch (element.*) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
this.elements.deinit();
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !*Events {
|
||||
this.events.clearRetainingCapacity();
|
||||
// order is important
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
// adjust size according to the containing elements
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
const len: u16 = @truncate(this.elements.items.len);
|
||||
const element_cols = @divTrunc(size.cols, len);
|
||||
var overflow = size.cols % len;
|
||||
var offset: u16 = 0;
|
||||
// adjust size according to the containing elements
|
||||
for (this.elements.items) |*element| {
|
||||
var cols = element_cols;
|
||||
if (overflow > 0) {
|
||||
overflow -|= 1;
|
||||
cols += 1;
|
||||
}
|
||||
const sub_event: Event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col + offset,
|
||||
.row = size.anchor.row,
|
||||
},
|
||||
.cols = cols,
|
||||
.rows = size.rows,
|
||||
},
|
||||
};
|
||||
offset += cols;
|
||||
switch (element.*) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(sub_event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(sub_event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {
|
||||
for (this.elements.items) |*element| {
|
||||
switch (element.*) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return &this.events;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
for (this.elements.items) |*element| {
|
||||
switch (element.*) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
//! Margin layout for a nested `Layout`s or `Widget`s.
|
||||
//!
|
||||
//! # Example
|
||||
//! ...
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Key = terminal.Key;
|
||||
|
||||
const log = std.log.scoped(.layout_margin);
|
||||
|
||||
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!isTaggedUnion(Element)) {
|
||||
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) {
|
||||
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) {
|
||||
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[1].name);
|
||||
}
|
||||
const Events = std.ArrayList(Event);
|
||||
return struct {
|
||||
size: terminal.Size = undefined,
|
||||
require_render: bool = false,
|
||||
element: Element = undefined,
|
||||
events: Events = undefined,
|
||||
config: Config = undefined,
|
||||
|
||||
const Config = struct {
|
||||
margin: ?u8 = undefined,
|
||||
left: u8 = 0,
|
||||
right: u8 = 0,
|
||||
top: u8 = 0,
|
||||
bottom: u8 = 0,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) @This() {
|
||||
if (config.margin) |margin| {
|
||||
std.debug.assert(margin <= 50);
|
||||
} else {
|
||||
std.debug.assert(config.left + config.right < 100);
|
||||
std.debug.assert(config.top + config.bottom < 100);
|
||||
}
|
||||
return .{
|
||||
.config = config,
|
||||
.element = element,
|
||||
.events = Events.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !*Events {
|
||||
this.events.clearRetainingCapacity();
|
||||
// order is important
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
this.require_render = true;
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
var sub_event: Event = undefined;
|
||||
if (this.config.margin) |margin| {
|
||||
// used overall margin
|
||||
const h_margin: u16 = @divTrunc(margin * size.cols, 100);
|
||||
const v_margin: u16 = @divFloor(margin * size.rows, 100);
|
||||
sub_event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col + h_margin,
|
||||
.row = size.anchor.row + v_margin,
|
||||
},
|
||||
.cols = size.cols -| (h_margin * 2),
|
||||
.rows = size.rows -| (v_margin * 2),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// use all for directions individually
|
||||
const left_margin: u16 = @divFloor(this.config.left * size.cols, 100);
|
||||
const right_margin: u16 = @divFloor(this.config.right * size.cols, 100);
|
||||
const top_margin: u16 = @divFloor(this.config.top * size.rows, 100);
|
||||
const bottom_margin: u16 = @divFloor(this.config.bottom * size.rows, 100);
|
||||
sub_event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col + left_margin,
|
||||
.row = size.anchor.row + top_margin,
|
||||
},
|
||||
.cols = size.cols -| left_margin -| right_margin,
|
||||
.rows = size.rows -| top_margin -| bottom_margin,
|
||||
},
|
||||
};
|
||||
}
|
||||
// adjust size according to the containing elements
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(sub_event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(sub_event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
return &this.events;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
if (this.require_render) {
|
||||
try renderer.clear(this.size);
|
||||
this.require_render = false;
|
||||
}
|
||||
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
//! Padding layout for a nested `Layout`s or `Widget`s.
|
||||
//!
|
||||
//! # Example
|
||||
//! ...
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Key = terminal.Key;
|
||||
|
||||
const log = std.log.scoped(.layout_padding);
|
||||
|
||||
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!isTaggedUnion(Element)) {
|
||||
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) {
|
||||
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) {
|
||||
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[1].name);
|
||||
}
|
||||
const Events = std.ArrayList(Event);
|
||||
return struct {
|
||||
size: terminal.Size = undefined,
|
||||
require_render: bool = false,
|
||||
element: Element = undefined,
|
||||
events: Events = undefined,
|
||||
config: Config = undefined,
|
||||
|
||||
const Config = struct {
|
||||
padding: ?u16 = undefined,
|
||||
left: u16 = 0,
|
||||
right: u16 = 0,
|
||||
top: u16 = 0,
|
||||
bottom: u16 = 0,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) @This() {
|
||||
return .{
|
||||
.config = config,
|
||||
.element = element,
|
||||
.events = Events.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !*Events {
|
||||
this.events.clearRetainingCapacity();
|
||||
// order is important
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
this.require_render = true;
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
var sub_event: Event = undefined;
|
||||
if (this.config.padding) |padding| {
|
||||
// used overall padding
|
||||
sub_event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col + padding,
|
||||
.row = size.anchor.row + padding,
|
||||
},
|
||||
.cols = size.cols -| (padding * 2),
|
||||
.rows = size.rows -| (padding * 2),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// use all for directions individually
|
||||
sub_event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col + this.config.left,
|
||||
.row = size.anchor.row + this.config.top,
|
||||
},
|
||||
.cols = size.cols -| this.config.left -| this.config.right,
|
||||
.rows = size.rows -| this.config.top -| this.config.bottom,
|
||||
},
|
||||
};
|
||||
}
|
||||
// adjust size according to the containing elements
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(sub_event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(sub_event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
return &this.events;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
if (this.require_render) {
|
||||
try renderer.clear(this.size);
|
||||
this.require_render = false;
|
||||
}
|
||||
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
//! Vertical Container layout for nested `Layout`s and/or `Widget`s.
|
||||
//! The contained elements are sized according to the provided configuration.
|
||||
//! For an evenly spaced vertical stacking see the `VStack` layout.
|
||||
//!
|
||||
//! # Example
|
||||
//! ...
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Key = terminal.Key;
|
||||
|
||||
const log = std.log.scoped(.layout_vcontainer);
|
||||
|
||||
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!isTaggedUnion(Element)) {
|
||||
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) {
|
||||
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) {
|
||||
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[1].name);
|
||||
}
|
||||
const Container = struct {
|
||||
element: Element,
|
||||
container_size: u8, // 0 - 100 %
|
||||
};
|
||||
const Containers = std.ArrayList(Container);
|
||||
const LayoutType = @typeInfo(Element).Union.fields[0].type;
|
||||
const WidgetType = @typeInfo(Element).Union.fields[1].type;
|
||||
const Events = std.ArrayList(Event);
|
||||
return struct {
|
||||
// TODO: current focused `Element`?
|
||||
size: terminal.Size = undefined,
|
||||
containers: Containers = undefined,
|
||||
events: Events = undefined,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, children: anytype) @This() {
|
||||
const ArgsType = @TypeOf(children);
|
||||
const args_type_info = @typeInfo(ArgsType);
|
||||
if (args_type_info != .Struct) {
|
||||
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
|
||||
}
|
||||
comptime var total_size = 0;
|
||||
const fields_info = args_type_info.Struct.fields;
|
||||
var containers = Containers.initCapacity(allocator, fields_info.len) catch @panic("OOM");
|
||||
inline for (comptime fields_info) |field| {
|
||||
const child = @field(children, field.name);
|
||||
const ChildType = @TypeOf(child);
|
||||
const child_type_info = @typeInfo(ChildType);
|
||||
if (child_type_info != .Struct) {
|
||||
@compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType));
|
||||
}
|
||||
const child_fields = child_type_info.Struct.fields;
|
||||
if (child_fields.len != 2) {
|
||||
@compileError("expected nested tuple or struct to have exactly 2 fields, but found " ++ child_fields.len);
|
||||
}
|
||||
const element = @field(child, child_fields[0].name);
|
||||
const ElementType = @TypeOf(element);
|
||||
const element_size = @field(child, child_fields[1].name);
|
||||
const ElementSizeType = @TypeOf(element_size);
|
||||
if (ElementSizeType != u8 and ElementSizeType != comptime_int) {
|
||||
@compileError("expected an u8 or comptime_int as second argument of nested tuple or struct child, but found " ++ @typeName(ElementSizeType));
|
||||
}
|
||||
total_size += element_size;
|
||||
if (total_size > 100) {
|
||||
@compileError("cannot place element: " ++ child_fields[0].name ++ " as total size of used container elements would overflow");
|
||||
}
|
||||
if (ElementType == WidgetType) {
|
||||
containers.append(.{
|
||||
.element = .{ .widget = element },
|
||||
.container_size = element_size,
|
||||
}) catch {};
|
||||
continue;
|
||||
}
|
||||
if (ElementType == LayoutType) {
|
||||
containers.append(.{
|
||||
.element = .{ .layout = element },
|
||||
.container_size = element_size,
|
||||
}) catch {};
|
||||
continue;
|
||||
}
|
||||
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
|
||||
}
|
||||
return .{
|
||||
.containers = containers,
|
||||
.events = Events.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
this.containers.deinit();
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !*Events {
|
||||
this.events.clearRetainingCapacity();
|
||||
// order is important
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
// adjust size according to the containing elements
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
// adjust size according to the container size
|
||||
var offset: u16 = 0;
|
||||
for (this.containers.items) |*container| {
|
||||
const rows = @divTrunc(size.rows * container.container_size, 100);
|
||||
const sub_event: Event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col,
|
||||
.row = size.anchor.row + offset,
|
||||
},
|
||||
.cols = size.cols,
|
||||
.rows = rows,
|
||||
},
|
||||
};
|
||||
offset += rows;
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(sub_event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(sub_event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return &this.events;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
//! Vertical Stacking layout for nested `Layout`s and/or `Widget`s.
|
||||
//!
|
||||
//! # Example
|
||||
//! ...
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Key = terminal.Key;
|
||||
|
||||
const log = std.log.scoped(.layout_vstack);
|
||||
|
||||
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!isTaggedUnion(Element)) {
|
||||
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) {
|
||||
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) {
|
||||
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[1].name);
|
||||
}
|
||||
const Elements = std.ArrayList(Element);
|
||||
const LayoutType = @typeInfo(Element).Union.fields[0].type;
|
||||
const WidgetType = @typeInfo(Element).Union.fields[1].type;
|
||||
const Events = std.ArrayList(Event);
|
||||
return struct {
|
||||
// TODO: current focused `Element`?
|
||||
size: terminal.Size = undefined,
|
||||
elements: Elements = undefined,
|
||||
events: Events = undefined,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, children: anytype) @This() {
|
||||
const ArgsType = @TypeOf(children);
|
||||
const args_type_info = @typeInfo(ArgsType);
|
||||
if (args_type_info != .Struct) {
|
||||
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
|
||||
}
|
||||
const fields_info = args_type_info.Struct.fields;
|
||||
var elements = Elements.initCapacity(allocator, fields_info.len) catch @panic("OOM");
|
||||
inline for (comptime fields_info) |field| {
|
||||
const child = @field(children, field.name);
|
||||
const ChildType = @TypeOf(child);
|
||||
if (ChildType == WidgetType) {
|
||||
elements.append(.{ .widget = child }) catch {};
|
||||
continue;
|
||||
}
|
||||
if (ChildType == LayoutType) {
|
||||
elements.append(.{ .layout = child }) catch {};
|
||||
continue;
|
||||
}
|
||||
@compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
|
||||
}
|
||||
return .{
|
||||
.elements = elements,
|
||||
.events = Events.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
for (this.elements.items) |*element| {
|
||||
switch (element.*) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
this.elements.deinit();
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !*Events {
|
||||
this.events.clearRetainingCapacity();
|
||||
// order is important
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
const len: u16 = @truncate(this.elements.items.len);
|
||||
const element_rows = @divTrunc(size.rows, len);
|
||||
var overflow = size.rows % len;
|
||||
var offset: u16 = 0;
|
||||
// adjust size according to the containing elements
|
||||
for (this.elements.items) |*element| {
|
||||
var rows = element_rows;
|
||||
if (overflow > 0) {
|
||||
overflow -|= 1;
|
||||
rows += 1;
|
||||
}
|
||||
const sub_event: Event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col,
|
||||
.row = size.anchor.row + offset,
|
||||
},
|
||||
.cols = size.cols,
|
||||
.rows = rows,
|
||||
},
|
||||
};
|
||||
offset += rows;
|
||||
switch (element.*) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(sub_event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(sub_event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {
|
||||
for (this.elements.items) |*element| {
|
||||
switch (element.*) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return &this.events;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
for (this.elements.items) |*element| {
|
||||
switch (element.*) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
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;
|
||||
}
|
||||
193
src/render.zig
193
src/render.zig
@@ -1,105 +1,110 @@
|
||||
//! Renderer which holds the screen to compare with the previous screen for efficient rendering.
|
||||
//! Each renderer should at least implement these functions:
|
||||
//! - resize(this: *@This(), size: Size) void {}
|
||||
//! - clear(this: *@This(), size: Size) !void {}
|
||||
//! - render(this: *@This(), size: Size, contents: []u8) !void {}
|
||||
//!
|
||||
//! Each `Renderer` should be able to be used interchangeable without having to
|
||||
//! change any code of any `Layout` or `Widget`. The only change should be the
|
||||
//! passed type to `zterm.App` _R_ parameter.
|
||||
const std = @import("std");
|
||||
const terminal = @import("terminal.zig");
|
||||
|
||||
const Cells = []const terminal.Cell;
|
||||
const Position = terminal.Position;
|
||||
const Size = terminal.Size;
|
||||
const Cell = @import("cell.zig");
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
pub fn Direct(comptime fullscreen: bool) type {
|
||||
const log = std.log.scoped(.renderer_direct);
|
||||
_ = log;
|
||||
_ = fullscreen;
|
||||
return struct {
|
||||
pub fn resize(this: *@This(), size: Size) void {
|
||||
_ = this;
|
||||
_ = size;
|
||||
/// Double-buffered intermediate rendering pipeline
|
||||
pub const Buffered = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
created: bool,
|
||||
size: Point,
|
||||
screen: []Cell,
|
||||
virtual_screen: []Cell,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) @This() {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.created = false,
|
||||
.size = undefined,
|
||||
.screen = undefined,
|
||||
.virtual_screen = undefined,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
if (this.created) {
|
||||
this.allocator.free(this.screen);
|
||||
this.allocator.free(this.virtual_screen);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(this: *@This(), size: Size) !void {
|
||||
_ = this;
|
||||
// TODO: this should instead by dynamic and correct of size (terminal could be too large currently)
|
||||
std.debug.assert(1028 > size.cols);
|
||||
var buf: [1028]u8 = undefined;
|
||||
@memset(buf[0..], ' ');
|
||||
for (0..size.rows) |r| {
|
||||
const row: u16 = @truncate(r);
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = size.anchor.col,
|
||||
.row = size.anchor.row + row,
|
||||
});
|
||||
_ = try terminal.write(buf[0..size.cols]);
|
||||
pub fn resize(this: *@This()) !void {
|
||||
const size = terminal.getTerminalSize();
|
||||
if (std.meta.eql(this.size, size)) return;
|
||||
|
||||
this.size = size;
|
||||
const n = @as(usize, this.size.x) * @as(usize, this.size.y);
|
||||
|
||||
if (!this.created) {
|
||||
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
||||
|
||||
this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
||||
@memset(this.virtual_screen, .{});
|
||||
|
||||
this.created = true;
|
||||
} else {
|
||||
this.allocator.free(this.screen);
|
||||
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
||||
|
||||
this.allocator.free(this.virtual_screen);
|
||||
this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
||||
@memset(this.virtual_screen, .{});
|
||||
}
|
||||
try this.clear();
|
||||
}
|
||||
|
||||
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
|
||||
pub fn clear(this: *@This()) !void {
|
||||
try terminal.clearScreen();
|
||||
@memset(this.screen, .{});
|
||||
}
|
||||
|
||||
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
|
||||
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
|
||||
const size: Point = container.size;
|
||||
const origin: Point = container.origin;
|
||||
const cells: []const Cell = try container.content();
|
||||
|
||||
if (cells.len == 0) return;
|
||||
|
||||
var idx: usize = 0;
|
||||
var vs = this.virtual_screen;
|
||||
const anchor: usize = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
|
||||
|
||||
blk: for (0..size.y) |row| {
|
||||
for (0..size.x) |col| {
|
||||
vs[anchor + (row * this.size.x) + col] = cells[idx];
|
||||
idx += 1;
|
||||
|
||||
if (cells.len == idx) break :blk;
|
||||
}
|
||||
}
|
||||
// free immediately
|
||||
container.allocator.free(cells);
|
||||
|
||||
pub fn render(this: *@This(), size: Size, cells: Cells) !void {
|
||||
_ = this;
|
||||
try terminal.setCursorPosition(size.anchor);
|
||||
var row: u16 = 0;
|
||||
var remaining_cols = size.cols;
|
||||
const writer = terminal.writer();
|
||||
for (cells) |cell| {
|
||||
var idx: usize = 0;
|
||||
print_cell: while (true) {
|
||||
const cell_len = cell.len(idx);
|
||||
if (cell_len > remaining_cols) {
|
||||
const result = try cell.writeUpToNewline(writer, idx, idx + remaining_cols);
|
||||
row += 1;
|
||||
if (row >= size.rows) {
|
||||
return; // we are done
|
||||
}
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = size.anchor.col,
|
||||
.row = size.anchor.row + row,
|
||||
});
|
||||
remaining_cols = size.cols;
|
||||
idx = result.idx;
|
||||
if (result.newline) {
|
||||
idx += 1; // skip over newline
|
||||
} else {
|
||||
// there is still content to the newline (which will not be printed)
|
||||
for (idx..cell.content.len) |i| {
|
||||
if (cell.content[i] == '\n') {
|
||||
idx = i + 1;
|
||||
continue :print_cell;
|
||||
}
|
||||
}
|
||||
break; // go to next cell (as we went to the end of the cell and do not print on the next line)
|
||||
}
|
||||
} else {
|
||||
// print rest of cell
|
||||
const result = try cell.writeUpToNewline(writer, idx, idx + cell_len);
|
||||
if (result.newline) {
|
||||
row += 1;
|
||||
if (row >= size.rows) {
|
||||
return; // we are done
|
||||
}
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = size.anchor.col,
|
||||
.row = size.anchor.row + row,
|
||||
});
|
||||
remaining_cols = size.cols;
|
||||
idx = result.idx + 1; // skip over newline
|
||||
} else {
|
||||
remaining_cols -= @truncate(cell_len - idx);
|
||||
idx = 0;
|
||||
break; // go to next cell
|
||||
}
|
||||
}
|
||||
// written all cell contents
|
||||
if (idx >= cell.content.len) {
|
||||
break; // go to next cell
|
||||
}
|
||||
}
|
||||
for (container.elements.items) |*element| try this.render(T, element);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// TODO measure timings of rendered frames?
|
||||
const writer = terminal.writer();
|
||||
const s = this.screen;
|
||||
const vs = this.virtual_screen;
|
||||
for (0..this.size.y) |row| {
|
||||
for (0..this.size.x) |col| {
|
||||
const idx = (row * this.size.x) + col;
|
||||
const cs = s[idx];
|
||||
const cvs = vs[idx];
|
||||
if (cs.eql(cvs)) continue;
|
||||
|
||||
// render differences found in virtual screen
|
||||
try terminal.setCursorPosition(.{ .y = @truncate(row + 1), .x = @truncate(col + 1) });
|
||||
try cvs.value(writer);
|
||||
// update screen to be the virtual screen for the next frame
|
||||
s[idx] = vs[idx];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
72
src/style.zig
Normal file
72
src/style.zig
Normal file
@@ -0,0 +1,72 @@
|
||||
//! Helper function collection to provide ascii encodings for styling outputs.
|
||||
//! Stylings are implemented such that they can be nested in anyway to support
|
||||
//! multiple styles (i.e. bold and italic).
|
||||
//!
|
||||
//! Stylings however also include highlighting for specific terminal capabilities.
|
||||
//! For example url highlighting.
|
||||
|
||||
// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License)
|
||||
// with slight modifications
|
||||
const std = @import("std");
|
||||
|
||||
const Color = @import("color.zig").Color;
|
||||
|
||||
pub const Style = @This();
|
||||
|
||||
pub const Underline = enum {
|
||||
off,
|
||||
single,
|
||||
double,
|
||||
curly,
|
||||
dotted,
|
||||
dashed,
|
||||
};
|
||||
|
||||
pub const Emphasis = enum(u8) {
|
||||
reset = 0,
|
||||
bold = 1,
|
||||
dim,
|
||||
italic,
|
||||
underline,
|
||||
blink,
|
||||
invert = 7,
|
||||
hidden,
|
||||
strikethrough,
|
||||
};
|
||||
|
||||
fg: Color = .default,
|
||||
bg: Color = .default,
|
||||
ul: Color = .default,
|
||||
ul_style: Underline = .off,
|
||||
emphasis: []const Emphasis,
|
||||
|
||||
pub fn eql(this: Style, other: Style) bool {
|
||||
return std.meta.eql(this, other);
|
||||
}
|
||||
|
||||
pub fn value(this: Style, writer: anytype, cp: u21) !void {
|
||||
var buffer: [4]u8 = undefined;
|
||||
const bytes = try std.unicode.utf8Encode(cp, &buffer);
|
||||
std.debug.assert(bytes > 0);
|
||||
// build ansi sequence for 256 colors ...
|
||||
// foreground
|
||||
try std.fmt.format(writer, "\x1b[", .{});
|
||||
try this.fg.write(writer, .fg);
|
||||
// background
|
||||
try std.fmt.format(writer, ";", .{});
|
||||
try this.bg.write(writer, .bg);
|
||||
// underline
|
||||
// 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 this.ul.write(writer, .ul);
|
||||
// append styles (aka attributes like bold, italic, strikethrough, etc.)
|
||||
for (this.emphasis) |attribute| try std.fmt.format(writer, ";{d}", .{@intFromEnum(attribute)});
|
||||
try std.fmt.format(writer, "m", .{});
|
||||
// content
|
||||
try std.fmt.format(writer, "{s}", .{buffer[0..bytes]});
|
||||
try std.fmt.format(writer, "\x1b[0m", .{});
|
||||
}
|
||||
|
||||
// TODO implement helper functions for terminal capabilities:
|
||||
// - links / url display (osc 8)
|
||||
// - show / hide cursor?
|
||||
@@ -1,9 +1,12 @@
|
||||
const std = @import("std");
|
||||
pub const Key = @import("terminal/Key.zig");
|
||||
pub const Size = @import("terminal/Size.zig");
|
||||
pub const Position = @import("terminal/Position.zig");
|
||||
pub const Cell = @import("terminal/Cell.zig");
|
||||
pub const code_point = @import("code_point");
|
||||
const code_point = @import("code_point");
|
||||
const ctlseqs = @import("ctlseqs.zig");
|
||||
const input = @import("input.zig");
|
||||
|
||||
const Key = input.Key;
|
||||
const Point = @import("point.zig").Point;
|
||||
const Size = @import("point.zig").Point;
|
||||
const Cell = @import("cell.zig");
|
||||
|
||||
const log = std.log.scoped(.terminal);
|
||||
|
||||
@@ -20,39 +23,47 @@ pub const ReportMode = enum {
|
||||
pub fn getTerminalSize() Size {
|
||||
var ws: std.posix.winsize = undefined;
|
||||
_ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws));
|
||||
return .{ .cols = ws.ws_col, .rows = ws.ws_row };
|
||||
return .{ .x = ws.col, .y = ws.row };
|
||||
}
|
||||
|
||||
pub fn saveScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?47h");
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.save_screen);
|
||||
}
|
||||
|
||||
pub fn restoreScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?47l");
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.restore_screen);
|
||||
}
|
||||
|
||||
pub fn enterAltScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?1049h");
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.smcup);
|
||||
}
|
||||
|
||||
pub fn existAltScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?1049l");
|
||||
pub fn exitAltScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.rmcup);
|
||||
}
|
||||
|
||||
pub fn clearScreen() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[2J");
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.clear_screen);
|
||||
}
|
||||
|
||||
pub fn hideCursor() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?25l");
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.hide_cursor);
|
||||
}
|
||||
|
||||
pub fn showCursor() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?25h");
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.show_cursor);
|
||||
}
|
||||
|
||||
pub fn setCursorPositionHome() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[H");
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.home);
|
||||
}
|
||||
|
||||
pub fn enableMouseSupport() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.mouse_set);
|
||||
}
|
||||
|
||||
pub fn disableMouseSupport() !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.mouse_reset);
|
||||
}
|
||||
|
||||
pub fn read(buf: []u8) !usize {
|
||||
@@ -78,13 +89,13 @@ pub fn writer() Writer {
|
||||
return .{ .context = .{} };
|
||||
}
|
||||
|
||||
pub fn setCursorPosition(pos: Position) !void {
|
||||
pub fn setCursorPosition(pos: Point) !void {
|
||||
var buf: [64]u8 = undefined;
|
||||
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col });
|
||||
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y, pos.x });
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
|
||||
}
|
||||
|
||||
pub fn getCursorPosition() !Position {
|
||||
pub fn getCursorPosition() !Size.Position {
|
||||
// Needs Raw mode (no wait for \n) to work properly cause
|
||||
// control sequence will not be written without it.
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[6n");
|
||||
@@ -126,8 +137,8 @@ pub fn getCursorPosition() !Position {
|
||||
}
|
||||
|
||||
return .{
|
||||
.row = try std.fmt.parseInt(u16, row[0..ridx], 10),
|
||||
.col = try std.fmt.parseInt(u16, col[0..cidx], 10),
|
||||
.x = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
|
||||
.y = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -227,7 +238,3 @@ fn getReportMode(ps: u8) ReportMode {
|
||||
else => ReportMode.not_recognized,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
_ = Cell;
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
const std = @import("std");
|
||||
pub const Style = @import("Style.zig");
|
||||
|
||||
style: Style = .{},
|
||||
content: []const u8 = undefined,
|
||||
|
||||
pub const Result = struct {
|
||||
idx: usize,
|
||||
newline: bool,
|
||||
};
|
||||
|
||||
pub fn len(this: @This(), start: usize) usize {
|
||||
std.debug.assert(this.content.len > start);
|
||||
return this.content[start..].len;
|
||||
}
|
||||
|
||||
pub fn write(this: @This(), writer: anytype, start: usize, end: usize) !void {
|
||||
std.debug.assert(this.content.len > start);
|
||||
std.debug.assert(this.content.len >= end);
|
||||
std.debug.assert(start < end);
|
||||
|
||||
try this.style.value(writer, this.content[start..end]);
|
||||
}
|
||||
|
||||
pub fn writeUpToNewline(this: @This(), writer: anytype, start: usize, end: usize) !Result {
|
||||
std.debug.assert(this.content.len > start);
|
||||
std.debug.assert(this.content.len >= end);
|
||||
std.debug.assert(start < end);
|
||||
|
||||
for (start..end) |i| {
|
||||
if (this.content[i] == '\n') {
|
||||
if (start < i) {
|
||||
// this is just an empty line with a newline
|
||||
try this.value(writer, start, i);
|
||||
}
|
||||
return .{
|
||||
.idx = i,
|
||||
.newline = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
try this.write(writer, start, end);
|
||||
return .{
|
||||
.idx = end,
|
||||
.newline = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn value(this: @This(), writer: anytype, start: usize, end: usize) !void {
|
||||
std.debug.assert(start < this.content.len);
|
||||
std.debug.assert(this.content.len >= end);
|
||||
std.debug.assert(start < end);
|
||||
try this.style.value(writer, this.content[start..end]);
|
||||
}
|
||||
|
||||
// not really supported
|
||||
pub fn format(this: @This(), writer: anytype, comptime fmt: []const u8, args: anytype) !void {
|
||||
try this.style.format(writer, fmt, args); // NOTE: args should contain this.content[start..end] or this.content
|
||||
}
|
||||
|
||||
test {
|
||||
_ = Style;
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
//! Keybindings and Modifiers for user input detection and selection.
|
||||
const std = @import("std");
|
||||
|
||||
pub const Modifier = struct {
|
||||
shift: bool = false,
|
||||
alt: bool = false,
|
||||
ctrl: bool = false,
|
||||
};
|
||||
|
||||
cp: u21,
|
||||
mod: Modifier = .{},
|
||||
|
||||
/// Compare _this_ `Key` with an _other_ `Key`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Configure `ctrl+c` to quit the application (done in main event loop of the application):
|
||||
///
|
||||
/// ```zig
|
||||
/// switch (event) {
|
||||
/// .quit => break,
|
||||
/// .key => |key| {
|
||||
/// // ctrl+c to quit
|
||||
/// if (terminal.Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
/// app.quit.set();
|
||||
/// }
|
||||
/// },
|
||||
/// else => {},
|
||||
/// }
|
||||
/// ```
|
||||
pub fn matches(this: @This(), other: @This()) bool {
|
||||
return std.meta.eql(this, other);
|
||||
}
|
||||
|
||||
// codepoints for keys
|
||||
pub const tab: u21 = 0x09;
|
||||
pub const enter: u21 = 0x0D;
|
||||
pub const escape: u21 = 0x1B;
|
||||
pub const space: u21 = 0x20;
|
||||
pub const backspace: u21 = 0x7F;
|
||||
|
||||
// kitty key encodings (re-used here)
|
||||
pub const insert: u21 = 57348;
|
||||
pub const delete: u21 = 57349;
|
||||
pub const left: u21 = 57350;
|
||||
pub const right: u21 = 57351;
|
||||
pub const up: u21 = 57352;
|
||||
pub const down: u21 = 57353;
|
||||
pub const page_up: u21 = 57354;
|
||||
pub const page_down: u21 = 57355;
|
||||
pub const home: u21 = 57356;
|
||||
pub const end: u21 = 57357;
|
||||
pub const caps_lock: u21 = 57358;
|
||||
pub const scroll_lock: u21 = 57359;
|
||||
pub const num_lock: u21 = 57360;
|
||||
pub const print_screen: u21 = 57361;
|
||||
pub const pause: u21 = 57362;
|
||||
pub const menu: u21 = 57363;
|
||||
pub const f1: u21 = 57364;
|
||||
pub const f2: u21 = 57365;
|
||||
pub const f3: u21 = 57366;
|
||||
pub const f4: u21 = 57367;
|
||||
pub const f5: u21 = 57368;
|
||||
pub const f6: u21 = 57369;
|
||||
pub const f7: u21 = 57370;
|
||||
pub const f8: u21 = 57371;
|
||||
pub const f9: u21 = 57372;
|
||||
pub const f10: u21 = 57373;
|
||||
pub const f11: u21 = 57374;
|
||||
pub const f12: u21 = 57375;
|
||||
pub const f13: u21 = 57376;
|
||||
pub const f14: u21 = 57377;
|
||||
pub const f15: u21 = 57378;
|
||||
pub const @"f16": u21 = 57379;
|
||||
pub const f17: u21 = 57380;
|
||||
pub const f18: u21 = 57381;
|
||||
pub const f19: u21 = 57382;
|
||||
pub const f20: u21 = 57383;
|
||||
pub const f21: u21 = 57384;
|
||||
pub const f22: u21 = 57385;
|
||||
pub const f23: u21 = 57386;
|
||||
pub const f24: u21 = 57387;
|
||||
pub const f25: u21 = 57388;
|
||||
pub const f26: u21 = 57389;
|
||||
pub const f27: u21 = 57390;
|
||||
pub const f28: u21 = 57391;
|
||||
pub const f29: u21 = 57392;
|
||||
pub const f30: u21 = 57393;
|
||||
pub const f31: u21 = 57394;
|
||||
pub const @"f32": u21 = 57395;
|
||||
pub const f33: u21 = 57396;
|
||||
pub const f34: u21 = 57397;
|
||||
pub const f35: u21 = 57398;
|
||||
pub const kp_0: u21 = 57399;
|
||||
pub const kp_1: u21 = 57400;
|
||||
pub const kp_2: u21 = 57401;
|
||||
pub const kp_3: u21 = 57402;
|
||||
pub const kp_4: u21 = 57403;
|
||||
pub const kp_5: u21 = 57404;
|
||||
pub const kp_6: u21 = 57405;
|
||||
pub const kp_7: u21 = 57406;
|
||||
pub const kp_8: u21 = 57407;
|
||||
pub const kp_9: u21 = 57408;
|
||||
pub const kp_decimal: u21 = 57409;
|
||||
pub const kp_divide: u21 = 57410;
|
||||
pub const kp_multiply: u21 = 57411;
|
||||
pub const kp_subtract: u21 = 57412;
|
||||
pub const kp_add: u21 = 57413;
|
||||
pub const kp_enter: u21 = 57414;
|
||||
pub const kp_equal: u21 = 57415;
|
||||
pub const kp_separator: u21 = 57416;
|
||||
pub const kp_left: u21 = 57417;
|
||||
pub const kp_right: u21 = 57418;
|
||||
pub const kp_up: u21 = 57419;
|
||||
pub const kp_down: u21 = 57420;
|
||||
pub const kp_page_up: u21 = 57421;
|
||||
pub const kp_page_down: u21 = 57422;
|
||||
pub const kp_home: u21 = 57423;
|
||||
pub const kp_end: u21 = 57424;
|
||||
pub const kp_insert: u21 = 57425;
|
||||
pub const kp_delete: u21 = 57426;
|
||||
pub const kp_begin: u21 = 57427;
|
||||
pub const media_play: u21 = 57428;
|
||||
pub const media_pause: u21 = 57429;
|
||||
pub const media_play_pause: u21 = 57430;
|
||||
pub const media_reverse: u21 = 57431;
|
||||
pub const media_stop: u21 = 57432;
|
||||
pub const media_fast_forward: u21 = 57433;
|
||||
pub const media_rewind: u21 = 57434;
|
||||
pub const media_track_next: u21 = 57435;
|
||||
pub const media_track_previous: u21 = 57436;
|
||||
pub const media_record: u21 = 57437;
|
||||
pub const lower_volume: u21 = 57438;
|
||||
pub const raise_volume: u21 = 57439;
|
||||
pub const mute_volume: u21 = 57440;
|
||||
pub const left_shift: u21 = 57441;
|
||||
pub const left_control: u21 = 57442;
|
||||
pub const left_alt: u21 = 57443;
|
||||
pub const left_super: u21 = 57444;
|
||||
pub const left_hyper: u21 = 57445;
|
||||
pub const left_meta: u21 = 57446;
|
||||
pub const right_shift: u21 = 57447;
|
||||
pub const right_control: u21 = 57448;
|
||||
pub const right_alt: u21 = 57449;
|
||||
pub const right_super: u21 = 57450;
|
||||
pub const right_hyper: u21 = 57451;
|
||||
pub const right_meta: u21 = 57452;
|
||||
pub const iso_level_3_shift: u21 = 57453;
|
||||
pub const iso_level_5_shift: u21 = 57454;
|
||||
@@ -1,2 +0,0 @@
|
||||
col: u16,
|
||||
row: u16,
|
||||
@@ -1,5 +0,0 @@
|
||||
const Position = @import("Position.zig");
|
||||
|
||||
anchor: Position = .{ .col = 1, .row = 1 }, // top left corner by default
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
@@ -1,289 +0,0 @@
|
||||
//! Helper function collection to provide ascii encodings for styling outputs.
|
||||
//! Stylings are implemented such that they can be nested in anyway to support
|
||||
//! multiple styles (i.e. bold and italic).
|
||||
//!
|
||||
//! Stylings however also include highlighting for specific terminal capabilities.
|
||||
//! For example url highlighting.
|
||||
|
||||
// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License)
|
||||
// with slight modifications
|
||||
const std = @import("std");
|
||||
const ctlseqs = @import("ctlseqs.zig");
|
||||
|
||||
pub const Underline = enum {
|
||||
off,
|
||||
single,
|
||||
double,
|
||||
curly,
|
||||
dotted,
|
||||
dashed,
|
||||
};
|
||||
|
||||
pub const Color = union(enum) {
|
||||
default,
|
||||
index: u8,
|
||||
rgb: [3]u8,
|
||||
|
||||
pub fn eql(a: @This(), b: @This()) bool {
|
||||
switch (a) {
|
||||
.default => return b == .default,
|
||||
.index => |a_idx| {
|
||||
switch (b) {
|
||||
.index => |b_idx| return a_idx == b_idx,
|
||||
else => return false,
|
||||
}
|
||||
},
|
||||
.rgb => |a_rgb| {
|
||||
switch (b) {
|
||||
.rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and
|
||||
a_rgb[1] == b_rgb[1] and
|
||||
a_rgb[2] == b_rgb[2],
|
||||
else => return false,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rgbFromUint(val: u24) Color {
|
||||
const r_bits = val & 0b11111111_00000000_00000000;
|
||||
const g_bits = val & 0b00000000_11111111_00000000;
|
||||
const b_bits = val & 0b00000000_00000000_11111111;
|
||||
const rgb = [_]u8{
|
||||
@truncate(r_bits >> 16),
|
||||
@truncate(g_bits >> 8),
|
||||
@truncate(b_bits),
|
||||
};
|
||||
return .{ .rgb = rgb };
|
||||
}
|
||||
|
||||
/// parse an XParseColor-style rgb specification into an rgb Color. The spec
|
||||
/// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always
|
||||
/// be the same as the low two bits.
|
||||
pub fn rgbFromSpec(spec: []const u8) !Color {
|
||||
var iter = std.mem.splitScalar(u8, spec, ':');
|
||||
const prefix = iter.next() orelse return error.InvalidColorSpec;
|
||||
if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec;
|
||||
|
||||
const spec_str = iter.next() orelse return error.InvalidColorSpec;
|
||||
|
||||
var spec_iter = std.mem.splitScalar(u8, spec_str, '/');
|
||||
|
||||
const r_raw = spec_iter.next() orelse return error.InvalidColorSpec;
|
||||
if (r_raw.len != 4) return error.InvalidColorSpec;
|
||||
|
||||
const g_raw = spec_iter.next() orelse return error.InvalidColorSpec;
|
||||
if (g_raw.len != 4) return error.InvalidColorSpec;
|
||||
|
||||
const b_raw = spec_iter.next() orelse return error.InvalidColorSpec;
|
||||
if (b_raw.len != 4) return error.InvalidColorSpec;
|
||||
|
||||
const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16);
|
||||
const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16);
|
||||
const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16);
|
||||
|
||||
return .{
|
||||
.rgb = [_]u8{ r, g, b },
|
||||
};
|
||||
}
|
||||
|
||||
test "rgbFromSpec" {
|
||||
const spec = "rgb:aaaa/bbbb/cccc";
|
||||
const actual = try rgbFromSpec(spec);
|
||||
switch (actual) {
|
||||
.rgb => |rgb| {
|
||||
try std.testing.expectEqual(0xAA, rgb[0]);
|
||||
try std.testing.expectEqual(0xBB, rgb[1]);
|
||||
try std.testing.expectEqual(0xCC, rgb[2]);
|
||||
},
|
||||
else => try std.testing.expect(false),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fg: Color = .default,
|
||||
bg: Color = .default,
|
||||
ul: Color = .default,
|
||||
ul_style: Underline = .off,
|
||||
|
||||
bold: bool = false,
|
||||
dim: bool = false,
|
||||
italic: bool = false,
|
||||
blink: bool = false,
|
||||
reverse: bool = false,
|
||||
invisible: bool = false,
|
||||
strikethrough: bool = false,
|
||||
|
||||
fn start(this: @This(), writer: anytype) !void {
|
||||
// foreground
|
||||
switch (this.fg) {
|
||||
.default => try std.fmt.format(writer, ctlseqs.fg_reset, .{}),
|
||||
.index => |idx| {
|
||||
switch (idx) {
|
||||
0...7 => {
|
||||
try std.fmt.format(writer, ctlseqs.fg_base, .{idx});
|
||||
},
|
||||
8...15 => {
|
||||
try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8});
|
||||
},
|
||||
else => {
|
||||
try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx});
|
||||
},
|
||||
}
|
||||
},
|
||||
.rgb => |rgb| {
|
||||
try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] });
|
||||
},
|
||||
}
|
||||
// background
|
||||
switch (this.bg) {
|
||||
.default => try std.fmt.format(writer, ctlseqs.bg_reset, .{}),
|
||||
.index => |idx| {
|
||||
switch (idx) {
|
||||
0...7 => {
|
||||
try std.fmt.format(writer, ctlseqs.bg_base, .{idx});
|
||||
},
|
||||
8...15 => {
|
||||
try std.fmt.format(writer, ctlseqs.bg_bright, .{idx});
|
||||
},
|
||||
else => {
|
||||
try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx});
|
||||
},
|
||||
}
|
||||
},
|
||||
.rgb => |rgb| {
|
||||
try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] });
|
||||
},
|
||||
}
|
||||
// underline color
|
||||
switch (this.ul) {
|
||||
.default => try std.fmt.format(writer, ctlseqs.ul_reset, .{}),
|
||||
.index => |idx| {
|
||||
try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx});
|
||||
},
|
||||
.rgb => |rgb| {
|
||||
try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] });
|
||||
},
|
||||
}
|
||||
// underline style
|
||||
switch (this.ul_style) {
|
||||
.off => try std.fmt.format(writer, ctlseqs.ul_off, .{}),
|
||||
.single => try std.fmt.format(writer, ctlseqs.ul_single, .{}),
|
||||
.double => try std.fmt.format(writer, ctlseqs.ul_double, .{}),
|
||||
.curly => try std.fmt.format(writer, ctlseqs.ul_curly, .{}),
|
||||
.dotted => try std.fmt.format(writer, ctlseqs.ul_dotted, .{}),
|
||||
.dashed => try std.fmt.format(writer, ctlseqs.ul_dashed, .{}),
|
||||
}
|
||||
// bold
|
||||
switch (this.bold) {
|
||||
true => try std.fmt.format(writer, ctlseqs.bold_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
|
||||
}
|
||||
// dim
|
||||
switch (this.dim) {
|
||||
true => try std.fmt.format(writer, ctlseqs.dim_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
|
||||
}
|
||||
// italic
|
||||
switch (this.italic) {
|
||||
true => try std.fmt.format(writer, ctlseqs.italic_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.italic_reset, .{}),
|
||||
}
|
||||
// blink
|
||||
switch (this.blink) {
|
||||
true => try std.fmt.format(writer, ctlseqs.blink_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.blink_reset, .{}),
|
||||
}
|
||||
// reverse
|
||||
switch (this.reverse) {
|
||||
true => try std.fmt.format(writer, ctlseqs.reverse_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}),
|
||||
}
|
||||
// invisible
|
||||
switch (this.invisible) {
|
||||
true => try std.fmt.format(writer, ctlseqs.invisible_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}),
|
||||
}
|
||||
// strikethrough
|
||||
switch (this.strikethrough) {
|
||||
true => try std.fmt.format(writer, ctlseqs.strikethrough_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}),
|
||||
}
|
||||
}
|
||||
|
||||
fn end(this: @This(), writer: anytype) !void {
|
||||
// foreground
|
||||
switch (this.fg) {
|
||||
.default => {},
|
||||
else => try std.fmt.format(writer, ctlseqs.fg_reset, .{}),
|
||||
}
|
||||
// background
|
||||
switch (this.bg) {
|
||||
.default => {},
|
||||
else => try std.fmt.format(writer, ctlseqs.bg_reset, .{}),
|
||||
}
|
||||
// underline color
|
||||
switch (this.ul) {
|
||||
.default => {},
|
||||
else => try std.fmt.format(writer, ctlseqs.ul_reset, .{}),
|
||||
}
|
||||
// underline style
|
||||
switch (this.ul_style) {
|
||||
.off => {},
|
||||
else => try std.fmt.format(writer, ctlseqs.ul_off, .{}),
|
||||
}
|
||||
// bold
|
||||
switch (this.bold) {
|
||||
true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// dim
|
||||
switch (this.dim) {
|
||||
true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// italic
|
||||
switch (this.italic) {
|
||||
true => try std.fmt.format(writer, ctlseqs.italic_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// blink
|
||||
switch (this.blink) {
|
||||
true => try std.fmt.format(writer, ctlseqs.blink_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// reverse
|
||||
switch (this.reverse) {
|
||||
true => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// invisible
|
||||
switch (this.invisible) {
|
||||
true => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// strikethrough
|
||||
switch (this.strikethrough) {
|
||||
true => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(this: @This(), writer: anytype, comptime content: []const u8, args: anytype) !void {
|
||||
try this.start(writer);
|
||||
try std.fmt.format(writer, content, args);
|
||||
try this.end(writer);
|
||||
}
|
||||
|
||||
pub fn value(this: @This(), writer: anytype, content: []const u8) !void {
|
||||
try this.start(writer);
|
||||
_ = try writer.write(content);
|
||||
try this.end(writer);
|
||||
}
|
||||
|
||||
// TODO: implement helper functions for terminal capabilities:
|
||||
// - links / url display (osc 8)
|
||||
// - show / hide cursor?
|
||||
|
||||
test {
|
||||
_ = Color;
|
||||
}
|
||||
1
src/test/container/border.all.zon
Normal file
1
src/test/container/border.all.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/border.horizontal.zon
Normal file
1
src/test/container/border.horizontal.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/border.vertical.zon
Normal file
1
src/test/container/border.vertical.zon
Normal file
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
201
src/testing.zig
Normal file
201
src/testing.zig
Normal file
@@ -0,0 +1,201 @@
|
||||
//! Testing namespace for `zterm` to provide testing capabilities for `Containers`, `Event` handling, `App`s and `Element` implementations.
|
||||
const std = @import("std");
|
||||
const event = @import("event.zig");
|
||||
const Container = @import("container.zig").Container;
|
||||
|
||||
const Cell = @import("cell.zig");
|
||||
const DisplayWidth = @import("DisplayWidth");
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
// TODO how would I describe the expected screens?
|
||||
// - including styling?
|
||||
// - compare generated strings instead? -> how would this be generated for the user?
|
||||
|
||||
/// Single-buffer test rendering pipeline for testing purposes.
|
||||
pub const Renderer = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
size: Point,
|
||||
screen: []Cell,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, size: Point) @This() {
|
||||
const screen = allocator.alloc(Cell, @as(usize, size.x) * @as(usize, size.y)) catch @panic("testing.zig: Out of memory.");
|
||||
@memset(screen, .{});
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.size = size,
|
||||
.screen = screen,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.allocator.free(this.screen);
|
||||
}
|
||||
|
||||
pub fn resize(this: *@This(), size: Point) !void {
|
||||
this.size = size;
|
||||
const n = @as(usize, size.x) * @as(usize, size.y);
|
||||
|
||||
this.allocator.free(this.screen);
|
||||
this.screen = this.allocator.alloc(Cell, n) catch @panic("testing.zig: Out of memory.");
|
||||
@memset(this.screen, .{});
|
||||
}
|
||||
|
||||
pub fn clear(this: *@This()) !void {
|
||||
@memset(this.screen, .{});
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), comptime T: type, container: *const T) !void {
|
||||
const size: Point = container.size;
|
||||
const origin: Point = container.origin;
|
||||
const cells: []const Cell = try container.content();
|
||||
|
||||
if (cells.len == 0) return;
|
||||
|
||||
var idx: usize = 0;
|
||||
const anchor = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
|
||||
|
||||
blk: for (0..size.y) |row| {
|
||||
for (0..size.x) |col| {
|
||||
const cell = cells[idx];
|
||||
idx += 1;
|
||||
|
||||
this.screen[anchor + (row * this.size.x) + col].style = cell.style;
|
||||
this.screen[anchor + (row * this.size.x) + col].cp = cell.cp;
|
||||
|
||||
if (cells.len == idx) break :blk;
|
||||
}
|
||||
}
|
||||
// free immediately
|
||||
container.allocator.free(cells);
|
||||
|
||||
for (container.elements.items) |*element| try this.render(T, element);
|
||||
}
|
||||
|
||||
pub fn save(this: @This(), writer: anytype) !void {
|
||||
try std.zon.stringify.serialize(this.screen, .{ .whitespace = false }, writer);
|
||||
}
|
||||
};
|
||||
|
||||
/// This function is intended to be used only in tests. Test if a `Container`'s
|
||||
/// rendered contents are equal to the expected `Cell` slice.
|
||||
///
|
||||
/// # Test data creation
|
||||
///
|
||||
/// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method:
|
||||
///
|
||||
/// ```zig
|
||||
/// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true });
|
||||
/// defer file.close();
|
||||
///
|
||||
/// const allocator = std.testing.allocator;
|
||||
/// var renderer: testing.Renderer = .init(allocator, size);
|
||||
/// defer renderer.deinit();
|
||||
///
|
||||
/// try container.handle(.{ .size = size });
|
||||
/// try renderer.render(Container(event.SystemEvent), &container);
|
||||
/// try renderer.save(file.writer());
|
||||
/// ```
|
||||
///
|
||||
/// # Testing against created data
|
||||
///
|
||||
/// Then later load that .zon file at compile time and run your test against this `Cell` slice.
|
||||
///
|
||||
/// ```zig
|
||||
/// var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
/// .border = .{
|
||||
/// .color = .green,
|
||||
/// .sides = .all,
|
||||
/// },
|
||||
/// }, .{});
|
||||
/// defer container.deinit();
|
||||
///
|
||||
/// try testing.expectContainerScreen(.{
|
||||
/// .rows = 20,
|
||||
/// .cols = 30,
|
||||
/// }, &container, @import("test/container/border.all.zon"));
|
||||
/// ```
|
||||
pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEvent), expected: []const Cell) !void {
|
||||
const allocator = std.testing.allocator;
|
||||
var renderer: Renderer = .init(allocator, size);
|
||||
defer renderer.deinit();
|
||||
|
||||
try renderer.resize(size);
|
||||
container.resize(size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(Container(event.SystemEvent), container);
|
||||
|
||||
try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
|
||||
}
|
||||
|
||||
/// This function is intended to be used only in tests. Test if the two
|
||||
/// provided cell arrays are identical. Usually the `Cell` slices are
|
||||
/// the contents of a given screen from the `zterm.testing.Renderer`. See
|
||||
/// `zterm.testing.expectContainerScreen` for an example usage.
|
||||
pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actual: []const Cell) !void {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
try std.testing.expectEqual(expected.len, actual.len);
|
||||
try std.testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
|
||||
|
||||
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
|
||||
defer expected_cps.deinit();
|
||||
|
||||
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
|
||||
defer actual_cps.deinit();
|
||||
|
||||
var output = try std.ArrayList(u8).initCapacity(allocator, expected_cps.capacity * actual_cps.capacity + 5 * size.y);
|
||||
defer output.deinit();
|
||||
|
||||
var buffer = std.io.bufferedWriter(output.writer());
|
||||
defer buffer.flush() catch {};
|
||||
|
||||
const writer = buffer.writer();
|
||||
var differ = false;
|
||||
|
||||
const dwd = try DisplayWidth.DisplayWidthData.init(allocator);
|
||||
defer dwd.deinit();
|
||||
const dw: DisplayWidth = .{ .data = &dwd };
|
||||
|
||||
const expected_centered = try dw.center(allocator, "Expected Screen", size.x, " ");
|
||||
defer allocator.free(expected_centered);
|
||||
|
||||
const actual_centered = try dw.center(allocator, "Actual Screen", size.x, " ");
|
||||
defer allocator.free(actual_centered);
|
||||
|
||||
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
|
||||
|
||||
for (origin.y..size.y) |row| {
|
||||
defer {
|
||||
expected_cps.clearRetainingCapacity();
|
||||
actual_cps.clearRetainingCapacity();
|
||||
}
|
||||
for (origin.x..size.x) |col| {
|
||||
const expected_cell = expected[(row * size.x) + col];
|
||||
const actual_cell = actual[(row * size.x) + col];
|
||||
|
||||
if (!expected_cell.eql(actual_cell)) differ = true;
|
||||
|
||||
try expected_cps.append(expected_cell);
|
||||
try actual_cps.append(actual_cell);
|
||||
}
|
||||
|
||||
// write screens both formatted to buffer
|
||||
for (expected_cps.items) |cell| try cell.value(writer);
|
||||
_ = try writer.write(" ┆ ");
|
||||
for (actual_cps.items) |cell| try cell.value(writer);
|
||||
_ = try writer.write("\n");
|
||||
}
|
||||
|
||||
if (!differ) return;
|
||||
|
||||
// test failed
|
||||
try buffer.flush();
|
||||
|
||||
std.debug.lockStdErr();
|
||||
defer std.debug.unlockStdErr();
|
||||
|
||||
const std_writer = std.io.getStdErr().writer();
|
||||
try std_writer.writeAll(output.items);
|
||||
return error.TestExpectEqualCells;
|
||||
}
|
||||
104
src/widget.zig
104
src/widget.zig
@@ -1,104 +0,0 @@
|
||||
//! Dynamic dispatch for widget implementations. Each `Widget` has to implement
|
||||
//! the `Widget.Interface`.
|
||||
//!
|
||||
//! Create a `Widget` using `createFrom(object: anytype)` and use them through
|
||||
//! the defined `Widget.Interface`. The widget will take care of calling the
|
||||
//! correct implementation of the corresponding underlying type.
|
||||
//!
|
||||
//! Each `Widget` may cache its content and should if the contents will not
|
||||
//! change for a long time.
|
||||
//!
|
||||
//! When `Widget.render` is called the provided `Renderer` type is expected
|
||||
//! which handles how contents are rendered for a given widget.
|
||||
const isTaggedUnion = @import("event.zig").isTaggedUnion;
|
||||
|
||||
const log = @import("std").log.scoped(.widget);
|
||||
|
||||
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
const Type = struct {
|
||||
const WidgetType = @This();
|
||||
const Ptr = usize;
|
||||
pub const Interface = @import("interface").Interface(.{
|
||||
.handle = fn (anytype, Event) ?Event,
|
||||
.render = fn (anytype, *Renderer) anyerror!void,
|
||||
.deinit = fn (anytype) void,
|
||||
}, .{});
|
||||
|
||||
const VTable = struct {
|
||||
handle: *const fn (this: *WidgetType, event: Event) ?Event,
|
||||
render: *const fn (this: *WidgetType, renderer: *Renderer) anyerror!void,
|
||||
deinit: *const fn (this: *WidgetType) void,
|
||||
};
|
||||
|
||||
object: Ptr = undefined,
|
||||
vtable: *const VTable = undefined,
|
||||
|
||||
// Handle the provided `Event` for this `Widget`.
|
||||
pub fn handle(this: *WidgetType, event: Event) ?Event {
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return this.vtable.handle(this, event);
|
||||
}
|
||||
|
||||
// Render the content of this `Widget` given the `Size` of the widget (.resize System`Event`).
|
||||
pub fn render(this: *WidgetType, renderer: *Renderer) !void {
|
||||
try this.vtable.render(this, renderer);
|
||||
}
|
||||
|
||||
pub fn deinit(this: *WidgetType) void {
|
||||
this.vtable.deinit(this);
|
||||
this.* = undefined;
|
||||
}
|
||||
|
||||
pub fn createFrom(object: anytype) WidgetType {
|
||||
return WidgetType{
|
||||
.object = @intFromPtr(object),
|
||||
.vtable = &.{
|
||||
.handle = struct {
|
||||
// Handle the provided `Event` for this `Widget`.
|
||||
fn handle(this: *WidgetType, event: Event) ?Event {
|
||||
const widget: @TypeOf(object) = @ptrFromInt(this.object);
|
||||
return widget.handle(event);
|
||||
}
|
||||
}.handle,
|
||||
.render = struct {
|
||||
// Return the entire content of this `Widget`.
|
||||
fn render(this: *WidgetType, renderer: *Renderer) !void {
|
||||
const widget: @TypeOf(object) = @ptrFromInt(this.object);
|
||||
try widget.render(renderer);
|
||||
}
|
||||
}.render,
|
||||
.deinit = struct {
|
||||
fn deinit(this: *WidgetType) void {
|
||||
const widget: @TypeOf(object) = @ptrFromInt(this.object);
|
||||
widget.deinit();
|
||||
}
|
||||
}.deinit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// import and export of `Widget` implementations
|
||||
pub const Text = @import("widget/Text.zig").Widget(Event, Renderer);
|
||||
pub const RawText = @import("widget/RawText.zig").Widget(Event, Renderer);
|
||||
pub const Spacer = @import("widget/Spacer.zig").Widget(Event, Renderer);
|
||||
};
|
||||
// test widget implementation satisfies the interface
|
||||
comptime Type.Interface.satisfiedBy(Type);
|
||||
comptime Type.Interface.satisfiedBy(Type.Text);
|
||||
comptime Type.Interface.satisfiedBy(Type.RawText);
|
||||
comptime Type.Interface.satisfiedBy(Type.Spacer);
|
||||
return Type;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Key = terminal.Key;
|
||||
const Style = terminal.Style;
|
||||
|
||||
const log = std.log.scoped(.widget_rawtext);
|
||||
|
||||
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
const Contents = std.ArrayList(u8);
|
||||
return struct {
|
||||
contents: Contents = undefined,
|
||||
line_index: std.ArrayList(usize) = undefined,
|
||||
line: usize = 0,
|
||||
size: terminal.Size = undefined,
|
||||
require_render: bool = false,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, file: std.fs.File) @This() {
|
||||
var contents = Contents.init(allocator);
|
||||
var line_index = std.ArrayList(usize).init(allocator);
|
||||
file.reader().readAllArrayList(&contents, std.math.maxInt(usize)) catch {};
|
||||
line_index.append(0) catch {};
|
||||
for (contents.items, 0..) |item, i| {
|
||||
if (item == '\n') {
|
||||
line_index.append(i + 1) catch {};
|
||||
}
|
||||
}
|
||||
return .{
|
||||
.contents = contents,
|
||||
.line_index = line_index,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.contents.deinit();
|
||||
this.line_index.deinit();
|
||||
this.* = undefined;
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) ?Event {
|
||||
var require_render = true;
|
||||
switch (event) {
|
||||
// store the received size
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
if (this.line > this.line_index.items.len -| 1 -| size.rows) {
|
||||
this.line = this.line_index.items.len -| 1 -| size.rows;
|
||||
}
|
||||
},
|
||||
.key => |key| {
|
||||
if (key.matches(.{ .cp = 'g' })) {
|
||||
// top
|
||||
this.line = 0;
|
||||
} else if (key.matches(.{ .cp = 'G' })) {
|
||||
// bottom
|
||||
this.line = this.line_index.items.len -| 1 -| this.size.rows;
|
||||
} else if (key.matches(.{ .cp = 'j' })) {
|
||||
// down
|
||||
if (this.line < this.line_index.items.len -| 1 -| this.size.rows) {
|
||||
this.line +|= 1;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'k' })) {
|
||||
// up
|
||||
this.line -|= 1;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
},
|
||||
else => {
|
||||
require_render = false;
|
||||
},
|
||||
}
|
||||
this.require_render = require_render;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
if (!this.require_render) {
|
||||
return;
|
||||
}
|
||||
try renderer.clear(this.size);
|
||||
if (this.size.rows >= this.line_index.items.len) {
|
||||
try renderer.render(this.size, &[_]terminal.Cell{
|
||||
.{ .content = this.contents.items, .style = .{ .dim = true, .fg = .{ .index = 8 } } },
|
||||
});
|
||||
} else {
|
||||
// more rows than we can display
|
||||
const i = this.line_index.items[this.line];
|
||||
const e = this.size.rows + this.line;
|
||||
if (e > this.line_index.items.len) {
|
||||
try renderer.render(this.size, &[_]terminal.Cell{
|
||||
.{ .content = this.contents.items[i..], .style = .{ .dim = true, .fg = .{ .index = 7 } } },
|
||||
});
|
||||
return;
|
||||
}
|
||||
const x = this.line_index.items[e];
|
||||
try renderer.render(this.size, &[_]terminal.Cell{
|
||||
.{ .content = this.contents.items[i..x], .style = .{ .dim = true, .fg = .{ .index = 9 } } },
|
||||
});
|
||||
}
|
||||
this.require_render = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
|
||||
const log = std.log.scoped(.widget_spacer);
|
||||
|
||||
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
return struct {
|
||||
size: terminal.Size = undefined,
|
||||
size_changed: bool = false,
|
||||
|
||||
pub fn init() @This() {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.* = undefined;
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) ?Event {
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
this.size_changed = true;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
if (this.size_changed) {
|
||||
try renderer.clear(this.size);
|
||||
this.size_changed = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Cell = terminal.Cell;
|
||||
|
||||
const log = std.log.scoped(.widget_text);
|
||||
|
||||
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
return struct {
|
||||
contents: []const Cell = undefined,
|
||||
size: terminal.Size = undefined,
|
||||
require_render: bool = false,
|
||||
|
||||
pub fn init(contents: []const Cell) @This() {
|
||||
return .{
|
||||
.contents = contents,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.* = undefined;
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) ?Event {
|
||||
switch (event) {
|
||||
// store the received size
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
this.require_render = true;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
if (!this.require_render) {
|
||||
return;
|
||||
}
|
||||
try renderer.clear(this.size);
|
||||
try renderer.render(this.size, this.contents);
|
||||
this.require_render = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,42 @@
|
||||
// private imports
|
||||
const terminal = @import("terminal.zig");
|
||||
const container = @import("container.zig");
|
||||
const color = @import("color.zig");
|
||||
const size = @import("point.zig");
|
||||
|
||||
// public exports
|
||||
pub const input = @import("input.zig");
|
||||
pub const testing = @import("testing.zig");
|
||||
|
||||
// public import / exports
|
||||
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.Container`
|
||||
// `App.Element`
|
||||
pub const Renderer = @import("render.zig");
|
||||
|
||||
pub const Key = terminal.Key;
|
||||
pub const Position = terminal.Position;
|
||||
pub const Size = terminal.Size;
|
||||
pub const Cell = terminal.Cell;
|
||||
// Container Configurations
|
||||
pub const Border = container.Border;
|
||||
pub const Rectangle = container.Rectangle;
|
||||
pub const Scroll = container.Scroll;
|
||||
pub const Layout = container.Layout;
|
||||
|
||||
pub const Cell = @import("cell.zig");
|
||||
pub const Color = color.Color;
|
||||
pub const Key = input.Key;
|
||||
pub const Mouse = input.Mouse;
|
||||
pub const Point = @import("point.zig").Point;
|
||||
pub const Style = @import("style.zig");
|
||||
|
||||
test {
|
||||
_ = @import("terminal.zig");
|
||||
_ = @import("container.zig");
|
||||
_ = @import("queue.zig");
|
||||
_ = @import("error.zig");
|
||||
_ = @import("point.zig");
|
||||
|
||||
_ = color;
|
||||
|
||||
_ = Cell;
|
||||
_ = Key;
|
||||
_ = Style;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user