From d2f02897dc0950f0e90591b5386134ab794f0006 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Mon, 22 Jan 2024 20:09:35 -0600 Subject: [PATCH] render: use different internal model of screen We use two screens: one which the user provides a slice of bytes for the graphemes, and the user owns the bytes. We copy those bytes to our internal model so that we can compare between frames Signed-off-by: Tim Culverhouse --- examples/text_input.zig | 5 +- src/InternalScreen.zig | 73 +++++++++++++++++++++++++++ src/Screen.zig | 17 +++---- src/vaxis.zig | 16 +++--- src/widgets/TextInput.zig | 103 +++++++++++++++++++++++++++++++------- 5 files changed, 179 insertions(+), 35 deletions(-) create mode 100644 src/InternalScreen.zig diff --git a/examples/text_input.zig b/examples/text_input.zig index 8708147..66f0596 100644 --- a/examples/text_input.zig +++ b/examples/text_input.zig @@ -31,7 +31,8 @@ pub fn main() !void { // We'll adjust the color index every keypress for the border var color_idx: u8 = 0; - var text_input: TextInput = .{}; + var text_input = TextInput.init(alloc); + defer text_input.deinit(); // The main event loop. Vaxis provides a thread safe, blocking, buffered // queue which can serve as the primary event queue for an application @@ -47,7 +48,7 @@ pub fn main() !void { 255 => 0, else => color_idx + 1, }; - text_input.update(.{ .key_press = key }); + try text_input.update(.{ .key_press = key }); if (key.codepoint == 'c' and key.mods.ctrl) { break :outer; } diff --git a/src/InternalScreen.zig b/src/InternalScreen.zig new file mode 100644 index 0000000..ea0e949 --- /dev/null +++ b/src/InternalScreen.zig @@ -0,0 +1,73 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Style = @import("cell.zig").Style; +const Cell = @import("cell.zig").Cell; + +const log = std.log.scoped(.internal_screen); + +const InternalScreen = @This(); + +pub const InternalCell = struct { + char: std.ArrayList(u8) = undefined, + style: Style = .{}, + + pub fn eql(self: InternalCell, cell: Cell) bool { + return std.mem.eql(u8, self.char.items, cell.char.grapheme) and std.meta.eql(self.style, cell.style); + } +}; + +width: usize = 0, +height: usize = 0, + +buf: []InternalCell = undefined, + +cursor_row: usize = 0, +cursor_col: usize = 0, +cursor_vis: bool = false, + +/// sets each cell to the default cell +pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !InternalScreen { + var screen = InternalScreen{}; + screen.buf = try alloc.alloc(InternalCell, w * h); + for (screen.buf, 0..) |_, i| { + screen.buf[i] = .{ + .char = try std.ArrayList(u8).initCapacity(alloc, 1), + }; + } + screen.width = w; + screen.height = h; + return screen; +} + +pub fn deinit(self: *InternalScreen, alloc: std.mem.Allocator) void { + for (self.buf, 0..) |_, i| { + self.buf[i].char.deinit(); + } + + alloc.free(self.buf); +} + +/// writes a cell to a location. 0 indexed +pub fn writeCell( + self: *InternalScreen, + col: usize, + row: usize, + char: []const u8, + style: Style, +) void { + if (self.width < col) { + // column out of bounds + return; + } + if (self.height < row) { + // height out of bounds + return; + } + const i = (row * self.width) + col; + assert(i < self.buf.len); + self.buf[i].char.clearRetainingCapacity(); + self.buf[i].char.appendSlice(char) catch { + log.warn("couldn't write grapheme", .{}); + }; + self.buf[i].style = style; +} diff --git a/src/Screen.zig b/src/Screen.zig index 170451f..32440c8 100644 --- a/src/Screen.zig +++ b/src/Screen.zig @@ -16,24 +16,21 @@ cursor_row: usize = 0, cursor_col: usize = 0, cursor_vis: bool = false, -/// sets each cell to the default cell -pub fn init(self: *Screen) void { +pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen { + var self = Screen{ + .buf = try alloc.alloc(Cell, w * h), + .width = w, + .height = h, + }; for (self.buf, 0..) |_, i| { self.buf[i] = .{}; } + return self; } - pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void { alloc.free(self.buf); } -pub fn resize(self: *Screen, alloc: std.mem.Allocator, w: usize, h: usize) !void { - alloc.free(self.buf); - self.buf = try alloc.alloc(Cell, w * h); - self.width = w; - self.height = h; -} - /// writes a cell to a location. 0 indexed pub fn writeCell(self: *Screen, col: usize, row: usize, cell: Cell) void { if (self.width < col) { diff --git a/src/vaxis.zig b/src/vaxis.zig index 60cf7cb..cbd9d38 100644 --- a/src/vaxis.zig +++ b/src/vaxis.zig @@ -6,6 +6,7 @@ const Tty = @import("Tty.zig"); const Winsize = Tty.Winsize; const Key = @import("Key.zig"); const Screen = @import("Screen.zig"); +const InternalScreen = @import("InternalScreen.zig"); const Window = @import("Window.zig"); const Options = @import("Options.zig"); const Style = @import("cell.zig").Style; @@ -38,7 +39,7 @@ pub fn Vaxis(comptime T: type) type { screen: Screen, // The last screen we drew. We keep this so we can efficiently update on // the next render - screen_last: Screen, + screen_last: InternalScreen = undefined, alt_screen: bool, @@ -113,11 +114,14 @@ pub fn Vaxis(comptime T: type) type { /// freed when resizing pub fn resize(self: *Self, alloc: std.mem.Allocator, winsize: Winsize) !void { log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); - try self.screen.resize(alloc, winsize.cols, winsize.rows); + self.screen.deinit(alloc); + self.screen = try Screen.init(alloc, winsize.cols, winsize.rows); + // try self.screen.int(alloc, winsize.cols, winsize.rows); // we only init our current screen. This has the effect of redrawing // every cell - self.screen.init(); - try self.screen_last.resize(alloc, winsize.cols, winsize.rows); + self.screen_last.deinit(alloc); + self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows); + // try self.screen_last.resize(alloc, winsize.cols, winsize.rows); } /// returns a Window comprising of the entire terminal screen @@ -214,7 +218,7 @@ pub fn Vaxis(comptime T: type) type { } // If cell is the same as our last frame, we don't need to do // anything - if (std.meta.eql(cell, self.screen_last.buf[i])) { + if (self.screen_last.buf[i].eql(cell)) { reposition = true; // Close any osc8 sequence we might be in before // repositioning @@ -225,7 +229,7 @@ pub fn Vaxis(comptime T: type) type { } defer cursor = cell.style; // Set this cell in the last frame - self.screen_last.buf[i] = cell; + self.screen_last.writeCell(col, row, cell.char.grapheme, cell.style); // reposition the cursor, if needed if (reposition) { diff --git a/src/widgets/TextInput.zig b/src/widgets/TextInput.zig index 0dd7498..cc883d7 100644 --- a/src/widgets/TextInput.zig +++ b/src/widgets/TextInput.zig @@ -16,27 +16,43 @@ const Event = union(enum) { // Index of our cursor cursor_idx: usize = 0, +grapheme_count: usize = 0, -// the actual line of input -buffer: [4096]u8 = undefined, -buffer_idx: usize = 0, +buf: std.ArrayList(u8), -pub fn update(self: *TextInput, event: Event) void { +pub fn init(alloc: std.mem.Allocator) TextInput { + return TextInput{ + .buf = std.ArrayList(u8).init(alloc), + }; +} + +pub fn deinit(self: *TextInput) void { + self.buf.deinit(); +} + +pub fn update(self: *TextInput, event: Event) !void { switch (event) { .key_press => |key| { if (key.text) |text| { - @memcpy(self.buffer[self.buffer_idx .. self.buffer_idx + text.len], text); - self.buffer_idx += text.len; - self.cursor_idx += strWidth(text, .full) catch 1; + try self.buf.insertSlice(self.byteOffsetToCursor(), text); + self.cursor_idx += 1; + self.grapheme_count += 1; } switch (key.codepoint) { 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 - // This also doesn't work with graphemes yet - if (self.buffer_idx == 0) return; - self.buffer_idx -= 1; - self.cursor_idx -= 1; + if (self.cursor_idx == 0) return; + // Get the grapheme behind our cursor + self.deleteBeforeCursor(); + }, + Key.delete => { + if (self.cursor_idx == self.grapheme_count) return; + self.deleteAtCursor(); + }, + Key.left => { + if (self.cursor_idx > 0) self.cursor_idx -= 1; + }, + Key.right => { + if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1; }, else => {}, } @@ -45,11 +61,12 @@ pub fn update(self: *TextInput, event: Event) void { } pub fn draw(self: *TextInput, win: Window) void { - const input = self.buffer[0..self.buffer_idx]; - var iter = GraphemeIterator.init(input); + var iter = GraphemeIterator.init(self.buf.items); var col: usize = 0; + var i: usize = 0; + var cursor_idx: usize = 0; while (iter.next()) |grapheme| { - const g = grapheme.slice(input); + const g = grapheme.slice(self.buf.items); const w = strWidth(g, .full) catch 1; win.writeCell(col, 0, .{ .char = .{ @@ -58,6 +75,58 @@ pub fn draw(self: *TextInput, win: Window) void { }, }); col += w; + i += 1; + if (i == self.cursor_idx) cursor_idx = col; + } + win.showCursor(cursor_idx, 0); +} + +// returns the number of bytes before the cursor +fn byteOffsetToCursor(self: TextInput) usize { + var iter = GraphemeIterator.init(self.buf.items); + var offset: usize = 0; + var i: usize = 0; + while (iter.next()) |grapheme| { + if (i == self.cursor_idx) break; + offset += grapheme.len; + i += 1; + } + return offset; +} + +fn deleteBeforeCursor(self: *TextInput) void { + var iter = GraphemeIterator.init(self.buf.items); + var offset: usize = 0; + var i: usize = 1; + while (iter.next()) |grapheme| { + if (i == self.cursor_idx) { + var j: usize = 0; + while (j < grapheme.len) : (j += 1) { + _ = self.buf.orderedRemove(offset); + } + self.cursor_idx -= 1; + self.grapheme_count -= 1; + return; + } + offset += grapheme.len; + i += 1; + } +} + +fn deleteAtCursor(self: *TextInput) void { + var iter = GraphemeIterator.init(self.buf.items); + var offset: usize = 0; + var i: usize = 1; + while (iter.next()) |grapheme| { + if (i == self.cursor_idx + 1) { + var j: usize = 0; + while (j < grapheme.len) : (j += 1) { + _ = self.buf.orderedRemove(offset); + } + self.grapheme_count -= 1; + return; + } + offset += grapheme.len; + i += 1; } - win.showCursor(self.cursor_idx, 0); }