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
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m9s
This commit is contained in:
196
src/element.zig
196
src/element.zig
@@ -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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user