a modern tui library written in zig
at main 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}