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:
386
src/element.zig
386
src/element.zig
@@ -502,6 +502,215 @@ pub fn Scrollable(Model: type, Event: type) type {
|
||||
};
|
||||
} // Scrollable(Model: type, Event: type)
|
||||
|
||||
pub fn TextField(Model: type, Event: type) type {
|
||||
return struct {
|
||||
allocator: Allocator,
|
||||
/// Offset from the end describing the current position of the cursor.
|
||||
cursor_offset: usize = 0,
|
||||
/// Configuration for the InputField.
|
||||
configuration: Configuration,
|
||||
/// Array holding the value of the input.
|
||||
input: std.ArrayList(u21),
|
||||
|
||||
/// Configuration for InputField's.
|
||||
pub const Configuration = packed struct {
|
||||
color: Color,
|
||||
cursor: Color,
|
||||
|
||||
pub fn init(color: Color, cursor: Color) @This() {
|
||||
return .{
|
||||
.color = color,
|
||||
.cursor = cursor,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, configuration: Configuration) @This() {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.configuration = configuration,
|
||||
.input = std.ArrayList(u21).initCapacity(allocator, 8) catch unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
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) {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.deinit = element_deinit,
|
||||
.handle = element_handle,
|
||||
.content = element_content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn handle(this: *@This(), model: *Model, event: Event) !void {
|
||||
_ = model;
|
||||
switch (event) {
|
||||
.key => |key| {
|
||||
assert(this.cursor_offset >= 0 and this.cursor_offset <= this.input.items.len);
|
||||
|
||||
// readline commands
|
||||
if (key.eql(.{ .cp = input.Left }) or key.eql(.{ .cp = 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 = input.Right }) or key.eql(.{ .cp = 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 = '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;
|
||||
}
|
||||
}
|
||||
|
||||
if (key.eql(.{ .cp = 'b', .mod = .{ .alt = true } }) or key.eql(.{ .cp = input.Left, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = 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 = input.Right, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = 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;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO should this also accept `input.Enter` and insert `\n` into the value of the field?
|
||||
// usual input keys
|
||||
if (key.isAscii()) try this.input.insert(this.allocator, this.input.items.len - this.cursor_offset, key.cp);
|
||||
|
||||
if (key.eql(.{ .cp = 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 = input.Delete }) or key.eql(.{ .cp = 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?
|
||||
// - 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 })) {}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn element_handle(ctx: *anyopaque, model: *Model, event: Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
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;
|
||||
for (this.input.items[offset..], 0..) |cp, idx| {
|
||||
cells[idx].style.fg = this.configuration.color;
|
||||
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`
|
||||
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;
|
||||
}
|
||||
}
|
||||
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_color = this.configuration.cursor;
|
||||
cells[this.input.items.len - this.cursor_offset].style.cursor_shape = .bar_blinking;
|
||||
} else {
|
||||
cells[this.input.items.len - offset - this.cursor_offset].style.cursor = true;
|
||||
cells[this.input.items.len - offset - this.cursor_offset].style.cursor_color = this.configuration.cursor;
|
||||
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
|
||||
@@ -509,7 +718,7 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
|
||||
// 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 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| {
|
||||
@@ -518,8 +727,8 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
|
||||
.int => |num| {
|
||||
if (num.signedness != .unsigned) @compileError(err_msg);
|
||||
switch (num.bits) {
|
||||
8 => break :blk .ascii,
|
||||
21 => break :blk .utf8,
|
||||
8 => break :blk .bytes,
|
||||
21 => break :blk .code_points,
|
||||
else => @compileError(err_msg),
|
||||
}
|
||||
},
|
||||
@@ -530,41 +739,21 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
|
||||
}
|
||||
};
|
||||
return struct {
|
||||
allocator: Allocator,
|
||||
/// Offset from the end describing the current position of the cursor.
|
||||
cursor_offset: usize = 0,
|
||||
/// Configuration for the InputField.
|
||||
configuration: Configuration,
|
||||
/// Array holding the value of the input.
|
||||
input: std.ArrayList(u21),
|
||||
/// 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,
|
||||
|
||||
/// Configuration for InputField's.
|
||||
pub const Configuration = packed struct {
|
||||
color: Color,
|
||||
cursor: Color,
|
||||
|
||||
pub fn init(color: Color, cursor: Color) @This() {
|
||||
return .{
|
||||
.color = color,
|
||||
.cursor = cursor,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, queue: *Queue, configuration: Configuration) @This() {
|
||||
pub fn init(allocator: Allocator, queue: *Queue, configuration: TextField(Model, Event).Configuration) @This() {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.configuration = configuration,
|
||||
.input = std.ArrayList(u21).initCapacity(allocator, 8) catch unreachable,
|
||||
.text_field = .init(allocator, configuration),
|
||||
.queue = queue,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(ctx: *anyopaque) void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
this.input.deinit(this.allocator);
|
||||
this.text_field.deinit();
|
||||
}
|
||||
|
||||
pub fn element(this: *@This()) Element(Model, Event) {
|
||||
@@ -578,147 +767,32 @@ pub fn Input(Model: type, Event: type, Queue: type) fn (meta.FieldEnum(Event)) t
|
||||
};
|
||||
}
|
||||
|
||||
fn handle(ctx: *anyopaque, _: *Model, event: Event) !void {
|
||||
fn handle(ctx: *anyopaque, model: *Model, event: Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
.key => |key| {
|
||||
assert(this.cursor_offset >= 0 and this.cursor_offset <= this.input.items.len);
|
||||
|
||||
// readline commands
|
||||
if (key.eql(.{ .cp = input.Left }) or key.eql(.{ .cp = 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 = input.Right }) or key.eql(.{ .cp = 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 = '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;
|
||||
}
|
||||
}
|
||||
|
||||
if (key.eql(.{ .cp = 'b', .mod = .{ .alt = true } }) or key.eql(.{ .cp = input.Left, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = 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 = input.Right, .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = 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
|
||||
if (key.isAscii()) try this.input.insert(this.allocator, this.input.items.len - this.cursor_offset, key.cp);
|
||||
|
||||
if (key.eql(.{ .cp = 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 = input.Delete }) or key.eql(.{ .cp = input.KpDelete })) {
|
||||
if (this.cursor_offset > 0) {
|
||||
_ = this.input.orderedRemove(this.input.items.len - this.cursor_offset);
|
||||
this.cursor_offset -= 1;
|
||||
}
|
||||
}
|
||||
// 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 })) {
|
||||
if (key.eql(.{ .cp = input.Enter }) or key.eql(.{ .cp = input.KpEnter })) this.queue.push(@unionInit(
|
||||
Event,
|
||||
@tagName(accept_event),
|
||||
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;
|
||||
}
|
||||
.bytes => try this.text_field.toOwnedSlice(.bytes),
|
||||
.code_points => try this.text_field.toOwnedSlice(.code_points),
|
||||
},
|
||||
));
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, _: *const Model, cells: []Cell, size: Point) !void {
|
||||
fn content(ctx: *anyopaque, model: *const Model, cells: []Cell, size: Point) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
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;
|
||||
for (this.input.items[offset..], 0..) |cp, idx| {
|
||||
cells[idx].style.fg = this.configuration.color;
|
||||
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`
|
||||
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;
|
||||
}
|
||||
}
|
||||
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_color = this.configuration.cursor;
|
||||
cells[this.input.items.len - this.cursor_offset].style.cursor_shape = .bar_blinking;
|
||||
} else {
|
||||
cells[this.input.items.len - offset - this.cursor_offset].style.cursor = true;
|
||||
cells[this.input.items.len - offset - this.cursor_offset].style.cursor_color = this.configuration.cursor;
|
||||
cells[this.input.items.len - offset - this.cursor_offset].style.cursor_shape = .bar_blinking;
|
||||
}
|
||||
// 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)
|
||||
// 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 {
|
||||
return struct {
|
||||
buf: [size]T = undefined,
|
||||
|
||||
Reference in New Issue
Block a user