diff --git a/README.md b/README.md index 6b8082d..3ec22c0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ `zterm` is a terminal user interface library to implement terminal (fullscreen or inline) applications. > [!NOTE] -> Only builds using the master version might will work. +> Only builds using the master version are tested to work. ## Usage @@ -25,3 +25,46 @@ exe.root_module.addImport("zterm", zterm.module("zterm")); ``` For an example you can take a look at [build.zig](build.zig) for an example. + +--- +## Design Goals + +This project draws heavy inspiration from +[clay](https://github.com/nicbarker/clay) in the way the layout is declared by +the user. As terminal applications usually are rendered in intermediate mode, +the rendering is also part of the event loop. Such that every time an event +happens a render call will usually be done as well. However this is not strickly +necessary and can be separated to have a fixed rendering of every 16ms (i.e. for +60 fps), etc. + +There is only one generic container which contains properties and elements (or +children) which can also be containers, such that each layout in the end is +a tree. + +The library is designed to be very basic and not to provide any more complex +elements such as input fields, drop-down menu's, buttons, etc. Some of them are +either easy to implement yourself, specific for you needs or a too complex to +be provided by the library effectively. For these use-cases there may be other +libraries that build on top of this one to provide the complex elements as some +sort of pre-built elements for you to use in your application (or you create +them yourself). + +There are only very few system events, that are used by the built-in containers +and properties accordingly. For you own widgets (i.e. a collection of elements) +you can extend the events to include your own events to communicate between +elements, effect the control flow and the corresponding generated layouts and +much more. + +As this is a terminal based layout library it also provides a rendering pipeline +alongside the event loop implementation. Usually the event loop is waiting +blocking and will only cause a re-draw (**intermediate mode**) after each event. +Even though the each frame is regenerated from scratch each render loop, the +corresponding application is still pretty performant as the renderer uses a +double buffered intermediate mode implementation to only apply the changes from +each frame to the next to the terminal. + +This library is also designed to work accordingly in ssh hosted environments, +such that an application created using this library can be accessed directly +via ssh. This provides security through the ssh protocol and can defer the +synchronization process, as users may access the same running instance. Which is +the primary use-case for myself to create this library in the first place. diff --git a/build.zig b/build.zig index 3a94af0..930bbd6 100644 --- a/build.zig +++ b/build.zig @@ -1,110 +1,42 @@ const std = @import("std"); -// Although this function looks imperative, note that its job is to -// declaratively construct a build graph that will be executed by an external -// runner. pub fn build(b: *std.Build) void { - // Standard target options allows the person running `zig build` to choose - // what target to build for. Here we do not override the defaults, which - // means any target is allowed, and the default is native. Other options - // for restricting supported target set are available. const target = b.standardTargetOptions(.{}); - - // Standard optimization options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not - // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); const zg = b.dependency("zg", .{ .target = target, .optimize = optimize, }); - const interface = b.dependency("interface", .{ - .target = target, - .optimize = optimize, - }); + // library const lib = b.addModule("zterm", .{ .root_source_file = b.path("src/zterm.zig"), .target = target, .optimize = optimize, }); - lib.addImport("interface", interface.module("interface")); lib.addImport("code_point", zg.module("code_point")); - // example executables - const stack_example = b.addExecutable(.{ - .name = "stack", - .root_source_file = b.path("examples/stack.zig"), - .target = target, - .optimize = optimize, - }); - stack_example.root_module.addImport("zterm", lib); + // TODO: examples (not yet available) + // const stack_example = b.addExecutable(.{ + // .name = "stack", + // .root_source_file = b.path("examples/stack.zig"), + // .target = target, + // .optimize = optimize, + // }); + // stack_example.root_module.addImport("zterm", lib); + // b.installArtifact(stack_example); - const container_example = b.addExecutable(.{ - .name = "container", - .root_source_file = b.path("examples/container.zig"), - .target = target, - .optimize = optimize, - }); - container_example.root_module.addImport("zterm", lib); - - const padding_example = b.addExecutable(.{ - .name = "padding", - .root_source_file = b.path("examples/padding.zig"), - .target = target, - .optimize = optimize, - }); - padding_example.root_module.addImport("zterm", lib); - - const exec_example = b.addExecutable(.{ - .name = "exec", - .root_source_file = b.path("examples/exec.zig"), - .target = target, - .optimize = optimize, - }); - exec_example.root_module.addImport("zterm", lib); - - const tui_example = b.addExecutable(.{ - .name = "tui", - .root_source_file = b.path("examples/tui.zig"), - .target = target, - .optimize = optimize, - }); - tui_example.root_module.addImport("zterm", lib); - - const tabs_example = b.addExecutable(.{ - .name = "tabs", - .root_source_file = b.path("examples/tabs.zig"), - .target = target, - .optimize = optimize, - }); - tabs_example.root_module.addImport("zterm", lib); - - // This declares intent for the executable to be installed into the - // standard location when the user invokes the "install" step (the default - // step when running `zig build`). - b.installArtifact(stack_example); - b.installArtifact(container_example); - b.installArtifact(padding_example); - b.installArtifact(exec_example); - b.installArtifact(tui_example); - b.installArtifact(tabs_example); - - // Creates a step for unit testing. This only builds the test executable - // but does not run it. + // testing const lib_unit_tests = b.addTest(.{ .root_source_file = b.path("src/zterm.zig"), .target = target, .optimize = optimize, }); - lib_unit_tests.root_module.addImport("zg", zg.module("code_point")); + lib_unit_tests.root_module.addImport("code_point", zg.module("code_point")); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); - // Similar to creating the run step earlier, this exposes a `test` step to - // the `zig build --help` menu, providing a way for the user to request - // running the unit tests. const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_lib_unit_tests.step); } diff --git a/build.zig.zon b/build.zig.zon index 5998c03..1908181 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -27,10 +27,6 @@ .url = "git+https://codeberg.org/atman/zg#a363f507fc39b96fc48d693665a823a358345326", .hash = "1220fe42e39fd141c84fd7d5cf69945309bb47253033e68788f99bdfe5585fbc711a", }, - .interface = .{ - .url = "git+https://github.com/yves-biener/zig-interface#ef47e045df19e09250fff45c0702d014fb3d3c37", - .hash = "1220a442e8d9b813572bab7a55eef504c83b628f0b17fd283e776dbc1d1a3d98e842", - }, }, .paths = .{ "build.zig", diff --git a/examples/container.zig b/examples/container.zig deleted file mode 100644 index 30c0a34..0000000 --- a/examples/container.zig +++ /dev/null @@ -1,96 +0,0 @@ -const std = @import("std"); -const zterm = @import("zterm"); - -const App = zterm.App( - union(enum) {}, - zterm.Renderer.Direct, - true, -); -const Key = zterm.Key; -const Layout = App.Layout; -const Widget = App.Widget; - -const log = std.log.scoped(.container); - -pub fn main() !void { - errdefer |err| log.err("Application Error: {any}", .{err}); - - var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; - defer { - const deinit_status = gpa.deinit(); - // fail test; can't try in defer as defer is executed after we return - if (deinit_status == .leak) { - log.err("memory leak", .{}); - } - } - const allocator = gpa.allocator(); - - var app: App = .{}; - var renderer: App.Renderer = .{}; - // TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents - // -> size hint how much should it use? - - var layout = Layout.createFrom(Layout.HContainer.init(allocator, .{ - .{ - Widget.createFrom(Widget.Spacer.init(allocator)), - 15, - }, - .{ - Layout.createFrom(Layout.VContainer.init(allocator, .{ - .{ - Widget.createFrom(Widget.Spacer.init(allocator)), - 25, - }, - .{ - Widget.createFrom(blk: { - const file = try std.fs.cwd().openFile("./src/app.zig", .{}); - defer file.close(); - break :blk Widget.RawText.init(allocator, file); - }), - 50, - }, - .{ - Widget.createFrom(Widget.Spacer.init(allocator)), - 25, - }, - })), - 70, - }, - .{ - Widget.createFrom(Widget.Spacer.init(allocator)), - 15, - }, - })); - defer layout.deinit(); - - try app.start(null); - defer app.stop() catch unreachable; - - // App.Event loop - while (true) { - const event = app.nextEvent(); - log.debug("received event: {s}", .{@tagName(event)}); - - switch (event) { - .quit => break, - .resize => |size| { - renderer.resize(size); - }, - .key => |key| { - // ctrl+c to quit - if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) { - app.quit(); - } - }, - .err => |err| { - log.err("Received {any} with message: {s}", .{ err.err, err.msg }); - }, - else => {}, - } - const events = try layout.handle(event); - for (events.items) |e| { - app.postEvent(e); - } - try layout.render(&renderer); - } -} diff --git a/examples/exec.zig b/examples/exec.zig deleted file mode 100644 index 16996f2..0000000 --- a/examples/exec.zig +++ /dev/null @@ -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); - } -} diff --git a/examples/padding.zig b/examples/padding.zig deleted file mode 100644 index f992870..0000000 --- a/examples/padding.zig +++ /dev/null @@ -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); - } -} diff --git a/examples/stack.zig b/examples/stack.zig deleted file mode 100644 index b5ff52a..0000000 --- a/examples/stack.zig +++ /dev/null @@ -1,133 +0,0 @@ -const std = @import("std"); -const zterm = @import("zterm"); - -const App = zterm.App( - union(enum) {}, - zterm.Renderer.Buffered, - true, -); -const Key = zterm.Key; -const Layout = App.Layout; -const Widget = App.Widget; - -const log = std.log.scoped(.stack); - -pub fn main() !void { - errdefer |err| log.err("Application Error: {any}", .{err}); - - var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; - defer { - const deinit_status = gpa.deinit(); - // fail test; can't try in defer as defer is executed after we return - if (deinit_status == .leak) { - log.err("memory leak", .{}); - } - } - const allocator = gpa.allocator(); - - var app: App = .{}; - var renderer = App.Renderer.init(allocator); - defer renderer.deinit(); - // TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents - // -> size hint how much should it use? - - var layout = Layout.createFrom(Layout.Framing.init(allocator, .{ - .style = .{ - .fg = .{ - .index = 6, - }, - }, - .frame = .round, - .title = .{ - .str = "HStack", - .style = .{ - .ul_style = .single, - .ul = .{ .index = 6 }, - .bold = true, - }, - }, - }, .{ - .layout = Layout.createFrom(Layout.HStack.init(allocator, .{ - Widget.createFrom(Widget.Spacer.init(allocator)), - Layout.createFrom(Layout.Framing.init( - allocator, - .{ - .style = .{ - .fg = .{ - .index = 6, - }, - }, - .frame = .round, - .title = .{ - .str = "VStack", - .style = .{ - .ul_style = .single, - .ul = .{ .index = 6 }, - .bold = true, - }, - }, - }, - .{ - .layout = Layout.createFrom( - Layout.Margin.init( - allocator, - .{ - .margin = 10, - }, - .{ - .layout = Layout.createFrom(Layout.VStack.init(allocator, .{ - // Widget.createFrom(blk: { - // const file = try std.fs.cwd().openFile("./examples/stack.zig", .{}); - // defer file.close(); - // break :blk Widget.RawText.init(allocator, file); - // }), - Widget.createFrom(Widget.Spacer.init(allocator)), - Widget.createFrom(Widget.Spacer.init(allocator)), - Widget.createFrom(Widget.Spacer.init(allocator)), - // Widget.createFrom(blk: { - // const file = try std.fs.cwd().openFile("./examples/stack.zig", .{}); - // defer file.close(); - // break :blk Widget.RawText.init(allocator, file); - // }), - })), - }, - ), - ), - }, - )), - Widget.createFrom(Widget.Spacer.init(allocator)), - })), - })); - defer layout.deinit(); - - try app.start(null); - defer app.stop() catch unreachable; - - // App.Event loop - while (true) { - const event = app.nextEvent(); - log.debug("received event: {s}", .{@tagName(event)}); - - switch (event) { - .quit => break, - .resize => |size| { - renderer.resize(size); - }, - .key => |key| { - // ctrl+c to quit - if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) { - app.quit(); - } - }, - .err => |err| { - log.err("Received {any} with message: {s}", .{ err.err, err.msg }); - }, - else => {}, - } - const events = try layout.handle(event); - for (events.items) |e| { - app.postEvent(e); - } - try layout.render(&renderer); - } -} diff --git a/examples/tabs.zig b/examples/tabs.zig deleted file mode 100644 index fe0624b..0000000 --- a/examples/tabs.zig +++ /dev/null @@ -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); - } -} diff --git a/examples/tui.zig b/examples/tui.zig deleted file mode 100644 index 0ca5886..0000000 --- a/examples/tui.zig +++ /dev/null @@ -1,139 +0,0 @@ -const std = @import("std"); -const zterm = @import("zterm"); - -const App = zterm.App( - union(enum) { - view: union(enum) { - tui, // view instance to the corresponding view for 'tui' - }, - }, - zterm.Renderer.Buffered, - true, -); -const Cell = zterm.Cell; -const Key = zterm.Key; -const Layout = App.Layout; -const Widget = App.Widget; -const View = App.View; - -const Tui = struct { - const Events = std.ArrayList(App.Event); - allocator: std.mem.Allocator, - layout: Layout, - - pub fn init(allocator: std.mem.Allocator) *Tui { - var tui = allocator.create(Tui) catch @panic("Out of memory: tui.zig"); - tui.allocator = allocator; - tui.layout = Layout.createFrom(Layout.VStack.init(allocator, .{ - Layout.createFrom(Layout.HStack.init(allocator, .{ - Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ - .{ .rune = 'Y', .style = .{ .fg = .{ .index = 6 }, .bold = true } }, - })), - Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ - .{ .rune = 'F', .style = .{ .fg = .{ .index = 6 }, .bold = true } }, - })), - Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ - .{ .rune = 'C', .style = .{ .fg = .{ .index = 6 }, .bold = true } }, - })), - })), - // .{ - // Layout.createFrom(Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{ - // .widget = Widget.createFrom(Widget.Text.init(allocator, .default, &[1]Cell{ - // .{ .rune = 'D', .style = .{ .ul = .default, .ul_style = .single } }, - // })), - // })), - // 90, - // }, - })); - return tui; - } - - pub fn deinit(this: *Tui) void { - this.layout.deinit(); - this.allocator.destroy(this); - } - - pub fn enable(this: *Tui) void { - _ = this; - } - - pub fn disable(this: *Tui) void { - _ = this; - } - - pub fn handle(this: *Tui, event: App.Event) !*Events { - return try this.layout.handle(event); - } - - pub fn render(this: *Tui, renderer: *App.Renderer) !void { - try this.layout.render(renderer); - } -}; - -// TODO: create additional example with a bit more complex functionality for -// dynamic layouts, switching views, etc. - -const log = std.log.scoped(.tui); - -pub fn main() !void { - errdefer |err| log.err("Application Error: {any}", .{err}); - - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - - const allocator = arena.allocator(); - - var app: App = .{}; - var renderer = App.Renderer.init(allocator); - defer renderer.deinit(); - - var view: View = undefined; - - var tui_view = View.createFrom(Tui.init(allocator)); - defer tui_view.deinit(); - - view = tui_view; - try app.start(null); - defer app.stop() catch unreachable; - - // App.Event loop - while (true) { - const event = app.nextEvent(); - - switch (event) { - .quit => break, - .resize => |size| { - renderer.resize(size); - }, - .key => |key| { - // ctrl+c to quit - if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) { - app.quit(); - } - }, - .err => |err| { - log.err("Received {any} with message: {s}", .{ err.err, err.msg }); - }, - .view => |v| { - switch (v) { - .tui => { - view = tui_view; - // NOTE: report potentially new screen size - const events = try view.handle(.{ .resize = renderer.size }); - for (events.items) |e| { - app.postEvent(e); - } - }, - } - }, - else => {}, - } - - const events = try view.handle(event); - for (events.items) |e| { - app.postEvent(e); - } - try view.render(&renderer); - try renderer.flush(); - } -} diff --git a/src/app.zig b/src/app.zig index 500ad48..fdb236a 100644 --- a/src/app.zig +++ b/src/app.zig @@ -6,7 +6,7 @@ const event = @import("event.zig"); const mergeTaggedUnions = event.mergeTaggedUnions; const isTaggedUnion = event.isTaggedUnion; -const Key = terminal.Key; +const Key = @import("key.zig"); const Queue = @import("queue.zig").Queue; const log = std.log.scoped(.app); diff --git a/src/terminal/Cell.zig b/src/cell.zig similarity index 51% rename from src/terminal/Cell.zig rename to src/cell.zig index 2ff7327..55135bb 100644 --- a/src/terminal/Cell.zig +++ b/src/cell.zig @@ -1,27 +1,20 @@ const std = @import("std"); -pub const Style = @import("Style.zig"); +const Style = @import("Style.zig"); + +pub const Cell = @This(); style: Style = .{}, rune: u8 = ' ', -pub const Character = struct { - grapheme: []const u8, - width: u8, -}; - -pub fn eql(this: @This(), other: @This()) bool { +pub fn eql(this: Cell, other: Cell) bool { return this.rune == other.rune and this.style.eql(other.style); } -pub fn reset(this: *@This()) void { +pub fn reset(this: *Cell) void { this.style = .{ .fg = .default, .bg = .default, .ul = .default, .ul_style = .off }; this.rune = ' '; } -pub fn value(this: @This(), writer: anytype) !void { +pub fn value(this: Cell, writer: anytype) !void { try this.style.value(writer, this.rune); } - -test { - _ = Style; -} diff --git a/src/color.zig b/src/color.zig new file mode 100644 index 0000000..07f2f37 --- /dev/null +++ b/src/color.zig @@ -0,0 +1,86 @@ +const std = @import("std"); + +pub const Color = union(enum) { + default, + index: u8, + rgb: [3]u8, + + pub fn eql(a: Color, b: Color) bool { + switch (a) { + .default => return b == .default, + .index => |a_idx| { + switch (b) { + .index => |b_idx| return a_idx == b_idx, + else => return false, + } + }, + .rgb => |a_rgb| { + switch (b) { + .rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and + a_rgb[1] == b_rgb[1] and + a_rgb[2] == b_rgb[2], + else => return false, + } + }, + } + } + + pub fn rgbFromUint(val: u24) Color { + const r_bits = val & 0b11111111_00000000_00000000; + const g_bits = val & 0b00000000_11111111_00000000; + const b_bits = val & 0b00000000_00000000_11111111; + const rgb = [_]u8{ + @truncate(r_bits >> 16), + @truncate(g_bits >> 8), + @truncate(b_bits), + }; + return .{ .rgb = rgb }; + } + + /// parse an XParseColor-style rgb specification into an rgb Color. The spec + /// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always + /// be the same as the low two bits. + pub fn rgbFromSpec(spec: []const u8) !Color { + var iter = std.mem.splitScalar(u8, spec, ':'); + const prefix = iter.next() orelse return error.InvalidColorSpec; + if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec; + + const spec_str = iter.next() orelse return error.InvalidColorSpec; + + var spec_iter = std.mem.splitScalar(u8, spec_str, '/'); + + const r_raw = spec_iter.next() orelse return error.InvalidColorSpec; + if (r_raw.len != 4) return error.InvalidColorSpec; + + const g_raw = spec_iter.next() orelse return error.InvalidColorSpec; + if (g_raw.len != 4) return error.InvalidColorSpec; + + const b_raw = spec_iter.next() orelse return error.InvalidColorSpec; + if (b_raw.len != 4) return error.InvalidColorSpec; + + const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16); + const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16); + const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16); + + return .{ + .rgb = [_]u8{ r, g, b }, + }; + } + + test "rgbFromSpec" { + const spec = "rgb:aaaa/bbbb/cccc"; + const actual = try rgbFromSpec(spec); + switch (actual) { + .rgb => |rgb| { + try std.testing.expectEqual(0xAA, rgb[0]); + try std.testing.expectEqual(0xBB, rgb[1]); + try std.testing.expectEqual(0xCC, rgb[2]); + }, + else => try std.testing.expect(false), + } + } +}; + +test { + _ = Color; +} diff --git a/src/container.zig b/src/container.zig new file mode 100644 index 0000000..e69de29 diff --git a/src/terminal/ctlseqs.zig b/src/ctlseqs.zig similarity index 100% rename from src/terminal/ctlseqs.zig rename to src/ctlseqs.zig diff --git a/src/element.zig b/src/element.zig new file mode 100644 index 0000000..e69de29 diff --git a/src/event.zig b/src/event.zig index 8d9f85c..86c0e24 100644 --- a/src/event.zig +++ b/src/event.zig @@ -3,26 +3,31 @@ const std = @import("std"); const terminal = @import("terminal.zig"); -const Size = terminal.Size; -const Key = terminal.Key; - -pub const Error = struct { - err: anyerror, - msg: []const u8, -}; +const Size = @import("size.zig"); +const Key = @import("key.zig"); // System events available to every application. -// TODO: should this also already include the .view enum option? pub const SystemEvent = union(enum) { + /// Initialize event, which is send once at the beginning of the event loop and before the first render loop + init, + /// Quit event to signify the end of the event loop (rendering should stop afterwards) quit, - err: Error, + /// Error event to notify other containers about a recoverable error + err: struct { + err: anyerror, + /// associated error message + msg: []const u8, + }, + /// Resize event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in resize: Size, + /// Input key event received from the user key: Key, + /// Focus event for mouse interaction + /// TODO: this should instead be a union with a `Size` to derive which container / element the focus meant for focus: bool, }; pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type { - // TODO: should this expect one of the unions to contain the .view value option with its corresponding associated type? if (!isTaggedUnion(A) or !isTaggedUnion(B)) { @compileError("Both types for merging tagged unions need to be of type `union(enum)`."); } diff --git a/src/terminal/Key.zig b/src/key.zig similarity index 99% rename from src/terminal/Key.zig rename to src/key.zig index eec5f8c..ec459c4 100644 --- a/src/terminal/Key.zig +++ b/src/key.zig @@ -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, diff --git a/src/layout.zig b/src/layout.zig deleted file mode 100644 index cc69d8e..0000000 --- a/src/layout.zig +++ /dev/null @@ -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; -} diff --git a/src/layout/Framing.zig b/src/layout/Framing.zig deleted file mode 100644 index 3885e0f..0000000 --- a/src/layout/Framing.zig +++ /dev/null @@ -1,198 +0,0 @@ -//! Framing layout for a nested `Layout`s or `Widget`s. -//! -//! # Example -//! ... -const std = @import("std"); -const terminal = @import("../terminal.zig"); - -const isTaggedUnion = @import("../event.zig").isTaggedUnion; -const Error = @import("../event.zig").Error; -const Key = terminal.Key; -const Style = terminal.Cell.Style; - -const log = std.log.scoped(.layout_framing); - -pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type { - if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); - } - if (!isTaggedUnion(Element)) { - @compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); - } - if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) { - @compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name); - } - if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) { - @compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name); - } - const Events = std.ArrayList(Event); - return struct { - allocator: std.mem.Allocator, - size: terminal.Size, - require_render: bool, - element: Element, - events: Events, - config: Config, - - const Config = struct { - style: Style = .{ .fg = .default }, - frame: Frame = .round, - title: Title = .{}, - - const Title = struct { - str: []const u8 = &.{}, - style: Style = .{ .fg = .default }, - }; - - const Frame = enum { - round, - square, - }; - }; - - pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) *@This() { - var this = allocator.create(@This()) catch @panic("Framing.zig: Failed to create."); - this.allocator = allocator; - this.require_render = true; - this.config = config; - this.element = element; - this.events = Events.init(allocator); - return this; - } - - pub fn deinit(this: *@This()) void { - this.events.deinit(); - switch ((&this.element).*) { - .layout => |*layout| { - layout.deinit(); - }, - .widget => |*widget| { - widget.deinit(); - }, - } - this.allocator.destroy(this); - } - - pub fn handle(this: *@This(), event: Event) !*Events { - this.events.clearRetainingCapacity(); - // order is important - switch (event) { - .resize => |size| { - this.size = size; - this.require_render = true; - log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ - size.anchor.col, - size.anchor.row, - size.cols, - size.rows, - }); - // adjust size according to the containing elements - const sub_event: Event = .{ - .resize = .{ - .anchor = .{ - .col = size.anchor.col + 1, - .row = size.anchor.row + 1, - }, - .cols = size.cols -| 2, - .rows = size.rows -| 2, - }, - }; - switch ((&this.element).*) { - .layout => |*layout| { - const events = try layout.handle(sub_event); - try this.events.appendSlice(events.items); - }, - .widget => |*widget| { - if (widget.handle(sub_event)) |e| { - try this.events.append(e); - } - }, - } - }, - else => { - switch ((&this.element).*) { - .layout => |*layout| { - const events = try layout.handle(event); - try this.events.appendSlice(events.items); - }, - .widget => |*widget| { - if (widget.handle(event)) |e| { - try this.events.append(e); - } - }, - } - }, - } - return &this.events; - } - - const round_frame: [6][]const u8 = .{ "╭", "─", "╮", "│", "╰", "╯" }; - const square_frame: [6][]const u8 = .{ "┌", "─", "┐", "│", "└", "┘" }; - - fn renderFrame(this: *@This(), renderer: *Renderer) !void { - // FIXME: use renderer instead! - _ = renderer; - const frame = switch (this.config.frame) { - .round => round_frame, - .square => square_frame, - }; - std.debug.assert(frame.len == 6); - // render top: +---+ - try terminal.setCursorPosition(this.size.anchor); - const writer = terminal.writer(); - // try this.config.style.value(writer, frame[0]); - for (0..this.config.title.str.len) |i| { - try this.config.title.style.value(writer, this.config.title.str[i]); - } - for (0..this.size.cols -| 2 -| this.config.title.str.len) |_| { - // try this.config.style.value(writer, frame[1]); - } - // try this.config.style.value(writer, frame[2]); - // render left: | - for (1..this.size.rows -| 1) |r| { - const row: u16 = @truncate(r); - try terminal.setCursorPosition(.{ - .col = this.size.anchor.col, - .row = this.size.anchor.row + row, - }); - // try this.config.style.value(writer, frame[3]); - } - // render right: | - for (1..this.size.rows -| 1) |r| { - const row: u16 = @truncate(r); - try terminal.setCursorPosition(.{ - .col = this.size.anchor.col + this.size.cols -| 1, - .row = this.size.anchor.row + row, - }); - // try this.config.style.value(writer, frame[3]); - } - // render bottom: +---+ - try terminal.setCursorPosition(.{ - .col = this.size.anchor.col, - .row = this.size.anchor.row + this.size.rows - 1, - }); - // try this.config.style.value(writer, frame[4]); - for (0..this.size.cols -| 2) |_| { - // try this.config.style.value(writer, frame[1]); - } - // try this.config.style.value(writer, frame[5]); - } - - pub fn render(this: *@This(), renderer: *Renderer) !void { - if (this.require_render) { - try renderer.clear(this.size); - try this.renderFrame(renderer); - this.require_render = false; - } - - switch ((&this.element).*) { - .layout => |*layout| { - try layout.render(renderer); - }, - .widget => |*widget| { - try widget.render(renderer); - }, - } - } - }; -} diff --git a/src/layout/HContainer.zig b/src/layout/HContainer.zig deleted file mode 100644 index b28e179..0000000 --- a/src/layout/HContainer.zig +++ /dev/null @@ -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); - }, - } - } - } - }; -} diff --git a/src/layout/HStack.zig b/src/layout/HStack.zig deleted file mode 100644 index 8c01af3..0000000 --- a/src/layout/HStack.zig +++ /dev/null @@ -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); - }, - } - } - } - }; -} diff --git a/src/layout/Margin.zig b/src/layout/Margin.zig deleted file mode 100644 index b6177c5..0000000 --- a/src/layout/Margin.zig +++ /dev/null @@ -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); - }, - } - } - }; -} diff --git a/src/layout/Padding.zig b/src/layout/Padding.zig deleted file mode 100644 index 649cdc6..0000000 --- a/src/layout/Padding.zig +++ /dev/null @@ -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); - }, - } - } - }; -} diff --git a/src/layout/Tab.zig b/src/layout/Tab.zig deleted file mode 100644 index b5cea83..0000000 --- a/src/layout/Tab.zig +++ /dev/null @@ -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); - }, - } - } - }; -} diff --git a/src/layout/VContainer.zig b/src/layout/VContainer.zig deleted file mode 100644 index 9e7428c..0000000 --- a/src/layout/VContainer.zig +++ /dev/null @@ -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); - }, - } - } - } - }; -} diff --git a/src/layout/VStack.zig b/src/layout/VStack.zig deleted file mode 100644 index f7bdd82..0000000 --- a/src/layout/VStack.zig +++ /dev/null @@ -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); - }, - } - } - } - }; -} diff --git a/src/properties.zig b/src/properties.zig new file mode 100644 index 0000000..e69de29 diff --git a/src/render.zig b/src/render.zig index 729fc18..30acd98 100644 --- a/src/render.zig +++ b/src/render.zig @@ -106,7 +106,7 @@ pub fn Buffered(comptime fullscreen: bool) type { if (cs.eql(cvs)) continue; // render differences found in virtual screen - // TODO: improve the writing speed (many unecessary writes (i.e. the style for every character..)) + // TODO: improve the writing speed (many unnecessary writes (i.e. the style for every character..)) try terminal.setCursorPosition(.{ .row = @truncate(row), .col = @truncate(col) }); try cvs.value(writer); // update screen to be the virtual screen for the next frame diff --git a/src/size.zig b/src/size.zig new file mode 100644 index 0000000..613cf7a --- /dev/null +++ b/src/size.zig @@ -0,0 +1,10 @@ +pub const Size = @This(); + +pub const Position = struct { + col: u16, + row: u16, +}; + +anchor: Position = .{}, +cols: u16, +rows: u16, diff --git a/src/terminal/Style.zig b/src/style.zig similarity index 70% rename from src/terminal/Style.zig rename to src/style.zig index 6538d77..bc195ba 100644 --- a/src/terminal/Style.zig +++ b/src/style.zig @@ -10,6 +10,10 @@ const std = @import("std"); const ctlseqs = @import("ctlseqs.zig"); +const Color = @import("color.zig").Color; + +pub const Style = @This(); + pub const Underline = enum { off, single, @@ -19,87 +23,6 @@ pub const Underline = enum { dashed, }; -pub const Color = union(enum) { - default, - index: u8, - rgb: [3]u8, - - pub fn eql(a: @This(), b: @This()) bool { - switch (a) { - .default => return b == .default, - .index => |a_idx| { - switch (b) { - .index => |b_idx| return a_idx == b_idx, - else => return false, - } - }, - .rgb => |a_rgb| { - switch (b) { - .rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and - a_rgb[1] == b_rgb[1] and - a_rgb[2] == b_rgb[2], - else => return false, - } - }, - } - } - - pub fn rgbFromUint(val: u24) Color { - const r_bits = val & 0b11111111_00000000_00000000; - const g_bits = val & 0b00000000_11111111_00000000; - const b_bits = val & 0b00000000_00000000_11111111; - const rgb = [_]u8{ - @truncate(r_bits >> 16), - @truncate(g_bits >> 8), - @truncate(b_bits), - }; - return .{ .rgb = rgb }; - } - - /// parse an XParseColor-style rgb specification into an rgb Color. The spec - /// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always - /// be the same as the low two bits. - pub fn rgbFromSpec(spec: []const u8) !Color { - var iter = std.mem.splitScalar(u8, spec, ':'); - const prefix = iter.next() orelse return error.InvalidColorSpec; - if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec; - - const spec_str = iter.next() orelse return error.InvalidColorSpec; - - var spec_iter = std.mem.splitScalar(u8, spec_str, '/'); - - const r_raw = spec_iter.next() orelse return error.InvalidColorSpec; - if (r_raw.len != 4) return error.InvalidColorSpec; - - const g_raw = spec_iter.next() orelse return error.InvalidColorSpec; - if (g_raw.len != 4) return error.InvalidColorSpec; - - const b_raw = spec_iter.next() orelse return error.InvalidColorSpec; - if (b_raw.len != 4) return error.InvalidColorSpec; - - const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16); - const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16); - const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16); - - return .{ - .rgb = [_]u8{ r, g, b }, - }; - } - - test "rgbFromSpec" { - const spec = "rgb:aaaa/bbbb/cccc"; - const actual = try rgbFromSpec(spec); - switch (actual) { - .rgb => |rgb| { - try std.testing.expectEqual(0xAA, rgb[0]); - try std.testing.expectEqual(0xBB, rgb[1]); - try std.testing.expectEqual(0xCC, rgb[2]); - }, - else => try std.testing.expect(false), - } - } -}; - fg: Color = .default, bg: Color = .default, ul: Color = .default, @@ -113,7 +36,7 @@ reverse: bool = false, invisible: bool = false, strikethrough: bool = false, -pub fn eql(this: @This(), other: @This()) bool { +pub fn eql(this: Style, other: Style) bool { return this.fg.eql(other.fg) and this.bg.eql(other.bg) and this.ul.eql(other.ul) and @@ -129,7 +52,7 @@ pub fn eql(this: @This(), other: @This()) bool { /// Merge _other_ `Style` to _this_ style and overwrite _this_ `Style`'s value /// if the _other_ value differs from the default value. -pub fn merge(this: *@This(), other: @This()) void { +pub fn merge(this: *Style, other: Style) void { if (other.fg != .default) this.fg = other.fg; if (other.bg != .default) this.bg = other.bg; if (other.ul != .default) this.ul = other.ul; @@ -143,7 +66,7 @@ pub fn merge(this: *@This(), other: @This()) void { if (other.strikethrough != false) this.strikethrough = other.strikethrough; } -fn start(this: @This(), writer: anytype) !void { +fn start(this: Style, writer: anytype) !void { // foreground switch (this.fg) { .default => try std.fmt.format(writer, ctlseqs.fg_reset, .{}), @@ -240,7 +163,7 @@ fn start(this: @This(), writer: anytype) !void { } } -fn end(this: @This(), writer: anytype) !void { +fn end(this: Style, writer: anytype) !void { // foreground switch (this.fg) { .default => {}, @@ -298,7 +221,7 @@ fn end(this: @This(), writer: anytype) !void { } } -pub fn value(this: @This(), writer: anytype, content: u8) !void { +pub fn value(this: Style, writer: anytype, content: u8) !void { try this.start(writer); _ = try writer.write(&[_]u8{content}); try this.end(writer); @@ -307,7 +230,3 @@ pub fn value(this: @This(), writer: anytype, content: u8) !void { // TODO: implement helper functions for terminal capabilities: // - links / url display (osc 8) // - show / hide cursor? - -test { - _ = Color; -} diff --git a/src/terminal.zig b/src/terminal.zig index 9ca3710..79741a0 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -1,9 +1,9 @@ const std = @import("std"); -pub const Key = @import("terminal/Key.zig"); -pub const Size = @import("terminal/Size.zig"); -pub const Position = @import("terminal/Position.zig"); -pub const Cell = @import("terminal/Cell.zig"); -pub const code_point = @import("code_point"); +const code_point = @import("code_point"); + +const Key = @import("key.zig"); +const Size = @import("size.zig"); +const Cell = @import("cell.zig"); const log = std.log.scoped(.terminal); @@ -78,13 +78,13 @@ pub fn writer() Writer { return .{ .context = .{} }; } -pub fn setCursorPosition(pos: Position) !void { +pub fn setCursorPosition(pos: Size.Position) !void { var buf: [64]u8 = undefined; const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col }); _ = try std.posix.write(std.posix.STDIN_FILENO, value); } -pub fn getCursorPosition() !Position { +pub fn getCursorPosition() !Size.Position { // Needs Raw mode (no wait for \n) to work properly cause // control sequence will not be written without it. _ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[6n"); @@ -227,7 +227,3 @@ fn getReportMode(ps: u8) ReportMode { else => ReportMode.not_recognized, }; } - -test { - _ = Cell; -} diff --git a/src/terminal/Position.zig b/src/terminal/Position.zig deleted file mode 100644 index 4a0f7b0..0000000 --- a/src/terminal/Position.zig +++ /dev/null @@ -1,2 +0,0 @@ -col: u16, -row: u16, diff --git a/src/terminal/Size.zig b/src/terminal/Size.zig deleted file mode 100644 index eb2b18f..0000000 --- a/src/terminal/Size.zig +++ /dev/null @@ -1,5 +0,0 @@ -const Position = @import("Position.zig"); - -anchor: Position = .{ .col = 0, .row = 0 }, // top left corner by default -cols: u16, -rows: u16, diff --git a/src/view.zig b/src/view.zig deleted file mode 100644 index f77470d..0000000 --- a/src/view.zig +++ /dev/null @@ -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, - }, - }; - } - }; -} diff --git a/src/widget.zig b/src/widget.zig deleted file mode 100644 index 6bdc8bd..0000000 --- a/src/widget.zig +++ /dev/null @@ -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; -} diff --git a/src/widget/Input.zig b/src/widget/Input.zig deleted file mode 100644 index d82230e..0000000 --- a/src/widget/Input.zig +++ /dev/null @@ -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)); //