From 5148d20f528f1e95b257dd953ff023f9ba3ab077 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 21 Jan 2024 19:12:46 -0600 Subject: [PATCH] widgets: create an initial text_input and border widget Signed-off-by: Tim Culverhouse --- build.zig | 2 +- examples/text_input.zig | 79 +++++++++++++++++++++++++++++++++++++++ src/Key.zig | 4 +- src/main.zig | 2 + src/widgets/TextInput.zig | 52 ++++++++++++++++++++++++++ src/widgets/border.zig | 31 +++++++++++++++ src/widgets/main.zig | 2 + 7 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 examples/text_input.zig create mode 100644 src/widgets/TextInput.zig create mode 100644 src/widgets/border.zig create mode 100644 src/widgets/main.zig diff --git a/build.zig b/build.zig index b4bfc0e..33c7b7f 100644 --- a/build.zig +++ b/build.zig @@ -14,7 +14,7 @@ pub fn build(b: *std.Build) void { const exe = b.addExecutable(.{ .name = "vaxis", - .root_source_file = .{ .path = "examples/main.zig" }, + .root_source_file = .{ .path = "examples/text_input.zig" }, .target = target, .optimize = optimize, }); diff --git a/examples/text_input.zig b/examples/text_input.zig new file mode 100644 index 0000000..cb6df30 --- /dev/null +++ b/examples/text_input.zig @@ -0,0 +1,79 @@ +const std = @import("std"); +const vaxis = @import("vaxis"); +const Cell = vaxis.Cell; +const TextInput = vaxis.widgets.TextInput; +const border = vaxis.widgets.border; + +const log = std.log.scoped(.main); +pub fn main() !void { + 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 alloc = gpa.allocator(); + + // Initialize Vaxis + var vx = try vaxis.init(Event, .{}); + defer vx.deinit(alloc); + + // Start the read loop. This puts the terminal in raw mode and begins + // reading user input + try vx.start(); + defer vx.stop(); + + // Optionally enter the alternate screen + try vx.enterAltScreen(); + + var text_input: TextInput = .{}; + + // The main event loop. Vaxis provides a thread safe, blocking, buffered + // queue which can serve as the primary event queue for an application + outer: while (true) { + // nextEvent blocks until an event is in the queue + const event = vx.nextEvent(); + log.debug("event: {}\r\n", .{event}); + // exhaustive switching ftw. Vaxis will send events if your EventType + // enum has the fields for those events (ie "key_press", "winsize") + switch (event) { + .key_press => |key| { + text_input.update(.{ .key_press = key }); + if (key.codepoint == 'c' and key.mods.ctrl) { + break :outer; + } + }, + .winsize => |ws| { + try vx.resize(alloc, ws); + }, + else => {}, + } + + // vx.window() returns the root window. This window is the size of the + // terminal and can spawn child windows as logical areas. Child windows + // cannot draw outside of their bounds + const win = vx.window(); + // Clear the entire space because we are drawing in immediate mode. + // vaxis double buffers the screen. This new frame will be compared to + // the old and only updated cells will be drawn + win.clear(); + const child = win.initChild(win.width / 2 - 20, win.height / 2 - 3, .{ .limit = 40 }, .{ .limit = 3 }); + // draw the text_input using a bordered window + text_input.draw(border.all(child, .{})); + + // Render the screen + try vx.render(); + } +} + +// Our EventType. This can contain internal events as well as Vaxis events. +// Internal events can be posted into the same queue as vaxis events to allow +// for a single event loop with exhaustive switching. Booya +const Event = union(enum) { + key_press: vaxis.Key, + winsize: vaxis.Winsize, + focus_in, + foo: u8, +}; diff --git a/src/Key.zig b/src/Key.zig index d0486a5..6bace5a 100644 --- a/src/Key.zig +++ b/src/Key.zig @@ -14,7 +14,9 @@ pub const Modifiers = packed struct(u8) { /// the unicode codepoint of the key event. codepoint: u21, -/// the text generated from the key event, if any +/// the text generated from the key event. This will only contain a value if the +/// event generated a multi-codepoint grapheme. If there was only a single +/// codepoint, library users can encode the codepoint directly text: ?[]const u8 = null, /// the shifted codepoint of this key event. This will only be present if the diff --git a/src/main.zig b/src/main.zig index bac994d..502088d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,6 +7,8 @@ pub const Cell = cell.Cell; pub const Key = @import("Key.zig"); pub const Winsize = @import("Tty.zig").Winsize; +pub const widgets = @import("widgets/main.zig"); + /// Initialize a Vaxis application. pub fn init(comptime EventType: type, opts: Options) !Vaxis(EventType) { return Vaxis(EventType).init(opts); diff --git a/src/widgets/TextInput.zig b/src/widgets/TextInput.zig new file mode 100644 index 0000000..7a6ee9f --- /dev/null +++ b/src/widgets/TextInput.zig @@ -0,0 +1,52 @@ +const std = @import("std"); +const Cell = @import("../cell.zig").Cell; +const Key = @import("../Key.zig"); +const Window = @import("../Window.zig"); + +const log = std.log.scoped(.text_input); + +const TextInput = @This(); + +/// The events that this widget handles +const Event = union(enum) { + key_press: Key, +}; + +// Index of our cursor +cursor_idx: usize = 0, + +// the actual line of input +buffer: [4096]u8 = undefined, +buffer_idx: usize = 0, + +pub fn update(self: *TextInput, event: Event) void { + switch (event) { + .key_press => |key| { + switch (key.codepoint) { + 0x20...0x7E => { + self.buffer[self.buffer_idx] = @truncate(key.codepoint); + self.buffer_idx += 1; + self.cursor_idx += 1; + }, + Key.backspace => { + // TODO: this only works at the end of the array. Then + // again, we don't have any means to move the cursor yet + if (self.buffer_idx == 0) return; + self.buffer_idx -= 1; + }, + else => {}, + } + }, + } +} + +pub fn draw(self: *TextInput, win: Window) void { + for (0.., self.buffer[0..self.buffer_idx]) |i, b| { + win.writeCell(i, 0, .{ + .char = .{ + .grapheme = &[_]u8{b}, + .width = 1, + }, + }); + } +} diff --git a/src/widgets/border.zig b/src/widgets/border.zig new file mode 100644 index 0000000..dc1e73a --- /dev/null +++ b/src/widgets/border.zig @@ -0,0 +1,31 @@ +const Window = @import("../Window.zig"); +const cell = @import("../cell.zig"); +const Character = cell.Character; +const Style = cell.Style; + +const horizontal = Character{ .grapheme = "─", .width = 1 }; +const vertical = Character{ .grapheme = "│", .width = 1 }; +const top_left = Character{ .grapheme = "╭", .width = 1 }; +const top_right = Character{ .grapheme = "╮", .width = 1 }; +const bottom_right = Character{ .grapheme = "╯", .width = 1 }; +const bottom_left = Character{ .grapheme = "╰", .width = 1 }; + +pub fn all(win: Window, style: Style) Window { + const h = win.height; + const w = win.width; + win.writeCell(0, 0, .{ .char = top_left, .style = style }); + win.writeCell(0, h - 1, .{ .char = bottom_left, .style = style }); + win.writeCell(w - 1, 0, .{ .char = top_right, .style = style }); + win.writeCell(w - 1, h - 1, .{ .char = bottom_right, .style = style }); + var i: usize = 1; + while (i < (h - 1)) : (i += 1) { + win.writeCell(0, i, .{ .char = vertical, .style = style }); + win.writeCell(w - 1, i, .{ .char = vertical, .style = style }); + } + i = 1; + while (i < w - 1) : (i += 1) { + win.writeCell(i, 0, .{ .char = horizontal, .style = style }); + win.writeCell(i, h - 1, .{ .char = horizontal, .style = style }); + } + return win.initChild(1, 1, .{ .limit = w - 2 }, .{ .limit = w - 2 }); +} diff --git a/src/widgets/main.zig b/src/widgets/main.zig new file mode 100644 index 0000000..1eabb24 --- /dev/null +++ b/src/widgets/main.zig @@ -0,0 +1,2 @@ +pub const TextInput = @import("TextInput.zig"); +pub const border = @import("border.zig");