a modern tui library written in zig

widgets(text_input): use internal GapBuffer impl

Use a tiny GapBuffer implementation internal to the library.

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>

Changed files
+197 -141
src
-8
build.zig
··· 3 3 pub fn build(b: *std.Build) void { 4 4 const include_libxev = b.option(bool, "libxev", "Enable support for libxev library (default: true)") orelse true; 5 5 const include_images = b.option(bool, "images", "Enable support for images (default: true)") orelse true; 6 - const include_text_input = b.option(bool, "text_input", "Enable support for the TextInput widget (default: true)") orelse true; 7 6 const include_aio = b.option(bool, "aio", "Enable support for zig-aio library (default: false)") orelse false; 8 7 9 8 const options = b.addOptions(); 10 9 options.addOption(bool, "libxev", include_libxev); 11 10 options.addOption(bool, "images", include_images); 12 - options.addOption(bool, "text_input", include_text_input); 13 11 options.addOption(bool, "aio", include_aio); 14 12 15 13 const options_mod = options.createModule(); ··· 27 25 .optimize = optimize, 28 26 .target = target, 29 27 }) else null; 30 - const gap_buffer_dep = if (include_text_input) b.lazyDependency("gap_buffer", .{ 31 - .optimize = optimize, 32 - .target = target, 33 - }) else null; 34 28 const xev_dep = if (include_libxev) b.lazyDependency("libxev", .{ 35 29 .optimize = optimize, 36 30 .target = target, ··· 50 44 vaxis_mod.addImport("grapheme", zg_dep.module("grapheme")); 51 45 vaxis_mod.addImport("DisplayWidth", zg_dep.module("DisplayWidth")); 52 46 if (zigimg_dep) |dep| vaxis_mod.addImport("zigimg", dep.module("zigimg")); 53 - if (gap_buffer_dep) |dep| vaxis_mod.addImport("gap_buffer", dep.module("gap_buffer")); 54 47 if (xev_dep) |dep| vaxis_mod.addImport("xev", dep.module("xev")); 55 48 if (aio_dep) |dep| vaxis_mod.addImport("aio", dep.module("aio")); 56 49 if (aio_dep) |dep| vaxis_mod.addImport("coro", dep.module("coro")); ··· 100 93 tests.root_module.addImport("grapheme", zg_dep.module("grapheme")); 101 94 tests.root_module.addImport("DisplayWidth", zg_dep.module("DisplayWidth")); 102 95 if (zigimg_dep) |dep| tests.root_module.addImport("zigimg", dep.module("zigimg")); 103 - if (gap_buffer_dep) |dep| tests.root_module.addImport("gap_buffer", dep.module("gap_buffer")); 104 96 tests.root_module.addImport("build_options", options_mod); 105 97 106 98 const tests_run = b.addRunArtifact(tests);
-4
build.zig.zon
··· 8 8 .hash = "1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5", 9 9 .lazy = true, 10 10 }, 11 - .gap_buffer = .{ 12 - .url = "git+https://github.com/ryleelyman/GapBuffer.zig#9039708e09fc3eb5f698ab5694a436afe503c6a6", 13 - .hash = "1220f525973ae804ec0284556bfc47db7b6a8dc86464a853956ef859d6e0fb5fa93b", 14 - }, 15 11 .zg = .{ 16 12 .url = "git+https://codeberg.org/dude_the_builder/zg?ref=master#689ab6b83d08c02724b99d199d650ff731250998", 17 13 .hash = "12200d1ce5f9733a9437415d85665ad5fbc85a4d27689fd337fecad8014acffe3aa5",
+1 -4
src/widgets.zig
··· 11 11 pub const TextView = @import("widgets/TextView.zig"); 12 12 pub const CodeView = @import("widgets/CodeView.zig"); 13 13 pub const Terminal = @import("widgets/terminal/Terminal.zig"); 14 - 15 - // Widgets with dependencies 16 - 17 - pub const TextInput = if (opts.text_input) @import("widgets/TextInput.zig") else undefined; 14 + pub const TextInput = @import("widgets/TextInput.zig");
+196 -125
src/widgets/TextInput.zig
··· 3 3 const Key = @import("../Key.zig"); 4 4 const Cell = @import("../Cell.zig"); 5 5 const Window = @import("../Window.zig"); 6 - const GapBuffer = @import("gap_buffer").GapBuffer; 7 6 const Unicode = @import("../Unicode.zig"); 8 7 9 8 const TextInput = @This(); ··· 18 17 // Index of our cursor 19 18 cursor_idx: usize = 0, 20 19 grapheme_count: usize = 0, 21 - buf: GapBuffer(u8), 20 + buf: Buffer, 22 21 23 22 /// the number of graphemes to skip when drawing. Used for horizontal scrolling 24 23 draw_offset: usize = 0, ··· 33 32 34 33 pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextInput { 35 34 return TextInput{ 36 - .buf = GapBuffer(u8).init(alloc), 35 + .buf = Buffer.init(alloc), 37 36 .unicode = unicode, 38 37 }; 39 38 } ··· 47 46 .key_press => |key| { 48 47 if (key.matches(Key.backspace, .{})) { 49 48 if (self.cursor_idx == 0) return; 50 - try self.deleteBeforeCursor(); 49 + self.deleteBeforeCursor(); 51 50 } else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) { 52 - if (self.cursor_idx == self.grapheme_count) return; 53 - try self.deleteAtCursor(); 51 + self.deleteAfterCursor(); 54 52 } else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) { 55 - if (self.cursor_idx > 0) self.cursor_idx -= 1; 53 + self.cursorLeft(); 56 54 } else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) { 57 - if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1; 55 + self.cursorRight(); 58 56 } else if (key.matches('a', .{ .ctrl = true }) or key.matches(Key.home, .{})) { 57 + self.buf.moveGapLeft(self.buf.firstHalf().len); 59 58 self.cursor_idx = 0; 60 59 } else if (key.matches('e', .{ .ctrl = true }) or key.matches(Key.end, .{})) { 60 + self.buf.moveGapRight(self.buf.secondHalf().len); 61 61 self.cursor_idx = self.grapheme_count; 62 62 } else if (key.matches('k', .{ .ctrl = true })) { 63 - try self.deleteToEnd(); 63 + self.deleteToEnd(); 64 64 } else if (key.matches('u', .{ .ctrl = true })) { 65 - try self.deleteToStart(); 65 + self.deleteToStart(); 66 66 } else if (key.text) |text| { 67 - try self.buf.insertSliceBefore(self.byteOffsetToCursor(), text); 68 - self.cursor_idx += 1; 69 - self.grapheme_count += 1; 67 + try self.insertSliceAtCursor(text); 70 68 } 71 69 }, 72 70 } 73 71 } 74 72 75 73 /// insert text at the cursor position 76 - pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) !void { 74 + pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) std.mem.Allocator.Error!void { 77 75 var iter = self.unicode.graphemeIterator(data); 78 76 var byte_offset_to_cursor = self.byteOffsetToCursor(); 79 77 while (iter.next()) |text| { 80 - try self.buf.insertSliceBefore(byte_offset_to_cursor, text.bytes(data)); 78 + try self.buf.insertSliceAtCursor(text.bytes(data)); 81 79 byte_offset_to_cursor += text.len; 82 80 self.cursor_idx += 1; 83 81 self.grapheme_count += 1; ··· 85 83 } 86 84 87 85 pub fn sliceToCursor(self: *TextInput, buf: []u8) []const u8 { 88 - const offset = self.byteOffsetToCursor(); 89 - assert(offset <= buf.len); // provided buf was too small 90 - 91 - if (offset <= self.buf.items.len) { 92 - @memcpy(buf[0..offset], self.buf.items[0..offset]); 93 - } else { 94 - @memcpy(buf[0..self.buf.items.len], self.buf.items); 95 - const second_half = self.buf.secondHalf(); 96 - const copy_len = offset - self.buf.items.len; 97 - @memcpy(buf[self.buf.items.len .. self.buf.items.len + copy_len], second_half[0..copy_len]); 98 - } 99 - return buf[0..offset]; 86 + assert(buf.len >= self.buf.cursor); 87 + @memcpy(buf[0..self.buf.cursor], self.buf.firstHalf()); 88 + return buf[0..self.buf.cursor]; 100 89 } 101 90 102 91 /// calculates the display width from the draw_offset to the cursor 103 92 fn widthToCursor(self: *TextInput, win: Window) usize { 104 93 var width: usize = 0; 105 - var first_iter = self.unicode.graphemeIterator(self.buf.items); 94 + const first_half = self.buf.firstHalf(); 95 + var first_iter = self.unicode.graphemeIterator(first_half); 106 96 var i: usize = 0; 107 97 while (first_iter.next()) |grapheme| { 108 98 defer i += 1; ··· 110 100 continue; 111 101 } 112 102 if (i == self.cursor_idx) return width; 113 - const g = grapheme.bytes(self.buf.items); 114 - width += win.gwidth(g); 115 - } 116 - const second_half = self.buf.secondHalf(); 117 - var second_iter = self.unicode.graphemeIterator(second_half); 118 - while (second_iter.next()) |grapheme| { 119 - defer i += 1; 120 - if (i < self.draw_offset) { 121 - continue; 122 - } 123 - if (i == self.cursor_idx) return width; 124 - const g = grapheme.bytes(second_half); 103 + const g = grapheme.bytes(first_half); 125 104 width += win.gwidth(g); 126 105 } 127 106 return width; 128 107 } 129 108 109 + fn cursorLeft(self: *TextInput) void { 110 + if (self.cursor_idx == 0) return; 111 + // We need to find the size of the last grapheme in the first half 112 + var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 113 + var len: usize = 0; 114 + while (iter.next()) |grapheme| { 115 + len = grapheme.len; 116 + } 117 + self.buf.moveGapLeft(len); 118 + self.cursor_idx -= 1; 119 + } 120 + 121 + fn cursorRight(self: *TextInput) void { 122 + if (self.cursor_idx >= self.grapheme_count) return; 123 + var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 124 + const grapheme = iter.next() orelse return; 125 + self.buf.moveGapRight(grapheme.len); 126 + self.cursor_idx += 1; 127 + } 128 + 130 129 pub fn draw(self: *TextInput, win: Window) void { 131 130 if (self.cursor_idx < self.draw_offset) self.draw_offset = self.cursor_idx; 132 131 if (win.width == 0) return; ··· 143 142 144 143 // assumption!! the gap is never within a grapheme 145 144 // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. 146 - var first_iter = self.unicode.graphemeIterator(self.buf.items); 145 + const first_half = self.buf.firstHalf(); 146 + var first_iter = self.unicode.graphemeIterator(first_half); 147 147 var col: usize = 0; 148 148 var i: usize = 0; 149 149 while (first_iter.next()) |grapheme| { ··· 151 151 i += 1; 152 152 continue; 153 153 } 154 - const g = grapheme.bytes(self.buf.items); 154 + const g = grapheme.bytes(first_half); 155 155 const w = win.gwidth(g); 156 156 if (col + w >= win.width) { 157 157 win.writeCell(win.width - 1, 0, .{ .char = ellipsis }); ··· 220 220 } 221 221 222 222 // returns the number of bytes before the cursor 223 - // (since GapBuffers are strictly speaking not contiguous, this is a number in 0..realLength() 224 - // which would need to be fed to realIndex() to get an actual offset into self.buf.items.ptr) 225 223 pub fn byteOffsetToCursor(self: TextInput) usize { 226 - // assumption! the gap is never in the middle of a grapheme 227 - // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. 228 - var iter = self.unicode.graphemeIterator(self.buf.items); 229 - var offset: usize = 0; 230 - var i: usize = 0; 231 - while (iter.next()) |grapheme| { 232 - if (i == self.cursor_idx) break; 233 - offset += grapheme.len; 234 - i += 1; 235 - } else { 236 - var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 237 - while (second_iter.next()) |grapheme| { 238 - if (i == self.cursor_idx) break; 239 - offset += grapheme.len; 240 - i += 1; 241 - } 242 - } 243 - return offset; 224 + return self.buf.cursor; 244 225 } 245 226 246 - fn deleteToEnd(self: *TextInput) !void { 247 - const offset = self.byteOffsetToCursor(); 248 - try self.buf.replaceRangeAfter(offset, self.buf.realLength() - offset, &.{}); 227 + fn deleteToEnd(self: *TextInput) void { 228 + self.buf.growGapRight(self.buf.secondHalf().len); 249 229 self.grapheme_count = self.cursor_idx; 250 230 } 251 231 252 - fn deleteToStart(self: *TextInput) !void { 253 - const offset = self.byteOffsetToCursor(); 254 - try self.buf.replaceRangeBefore(0, offset, &.{}); 232 + fn deleteToStart(self: *TextInput) void { 233 + self.buf.growGapLeft(self.buf.cursor); 255 234 self.grapheme_count -= self.cursor_idx; 256 235 self.cursor_idx = 0; 257 236 } 258 237 259 - fn deleteBeforeCursor(self: *TextInput) !void { 260 - // assumption! the gap is never in the middle of a grapheme 261 - // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. 262 - var iter = self.unicode.graphemeIterator(self.buf.items); 263 - var offset: usize = 0; 264 - var i: usize = 1; 238 + fn deleteBeforeCursor(self: *TextInput) void { 239 + if (self.cursor_idx == 0) return; 240 + // We need to find the size of the last grapheme in the first half 241 + var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 242 + var len: usize = 0; 265 243 while (iter.next()) |grapheme| { 266 - if (i == self.cursor_idx) { 267 - try self.buf.replaceRangeBefore(offset, grapheme.len, &.{}); 268 - self.cursor_idx -= 1; 269 - self.grapheme_count -= 1; 270 - return; 271 - } 272 - offset += grapheme.len; 273 - i += 1; 274 - } else { 275 - var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 276 - while (second_iter.next()) |grapheme| { 277 - if (i == self.cursor_idx) { 278 - try self.buf.replaceRangeBefore(offset, grapheme.len, &.{}); 279 - self.cursor_idx -= 1; 280 - self.grapheme_count -= 1; 281 - return; 282 - } 283 - offset += grapheme.len; 284 - i += 1; 285 - } 244 + len = grapheme.len; 286 245 } 246 + self.buf.growGapLeft(len); 247 + self.cursor_idx -= 1; 248 + self.grapheme_count -= 1; 287 249 } 288 250 289 - fn deleteAtCursor(self: *TextInput) !void { 290 - // assumption! the gap is never in the middle of a grapheme 291 - // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. 292 - var iter = self.unicode.graphemeIterator(self.buf.items); 293 - var offset: usize = 0; 294 - var i: usize = 1; 295 - while (iter.next()) |grapheme| { 296 - if (i == self.cursor_idx + 1) { 297 - try self.buf.replaceRangeAfter(offset, grapheme.len, &.{}); 298 - self.grapheme_count -= 1; 299 - return; 300 - } 301 - offset += grapheme.len; 302 - i += 1; 303 - } else { 304 - var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 305 - while (second_iter.next()) |grapheme| { 306 - if (i == self.cursor_idx + 1) { 307 - try self.buf.replaceRangeAfter(offset, grapheme.len, &.{}); 308 - self.grapheme_count -= 1; 309 - return; 310 - } 311 - offset += grapheme.len; 312 - i += 1; 313 - } 314 - } 251 + fn deleteAfterCursor(self: *TextInput) void { 252 + if (self.cursor_idx == self.grapheme_count) return; 253 + var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 254 + const grapheme = iter.next() orelse return; 255 + self.buf.growGapRight(grapheme.len); 256 + self.grapheme_count -= 1; 315 257 } 316 258 317 259 test "assertion" { ··· 337 279 var input = init(alloc, &unicode); 338 280 defer input.deinit(); 339 281 try input.insertSliceAtCursor("hello, world"); 340 - input.cursor_idx = 2; 282 + input.cursorLeft(); 283 + input.cursorLeft(); 284 + input.cursorLeft(); 341 285 var buf: [32]u8 = undefined; 342 - try std.testing.expectEqualStrings("he", input.sliceToCursor(&buf)); 343 - input.buf.moveGap(3); 344 - input.cursor_idx = 5; 345 - try std.testing.expectEqualStrings("hello", input.sliceToCursor(&buf)); 286 + try std.testing.expectEqualStrings("hello, wo", input.sliceToCursor(&buf)); 287 + input.cursorRight(); 288 + try std.testing.expectEqualStrings("hello, wor", input.sliceToCursor(&buf)); 289 + } 290 + 291 + const Buffer = struct { 292 + allocator: std.mem.Allocator, 293 + buffer: []u8, 294 + cursor: usize, 295 + gap_size: usize, 296 + 297 + fn init(allocator: std.mem.Allocator) Buffer { 298 + return .{ 299 + .allocator = allocator, 300 + .buffer = &.{}, 301 + .cursor = 0, 302 + .gap_size = 0, 303 + }; 304 + } 305 + 306 + fn deinit(self: *Buffer) void { 307 + self.allocator.free(self.buffer); 308 + } 309 + 310 + fn firstHalf(self: Buffer) []const u8 { 311 + return self.buffer[0..self.cursor]; 312 + } 313 + 314 + fn secondHalf(self: Buffer) []const u8 { 315 + return self.buffer[self.cursor + self.gap_size ..]; 316 + } 317 + 318 + fn grow(self: *Buffer, n: usize) std.mem.Allocator.Error!void { 319 + // Always grow by 512 bytes 320 + const new_size = self.buffer.len + n + 512; 321 + // Allocate the new memory 322 + const new_memory = try self.allocator.alloc(u8, new_size); 323 + // Copy the first half 324 + @memcpy(new_memory[0..self.cursor], self.firstHalf()); 325 + // Copy the second half 326 + const second_half = self.secondHalf(); 327 + @memcpy(new_memory[new_size - second_half.len ..], second_half); 328 + self.allocator.free(self.buffer); 329 + self.buffer = new_memory; 330 + self.gap_size = new_size - second_half.len - self.cursor; 331 + } 332 + 333 + fn insertSliceAtCursor(self: *Buffer, slice: []const u8) std.mem.Allocator.Error!void { 334 + if (slice.len == 0) return; 335 + if (self.gap_size <= slice.len) try self.grow(slice.len); 336 + @memcpy(self.buffer[self.cursor .. self.cursor + slice.len], slice); 337 + self.cursor += slice.len; 338 + self.gap_size -= slice.len; 339 + } 340 + 341 + /// Move the gap n bytes to the left 342 + fn moveGapLeft(self: *Buffer, n: usize) void { 343 + const new_idx = self.cursor -| n; 344 + const dst = self.buffer[new_idx + self.gap_size ..]; 345 + const src = self.buffer[new_idx..self.cursor]; 346 + std.mem.copyForwards(u8, dst, src); 347 + self.cursor = new_idx; 348 + } 349 + 350 + fn moveGapRight(self: *Buffer, n: usize) void { 351 + const new_idx = self.cursor + n; 352 + const dst = self.buffer[self.cursor..]; 353 + const src = self.buffer[self.cursor + self.gap_size .. new_idx + self.gap_size]; 354 + std.mem.copyForwards(u8, dst, src); 355 + self.cursor = new_idx; 356 + } 357 + 358 + /// grow the gap by moving the cursor n bytes to the left 359 + fn growGapLeft(self: *Buffer, n: usize) void { 360 + // gap grows by the delta 361 + self.gap_size += n; 362 + self.cursor -|= n; 363 + } 364 + 365 + /// grow the gap by removing n bytes after the cursor 366 + fn growGapRight(self: *Buffer, n: usize) void { 367 + self.gap_size = @min(self.gap_size + n, self.buffer.len - self.cursor); 368 + } 369 + 370 + fn clearAndFree(self: *Buffer) void { 371 + self.cursor = 0; 372 + self.allocator.free(self.buffer); 373 + self.buffer = &.{}; 374 + self.gap_size = 0; 375 + } 376 + 377 + fn clearRetainingCapacity(self: *Buffer) void { 378 + self.cursor = 0; 379 + self.gap_size = self.buffer.len; 380 + } 381 + 382 + fn toOwnedSlice(self: *Buffer) std.mem.Allocator.Error![]const u8 { 383 + const first_half = self.firstHalf(); 384 + const second_half = self.secondHalf(); 385 + const buf = try self.allocator.alloc(u8, first_half.len + second_half.len); 386 + @memcpy(buf[0..first_half.len], first_half); 387 + @memcpy(buf[first_half.len..], second_half); 388 + self.clearAndFree(); 389 + } 390 + }; 391 + 392 + test "TextInput.zig: Buffer" { 393 + var gap_buf = Buffer.init(std.testing.allocator); 394 + defer gap_buf.deinit(); 395 + 396 + try gap_buf.insertSliceAtCursor("abc"); 397 + try std.testing.expectEqualStrings("abc", gap_buf.firstHalf()); 398 + try std.testing.expectEqualStrings("", gap_buf.secondHalf()); 399 + 400 + gap_buf.moveGapLeft(1); 401 + try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 402 + try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 403 + 404 + try gap_buf.insertSliceAtCursor(" "); 405 + try std.testing.expectEqualStrings("ab ", gap_buf.firstHalf()); 406 + try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 407 + 408 + gap_buf.growGapLeft(1); 409 + try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 410 + try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 411 + try std.testing.expectEqual(2, gap_buf.cursor); 412 + 413 + gap_buf.growGapRight(1); 414 + try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 415 + try std.testing.expectEqualStrings("", gap_buf.secondHalf()); 416 + try std.testing.expectEqual(2, gap_buf.cursor); 346 417 }