this repo has no description
13
fork

Configure Feed

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

vxfw: add TextField widget

Add the TextField widget. TextField is a single line user input field.
It supports onChange and onSubmit callbacks

+601
+600
src/vxfw/TextField.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("../main.zig"); 3 + 4 + const vxfw = @import("vxfw.zig"); 5 + 6 + const assert = std.debug.assert; 7 + 8 + const Allocator = std.mem.Allocator; 9 + const Key = vaxis.Key; 10 + const Cell = vaxis.Cell; 11 + const Window = vaxis.Window; 12 + const Unicode = vaxis.Unicode; 13 + 14 + const TextField = @This(); 15 + 16 + const ellipsis: Cell.Character = .{ .grapheme = "…", .width = 1 }; 17 + 18 + // Index of our cursor 19 + buf: Buffer, 20 + 21 + /// the number of graphemes to skip when drawing. Used for horizontal scrolling 22 + draw_offset: u16 = 0, 23 + /// the column we placed the cursor the last time we drew 24 + prev_cursor_col: u16 = 0, 25 + /// the grapheme index of the cursor the last time we drew 26 + prev_cursor_idx: u16 = 0, 27 + /// approximate distance from an edge before we scroll 28 + scroll_offset: u4 = 4, 29 + /// Previous width we drew at 30 + prev_width: u16 = 0, 31 + 32 + unicode: *const Unicode, 33 + 34 + previous_val: []const u8 = "", 35 + 36 + userdata: ?*anyopaque = null, 37 + onChange: ?*const fn (?*anyopaque, *vxfw.EventContext, []const u8) anyerror!void = null, 38 + onSubmit: ?*const fn (?*anyopaque, *vxfw.EventContext, []const u8) anyerror!void = null, 39 + 40 + pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextField { 41 + return TextField{ 42 + .buf = Buffer.init(alloc), 43 + .unicode = unicode, 44 + }; 45 + } 46 + 47 + pub fn deinit(self: *TextField) void { 48 + self.buf.allocator.free(self.previous_val); 49 + self.buf.deinit(); 50 + } 51 + 52 + pub fn widget(self: *TextField) vxfw.Widget { 53 + return .{ 54 + .userdata = self, 55 + .eventHandler = typeErasedEventHandler, 56 + .drawFn = typeErasedDrawFn, 57 + }; 58 + } 59 + 60 + fn 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 + 65 + pub 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, .{})) { 106 + if (self.onSubmit) |onSubmit| { 107 + try onSubmit(self.userdata, ctx, self.previous_val); 108 + return ctx.consumeAndRedraw(); 109 + } 110 + } else if (key.text) |text| { 111 + try self.insertSliceAtCursor(text); 112 + return self.checkChanged(ctx); 113 + } 114 + }, 115 + else => {}, 116 + } 117 + } 118 + 119 + fn checkChanged(self: *TextField, ctx: *vxfw.EventContext) anyerror!void { 120 + const new = try self.buf.dupe(); 121 + if (std.mem.eql(u8, new, self.previous_val)) { 122 + self.buf.allocator.free(new); 123 + return ctx.consumeAndRedraw(); 124 + } 125 + self.buf.allocator.free(self.previous_val); 126 + self.previous_val = new; 127 + if (self.onChange) |onChange| { 128 + try onChange(self.userdata, ctx, new); 129 + } 130 + ctx.consumeAndRedraw(); 131 + } 132 + 133 + /// insert text at the cursor position 134 + pub fn insertSliceAtCursor(self: *TextField, data: []const u8) std.mem.Allocator.Error!void { 135 + var iter = self.unicode.graphemeIterator(data); 136 + while (iter.next()) |text| { 137 + try self.buf.insertSliceAtCursor(text.bytes(data)); 138 + } 139 + } 140 + 141 + pub fn sliceToCursor(self: *TextField, buf: []u8) []const u8 { 142 + assert(buf.len >= self.buf.cursor); 143 + @memcpy(buf[0..self.buf.cursor], self.buf.firstHalf()); 144 + return buf[0..self.buf.cursor]; 145 + } 146 + 147 + /// calculates the display width from the draw_offset to the cursor 148 + pub fn widthToCursor(self: *TextField, ctx: vxfw.DrawContext) u16 { 149 + var width: u16 = 0; 150 + const first_half = self.buf.firstHalf(); 151 + var first_iter = self.unicode.graphemeIterator(first_half); 152 + var i: usize = 0; 153 + while (first_iter.next()) |grapheme| { 154 + defer i += 1; 155 + if (i < self.draw_offset) { 156 + continue; 157 + } 158 + const g = grapheme.bytes(first_half); 159 + width += @intCast(ctx.stringWidth(g)); 160 + } 161 + return width; 162 + } 163 + 164 + pub fn cursorLeft(self: *TextField) void { 165 + // We need to find the size of the last grapheme in the first half 166 + var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 167 + var len: usize = 0; 168 + while (iter.next()) |grapheme| { 169 + len = grapheme.len; 170 + } 171 + self.buf.moveGapLeft(len); 172 + } 173 + 174 + pub fn cursorRight(self: *TextField) void { 175 + var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 176 + const grapheme = iter.next() orelse return; 177 + self.buf.moveGapRight(grapheme.len); 178 + } 179 + 180 + pub fn graphemesBeforeCursor(self: *const TextField) u16 { 181 + const first_half = self.buf.firstHalf(); 182 + var first_iter = self.unicode.graphemeIterator(first_half); 183 + var i: u16 = 0; 184 + while (first_iter.next()) |_| { 185 + i += 1; 186 + } 187 + return i; 188 + } 189 + 190 + fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 191 + const self: *TextField = @ptrCast(@alignCast(ptr)); 192 + return self.draw(ctx); 193 + } 194 + 195 + pub fn draw(self: *TextField, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 196 + std.debug.assert(ctx.max.width != null); 197 + const max_width = ctx.max.width.?; 198 + if (max_width != self.prev_width) { 199 + self.prev_width = max_width; 200 + self.draw_offset = 0; 201 + self.prev_cursor_col = 0; 202 + } 203 + // Create a surface with max width and a minimum height of 1. 204 + var surface = try vxfw.Surface.init( 205 + ctx.arena, 206 + self.widget(), 207 + .{ .width = max_width, .height = @max(ctx.min.height, 1) }, 208 + ); 209 + surface.focusable = true; 210 + surface.handles_mouse = true; 211 + 212 + const base: vaxis.Cell = .{ .style = .{} }; 213 + @memset(surface.buffer, base); 214 + const style: vaxis.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 = self.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 = self.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 + 296 + pub fn clearAndFree(self: *TextField) void { 297 + self.buf.clearAndFree(); 298 + self.reset(); 299 + } 300 + 301 + pub fn clearRetainingCapacity(self: *TextField) void { 302 + self.buf.clearRetainingCapacity(); 303 + self.reset(); 304 + } 305 + 306 + pub fn toOwnedSlice(self: *TextField) ![]const u8 { 307 + defer self.reset(); 308 + return self.buf.toOwnedSlice(); 309 + } 310 + 311 + pub 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 318 + pub fn byteOffsetToCursor(self: TextField) usize { 319 + return self.buf.cursor; 320 + } 321 + 322 + pub fn deleteToEnd(self: *TextField) void { 323 + self.buf.growGapRight(self.buf.secondHalf().len); 324 + } 325 + 326 + pub fn deleteToStart(self: *TextField) void { 327 + self.buf.growGapLeft(self.buf.cursor); 328 + } 329 + 330 + pub fn deleteBeforeCursor(self: *TextField) void { 331 + // We need to find the size of the last grapheme in the first half 332 + var iter = self.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 + 340 + pub fn deleteAfterCursor(self: *TextField) void { 341 + var iter = self.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 348 + pub 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 + 357 + pub 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 + 365 + pub 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 + 373 + pub 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 + 383 + test "sliceToCursor" { 384 + const alloc = std.testing.allocator_instance.allocator(); 385 + const unicode = try Unicode.init(alloc); 386 + defer unicode.deinit(); 387 + var input = init(alloc, &unicode); 388 + defer input.deinit(); 389 + try input.insertSliceAtCursor("hello, world"); 390 + input.cursorLeft(); 391 + input.cursorLeft(); 392 + input.cursorLeft(); 393 + var buf: [32]u8 = undefined; 394 + try std.testing.expectEqualStrings("hello, wo", input.sliceToCursor(&buf)); 395 + input.cursorRight(); 396 + try std.testing.expectEqualStrings("hello, wor", input.sliceToCursor(&buf)); 397 + } 398 + 399 + pub const Buffer = struct { 400 + allocator: std.mem.Allocator, 401 + buffer: []u8, 402 + cursor: usize, 403 + gap_size: usize, 404 + 405 + pub fn init(allocator: std.mem.Allocator) Buffer { 406 + return .{ 407 + .allocator = allocator, 408 + .buffer = &.{}, 409 + .cursor = 0, 410 + .gap_size = 0, 411 + }; 412 + } 413 + 414 + pub fn deinit(self: *Buffer) void { 415 + self.allocator.free(self.buffer); 416 + } 417 + 418 + pub fn firstHalf(self: Buffer) []const u8 { 419 + return self.buffer[0..self.cursor]; 420 + } 421 + 422 + pub fn secondHalf(self: Buffer) []const u8 { 423 + return self.buffer[self.cursor + self.gap_size ..]; 424 + } 425 + 426 + pub fn grow(self: *Buffer, n: usize) std.mem.Allocator.Error!void { 427 + // Always grow by 512 bytes 428 + const new_size = self.buffer.len + n + 512; 429 + // Allocate the new memory 430 + const new_memory = try self.allocator.alloc(u8, new_size); 431 + // Copy the first half 432 + @memcpy(new_memory[0..self.cursor], self.firstHalf()); 433 + // Copy the second half 434 + const second_half = self.secondHalf(); 435 + @memcpy(new_memory[new_size - second_half.len ..], second_half); 436 + self.allocator.free(self.buffer); 437 + self.buffer = new_memory; 438 + self.gap_size = new_size - second_half.len - self.cursor; 439 + } 440 + 441 + pub fn insertSliceAtCursor(self: *Buffer, slice: []const u8) std.mem.Allocator.Error!void { 442 + if (slice.len == 0) return; 443 + if (self.gap_size <= slice.len) try self.grow(slice.len); 444 + @memcpy(self.buffer[self.cursor .. self.cursor + slice.len], slice); 445 + self.cursor += slice.len; 446 + self.gap_size -= slice.len; 447 + } 448 + 449 + /// Move the gap n bytes to the left 450 + pub fn moveGapLeft(self: *Buffer, n: usize) void { 451 + const new_idx = self.cursor -| n; 452 + const dst = self.buffer[new_idx + self.gap_size ..]; 453 + const src = self.buffer[new_idx..self.cursor]; 454 + std.mem.copyForwards(u8, dst, src); 455 + self.cursor = new_idx; 456 + } 457 + 458 + pub fn moveGapRight(self: *Buffer, n: usize) void { 459 + const new_idx = self.cursor + n; 460 + const dst = self.buffer[self.cursor..]; 461 + const src = self.buffer[self.cursor + self.gap_size .. new_idx + self.gap_size]; 462 + std.mem.copyForwards(u8, dst, src); 463 + self.cursor = new_idx; 464 + } 465 + 466 + /// grow the gap by moving the cursor n bytes to the left 467 + pub fn growGapLeft(self: *Buffer, n: usize) void { 468 + // gap grows by the delta 469 + self.gap_size += n; 470 + self.cursor -|= n; 471 + } 472 + 473 + /// grow the gap by removing n bytes after the cursor 474 + pub fn growGapRight(self: *Buffer, n: usize) void { 475 + self.gap_size = @min(self.gap_size + n, self.buffer.len - self.cursor); 476 + } 477 + 478 + pub fn clearAndFree(self: *Buffer) void { 479 + self.cursor = 0; 480 + self.allocator.free(self.buffer); 481 + self.buffer = &.{}; 482 + self.gap_size = 0; 483 + } 484 + 485 + pub fn clearRetainingCapacity(self: *Buffer) void { 486 + self.cursor = 0; 487 + self.gap_size = self.buffer.len; 488 + } 489 + 490 + pub fn toOwnedSlice(self: *Buffer) std.mem.Allocator.Error![]const u8 { 491 + const slice = try self.dupe(); 492 + self.clearAndFree(); 493 + return slice; 494 + } 495 + 496 + pub fn realLength(self: *const Buffer) usize { 497 + return self.firstHalf().len + self.secondHalf().len; 498 + } 499 + 500 + pub fn dupe(self: *const Buffer) std.mem.Allocator.Error![]const u8 { 501 + const first_half = self.firstHalf(); 502 + const second_half = self.secondHalf(); 503 + const buf = try self.allocator.alloc(u8, first_half.len + second_half.len); 504 + @memcpy(buf[0..first_half.len], first_half); 505 + @memcpy(buf[first_half.len..], second_half); 506 + return buf; 507 + } 508 + }; 509 + 510 + test "TextField.zig: Buffer" { 511 + var gap_buf = Buffer.init(std.testing.allocator); 512 + defer gap_buf.deinit(); 513 + 514 + try gap_buf.insertSliceAtCursor("abc"); 515 + try std.testing.expectEqualStrings("abc", gap_buf.firstHalf()); 516 + try std.testing.expectEqualStrings("", gap_buf.secondHalf()); 517 + 518 + gap_buf.moveGapLeft(1); 519 + try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 520 + try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 521 + 522 + try gap_buf.insertSliceAtCursor(" "); 523 + try std.testing.expectEqualStrings("ab ", gap_buf.firstHalf()); 524 + try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 525 + 526 + gap_buf.growGapLeft(1); 527 + try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 528 + try std.testing.expectEqualStrings("c", gap_buf.secondHalf()); 529 + try std.testing.expectEqual(2, gap_buf.cursor); 530 + 531 + gap_buf.growGapRight(1); 532 + try std.testing.expectEqualStrings("ab", gap_buf.firstHalf()); 533 + try std.testing.expectEqualStrings("", gap_buf.secondHalf()); 534 + try std.testing.expectEqual(2, gap_buf.cursor); 535 + } 536 + 537 + test TextField { 538 + // Boiler plate draw context init 539 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 540 + defer arena.deinit(); 541 + const ucd = try vaxis.Unicode.init(arena.allocator()); 542 + vxfw.DrawContext.init(&ucd, .unicode); 543 + 544 + // Create some object which reacts to text field changes 545 + const Foo = struct { 546 + allocator: std.mem.Allocator, 547 + text: []const u8, 548 + 549 + fn onChange(ptr: ?*anyopaque, ctx: *vxfw.EventContext, str: []const u8) anyerror!void { 550 + const foo: *@This() = @ptrCast(@alignCast(ptr)); 551 + foo.text = try foo.allocator.dupe(u8, str); 552 + ctx.consumeAndRedraw(); 553 + } 554 + }; 555 + var foo: Foo = .{ .text = "", .allocator = arena.allocator() }; 556 + 557 + // Text field expands to the width, so it can't be null. It is always 1 line tall 558 + const draw_ctx: vxfw.DrawContext = .{ 559 + .arena = arena.allocator(), 560 + .min = .{}, 561 + .max = .{ .width = 8, .height = 1 }, 562 + }; 563 + _ = draw_ctx; 564 + 565 + var ctx: vxfw.EventContext = .{ 566 + .cmds = vxfw.CommandList.init(arena.allocator()), 567 + }; 568 + 569 + // Enough boiler plate...Create the text field 570 + var text_field = TextField.init(std.testing.allocator, &ucd); 571 + defer text_field.deinit(); 572 + text_field.onChange = Foo.onChange; 573 + text_field.onSubmit = Foo.onChange; 574 + text_field.userdata = &foo; 575 + 576 + const tf_widget = text_field.widget(); 577 + // Send some key events to the widget 578 + try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'H', .text = "H" } }); 579 + // The foo object stores the last text that we saw from an onChange call 580 + try std.testing.expectEqualStrings("H", foo.text); 581 + try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'e', .text = "e" } }); 582 + try std.testing.expectEqualStrings("He", foo.text); 583 + try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'l', .text = "l" } }); 584 + try std.testing.expectEqualStrings("Hel", foo.text); 585 + try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'l', .text = "l" } }); 586 + try std.testing.expectEqualStrings("Hell", foo.text); 587 + try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'o', .text = "o" } }); 588 + try std.testing.expectEqualStrings("Hello", foo.text); 589 + 590 + // An arrow moves the cursor. The text doesn't change 591 + try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = vaxis.Key.left } }); 592 + try std.testing.expectEqualStrings("Hello", foo.text); 593 + 594 + try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = '_', .text = "_" } }); 595 + try std.testing.expectEqualStrings("Hell_o", foo.text); 596 + } 597 + 598 + test "refAllDecls" { 599 + std.testing.refAllDecls(@This()); 600 + }
+1
src/vxfw/vxfw.zig
··· 14 14 pub const ListView = @import("ListView.zig"); 15 15 pub const RichText = @import("RichText.zig"); 16 16 pub const Text = @import("Text.zig"); 17 + pub const TextField = @import("TextField.zig"); 17 18 18 19 pub const CommandList = std.ArrayList(Command); 19 20