this repo has no description
13
fork

Configure Feed

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

widgets(text_input): impl word motion and deletes

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

+60 -28
+60 -28
src/widgets/TextInput.zig
··· 15 15 const ellipsis: Cell.Character = .{ .grapheme = "…", .width = 1 }; 16 16 17 17 // Index of our cursor 18 - cursor_idx: usize = 0, 19 - grapheme_count: usize = 0, 20 18 buf: Buffer, 21 19 22 20 /// the number of graphemes to skip when drawing. Used for horizontal scrolling ··· 45 43 switch (event) { 46 44 .key_press => |key| { 47 45 if (key.matches(Key.backspace, .{})) { 48 - if (self.cursor_idx == 0) return; 49 46 self.deleteBeforeCursor(); 50 47 } else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) { 51 48 self.deleteAfterCursor(); ··· 55 52 self.cursorRight(); 56 53 } else if (key.matches('a', .{ .ctrl = true }) or key.matches(Key.home, .{})) { 57 54 self.buf.moveGapLeft(self.buf.firstHalf().len); 58 - self.cursor_idx = 0; 59 55 } else if (key.matches('e', .{ .ctrl = true }) or key.matches(Key.end, .{})) { 60 56 self.buf.moveGapRight(self.buf.secondHalf().len); 61 - self.cursor_idx = self.grapheme_count; 62 57 } else if (key.matches('k', .{ .ctrl = true })) { 63 58 self.deleteToEnd(); 64 59 } else if (key.matches('u', .{ .ctrl = true })) { 65 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(); 66 69 } else if (key.text) |text| { 67 70 try self.insertSliceAtCursor(text); 68 71 } ··· 73 76 /// insert text at the cursor position 74 77 pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) std.mem.Allocator.Error!void { 75 78 var iter = self.unicode.graphemeIterator(data); 76 - var byte_offset_to_cursor = self.byteOffsetToCursor(); 77 79 while (iter.next()) |text| { 78 80 try self.buf.insertSliceAtCursor(text.bytes(data)); 79 - byte_offset_to_cursor += text.len; 80 - self.cursor_idx += 1; 81 - self.grapheme_count += 1; 82 81 } 83 82 } 84 83 ··· 99 98 if (i < self.draw_offset) { 100 99 continue; 101 100 } 102 - if (i == self.cursor_idx) return width; 103 101 const g = grapheme.bytes(first_half); 104 102 width += win.gwidth(g); 105 103 } ··· 107 105 } 108 106 109 107 fn cursorLeft(self: *TextInput) void { 110 - if (self.cursor_idx == 0) return; 111 108 // We need to find the size of the last grapheme in the first half 112 109 var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 113 110 var len: usize = 0; ··· 115 112 len = grapheme.len; 116 113 } 117 114 self.buf.moveGapLeft(len); 118 - self.cursor_idx -= 1; 119 115 } 120 116 121 117 fn cursorRight(self: *TextInput) void { 122 - if (self.cursor_idx >= self.grapheme_count) return; 123 118 var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 124 119 const grapheme = iter.next() orelse return; 125 120 self.buf.moveGapRight(grapheme.len); 126 - self.cursor_idx += 1; 121 + } 122 + 123 + 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; 127 131 } 128 132 129 133 pub fn draw(self: *TextInput, win: Window) void { 130 - if (self.cursor_idx < self.draw_offset) self.draw_offset = self.cursor_idx; 134 + const cursor_idx = self.graphemesBeforeCursor(); 135 + if (cursor_idx < self.draw_offset) self.draw_offset = cursor_idx; 131 136 if (win.width == 0) return; 132 137 while (true) { 133 138 const width = self.widthToCursor(win); ··· 137 142 } else break; 138 143 } 139 144 140 - self.prev_cursor_idx = self.cursor_idx; 145 + self.prev_cursor_idx = cursor_idx; 141 146 self.prev_cursor_col = 0; 142 147 143 148 // assumption!! the gap is never within a grapheme ··· 165 170 }); 166 171 col += w; 167 172 i += 1; 168 - if (i == self.cursor_idx) self.prev_cursor_col = col; 173 + if (i == cursor_idx) self.prev_cursor_col = col; 169 174 } 170 175 const second_half = self.buf.secondHalf(); 171 176 var second_iter = self.unicode.graphemeIterator(second_half); ··· 188 193 }); 189 194 col += w; 190 195 i += 1; 191 - if (i == self.cursor_idx) self.prev_cursor_col = col; 196 + if (i == cursor_idx) self.prev_cursor_col = col; 192 197 } 193 198 if (self.draw_offset > 0) { 194 199 win.writeCell(0, 0, .{ .char = ellipsis }); ··· 212 217 } 213 218 214 219 fn reset(self: *TextInput) void { 215 - self.cursor_idx = 0; 216 - self.grapheme_count = 0; 217 220 self.draw_offset = 0; 218 221 self.prev_cursor_col = 0; 219 222 self.prev_cursor_idx = 0; ··· 226 229 227 230 fn deleteToEnd(self: *TextInput) void { 228 231 self.buf.growGapRight(self.buf.secondHalf().len); 229 - self.grapheme_count = self.cursor_idx; 230 232 } 231 233 232 234 fn deleteToStart(self: *TextInput) void { 233 235 self.buf.growGapLeft(self.buf.cursor); 234 - self.grapheme_count -= self.cursor_idx; 235 - self.cursor_idx = 0; 236 236 } 237 237 238 238 fn deleteBeforeCursor(self: *TextInput) void { 239 - if (self.cursor_idx == 0) return; 240 239 // We need to find the size of the last grapheme in the first half 241 240 var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 242 241 var len: usize = 0; ··· 244 243 len = grapheme.len; 245 244 } 246 245 self.buf.growGapLeft(len); 247 - self.cursor_idx -= 1; 248 - self.grapheme_count -= 1; 249 246 } 250 247 251 248 fn deleteAfterCursor(self: *TextInput) void { 252 - if (self.cursor_idx == self.grapheme_count) return; 253 249 var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 254 250 const grapheme = iter.next() orelse return; 255 251 self.buf.growGapRight(grapheme.len); 256 - self.grapheme_count -= 1; 252 + } 253 + 254 + /// Moves the cursor backward by words. If the character before the cursor is a space, the cursor is 255 + /// positioned just after the next previous space 256 + fn moveBackwardWordwise(self: *TextInput) void { 257 + const trimmed = std.mem.trimRight(u8, self.buf.firstHalf(), " "); 258 + const idx = if (std.mem.lastIndexOfScalar(u8, trimmed, ' ')) |last| 259 + last + 1 260 + else 261 + 0; 262 + self.buf.moveGapLeft(self.buf.cursor - idx); 263 + } 264 + 265 + fn moveForwardWordwise(self: *TextInput) void { 266 + const second_half = self.buf.secondHalf(); 267 + var i: usize = 0; 268 + while (i < second_half.len and second_half[i] == ' ') : (i += 1) {} 269 + const idx = std.mem.indexOfScalarPos(u8, second_half, i, ' ') orelse second_half.len; 270 + self.buf.moveGapRight(idx); 271 + } 272 + 273 + fn deleteWordBefore(self: *TextInput) void { 274 + // Store current cursor position. Move one word backward. Delete after the cursor the bytes we 275 + // moved 276 + const pre = self.buf.cursor; 277 + self.moveBackwardWordwise(); 278 + self.buf.growGapRight(pre - self.buf.cursor); 279 + } 280 + 281 + fn deleteWordAfter(self: *TextInput) void { 282 + // Store current cursor position. Move one word backward. Delete after the cursor the bytes we 283 + // moved 284 + const second_half = self.buf.secondHalf(); 285 + var i: usize = 0; 286 + while (i < second_half.len and second_half[i] == ' ') : (i += 1) {} 287 + const idx = std.mem.indexOfScalarPos(u8, second_half, i, ' ') orelse second_half.len; 288 + self.buf.growGapRight(idx); 257 289 } 258 290 259 291 test "assertion" {