this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

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