2 Commits

Author SHA1 Message Date
439520d4fe mod(example/input): ellipse rendering with scrolling for text field contents
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 17s
2025-06-28 22:09:36 +02:00
ded1f2c17e mod(example/input): reorder input handling cases; add alt-b/f binding implementations 2025-06-28 22:08:19 +02:00
2 changed files with 72 additions and 31 deletions

View File

@@ -87,10 +87,6 @@ const InputField = struct {
.key => |key| { .key => |key| {
if (key.isAscii()) try this.input.append(key.cp); if (key.isAscii()) try this.input.append(key.cp);
// TODO support arrow keys for navigation?
// TODO support readline keybindings (i.e. ctrl-k, ctrl-u, ctrl-b, ctrl-f, etc. and the equivalent alt bindings)
// create an own `Element` implementation from this
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter })) if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
this.queue.push(.{ .accept = try this.input.toOwnedSlice() }); this.queue.push(.{ .accept = try this.input.toOwnedSlice() });

View File

@@ -28,16 +28,24 @@ const QuitText = struct {
const InputField = struct { const InputField = struct {
/// Offset from the end describing the current position of the cursor. /// Offset from the end describing the current position of the cursor.
cursor_offset: usize = 0, cursor_offset: usize = 0,
/// Configuration for the InputField.
configuration: Configuration,
/// Array holding the value of the input. /// Array holding the value of the input.
input: std.ArrayList(u21), input: std.ArrayList(u21),
/// Reference to the app's queue to issue the associated event to trigger when completing the input. /// Reference to the app's queue to issue the associated event to trigger when completing the input.
queue: *App.Queue, queue: *App.Queue,
/// Configuration for InputField's.
pub const Configuration = struct {
color: Color = .default,
};
// TODO make the event to trigger user defined (needs to be `comptime`) // TODO make the event to trigger user defined (needs to be `comptime`)
// - can this even be agnostic to `u8` / `u21`? // - can this even be agnostic to `u8` / `u21`?
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() { pub fn init(allocator: std.mem.Allocator, queue: *App.Queue, configuration: Configuration) @This() {
return .{ return .{
.configuration = configuration,
.input = .init(allocator), .input = .init(allocator),
.queue = queue, .queue = queue,
}; };
@@ -63,7 +71,7 @@ const InputField = struct {
.key => |key| { .key => |key| {
assert(this.cursor_offset >= 0 and this.cursor_offset <= this.input.items.len); assert(this.cursor_offset >= 0 and this.cursor_offset <= this.input.items.len);
// see *form* implementation in `zk`, which might become part of the library // readline commands
if (key.eql(.{ .cp = zterm.input.Left }) or key.eql(.{ .cp = zterm.input.KpLeft }) or key.eql(.{ .cp = 'b', .mod = .{ .ctrl = true } })) { 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 (this.cursor_offset < this.input.items.len) this.cursor_offset += 1;
} }
@@ -75,22 +83,6 @@ const InputField = struct {
if (key.eql(.{ .cp = 'a', .mod = .{ .ctrl = true } })) this.cursor_offset = this.input.items.len; 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 } })) { if (key.eql(.{ .cp = 'k', .mod = .{ .ctrl = true } })) {
while (this.cursor_offset > 0) { while (this.cursor_offset > 0) {
_ = this.input.pop(); _ = this.input.pop();
@@ -115,10 +107,52 @@ const InputField = struct {
} }
} }
if (key.eql(.{ .cp = 'b', .mod = .{ .alt = true } }) or key.eql(.{ .cp = zterm.input.Left, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = zterm.input.KpLeft, .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
this.cursor_offset += 1;
if (this.cursor_offset == this.input.items.len) break;
const next = this.input.items[this.input.items.len - this.cursor_offset - 1];
if (next != ' ') non_whitespace = true;
}
}
if (key.eql(.{ .cp = 'f', .mod = .{ .alt = true } }) or key.eql(.{ .cp = zterm.input.Right, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = zterm.input.KpRight, .mod = .{ .ctrl = true } })) {
var non_whitespace = false;
while (this.cursor_offset > 0) {
if (non_whitespace and this.input.items[this.input.items.len - this.cursor_offset - 1] == ' ') {
this.cursor_offset += 1; // correct cursor position back again to make sure the cursor is not on the whitespace, but at the end of the jumped word
break;
}
// see backspace
this.cursor_offset -= 1;
const next = this.input.items[this.input.items.len - this.cursor_offset - 1];
if (next != ' ') non_whitespace = true;
}
}
// usual input keys // usual input keys
if (key.isAscii()) try this.input.insert(this.input.items.len - this.cursor_offset, key.cp); if (key.isAscii()) try this.input.insert(this.input.items.len - this.cursor_offset, key.cp);
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 enter to accept? // TODO enter to accept?
// - shift+enter is not recognized by the input reader of `zterm`, so currently it is not possible to add newlines into the text box?
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter })) { if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter })) {
this.queue.push(.{ .accept = try this.input.toOwnedSlice() }); this.queue.push(.{ .accept = try this.input.toOwnedSlice() });
this.cursor_offset = 0; this.cursor_offset = 0;
@@ -132,17 +166,24 @@ const InputField = struct {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
// TODO add configuration for coloring the text! const offset = if (this.input.items.len - this.cursor_offset + 1 >= cells.len) this.input.items.len + 1 - cells.len else 0;
for (this.input.items, 0..) |cp, idx| { for (this.input.items[offset..], 0..) |cp, idx| {
cells[idx].style.fg = .black; cells[idx].style.fg = this.configuration.color;
cells[idx].cp = cp; cells[idx].cp = cp;
// display ellipse at the beginning
if (offset > 0 and idx == 0) cells[idx].cp = '…';
// NOTE do not write over the contents of this `Container`'s `Size` // NOTE do not write over the contents of this `Container`'s `Size`
if (idx == cells.len - 1) break; if (idx == cells.len - 1) {
// display ellipse at the end
if (this.input.items.len >= cells.len and this.cursor_offset > 0) cells[idx].cp = '…';
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)
if (this.input.items.len < cells.len) cells[this.input.items.len - this.cursor_offset].style.cursor = true; cells[this.input.items.len - this.cursor_offset].style.cursor = true
else
cells[this.input.items.len - offset - this.cursor_offset].style.cursor = true;
} }
}; };
@@ -191,7 +232,9 @@ pub fn main() !void {
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
var input_field: InputField = .init(allocator, &app.queue); var input_field: InputField = .init(allocator, &app.queue, .{
.color = .black,
});
defer input_field.deinit(); defer input_field.deinit();
var mouse_draw: MouseDraw = .{}; var mouse_draw: MouseDraw = .{};
@@ -211,7 +254,7 @@ pub fn main() !void {
.rectangle = .{ .fill = .light_grey }, .rectangle = .{ .fill = .light_grey },
.size = .{ .size = .{
.grow = .horizontal, .grow = .horizontal,
.dim = .{ .y = 10 }, .dim = .{ .y = 1 },
}, },
}, input_field.element())); }, input_field.element()));
@@ -284,6 +327,8 @@ const log = std.log.scoped(.default);
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const zterm = @import("zterm"); const zterm = @import("zterm");
const Color = zterm.Color;
const App = zterm.App(union(enum) { const App = zterm.App(union(enum) {
accept: []u21, accept: []u21,
}); });