a modern tui library written in zig
at v0.5.1 16 kB view raw
1const std = @import("std"); 2const assert = std.debug.assert; 3const Key = @import("../Key.zig"); 4const Cell = @import("../Cell.zig"); 5const Window = @import("../Window.zig"); 6const Unicode = @import("../Unicode.zig"); 7 8const TextInput = @This(); 9 10/// The events that this widget handles 11const Event = union(enum) { 12 key_press: Key, 13}; 14 15const ellipsis: Cell.Character = .{ .grapheme = "", .width = 1 }; 16 17// Index of our cursor 18buf: Buffer, 19 20/// the number of graphemes to skip when drawing. Used for horizontal scrolling 21draw_offset: usize = 0, 22/// the column we placed the cursor the last time we drew 23prev_cursor_col: usize = 0, 24/// the grapheme index of the cursor the last time we drew 25prev_cursor_idx: usize = 0, 26/// approximate distance from an edge before we scroll 27scroll_offset: usize = 4, 28 29unicode: *const Unicode, 30 31pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextInput { 32 return TextInput{ 33 .buf = Buffer.init(alloc), 34 .unicode = unicode, 35 }; 36} 37 38pub fn deinit(self: *TextInput) void { 39 self.buf.deinit(); 40} 41 42pub fn update(self: *TextInput, event: Event) !void { 43 switch (event) { 44 .key_press => |key| { 45 if (key.matches(Key.backspace, .{})) { 46 self.deleteBeforeCursor(); 47 } else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) { 48 self.deleteAfterCursor(); 49 } else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) { 50 self.cursorLeft(); 51 } else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) { 52 self.cursorRight(); 53 } else if (key.matches('a', .{ .ctrl = true }) or key.matches(Key.home, .{})) { 54 self.buf.moveGapLeft(self.buf.firstHalf().len); 55 } else if (key.matches('e', .{ .ctrl = true }) or key.matches(Key.end, .{})) { 56 self.buf.moveGapRight(self.buf.secondHalf().len); 57 } else if (key.matches('k', .{ .ctrl = true })) { 58 self.deleteToEnd(); 59 } else if (key.matches('u', .{ .ctrl = true })) { 60 self.deleteToStart(); 61 } else if (key.matches('b', .{ .alt = true }) or key.matches(Key.left, .{ .alt = true })) { 62 self.moveBackwardWordwise(); 63 } else if (key.matches('f', .{ .alt = true }) or key.matches(Key.right, .{ .alt = true })) { 64 self.moveForwardWordwise(); 65 } else if (key.matches('w', .{ .ctrl = true }) or key.matches(Key.backspace, .{ .alt = true })) { 66 self.deleteWordBefore(); 67 } else if (key.matches('d', .{ .alt = true })) { 68 self.deleteWordAfter(); 69 } else if (key.text) |text| { 70 try self.insertSliceAtCursor(text); 71 } 72 }, 73 } 74} 75 76/// insert text at the cursor position 77pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) std.mem.Allocator.Error!void { 78 var iter = self.unicode.graphemeIterator(data); 79 while (iter.next()) |text| { 80 try self.buf.insertSliceAtCursor(text.bytes(data)); 81 } 82} 83 84pub fn sliceToCursor(self: *TextInput, buf: []u8) []const u8 { 85 assert(buf.len >= self.buf.cursor); 86 @memcpy(buf[0..self.buf.cursor], self.buf.firstHalf()); 87 return buf[0..self.buf.cursor]; 88} 89 90/// calculates the display width from the draw_offset to the cursor 91pub fn widthToCursor(self: *TextInput, win: Window) usize { 92 var width: usize = 0; 93 const first_half = self.buf.firstHalf(); 94 var first_iter = self.unicode.graphemeIterator(first_half); 95 var i: usize = 0; 96 while (first_iter.next()) |grapheme| { 97 defer i += 1; 98 if (i < self.draw_offset) { 99 continue; 100 } 101 const g = grapheme.bytes(first_half); 102 width += win.gwidth(g); 103 } 104 return width; 105} 106 107pub fn cursorLeft(self: *TextInput) void { 108 // We need to find the size of the last grapheme in the first half 109 var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 110 var len: usize = 0; 111 while (iter.next()) |grapheme| { 112 len = grapheme.len; 113 } 114 self.buf.moveGapLeft(len); 115} 116 117pub fn cursorRight(self: *TextInput) void { 118 var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 119 const grapheme = iter.next() orelse return; 120 self.buf.moveGapRight(grapheme.len); 121} 122 123pub fn graphemesBeforeCursor(self: *const TextInput) usize { 124 const first_half = self.buf.firstHalf(); 125 var first_iter = self.unicode.graphemeIterator(first_half); 126 var i: usize = 0; 127 while (first_iter.next()) |_| { 128 i += 1; 129 } 130 return i; 131} 132 133pub fn draw(self: *TextInput, win: Window) void { 134 self.drawWithStyle(win, .{}); 135} 136 137pub fn drawWithStyle(self: *TextInput, win: Window, style: Cell.Style) void { 138 const cursor_idx = self.graphemesBeforeCursor(); 139 if (cursor_idx < self.draw_offset) self.draw_offset = cursor_idx; 140 if (win.width == 0) return; 141 while (true) { 142 const width = self.widthToCursor(win); 143 if (width >= win.width) { 144 self.draw_offset +|= width - win.width + 1; 145 continue; 146 } else break; 147 } 148 149 self.prev_cursor_idx = cursor_idx; 150 self.prev_cursor_col = 0; 151 152 // assumption!! the gap is never within a grapheme 153 // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. 154 const first_half = self.buf.firstHalf(); 155 var first_iter = self.unicode.graphemeIterator(first_half); 156 var col: usize = 0; 157 var i: usize = 0; 158 while (first_iter.next()) |grapheme| { 159 if (i < self.draw_offset) { 160 i += 1; 161 continue; 162 } 163 const g = grapheme.bytes(first_half); 164 const w = win.gwidth(g); 165 if (col + w >= win.width) { 166 win.writeCell(win.width - 1, 0, .{ 167 .char = ellipsis, 168 .style = style, 169 }); 170 break; 171 } 172 win.writeCell(col, 0, .{ 173 .char = .{ 174 .grapheme = g, 175 .width = w, 176 }, 177 .style = style, 178 }); 179 col += w; 180 i += 1; 181 if (i == cursor_idx) self.prev_cursor_col = col; 182 } 183 const second_half = self.buf.secondHalf(); 184 var second_iter = self.unicode.graphemeIterator(second_half); 185 while (second_iter.next()) |grapheme| { 186 if (i < self.draw_offset) { 187 i += 1; 188 continue; 189 } 190 const g = grapheme.bytes(second_half); 191 const w = win.gwidth(g); 192 if (col + w > win.width) { 193 win.writeCell(win.width - 1, 0, .{ 194 .char = ellipsis, 195 .style = style, 196 }); 197 break; 198 } 199 win.writeCell(col, 0, .{ 200 .char = .{ 201 .grapheme = g, 202 .width = w, 203 }, 204 .style = style, 205 }); 206 col += w; 207 i += 1; 208 if (i == cursor_idx) self.prev_cursor_col = col; 209 } 210 if (self.draw_offset > 0) { 211 win.writeCell(0, 0, .{ 212 .char = ellipsis, 213 .style = style, 214 }); 215 } 216 win.showCursor(self.prev_cursor_col, 0); 217} 218 219pub fn clearAndFree(self: *TextInput) void { 220 self.buf.clearAndFree(); 221 self.reset(); 222} 223 224pub fn clearRetainingCapacity(self: *TextInput) void { 225 self.buf.clearRetainingCapacity(); 226 self.reset(); 227} 228 229pub fn toOwnedSlice(self: *TextInput) ![]const u8 { 230 defer self.reset(); 231 return self.buf.toOwnedSlice(); 232} 233 234pub fn reset(self: *TextInput) void { 235 self.draw_offset = 0; 236 self.prev_cursor_col = 0; 237 self.prev_cursor_idx = 0; 238} 239 240// returns the number of bytes before the cursor 241pub fn byteOffsetToCursor(self: TextInput) usize { 242 return self.buf.cursor; 243} 244 245pub fn deleteToEnd(self: *TextInput) void { 246 self.buf.growGapRight(self.buf.secondHalf().len); 247} 248 249pub fn deleteToStart(self: *TextInput) void { 250 self.buf.growGapLeft(self.buf.cursor); 251} 252 253pub fn deleteBeforeCursor(self: *TextInput) void { 254 // We need to find the size of the last grapheme in the first half 255 var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 256 var len: usize = 0; 257 while (iter.next()) |grapheme| { 258 len = grapheme.len; 259 } 260 self.buf.growGapLeft(len); 261} 262 263pub fn deleteAfterCursor(self: *TextInput) void { 264 var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 265 const grapheme = iter.next() orelse return; 266 self.buf.growGapRight(grapheme.len); 267} 268 269/// Moves the cursor backward by words. If the character before the cursor is a space, the cursor is 270/// positioned just after the next previous space 271pub fn moveBackwardWordwise(self: *TextInput) void { 272 const trimmed = std.mem.trimRight(u8, self.buf.firstHalf(), " "); 273 const idx = if (std.mem.lastIndexOfScalar(u8, trimmed, ' ')) |last| 274 last + 1 275 else 276 0; 277 self.buf.moveGapLeft(self.buf.cursor - idx); 278} 279 280pub fn moveForwardWordwise(self: *TextInput) void { 281 const second_half = self.buf.secondHalf(); 282 var i: usize = 0; 283 while (i < second_half.len and second_half[i] == ' ') : (i += 1) {} 284 const idx = std.mem.indexOfScalarPos(u8, second_half, i, ' ') orelse second_half.len; 285 self.buf.moveGapRight(idx); 286} 287 288pub fn deleteWordBefore(self: *TextInput) void { 289 // Store current cursor position. Move one word backward. Delete after the cursor the bytes we 290 // moved 291 const pre = self.buf.cursor; 292 self.moveBackwardWordwise(); 293 self.buf.growGapRight(pre - self.buf.cursor); 294} 295 296pub fn deleteWordAfter(self: *TextInput) void { 297 // Store current cursor position. Move one word backward. Delete after the cursor the bytes we 298 // moved 299 const second_half = self.buf.secondHalf(); 300 var i: usize = 0; 301 while (i < second_half.len and second_half[i] == ' ') : (i += 1) {} 302 const idx = std.mem.indexOfScalarPos(u8, second_half, i, ' ') orelse second_half.len; 303 self.buf.growGapRight(idx); 304} 305 306test "assertion" { 307 const alloc = std.testing.allocator_instance.allocator(); 308 const unicode = try Unicode.init(alloc); 309 defer unicode.deinit(); 310 const astronaut = "👩‍🚀"; 311 const astronaut_emoji: Key = .{ 312 .text = astronaut, 313 .codepoint = try std.unicode.utf8Decode(astronaut[0..4]), 314 }; 315 var input = TextInput.init(std.testing.allocator, &unicode); 316 defer input.deinit(); 317 for (0..6) |_| { 318 try input.update(.{ .key_press = astronaut_emoji }); 319 } 320} 321 322test "sliceToCursor" { 323 const alloc = std.testing.allocator_instance.allocator(); 324 const unicode = try Unicode.init(alloc); 325 defer unicode.deinit(); 326 var input = init(alloc, &unicode); 327 defer input.deinit(); 328 try input.insertSliceAtCursor("hello, world"); 329 input.cursorLeft(); 330 input.cursorLeft(); 331 input.cursorLeft(); 332 var buf: [32]u8 = undefined; 333 try std.testing.expectEqualStrings("hello, wo", input.sliceToCursor(&buf)); 334 input.cursorRight(); 335 try std.testing.expectEqualStrings("hello, wor", input.sliceToCursor(&buf)); 336} 337 338pub const Buffer = struct { 339 allocator: std.mem.Allocator, 340 buffer: []u8, 341 cursor: usize, 342 gap_size: usize, 343 344 pub fn init(allocator: std.mem.Allocator) Buffer { 345 return .{ 346 .allocator = allocator, 347 .buffer = &.{}, 348 .cursor = 0, 349 .gap_size = 0, 350 }; 351 } 352 353 pub fn deinit(self: *Buffer) void { 354 self.allocator.free(self.buffer); 355 } 356 357 pub fn firstHalf(self: Buffer) []const u8 { 358 return self.buffer[0..self.cursor]; 359 } 360 361 pub fn secondHalf(self: Buffer) []const u8 { 362 return self.buffer[self.cursor + self.gap_size ..]; 363 } 364 365 pub fn grow(self: *Buffer, n: usize) std.mem.Allocator.Error!void { 366 // Always grow by 512 bytes 367 const new_size = self.buffer.len + n + 512; 368 // Allocate the new memory 369 const new_memory = try self.allocator.alloc(u8, new_size); 370 // Copy the first half 371 @memcpy(new_memory[0..self.cursor], self.firstHalf()); 372 // Copy the second half 373 const second_half = self.secondHalf(); 374 @memcpy(new_memory[new_size - second_half.len ..], second_half); 375 self.allocator.free(self.buffer); 376 self.buffer = new_memory; 377 self.gap_size = new_size - second_half.len - self.cursor; 378 } 379 380 pub fn insertSliceAtCursor(self: *Buffer, slice: []const u8) std.mem.Allocator.Error!void { 381 if (slice.len == 0) return; 382 if (self.gap_size <= slice.len) try self.grow(slice.len); 383 @memcpy(self.buffer[self.cursor .. self.cursor + slice.len], slice); 384 self.cursor += slice.len; 385 self.gap_size -= slice.len; 386 } 387 388 /// Move the gap n bytes to the left 389 pub fn moveGapLeft(self: *Buffer, n: usize) void { 390 const new_idx = self.cursor -| n; 391 const dst = self.buffer[new_idx + self.gap_size ..]; 392 const src = self.buffer[new_idx..self.cursor]; 393 std.mem.copyForwards(u8, dst, src); 394 self.cursor = new_idx; 395 } 396 397 pub fn moveGapRight(self: *Buffer, n: usize) void { 398 const new_idx = self.cursor + n; 399 const dst = self.buffer[self.cursor..]; 400 const src = self.buffer[self.cursor + self.gap_size .. new_idx + self.gap_size]; 401 std.mem.copyForwards(u8, dst, src); 402 self.cursor = new_idx; 403 } 404 405 /// grow the gap by moving the cursor n bytes to the left 406 pub fn growGapLeft(self: *Buffer, n: usize) void { 407 // gap grows by the delta 408 self.gap_size += n; 409 self.cursor -|= n; 410 } 411 412 /// grow the gap by removing n bytes after the cursor 413 pub fn growGapRight(self: *Buffer, n: usize) void { 414 self.gap_size = @min(self.gap_size + n, self.buffer.len - self.cursor); 415 } 416 417 pub fn clearAndFree(self: *Buffer) void { 418 self.cursor = 0; 419 self.allocator.free(self.buffer); 420 self.buffer = &.{}; 421 self.gap_size = 0; 422 } 423 424 pub fn clearRetainingCapacity(self: *Buffer) void { 425 self.cursor = 0; 426 self.gap_size = self.buffer.len; 427 } 428 429 pub fn toOwnedSlice(self: *Buffer) std.mem.Allocator.Error![]const u8 { 430 const first_half = self.firstHalf(); 431 const second_half = self.secondHalf(); 432 const buf = try self.allocator.alloc(u8, first_half.len + second_half.len); 433 @memcpy(buf[0..first_half.len], first_half); 434 @memcpy(buf[first_half.len..], second_half); 435 self.clearAndFree(); 436 return buf; 437 } 438 439 pub fn realLength(self: *const Buffer) usize { 440 return self.firstHalf().len + self.secondHalf().len; 441 } 442}; 443 444test "TextInput.zig: Buffer" { 445 var gap_buf = Buffer.init(std.testing.allocator); 446 defer gap_buf.deinit(); 447 448 try gap_buf.insertSliceAtCursor("abc"); 449 try std.testing.expectEqualStrings("abc", gap_buf.firstHalf()); 450 try std.testing.expectEqualStrings("", gap_buf.secondHalf()); 451 452 gap_buf.moveGapLeft(1); 453 try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 454 try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 455 456 try gap_buf.insertSliceAtCursor(" "); 457 try std.testing.expectEqualStrings("ab ", gap_buf.firstHalf()); 458 try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 459 460 gap_buf.growGapLeft(1); 461 try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 462 try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 463 try std.testing.expectEqual(2, gap_buf.cursor); 464 465 gap_buf.growGapRight(1); 466 try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 467 try std.testing.expectEqualStrings("", gap_buf.secondHalf()); 468 try std.testing.expectEqual(2, gap_buf.cursor); 469}