this repo has no description
13
fork

Configure Feed

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

at 3943a6f42f7f4c73ab9493dfb0a4e123be15d302 598 lines 21 kB view raw
1const std = @import("std"); 2const vaxis = @import("../main.zig"); 3 4const vxfw = @import("vxfw.zig"); 5 6const assert = std.debug.assert; 7 8const Allocator = std.mem.Allocator; 9const Key = vaxis.Key; 10const Cell = vaxis.Cell; 11const Window = vaxis.Window; 12const unicode = vaxis.unicode; 13 14const TextField = @This(); 15 16const ellipsis: Cell.Character = .{ .grapheme = "", .width = 1 }; 17 18// Index of our cursor 19buf: Buffer, 20 21/// Style to draw the TextField with 22style: vaxis.Style = .{}, 23 24/// the number of graphemes to skip when drawing. Used for horizontal scrolling 25draw_offset: u16 = 0, 26/// the column we placed the cursor the last time we drew 27prev_cursor_col: u16 = 0, 28/// the grapheme index of the cursor the last time we drew 29prev_cursor_idx: u16 = 0, 30/// approximate distance from an edge before we scroll 31scroll_offset: u4 = 4, 32/// Previous width we drew at 33prev_width: u16 = 0, 34 35previous_val: []const u8 = "", 36 37userdata: ?*anyopaque = null, 38onChange: ?*const fn (?*anyopaque, *vxfw.EventContext, []const u8) anyerror!void = null, 39onSubmit: ?*const fn (?*anyopaque, *vxfw.EventContext, []const u8) anyerror!void = null, 40 41pub fn init(alloc: std.mem.Allocator) TextField { 42 return TextField{ 43 .buf = Buffer.init(alloc), 44 }; 45} 46 47pub fn deinit(self: *TextField) void { 48 self.buf.allocator.free(self.previous_val); 49 self.buf.deinit(); 50} 51 52pub fn widget(self: *TextField) vxfw.Widget { 53 return .{ 54 .userdata = self, 55 .eventHandler = typeErasedEventHandler, 56 .drawFn = typeErasedDrawFn, 57 }; 58} 59 60fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 61 const self: *TextField = @ptrCast(@alignCast(ptr)); 62 return self.handleEvent(ctx, event); 63} 64 65pub fn handleEvent(self: *TextField, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 66 switch (event) { 67 .focus_out, .focus_in => ctx.redraw = true, 68 .key_press => |key| { 69 if (key.matches(Key.backspace, .{})) { 70 self.deleteBeforeCursor(); 71 return self.checkChanged(ctx); 72 } else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) { 73 self.deleteAfterCursor(); 74 return self.checkChanged(ctx); 75 } else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) { 76 self.cursorLeft(); 77 return ctx.consumeAndRedraw(); 78 } else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) { 79 self.cursorRight(); 80 return ctx.consumeAndRedraw(); 81 } else if (key.matches('a', .{ .ctrl = true }) or key.matches(Key.home, .{})) { 82 self.buf.moveGapLeft(self.buf.firstHalf().len); 83 return ctx.consumeAndRedraw(); 84 } else if (key.matches('e', .{ .ctrl = true }) or key.matches(Key.end, .{})) { 85 self.buf.moveGapRight(self.buf.secondHalf().len); 86 return ctx.consumeAndRedraw(); 87 } else if (key.matches('k', .{ .ctrl = true })) { 88 self.deleteToEnd(); 89 return self.checkChanged(ctx); 90 } else if (key.matches('u', .{ .ctrl = true })) { 91 self.deleteToStart(); 92 return self.checkChanged(ctx); 93 } else if (key.matches('b', .{ .alt = true }) or key.matches(Key.left, .{ .alt = true })) { 94 self.moveBackwardWordwise(); 95 return ctx.consumeAndRedraw(); 96 } else if (key.matches('f', .{ .alt = true }) or key.matches(Key.right, .{ .alt = true })) { 97 self.moveForwardWordwise(); 98 return ctx.consumeAndRedraw(); 99 } else if (key.matches('w', .{ .ctrl = true }) or key.matches(Key.backspace, .{ .alt = true })) { 100 self.deleteWordBefore(); 101 return self.checkChanged(ctx); 102 } else if (key.matches('d', .{ .alt = true })) { 103 self.deleteWordAfter(); 104 return self.checkChanged(ctx); 105 } else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) { 106 if (self.onSubmit) |onSubmit| { 107 const value = try self.toOwnedSlice(); 108 // Get a ref to the allocator in case onSubmit deinits the TextField 109 const allocator = self.buf.allocator; 110 defer allocator.free(value); 111 try onSubmit(self.userdata, ctx, value); 112 return ctx.consumeAndRedraw(); 113 } 114 } else if (key.text) |text| { 115 try self.insertSliceAtCursor(text); 116 return self.checkChanged(ctx); 117 } 118 }, 119 else => {}, 120 } 121} 122 123fn checkChanged(self: *TextField, ctx: *vxfw.EventContext) anyerror!void { 124 ctx.consumeAndRedraw(); 125 const onChange = self.onChange orelse return; 126 const new = try self.buf.dupe(); 127 defer { 128 self.buf.allocator.free(self.previous_val); 129 self.previous_val = new; 130 } 131 if (std.mem.eql(u8, new, self.previous_val)) return; 132 try onChange(self.userdata, ctx, new); 133} 134 135/// insert text at the cursor position 136pub fn insertSliceAtCursor(self: *TextField, data: []const u8) std.mem.Allocator.Error!void { 137 var iter = unicode.graphemeIterator(data); 138 while (iter.next()) |text| { 139 try self.buf.insertSliceAtCursor(text.bytes(data)); 140 } 141} 142 143pub fn sliceToCursor(self: *TextField, buf: []u8) []const u8 { 144 assert(buf.len >= self.buf.cursor); 145 @memcpy(buf[0..self.buf.cursor], self.buf.firstHalf()); 146 return buf[0..self.buf.cursor]; 147} 148 149/// calculates the display width from the draw_offset to the cursor 150pub fn widthToCursor(self: *TextField, ctx: vxfw.DrawContext) u16 { 151 var width: u16 = 0; 152 const first_half = self.buf.firstHalf(); 153 var first_iter = unicode.graphemeIterator(first_half); 154 var i: usize = 0; 155 while (first_iter.next()) |grapheme| { 156 defer i += 1; 157 if (i < self.draw_offset) { 158 continue; 159 } 160 const g = grapheme.bytes(first_half); 161 width += @intCast(ctx.stringWidth(g)); 162 } 163 return width; 164} 165 166pub fn cursorLeft(self: *TextField) void { 167 // We need to find the size of the last grapheme in the first half 168 var iter = unicode.graphemeIterator(self.buf.firstHalf()); 169 var len: usize = 0; 170 while (iter.next()) |grapheme| { 171 len = grapheme.len; 172 } 173 self.buf.moveGapLeft(len); 174} 175 176pub fn cursorRight(self: *TextField) void { 177 var iter = unicode.graphemeIterator(self.buf.secondHalf()); 178 const grapheme = iter.next() orelse return; 179 self.buf.moveGapRight(grapheme.len); 180} 181 182pub fn graphemesBeforeCursor(self: *const TextField) u16 { 183 const first_half = self.buf.firstHalf(); 184 var first_iter = unicode.graphemeIterator(first_half); 185 var i: u16 = 0; 186 while (first_iter.next()) |_| { 187 i += 1; 188 } 189 return i; 190} 191 192fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 193 const self: *TextField = @ptrCast(@alignCast(ptr)); 194 return self.draw(ctx); 195} 196 197pub fn draw(self: *TextField, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 198 std.debug.assert(ctx.max.width != null); 199 const max_width = ctx.max.width.?; 200 if (max_width != self.prev_width) { 201 self.prev_width = max_width; 202 self.draw_offset = 0; 203 self.prev_cursor_col = 0; 204 } 205 // Create a surface with max width and a minimum height of 1. 206 var surface = try vxfw.Surface.init( 207 ctx.arena, 208 self.widget(), 209 .{ .width = max_width, .height = @max(ctx.min.height, 1) }, 210 ); 211 212 const base: vaxis.Cell = .{ .style = self.style }; 213 @memset(surface.buffer, base); 214 const style = self.style; 215 const cursor_idx = self.graphemesBeforeCursor(); 216 if (cursor_idx < self.draw_offset) self.draw_offset = cursor_idx; 217 if (max_width == 0) return surface; 218 while (true) { 219 const width = self.widthToCursor(ctx); 220 if (width >= max_width) { 221 self.draw_offset +|= width - max_width + 1; 222 continue; 223 } else break; 224 } 225 226 self.prev_cursor_idx = cursor_idx; 227 self.prev_cursor_col = 0; 228 229 const first_half = self.buf.firstHalf(); 230 var first_iter = unicode.graphemeIterator(first_half); 231 var col: u16 = 0; 232 var i: u16 = 0; 233 while (first_iter.next()) |grapheme| { 234 if (i < self.draw_offset) { 235 i += 1; 236 continue; 237 } 238 const g = grapheme.bytes(first_half); 239 const w: u8 = @intCast(ctx.stringWidth(g)); 240 if (col + w >= max_width) { 241 surface.writeCell(max_width - 1, 0, .{ 242 .char = ellipsis, 243 .style = style, 244 }); 245 break; 246 } 247 surface.writeCell(@intCast(col), 0, .{ 248 .char = .{ 249 .grapheme = g, 250 .width = w, 251 }, 252 .style = style, 253 }); 254 col += w; 255 i += 1; 256 if (i == cursor_idx) self.prev_cursor_col = col; 257 } 258 const second_half = self.buf.secondHalf(); 259 var second_iter = unicode.graphemeIterator(second_half); 260 while (second_iter.next()) |grapheme| { 261 if (i < self.draw_offset) { 262 i += 1; 263 continue; 264 } 265 const g = grapheme.bytes(second_half); 266 const w: u8 = @intCast(ctx.stringWidth(g)); 267 if (col + w > max_width) { 268 surface.writeCell(max_width - 1, 0, .{ 269 .char = ellipsis, 270 .style = style, 271 }); 272 break; 273 } 274 surface.writeCell(@intCast(col), 0, .{ 275 .char = .{ 276 .grapheme = g, 277 .width = w, 278 }, 279 .style = style, 280 }); 281 col += w; 282 i += 1; 283 if (i == cursor_idx) self.prev_cursor_col = col; 284 } 285 if (self.draw_offset > 0) { 286 surface.writeCell(0, 0, .{ 287 .char = ellipsis, 288 .style = style, 289 }); 290 } 291 surface.cursor = .{ .col = @intCast(self.prev_cursor_col), .row = 0 }; 292 return surface; 293 // win.showCursor(self.prev_cursor_col, 0); 294} 295 296pub fn clearAndFree(self: *TextField) void { 297 self.buf.clearAndFree(); 298 self.reset(); 299} 300 301pub fn clearRetainingCapacity(self: *TextField) void { 302 self.buf.clearRetainingCapacity(); 303 self.reset(); 304} 305 306pub fn toOwnedSlice(self: *TextField) ![]const u8 { 307 defer self.reset(); 308 return self.buf.toOwnedSlice(); 309} 310 311pub fn reset(self: *TextField) void { 312 self.draw_offset = 0; 313 self.prev_cursor_col = 0; 314 self.prev_cursor_idx = 0; 315} 316 317// returns the number of bytes before the cursor 318pub fn byteOffsetToCursor(self: TextField) usize { 319 return self.buf.cursor; 320} 321 322pub fn deleteToEnd(self: *TextField) void { 323 self.buf.growGapRight(self.buf.secondHalf().len); 324} 325 326pub fn deleteToStart(self: *TextField) void { 327 self.buf.growGapLeft(self.buf.cursor); 328} 329 330pub fn deleteBeforeCursor(self: *TextField) void { 331 // We need to find the size of the last grapheme in the first half 332 var iter = unicode.graphemeIterator(self.buf.firstHalf()); 333 var len: usize = 0; 334 while (iter.next()) |grapheme| { 335 len = grapheme.len; 336 } 337 self.buf.growGapLeft(len); 338} 339 340pub fn deleteAfterCursor(self: *TextField) void { 341 var iter = unicode.graphemeIterator(self.buf.secondHalf()); 342 const grapheme = iter.next() orelse return; 343 self.buf.growGapRight(grapheme.len); 344} 345 346/// Moves the cursor backward by words. If the character before the cursor is a space, the cursor is 347/// positioned just after the next previous space 348pub fn moveBackwardWordwise(self: *TextField) void { 349 const trimmed = std.mem.trimRight(u8, self.buf.firstHalf(), " "); 350 const idx = if (std.mem.lastIndexOfScalar(u8, trimmed, ' ')) |last| 351 last + 1 352 else 353 0; 354 self.buf.moveGapLeft(self.buf.cursor - idx); 355} 356 357pub fn moveForwardWordwise(self: *TextField) void { 358 const second_half = self.buf.secondHalf(); 359 var i: usize = 0; 360 while (i < second_half.len and second_half[i] == ' ') : (i += 1) {} 361 const idx = std.mem.indexOfScalarPos(u8, second_half, i, ' ') orelse second_half.len; 362 self.buf.moveGapRight(idx); 363} 364 365pub fn deleteWordBefore(self: *TextField) void { 366 // Store current cursor position. Move one word backward. Delete after the cursor the bytes we 367 // moved 368 const pre = self.buf.cursor; 369 self.moveBackwardWordwise(); 370 self.buf.growGapRight(pre - self.buf.cursor); 371} 372 373pub fn deleteWordAfter(self: *TextField) void { 374 // Store current cursor position. Move one word backward. Delete after the cursor the bytes we 375 // moved 376 const second_half = self.buf.secondHalf(); 377 var i: usize = 0; 378 while (i < second_half.len and second_half[i] == ' ') : (i += 1) {} 379 const idx = std.mem.indexOfScalarPos(u8, second_half, i, ' ') orelse second_half.len; 380 self.buf.growGapRight(idx); 381} 382 383test "sliceToCursor" { 384 var input = init(std.testing.allocator); 385 defer input.deinit(); 386 try input.insertSliceAtCursor("hello, world"); 387 input.cursorLeft(); 388 input.cursorLeft(); 389 input.cursorLeft(); 390 var buf: [32]u8 = undefined; 391 try std.testing.expectEqualStrings("hello, wo", input.sliceToCursor(&buf)); 392 input.cursorRight(); 393 try std.testing.expectEqualStrings("hello, wor", input.sliceToCursor(&buf)); 394} 395 396pub const Buffer = struct { 397 allocator: std.mem.Allocator, 398 buffer: []u8, 399 cursor: usize, 400 gap_size: usize, 401 402 pub fn init(allocator: std.mem.Allocator) Buffer { 403 return .{ 404 .allocator = allocator, 405 .buffer = &.{}, 406 .cursor = 0, 407 .gap_size = 0, 408 }; 409 } 410 411 pub fn deinit(self: *Buffer) void { 412 self.allocator.free(self.buffer); 413 } 414 415 pub fn firstHalf(self: Buffer) []const u8 { 416 return self.buffer[0..self.cursor]; 417 } 418 419 pub fn secondHalf(self: Buffer) []const u8 { 420 return self.buffer[self.cursor + self.gap_size ..]; 421 } 422 423 pub fn grow(self: *Buffer, n: usize) std.mem.Allocator.Error!void { 424 // Always grow by 512 bytes 425 const new_size = self.buffer.len + n + 512; 426 // Allocate the new memory 427 const new_memory = try self.allocator.alloc(u8, new_size); 428 // Copy the first half 429 @memcpy(new_memory[0..self.cursor], self.firstHalf()); 430 // Copy the second half 431 const second_half = self.secondHalf(); 432 @memcpy(new_memory[new_size - second_half.len ..], second_half); 433 self.allocator.free(self.buffer); 434 self.buffer = new_memory; 435 self.gap_size = new_size - second_half.len - self.cursor; 436 } 437 438 pub fn insertSliceAtCursor(self: *Buffer, slice: []const u8) std.mem.Allocator.Error!void { 439 if (slice.len == 0) return; 440 if (self.gap_size <= slice.len) try self.grow(slice.len); 441 @memcpy(self.buffer[self.cursor .. self.cursor + slice.len], slice); 442 self.cursor += slice.len; 443 self.gap_size -= slice.len; 444 } 445 446 /// Move the gap n bytes to the left 447 pub fn moveGapLeft(self: *Buffer, n: usize) void { 448 const new_idx = self.cursor -| n; 449 const dst = self.buffer[new_idx + self.gap_size ..]; 450 const src = self.buffer[new_idx..self.cursor]; 451 std.mem.copyForwards(u8, dst, src); 452 self.cursor = new_idx; 453 } 454 455 pub fn moveGapRight(self: *Buffer, n: usize) void { 456 const new_idx = self.cursor + n; 457 const dst = self.buffer[self.cursor..]; 458 const src = self.buffer[self.cursor + self.gap_size .. new_idx + self.gap_size]; 459 std.mem.copyForwards(u8, dst, src); 460 self.cursor = new_idx; 461 } 462 463 /// grow the gap by moving the cursor n bytes to the left 464 pub fn growGapLeft(self: *Buffer, n: usize) void { 465 // gap grows by the delta 466 self.gap_size += n; 467 self.cursor -|= n; 468 } 469 470 /// grow the gap by removing n bytes after the cursor 471 pub fn growGapRight(self: *Buffer, n: usize) void { 472 self.gap_size = @min(self.gap_size + n, self.buffer.len - self.cursor); 473 } 474 475 pub fn clearAndFree(self: *Buffer) void { 476 self.cursor = 0; 477 self.allocator.free(self.buffer); 478 self.buffer = &.{}; 479 self.gap_size = 0; 480 } 481 482 pub fn clearRetainingCapacity(self: *Buffer) void { 483 self.cursor = 0; 484 self.gap_size = self.buffer.len; 485 } 486 487 pub fn toOwnedSlice(self: *Buffer) std.mem.Allocator.Error![]const u8 { 488 const slice = try self.dupe(); 489 self.clearAndFree(); 490 return slice; 491 } 492 493 pub fn realLength(self: *const Buffer) usize { 494 return self.firstHalf().len + self.secondHalf().len; 495 } 496 497 pub fn dupe(self: *const Buffer) std.mem.Allocator.Error![]const u8 { 498 const first_half = self.firstHalf(); 499 const second_half = self.secondHalf(); 500 const buf = try self.allocator.alloc(u8, first_half.len + second_half.len); 501 @memcpy(buf[0..first_half.len], first_half); 502 @memcpy(buf[first_half.len..], second_half); 503 return buf; 504 } 505}; 506 507test "TextField.zig: Buffer" { 508 var gap_buf = Buffer.init(std.testing.allocator); 509 defer gap_buf.deinit(); 510 511 try gap_buf.insertSliceAtCursor("abc"); 512 try std.testing.expectEqualStrings("abc", gap_buf.firstHalf()); 513 try std.testing.expectEqualStrings("", gap_buf.secondHalf()); 514 515 gap_buf.moveGapLeft(1); 516 try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 517 try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 518 519 try gap_buf.insertSliceAtCursor(" "); 520 try std.testing.expectEqualStrings("ab ", gap_buf.firstHalf()); 521 try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 522 523 gap_buf.growGapLeft(1); 524 try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 525 try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 526 try std.testing.expectEqual(2, gap_buf.cursor); 527 528 gap_buf.growGapRight(1); 529 try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 530 try std.testing.expectEqualStrings("", gap_buf.secondHalf()); 531 try std.testing.expectEqual(2, gap_buf.cursor); 532} 533 534test TextField { 535 // Boiler plate draw context init 536 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 537 defer arena.deinit(); 538 vxfw.DrawContext.init(.unicode); 539 540 // Create some object which reacts to text field changes 541 const Foo = struct { 542 allocator: std.mem.Allocator, 543 text: []const u8, 544 545 fn onChange(ptr: ?*anyopaque, ctx: *vxfw.EventContext, str: []const u8) anyerror!void { 546 const foo: *@This() = @ptrCast(@alignCast(ptr)); 547 foo.text = try foo.allocator.dupe(u8, str); 548 ctx.consumeAndRedraw(); 549 } 550 }; 551 var foo: Foo = .{ .text = "", .allocator = arena.allocator() }; 552 553 // Text field expands to the width, so it can't be null. It is always 1 line tall 554 const draw_ctx: vxfw.DrawContext = .{ 555 .arena = arena.allocator(), 556 .min = .{}, 557 .max = .{ .width = 8, .height = 1 }, 558 .cell_size = .{ .width = 10, .height = 20 }, 559 }; 560 _ = draw_ctx; 561 562 var ctx: vxfw.EventContext = .{ 563 .alloc = arena.allocator(), 564 .cmds = .empty, 565 }; 566 567 // Enough boiler plate...Create the text field 568 var text_field = TextField.init(std.testing.allocator); 569 defer text_field.deinit(); 570 text_field.onChange = Foo.onChange; 571 text_field.onSubmit = Foo.onChange; 572 text_field.userdata = &foo; 573 574 const tf_widget = text_field.widget(); 575 // Send some key events to the widget 576 try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'H', .text = "H" } }); 577 // The foo object stores the last text that we saw from an onChange call 578 try std.testing.expectEqualStrings("H", foo.text); 579 try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'e', .text = "e" } }); 580 try std.testing.expectEqualStrings("He", foo.text); 581 try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'l', .text = "l" } }); 582 try std.testing.expectEqualStrings("Hel", foo.text); 583 try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'l', .text = "l" } }); 584 try std.testing.expectEqualStrings("Hell", foo.text); 585 try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'o', .text = "o" } }); 586 try std.testing.expectEqualStrings("Hello", foo.text); 587 588 // An arrow moves the cursor. The text doesn't change 589 try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = vaxis.Key.left } }); 590 try std.testing.expectEqualStrings("Hello", foo.text); 591 592 try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = '_', .text = "_" } }); 593 try std.testing.expectEqualStrings("Hell_o", foo.text); 594} 595 596test "refAllDecls" { 597 std.testing.refAllDecls(@This()); 598}