diff --git a/examples/elements/input.zig b/examples/elements/input.zig index 69d5e0d..7239c5a 100644 --- a/examples/elements/input.zig +++ b/examples/elements/input.zig @@ -24,10 +24,18 @@ const QuitText = struct { } }; +// TODO create an own `Element` implementation from this const InputField = struct { + /// Offset from the end describing the current position of the cursor. + cursor_offset: usize = 0, + /// Array holding the value of the input. input: std.ArrayList(u21), + /// Reference to the app's queue to issue the associated event to trigger when completing the input. queue: *App.Queue, + // TODO make the event to trigger user defined (needs to be `comptime`) + // - can this even be agnostic to `u8` / `u21`? + pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() { return .{ .input = .init(allocator), @@ -53,13 +61,68 @@ const InputField = struct { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { .key => |key| { - if (key.isAscii()) try this.input.append(key.cp); + assert(this.cursor_offset >= 0 and this.cursor_offset <= this.input.items.len); - if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter })) + // see *form* implementation in `zk`, which might become part of the library + if (key.eql(.{ .cp = zterm.input.Left }) or key.eql(.{ .cp = zterm.input.KpLeft }) or key.eql(.{ .cp = 'b', .mod = .{ .ctrl = true } })) { + if (this.cursor_offset < this.input.items.len) this.cursor_offset += 1; + } + + if (key.eql(.{ .cp = zterm.input.Right }) or key.eql(.{ .cp = zterm.input.KpRight }) or key.eql(.{ .cp = 'f', .mod = .{ .ctrl = true } })) + this.cursor_offset -|= 1; + + if (key.eql(.{ .cp = 'e', .mod = .{ .ctrl = true } })) this.cursor_offset = 0; + + if (key.eql(.{ .cp = 'a', .mod = .{ .ctrl = true } })) this.cursor_offset = this.input.items.len; + + if (key.eql(.{ .cp = zterm.input.Backspace })) { + if (this.cursor_offset < this.input.items.len) { + _ = if (this.cursor_offset == 0) this.input.pop() else this.input.orderedRemove(this.input.items.len - this.cursor_offset - 1); + } + } + + if (key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete })) { + if (this.cursor_offset > 0) { + _ = this.input.orderedRemove(this.input.items.len - this.cursor_offset); + this.cursor_offset -= 1; + } + } + + // TODO support readline keybindings (i.e. alt bindings alt-b, alt-f + // TODO maybe even ctrl-left, ctrl-right?) + // readline commands + if (key.eql(.{ .cp = 'k', .mod = .{ .ctrl = true } })) { + while (this.cursor_offset > 0) { + _ = this.input.pop(); + this.cursor_offset -= 1; + } + } + + if (key.eql(.{ .cp = 'u', .mod = .{ .ctrl = true } })) { + const len = this.input.items.len - this.cursor_offset; + for (0..len) |_| _ = this.input.orderedRemove(0); + this.cursor_offset = this.input.items.len; + } + + if (key.eql(.{ .cp = 'w', .mod = .{ .ctrl = true } })) { + var non_whitespace = false; + while (this.cursor_offset < this.input.items.len) { + if (non_whitespace and this.input.items[this.input.items.len - this.cursor_offset - 1] == ' ') break; + + // see backspace + const removed = if (this.cursor_offset == 0) this.input.pop() else this.input.orderedRemove(this.input.items.len - this.cursor_offset - 1); + if (removed != ' ') non_whitespace = true; + } + } + + // usual input keys + if (key.isAscii()) try this.input.insert(this.input.items.len - this.cursor_offset, key.cp); + + // TODO enter to accept? + if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter })) { this.queue.push(.{ .accept = try this.input.toOwnedSlice() }); - - if (key.eql(.{ .cp = zterm.input.Backspace }) or key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete })) - _ = this.input.pop(); + this.cursor_offset = 0; + } }, else => {}, } @@ -69,22 +132,17 @@ const InputField = struct { const this: *@This() = @ptrCast(@alignCast(ctx)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); - if (this.input.items.len == 0) return; - - const row = 1; - const col = 1; - const anchor = (row * size.x) + col; - + // TODO add configuration for coloring the text! for (this.input.items, 0..) |cp, idx| { - cells[anchor + idx].style.fg = .black; - cells[anchor + idx].style.cursor = false; - cells[anchor + idx].cp = cp; + cells[idx].style.fg = .black; + cells[idx].cp = cp; // NOTE do not write over the contents of this `Container`'s `Size` - if (anchor + idx == cells.len - 1) break; - - if (idx == this.input.items.len - 1) cells[anchor + idx + 1].style.cursor = true; + if (idx == cells.len - 1) break; } + // TODO show ellipse `..` (maybe with configuration where - start, middle, end) + // show cursor after text (if there is still space available) + if (this.input.items.len < cells.len) cells[this.input.items.len - this.cursor_offset].style.cursor = true; } };