mod(structure): update project structure
Remove examples, add description for design goals in README.md and apply re-names and naming changes accordingly for the project structure. Implement a flat hierachry, as the library shall remain pretty simple.
This commit is contained in:
45
README.md
45
README.md
@@ -3,7 +3,7 @@
|
||||
`zterm` is a terminal user interface library to implement terminal (fullscreen or inline) applications.
|
||||
|
||||
> [!NOTE]
|
||||
> Only builds using the master version might will work.
|
||||
> Only builds using the master version are tested to work.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -25,3 +25,46 @@ exe.root_module.addImport("zterm", zterm.module("zterm"));
|
||||
```
|
||||
|
||||
For an example you can take a look at [build.zig](build.zig) for an example.
|
||||
|
||||
---
|
||||
## Design Goals
|
||||
|
||||
This project draws heavy inspiration from
|
||||
[clay](https://github.com/nicbarker/clay) in the way the layout is declared by
|
||||
the user. As terminal applications usually are rendered in intermediate mode,
|
||||
the rendering is also part of the event loop. Such that every time an event
|
||||
happens a render call will usually be done as well. However this is not strickly
|
||||
necessary and can be separated to have a fixed rendering of every 16ms (i.e. for
|
||||
60 fps), etc.
|
||||
|
||||
There is only one generic container which contains properties and elements (or
|
||||
children) which can also be containers, such that each layout in the end is
|
||||
a tree.
|
||||
|
||||
The library is designed to be very basic and not to provide any more complex
|
||||
elements such as input fields, drop-down menu's, buttons, etc. Some of them are
|
||||
either easy to implement yourself, specific for you needs or a too complex to
|
||||
be provided by the library effectively. For these use-cases there may be other
|
||||
libraries that build on top of this one to provide the complex elements as some
|
||||
sort of pre-built elements for you to use in your application (or you create
|
||||
them yourself).
|
||||
|
||||
There are only very few system events, that are used by the built-in containers
|
||||
and properties accordingly. For you own widgets (i.e. a collection of elements)
|
||||
you can extend the events to include your own events to communicate between
|
||||
elements, effect the control flow and the corresponding generated layouts and
|
||||
much more.
|
||||
|
||||
As this is a terminal based layout library it also provides a rendering pipeline
|
||||
alongside the event loop implementation. Usually the event loop is waiting
|
||||
blocking and will only cause a re-draw (**intermediate mode**) after each event.
|
||||
Even though the each frame is regenerated from scratch each render loop, the
|
||||
corresponding application is still pretty performant as the renderer uses a
|
||||
double buffered intermediate mode implementation to only apply the changes from
|
||||
each frame to the next to the terminal.
|
||||
|
||||
This library is also designed to work accordingly in ssh hosted environments,
|
||||
such that an application created using this library can be accessed directly
|
||||
via ssh. This provides security through the ssh protocol and can defer the
|
||||
synchronization process, as users may access the same running instance. Which is
|
||||
the primary use-case for myself to create this library in the first place.
|
||||
|
||||
92
build.zig
92
build.zig
@@ -1,110 +1,42 @@
|
||||
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 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"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
stack_example.root_module.addImport("zterm", lib);
|
||||
// TODO: examples (not yet available)
|
||||
// const stack_example = b.addExecutable(.{
|
||||
// .name = "stack",
|
||||
// .root_source_file = b.path("examples/stack.zig"),
|
||||
// .target = target,
|
||||
// .optimize = optimize,
|
||||
// });
|
||||
// stack_example.root_module.addImport("zterm", lib);
|
||||
// b.installArtifact(stack_example);
|
||||
|
||||
const container_example = b.addExecutable(.{
|
||||
.name = "container",
|
||||
.root_source_file = b.path("examples/container.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
container_example.root_module.addImport("zterm", lib);
|
||||
|
||||
const padding_example = b.addExecutable(.{
|
||||
.name = "padding",
|
||||
.root_source_file = b.path("examples/padding.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
padding_example.root_module.addImport("zterm", lib);
|
||||
|
||||
const exec_example = b.addExecutable(.{
|
||||
.name = "exec",
|
||||
.root_source_file = b.path("examples/exec.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exec_example.root_module.addImport("zterm", lib);
|
||||
|
||||
const tui_example = b.addExecutable(.{
|
||||
.name = "tui",
|
||||
.root_source_file = b.path("examples/tui.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
tui_example.root_module.addImport("zterm", lib);
|
||||
|
||||
const tabs_example = b.addExecutable(.{
|
||||
.name = "tabs",
|
||||
.root_source_file = b.path("examples/tabs.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
tabs_example.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);
|
||||
b.installArtifact(tui_example);
|
||||
b.installArtifact(tabs_example);
|
||||
|
||||
// Creates a step for unit testing. This only builds the test executable
|
||||
// but does not run it.
|
||||
// testing
|
||||
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"));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -27,10 +27,6 @@
|
||||
.url = "git+https://codeberg.org/atman/zg#a363f507fc39b96fc48d693665a823a358345326",
|
||||
.hash = "1220fe42e39fd141c84fd7d5cf69945309bb47253033e68788f99bdfe5585fbc711a",
|
||||
},
|
||||
.interface = .{
|
||||
.url = "git+https://github.com/yves-biener/zig-interface#ef47e045df19e09250fff45c0702d014fb3d3c37",
|
||||
.hash = "1220a442e8d9b813572bab7a55eef504c83b628f0b17fd283e776dbc1d1a3d98e842",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
|
||||
@@ -1,96 +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.HContainer.init(allocator, .{
|
||||
.{
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
15,
|
||||
},
|
||||
.{
|
||||
Layout.createFrom(Layout.VContainer.init(allocator, .{
|
||||
.{
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
25,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./src/app.zig", .{});
|
||||
defer file.close();
|
||||
break :blk Widget.RawText.init(allocator, file);
|
||||
}),
|
||||
50,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
25,
|
||||
},
|
||||
})),
|
||||
70,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
15,
|
||||
},
|
||||
}));
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start(null);
|
||||
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 });
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +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.VContainer.init(allocator, .{
|
||||
.{
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
45,
|
||||
},
|
||||
.{
|
||||
Layout.createFrom(Layout.Framing.init(allocator, .{}, .{
|
||||
.widget = Widget.createFrom(Widget.Text.init(allocator, .center, &[_]Cell{
|
||||
.{ .content = "Press " },
|
||||
.{ .content = "Ctrl+n", .style = .{ .fg = .{ .index = 6 } } },
|
||||
.{ .content = " to launch $EDITOR" },
|
||||
})),
|
||||
})),
|
||||
10,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
45,
|
||||
},
|
||||
}));
|
||||
defer layout.deinit();
|
||||
|
||||
const min_size: zterm.Size = .{
|
||||
.cols = 25,
|
||||
.rows = 20,
|
||||
};
|
||||
|
||||
try app.start(min_size);
|
||||
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(min_size) 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 });
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +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.Padding.init(allocator, .{
|
||||
.padding = 15,
|
||||
}, .{
|
||||
.layout = Layout.createFrom(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(Layout.Margin.init(
|
||||
allocator,
|
||||
.{
|
||||
.margin = 10,
|
||||
},
|
||||
.{
|
||||
.widget = Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./examples/padding.zig", .{});
|
||||
defer file.close();
|
||||
break :blk Widget.RawText.init(allocator, file);
|
||||
}),
|
||||
},
|
||||
)),
|
||||
},
|
||||
)),
|
||||
}));
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start(null);
|
||||
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 });
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Buffered,
|
||||
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.init(allocator);
|
||||
defer renderer.deinit();
|
||||
// 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.Framing.init(allocator, .{
|
||||
.style = .{
|
||||
.fg = .{
|
||||
.index = 6,
|
||||
},
|
||||
},
|
||||
.frame = .round,
|
||||
.title = .{
|
||||
.str = "HStack",
|
||||
.style = .{
|
||||
.ul_style = .single,
|
||||
.ul = .{ .index = 6 },
|
||||
.bold = true,
|
||||
},
|
||||
},
|
||||
}, .{
|
||||
.layout = Layout.createFrom(Layout.HStack.init(allocator, .{
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
Layout.createFrom(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(
|
||||
Layout.Margin.init(
|
||||
allocator,
|
||||
.{
|
||||
.margin = 10,
|
||||
},
|
||||
.{
|
||||
.layout = Layout.createFrom(Layout.VStack.init(allocator, .{
|
||||
// Widget.createFrom(blk: {
|
||||
// const file = try std.fs.cwd().openFile("./examples/stack.zig", .{});
|
||||
// defer file.close();
|
||||
// break :blk Widget.RawText.init(allocator, file);
|
||||
// }),
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
// Widget.createFrom(blk: {
|
||||
// const file = try std.fs.cwd().openFile("./examples/stack.zig", .{});
|
||||
// defer file.close();
|
||||
// break :blk Widget.RawText.init(allocator, file);
|
||||
// }),
|
||||
})),
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
)),
|
||||
Widget.createFrom(Widget.Spacer.init(allocator)),
|
||||
})),
|
||||
}));
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start(null);
|
||||
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 });
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +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(.tabs);
|
||||
|
||||
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.Tab.init(allocator, .{}, .{
|
||||
.{
|
||||
Layout.createFrom(Layout.Margin.init(allocator, .{ .margin = 10 }, .{
|
||||
.widget = Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./examples/tabs.zig", .{});
|
||||
defer file.close();
|
||||
break :blk Widget.RawText.init(allocator, file);
|
||||
}),
|
||||
})),
|
||||
"Tab 2",
|
||||
Cell.Style.Color{ .index = 6 },
|
||||
},
|
||||
.{
|
||||
Layout.createFrom(Layout.Framing.init(
|
||||
allocator,
|
||||
.{
|
||||
.frame = .round,
|
||||
.title = .{
|
||||
.str = "Content in Margin",
|
||||
.style = .{
|
||||
.ul_style = .single,
|
||||
.ul = .{ .index = 4 },
|
||||
.bold = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
.layout = Layout.createFrom(Layout.Margin.init(allocator, .{ .margin = 10 }, .{
|
||||
.widget = Widget.createFrom(Widget.List.init(allocator, .ordered, .{
|
||||
&[_]Cell{.{ .content = "First entry" }},
|
||||
&[_]Cell{.{ .content = "Second entry" }},
|
||||
&[_]Cell{.{ .content = "Third entry" }},
|
||||
})),
|
||||
})),
|
||||
},
|
||||
)),
|
||||
"Tab 1",
|
||||
Cell.Style.Color{ .index = 4 },
|
||||
},
|
||||
}));
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start(null);
|
||||
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 });
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
139
examples/tui.zig
139
examples/tui.zig
@@ -1,139 +0,0 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {
|
||||
view: union(enum) {
|
||||
tui, // view instance to the corresponding view for 'tui'
|
||||
},
|
||||
},
|
||||
zterm.Renderer.Buffered,
|
||||
true,
|
||||
);
|
||||
const Cell = zterm.Cell;
|
||||
const Key = zterm.Key;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
const View = App.View;
|
||||
|
||||
const Tui = struct {
|
||||
const Events = std.ArrayList(App.Event);
|
||||
allocator: std.mem.Allocator,
|
||||
layout: Layout,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) *Tui {
|
||||
var tui = allocator.create(Tui) catch @panic("Out of memory: tui.zig");
|
||||
tui.allocator = allocator;
|
||||
tui.layout = Layout.createFrom(Layout.VStack.init(allocator, .{
|
||||
Layout.createFrom(Layout.HStack.init(allocator, .{
|
||||
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
|
||||
.{ .rune = 'Y', .style = .{ .fg = .{ .index = 6 }, .bold = true } },
|
||||
})),
|
||||
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
|
||||
.{ .rune = 'F', .style = .{ .fg = .{ .index = 6 }, .bold = true } },
|
||||
})),
|
||||
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
|
||||
.{ .rune = 'C', .style = .{ .fg = .{ .index = 6 }, .bold = true } },
|
||||
})),
|
||||
})),
|
||||
// .{
|
||||
// Layout.createFrom(Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{
|
||||
// .widget = Widget.createFrom(Widget.Text.init(allocator, .default, &[1]Cell{
|
||||
// .{ .rune = 'D', .style = .{ .ul = .default, .ul_style = .single } },
|
||||
// })),
|
||||
// })),
|
||||
// 90,
|
||||
// },
|
||||
}));
|
||||
return tui;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Tui) void {
|
||||
this.layout.deinit();
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
pub fn enable(this: *Tui) void {
|
||||
_ = this;
|
||||
}
|
||||
|
||||
pub fn disable(this: *Tui) void {
|
||||
_ = this;
|
||||
}
|
||||
|
||||
pub fn handle(this: *Tui, event: App.Event) !*Events {
|
||||
return try this.layout.handle(event);
|
||||
}
|
||||
|
||||
pub fn render(this: *Tui, renderer: *App.Renderer) !void {
|
||||
try this.layout.render(renderer);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: create additional example with a bit more complex functionality for
|
||||
// dynamic layouts, switching views, etc.
|
||||
|
||||
const log = std.log.scoped(.tui);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const allocator = arena.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer = App.Renderer.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var view: View = undefined;
|
||||
|
||||
var tui_view = View.createFrom(Tui.init(allocator));
|
||||
defer tui_view.deinit();
|
||||
|
||||
view = tui_view;
|
||||
try app.start(null);
|
||||
defer app.stop() catch unreachable;
|
||||
|
||||
// App.Event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
|
||||
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 });
|
||||
},
|
||||
.view => |v| {
|
||||
switch (v) {
|
||||
.tui => {
|
||||
view = tui_view;
|
||||
// NOTE: report potentially new screen size
|
||||
const events = try view.handle(.{ .resize = renderer.size });
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
const events = try view.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try view.render(&renderer);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ const event = @import("event.zig");
|
||||
const mergeTaggedUnions = event.mergeTaggedUnions;
|
||||
const isTaggedUnion = event.isTaggedUnion;
|
||||
|
||||
const Key = terminal.Key;
|
||||
const Key = @import("key.zig");
|
||||
const Queue = @import("queue.zig").Queue;
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
const std = @import("std");
|
||||
pub const Style = @import("Style.zig");
|
||||
const Style = @import("Style.zig");
|
||||
|
||||
pub const Cell = @This();
|
||||
|
||||
style: Style = .{},
|
||||
rune: u8 = ' ',
|
||||
|
||||
pub const Character = struct {
|
||||
grapheme: []const u8,
|
||||
width: u8,
|
||||
};
|
||||
|
||||
pub fn eql(this: @This(), other: @This()) bool {
|
||||
pub fn eql(this: Cell, other: Cell) bool {
|
||||
return this.rune == other.rune and this.style.eql(other.style);
|
||||
}
|
||||
|
||||
pub fn reset(this: *@This()) void {
|
||||
pub fn reset(this: *Cell) void {
|
||||
this.style = .{ .fg = .default, .bg = .default, .ul = .default, .ul_style = .off };
|
||||
this.rune = ' ';
|
||||
}
|
||||
|
||||
pub fn value(this: @This(), writer: anytype) !void {
|
||||
pub fn value(this: Cell, writer: anytype) !void {
|
||||
try this.style.value(writer, this.rune);
|
||||
}
|
||||
|
||||
test {
|
||||
_ = Style;
|
||||
}
|
||||
86
src/color.zig
Normal file
86
src/color.zig
Normal file
@@ -0,0 +1,86 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Color = union(enum) {
|
||||
default,
|
||||
index: u8,
|
||||
rgb: [3]u8,
|
||||
|
||||
pub fn eql(a: Color, b: Color) 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),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
_ = Color;
|
||||
}
|
||||
0
src/container.zig
Normal file
0
src/container.zig
Normal file
0
src/element.zig
Normal file
0
src/element.zig
Normal file
@@ -3,26 +3,31 @@
|
||||
const std = @import("std");
|
||||
const terminal = @import("terminal.zig");
|
||||
|
||||
const Size = terminal.Size;
|
||||
const Key = terminal.Key;
|
||||
|
||||
pub const Error = struct {
|
||||
err: anyerror,
|
||||
msg: []const u8,
|
||||
};
|
||||
const Size = @import("size.zig");
|
||||
const Key = @import("key.zig");
|
||||
|
||||
// System events available to every application.
|
||||
// TODO: should this also already include the .view enum option?
|
||||
pub const SystemEvent = union(enum) {
|
||||
/// Initialize event, which is send once at the beginning of the event loop and before the first render loop
|
||||
init,
|
||||
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
|
||||
quit,
|
||||
err: Error,
|
||||
/// Error event to notify other containers about a recoverable error
|
||||
err: struct {
|
||||
err: anyerror,
|
||||
/// associated error message
|
||||
msg: []const u8,
|
||||
},
|
||||
/// Resize event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in
|
||||
resize: Size,
|
||||
/// Input key event received from the user
|
||||
key: Key,
|
||||
/// 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 {
|
||||
// TODO: should this expect one of the unions to contain the .view value option with its corresponding associated type?
|
||||
if (!isTaggedUnion(A) or !isTaggedUnion(B)) {
|
||||
@compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Keybindings and Modifiers for user input detection and selection.
|
||||
const std = @import("std");
|
||||
|
||||
pub const Key = @This();
|
||||
|
||||
pub const Modifier = struct {
|
||||
shift: bool = false,
|
||||
alt: bool = false,
|
||||
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),
|
||||
};
|
||||
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: *anyopaque = 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);
|
||||
}
|
||||
|
||||
pub fn createFrom(object: anytype) LayoutType {
|
||||
return LayoutType{
|
||||
.object = @ptrCast(@alignCast(object)),
|
||||
.vtable = &.{
|
||||
.handle = struct {
|
||||
// Handle the provided `Event` for this `Layout`.
|
||||
fn handle(this: *LayoutType, event: Event) !*Events {
|
||||
const layout: @TypeOf(object) = @ptrCast(@alignCast(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) = @ptrCast(@alignCast(this.object));
|
||||
try layout.render(renderer);
|
||||
}
|
||||
}.render,
|
||||
.deinit = struct {
|
||||
fn deinit(this: *LayoutType) void {
|
||||
const layout: @TypeOf(object) = @ptrCast(@alignCast(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);
|
||||
pub const Tab = @import("layout/Tab.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);
|
||||
comptime Type.Interface.satisfiedBy(Type.Tab);
|
||||
return Type;
|
||||
}
|
||||
@@ -1,198 +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 {
|
||||
allocator: std.mem.Allocator,
|
||||
size: terminal.Size,
|
||||
require_render: bool,
|
||||
element: Element,
|
||||
events: Events,
|
||||
config: Config,
|
||||
|
||||
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() {
|
||||
var this = allocator.create(@This()) catch @panic("Framing.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.require_render = true;
|
||||
this.config = config;
|
||||
this.element = element;
|
||||
this.events = Events.init(allocator);
|
||||
return this;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
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: [6][]const u8 = .{ "╭", "─", "╮", "│", "╰", "╯" };
|
||||
const square_frame: [6][]const u8 = .{ "┌", "─", "┐", "│", "└", "┘" };
|
||||
|
||||
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]);
|
||||
for (0..this.config.title.str.len) |i| {
|
||||
try this.config.title.style.value(writer, this.config.title.str[i]);
|
||||
}
|
||||
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,187 +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`?
|
||||
allocator: std.mem.Allocator,
|
||||
size: terminal.Size,
|
||||
containers: Containers,
|
||||
events: Events,
|
||||
|
||||
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("HContainer.zig: out of memory");
|
||||
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));
|
||||
}
|
||||
var this = allocator.create(@This()) catch @panic("HContainer.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.containers = containers;
|
||||
this.events = Events.init(allocator);
|
||||
return this;
|
||||
}
|
||||
|
||||
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();
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
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,162 +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`?
|
||||
allocator: std.mem.Allocator,
|
||||
size: terminal.Size,
|
||||
elements: Elements,
|
||||
events: Events,
|
||||
|
||||
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("HStack.zig: out of memory");
|
||||
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));
|
||||
}
|
||||
var this = allocator.create(@This()) catch @panic("HStack.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.elements = elements;
|
||||
this.events = Events.init(allocator);
|
||||
return this;
|
||||
}
|
||||
|
||||
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();
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
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,164 +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 {
|
||||
allocator: std.mem.Allocator,
|
||||
size: terminal.Size,
|
||||
require_render: bool,
|
||||
element: Element,
|
||||
events: Events,
|
||||
config: Config,
|
||||
|
||||
const Config = struct {
|
||||
margin: ?u8 = null,
|
||||
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);
|
||||
}
|
||||
var this = allocator.create(@This()) catch @panic("Margin.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.require_render = true;
|
||||
this.config = config;
|
||||
this.element = element;
|
||||
this.events = Events.init(allocator);
|
||||
return this;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
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,152 +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 {
|
||||
allocator: std.mem.Allocator,
|
||||
size: terminal.Size,
|
||||
require_render: bool,
|
||||
element: Element,
|
||||
events: Events,
|
||||
config: Config,
|
||||
|
||||
const Config = struct {
|
||||
padding: ?u16 = null,
|
||||
left: u16 = 0,
|
||||
right: u16 = 0,
|
||||
top: u16 = 0,
|
||||
bottom: u16 = 0,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) *@This() {
|
||||
var this = allocator.create(@This()) catch @panic("Padding.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.require_render = true;
|
||||
this.config = config;
|
||||
this.element = element;
|
||||
this.events = Events.init(allocator);
|
||||
return this;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
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,314 +0,0 @@
|
||||
//! Tab 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 Cell = terminal.Cell;
|
||||
const Style = Cell.Style;
|
||||
const Color = Style.Color;
|
||||
|
||||
const log = std.log.scoped(.layout_tab);
|
||||
|
||||
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);
|
||||
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
|
||||
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
|
||||
const Tab = struct {
|
||||
element: Element,
|
||||
title: []const u8,
|
||||
color: Color,
|
||||
};
|
||||
const Tabs = std.ArrayList(Tab);
|
||||
return struct {
|
||||
allocator: std.mem.Allocator,
|
||||
size: terminal.Size,
|
||||
require_render: bool,
|
||||
tabs: Tabs,
|
||||
active_tab: usize,
|
||||
events: Events,
|
||||
config: Config,
|
||||
|
||||
const Config = struct {
|
||||
frame: Frame = .round,
|
||||
|
||||
const Frame = enum {
|
||||
round,
|
||||
square,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, config: Config, 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 tabs = Tabs.initCapacity(allocator, fields_info.len) catch @panic("Tab.zig: out of memory");
|
||||
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 != 3) {
|
||||
@compileError("expected nested tuple or struct to have exactly 3 fields, but found " ++ child_fields.len);
|
||||
}
|
||||
const element = @field(child, child_fields[0].name);
|
||||
const ElementType = @TypeOf(element);
|
||||
const tab_title = @field(child, child_fields[1].name);
|
||||
const TabTitleType = @TypeOf(tab_title);
|
||||
const tab_title_type_info = @typeInfo(TabTitleType);
|
||||
const tab_color = @field(child, child_fields[2].name);
|
||||
const TabColorType = @TypeOf(tab_color);
|
||||
if (tab_title_type_info != .array and tab_title_type_info != .pointer) {
|
||||
// TODO: check for inner type of the title to be u8
|
||||
@compileError("expected an u8 array second argument of nested tuple or struct child, but found " ++ @tagName(tab_title_type_info));
|
||||
}
|
||||
if (TabColorType != Color) {
|
||||
@compileError("expected an Color typed third argument of nested tuple or struct child, but found " ++ @typeName(TabColorType));
|
||||
}
|
||||
if (ElementType == WidgetType) {
|
||||
tabs.append(.{
|
||||
.element = .{ .widget = element },
|
||||
.title = tab_title,
|
||||
.color = tab_color,
|
||||
}) catch {};
|
||||
continue;
|
||||
}
|
||||
if (ElementType == LayoutType) {
|
||||
tabs.append(.{
|
||||
.element = .{ .layout = element },
|
||||
.title = tab_title,
|
||||
.color = tab_color,
|
||||
}) catch {};
|
||||
continue;
|
||||
}
|
||||
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
|
||||
}
|
||||
var this = allocator.create(@This()) catch @panic("Tab.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.active_tab = 0;
|
||||
this.require_render = true;
|
||||
this.config = config;
|
||||
this.tabs = tabs;
|
||||
this.events = Events.init(allocator);
|
||||
return this;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
for (this.tabs.items) |*tab| {
|
||||
switch (tab.element) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
this.tabs.deinit();
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
fn resize_active_tab(this: *@This()) !void {
|
||||
const sub_event: Event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = this.size.anchor.col + 1,
|
||||
.row = this.size.anchor.row + 1,
|
||||
},
|
||||
.cols = this.size.cols -| 2,
|
||||
.rows = this.size.rows -| 2,
|
||||
},
|
||||
};
|
||||
// resize active tab to re-render the widget in the following render loop
|
||||
var tab = this.tabs.items[this.active_tab];
|
||||
switch ((&tab.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);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !*Events {
|
||||
this.events.clearRetainingCapacity();
|
||||
if (this.tabs.items.len == 0) {
|
||||
return &this.events;
|
||||
}
|
||||
// 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,
|
||||
});
|
||||
try this.resize_active_tab();
|
||||
},
|
||||
.key => |key| {
|
||||
// tab -> cycle forward
|
||||
// back-tab -> cycle backward
|
||||
if (key.matches(.{ .cp = Key.tab })) {
|
||||
this.active_tab += 1;
|
||||
this.active_tab %= this.tabs.items.len;
|
||||
this.require_render = true;
|
||||
try this.resize_active_tab();
|
||||
} else if (key.matches(.{ .cp = Key.tab, .mod = .{ .shift = true } })) { // backtab / shift + tab
|
||||
if (this.active_tab > 0) {
|
||||
this.active_tab -|= 1;
|
||||
} else {
|
||||
this.active_tab = this.tabs.items.len - 1;
|
||||
}
|
||||
this.require_render = true;
|
||||
try this.resize_active_tab();
|
||||
} else {
|
||||
// TODO: absorb tab key or send key down too?
|
||||
var tab = this.tabs.items[this.active_tab];
|
||||
switch ((&tab.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);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {
|
||||
// NOTE: should this only send the event to the 'active_tab'
|
||||
var tab = this.tabs.items[this.active_tab];
|
||||
switch ((&tab.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();
|
||||
var style: Style = .{ .fg = this.tabs.items[this.active_tab].color };
|
||||
try style.value(writer, frame[0]);
|
||||
var tab_title_len: usize = 0;
|
||||
for (this.tabs.items, 0..) |tab, idx| {
|
||||
var tab_style: Cell.Style = .{
|
||||
.fg = tab.color,
|
||||
.bg = .default,
|
||||
};
|
||||
if (idx == this.active_tab) {
|
||||
tab_style.fg = .default;
|
||||
tab_style.bg = tab.color;
|
||||
}
|
||||
const cell: Cell = .{
|
||||
.content = tab.title,
|
||||
.style = tab_style,
|
||||
};
|
||||
try cell.value(writer, 0, tab.title.len);
|
||||
tab_title_len += tab.title.len;
|
||||
}
|
||||
for (0..this.size.cols -| 2 -| tab_title_len) |_| {
|
||||
try style.value(writer, frame[1]);
|
||||
}
|
||||
try 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 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 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 style.value(writer, frame[4]);
|
||||
for (0..this.size.cols -| 2) |_| {
|
||||
try style.value(writer, frame[1]);
|
||||
}
|
||||
try 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;
|
||||
}
|
||||
|
||||
var tab = this.tabs.items[this.active_tab];
|
||||
switch ((&tab.element).*) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,187 +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`?
|
||||
allocator: std.mem.Allocator,
|
||||
size: terminal.Size,
|
||||
containers: Containers,
|
||||
events: Events,
|
||||
|
||||
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("VContainer.zig: out of memory");
|
||||
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));
|
||||
}
|
||||
var this = allocator.create(@This()) catch @panic("VContainer.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.containers = containers;
|
||||
this.events = Events.init(allocator);
|
||||
return this;
|
||||
}
|
||||
|
||||
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();
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
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,161 +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`?
|
||||
allocator: std.mem.Allocator,
|
||||
size: terminal.Size,
|
||||
elements: Elements,
|
||||
events: Events,
|
||||
|
||||
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("VStack.zig out of memory");
|
||||
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));
|
||||
}
|
||||
var this = allocator.create(@This()) catch @panic("VStack.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.elements = elements;
|
||||
this.events = Events.init(allocator);
|
||||
return this;
|
||||
}
|
||||
|
||||
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();
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
0
src/properties.zig
Normal file
0
src/properties.zig
Normal file
@@ -106,7 +106,7 @@ pub fn Buffered(comptime fullscreen: bool) type {
|
||||
if (cs.eql(cvs))
|
||||
continue;
|
||||
// render differences found in virtual screen
|
||||
// TODO: improve the writing speed (many unecessary writes (i.e. the style for every character..))
|
||||
// TODO: improve the writing speed (many unnecessary writes (i.e. the style for every character..))
|
||||
try terminal.setCursorPosition(.{ .row = @truncate(row), .col = @truncate(col) });
|
||||
try cvs.value(writer);
|
||||
// update screen to be the virtual screen for the next frame
|
||||
|
||||
10
src/size.zig
Normal file
10
src/size.zig
Normal file
@@ -0,0 +1,10 @@
|
||||
pub const Size = @This();
|
||||
|
||||
pub const Position = struct {
|
||||
col: u16,
|
||||
row: u16,
|
||||
};
|
||||
|
||||
anchor: Position = .{},
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
@@ -10,6 +10,10 @@
|
||||
const std = @import("std");
|
||||
const ctlseqs = @import("ctlseqs.zig");
|
||||
|
||||
const Color = @import("color.zig").Color;
|
||||
|
||||
pub const Style = @This();
|
||||
|
||||
pub const Underline = enum {
|
||||
off,
|
||||
single,
|
||||
@@ -19,87 +23,6 @@ pub const Underline = enum {
|
||||
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,
|
||||
@@ -113,7 +36,7 @@ reverse: bool = false,
|
||||
invisible: bool = false,
|
||||
strikethrough: bool = false,
|
||||
|
||||
pub fn eql(this: @This(), other: @This()) bool {
|
||||
pub fn eql(this: Style, other: Style) bool {
|
||||
return this.fg.eql(other.fg) and
|
||||
this.bg.eql(other.bg) and
|
||||
this.ul.eql(other.ul) and
|
||||
@@ -129,7 +52,7 @@ pub fn eql(this: @This(), other: @This()) bool {
|
||||
|
||||
/// Merge _other_ `Style` to _this_ style and overwrite _this_ `Style`'s value
|
||||
/// if the _other_ value differs from the default value.
|
||||
pub fn merge(this: *@This(), other: @This()) void {
|
||||
pub fn merge(this: *Style, other: Style) void {
|
||||
if (other.fg != .default) this.fg = other.fg;
|
||||
if (other.bg != .default) this.bg = other.bg;
|
||||
if (other.ul != .default) this.ul = other.ul;
|
||||
@@ -143,7 +66,7 @@ pub fn merge(this: *@This(), other: @This()) void {
|
||||
if (other.strikethrough != false) this.strikethrough = other.strikethrough;
|
||||
}
|
||||
|
||||
fn start(this: @This(), writer: anytype) !void {
|
||||
fn start(this: Style, writer: anytype) !void {
|
||||
// foreground
|
||||
switch (this.fg) {
|
||||
.default => try std.fmt.format(writer, ctlseqs.fg_reset, .{}),
|
||||
@@ -240,7 +163,7 @@ fn start(this: @This(), writer: anytype) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn end(this: @This(), writer: anytype) !void {
|
||||
fn end(this: Style, writer: anytype) !void {
|
||||
// foreground
|
||||
switch (this.fg) {
|
||||
.default => {},
|
||||
@@ -298,7 +221,7 @@ fn end(this: @This(), writer: anytype) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(this: @This(), writer: anytype, content: u8) !void {
|
||||
pub fn value(this: Style, writer: anytype, content: u8) !void {
|
||||
try this.start(writer);
|
||||
_ = try writer.write(&[_]u8{content});
|
||||
try this.end(writer);
|
||||
@@ -307,7 +230,3 @@ pub fn value(this: @This(), writer: anytype, content: u8) !void {
|
||||
// TODO: implement helper functions for terminal capabilities:
|
||||
// - links / url display (osc 8)
|
||||
// - show / hide cursor?
|
||||
|
||||
test {
|
||||
_ = Color;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
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 Key = @import("key.zig");
|
||||
const Size = @import("size.zig");
|
||||
const Cell = @import("cell.zig");
|
||||
|
||||
const log = std.log.scoped(.terminal);
|
||||
|
||||
@@ -78,13 +78,13 @@ pub fn writer() Writer {
|
||||
return .{ .context = .{} };
|
||||
}
|
||||
|
||||
pub fn setCursorPosition(pos: Position) !void {
|
||||
pub fn setCursorPosition(pos: Size.Position) !void {
|
||||
var buf: [64]u8 = undefined;
|
||||
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col });
|
||||
_ = 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");
|
||||
@@ -227,7 +227,3 @@ fn getReportMode(ps: u8) ReportMode {
|
||||
else => ReportMode.not_recognized,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
_ = Cell;
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
col: u16,
|
||||
row: u16,
|
||||
@@ -1,5 +0,0 @@
|
||||
const Position = @import("Position.zig");
|
||||
|
||||
anchor: Position = .{ .col = 0, .row = 0 }, // top left corner by default
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
116
src/view.zig
116
src/view.zig
@@ -1,116 +0,0 @@
|
||||
//! Dynamic dispatch for view implementations. Each `View` has to implement the `View.Interface`
|
||||
//!
|
||||
//! Create a `View` using `createFrom(object: anytype)` and use them through
|
||||
//! the defined `View.Interface`. The view will take care of calling the
|
||||
//! correct implementation of the corresponding underlying type.
|
||||
//!
|
||||
//! A `View` holds the necessary `Layout`'s for different screen sizes as well
|
||||
//! as the corresponding used `Widget`'s alongside holding the corresponding memory
|
||||
//! for the data shown through the `View`.
|
||||
const std = @import("std");
|
||||
|
||||
const isTaggedUnion = @import("event.zig").isTaggedUnion;
|
||||
|
||||
const log = std.log.scoped(.view);
|
||||
|
||||
pub fn View(comptime Event: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `View(comptime Event: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
const Events = std.ArrayList(Event);
|
||||
return struct {
|
||||
const ViewType = @This();
|
||||
pub const Interface = @import("interface").Interface(.{
|
||||
.handle = fn (anytype, Event) anyerror!*Events,
|
||||
.render = fn (anytype, *Renderer) anyerror!void,
|
||||
.enable = fn (anytype) void,
|
||||
.disable = fn (anytype) void,
|
||||
.deinit = fn (anytype) void,
|
||||
}, .{});
|
||||
|
||||
// TODO: this VTable creation and abstraction could maybe even be done through a comptime implementation -> another library?
|
||||
const VTable = struct {
|
||||
handle: *const fn (this: *ViewType, event: Event) anyerror!*Events,
|
||||
render: *const fn (this: *ViewType, renderer: *Renderer) anyerror!void,
|
||||
enable: *const fn (this: *ViewType) void,
|
||||
disable: *const fn (this: *ViewType) void,
|
||||
deinit: *const fn (this: *ViewType) void,
|
||||
};
|
||||
|
||||
object: *anyopaque = undefined,
|
||||
vtable: *const VTable = undefined,
|
||||
|
||||
/// Handle the provided `Event` for this `View`.
|
||||
pub fn handle(this: *ViewType, event: Event) !*Events {
|
||||
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 `View` given the `Size` of the available terminal (.resize System`Event`).
|
||||
pub fn render(this: *ViewType, renderer: *Renderer) !void {
|
||||
try this.vtable.render(this, renderer);
|
||||
}
|
||||
|
||||
/// Function to call when this `View` will be handled and rendered as the 'active' `View`.
|
||||
pub fn enable(this: *ViewType) void {
|
||||
this.vtable.enable(this);
|
||||
}
|
||||
|
||||
/// Function to call when this `View` will no longer be 'active'.
|
||||
pub fn disable(this: *ViewType) void {
|
||||
this.vtable.disable(this);
|
||||
}
|
||||
|
||||
pub fn deinit(this: *ViewType) void {
|
||||
this.vtable.deinit(this);
|
||||
}
|
||||
|
||||
pub fn createFrom(object: anytype) ViewType {
|
||||
return ViewType{
|
||||
.object = @ptrCast(@alignCast(object)),
|
||||
.vtable = &.{
|
||||
.handle = struct {
|
||||
fn handle(this: *ViewType, event: Event) !*Events {
|
||||
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
|
||||
return view.handle(event);
|
||||
}
|
||||
}.handle,
|
||||
.render = struct {
|
||||
fn render(this: *ViewType, renderer: *Renderer) !void {
|
||||
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
|
||||
try view.render(renderer);
|
||||
}
|
||||
}.render,
|
||||
.enable = struct {
|
||||
fn enable(this: *ViewType) void {
|
||||
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
|
||||
view.enable();
|
||||
}
|
||||
}.enable,
|
||||
.disable = struct {
|
||||
fn disable(this: *ViewType) void {
|
||||
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
|
||||
view.disable();
|
||||
}
|
||||
}.disable,
|
||||
.deinit = struct {
|
||||
fn deinit(this: *ViewType) void {
|
||||
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
|
||||
view.deinit();
|
||||
}
|
||||
}.deinit,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
107
src/widget.zig
107
src/widget.zig
@@ -1,107 +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();
|
||||
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: *anyopaque = 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);
|
||||
}
|
||||
|
||||
pub fn createFrom(object: anytype) WidgetType {
|
||||
return WidgetType{
|
||||
.object = @ptrCast(@alignCast(object)),
|
||||
.vtable = &.{
|
||||
.handle = struct {
|
||||
// Handle the provided `Event` for this `Widget`.
|
||||
fn handle(this: *WidgetType, event: Event) ?Event {
|
||||
const widget: @TypeOf(object) = @ptrCast(@alignCast(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) = @ptrCast(@alignCast(this.object));
|
||||
try widget.render(renderer);
|
||||
}
|
||||
}.render,
|
||||
.deinit = struct {
|
||||
fn deinit(this: *WidgetType) void {
|
||||
const widget: @TypeOf(object) = @ptrCast(@alignCast(this.object));
|
||||
widget.deinit();
|
||||
}
|
||||
}.deinit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: implement a minimal size requirement for Widgets to render correctly?
|
||||
// import and export of `Widget` implementations
|
||||
pub const Input = @import("widget/Input.zig").Widget(Event, Renderer);
|
||||
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);
|
||||
pub const List = @import("widget/List.zig").Widget(Event, Renderer);
|
||||
};
|
||||
// test widget implementation satisfies the interface
|
||||
comptime Type.Interface.satisfiedBy(Type);
|
||||
comptime Type.Interface.satisfiedBy(Type.Input);
|
||||
comptime Type.Interface.satisfiedBy(Type.Text);
|
||||
comptime Type.Interface.satisfiedBy(Type.RawText);
|
||||
comptime Type.Interface.satisfiedBy(Type.Spacer);
|
||||
comptime Type.Interface.satisfiedBy(Type.List);
|
||||
return Type;
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Cell = terminal.Cell;
|
||||
const Key = terminal.Key;
|
||||
const Size = terminal.Size;
|
||||
|
||||
const log = std.log.scoped(.widget_input);
|
||||
|
||||
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 {
|
||||
active: bool,
|
||||
allocator: std.mem.Allocator,
|
||||
label: ?[]const u8,
|
||||
placeholder: ?[]const u8,
|
||||
size: Size,
|
||||
require_render: bool,
|
||||
value: std.ArrayList(u8),
|
||||
/// value content length
|
||||
value_len: usize,
|
||||
/// current cursor position
|
||||
cursor_idx: usize,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, label: ?[]const u8, placeholder: ?[]const u8) *@This() {
|
||||
var value = std.ArrayList(u8).init(allocator);
|
||||
value.resize(32) catch @panic("Input.zig: out of memory");
|
||||
var this = allocator.create(@This()) catch @panic("Input.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.active = false;
|
||||
this.require_render = true;
|
||||
this.label = null;
|
||||
this.placeholder = null;
|
||||
this.value_len = 0;
|
||||
this.cursor_idx = 0;
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
this.placeholder = placeholder;
|
||||
return this;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.value.deinit();
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
pub fn getValue(this: *const @This()) []const u8 {
|
||||
return this.value.items[0..this.value_len];
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) ?Event {
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
this.require_render = true;
|
||||
|
||||
var required_cols: u16 = 4; // '...c'
|
||||
if (this.label) |label| {
|
||||
required_cols += @as(u16, @truncate(label.len)); // <label>
|
||||
required_cols += 2; // ': '
|
||||
}
|
||||
if (this.size.cols < required_cols) {
|
||||
return .{ .err = .{
|
||||
.err = error.InsufficientSize,
|
||||
.msg = "Received Size is too small to render App.Widget.Input correctly",
|
||||
} };
|
||||
}
|
||||
},
|
||||
.key => |key| {
|
||||
if (!this.active) {
|
||||
return null;
|
||||
}
|
||||
if (key.matches(.{ .cp = Key.tab }) or key.matches(.{ .cp = Key.enter })) {
|
||||
// ignored keys
|
||||
} else if (key.mod.alt or key.mod.ctrl or key.matches(.{ .cp = Key.escape })) {
|
||||
// TODO: what about ctrl-v, ctrl-w, alt-b, alt-f?
|
||||
// ignored keys
|
||||
} else if (key.matches(.{ .cp = Key.backspace })) {
|
||||
// remove one character
|
||||
_ = this.value.orderedRemove(this.cursor_idx);
|
||||
this.cursor_idx -|= 1;
|
||||
this.value_len -|= 1;
|
||||
this.require_render = true;
|
||||
} else if (key.matches(.{ .cp = Key.left }) or key.matches(.{ .cp = 'b', .mod = .{ .ctrl = true } })) {
|
||||
// left
|
||||
this.cursor_idx -|= 1;
|
||||
this.require_render = true;
|
||||
} else if (key.matches(.{ .cp = Key.right }) or key.matches(.{ .cp = 'f', .mod = .{ .ctrl = true } })) {
|
||||
// right
|
||||
if (this.cursor_idx < this.value_len) {
|
||||
this.cursor_idx += 1;
|
||||
this.require_render = true;
|
||||
}
|
||||
} else {
|
||||
if (this.value.items.len <= this.value_len) {
|
||||
// double capacity in case we need more space
|
||||
this.value.resize(this.value.capacity * 2) catch |err| {
|
||||
return .{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Could not resize input value buffer",
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
this.value.insert(this.cursor_idx, @as(u8, @truncate(key.cp))) catch @panic("Input.zig: out of memory");
|
||||
this.cursor_idx += 1;
|
||||
this.value_len += 1;
|
||||
this.require_render = true;
|
||||
}
|
||||
// TODO: handle key input that should be used for the input field value
|
||||
// - move cursor using arrow keys
|
||||
// - allow word-wise navigation?
|
||||
// - add / remove characters
|
||||
// - allow removal of words?
|
||||
// - do not support pasting, as that can be done by the terminal emulator (not sure if this would even work correctly over ssh)
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overview of the rendered contents:
|
||||
//
|
||||
// With both label and placeholder:
|
||||
// <label>: <placeholder>
|
||||
// Without any label:
|
||||
// <placeholder>
|
||||
// Without any placeholder, but a label:
|
||||
// <label>: ____________
|
||||
// With neither label nor placeholder:
|
||||
// ____________
|
||||
// When value is not an empty string, the corresponding placeholder
|
||||
// (if any) will be replaced with the current value. The current
|
||||
// cursor position is show when this input field is `active`.
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
if (!this.require_render) {
|
||||
return;
|
||||
}
|
||||
try renderer.clear(this.size);
|
||||
var size = this.size;
|
||||
this.require_render = false;
|
||||
if (this.label) |label| {
|
||||
const label_style: Cell.Style = .{
|
||||
.fg = .default,
|
||||
.italic = true,
|
||||
};
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{
|
||||
.content = label,
|
||||
.style = label_style,
|
||||
},
|
||||
});
|
||||
size.anchor.col += @as(u16, @truncate(label.len));
|
||||
size.cols -= @as(u16, @truncate(label.len));
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{
|
||||
.content = ":",
|
||||
},
|
||||
});
|
||||
size.anchor.col += 2;
|
||||
size.cols -= 2;
|
||||
}
|
||||
if (this.value_len > 0) {
|
||||
var start: usize = 0;
|
||||
// TODO: moving the cursor position will change position of the '..' placement (i.e. at the beginning, at the end or both)
|
||||
// truncate representation according to the available space
|
||||
if (this.value_len >= size.cols - 1) {
|
||||
start = this.value_len -| (size.cols - 3);
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{
|
||||
.content = "..",
|
||||
.style = .{ .dim = true },
|
||||
},
|
||||
});
|
||||
size.anchor.col += 2;
|
||||
size.cols -|= 2;
|
||||
}
|
||||
// print current value representation (and cursor position if active)
|
||||
if (this.cursor_idx == 0 and this.value_len > 1) {
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{
|
||||
.content = this.value.items[0..1],
|
||||
.style = .{ .reverse = true },
|
||||
},
|
||||
.{
|
||||
.content = this.value.items[1..this.value_len],
|
||||
},
|
||||
});
|
||||
} else if (this.cursor_idx == 0 and this.value_len == 1) {
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{
|
||||
.content = this.value.items[0..1],
|
||||
.style = .{ .reverse = true },
|
||||
},
|
||||
});
|
||||
} else if (this.cursor_idx == this.value_len) {
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{
|
||||
.content = this.value.items[start..this.cursor_idx],
|
||||
},
|
||||
.{
|
||||
.content = " ",
|
||||
.style = .{ .reverse = true },
|
||||
},
|
||||
});
|
||||
} else {
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{
|
||||
.content = this.value.items[start..this.cursor_idx],
|
||||
},
|
||||
.{
|
||||
.content = this.value.items[this.cursor_idx .. this.cursor_idx + 1],
|
||||
.style = .{ .reverse = true, .blink = true },
|
||||
},
|
||||
});
|
||||
size.anchor.col += @as(u16, @truncate(this.cursor_idx)) + 1;
|
||||
size.cols -= @as(u16, @truncate(this.cursor_idx)) + 1;
|
||||
if (this.value_len > this.cursor_idx + 1) {
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{
|
||||
.content = this.value.items[this.cursor_idx + 1 .. this.value_len],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.placeholder) |placeholder| {
|
||||
var placeholder_style: Cell.Style = .{
|
||||
.fg = .default,
|
||||
.dim = true,
|
||||
};
|
||||
if (this.active) {
|
||||
placeholder_style.blink = true;
|
||||
}
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{
|
||||
.content = placeholder,
|
||||
.style = placeholder_style,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,147 +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 Size = terminal.Size;
|
||||
|
||||
const log = std.log.scoped(.widget_list);
|
||||
|
||||
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 ListItems = std.ArrayList([]const Cell);
|
||||
return struct {
|
||||
allocator: std.mem.Allocator,
|
||||
idx: usize,
|
||||
config: ListType,
|
||||
contents: ListItems,
|
||||
size: terminal.Size,
|
||||
require_render: bool,
|
||||
|
||||
const ListType = enum {
|
||||
unordered,
|
||||
ordered,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, config: ListType, 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 contents = ListItems.initCapacity(allocator, fields_info.len) catch @panic("List.zig: out of memory");
|
||||
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 != .array and child_type_info != .pointer) {
|
||||
@compileError("child: " ++ field.name ++ " is not an array of const Cell but " ++ @typeName(ChildType));
|
||||
}
|
||||
contents.append(child) catch {};
|
||||
}
|
||||
var this = allocator.create(@This()) catch @panic("List.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.require_render = true;
|
||||
this.idx = 0;
|
||||
this.config = config;
|
||||
this.contents = contents;
|
||||
return this;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.contents.deinit();
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) ?Event {
|
||||
switch (event) {
|
||||
// store the received size
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
this.require_render = true;
|
||||
},
|
||||
.key => |key| {
|
||||
var require_render = true;
|
||||
if (key.matches(.{ .cp = 'g' }) or key.matches(.{ .cp = terminal.Key.home })) {
|
||||
// top
|
||||
if (this.idx != 0) {
|
||||
this.idx = 0;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'G' }) or key.matches(.{ .cp = terminal.Key.end })) {
|
||||
// bottom
|
||||
if (this.idx < this.contents.items.len -| 1) {
|
||||
this.idx = this.contents.items.len -| 1;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'j' }) or key.matches(.{ .cp = terminal.Key.down })) {
|
||||
// down
|
||||
if (this.idx < this.contents.items.len -| 1) {
|
||||
this.idx += 1;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'k' }) or key.matches(.{ .cp = terminal.Key.up })) {
|
||||
// up
|
||||
if (this.idx > 0) {
|
||||
this.idx -= 1;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
this.require_render = require_render;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
if (!this.require_render) {
|
||||
return;
|
||||
}
|
||||
try renderer.clear(this.size);
|
||||
var row: u16 = 0;
|
||||
for (this.contents.items[this.idx..], this.idx + 1..) |content, num| {
|
||||
var size: Size = .{
|
||||
.anchor = .{
|
||||
.col = this.size.anchor.col,
|
||||
.row = this.size.anchor.row + row,
|
||||
},
|
||||
.rows = this.size.rows -| row,
|
||||
.cols = this.size.cols,
|
||||
};
|
||||
switch (this.config) {
|
||||
.unordered => {
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{ .content = "•" },
|
||||
});
|
||||
size.anchor.col += 2;
|
||||
size.cols -|= 2;
|
||||
},
|
||||
.ordered => {
|
||||
var buf: [32]u8 = undefined;
|
||||
const val = try std.fmt.bufPrint(&buf, "{d}.", .{num});
|
||||
try renderer.render(size, &[_]Cell{
|
||||
.{ .content = val },
|
||||
});
|
||||
const cols: u16 = @truncate(val.len + 1);
|
||||
size.anchor.col += cols;
|
||||
size.cols -|= cols;
|
||||
},
|
||||
}
|
||||
try renderer.render(size, content);
|
||||
row += 1; // NOTE: as there are no line breaks currently there will always exactly one line be written
|
||||
}
|
||||
this.require_render = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,127 +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 {
|
||||
allocator: std.mem.Allocator,
|
||||
contents: Contents,
|
||||
line_index: std.ArrayList(usize),
|
||||
line: usize,
|
||||
size: terminal.Size,
|
||||
require_render: bool,
|
||||
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
var this = allocator.create(@This()) catch @panic("RawText.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.line = 0;
|
||||
this.require_render = true;
|
||||
this.contents = contents;
|
||||
this.line_index = line_index;
|
||||
return this;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.contents.deinit();
|
||||
this.line_index.deinit();
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
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' }) or key.matches(.{ .cp = Key.home })) {
|
||||
// top
|
||||
if (this.line != 0) {
|
||||
this.line = 0;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'G' }) or key.matches(.{ .cp = Key.end })) {
|
||||
// bottom
|
||||
if (this.line < this.line_index.items.len -| 1 -| this.size.rows) {
|
||||
this.line = this.line_index.items.len -| 1 -| this.size.rows;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'j' }) or key.matches(.{ .cp = Key.down })) {
|
||||
// down
|
||||
if (this.line < this.line_index.items.len -| 1 -| this.size.rows) {
|
||||
this.line += 1;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'k' }) or key.matches(.{ .cp = Key.up })) {
|
||||
// up
|
||||
if (this.line > 0) {
|
||||
this.line -= 1;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} 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,47 +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 {
|
||||
allocator: std.mem.Allocator,
|
||||
size: terminal.Size,
|
||||
size_changed: bool,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) *@This() {
|
||||
var this = allocator.create(@This()) catch @panic("Space.zig: Failed to create.");
|
||||
this.allocator = allocator;
|
||||
this.size_changed = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
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,141 +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 {
|
||||
allocator: std.mem.Allocator,
|
||||
alignment: Alignment,
|
||||
contents: []const Cell,
|
||||
size: terminal.Size,
|
||||
require_render: bool,
|
||||
|
||||
const Alignment = enum {
|
||||
default,
|
||||
center,
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, alignment: Alignment, contents: []const Cell) *@This() {
|
||||
var this = allocator.create(@This()) catch @panic("Text.zig: Failed to create");
|
||||
this.allocator = allocator;
|
||||
this.require_render = true;
|
||||
this.alignment = alignment;
|
||||
this.contents = contents;
|
||||
return this;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.allocator.destroy(this);
|
||||
}
|
||||
|
||||
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);
|
||||
// update size for aligned contents, default will not change size
|
||||
const size: terminal.Size = blk: {
|
||||
switch (this.alignment) {
|
||||
.default => break :blk this.size,
|
||||
.center => {
|
||||
const length_usize = this.contents.len;
|
||||
const length: u16 = @truncate(length_usize);
|
||||
const cols = @min(length, this.size.cols);
|
||||
const rows = cols / length;
|
||||
break :blk .{
|
||||
.anchor = .{
|
||||
.col = this.size.anchor.col + @divTrunc(this.size.cols, 2) - @divTrunc(cols, 2),
|
||||
.row = this.size.anchor.row + @divTrunc(this.size.rows, 2),
|
||||
},
|
||||
.rows = rows,
|
||||
.cols = cols,
|
||||
};
|
||||
},
|
||||
.top => {
|
||||
const length_usize = this.contents.len;
|
||||
const length: u16 = @truncate(length_usize);
|
||||
const cols = @min(length, this.size.cols);
|
||||
const rows = cols / length;
|
||||
break :blk .{
|
||||
.anchor = .{
|
||||
.col = this.size.anchor.col + @divTrunc(this.size.cols, 2) - @divTrunc(cols, 2),
|
||||
.row = this.size.anchor.row,
|
||||
},
|
||||
.rows = rows,
|
||||
.cols = cols,
|
||||
};
|
||||
},
|
||||
.bottom => {
|
||||
const length_usize = this.contents.len;
|
||||
const length: u16 = @truncate(length_usize);
|
||||
const cols = @min(length, this.size.cols);
|
||||
const rows = cols / length;
|
||||
break :blk .{
|
||||
.anchor = .{
|
||||
.col = this.size.anchor.col + @divTrunc(this.size.cols, 2) - @divTrunc(cols, 2),
|
||||
.row = this.size.anchor.row + this.size.rows - rows,
|
||||
},
|
||||
.rows = rows,
|
||||
.cols = cols,
|
||||
};
|
||||
},
|
||||
.left => {
|
||||
const length_usize = this.contents.len;
|
||||
const length: u16 = @truncate(length_usize);
|
||||
const cols = @min(length, this.size.cols);
|
||||
const rows = cols / length;
|
||||
break :blk .{
|
||||
.anchor = .{
|
||||
.col = this.size.anchor.col,
|
||||
.row = this.size.anchor.row + @divTrunc(this.size.rows, 2) - @divTrunc(rows, 2),
|
||||
},
|
||||
.rows = rows,
|
||||
.cols = cols,
|
||||
};
|
||||
},
|
||||
.right => {
|
||||
const length_usize = this.contents.len;
|
||||
const length: u16 = @truncate(length_usize);
|
||||
const cols = @min(length, this.size.cols);
|
||||
const rows = cols / length;
|
||||
break :blk .{
|
||||
.anchor = .{
|
||||
.col = this.size.anchor.col + this.size.cols - cols,
|
||||
.row = this.size.anchor.row + @divTrunc(this.size.rows, 2) - @divTrunc(rows, 2),
|
||||
},
|
||||
.rows = rows,
|
||||
.cols = cols,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
log.debug("Text.contents: {any}", .{this.contents});
|
||||
try renderer.render(size, this.contents);
|
||||
this.require_render = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
// private imports
|
||||
|
||||
// public import / exports
|
||||
pub const terminal = @import("terminal.zig");
|
||||
pub const App = @import("app.zig").App;
|
||||
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;
|
||||
pub const Cell = @import("cell.zig");
|
||||
pub const Color = @import("color.zig").Color;
|
||||
pub const Key = @import("key.zig");
|
||||
pub const Size = @import("size.zig");
|
||||
pub const Style = @import("style.zig");
|
||||
|
||||
test {
|
||||
_ = @import("terminal.zig");
|
||||
_ = @import("queue.zig");
|
||||
|
||||
_ = Cell;
|
||||
_ = Color;
|
||||
_ = Key;
|
||||
_ = Size;
|
||||
_ = Style;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user