From c4639bf4bbf928e1c9847d895e77f80d5d3e3042 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Thu, 20 Feb 2025 23:48:57 +0100 Subject: [PATCH] add(example): input with simple text field --- build.zig | 21 ++++++-- examples/input.zig | 128 ++++++++++++++++++++++++++++++++++++++++++++ examples/layout.zig | 15 ++---- 3 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 examples/input.zig diff --git a/build.zig b/build.zig index c41ab42..13fdda7 100644 --- a/build.zig +++ b/build.zig @@ -5,6 +5,7 @@ pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); const Examples = enum { + Input, Layout, Scrollable, }; @@ -28,14 +29,14 @@ pub fn build(b: *std.Build) void { }); lib.addImport("code_point", zg.module("code_point")); - // Examples.Scrollable - const scrollable = b.addExecutable(.{ - .name = "scrollable", - .root_source_file = b.path("examples/scrollable.zig"), + // Examples.Input + const input = b.addExecutable(.{ + .name = "input", + .root_source_file = b.path("examples/input.zig"), .target = target, .optimize = optimize, }); - scrollable.root_module.addImport("zterm", lib); + input.root_module.addImport("zterm", lib); // Examples.Layout const layout = b.addExecutable(.{ @@ -46,8 +47,18 @@ pub fn build(b: *std.Build) void { }); layout.root_module.addImport("zterm", lib); + // Examples.Scrollable + const scrollable = b.addExecutable(.{ + .name = "scrollable", + .root_source_file = b.path("examples/scrollable.zig"), + .target = target, + .optimize = optimize, + }); + scrollable.root_module.addImport("zterm", lib); + // mapping of user selected example to compile step const exe = switch (example) { + .Input => input, .Layout => layout, .Scrollable => scrollable, }; diff --git a/examples/input.zig b/examples/input.zig new file mode 100644 index 0000000..b1aa93c --- /dev/null +++ b/examples/input.zig @@ -0,0 +1,128 @@ +const std = @import("std"); +const zterm = @import("zterm"); + +const App = zterm.App(union(enum) { + accept: []u21, +}); + +const log = std.log.scoped(.default); + +pub const InputField = struct { + input: std.ArrayList(u21), + queue: *App.Queue, + + pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() { + return .{ + .input = .init(allocator), + .queue = queue, + }; + } + + pub fn deinit(this: @This()) void { + this.input.deinit(); + } + + pub fn element(this: *@This()) App.Element { + return .{ + .ptr = this, + .vtable = &.{ + .handle = handle, + .content = content, + }, + }; + } + + fn handle(ctx: *anyopaque, event: App.Event) !void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + switch (event) { + .key => |key| { + if (key.isAscii()) try this.input.append(key.cp); + + if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter })) + this.queue.push(.{ .accept = try this.input.toOwnedSlice() }); + + if (key.eql(.{ .cp = zterm.input.Backspace }) or key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete })) + _ = this.input.pop(); + }, + else => {}, + } + } + + fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows)); + + if (this.input.items.len == 0) return; + + const row: u16 = 1; + const col: u16 = 1; + + for (this.input.items, 0..) |cp, idx| { + cells[(row * size.cols) + col + idx].style.fg = .black; + cells[(row * size.cols) + col + idx].cp = cp; + + // NOTE: do not write over the contents of this `Container`'s `Size` + if ((row * size.cols) + col + idx == cells.len - 1) break; + } + } +}; + +pub fn main() !void { + errdefer |err| log.err("Application Error: {any}", .{err}); + + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + defer if (gpa.deinit() == .leak) { + log.err("memory leak", .{}); + }; + + const allocator = gpa.allocator(); + + var app: App = .init; + var renderer = zterm.Renderer.Buffered.init(allocator); + defer renderer.deinit(); + + var input_field: InputField = .init(allocator, &app.queue); + defer input_field.deinit(); + + const element = input_field.element(); + + var container = try App.Container.init(allocator, .{ + .rectangle = .{ .fill = .grey }, + .layout = .{ .padding = .all(5) }, + }, .{}); + defer container.deinit(); + + try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element)); + + try app.start(); + defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); + + // event loop + while (true) { + const event = app.nextEvent(); + log.debug("received event: {s}", .{@tagName(event)}); + + switch (event) { + .init => continue, + .quit => break, + .resize => |size| try renderer.resize(size), + .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(), + .accept => |input| { + defer allocator.free(input); + log.info("Accepted input {any}", .{input}); + }, + .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), + else => {}, + } + + container.handle(event) catch |err| app.postEvent(.{ + .err = .{ + .err = err, + .msg = "Container Event handling failed", + }, + }); + + try renderer.render(@TypeOf(container), &container); + try renderer.flush(); + } +} diff --git a/examples/layout.zig b/examples/layout.zig index fae411f..aeccf5b 100644 --- a/examples/layout.zig +++ b/examples/layout.zig @@ -5,7 +5,7 @@ const App = zterm.App(union(enum) { send: []u21, }); -const log = std.log.scoped(.example); +const log = std.log.scoped(.default); pub const InputField = struct { // TODO: I would need the following features: @@ -67,10 +67,6 @@ pub const InputField = struct { for (this.input.items, 0..) |cp, idx| { cells[(row * size.cols) + col + idx].style.fg = .black; cells[(row * size.cols) + col + idx].cp = cp; - // NOTE: line wrapping happens automatically due to the way the container cells are constructed - // - it would also be possible to limit the line to cols you want to display (i.e. then only for a single row) - // - for areas you would rather go ahead and create another - // container and fit the text inside of that container (so it is dynamic to the screen size, etc.) // NOTE: do not write over the contents of this `Container`'s `Size` if ((row * size.cols) + col + idx == cells.len - 1) break; @@ -83,12 +79,9 @@ pub fn main() !void { // TODO: maybe create own allocator as some sort of arena allocator to have consistent memory usage var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; - defer { - const deinit_status = gpa.deinit(); - if (deinit_status == .leak) { - log.err("memory leak", .{}); - } - } + defer if (gpa.deinit() == .leak) { + log.err("memory leak", .{}); + }; const allocator = gpa.allocator(); var app: App = .init;