widgets: create an initial text_input and border widget

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Tim Culverhouse 2024-01-21 19:12:46 -06:00
parent 558b64544c
commit 5148d20f52
7 changed files with 170 additions and 2 deletions

View file

@ -14,7 +14,7 @@ pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "vaxis", .name = "vaxis",
.root_source_file = .{ .path = "examples/main.zig" }, .root_source_file = .{ .path = "examples/text_input.zig" },
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });

79
examples/text_input.zig Normal file
View file

@ -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,
};

View file

@ -14,7 +14,9 @@ pub const Modifiers = packed struct(u8) {
/// the unicode codepoint of the key event. /// the unicode codepoint of the key event.
codepoint: u21, 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, text: ?[]const u8 = null,
/// the shifted codepoint of this key event. This will only be present if the /// the shifted codepoint of this key event. This will only be present if the

View file

@ -7,6 +7,8 @@ pub const Cell = cell.Cell;
pub const Key = @import("Key.zig"); pub const Key = @import("Key.zig");
pub const Winsize = @import("Tty.zig").Winsize; pub const Winsize = @import("Tty.zig").Winsize;
pub const widgets = @import("widgets/main.zig");
/// Initialize a Vaxis application. /// Initialize a Vaxis application.
pub fn init(comptime EventType: type, opts: Options) !Vaxis(EventType) { pub fn init(comptime EventType: type, opts: Options) !Vaxis(EventType) {
return Vaxis(EventType).init(opts); return Vaxis(EventType).init(opts);

52
src/widgets/TextInput.zig Normal file
View file

@ -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,
},
});
}
}

31
src/widgets/border.zig Normal file
View file

@ -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 });
}

2
src/widgets/main.zig Normal file
View file

@ -0,0 +1,2 @@
pub const TextInput = @import("TextInput.zig");
pub const border = @import("border.zig");