mod(element/Input): split into TextField without associated event to automatically trigger
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m9s

This commit is contained in:
2025-12-27 17:06:07 +01:00
parent 06ab32bdde
commit 8b5d3757fc
2 changed files with 231 additions and 157 deletions

View File

@@ -502,33 +502,7 @@ pub fn Scrollable(Model: type, Event: type) type {
}; };
} // Scrollable(Model: type, Event: type) } // Scrollable(Model: type, Event: type)
// TODO features pub fn TextField(Model: type, Event: type) type {
// - clear input (with and without retaining of capacity) through an public api
// - make handle / content functions public
pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) type {
// NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
const input_struct = struct {
pub fn input_fn(accept_event: meta.FieldEnum(Event)) type {
const event_type: enum { ascii, utf8 } = blk: { // check for type correctness and the associated type to use for the passed `accept_event`
const err_msg = "Unexpected type for the associated input completion event to trigger. Only `[]u8` or `[]u21` are allowed.";
switch (@typeInfo(@FieldType(Event, @tagName(accept_event)))) {
.pointer => |pointer| {
if (pointer.size != .slice) @compileError(err_msg);
switch (@typeInfo(pointer.child)) {
.int => |num| {
if (num.signedness != .unsigned) @compileError(err_msg);
switch (num.bits) {
8 => break :blk .ascii,
21 => break :blk .utf8,
else => @compileError(err_msg),
}
},
else => @compileError(err_msg),
}
},
else => @compileError(err_msg),
}
};
return struct { return struct {
allocator: Allocator, allocator: Allocator,
/// Offset from the end describing the current position of the cursor. /// Offset from the end describing the current position of the cursor.
@@ -537,8 +511,6 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
configuration: Configuration, 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.
queue: *Queue,
/// Configuration for InputField's. /// Configuration for InputField's.
pub const Configuration = packed struct { pub const Configuration = packed struct {
@@ -553,33 +525,41 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
} }
}; };
pub fn init(allocator: Allocator, queue: *Queue, configuration: Configuration) @This() { pub fn init(allocator: Allocator, configuration: Configuration) @This() {
return .{ return .{
.allocator = allocator, .allocator = allocator,
.configuration = configuration, .configuration = configuration,
.input = std.ArrayList(u21).initCapacity(allocator, 8) catch unreachable, .input = std.ArrayList(u21).initCapacity(allocator, 8) catch unreachable,
.queue = queue,
}; };
} }
fn deinit(ctx: *anyopaque) void { pub fn deinit(this: *@This()) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
this.input.deinit(this.allocator); this.input.deinit(this.allocator);
} }
pub fn clear(this: *@This()) void {
this.input.clearRetainingCapacity();
this.cursor_offset = 0;
}
fn element_deinit(ctx: *anyopaque) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
this.deinit();
}
pub fn element(this: *@This()) Element(Model, Event) { pub fn element(this: *@This()) Element(Model, Event) {
return .{ return .{
.ptr = this, .ptr = this,
.vtable = &.{ .vtable = &.{
.deinit = deinit, .deinit = element_deinit,
.handle = handle, .handle = element_handle,
.content = content, .content = element_content,
}, },
}; };
} }
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void { pub fn handle(this: *@This(), model: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); _ = model;
switch (event) { switch (event) {
.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);
@@ -648,6 +628,7 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
} }
} }
// TODO should this also accept `input.Enter` and insert `\n` into the value of the field?
// usual input keys // usual input keys
if (key.isAscii()) try this.input.insert(this.allocator, this.input.items.len - this.cursor_offset, key.cp); if (key.isAscii()) try this.input.insert(this.allocator, this.input.items.len - this.cursor_offset, key.cp);
@@ -666,36 +647,38 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
// 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? // - 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 = input.Enter }) or key.eql(.{ .cp = input.KpEnter })) { if (key.eql(.{ .cp = input.Enter }) or key.eql(.{ .cp = input.KpEnter })) {}
switch (event_type) {
.ascii => {
// NOTE convert unicode characters to ascii characters; if non ascii characters are found this is will fail!
var slice = try this.allocator.alloc(u8, this.input.items.len);
for (0.., this.input.items) |i, c| slice[i] = @intCast(c);
this.input.clearAndFree(this.allocator);
this.queue.push(@unionInit(
Event,
@tagName(accept_event),
slice,
));
},
.utf8 => this.queue.push(@unionInit(
Event,
@tagName(accept_event),
try this.input.toOwnedSlice(this.allocator),
)),
}
this.cursor_offset = 0;
}
}, },
else => {}, else => {},
} }
} }
fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void { fn element_handle(ctx: *anyopaque, model: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx)); const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); try this.handle(model, event);
}
pub fn toOwnedSlice(this: *@This(), comptime t: enum { bytes, code_points }) !switch (t) {
.bytes => []u8,
.code_points => []u21,
} {
const value = switch (t) {
.bytes => blk: {
// NOTE convert unicode characters to ascii characters; if non ascii characters are found this is will fail!
var slice = try this.allocator.alloc(u8, this.input.items.len);
for (0.., this.input.items) |i, c| slice[i] = @intCast(c);
this.input.clearAndFree(this.allocator);
break :blk slice;
},
.code_points => try this.input.toOwnedSlice(this.allocator),
};
this.cursor_offset = 0;
return value;
}
pub fn content(this: *@This(), model: *const Model, cells: []Cell, size: Point) !void {
_ = model;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const offset = if (this.input.items.len - this.cursor_offset + 1 >= cells.len) this.input.items.len + 1 - cells.len else 0; 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[offset..], 0..) |cp, idx| { for (this.input.items[offset..], 0..) |cp, idx| {
cells[idx].style.fg = this.configuration.color; cells[idx].style.fg = this.configuration.color;
@@ -720,6 +703,97 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
cells[this.input.items.len - offset - this.cursor_offset].style.cursor_shape = .bar_blinking; cells[this.input.items.len - offset - this.cursor_offset].style.cursor_shape = .bar_blinking;
} }
} }
fn element_content(ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
try this.content(model, cells, size);
}
};
} // TextField(Model: type, Event: type)
// TODO features
// - clear input (with and without retaining of capacity) through an public api
// - make handle / content functions public
pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) type {
// NOTE the struct is necessary, as otherwise I cannot point to the function I want to return
const input_struct = struct {
pub fn input_fn(accept_event: meta.FieldEnum(Event)) type {
const event_type: enum { bytes, code_points } = blk: { // check for type correctness and the associated type to use for the passed `accept_event`
const err_msg = "Unexpected type for the associated input completion event to trigger. Only `[]u8` or `[]u21` are allowed.";
switch (@typeInfo(@FieldType(Event, @tagName(accept_event)))) {
.pointer => |pointer| {
if (pointer.size != .slice) @compileError(err_msg);
switch (@typeInfo(pointer.child)) {
.int => |num| {
if (num.signedness != .unsigned) @compileError(err_msg);
switch (num.bits) {
8 => break :blk .bytes,
21 => break :blk .code_points,
else => @compileError(err_msg),
}
},
else => @compileError(err_msg),
}
},
else => @compileError(err_msg),
}
};
return struct {
/// Inner `TextField` for capturing, displaying and handling user inputs.
text_field: TextField(Model, Event),
/// Reference to the app's queue to issue the associated event to trigger when completing the input.
queue: *Queue,
pub fn init(allocator: Allocator, queue: *Queue, configuration: TextField(Model, Event).Configuration) @This() {
return .{
.text_field = .init(allocator, configuration),
.queue = queue,
};
}
fn deinit(ctx: *anyopaque) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
this.text_field.deinit();
}
pub fn element(this: *@This()) Element(Model, Event) {
return .{
.ptr = this,
.vtable = &.{
.deinit = deinit,
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, model: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| {
// NOTE handle order of keys such that the `input.Enter` might not be processed twice! (leading to unexpected values)
try this.text_field.handle(model, event);
// 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 = input.Enter }) or key.eql(.{ .cp = input.KpEnter })) this.queue.push(@unionInit(
Event,
@tagName(accept_event),
switch (event_type) {
.bytes => try this.text_field.toOwnedSlice(.bytes),
.code_points => try this.text_field.toOwnedSlice(.code_points),
},
));
},
else => {},
}
}
fn content(ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
// NOTE size assertion against cells happens in the `TextField.content` method
try this.text_field.content(model, cells, size);
}
}; };
} }
}; };

View File

@@ -1,7 +1,7 @@
// taken from https://github.com/rockorager/libvaxis/blob/main/src/queue.zig (MIT-License) // taken from https://github.com/rockorager/libvaxis/blob/main/src/queue.zig (MIT-License)
// with slight modifications // with slight modifications
/// Queue implementation. Thread safe. Fixed size. Blocking push and pop. Polling through tryPop and tryPush. /// Queue implementation. Thread safe. Fixed size. _Blocking_ `push` and `pop`. _Polling_ through `tryPop` and `tryPush`.
pub fn Queue(comptime T: type, comptime size: usize) type { pub fn Queue(comptime T: type, comptime size: usize) type {
return struct { return struct {
buf: [size]T = undefined, buf: [size]T = undefined,