From 4fbbebbf0c429f29f2c22cdeb559bd5378c4e085 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Mon, 12 Aug 2024 20:18:41 -0500 Subject: [PATCH] widgets(text_input): use internal GapBuffer impl Use a tiny GapBuffer implementation internal to the library. Signed-off-by: Tim Culverhouse --- build.zig | 8 - build.zig.zon | 4 - src/widgets.zig | 5 +- src/widgets/TextInput.zig | 321 +++++++++++++++++++++++--------------- 4 files changed, 197 insertions(+), 141 deletions(-) diff --git a/build.zig b/build.zig index 78c58ec..a6e75f0 100644 --- a/build.zig +++ b/build.zig @@ -3,13 +3,11 @@ const std = @import("std"); pub fn build(b: *std.Build) void { const include_libxev = b.option(bool, "libxev", "Enable support for libxev library (default: true)") orelse true; const include_images = b.option(bool, "images", "Enable support for images (default: true)") orelse true; - const include_text_input = b.option(bool, "text_input", "Enable support for the TextInput widget (default: true)") orelse true; const include_aio = b.option(bool, "aio", "Enable support for zig-aio library (default: false)") orelse false; const options = b.addOptions(); options.addOption(bool, "libxev", include_libxev); options.addOption(bool, "images", include_images); - options.addOption(bool, "text_input", include_text_input); options.addOption(bool, "aio", include_aio); const options_mod = options.createModule(); @@ -27,10 +25,6 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .target = target, }) else null; - const gap_buffer_dep = if (include_text_input) b.lazyDependency("gap_buffer", .{ - .optimize = optimize, - .target = target, - }) else null; const xev_dep = if (include_libxev) b.lazyDependency("libxev", .{ .optimize = optimize, .target = target, @@ -50,7 +44,6 @@ pub fn build(b: *std.Build) void { vaxis_mod.addImport("grapheme", zg_dep.module("grapheme")); vaxis_mod.addImport("DisplayWidth", zg_dep.module("DisplayWidth")); if (zigimg_dep) |dep| vaxis_mod.addImport("zigimg", dep.module("zigimg")); - if (gap_buffer_dep) |dep| vaxis_mod.addImport("gap_buffer", dep.module("gap_buffer")); if (xev_dep) |dep| vaxis_mod.addImport("xev", dep.module("xev")); if (aio_dep) |dep| vaxis_mod.addImport("aio", dep.module("aio")); if (aio_dep) |dep| vaxis_mod.addImport("coro", dep.module("coro")); @@ -100,7 +93,6 @@ pub fn build(b: *std.Build) void { tests.root_module.addImport("grapheme", zg_dep.module("grapheme")); tests.root_module.addImport("DisplayWidth", zg_dep.module("DisplayWidth")); if (zigimg_dep) |dep| tests.root_module.addImport("zigimg", dep.module("zigimg")); - if (gap_buffer_dep) |dep| tests.root_module.addImport("gap_buffer", dep.module("gap_buffer")); tests.root_module.addImport("build_options", options_mod); const tests_run = b.addRunArtifact(tests); diff --git a/build.zig.zon b/build.zig.zon index 006968e..b7a4dfd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,10 +8,6 @@ .hash = "1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5", .lazy = true, }, - .gap_buffer = .{ - .url = "git+https://github.com/ryleelyman/GapBuffer.zig#9039708e09fc3eb5f698ab5694a436afe503c6a6", - .hash = "1220f525973ae804ec0284556bfc47db7b6a8dc86464a853956ef859d6e0fb5fa93b", - }, .zg = .{ .url = "git+https://codeberg.org/dude_the_builder/zg?ref=master#689ab6b83d08c02724b99d199d650ff731250998", .hash = "12200d1ce5f9733a9437415d85665ad5fbc85a4d27689fd337fecad8014acffe3aa5", diff --git a/src/widgets.zig b/src/widgets.zig index 45084af..34a580a 100644 --- a/src/widgets.zig +++ b/src/widgets.zig @@ -11,7 +11,4 @@ pub const LineNumbers = @import("widgets/LineNumbers.zig"); pub const TextView = @import("widgets/TextView.zig"); pub const CodeView = @import("widgets/CodeView.zig"); pub const Terminal = @import("widgets/terminal/Terminal.zig"); - -// Widgets with dependencies - -pub const TextInput = if (opts.text_input) @import("widgets/TextInput.zig") else undefined; +pub const TextInput = @import("widgets/TextInput.zig"); diff --git a/src/widgets/TextInput.zig b/src/widgets/TextInput.zig index fc1bbf3..76286a7 100644 --- a/src/widgets/TextInput.zig +++ b/src/widgets/TextInput.zig @@ -3,7 +3,6 @@ const assert = std.debug.assert; const Key = @import("../Key.zig"); const Cell = @import("../Cell.zig"); const Window = @import("../Window.zig"); -const GapBuffer = @import("gap_buffer").GapBuffer; const Unicode = @import("../Unicode.zig"); const TextInput = @This(); @@ -18,7 +17,7 @@ const ellipsis: Cell.Character = .{ .grapheme = "…", .width = 1 }; // Index of our cursor cursor_idx: usize = 0, grapheme_count: usize = 0, -buf: GapBuffer(u8), +buf: Buffer, /// the number of graphemes to skip when drawing. Used for horizontal scrolling draw_offset: usize = 0, @@ -33,7 +32,7 @@ unicode: *const Unicode, pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextInput { return TextInput{ - .buf = GapBuffer(u8).init(alloc), + .buf = Buffer.init(alloc), .unicode = unicode, }; } @@ -47,37 +46,36 @@ pub fn update(self: *TextInput, event: Event) !void { .key_press => |key| { if (key.matches(Key.backspace, .{})) { if (self.cursor_idx == 0) return; - try self.deleteBeforeCursor(); + self.deleteBeforeCursor(); } else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) { - if (self.cursor_idx == self.grapheme_count) return; - try self.deleteAtCursor(); + self.deleteAfterCursor(); } else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) { - if (self.cursor_idx > 0) self.cursor_idx -= 1; + self.cursorLeft(); } else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) { - if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1; + self.cursorRight(); } else if (key.matches('a', .{ .ctrl = true }) or key.matches(Key.home, .{})) { + self.buf.moveGapLeft(self.buf.firstHalf().len); self.cursor_idx = 0; } else if (key.matches('e', .{ .ctrl = true }) or key.matches(Key.end, .{})) { + self.buf.moveGapRight(self.buf.secondHalf().len); self.cursor_idx = self.grapheme_count; } else if (key.matches('k', .{ .ctrl = true })) { - try self.deleteToEnd(); + self.deleteToEnd(); } else if (key.matches('u', .{ .ctrl = true })) { - try self.deleteToStart(); + self.deleteToStart(); } else if (key.text) |text| { - try self.buf.insertSliceBefore(self.byteOffsetToCursor(), text); - self.cursor_idx += 1; - self.grapheme_count += 1; + try self.insertSliceAtCursor(text); } }, } } /// insert text at the cursor position -pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) !void { +pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) std.mem.Allocator.Error!void { var iter = self.unicode.graphemeIterator(data); var byte_offset_to_cursor = self.byteOffsetToCursor(); while (iter.next()) |text| { - try self.buf.insertSliceBefore(byte_offset_to_cursor, text.bytes(data)); + try self.buf.insertSliceAtCursor(text.bytes(data)); byte_offset_to_cursor += text.len; self.cursor_idx += 1; self.grapheme_count += 1; @@ -85,24 +83,16 @@ pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) !void { } pub fn sliceToCursor(self: *TextInput, buf: []u8) []const u8 { - const offset = self.byteOffsetToCursor(); - assert(offset <= buf.len); // provided buf was too small - - if (offset <= self.buf.items.len) { - @memcpy(buf[0..offset], self.buf.items[0..offset]); - } else { - @memcpy(buf[0..self.buf.items.len], self.buf.items); - const second_half = self.buf.secondHalf(); - const copy_len = offset - self.buf.items.len; - @memcpy(buf[self.buf.items.len .. self.buf.items.len + copy_len], second_half[0..copy_len]); - } - return buf[0..offset]; + assert(buf.len >= self.buf.cursor); + @memcpy(buf[0..self.buf.cursor], self.buf.firstHalf()); + return buf[0..self.buf.cursor]; } /// calculates the display width from the draw_offset to the cursor fn widthToCursor(self: *TextInput, win: Window) usize { var width: usize = 0; - var first_iter = self.unicode.graphemeIterator(self.buf.items); + const first_half = self.buf.firstHalf(); + var first_iter = self.unicode.graphemeIterator(first_half); var i: usize = 0; while (first_iter.next()) |grapheme| { defer i += 1; @@ -110,23 +100,32 @@ fn widthToCursor(self: *TextInput, win: Window) usize { continue; } if (i == self.cursor_idx) return width; - const g = grapheme.bytes(self.buf.items); - width += win.gwidth(g); - } - const second_half = self.buf.secondHalf(); - var second_iter = self.unicode.graphemeIterator(second_half); - while (second_iter.next()) |grapheme| { - defer i += 1; - if (i < self.draw_offset) { - continue; - } - if (i == self.cursor_idx) return width; - const g = grapheme.bytes(second_half); + const g = grapheme.bytes(first_half); width += win.gwidth(g); } return width; } +fn cursorLeft(self: *TextInput) void { + if (self.cursor_idx == 0) return; + // We need to find the size of the last grapheme in the first half + var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); + var len: usize = 0; + while (iter.next()) |grapheme| { + len = grapheme.len; + } + self.buf.moveGapLeft(len); + self.cursor_idx -= 1; +} + +fn cursorRight(self: *TextInput) void { + if (self.cursor_idx >= self.grapheme_count) return; + var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); + const grapheme = iter.next() orelse return; + self.buf.moveGapRight(grapheme.len); + self.cursor_idx += 1; +} + pub fn draw(self: *TextInput, win: Window) void { if (self.cursor_idx < self.draw_offset) self.draw_offset = self.cursor_idx; if (win.width == 0) return; @@ -143,7 +142,8 @@ pub fn draw(self: *TextInput, win: Window) void { // assumption!! the gap is never within a grapheme // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. - var first_iter = self.unicode.graphemeIterator(self.buf.items); + const first_half = self.buf.firstHalf(); + var first_iter = self.unicode.graphemeIterator(first_half); var col: usize = 0; var i: usize = 0; while (first_iter.next()) |grapheme| { @@ -151,7 +151,7 @@ pub fn draw(self: *TextInput, win: Window) void { i += 1; continue; } - const g = grapheme.bytes(self.buf.items); + const g = grapheme.bytes(first_half); const w = win.gwidth(g); if (col + w >= win.width) { win.writeCell(win.width - 1, 0, .{ .char = ellipsis }); @@ -220,98 +220,40 @@ fn reset(self: *TextInput) void { } // returns the number of bytes before the cursor -// (since GapBuffers are strictly speaking not contiguous, this is a number in 0..realLength() -// which would need to be fed to realIndex() to get an actual offset into self.buf.items.ptr) pub fn byteOffsetToCursor(self: TextInput) usize { - // assumption! the gap is never in the middle of a grapheme - // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. - var iter = self.unicode.graphemeIterator(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; - } else { - var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf()); - while (second_iter.next()) |grapheme| { - if (i == self.cursor_idx) break; - offset += grapheme.len; - i += 1; - } - } - return offset; + return self.buf.cursor; } -fn deleteToEnd(self: *TextInput) !void { - const offset = self.byteOffsetToCursor(); - try self.buf.replaceRangeAfter(offset, self.buf.realLength() - offset, &.{}); +fn deleteToEnd(self: *TextInput) void { + self.buf.growGapRight(self.buf.secondHalf().len); self.grapheme_count = self.cursor_idx; } -fn deleteToStart(self: *TextInput) !void { - const offset = self.byteOffsetToCursor(); - try self.buf.replaceRangeBefore(0, offset, &.{}); +fn deleteToStart(self: *TextInput) void { + self.buf.growGapLeft(self.buf.cursor); self.grapheme_count -= self.cursor_idx; self.cursor_idx = 0; } -fn deleteBeforeCursor(self: *TextInput) !void { - // assumption! the gap is never in the middle of a grapheme - // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. - var iter = self.unicode.graphemeIterator(self.buf.items); - var offset: usize = 0; - var i: usize = 1; +fn deleteBeforeCursor(self: *TextInput) void { + if (self.cursor_idx == 0) return; + // We need to find the size of the last grapheme in the first half + var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); + var len: usize = 0; while (iter.next()) |grapheme| { - if (i == self.cursor_idx) { - try self.buf.replaceRangeBefore(offset, grapheme.len, &.{}); - self.cursor_idx -= 1; - self.grapheme_count -= 1; - return; - } - offset += grapheme.len; - i += 1; - } else { - var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf()); - while (second_iter.next()) |grapheme| { - if (i == self.cursor_idx) { - try self.buf.replaceRangeBefore(offset, grapheme.len, &.{}); - self.cursor_idx -= 1; - self.grapheme_count -= 1; - return; - } - offset += grapheme.len; - i += 1; - } + len = grapheme.len; } + self.buf.growGapLeft(len); + self.cursor_idx -= 1; + self.grapheme_count -= 1; } -fn deleteAtCursor(self: *TextInput) !void { - // assumption! the gap is never in the middle of a grapheme - // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. - var iter = self.unicode.graphemeIterator(self.buf.items); - var offset: usize = 0; - var i: usize = 1; - while (iter.next()) |grapheme| { - if (i == self.cursor_idx + 1) { - try self.buf.replaceRangeAfter(offset, grapheme.len, &.{}); - self.grapheme_count -= 1; - return; - } - offset += grapheme.len; - i += 1; - } else { - var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf()); - while (second_iter.next()) |grapheme| { - if (i == self.cursor_idx + 1) { - try self.buf.replaceRangeAfter(offset, grapheme.len, &.{}); - self.grapheme_count -= 1; - return; - } - offset += grapheme.len; - i += 1; - } - } +fn deleteAfterCursor(self: *TextInput) void { + if (self.cursor_idx == self.grapheme_count) return; + var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); + const grapheme = iter.next() orelse return; + self.buf.growGapRight(grapheme.len); + self.grapheme_count -= 1; } test "assertion" { @@ -337,10 +279,139 @@ test "sliceToCursor" { var input = init(alloc, &unicode); defer input.deinit(); try input.insertSliceAtCursor("hello, world"); - input.cursor_idx = 2; + input.cursorLeft(); + input.cursorLeft(); + input.cursorLeft(); var buf: [32]u8 = undefined; - try std.testing.expectEqualStrings("he", input.sliceToCursor(&buf)); - input.buf.moveGap(3); - input.cursor_idx = 5; - try std.testing.expectEqualStrings("hello", input.sliceToCursor(&buf)); + try std.testing.expectEqualStrings("hello, wo", input.sliceToCursor(&buf)); + input.cursorRight(); + try std.testing.expectEqualStrings("hello, wor", input.sliceToCursor(&buf)); +} + +const Buffer = struct { + allocator: std.mem.Allocator, + buffer: []u8, + cursor: usize, + gap_size: usize, + + fn init(allocator: std.mem.Allocator) Buffer { + return .{ + .allocator = allocator, + .buffer = &.{}, + .cursor = 0, + .gap_size = 0, + }; + } + + fn deinit(self: *Buffer) void { + self.allocator.free(self.buffer); + } + + fn firstHalf(self: Buffer) []const u8 { + return self.buffer[0..self.cursor]; + } + + fn secondHalf(self: Buffer) []const u8 { + return self.buffer[self.cursor + self.gap_size ..]; + } + + fn grow(self: *Buffer, n: usize) std.mem.Allocator.Error!void { + // Always grow by 512 bytes + const new_size = self.buffer.len + n + 512; + // Allocate the new memory + const new_memory = try self.allocator.alloc(u8, new_size); + // Copy the first half + @memcpy(new_memory[0..self.cursor], self.firstHalf()); + // Copy the second half + const second_half = self.secondHalf(); + @memcpy(new_memory[new_size - second_half.len ..], second_half); + self.allocator.free(self.buffer); + self.buffer = new_memory; + self.gap_size = new_size - second_half.len - self.cursor; + } + + fn insertSliceAtCursor(self: *Buffer, slice: []const u8) std.mem.Allocator.Error!void { + if (slice.len == 0) return; + if (self.gap_size <= slice.len) try self.grow(slice.len); + @memcpy(self.buffer[self.cursor .. self.cursor + slice.len], slice); + self.cursor += slice.len; + self.gap_size -= slice.len; + } + + /// Move the gap n bytes to the left + fn moveGapLeft(self: *Buffer, n: usize) void { + const new_idx = self.cursor -| n; + const dst = self.buffer[new_idx + self.gap_size ..]; + const src = self.buffer[new_idx..self.cursor]; + std.mem.copyForwards(u8, dst, src); + self.cursor = new_idx; + } + + fn moveGapRight(self: *Buffer, n: usize) void { + const new_idx = self.cursor + n; + const dst = self.buffer[self.cursor..]; + const src = self.buffer[self.cursor + self.gap_size .. new_idx + self.gap_size]; + std.mem.copyForwards(u8, dst, src); + self.cursor = new_idx; + } + + /// grow the gap by moving the cursor n bytes to the left + fn growGapLeft(self: *Buffer, n: usize) void { + // gap grows by the delta + self.gap_size += n; + self.cursor -|= n; + } + + /// grow the gap by removing n bytes after the cursor + fn growGapRight(self: *Buffer, n: usize) void { + self.gap_size = @min(self.gap_size + n, self.buffer.len - self.cursor); + } + + fn clearAndFree(self: *Buffer) void { + self.cursor = 0; + self.allocator.free(self.buffer); + self.buffer = &.{}; + self.gap_size = 0; + } + + fn clearRetainingCapacity(self: *Buffer) void { + self.cursor = 0; + self.gap_size = self.buffer.len; + } + + fn toOwnedSlice(self: *Buffer) std.mem.Allocator.Error![]const u8 { + const first_half = self.firstHalf(); + const second_half = self.secondHalf(); + const buf = try self.allocator.alloc(u8, first_half.len + second_half.len); + @memcpy(buf[0..first_half.len], first_half); + @memcpy(buf[first_half.len..], second_half); + self.clearAndFree(); + } +}; + +test "TextInput.zig: Buffer" { + var gap_buf = Buffer.init(std.testing.allocator); + defer gap_buf.deinit(); + + try gap_buf.insertSliceAtCursor("abc"); + try std.testing.expectEqualStrings("abc", gap_buf.firstHalf()); + try std.testing.expectEqualStrings("", gap_buf.secondHalf()); + + gap_buf.moveGapLeft(1); + try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); + try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); + + try gap_buf.insertSliceAtCursor(" "); + try std.testing.expectEqualStrings("ab ", gap_buf.firstHalf()); + try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); + + gap_buf.growGapLeft(1); + try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); + try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); + try std.testing.expectEqual(2, gap_buf.cursor); + + gap_buf.growGapRight(1); + try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); + try std.testing.expectEqualStrings("", gap_buf.secondHalf()); + try std.testing.expectEqual(2, gap_buf.cursor); }