intermediate #1
154
README.md
154
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,155 @@ 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.
|
||||
|
||||
---
|
||||
## Roadmap
|
||||
|
||||
- [ ] Container rendering
|
||||
- [x] Layout
|
||||
- [x] direction
|
||||
- [x] vertical
|
||||
- [x] horizontal
|
||||
- [x] padding
|
||||
- [x] gap
|
||||
- [x] sizing (removed - for now at least)
|
||||
- width
|
||||
- height
|
||||
- options
|
||||
- fit
|
||||
- grow
|
||||
- fixed
|
||||
- percent
|
||||
- [x] Border
|
||||
- [x] sides
|
||||
- [x] corners
|
||||
- [x] separators
|
||||
- [x] Rectangle
|
||||
- [ ] User control
|
||||
- [x] event handling
|
||||
- [x] user content
|
||||
- [ ] Default `Element` implementations
|
||||
- [ ] Scrollable
|
||||
- [ ] user input handling
|
||||
- [ ] vertical
|
||||
- [ ] horizontal
|
||||
- [ ] scroll bar(s) rendering
|
||||
- [ ] vertical
|
||||
- [ ] horizontal
|
||||
- [ ] Content alignment (i.e. standard calculations done with the provided `Size`)
|
||||
- [ ] Text display
|
||||
- [ ] User input
|
||||
- [ ] single line
|
||||
- [ ] multi line
|
||||
- [ ] min size? (I don't have access to the `.resize` `Event`..)
|
||||
|
||||
Decorations should respect the layout and the viewport accordingly. This means
|
||||
that scrollbars are always visible (except there is no need to have a scrollbar)
|
||||
irrelevant depending on the size of the content. The rectangle apply to all
|
||||
cells of the content (and may be overwritten by child elements contents).
|
||||
The border of an element should be around independent of the scrolling of the
|
||||
contents, just like padding.
|
||||
|
||||
### Scrollable contents
|
||||
|
||||
Contents that is scrollable should be done *virtually* through the contents of
|
||||
the `Container`. This means each container contents implements scrolling for
|
||||
itself if required.
|
||||
|
||||
This still has one issue: Layout of child elements that are already too large
|
||||
(i.e. or become too small). The library could provide automatic rendering of a
|
||||
scrollbar given the right parameters however. The scrolling input action would
|
||||
then also be implemented by the user.
|
||||
|
||||
Open questions are regarding the sizing options (i.e. how is the size of a
|
||||
`Container` actually controlled?, how should it be controlled?, etc.). There
|
||||
should be support for the child elements to provide some kind of 'list'
|
||||
functionality built-in.
|
||||
|
||||
**REMINDER**: (mostly for myself) The library should be and remain simple. This
|
||||
means that some code for using the library may be duplicated, but this is not
|
||||
the main goal. Others may provide more re-usable code snippets that build on top
|
||||
of this library instead.
|
||||
|
||||
### User specific event handling and content rendering
|
||||
|
||||
For interactions controlled by the user each container can use an `Element`
|
||||
interface which contains functions which are called by the `Container`
|
||||
during event handling (i.e. `fn handle(..)`) and during rendering (i.e. `fn
|
||||
content(..)`) to provide user specific content and user interaction. The
|
||||
`Element` may be stateful, but may also be stateless and then be re-used in
|
||||
multiple different `Container`s.
|
||||
|
||||
Composing multiple `Element`s currently requires the implementation of a wrapper
|
||||
which contains the `Element`s that need to be handled (should work pretty well
|
||||
for stateless `Element`s). Such *stateless* `Element`s may be provided by this
|
||||
library.
|
||||
|
||||
### Input
|
||||
|
||||
How is the user input handled in the containers? Should there be active
|
||||
containers? Some input may happen for a specific container (i.e. when using
|
||||
mouse input). How would I handle scrolling for outer and inner elements of
|
||||
a container?
|
||||
|
||||
### Archive
|
||||
|
||||
The alignment and sizing options only make sense if both are available. For
|
||||
this the current implementation has the viewport size and the content size too
|
||||
linked. Therefore they have both been removed (at least for now):
|
||||
|
||||
- *fit*: adjust virtual space of container by the size of its children (i.e. a
|
||||
container needs to be able to get the necessary size of its children)
|
||||
- *grow*: use as much space as available (what exactly would be the difference
|
||||
between this option and *fit*?)
|
||||
- *fixed*: use exactly as much cells (in the specified direction)
|
||||
|
||||
- *center*: elements should have their anchor be placed accordingly to their
|
||||
size and the viewport size.
|
||||
- *left*: the anchor remains at zero (relative to the location of the
|
||||
container on the screen) -> similar to the current implementation!
|
||||
- *right*: the anchor is fixed to the right side (i.e. size of the contents -
|
||||
size of the viewport)
|
||||
|
||||
94
build.zig
94
build.zig
@@ -1,110 +1,66 @@
|
||||
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"),
|
||||
// main executable (usually used for testing)
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "zterm",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
stack_example.root_module.addImport("zterm", lib);
|
||||
exe.root_module.addImport("zterm", lib);
|
||||
|
||||
const container_example = b.addExecutable(.{
|
||||
// TODO: add example execution through optional argument to `zig run` to run
|
||||
// an example application instead of the main executable
|
||||
|
||||
// example applications (usually used for documentation and demonstrations)
|
||||
const container = b.addExecutable(.{
|
||||
.name = "container",
|
||||
.root_source_file = b.path("examples/container.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
container_example.root_module.addImport("zterm", lib);
|
||||
container.root_module.addImport("zterm", lib);
|
||||
b.installArtifact(container);
|
||||
|
||||
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);
|
||||
// zig build run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
// Allow additional arguments, like this: `zig build run -- arg1 arg2 etc`
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
|
||||
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);
|
||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||
// and can be selected like this: `zig build run`
|
||||
// This will evaluate the `run` step rather than the default, which is "install".
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
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.
|
||||
// zig build test
|
||||
const lib_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/zterm.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
lib_unit_tests.root_module.addImport("zg", zg.module("code_point"));
|
||||
lib_unit_tests.root_module.addImport("code_point", zg.module("code_point"));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -24,12 +24,8 @@
|
||||
// internet connectivity.
|
||||
.dependencies = .{
|
||||
.zg = .{
|
||||
.url = "git+https://codeberg.org/atman/zg#a363f507fc39b96fc48d693665a823a358345326",
|
||||
.hash = "1220fe42e39fd141c84fd7d5cf69945309bb47253033e68788f99bdfe5585fbc711a",
|
||||
},
|
||||
.interface = .{
|
||||
.url = "git+https://github.com/yves-biener/zig-interface#ef47e045df19e09250fff45c0702d014fb3d3c37",
|
||||
.hash = "1220a442e8d9b813572bab7a55eef504c83b628f0b17fd283e776dbc1d1a3d98e842",
|
||||
.url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc",
|
||||
.hash = "1220f3e29bc40856bfc06e0ee133f814b0011c76de987d8a6a458c2f34d82708899a",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
|
||||
@@ -1,96 +1,152 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Direct,
|
||||
true,
|
||||
);
|
||||
const App = zterm.App(union(enum) {});
|
||||
const Key = zterm.Key;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
|
||||
const log = std.log.scoped(.container);
|
||||
const log = std.log.scoped(.example);
|
||||
|
||||
pub const ExampleElement = packed struct {
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.handle = handle,
|
||||
.content = content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// example function to render contents for a `Container`
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
|
||||
// NOTE: error should only be returned here in case an in-recoverable exception has occurred
|
||||
const row = size.rows / 2;
|
||||
const col = size.cols / 2 -| 3;
|
||||
|
||||
for (0..5) |c| {
|
||||
cells[(row * size.cols) + col + c].style.fg = .black;
|
||||
cells[(row * size.cols) + col + c].cp = '-';
|
||||
}
|
||||
}
|
||||
|
||||
// example function to handle events for a `Container`
|
||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||
_ = ctx;
|
||||
switch (event) {
|
||||
.init => log.debug(".init event", .{}),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
// TODO: maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// 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 app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
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();
|
||||
var element_wrapper = ExampleElement{};
|
||||
const element = element_wrapper.element();
|
||||
|
||||
try app.start(null);
|
||||
defer app.stop() catch unreachable;
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.border = .{
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.line = .double,
|
||||
},
|
||||
},
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .all(5),
|
||||
.direction = .vertical,
|
||||
},
|
||||
}, element);
|
||||
var box = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
.layout = .{
|
||||
.gap = 1,
|
||||
.direction = .vertical,
|
||||
.padding = .vertical(1),
|
||||
},
|
||||
}, .{});
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, element));
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, .{}));
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, element));
|
||||
try container.append(box);
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.border = .{
|
||||
.color = .light_blue,
|
||||
.sides = .vertical,
|
||||
},
|
||||
}, .{}));
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
defer container.deinit(); // also de-initializes the children
|
||||
|
||||
// App.Event loop
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
// event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
.resize => |size| {
|
||||
renderer.resize(size);
|
||||
.init => {
|
||||
try container.handle(event);
|
||||
continue; // do not render
|
||||
},
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.key => |key| {
|
||||
// ctrl+c to quit
|
||||
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
app.quit();
|
||||
if (key.matches(.{ .cp = 'q' })) app.quit();
|
||||
|
||||
if (key.matches(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
||||
try app.interrupt();
|
||||
defer app.start() catch @panic("could not start app event loop");
|
||||
var child = std.process.Child.init(&.{"hx"}, allocator);
|
||||
_ = child.spawnAndWait() catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Spawning $EDITOR failed",
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
|
||||
},
|
||||
// NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
|
||||
// NOTE: returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,130 +0,0 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Direct,
|
||||
true,
|
||||
);
|
||||
const Key = zterm.Key;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
|
||||
const log = std.log.scoped(.stack);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// fail test; can't try in defer as defer is executed after we return
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer: App.Renderer = .{};
|
||||
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
|
||||
// -> size hint how much should it use?
|
||||
|
||||
var layout = Layout.createFrom(Layout.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(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);
|
||||
}
|
||||
}
|
||||
151
examples/tui.zig
151
examples/tui.zig
@@ -1,151 +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.Direct,
|
||||
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;
|
||||
// FIXME: the layout creates an 'incorrect alignment'?
|
||||
tui.layout = Layout.createFrom(Layout.VContainer.init(allocator, .{
|
||||
.{
|
||||
Layout.createFrom(Layout.Framing.init(allocator, .{
|
||||
.title = .{
|
||||
.str = "Welcome to my terminal website",
|
||||
.style = .{
|
||||
.ul = .{ .index = 6 },
|
||||
.ul_style = .single,
|
||||
},
|
||||
},
|
||||
}, .{
|
||||
.layout = Layout.createFrom(Layout.HContainer.init(allocator, .{
|
||||
.{
|
||||
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
|
||||
.{ .content = "Yves Biener", .style = .{ .bold = true } },
|
||||
})),
|
||||
25,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
|
||||
.{ .content = "File name", .style = .{ .bold = true } },
|
||||
})),
|
||||
50,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
|
||||
.{ .content = "Contacts", .style = .{ .bold = true } },
|
||||
})),
|
||||
25,
|
||||
},
|
||||
})),
|
||||
})),
|
||||
10,
|
||||
},
|
||||
.{
|
||||
Layout.createFrom(Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{
|
||||
.widget = Widget.createFrom(Widget.Text.init(allocator, .default, &[1]Cell{
|
||||
.{ .content = "Does this change anything", .style = .{ .ul = .default, .ul_style = .single } },
|
||||
})),
|
||||
})),
|
||||
90,
|
||||
},
|
||||
}));
|
||||
return tui;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Tui) void {
|
||||
this.layout.deinit();
|
||||
this.allocator.destroy(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 = .{};
|
||||
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);
|
||||
}
|
||||
}
|
||||
122
src/app.zig
122
src/app.zig
@@ -6,7 +6,8 @@ const event = @import("event.zig");
|
||||
const mergeTaggedUnions = event.mergeTaggedUnions;
|
||||
const isTaggedUnion = event.isTaggedUnion;
|
||||
|
||||
const Key = terminal.Key;
|
||||
const Key = @import("key.zig");
|
||||
const Size = @import("size.zig").Size;
|
||||
const Queue = @import("queue.zig").Queue;
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
@@ -15,14 +16,6 @@ const log = std.log.scoped(.app);
|
||||
/// an tagged union for all the user events that can be send through the
|
||||
/// applications event loop.
|
||||
///
|
||||
/// _R_ is the type function for the `Renderer` to use. The parameter boolean
|
||||
/// will be set to the _fullscreen_ value at compile time. The corresponding
|
||||
/// `Renderer` type is accessible through the generated type of this function.
|
||||
///
|
||||
/// _fullscreen_ will be used to configure the `App` and the `Renderer` to
|
||||
/// respect the corresponding configuration whether to render a fullscreen tui
|
||||
/// or an inline tui.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Create an `App` which renders using the `PlainRenderer` in fullscreen with
|
||||
@@ -32,41 +25,43 @@ const log = std.log.scoped(.app);
|
||||
/// const zterm = @import("zterm");
|
||||
/// const App = zterm.App(
|
||||
/// union(enum) {},
|
||||
/// zterm.Renderer.Direct,
|
||||
/// true,
|
||||
/// );
|
||||
/// // later on use
|
||||
/// var app: App = .{};
|
||||
/// var renderer: App.Renderer = .{};
|
||||
/// // later on create an `App` instance and start the event loop
|
||||
/// var app: App = .init;
|
||||
/// try app.start();
|
||||
/// defer app.stop() catch unreachable;
|
||||
/// ```
|
||||
pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fullscreen: bool) type {
|
||||
pub fn App(comptime E: type) type {
|
||||
if (!isTaggedUnion(E)) {
|
||||
@compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
return struct {
|
||||
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
|
||||
pub const Renderer = R(fullscreen);
|
||||
pub const Layout = @import("layout.zig").Layout(Event, Renderer);
|
||||
pub const Widget = @import("widget.zig").Widget(Event, Renderer);
|
||||
pub const View = @import("view.zig").View(Event, Renderer);
|
||||
pub const Container = @import("container.zig").Container(Event);
|
||||
pub const Element = @import("element.zig").Element(Event);
|
||||
|
||||
queue: Queue(Event, 256) = .{},
|
||||
thread: ?std.Thread = null,
|
||||
quit_event: std.Thread.ResetEvent = .{},
|
||||
queue: Queue(Event, 256),
|
||||
thread: ?std.Thread,
|
||||
quit_event: std.Thread.ResetEvent,
|
||||
termios: ?std.posix.termios = null,
|
||||
attached_handler: bool = false,
|
||||
min_size: ?terminal.Size = null,
|
||||
prev_size: terminal.Size = .{ .cols = 0, .rows = 0 },
|
||||
prev_size: Size,
|
||||
|
||||
pub const SignalHandler = struct {
|
||||
context: *anyopaque,
|
||||
callback: *const fn (context: *anyopaque) void,
|
||||
};
|
||||
|
||||
pub fn start(this: *@This(), min_size: ?terminal.Size) !void {
|
||||
if (fullscreen) { // a minimal size only really makes sense if the application is rendered fullscreen
|
||||
this.min_size = min_size;
|
||||
}
|
||||
pub const init: @This() = .{
|
||||
.queue = .{},
|
||||
.thread = null,
|
||||
.quit_event = .{},
|
||||
.termios = null,
|
||||
.attached_handler = false,
|
||||
.prev_size = .{},
|
||||
};
|
||||
|
||||
pub fn start(this: *@This()) !void {
|
||||
if (this.thread) |_| return;
|
||||
|
||||
if (!this.attached_handler) {
|
||||
@@ -82,6 +77,9 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
.callback = @This().winsizeCallback,
|
||||
});
|
||||
this.attached_handler = true;
|
||||
|
||||
// post init event (as the very first element to be in the queue - event loop)
|
||||
this.postEvent(.init);
|
||||
}
|
||||
|
||||
this.quit_event.reset();
|
||||
@@ -89,22 +87,22 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
|
||||
var termios: std.posix.termios = undefined;
|
||||
try terminal.enableRawMode(&termios);
|
||||
if (this.termios) |_| {} else {
|
||||
this.termios = termios;
|
||||
}
|
||||
if (fullscreen) {
|
||||
if (this.termios) |_| {} else this.termios = termios;
|
||||
|
||||
try terminal.saveScreen();
|
||||
try terminal.enterAltScreen();
|
||||
try terminal.hideCursor();
|
||||
}
|
||||
|
||||
// send initial size afterwards
|
||||
const size = terminal.getTerminalSize();
|
||||
this.postEvent(.{ .resize = size });
|
||||
this.prev_size = size;
|
||||
}
|
||||
|
||||
pub fn interrupt(this: *@This()) !void {
|
||||
this.quit_event.set();
|
||||
if (fullscreen) {
|
||||
try terminal.exitAltScreen();
|
||||
try terminal.restoreScreen();
|
||||
}
|
||||
if (this.thread) |thread| {
|
||||
thread.join();
|
||||
this.thread = null;
|
||||
@@ -115,12 +113,10 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
try this.interrupt();
|
||||
if (this.termios) |*termios| {
|
||||
try terminal.disableRawMode(termios);
|
||||
if (fullscreen) {
|
||||
try terminal.showCursor();
|
||||
try terminal.exitAltScreen();
|
||||
try terminal.restoreScreen();
|
||||
}
|
||||
}
|
||||
this.termios = null;
|
||||
}
|
||||
|
||||
@@ -144,15 +140,6 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
fn winsizeCallback(ptr: *anyopaque) void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ptr));
|
||||
const size = terminal.getTerminalSize();
|
||||
// check for minimal size (if any was provided)
|
||||
if (this.min_size) |min_size| {
|
||||
if (size.cols < min_size.cols or size.rows < min_size.rows) {
|
||||
this.postEvent(.{
|
||||
.err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
|
||||
this.postEvent(.{ .resize = size });
|
||||
this.prev_size = size;
|
||||
@@ -178,19 +165,7 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
// send initial terminal size
|
||||
// changes are handled by the winch signal handler
|
||||
// see `App.start` and `App.registerWinch` for details
|
||||
{
|
||||
// TODO: what should happen if the initial window size is too small?
|
||||
// -> currently the first render call will then crash the application (which happens anyway)
|
||||
const size = terminal.getTerminalSize();
|
||||
if (this.min_size) |min_size| {
|
||||
if (size.cols < min_size.cols or size.rows < min_size.rows) {
|
||||
this.postEvent(.{
|
||||
.err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" },
|
||||
});
|
||||
}
|
||||
}
|
||||
this.postEvent(.{ .resize = size });
|
||||
}
|
||||
{}
|
||||
|
||||
// thread to read user inputs
|
||||
var buf: [256]u8 = undefined;
|
||||
@@ -202,9 +177,8 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
if (buf[0] == 0x1b and read_bytes > 1) {
|
||||
switch (buf[1]) {
|
||||
0x4F => { // ss3
|
||||
if (read_bytes < 3) {
|
||||
continue;
|
||||
}
|
||||
if (read_bytes < 3) continue;
|
||||
|
||||
const key: ?Key = switch (buf[2]) {
|
||||
0x1B => null,
|
||||
'A' => .{ .cp = Key.up },
|
||||
@@ -220,14 +194,11 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
'S' => .{ .cp = Key.f4 },
|
||||
else => null,
|
||||
};
|
||||
if (key) |k| {
|
||||
this.postEvent(.{ .key = k });
|
||||
}
|
||||
if (key) |k| this.postEvent(.{ .key = k });
|
||||
},
|
||||
0x5B => { // csi
|
||||
if (read_bytes < 3) {
|
||||
continue;
|
||||
}
|
||||
if (read_bytes < 3) continue;
|
||||
|
||||
// We start iterating at index 2 to get past the '['
|
||||
const sequence = for (buf[2..], 2..) |b, i| {
|
||||
switch (b) {
|
||||
@@ -322,19 +293,10 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
|
||||
// TODO: only post the event if the size has changed?
|
||||
// because there might be too many resize events (which force a re-draw of the entire screen)
|
||||
const size: terminal.Size = .{
|
||||
const size: Size = .{
|
||||
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
|
||||
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
|
||||
};
|
||||
// check for minimal size (if any was provided)
|
||||
if (this.min_size) |min_size| {
|
||||
if (size.cols < min_size.cols or size.rows < min_size.rows) {
|
||||
this.postEvent(.{
|
||||
.err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" },
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
|
||||
this.postEvent(.{ .resize = size });
|
||||
this.prev_size = size;
|
||||
@@ -372,9 +334,7 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
0x7f => .{ .cp = Key.backspace },
|
||||
else => {
|
||||
var iter = terminal.code_point.Iterator{ .bytes = buf[0..read_bytes] };
|
||||
while (iter.next()) |cp| {
|
||||
this.postEvent(.{ .key = .{ .cp = cp.code } });
|
||||
}
|
||||
while (iter.next()) |cp| this.postEvent(.{ .key = .{ .cp = cp.code } });
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
21
src/cell.zig
Normal file
21
src/cell.zig
Normal file
@@ -0,0 +1,21 @@
|
||||
const std = @import("std");
|
||||
const Style = @import("style.zig");
|
||||
|
||||
pub const Cell = @This();
|
||||
|
||||
style: Style = .{ .attributes = &.{} },
|
||||
// TODO: embrace `zg` dependency more due to utf-8 encoding
|
||||
cp: u21 = ' ',
|
||||
|
||||
pub fn eql(this: Cell, other: Cell) bool {
|
||||
return this.cp == other.cp and this.style.eql(other.style);
|
||||
}
|
||||
|
||||
pub fn reset(this: *Cell) void {
|
||||
this.style = .{ .attributes = &.{} };
|
||||
this.cp = ' ';
|
||||
}
|
||||
|
||||
pub fn value(this: Cell, writer: anytype) !void {
|
||||
try this.style.value(writer, this.cp);
|
||||
}
|
||||
38
src/color.zig
Normal file
38
src/color.zig
Normal file
@@ -0,0 +1,38 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Color = enum(u8) {
|
||||
default = 0,
|
||||
black = 16,
|
||||
light_red = 1,
|
||||
light_green,
|
||||
light_yellow,
|
||||
light_blue,
|
||||
light_magenta,
|
||||
light_cyan,
|
||||
light_grey,
|
||||
grey,
|
||||
red,
|
||||
green,
|
||||
yellow,
|
||||
blue,
|
||||
magenta,
|
||||
cyan,
|
||||
white,
|
||||
// TODO: add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
|
||||
|
||||
pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void {
|
||||
if (this == .default) {
|
||||
switch (coloring) {
|
||||
.fg => try std.fmt.format(writer, "39", .{}),
|
||||
.bg => try std.fmt.format(writer, "49", .{}),
|
||||
.ul => try std.fmt.format(writer, "59", .{}),
|
||||
}
|
||||
} else {
|
||||
switch (coloring) {
|
||||
.fg => try std.fmt.format(writer, "38;5;{d}", .{@intFromEnum(this)}),
|
||||
.bg => try std.fmt.format(writer, "48;5;{d}", .{@intFromEnum(this)}),
|
||||
.ul => try std.fmt.format(writer, "58;5;{d}", .{@intFromEnum(this)}),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
406
src/container.zig
Normal file
406
src/container.zig
Normal file
@@ -0,0 +1,406 @@
|
||||
const std = @import("std");
|
||||
|
||||
const isTaggedUnion = @import("event.zig").isTaggedUnion;
|
||||
|
||||
const Cell = @import("cell.zig");
|
||||
const Color = @import("color.zig").Color;
|
||||
const Size = @import("size.zig").Size;
|
||||
const Style = @import("style.zig");
|
||||
|
||||
const log = std.log.scoped(.container);
|
||||
|
||||
/// Border configuration struct
|
||||
pub const Border = packed struct {
|
||||
pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' };
|
||||
pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' };
|
||||
pub const line: [2]u21 = .{ '│', '─' };
|
||||
pub const dotted: [2]u21 = .{ '┆', '┄' };
|
||||
pub const double: [2]u21 = .{ '║', '═' };
|
||||
/// Color to use for the border
|
||||
color: Color = .default,
|
||||
/// Configure the corner type to be used for the border
|
||||
corners: enum(u1) {
|
||||
squared,
|
||||
rounded,
|
||||
} = .squared,
|
||||
/// Configure the sides where the borders shall be rendered
|
||||
sides: packed struct {
|
||||
top: bool = false,
|
||||
bottom: bool = false,
|
||||
left: bool = false,
|
||||
right: bool = false,
|
||||
|
||||
/// Enable border sides for all four sides
|
||||
pub const all: @This() = .{ .top = true, .bottom = true, .left = true, .right = true };
|
||||
/// Enable border sides for the left and right sides
|
||||
pub const horizontal: @This() = .{ .left = true, .right = true };
|
||||
/// Enable border sides for the top and bottom sides
|
||||
pub const vertical: @This() = .{ .top = true, .bottom = true };
|
||||
} = .{},
|
||||
/// Configure separator borders between child element to added to the layout
|
||||
separator: packed struct {
|
||||
enabled: bool = false,
|
||||
color: Color = .white,
|
||||
line: enum(u2) {
|
||||
line,
|
||||
dotted,
|
||||
double,
|
||||
} = .line,
|
||||
} = .{},
|
||||
|
||||
// NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows`
|
||||
pub fn contents(this: @This(), cells: []Cell, size: Size, layout: Layout, len: u16) void {
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
|
||||
const frame = switch (this.corners) {
|
||||
.rounded => Border.rounded_border,
|
||||
.squared => Border.squared_border,
|
||||
};
|
||||
std.debug.assert(frame.len == 6);
|
||||
|
||||
// render top and bottom border
|
||||
for (0..size.cols) |col| {
|
||||
const last_row = @as(usize, size.rows - 1) * @as(usize, size.cols);
|
||||
if (this.sides.left and col == 0) {
|
||||
// top left corner
|
||||
if (this.sides.top) cells[col].cp = frame[0];
|
||||
// bottom left corner
|
||||
if (this.sides.bottom) cells[last_row + col].cp = frame[4];
|
||||
} else if (this.sides.right and col == size.cols - 1) {
|
||||
// top right corner
|
||||
if (this.sides.top) cells[col].cp = frame[2];
|
||||
// bottom left corner
|
||||
if (this.sides.bottom) cells[last_row + col].cp = frame[5];
|
||||
} else {
|
||||
// top side
|
||||
if (this.sides.top) cells[col].cp = frame[1];
|
||||
// bottom side
|
||||
if (this.sides.bottom) cells[last_row + col].cp = frame[1];
|
||||
}
|
||||
if (this.sides.top) cells[col].style.fg = this.color;
|
||||
if (this.sides.bottom) cells[last_row + col].style.fg = this.color;
|
||||
}
|
||||
// render left and right border
|
||||
for (1..size.rows -| 1) |row| {
|
||||
const idx = (row * size.cols);
|
||||
if (this.sides.left) {
|
||||
cells[idx].cp = frame[3]; // left
|
||||
cells[idx].style.fg = this.color;
|
||||
}
|
||||
if (this.sides.right) {
|
||||
cells[idx + size.cols - 1].cp = frame[3]; // right
|
||||
cells[idx + size.cols - 1].style.fg = this.color;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.separator.enabled) {
|
||||
// calculate where the separator would need to be
|
||||
const element_cols = blk: {
|
||||
var cols = size.cols - layout.gap * (len - 1);
|
||||
if (this.sides.left) cols -= 1;
|
||||
if (this.sides.right) cols -= 1;
|
||||
cols -= layout.padding.left + layout.padding.right;
|
||||
break :blk @divTrunc(cols, len);
|
||||
};
|
||||
const element_rows = blk: {
|
||||
var rows = size.rows - layout.gap * (len - 1);
|
||||
if (this.sides.top) rows -= 1;
|
||||
if (this.sides.bottom) rows -= 1;
|
||||
rows -= layout.padding.top + layout.padding.bottom;
|
||||
break :blk @divTrunc(rows, len);
|
||||
};
|
||||
var offset: u16 = switch (layout.direction) {
|
||||
.horizontal => layout.padding.left,
|
||||
.vertical => layout.padding.top,
|
||||
};
|
||||
var overflow = switch (layout.direction) {
|
||||
.horizontal => blk: {
|
||||
var cols = size.cols - layout.gap * (len - 1);
|
||||
if (this.sides.left) cols -= 1;
|
||||
if (this.sides.right) cols -= 1;
|
||||
cols -= layout.padding.left + layout.padding.right;
|
||||
break :blk cols - element_cols * len;
|
||||
},
|
||||
.vertical => blk: {
|
||||
var rows = size.rows - layout.gap * (len - 1);
|
||||
if (this.sides.top) rows -= 1;
|
||||
if (this.sides.bottom) rows -= 1;
|
||||
rows -= layout.padding.top + layout.padding.bottom;
|
||||
break :blk rows - element_rows * len;
|
||||
},
|
||||
};
|
||||
const line_cps: [2]u21 = switch (this.separator.line) {
|
||||
.line => line,
|
||||
.dotted => dotted,
|
||||
.double => double,
|
||||
};
|
||||
switch (layout.direction) {
|
||||
.horizontal => {
|
||||
offset += layout.gap / 2;
|
||||
for (0..len - 1) |_| {
|
||||
var cols = element_cols;
|
||||
if (overflow > 0) {
|
||||
overflow -|= 1;
|
||||
cols += 1;
|
||||
}
|
||||
offset += cols;
|
||||
for (1..size.rows -| 1) |row| {
|
||||
cells[row * size.cols + offset].cp = line_cps[0];
|
||||
cells[row * size.cols + offset].style.fg = this.separator.color;
|
||||
}
|
||||
offset += layout.gap;
|
||||
}
|
||||
},
|
||||
.vertical => {
|
||||
offset += layout.gap / 2;
|
||||
for (0..len - 1) |_| {
|
||||
var rows = element_rows;
|
||||
if (overflow > 0) {
|
||||
overflow -|= 1;
|
||||
rows += 1;
|
||||
}
|
||||
offset += rows;
|
||||
for (1..size.cols -| 1) |col| {
|
||||
cells[offset * size.cols + col].cp = line_cps[1];
|
||||
cells[offset * size.cols + col].style.fg = this.separator.color;
|
||||
}
|
||||
offset += layout.gap;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Rectangle configuration struct
|
||||
pub const Rectangle = packed struct {
|
||||
/// `Color` to use to fill the `Rectangle` with
|
||||
/// NOTE: used as background color when rendering! such that it renders the
|
||||
/// children accordingly without removing the coloring of the `Rectangle`
|
||||
fill: Color = .default,
|
||||
|
||||
// NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows`
|
||||
pub fn contents(this: @This(), cells: []Cell, size: Size) void {
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
|
||||
for (0..size.rows) |row| {
|
||||
for (0..size.cols) |col| {
|
||||
cells[(row * size.cols) + col].style.bg = this.fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Layout configuration struct
|
||||
pub const Layout = packed struct {
|
||||
/// control the direction in which child elements are laid out
|
||||
direction: enum(u1) { horizontal, vertical } = .horizontal,
|
||||
/// Padding outside of the child elements
|
||||
padding: packed struct {
|
||||
top: u16 = 0,
|
||||
bottom: u16 = 0,
|
||||
left: u16 = 0,
|
||||
right: u16 = 0,
|
||||
|
||||
/// Create a padding with equivalent padding in all four directions.
|
||||
pub fn all(padding: u16) @This() {
|
||||
return .{ .top = padding, .bottom = padding, .left = padding, .right = padding };
|
||||
}
|
||||
|
||||
/// Create a padding with equivalent padding in the left and right directions; others directions remain the default value.
|
||||
pub fn horizontal(padding: u16) @This() {
|
||||
return .{ .left = padding, .right = padding };
|
||||
}
|
||||
|
||||
/// Create a padding with equivalent padding in the top and bottom directions; others directions remain the default value.
|
||||
pub fn vertical(padding: u16) @This() {
|
||||
return .{ .top = padding, .bottom = padding };
|
||||
}
|
||||
} = .{},
|
||||
/// Padding used in between child elements as gaps when laid out
|
||||
gap: u16 = 0,
|
||||
};
|
||||
|
||||
pub fn Container(comptime Event: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Container(comptime Event: type)`");
|
||||
}
|
||||
const Element = @import("element.zig").Element(Event);
|
||||
return struct {
|
||||
allocator: std.mem.Allocator,
|
||||
size: Size,
|
||||
properties: Properties,
|
||||
element: Element,
|
||||
elements: std.ArrayList(@This()),
|
||||
|
||||
/// Properties for each `Container` to configure their layout,
|
||||
/// border, styling, etc. For details see the corresponding individual
|
||||
/// documentation of the members of this struct accordingly.
|
||||
pub const Properties = packed struct {
|
||||
border: Border = .{},
|
||||
rectangle: Rectangle = .{},
|
||||
layout: Layout = .{},
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
properties: Properties,
|
||||
element: Element,
|
||||
) !@This() {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.size = .{},
|
||||
.properties = properties,
|
||||
.element = element,
|
||||
.elements = std.ArrayList(@This()).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
for (this.elements.items) |*element| {
|
||||
element.deinit();
|
||||
}
|
||||
this.elements.deinit();
|
||||
}
|
||||
|
||||
pub fn append(this: *@This(), element: @This()) !void {
|
||||
try this.elements.append(element);
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !void {
|
||||
switch (event) {
|
||||
.resize => |size| resize: {
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
this.size = size;
|
||||
|
||||
if (this.elements.items.len == 0) break :resize;
|
||||
|
||||
if (this.properties.border.separator.enabled) this.properties.layout.gap += 1;
|
||||
|
||||
const layout = this.properties.layout;
|
||||
const sides = this.properties.border.sides;
|
||||
const padding = layout.padding;
|
||||
const gap = layout.gap;
|
||||
|
||||
const len: u16 = @truncate(this.elements.items.len);
|
||||
const element_cols = blk: {
|
||||
var cols = size.cols - gap * (len - 1);
|
||||
if (sides.left) cols -= 1;
|
||||
if (sides.right) cols -= 1;
|
||||
cols -= padding.left + padding.right;
|
||||
break :blk @divTrunc(cols, len);
|
||||
};
|
||||
const element_rows = blk: {
|
||||
var rows = size.rows - gap * (len - 1);
|
||||
if (sides.top) rows -= 1;
|
||||
if (sides.bottom) rows -= 1;
|
||||
rows -= padding.top + padding.bottom;
|
||||
break :blk @divTrunc(rows, len);
|
||||
};
|
||||
var offset: u16 = switch (layout.direction) {
|
||||
.horizontal => padding.left,
|
||||
.vertical => padding.top,
|
||||
};
|
||||
var overflow = switch (layout.direction) {
|
||||
.horizontal => blk: {
|
||||
var cols = size.cols - gap * (len - 1);
|
||||
if (sides.left) cols -= 1;
|
||||
if (sides.right) cols -= 1;
|
||||
cols -= padding.left + padding.right;
|
||||
break :blk cols - element_cols * len;
|
||||
},
|
||||
.vertical => blk: {
|
||||
var rows = size.rows - gap * (len - 1);
|
||||
if (sides.top) rows -= 1;
|
||||
if (sides.bottom) rows -= 1;
|
||||
rows -= padding.top + padding.bottom;
|
||||
break :blk rows - element_rows * len;
|
||||
},
|
||||
};
|
||||
|
||||
for (this.elements.items) |*element| {
|
||||
var element_size: Size = undefined;
|
||||
switch (layout.direction) {
|
||||
.horizontal => {
|
||||
var cols = element_cols;
|
||||
if (overflow > 0) {
|
||||
overflow -|= 1;
|
||||
cols += 1;
|
||||
}
|
||||
element_size = .{
|
||||
.anchor = .{
|
||||
.col = this.size.anchor.col + offset,
|
||||
.row = this.size.anchor.row,
|
||||
},
|
||||
.cols = cols,
|
||||
.rows = size.rows,
|
||||
};
|
||||
// border
|
||||
if (sides.top) element_size.rows -= 1;
|
||||
if (sides.bottom) element_size.rows -= 1;
|
||||
// padding
|
||||
element_size.anchor.row += padding.top;
|
||||
element_size.rows -= padding.top + padding.bottom;
|
||||
// gap
|
||||
offset += gap;
|
||||
offset += cols;
|
||||
},
|
||||
.vertical => {
|
||||
var rows = element_rows;
|
||||
if (overflow > 0) {
|
||||
overflow -|= 1;
|
||||
rows += 1;
|
||||
}
|
||||
element_size = .{
|
||||
.anchor = .{
|
||||
.col = this.size.anchor.col,
|
||||
.row = this.size.anchor.row + offset,
|
||||
},
|
||||
.cols = size.cols,
|
||||
.rows = rows,
|
||||
};
|
||||
// border
|
||||
if (sides.left) element_size.cols -= 1;
|
||||
if (sides.right) element_size.cols -= 1;
|
||||
// padding
|
||||
element_size.anchor.col += padding.left;
|
||||
element_size.cols -= padding.left + padding.right;
|
||||
// gap
|
||||
offset += gap;
|
||||
offset += rows;
|
||||
},
|
||||
}
|
||||
|
||||
// border resizing
|
||||
if (sides.top) element_size.anchor.row += 1;
|
||||
if (sides.left) element_size.anchor.col += 1;
|
||||
|
||||
try element.handle(.{ .resize = element_size });
|
||||
}
|
||||
},
|
||||
else => {
|
||||
try this.element.handle(event);
|
||||
for (this.elements.items) |*element| try element.handle(event);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contents(this: *const @This()) ![]const Cell {
|
||||
const cells = try this.allocator.alloc(Cell, @as(usize, this.size.cols) * @as(usize, this.size.rows));
|
||||
@memset(cells, .{});
|
||||
errdefer this.allocator.free(cells);
|
||||
|
||||
this.properties.border.contents(cells, this.size, this.properties.layout, @truncate(this.elements.items.len));
|
||||
this.properties.rectangle.contents(cells, this.size);
|
||||
|
||||
try this.element.content(cells, this.size);
|
||||
|
||||
return cells;
|
||||
}
|
||||
};
|
||||
}
|
||||
70
src/element.zig
Normal file
70
src/element.zig
Normal file
@@ -0,0 +1,70 @@
|
||||
//! Interface for Element's which describe the contents of a `Container`.
|
||||
const std = @import("std");
|
||||
|
||||
const Cell = @import("cell.zig");
|
||||
const Size = @import("size.zig").Size;
|
||||
|
||||
pub fn Element(Event: type) type {
|
||||
return struct {
|
||||
ptr: *anyopaque = undefined,
|
||||
vtable: *const VTable = &.{},
|
||||
|
||||
pub const VTable = struct {
|
||||
handle: ?*const fn (ctx: *anyopaque, event: Event) anyerror!void = null,
|
||||
content: ?*const fn (ctx: *anyopaque, cells: []Cell, size: Size) anyerror!void = null,
|
||||
};
|
||||
|
||||
/// Handle the received event. The event is one of the user provided
|
||||
/// events or a system event, with the exception of the `.resize`
|
||||
/// `Event` as every `Container` already handles that event.
|
||||
///
|
||||
/// In case of user errors this function should return an error. This
|
||||
/// error may then be used by the application to display information
|
||||
/// about the user error.
|
||||
pub inline fn handle(this: @This(), event: Event) !void {
|
||||
if (this.vtable.handle) |handle_fn|
|
||||
try handle_fn(this.ptr, event);
|
||||
}
|
||||
|
||||
/// Write content into the `cells` of the `Container`. The associated
|
||||
/// `cells` slice has the size of (`size.cols * size.rows`). The
|
||||
/// renderer will know where to place the contents on the screen.
|
||||
///
|
||||
/// This function should only fail with an error if the error is
|
||||
/// non-recoverable (i.e. an allocation error, system error, etc.).
|
||||
/// Otherwise user specific errors should be caught using the `handle`
|
||||
/// function before the rendering of the `Container` happens.
|
||||
pub inline fn content(this: @This(), cells: []Cell, size: Size) !void {
|
||||
if (this.vtable.content) |content_fn|
|
||||
try content_fn(this.ptr, cells, size);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// This is an empty template implementation for an Element type which `zterm` may provide.
|
||||
///
|
||||
/// TODO: Should elements need to be composible with each other, such that they may build complexer outputs?
|
||||
/// - the goal would rather be to have re-usable parts of handlers and/or content functions which serve similar functionalities.
|
||||
pub fn Template(Event: type) type {
|
||||
return packed struct {
|
||||
pub fn element(this: *@This()) Element(Event) {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.handle = handle,
|
||||
.content = content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn handle(ctx: *anyopaque, event: Event) !void {
|
||||
_ = ctx;
|
||||
_ = event;
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []Cell, size: Size) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -3,26 +3,32 @@
|
||||
const std = @import("std");
|
||||
const terminal = @import("terminal.zig");
|
||||
|
||||
const Size = terminal.Size;
|
||||
const Key = terminal.Key;
|
||||
const Size = @import("size.zig").Size;
|
||||
const Key = @import("key.zig");
|
||||
|
||||
pub const Error = struct {
|
||||
err: anyerror,
|
||||
msg: []const u8,
|
||||
};
|
||||
|
||||
// System events available to every application.
|
||||
// TODO: should this also already include the .view enum option?
|
||||
/// System events available to every `zterm.App`
|
||||
pub const SystemEvent = union(enum) {
|
||||
/// Initialize event, which is send once at the beginning of the event loop and before the first render loop
|
||||
/// TODO: not sure if this is necessary or if there is an actual usecase for this - for now it will remain
|
||||
init,
|
||||
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
|
||||
quit,
|
||||
err: Error,
|
||||
/// 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 = .{ "╭", "─", "╮", "│", "╰", "╯" };
|
||||
const square_frame = .{ "┌", "─", "┐", "│", "└", "┘" };
|
||||
|
||||
fn renderFrame(this: *@This(), renderer: *Renderer) !void {
|
||||
// FIXME: use renderer instead!
|
||||
_ = renderer;
|
||||
const frame = switch (this.config.frame) {
|
||||
.round => round_frame,
|
||||
.square => square_frame,
|
||||
};
|
||||
std.debug.assert(frame.len == 6);
|
||||
// render top: +---+
|
||||
try terminal.setCursorPosition(this.size.anchor);
|
||||
const writer = terminal.writer();
|
||||
try this.config.style.value(writer, frame[0]);
|
||||
if (this.config.title.str.len > 0) {
|
||||
try this.config.title.style.value(writer, this.config.title.str);
|
||||
}
|
||||
for (0..this.size.cols -| 2 -| this.config.title.str.len) |_| {
|
||||
try this.config.style.value(writer, frame[1]);
|
||||
}
|
||||
try this.config.style.value(writer, frame[2]);
|
||||
// render left: |
|
||||
for (1..this.size.rows -| 1) |r| {
|
||||
const row: u16 = @truncate(r);
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = this.size.anchor.col,
|
||||
.row = this.size.anchor.row + row,
|
||||
});
|
||||
try this.config.style.value(writer, frame[3]);
|
||||
}
|
||||
// render right: |
|
||||
for (1..this.size.rows -| 1) |r| {
|
||||
const row: u16 = @truncate(r);
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = this.size.anchor.col + this.size.cols -| 1,
|
||||
.row = this.size.anchor.row + row,
|
||||
});
|
||||
try this.config.style.value(writer, frame[3]);
|
||||
}
|
||||
// render bottom: +---+
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = this.size.anchor.col,
|
||||
.row = this.size.anchor.row + this.size.rows - 1,
|
||||
});
|
||||
try this.config.style.value(writer, frame[4]);
|
||||
for (0..this.size.cols -| 2) |_| {
|
||||
try this.config.style.value(writer, frame[1]);
|
||||
}
|
||||
try this.config.style.value(writer, frame[5]);
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
if (this.require_render) {
|
||||
try renderer.clear(this.size);
|
||||
try this.renderFrame(renderer);
|
||||
this.require_render = false;
|
||||
}
|
||||
|
||||
switch ((&this.element).*) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,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);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
171
src/main.zig
Normal file
171
src/main.zig
Normal file
@@ -0,0 +1,171 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(union(enum) {});
|
||||
const Key = zterm.Key;
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
pub const HelloWorldText = packed struct {
|
||||
const text = "Hello World";
|
||||
|
||||
text_color: zterm.Color = .black,
|
||||
|
||||
// example function to create the interface instance for this `Element` implementation
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.handle = handle,
|
||||
.content = content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// example function to render contents for a `Container`
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
|
||||
// NOTE: error should only be returned here in case an in-recoverable exception has occurred
|
||||
const row = size.rows / 2;
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
|
||||
for (0.., text) |idx, char| {
|
||||
cells[(row * size.cols) + col + idx].style.fg = this.text_color;
|
||||
cells[(row * size.cols) + col + idx].cp = char;
|
||||
}
|
||||
}
|
||||
|
||||
// example function to handle events for a `Container`
|
||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
.init => log.debug(".init event", .{}),
|
||||
.key => |key| {
|
||||
if (key.matches(.{ .cp = zterm.Key.space })) {
|
||||
var next_color_idx = @intFromEnum(this.text_color);
|
||||
next_color_idx += 1;
|
||||
next_color_idx %= 17; // iterate over the first 16 colors (but exclude `.default` == 0)
|
||||
if (next_color_idx == 0) next_color_idx += 1;
|
||||
this.text_color = @enumFromInt(next_color_idx);
|
||||
log.debug("Next color: {s}", .{@tagName(this.text_color)});
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
// TODO: maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||
defer renderer.deinit();
|
||||
|
||||
var element_wrapper: HelloWorldText = .{};
|
||||
const element = element_wrapper.element();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.border = .{
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.line = .double,
|
||||
},
|
||||
},
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .all(5),
|
||||
.direction = .vertical,
|
||||
},
|
||||
}, .{});
|
||||
var box = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
.layout = .{
|
||||
.gap = 1,
|
||||
.direction = .vertical,
|
||||
.padding = .vertical(1),
|
||||
},
|
||||
}, .{});
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, .{}));
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, element));
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, .{}));
|
||||
try container.append(box);
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.border = .{
|
||||
.color = .light_blue,
|
||||
.sides = .vertical,
|
||||
},
|
||||
}, .{}));
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
}, .{}));
|
||||
defer container.deinit(); // also de-initializes the children
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||
|
||||
// event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.init => {
|
||||
try container.handle(event);
|
||||
continue; // do not render
|
||||
},
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.key => |key| {
|
||||
if (key.matches(.{ .cp = 'q' })) app.quit();
|
||||
|
||||
// corresponding element could even be changed from the 'outside' (not forced through the event system)
|
||||
// event system however allows for cross element communication (i.e. broadcasting messages, etc.)
|
||||
if (key.matches(.{ .cp = zterm.Key.escape })) element_wrapper.text_color = .black;
|
||||
|
||||
if (key.matches(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
||||
try app.interrupt();
|
||||
defer app.start() catch @panic("could not start app event loop");
|
||||
var child = std.process.Child.init(&.{"hx"}, allocator);
|
||||
_ = child.spawnAndWait() catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Spawning $EDITOR failed",
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
// NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE: returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
0
src/properties.zig
Normal file
0
src/properties.zig
Normal file
185
src/render.zig
185
src/render.zig
@@ -1,108 +1,115 @@
|
||||
//! Renderer which holds the screen to compare with the previous screen for efficient rendering.
|
||||
//! Each renderer should at least implement these functions:
|
||||
//! - resize(this: *@This(), size: Size) void {}
|
||||
//! - clear(this: *@This(), size: Size) !void {}
|
||||
//! - render(this: *@This(), size: Size, contents: []u8) !void {}
|
||||
//!
|
||||
//! Each `Renderer` should be able to be used interchangeable without having to
|
||||
//! change any code of any `Layout` or `Widget`. The only change should be the
|
||||
//! passed type to `zterm.App` _R_ parameter.
|
||||
const std = @import("std");
|
||||
const terminal = @import("terminal.zig");
|
||||
|
||||
const Cells = []const terminal.Cell;
|
||||
const Position = terminal.Position;
|
||||
const Size = terminal.Size;
|
||||
const Cell = @import("cell.zig");
|
||||
const Position = @import("size.zig").Position;
|
||||
const Size = @import("size.zig").Size;
|
||||
|
||||
pub fn Direct(comptime fullscreen: bool) type {
|
||||
const log = std.log.scoped(.renderer_direct);
|
||||
_ = log;
|
||||
_ = fullscreen;
|
||||
return struct {
|
||||
size: Size = undefined,
|
||||
/// Double-buffered intermediate rendering pipeline
|
||||
pub const Buffered = struct {
|
||||
const log = std.log.scoped(.renderer_buffered);
|
||||
// _ = log;
|
||||
allocator: std.mem.Allocator,
|
||||
created: bool,
|
||||
size: Size,
|
||||
screen: []Cell,
|
||||
virtual_screen: []Cell,
|
||||
|
||||
pub fn resize(this: *@This(), size: Size) void {
|
||||
this.size = size;
|
||||
pub fn init(allocator: std.mem.Allocator) @This() {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.created = false,
|
||||
.size = undefined,
|
||||
.screen = undefined,
|
||||
.virtual_screen = undefined,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clear(this: *@This(), size: Size) !void {
|
||||
_ = this;
|
||||
// NOTE: clear on the entire screen may introduce too much overhead and could instead clear the entire screen instead.
|
||||
// - it could also then try to optimize for further *clear* calls, that result in pretty much a nop? -> how to identify those clear calls?
|
||||
// TODO: this should instead by dynamic and correct of size (terminal could be too large currently)
|
||||
std.debug.assert(1028 > size.cols);
|
||||
var buf: [1028]u8 = undefined;
|
||||
@memset(buf[0..], ' ');
|
||||
for (0..size.rows) |r| {
|
||||
const row: u16 = @truncate(r);
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = size.anchor.col,
|
||||
.row = size.anchor.row + row,
|
||||
});
|
||||
_ = try terminal.write(buf[0..size.cols]);
|
||||
pub fn deinit(this: *@This()) void {
|
||||
if (this.created) {
|
||||
this.allocator.free(this.screen);
|
||||
this.allocator.free(this.virtual_screen);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), size: Size, cells: Cells) !void {
|
||||
_ = this;
|
||||
try terminal.setCursorPosition(size.anchor);
|
||||
var row: u16 = 0;
|
||||
var remaining_cols = size.cols;
|
||||
const writer = terminal.writer();
|
||||
for (cells) |cell| {
|
||||
pub fn resize(this: *@This(), size: Size) !void {
|
||||
log.debug("renderer::resize", .{});
|
||||
defer this.size = size;
|
||||
|
||||
if (!this.created) {
|
||||
this.screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
|
||||
@memset(this.screen, .{});
|
||||
this.virtual_screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
|
||||
@memset(this.virtual_screen, .{});
|
||||
this.created = true;
|
||||
} else {
|
||||
this.allocator.free(this.screen);
|
||||
this.screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
|
||||
@memset(this.screen, .{});
|
||||
this.allocator.free(this.virtual_screen);
|
||||
this.virtual_screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
|
||||
@memset(this.virtual_screen, .{});
|
||||
}
|
||||
try this.clear();
|
||||
}
|
||||
|
||||
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
|
||||
pub fn clear(this: *@This()) !void {
|
||||
log.debug("renderer::clear", .{});
|
||||
try terminal.clearScreen();
|
||||
@memset(this.screen, .{});
|
||||
}
|
||||
|
||||
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
|
||||
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
|
||||
const size: Size = container.size;
|
||||
const cells: []const Cell = try container.contents();
|
||||
|
||||
if (cells.len == 0) return;
|
||||
|
||||
var idx: usize = 0;
|
||||
print_cell: while (true) {
|
||||
const cell_len = cell.len(idx);
|
||||
if (cell_len > remaining_cols) {
|
||||
const result = try cell.writeUpToNewline(writer, idx, idx + remaining_cols);
|
||||
row += 1;
|
||||
if (row >= size.rows) {
|
||||
return; // we are done
|
||||
}
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = size.anchor.col,
|
||||
.row = size.anchor.row + row,
|
||||
});
|
||||
remaining_cols = size.cols;
|
||||
idx = result.idx;
|
||||
if (result.newline) {
|
||||
idx += 1; // skip over newline
|
||||
} else {
|
||||
// there is still content to the newline (which will not be printed)
|
||||
for (idx..cell.content.len) |i| {
|
||||
if (cell.content[i] == '\n') {
|
||||
idx = i + 1;
|
||||
continue :print_cell;
|
||||
var vs = this.virtual_screen;
|
||||
const anchor: usize = (@as(usize, size.anchor.row) * @as(usize, this.size.cols)) + @as(usize, size.anchor.col);
|
||||
|
||||
blk: for (0..size.rows) |row| {
|
||||
for (0..size.cols) |col| {
|
||||
const cell = cells[idx];
|
||||
idx += 1;
|
||||
|
||||
vs[anchor + (row * this.size.cols) + col].style = cell.style;
|
||||
vs[anchor + (row * this.size.cols) + col].cp = cell.cp;
|
||||
|
||||
if (cells.len == idx) break :blk;
|
||||
}
|
||||
}
|
||||
break; // go to next cell (as we went to the end of the cell and do not print on the next line)
|
||||
}
|
||||
} else {
|
||||
// print rest of cell
|
||||
const result = try cell.writeUpToNewline(writer, idx, idx + cell_len);
|
||||
if (result.newline) {
|
||||
row += 1;
|
||||
if (row >= size.rows) {
|
||||
return; // we are done
|
||||
}
|
||||
try terminal.setCursorPosition(.{
|
||||
.col = size.anchor.col,
|
||||
.row = size.anchor.row + row,
|
||||
});
|
||||
remaining_cols = size.cols;
|
||||
idx = result.idx + 1; // skip over newline
|
||||
} else {
|
||||
remaining_cols -= @truncate(cell_len -| idx);
|
||||
idx = 0;
|
||||
break; // go to next cell
|
||||
// free immediately
|
||||
container.allocator.free(cells);
|
||||
|
||||
for (container.elements.items) |*element| {
|
||||
try this.render(T, element);
|
||||
}
|
||||
}
|
||||
// written all cell contents
|
||||
if (idx >= cell.content.len) {
|
||||
break; // go to next cell
|
||||
}
|
||||
|
||||
/// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop).
|
||||
pub fn flush(this: *@This()) !void {
|
||||
// TODO: measure timings of rendered frames?
|
||||
log.debug("renderer::flush", .{});
|
||||
const writer = terminal.writer();
|
||||
const s = this.screen;
|
||||
const vs = this.virtual_screen;
|
||||
for (0..this.size.rows) |row| {
|
||||
for (0..this.size.cols) |col| {
|
||||
const idx = (row * this.size.cols) + col;
|
||||
const cs = s[idx];
|
||||
const cvs = vs[idx];
|
||||
if (cs.eql(cvs)) continue;
|
||||
|
||||
// render differences found in virtual screen
|
||||
try terminal.setCursorPosition(.{ .row = @truncate(row + 1), .col = @truncate(col + 1) });
|
||||
try cvs.value(writer);
|
||||
// update screen to be the virtual screen for the next frame
|
||||
s[idx] = vs[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
10
src/size.zig
Normal file
10
src/size.zig
Normal file
@@ -0,0 +1,10 @@
|
||||
pub const Size = packed struct {
|
||||
anchor: Position = .{},
|
||||
cols: u16 = 0,
|
||||
rows: u16 = 0,
|
||||
};
|
||||
|
||||
pub const Position = packed struct {
|
||||
col: u16 = 0,
|
||||
row: u16 = 0,
|
||||
};
|
||||
73
src/style.zig
Normal file
73
src/style.zig
Normal file
@@ -0,0 +1,73 @@
|
||||
//! Helper function collection to provide ascii encodings for styling outputs.
|
||||
//! Stylings are implemented such that they can be nested in anyway to support
|
||||
//! multiple styles (i.e. bold and italic).
|
||||
//!
|
||||
//! Stylings however also include highlighting for specific terminal capabilities.
|
||||
//! For example url highlighting.
|
||||
|
||||
// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License)
|
||||
// with slight modifications
|
||||
const std = @import("std");
|
||||
|
||||
const Color = @import("color.zig").Color;
|
||||
|
||||
pub const Style = @This();
|
||||
|
||||
pub const Underline = enum {
|
||||
off,
|
||||
single,
|
||||
double,
|
||||
curly,
|
||||
dotted,
|
||||
dashed,
|
||||
};
|
||||
|
||||
pub const Attribute = enum(u8) {
|
||||
reset = 0,
|
||||
bold = 1,
|
||||
dim,
|
||||
italic,
|
||||
underline,
|
||||
blink,
|
||||
invert = 7,
|
||||
hidden,
|
||||
strikethrough,
|
||||
};
|
||||
|
||||
fg: Color = .white,
|
||||
bg: Color = .default,
|
||||
ul: Color = .default,
|
||||
ul_style: Underline = .off,
|
||||
attributes: []const Attribute,
|
||||
|
||||
pub fn eql(this: Style, other: Style) bool {
|
||||
return std.meta.eql(this, other);
|
||||
}
|
||||
|
||||
pub fn value(this: Style, writer: anytype, cp: u21) !void {
|
||||
var buffer: [4]u8 = undefined;
|
||||
const bytes = try std.unicode.utf8Encode(cp, &buffer);
|
||||
std.debug.assert(bytes > 0);
|
||||
// build ansi sequence for 256 colors ...
|
||||
// foreground
|
||||
try std.fmt.format(writer, "\x1b[", .{});
|
||||
try this.fg.write(writer, .fg);
|
||||
// background
|
||||
try std.fmt.format(writer, ";", .{});
|
||||
try this.bg.write(writer, .bg);
|
||||
// underline
|
||||
// FIX: assert that if the underline property is set that the ul style and the attribute for underlining is available
|
||||
try std.fmt.format(writer, ";", .{});
|
||||
try this.ul.write(writer, .ul);
|
||||
// append styles (aka attributes like bold, italic, strikethrough, etc.)
|
||||
for (this.attributes) |attribute| {
|
||||
try std.fmt.format(writer, ";{d}", .{@intFromEnum(attribute)});
|
||||
}
|
||||
try std.fmt.format(writer, "m", .{});
|
||||
// content
|
||||
try std.fmt.format(writer, "{s}", .{buffer});
|
||||
}
|
||||
|
||||
// TODO: implement helper functions for terminal capabilities:
|
||||
// - links / url display (osc 8)
|
||||
// - show / hide cursor?
|
||||
@@ -1,10 +1,11 @@
|
||||
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 Key = @import("key.zig");
|
||||
const Position = @import("size.zig").Position;
|
||||
const Size = @import("size.zig").Size;
|
||||
const Cell = @import("cell.zig");
|
||||
|
||||
const log = std.log.scoped(.terminal);
|
||||
|
||||
// Ref: https://vt100.net/docs/vt510-rm/DECRPM.html
|
||||
@@ -84,7 +85,7 @@ pub fn setCursorPosition(pos: Position) !void {
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
|
||||
}
|
||||
|
||||
pub fn getCursorPosition() !Position {
|
||||
pub fn getCursorPosition() !Size.Position {
|
||||
// Needs Raw mode (no wait for \n) to work properly cause
|
||||
// control sequence will not be written without it.
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[6n");
|
||||
@@ -126,8 +127,8 @@ pub fn getCursorPosition() !Position {
|
||||
}
|
||||
|
||||
return .{
|
||||
.row = try std.fmt.parseInt(u16, row[0..ridx], 10),
|
||||
.col = try std.fmt.parseInt(u16, col[0..cidx], 10),
|
||||
.row = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
|
||||
.col = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -227,7 +228,3 @@ fn getReportMode(ps: u8) ReportMode {
|
||||
else => ReportMode.not_recognized,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
_ = Cell;
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
const std = @import("std");
|
||||
pub const Style = @import("Style.zig");
|
||||
|
||||
style: Style = .{},
|
||||
content: []const u8 = undefined,
|
||||
|
||||
pub const Result = struct {
|
||||
idx: usize,
|
||||
newline: bool,
|
||||
};
|
||||
|
||||
pub fn len(this: @This(), start: usize) usize {
|
||||
std.debug.assert(this.content.len > start);
|
||||
return this.content[start..].len;
|
||||
}
|
||||
|
||||
pub fn write(this: @This(), writer: anytype, start: usize, end: usize) !void {
|
||||
std.debug.assert(this.content.len > start);
|
||||
std.debug.assert(this.content.len >= end);
|
||||
std.debug.assert(start < end);
|
||||
|
||||
try this.style.value(writer, this.content[start..end]);
|
||||
}
|
||||
|
||||
pub fn writeUpToNewline(this: @This(), writer: anytype, start: usize, end: usize) !Result {
|
||||
std.debug.assert(this.content.len > start);
|
||||
std.debug.assert(this.content.len >= end);
|
||||
std.debug.assert(start < end);
|
||||
|
||||
for (start..end) |i| {
|
||||
if (this.content[i] == '\n') {
|
||||
if (start < i) {
|
||||
// this is just an empty line with a newline
|
||||
try this.value(writer, start, i);
|
||||
}
|
||||
return .{
|
||||
.idx = i,
|
||||
.newline = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
try this.write(writer, start, end);
|
||||
return .{
|
||||
.idx = end,
|
||||
.newline = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn value(this: @This(), writer: anytype, start: usize, end: usize) !void {
|
||||
std.debug.assert(start < this.content.len);
|
||||
std.debug.assert(this.content.len >= end);
|
||||
std.debug.assert(start < end);
|
||||
try this.style.value(writer, this.content[start..end]);
|
||||
}
|
||||
|
||||
// not really supported
|
||||
pub fn format(this: @This(), writer: anytype, comptime fmt: []const u8, args: anytype) !void {
|
||||
try this.style.format(writer, fmt, args); // NOTE: args should contain this.content[start..end] or this.content
|
||||
}
|
||||
|
||||
test {
|
||||
_ = Style;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
col: u16,
|
||||
row: u16,
|
||||
@@ -1,5 +0,0 @@
|
||||
const Position = @import("Position.zig");
|
||||
|
||||
anchor: Position = .{ .col = 1, .row = 1 }, // top left corner by default
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
@@ -1,305 +0,0 @@
|
||||
//! Helper function collection to provide ascii encodings for styling outputs.
|
||||
//! Stylings are implemented such that they can be nested in anyway to support
|
||||
//! multiple styles (i.e. bold and italic).
|
||||
//!
|
||||
//! Stylings however also include highlighting for specific terminal capabilities.
|
||||
//! For example url highlighting.
|
||||
|
||||
// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License)
|
||||
// with slight modifications
|
||||
const std = @import("std");
|
||||
const ctlseqs = @import("ctlseqs.zig");
|
||||
|
||||
pub const Underline = enum {
|
||||
off,
|
||||
single,
|
||||
double,
|
||||
curly,
|
||||
dotted,
|
||||
dashed,
|
||||
};
|
||||
|
||||
pub const Color = union(enum) {
|
||||
default,
|
||||
index: u8,
|
||||
rgb: [3]u8,
|
||||
|
||||
pub fn eql(a: @This(), b: @This()) bool {
|
||||
switch (a) {
|
||||
.default => return b == .default,
|
||||
.index => |a_idx| {
|
||||
switch (b) {
|
||||
.index => |b_idx| return a_idx == b_idx,
|
||||
else => return false,
|
||||
}
|
||||
},
|
||||
.rgb => |a_rgb| {
|
||||
switch (b) {
|
||||
.rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and
|
||||
a_rgb[1] == b_rgb[1] and
|
||||
a_rgb[2] == b_rgb[2],
|
||||
else => return false,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rgbFromUint(val: u24) Color {
|
||||
const r_bits = val & 0b11111111_00000000_00000000;
|
||||
const g_bits = val & 0b00000000_11111111_00000000;
|
||||
const b_bits = val & 0b00000000_00000000_11111111;
|
||||
const rgb = [_]u8{
|
||||
@truncate(r_bits >> 16),
|
||||
@truncate(g_bits >> 8),
|
||||
@truncate(b_bits),
|
||||
};
|
||||
return .{ .rgb = rgb };
|
||||
}
|
||||
|
||||
/// parse an XParseColor-style rgb specification into an rgb Color. The spec
|
||||
/// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always
|
||||
/// be the same as the low two bits.
|
||||
pub fn rgbFromSpec(spec: []const u8) !Color {
|
||||
var iter = std.mem.splitScalar(u8, spec, ':');
|
||||
const prefix = iter.next() orelse return error.InvalidColorSpec;
|
||||
if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec;
|
||||
|
||||
const spec_str = iter.next() orelse return error.InvalidColorSpec;
|
||||
|
||||
var spec_iter = std.mem.splitScalar(u8, spec_str, '/');
|
||||
|
||||
const r_raw = spec_iter.next() orelse return error.InvalidColorSpec;
|
||||
if (r_raw.len != 4) return error.InvalidColorSpec;
|
||||
|
||||
const g_raw = spec_iter.next() orelse return error.InvalidColorSpec;
|
||||
if (g_raw.len != 4) return error.InvalidColorSpec;
|
||||
|
||||
const b_raw = spec_iter.next() orelse return error.InvalidColorSpec;
|
||||
if (b_raw.len != 4) return error.InvalidColorSpec;
|
||||
|
||||
const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16);
|
||||
const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16);
|
||||
const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16);
|
||||
|
||||
return .{
|
||||
.rgb = [_]u8{ r, g, b },
|
||||
};
|
||||
}
|
||||
|
||||
test "rgbFromSpec" {
|
||||
const spec = "rgb:aaaa/bbbb/cccc";
|
||||
const actual = try rgbFromSpec(spec);
|
||||
switch (actual) {
|
||||
.rgb => |rgb| {
|
||||
try std.testing.expectEqual(0xAA, rgb[0]);
|
||||
try std.testing.expectEqual(0xBB, rgb[1]);
|
||||
try std.testing.expectEqual(0xCC, rgb[2]);
|
||||
},
|
||||
else => try std.testing.expect(false),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fg: Color = .default,
|
||||
bg: Color = .default,
|
||||
ul: Color = .default,
|
||||
ul_style: Underline = .off,
|
||||
|
||||
bold: bool = false,
|
||||
dim: bool = false,
|
||||
italic: bool = false,
|
||||
blink: bool = false,
|
||||
reverse: bool = false,
|
||||
invisible: bool = false,
|
||||
strikethrough: bool = false,
|
||||
|
||||
/// 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 {
|
||||
if (other.fg != .default) this.fg = other.fg;
|
||||
if (other.bg != .default) this.bg = other.bg;
|
||||
if (other.ul != .default) this.ul = other.ul;
|
||||
if (other.ul_style != .off) this.ul_style = other.ul_style;
|
||||
if (other.bold != false) this.bold = other.bold;
|
||||
if (other.dim != false) this.dim = other.dim;
|
||||
if (other.italic != false) this.italic = other.italic;
|
||||
if (other.blink != false) this.blink = other.blink;
|
||||
if (other.reverse != false) this.reverse = other.reverse;
|
||||
if (other.invisible != false) this.invisible = other.invisible;
|
||||
if (other.strikethrough != false) this.strikethrough = other.strikethrough;
|
||||
}
|
||||
|
||||
fn start(this: @This(), writer: anytype) !void {
|
||||
// foreground
|
||||
switch (this.fg) {
|
||||
.default => try std.fmt.format(writer, ctlseqs.fg_reset, .{}),
|
||||
.index => |idx| {
|
||||
switch (idx) {
|
||||
0...7 => {
|
||||
try std.fmt.format(writer, ctlseqs.fg_base, .{idx});
|
||||
},
|
||||
8...15 => {
|
||||
try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8});
|
||||
},
|
||||
else => {
|
||||
try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx});
|
||||
},
|
||||
}
|
||||
},
|
||||
.rgb => |rgb| {
|
||||
try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] });
|
||||
},
|
||||
}
|
||||
// background
|
||||
switch (this.bg) {
|
||||
.default => try std.fmt.format(writer, ctlseqs.bg_reset, .{}),
|
||||
.index => |idx| {
|
||||
switch (idx) {
|
||||
0...7 => {
|
||||
try std.fmt.format(writer, ctlseqs.bg_base, .{idx});
|
||||
},
|
||||
8...15 => {
|
||||
try std.fmt.format(writer, ctlseqs.bg_bright, .{idx});
|
||||
},
|
||||
else => {
|
||||
try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx});
|
||||
},
|
||||
}
|
||||
},
|
||||
.rgb => |rgb| {
|
||||
try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] });
|
||||
},
|
||||
}
|
||||
// underline color
|
||||
switch (this.ul) {
|
||||
.default => try std.fmt.format(writer, ctlseqs.ul_reset, .{}),
|
||||
.index => |idx| {
|
||||
try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx});
|
||||
},
|
||||
.rgb => |rgb| {
|
||||
try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] });
|
||||
},
|
||||
}
|
||||
// underline style
|
||||
switch (this.ul_style) {
|
||||
.off => try std.fmt.format(writer, ctlseqs.ul_off, .{}),
|
||||
.single => try std.fmt.format(writer, ctlseqs.ul_single, .{}),
|
||||
.double => try std.fmt.format(writer, ctlseqs.ul_double, .{}),
|
||||
.curly => try std.fmt.format(writer, ctlseqs.ul_curly, .{}),
|
||||
.dotted => try std.fmt.format(writer, ctlseqs.ul_dotted, .{}),
|
||||
.dashed => try std.fmt.format(writer, ctlseqs.ul_dashed, .{}),
|
||||
}
|
||||
// bold
|
||||
switch (this.bold) {
|
||||
true => try std.fmt.format(writer, ctlseqs.bold_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
|
||||
}
|
||||
// dim
|
||||
switch (this.dim) {
|
||||
true => try std.fmt.format(writer, ctlseqs.dim_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
|
||||
}
|
||||
// italic
|
||||
switch (this.italic) {
|
||||
true => try std.fmt.format(writer, ctlseqs.italic_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.italic_reset, .{}),
|
||||
}
|
||||
// blink
|
||||
switch (this.blink) {
|
||||
true => try std.fmt.format(writer, ctlseqs.blink_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.blink_reset, .{}),
|
||||
}
|
||||
// reverse
|
||||
switch (this.reverse) {
|
||||
true => try std.fmt.format(writer, ctlseqs.reverse_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}),
|
||||
}
|
||||
// invisible
|
||||
switch (this.invisible) {
|
||||
true => try std.fmt.format(writer, ctlseqs.invisible_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}),
|
||||
}
|
||||
// strikethrough
|
||||
switch (this.strikethrough) {
|
||||
true => try std.fmt.format(writer, ctlseqs.strikethrough_set, .{}),
|
||||
false => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}),
|
||||
}
|
||||
}
|
||||
|
||||
fn end(this: @This(), writer: anytype) !void {
|
||||
// foreground
|
||||
switch (this.fg) {
|
||||
.default => {},
|
||||
else => try std.fmt.format(writer, ctlseqs.fg_reset, .{}),
|
||||
}
|
||||
// background
|
||||
switch (this.bg) {
|
||||
.default => {},
|
||||
else => try std.fmt.format(writer, ctlseqs.bg_reset, .{}),
|
||||
}
|
||||
// underline color
|
||||
switch (this.ul) {
|
||||
.default => {},
|
||||
else => try std.fmt.format(writer, ctlseqs.ul_reset, .{}),
|
||||
}
|
||||
// underline style
|
||||
switch (this.ul_style) {
|
||||
.off => {},
|
||||
else => try std.fmt.format(writer, ctlseqs.ul_off, .{}),
|
||||
}
|
||||
// bold
|
||||
switch (this.bold) {
|
||||
true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// dim
|
||||
switch (this.dim) {
|
||||
true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// italic
|
||||
switch (this.italic) {
|
||||
true => try std.fmt.format(writer, ctlseqs.italic_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// blink
|
||||
switch (this.blink) {
|
||||
true => try std.fmt.format(writer, ctlseqs.blink_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// reverse
|
||||
switch (this.reverse) {
|
||||
true => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// invisible
|
||||
switch (this.invisible) {
|
||||
true => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
// strikethrough
|
||||
switch (this.strikethrough) {
|
||||
true => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}),
|
||||
false => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(this: @This(), writer: anytype, comptime content: []const u8, args: anytype) !void {
|
||||
try this.start(writer);
|
||||
try std.fmt.format(writer, content, args);
|
||||
try this.end(writer);
|
||||
}
|
||||
|
||||
pub fn value(this: @This(), writer: anytype, content: []const u8) !void {
|
||||
try this.start(writer);
|
||||
_ = try writer.write(content);
|
||||
try this.end(writer);
|
||||
}
|
||||
|
||||
// TODO: implement helper functions for terminal capabilities:
|
||||
// - links / url display (osc 8)
|
||||
// - show / hide cursor?
|
||||
|
||||
test {
|
||||
_ = Color;
|
||||
}
|
||||
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,155 +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 => {
|
||||
var length_usize: usize = 0;
|
||||
for (this.contents) |content| {
|
||||
length_usize += content.content.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 => {
|
||||
var length_usize: usize = 0;
|
||||
for (this.contents) |content| {
|
||||
length_usize += content.content.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 => {
|
||||
var length_usize: usize = 0;
|
||||
for (this.contents) |content| {
|
||||
length_usize += content.content.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 => {
|
||||
var length_usize: usize = 0;
|
||||
for (this.contents) |content| {
|
||||
length_usize += content.content.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 => {
|
||||
var length_usize: usize = 0;
|
||||
for (this.contents) |content| {
|
||||
length_usize += content.content.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,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
try renderer.render(size, this.contents);
|
||||
this.require_render = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,35 @@
|
||||
// private imports
|
||||
const container = @import("container.zig");
|
||||
const color = @import("color.zig");
|
||||
const size = @import("size.zig");
|
||||
|
||||
// public import / exports
|
||||
pub const terminal = @import("terminal.zig");
|
||||
// public exports
|
||||
pub const App = @import("app.zig").App;
|
||||
// App also exports further types once initialized with the user events at compile time:
|
||||
// `App.Container`
|
||||
// `App.Element`
|
||||
pub const Renderer = @import("render.zig");
|
||||
|
||||
pub const Key = terminal.Key;
|
||||
pub const Position = terminal.Position;
|
||||
pub const Size = terminal.Size;
|
||||
pub const Cell = terminal.Cell;
|
||||
// Container Configurations
|
||||
pub const Border = container.Border;
|
||||
pub const Rectangle = container.Rectangle;
|
||||
pub const Scroll = container.Scroll;
|
||||
pub const Layout = container.Layout;
|
||||
|
||||
pub const Cell = @import("cell.zig");
|
||||
pub const Color = color.Color;
|
||||
pub const Key = @import("key.zig");
|
||||
pub const Size = size.Size;
|
||||
pub const Style = @import("style.zig");
|
||||
|
||||
test {
|
||||
_ = @import("terminal.zig");
|
||||
_ = @import("queue.zig");
|
||||
|
||||
_ = color;
|
||||
_ = size;
|
||||
|
||||
_ = Cell;
|
||||
_ = Key;
|
||||
_ = Style;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user