Compare commits
3 Commits
0cc0ed10d2
...
aeac4bdc83
| Author | SHA1 | Date | |
|---|---|---|---|
| aeac4bdc83 | |||
| 58982a53f2 | |||
| 273da37020 |
70
build.zig
70
build.zig
@@ -32,41 +32,46 @@ pub fn build(b: *std.Build) void {
|
||||
lib.addImport("interface", interface.module("interface"));
|
||||
lib.addImport("code_point", zg.module("code_point"));
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "zterm",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
// example executables
|
||||
const stack_example = b.addExecutable(.{
|
||||
.name = "stack",
|
||||
.root_source_file = b.path("examples/stack.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe.root_module.addImport("zterm", lib);
|
||||
stack_example.root_module.addImport("zterm", lib);
|
||||
|
||||
const container_example = b.addExecutable(.{
|
||||
.name = "container",
|
||||
.root_source_file = b.path("examples/container.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
container_example.root_module.addImport("zterm", lib);
|
||||
|
||||
const padding_example = b.addExecutable(.{
|
||||
.name = "padding",
|
||||
.root_source_file = b.path("examples/padding.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
padding_example.root_module.addImport("zterm", lib);
|
||||
|
||||
const exec_example = b.addExecutable(.{
|
||||
.name = "exec",
|
||||
.root_source_file = b.path("examples/exec.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exec_example.root_module.addImport("zterm", lib);
|
||||
|
||||
// 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(exe);
|
||||
|
||||
// This *creates* a Run step in the build graph, to be executed when another
|
||||
// step is evaluated that depends on it. The next line below will establish
|
||||
// such a dependency.
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
|
||||
// By making the run step depend on the install step, it will be run from the
|
||||
// installation directory rather than directly from within the cache directory.
|
||||
// This is not necessary, however, if the application depends on other installed
|
||||
// files, this ensures they will be present and in the expected location.
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
// This allows the user to pass arguments to the application in the build
|
||||
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||
// and can be selected like this: `zig build run`
|
||||
// This will evaluate the `run` step rather than the default, which is "install".
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
b.installArtifact(stack_example);
|
||||
b.installArtifact(container_example);
|
||||
b.installArtifact(padding_example);
|
||||
b.installArtifact(exec_example);
|
||||
|
||||
// Creates a step for unit testing. This only builds the test executable
|
||||
// but does not run it.
|
||||
@@ -79,18 +84,9 @@ pub fn build(b: *std.Build) void {
|
||||
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
|
||||
const exe_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const run_exe_unit_tests = b.addRunArtifact(exe_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);
|
||||
test_step.dependOn(&run_exe_unit_tests.step);
|
||||
}
|
||||
|
||||
114
examples/container.zig
Normal file
114
examples/container.zig
Normal file
@@ -0,0 +1,114 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Direct,
|
||||
true,
|
||||
);
|
||||
const Key = zterm.Key;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
|
||||
const log = std.log.scoped(.container);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// fail test; can't try in defer as defer is executed after we return
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer: App.Renderer = .{};
|
||||
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
|
||||
// -> size hint how much should it use?
|
||||
|
||||
var layout = Layout.createFrom(layout: {
|
||||
var stack = Layout.HContainer.init(allocator, .{
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
15,
|
||||
},
|
||||
.{
|
||||
Layout.createFrom(container: {
|
||||
var container = Layout.VContainer.init(allocator, .{
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
25,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./src/app.zig", .{});
|
||||
defer file.close();
|
||||
var widget = Widget.RawText.init(allocator, file);
|
||||
break :blk &widget;
|
||||
}),
|
||||
50,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
25,
|
||||
},
|
||||
});
|
||||
break :container &container;
|
||||
}),
|
||||
70,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
15,
|
||||
},
|
||||
});
|
||||
break :layout &stack;
|
||||
});
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch unreachable;
|
||||
|
||||
// App.Event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
.resize => |size| {
|
||||
renderer.resize(size);
|
||||
},
|
||||
.key => |key| {
|
||||
// ctrl+c to quit
|
||||
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
|
||||
},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
114
examples/exec.zig
Normal file
114
examples/exec.zig
Normal file
@@ -0,0 +1,114 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Direct,
|
||||
true,
|
||||
);
|
||||
const Key = zterm.Key;
|
||||
const Cell = zterm.Cell;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
|
||||
const log = std.log.scoped(.exec);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// fail test; can't try in defer as defer is executed after we return
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer: App.Renderer = .{};
|
||||
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
|
||||
// -> size hint how much should it use?
|
||||
|
||||
var layout = Layout.createFrom(layout: {
|
||||
var container = Layout.VContainer.init(allocator, .{
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
45,
|
||||
},
|
||||
.{
|
||||
Layout.createFrom(framing: {
|
||||
var framing = Layout.Framing.init(allocator, .{}, .{
|
||||
.widget = Widget.createFrom(blk: {
|
||||
var widget = Widget.Text.init(&[_]Cell{
|
||||
.{ .content = "Press " },
|
||||
.{ .content = "Ctrl+n", .style = .{ .fg = .{ .index = 6 } } },
|
||||
.{ .content = " to launch $EDITOR" },
|
||||
});
|
||||
break :blk &widget;
|
||||
}),
|
||||
});
|
||||
break :framing &framing;
|
||||
}),
|
||||
10,
|
||||
},
|
||||
.{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
45,
|
||||
},
|
||||
});
|
||||
break :layout &container;
|
||||
});
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch unreachable;
|
||||
|
||||
// App.Event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
.resize => |size| {
|
||||
renderer.resize(size);
|
||||
},
|
||||
.key => |key| {
|
||||
// ctrl+c to quit
|
||||
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
app.quit();
|
||||
}
|
||||
if (Key.matches(key, .{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
||||
try app.interrupt();
|
||||
defer app.start() catch @panic("could not start app event loop");
|
||||
// TODO: parse environment variables to extract the value of $EDITOR and use it here instead
|
||||
var child = std.process.Child.init(&.{"hx"}, allocator);
|
||||
_ = child.spawnAndWait() catch |err| {
|
||||
app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Spawning $EDITOR failed",
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
|
||||
},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
112
examples/padding.zig
Normal file
112
examples/padding.zig
Normal file
@@ -0,0 +1,112 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Direct,
|
||||
true,
|
||||
);
|
||||
const Key = zterm.Key;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
|
||||
const log = std.log.scoped(.padding);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// fail test; can't try in defer as defer is executed after we return
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer: App.Renderer = .{};
|
||||
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
|
||||
// -> size hint how much should it use?
|
||||
|
||||
var layout = Layout.createFrom(layout: {
|
||||
var padding = Layout.Padding.init(allocator, .{
|
||||
.padding = 15,
|
||||
}, .{
|
||||
.layout = Layout.createFrom(framing: {
|
||||
var framing = Layout.Framing.init(
|
||||
allocator,
|
||||
.{
|
||||
.style = .{
|
||||
.fg = .{
|
||||
.index = 6,
|
||||
},
|
||||
},
|
||||
.frame = .round,
|
||||
.title = .{
|
||||
.str = "Content in Margin",
|
||||
.style = .{
|
||||
.ul_style = .single,
|
||||
.ul = .{ .index = 6 },
|
||||
.bold = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
.layout = Layout.createFrom(margin: {
|
||||
var margin = Layout.Margin.init(
|
||||
allocator,
|
||||
.{
|
||||
.margin = 10,
|
||||
},
|
||||
.{
|
||||
.widget = Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./examples/padding.zig", .{});
|
||||
defer file.close();
|
||||
var widget = Widget.RawText.init(allocator, file);
|
||||
break :blk &widget;
|
||||
}),
|
||||
},
|
||||
);
|
||||
break :margin &margin;
|
||||
}),
|
||||
},
|
||||
);
|
||||
break :framing &framing;
|
||||
}),
|
||||
});
|
||||
break :layout &padding;
|
||||
});
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch unreachable;
|
||||
|
||||
// App.Event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
.resize => |size| {
|
||||
renderer.resize(size);
|
||||
},
|
||||
.key => |key| {
|
||||
// ctrl+c to quit
|
||||
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
|
||||
},
|
||||
}
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
156
examples/stack.zig
Normal file
156
examples/stack.zig
Normal file
@@ -0,0 +1,156 @@
|
||||
const std = @import("std");
|
||||
const zterm = @import("zterm");
|
||||
|
||||
const App = zterm.App(
|
||||
union(enum) {},
|
||||
zterm.Renderer.Direct,
|
||||
true,
|
||||
);
|
||||
const Key = zterm.Key;
|
||||
const Layout = App.Layout;
|
||||
const Widget = App.Widget;
|
||||
|
||||
const log = std.log.scoped(.stack);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// fail test; can't try in defer as defer is executed after we return
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer: App.Renderer = .{};
|
||||
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
|
||||
// -> size hint how much should it use?
|
||||
|
||||
var layout = Layout.createFrom(layout: {
|
||||
var framing = Layout.Framing.init(allocator, .{
|
||||
.style = .{
|
||||
.fg = .{
|
||||
.index = 6,
|
||||
},
|
||||
},
|
||||
.frame = .round,
|
||||
.title = .{
|
||||
.str = "HStack",
|
||||
.style = .{
|
||||
.ul_style = .single,
|
||||
.ul = .{ .index = 6 },
|
||||
.bold = true,
|
||||
},
|
||||
},
|
||||
}, .{
|
||||
.layout = Layout.createFrom(hstack: {
|
||||
var hstack = Layout.HStack.init(allocator, .{
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
Layout.createFrom(framing: {
|
||||
var framing = Layout.Framing.init(
|
||||
allocator,
|
||||
.{
|
||||
.style = .{
|
||||
.fg = .{
|
||||
.index = 6,
|
||||
},
|
||||
},
|
||||
.frame = .round,
|
||||
.title = .{
|
||||
.str = "VStack",
|
||||
.style = .{
|
||||
.ul_style = .single,
|
||||
.ul = .{ .index = 6 },
|
||||
.bold = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
.{
|
||||
.layout = Layout.createFrom(
|
||||
padding: {
|
||||
var padding = Layout.Margin.init(
|
||||
allocator,
|
||||
.{
|
||||
.margin = 10,
|
||||
},
|
||||
.{
|
||||
.layout = Layout.createFrom(vstack: {
|
||||
var vstack = Layout.VStack.init(allocator, .{
|
||||
Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./examples/stack.zig", .{});
|
||||
defer file.close();
|
||||
var widget = Widget.RawText.init(allocator, file);
|
||||
break :blk &widget;
|
||||
}),
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./examples/stack.zig", .{});
|
||||
defer file.close();
|
||||
var widget = Widget.RawText.init(allocator, file);
|
||||
break :blk &widget;
|
||||
}),
|
||||
});
|
||||
break :vstack &vstack;
|
||||
}),
|
||||
},
|
||||
);
|
||||
break :padding &padding;
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
break :framing &framing;
|
||||
}),
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
});
|
||||
break :hstack &hstack;
|
||||
}),
|
||||
});
|
||||
break :layout &framing;
|
||||
});
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch unreachable;
|
||||
|
||||
// App.Event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
.resize => |size| {
|
||||
renderer.resize(size);
|
||||
},
|
||||
.key => |key| {
|
||||
// ctrl+c to quit
|
||||
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
|
||||
},
|
||||
}
|
||||
// NOTE: this currently re-renders the screen for every key-press -> which might be a bit of an overkill
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,9 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type {
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -93,7 +95,9 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type {
|
||||
};
|
||||
// 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);
|
||||
|
||||
184
src/layout/HContainer.zig
Normal file
184
src/layout/HContainer.zig
Normal file
@@ -0,0 +1,184 @@
|
||||
//! Horizontal Container layout for nested `Layout`s and/or `Widget`s.
|
||||
//! The contained elements are sized according to the provided configuration.
|
||||
//! For an evenly spaced horizontal stacking see the `HStack` layout.
|
||||
//!
|
||||
//! # Example
|
||||
//! ...
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Key = terminal.Key;
|
||||
|
||||
const log = std.log.scoped(.layout_hcontainer);
|
||||
|
||||
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!isTaggedUnion(Element)) {
|
||||
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) {
|
||||
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) {
|
||||
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[1].name);
|
||||
}
|
||||
const Container = struct {
|
||||
element: Element,
|
||||
container_size: u8, // 0 - 100 %
|
||||
};
|
||||
const Containers = std.ArrayList(Container);
|
||||
const LayoutType = @typeInfo(Element).Union.fields[0].type;
|
||||
const WidgetType = @typeInfo(Element).Union.fields[1].type;
|
||||
const Events = std.ArrayList(Event);
|
||||
return struct {
|
||||
// TODO: current focused `Element`?
|
||||
size: terminal.Size = undefined,
|
||||
containers: Containers = undefined,
|
||||
events: Events = undefined,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, children: anytype) @This() {
|
||||
const ArgsType = @TypeOf(children);
|
||||
const args_type_info = @typeInfo(ArgsType);
|
||||
if (args_type_info != .Struct) {
|
||||
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
|
||||
}
|
||||
comptime var total_size = 0;
|
||||
const fields_info = args_type_info.Struct.fields;
|
||||
var containers = Containers.initCapacity(allocator, fields_info.len) catch @panic("OOM");
|
||||
inline for (comptime fields_info) |field| {
|
||||
const child = @field(children, field.name);
|
||||
const ChildType = @TypeOf(child);
|
||||
const child_type_info = @typeInfo(ChildType);
|
||||
if (child_type_info != .Struct) {
|
||||
@compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType));
|
||||
}
|
||||
const child_fields = child_type_info.Struct.fields;
|
||||
if (child_fields.len != 2) {
|
||||
@compileError("expected nested tuple or struct to have exactly 2 fields, but found " ++ child_fields.len);
|
||||
}
|
||||
const element = @field(child, child_fields[0].name);
|
||||
const ElementType = @TypeOf(element);
|
||||
const element_size = @field(child, child_fields[1].name);
|
||||
const ElementSizeType = @TypeOf(element_size);
|
||||
if (ElementSizeType != u8 and ElementSizeType != comptime_int) {
|
||||
@compileError("expected an u8 or comptime_int as second argument of nested tuple or struct child, but found " ++ @typeName(ElementSizeType));
|
||||
}
|
||||
total_size += element_size;
|
||||
if (total_size > 100) {
|
||||
@compileError("cannot place element: " ++ child_fields[0].name ++ " as total size of used container elements would overflow");
|
||||
}
|
||||
if (ElementType == WidgetType) {
|
||||
containers.append(.{
|
||||
.element = .{ .widget = element },
|
||||
.container_size = element_size,
|
||||
}) catch {};
|
||||
continue;
|
||||
}
|
||||
if (ElementType == LayoutType) {
|
||||
containers.append(.{
|
||||
.element = .{ .layout = element },
|
||||
.container_size = element_size,
|
||||
}) catch {};
|
||||
continue;
|
||||
}
|
||||
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
|
||||
}
|
||||
return .{
|
||||
.containers = containers,
|
||||
.events = Events.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
this.containers.deinit();
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !*Events {
|
||||
this.events.clearRetainingCapacity();
|
||||
// order is important
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
// adjust size according to the containing elements
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
// adjust size according to the container size
|
||||
var offset: u16 = 0;
|
||||
for (this.containers.items) |*container| {
|
||||
const cols = @divTrunc(size.cols * container.container_size, 100);
|
||||
const sub_event: Event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col + offset,
|
||||
.row = size.anchor.row,
|
||||
},
|
||||
.cols = cols,
|
||||
.rows = size.rows,
|
||||
},
|
||||
};
|
||||
offset += cols;
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(sub_event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(sub_event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return &this.events;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
184
src/layout/VContainer.zig
Normal file
184
src/layout/VContainer.zig
Normal file
@@ -0,0 +1,184 @@
|
||||
//! Vertical Container layout for nested `Layout`s and/or `Widget`s.
|
||||
//! The contained elements are sized according to the provided configuration.
|
||||
//! For an evenly spaced vertical stacking see the `VStack` layout.
|
||||
//!
|
||||
//! # Example
|
||||
//! ...
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Key = terminal.Key;
|
||||
|
||||
const log = std.log.scoped(.layout_vcontainer);
|
||||
|
||||
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!isTaggedUnion(Element)) {
|
||||
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) {
|
||||
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
|
||||
}
|
||||
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) {
|
||||
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[1].name);
|
||||
}
|
||||
const Container = struct {
|
||||
element: Element,
|
||||
container_size: u8, // 0 - 100 %
|
||||
};
|
||||
const Containers = std.ArrayList(Container);
|
||||
const LayoutType = @typeInfo(Element).Union.fields[0].type;
|
||||
const WidgetType = @typeInfo(Element).Union.fields[1].type;
|
||||
const Events = std.ArrayList(Event);
|
||||
return struct {
|
||||
// TODO: current focused `Element`?
|
||||
size: terminal.Size = undefined,
|
||||
containers: Containers = undefined,
|
||||
events: Events = undefined,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, children: anytype) @This() {
|
||||
const ArgsType = @TypeOf(children);
|
||||
const args_type_info = @typeInfo(ArgsType);
|
||||
if (args_type_info != .Struct) {
|
||||
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
|
||||
}
|
||||
comptime var total_size = 0;
|
||||
const fields_info = args_type_info.Struct.fields;
|
||||
var containers = Containers.initCapacity(allocator, fields_info.len) catch @panic("OOM");
|
||||
inline for (comptime fields_info) |field| {
|
||||
const child = @field(children, field.name);
|
||||
const ChildType = @TypeOf(child);
|
||||
const child_type_info = @typeInfo(ChildType);
|
||||
if (child_type_info != .Struct) {
|
||||
@compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType));
|
||||
}
|
||||
const child_fields = child_type_info.Struct.fields;
|
||||
if (child_fields.len != 2) {
|
||||
@compileError("expected nested tuple or struct to have exactly 2 fields, but found " ++ child_fields.len);
|
||||
}
|
||||
const element = @field(child, child_fields[0].name);
|
||||
const ElementType = @TypeOf(element);
|
||||
const element_size = @field(child, child_fields[1].name);
|
||||
const ElementSizeType = @TypeOf(element_size);
|
||||
if (ElementSizeType != u8 and ElementSizeType != comptime_int) {
|
||||
@compileError("expected an u8 or comptime_int as second argument of nested tuple or struct child, but found " ++ @typeName(ElementSizeType));
|
||||
}
|
||||
total_size += element_size;
|
||||
if (total_size > 100) {
|
||||
@compileError("cannot place element: " ++ child_fields[0].name ++ " as total size of used container elements would overflow");
|
||||
}
|
||||
if (ElementType == WidgetType) {
|
||||
containers.append(.{
|
||||
.element = .{ .widget = element },
|
||||
.container_size = element_size,
|
||||
}) catch {};
|
||||
continue;
|
||||
}
|
||||
if (ElementType == LayoutType) {
|
||||
containers.append(.{
|
||||
.element = .{ .layout = element },
|
||||
.container_size = element_size,
|
||||
}) catch {};
|
||||
continue;
|
||||
}
|
||||
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
|
||||
}
|
||||
return .{
|
||||
.containers = containers,
|
||||
.events = Events.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.events.deinit();
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
layout.deinit();
|
||||
},
|
||||
.widget => |*widget| {
|
||||
widget.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
this.containers.deinit();
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) !*Events {
|
||||
this.events.clearRetainingCapacity();
|
||||
// order is important
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
// adjust size according to the containing elements
|
||||
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
|
||||
size.anchor.col,
|
||||
size.anchor.row,
|
||||
size.cols,
|
||||
size.rows,
|
||||
});
|
||||
// adjust size according to the container size
|
||||
var offset: u16 = 0;
|
||||
for (this.containers.items) |*container| {
|
||||
const rows = @divTrunc(size.rows * container.container_size, 100);
|
||||
const sub_event: Event = .{
|
||||
.resize = .{
|
||||
.anchor = .{
|
||||
.col = size.anchor.col,
|
||||
.row = size.anchor.row + offset,
|
||||
},
|
||||
.cols = size.cols,
|
||||
.rows = rows,
|
||||
},
|
||||
};
|
||||
offset += rows;
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(sub_event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(sub_event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
const events = try layout.handle(event);
|
||||
try this.events.appendSlice(events.items);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
if (widget.handle(event)) |e| {
|
||||
try this.events.append(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return &this.events;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
for (this.containers.items) |*container| {
|
||||
switch (container.element) {
|
||||
.layout => |*layout| {
|
||||
try layout.render(renderer);
|
||||
},
|
||||
.widget => |*widget| {
|
||||
try widget.render(renderer);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
208
src/main.zig
208
src/main.zig
@@ -1,208 +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(.default);
|
||||
|
||||
pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
// fail test; can't try in defer as defer is executed after we return
|
||||
if (deinit_status == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .{};
|
||||
var renderer: App.Renderer = .{};
|
||||
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
|
||||
// -> size hint how much should it use?
|
||||
|
||||
// var layout = Layout.createFrom(layout: {
|
||||
// var stack = Layout.HStack.init(allocator, .{
|
||||
// Widget.createFrom(blk: {
|
||||
// var spacer = Widget.Spacer.init();
|
||||
// break :blk &spacer;
|
||||
// }),
|
||||
// Widget.createFrom(blk: {
|
||||
// const file = try std.fs.cwd().openFile("./src/app.zig", .{});
|
||||
// defer file.close();
|
||||
// var widget = Widget.RawText.init(allocator, file);
|
||||
// break :blk &widget;
|
||||
// }),
|
||||
// Widget.createFrom(blk: {
|
||||
// var spacer = Widget.Spacer.init();
|
||||
// break :blk &spacer;
|
||||
// }),
|
||||
// });
|
||||
// break :layout &stack;
|
||||
// });
|
||||
var layout = Layout.createFrom(
|
||||
padding: {
|
||||
var padding = Layout.Margin.init(
|
||||
allocator,
|
||||
.{
|
||||
.left = 15,
|
||||
.right = 15,
|
||||
},
|
||||
.{
|
||||
.layout = Layout.createFrom(framing: {
|
||||
var framing = Layout.Framing.init(allocator, .{ .style = .{ .fg = .{ .index = 6 } } }, .{
|
||||
.layout = Layout.createFrom(vstack: {
|
||||
var vstack = Layout.VStack.init(allocator, .{
|
||||
Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./src/app.zig", .{});
|
||||
defer file.close();
|
||||
var widget = Widget.RawText.init(allocator, file);
|
||||
break :blk &widget;
|
||||
}),
|
||||
Widget.createFrom(blk: {
|
||||
var spacer = Widget.Spacer.init();
|
||||
break :blk &spacer;
|
||||
}),
|
||||
Widget.createFrom(blk: {
|
||||
const file = try std.fs.cwd().openFile("./src/main.zig", .{});
|
||||
defer file.close();
|
||||
var widget = Widget.RawText.init(allocator, file);
|
||||
break :blk &widget;
|
||||
}),
|
||||
});
|
||||
break :vstack &vstack;
|
||||
}),
|
||||
});
|
||||
break :framing &framing;
|
||||
}),
|
||||
},
|
||||
);
|
||||
break :padding &padding;
|
||||
},
|
||||
);
|
||||
// var layout = Layout.createFrom(layout: {
|
||||
// var hstack = Layout.HStack.init(allocator, .{
|
||||
// Widget.createFrom(blk: {
|
||||
// var spacer = Widget.Spacer.init();
|
||||
// break :blk &spacer;
|
||||
// }),
|
||||
// Layout.createFrom(framing: {
|
||||
// var framing = Layout.Framing.init(
|
||||
// allocator,
|
||||
// .{
|
||||
// .style = .{
|
||||
// .fg = .{
|
||||
// .index = 6,
|
||||
// },
|
||||
// },
|
||||
// .frame = .round,
|
||||
// .title = .{
|
||||
// .str = "VStack",
|
||||
// .style = .{
|
||||
// .ul_style = .single,
|
||||
// .ul = .{ .index = 6 },
|
||||
// .bold = true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// .{
|
||||
// .layout = Layout.createFrom(
|
||||
// padding: {
|
||||
// var padding = Layout.Margin.init(
|
||||
// allocator,
|
||||
// .{
|
||||
// .margin = 10,
|
||||
// },
|
||||
// .{
|
||||
// .layout = Layout.createFrom(vstack: {
|
||||
// var vstack = Layout.VStack.init(allocator, .{
|
||||
// Widget.createFrom(blk: {
|
||||
// const file = try std.fs.cwd().openFile("./src/app.zig", .{});
|
||||
// defer file.close();
|
||||
// var widget = Widget.RawText.init(allocator, file);
|
||||
// break :blk &widget;
|
||||
// }),
|
||||
// Widget.createFrom(blk: {
|
||||
// var spacer = Widget.Spacer.init();
|
||||
// break :blk &spacer;
|
||||
// }),
|
||||
// Widget.createFrom(blk: {
|
||||
// const file = try std.fs.cwd().openFile("./src/main.zig", .{});
|
||||
// defer file.close();
|
||||
// var widget = Widget.RawText.init(allocator, file);
|
||||
// break :blk &widget;
|
||||
// }),
|
||||
// });
|
||||
// break :vstack &vstack;
|
||||
// }),
|
||||
// },
|
||||
// );
|
||||
// break :padding &padding;
|
||||
// },
|
||||
// ),
|
||||
// },
|
||||
// );
|
||||
// break :framing &framing;
|
||||
// }),
|
||||
// Widget.createFrom(blk: {
|
||||
// var spacer = Widget.Spacer.init();
|
||||
// break :blk &spacer;
|
||||
// }),
|
||||
// });
|
||||
// break :layout &hstack;
|
||||
// });
|
||||
defer layout.deinit();
|
||||
|
||||
try app.start();
|
||||
defer app.stop() catch unreachable;
|
||||
|
||||
// App.Event loop
|
||||
while (true) {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
.resize => |size| {
|
||||
renderer.resize(size);
|
||||
},
|
||||
.key => |key| {
|
||||
// ctrl+c to quit
|
||||
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
|
||||
app.quit();
|
||||
}
|
||||
if (Key.matches(key, .{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
||||
try app.interrupt();
|
||||
defer app.start() catch @panic("could not start app event loop");
|
||||
var child = std.process.Child.init(&.{"hx"}, allocator);
|
||||
_ = child.spawnAndWait() catch |err| {
|
||||
app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Spawning Helix failed",
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
|
||||
},
|
||||
}
|
||||
// NOTE: this currently re-renders the screen for every key-press -> which might be a bit of an overkill
|
||||
const events = try layout.handle(event);
|
||||
for (events.items) |e| {
|
||||
app.postEvent(e);
|
||||
}
|
||||
try layout.render(&renderer);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ const std = @import("std");
|
||||
pub const Style = @import("Style.zig");
|
||||
|
||||
style: Style = .{},
|
||||
content: []u8 = undefined,
|
||||
content: []const u8 = undefined,
|
||||
|
||||
pub const Result = struct {
|
||||
idx: usize,
|
||||
|
||||
@@ -91,11 +91,13 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type {
|
||||
}
|
||||
|
||||
// import and export of `Widget` implementations
|
||||
pub const Text = @import("widget/Text.zig").Widget(Event, Renderer);
|
||||
pub const RawText = @import("widget/RawText.zig").Widget(Event, Renderer);
|
||||
pub const Spacer = @import("widget/Spacer.zig").Widget(Event, Renderer);
|
||||
};
|
||||
// test widget implementation satisfies the interface
|
||||
comptime Type.Interface.satisfiedBy(Type);
|
||||
comptime Type.Interface.satisfiedBy(Type.Text);
|
||||
comptime Type.Interface.satisfiedBy(Type.RawText);
|
||||
comptime Type.Interface.satisfiedBy(Type.Spacer);
|
||||
return Type;
|
||||
|
||||
50
src/widget/Text.zig
Normal file
50
src/widget/Text.zig
Normal file
@@ -0,0 +1,50 @@
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal.zig");
|
||||
|
||||
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
|
||||
const Error = @import("../event.zig").Error;
|
||||
const Cell = terminal.Cell;
|
||||
|
||||
const log = std.log.scoped(.widget_text);
|
||||
|
||||
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
|
||||
if (!isTaggedUnion(Event)) {
|
||||
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
|
||||
}
|
||||
return struct {
|
||||
contents: []const Cell = undefined,
|
||||
size: terminal.Size = undefined,
|
||||
require_render: bool = false,
|
||||
|
||||
pub fn init(contents: []const Cell) @This() {
|
||||
return .{
|
||||
.contents = contents,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.* = undefined;
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), event: Event) ?Event {
|
||||
switch (event) {
|
||||
// store the received size
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
this.require_render = true;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), renderer: *Renderer) !void {
|
||||
if (!this.require_render) {
|
||||
return;
|
||||
}
|
||||
try renderer.clear(this.size);
|
||||
try renderer.render(this.size, this.contents);
|
||||
this.require_render = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user