this repo has no description
13
fork

Configure Feed

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

at 990fe29b38e57de69733025ab580b8917c2d092f 469 lines 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: 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 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) u16 { 92 var width: u16 = 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) u16 { 124 const first_half = self.buf.firstHalf(); 125 var first_iter = self.unicode.graphemeIterator(first_half); 126 var i: u16 = 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: u16 = 0; 157 var i: u16 = 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 = @intCast(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 = @intCast(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}