diff --git a/build.zig b/build.zig index 39860d3..707ba40 100644 --- a/build.zig +++ b/build.zig @@ -13,6 +13,7 @@ pub fn build(b: *std.Build) void { progress, radio_button, scrollable, + selection, // layouts: vertical, horizontal, @@ -65,6 +66,7 @@ pub fn build(b: *std.Build) void { .progress => "examples/elements/progress.zig", .radio_button => "examples/elements/radio-button.zig", .scrollable => "examples/elements/scrollable.zig", + .selection => "examples/elements/selection.zig", // layouts: .vertical => "examples/layouts/vertical.zig", .horizontal => "examples/layouts/horizontal.zig", diff --git a/src/app.zig b/src/app.zig index 51db452..3fcaa68 100644 --- a/src/app.zig +++ b/src/app.zig @@ -418,6 +418,7 @@ pub fn App(comptime E: type) type { pub const Progress = element.Progress(Event, Queue); pub const RadioButton = element.RadioButton(Event); pub const Scrollable = element.Scrollable(Event); + pub const Selection = element.Selection(Event); pub const Queue = queue.Queue(Event, 256); }; } diff --git a/src/element.zig b/src/element.zig index ed803d9..5d46508 100644 --- a/src/element.zig +++ b/src/element.zig @@ -65,7 +65,7 @@ pub fn Element(Event: type) type { } } }; -} +} // Element(Event: type) pub fn Alignment(Event: type) type { return struct { @@ -162,7 +162,7 @@ pub fn Alignment(Event: type) type { } } }; -} +} // Alignment(Event: type) pub fn Scrollable(Event: type) type { return struct { @@ -359,7 +359,7 @@ pub fn Scrollable(Event: type) type { this.container.allocator.free(container_cells); } }; -} +} // Scrollable(Event: type) pub fn Input(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 @@ -567,7 +567,7 @@ pub fn Input(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { } }; return input_struct.input_fn; -} +} // Input(Event: type, Queue: type) pub fn Button(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 @@ -645,7 +645,7 @@ pub fn Button(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { } }; return button_struct.button_fn; -} +} // Button(Event: type, Queue: type) pub fn RadioButton(Event: type) type { return struct { @@ -709,7 +709,149 @@ pub fn RadioButton(Event: type) type { } } }; -} +} // RadioButton(Event: type) + +pub fn Selection(Event: type) fn (type) type { + const selection_struct = struct { + pub fn selection_fn(Enum: type) type { + switch (@typeInfo(Enum)) { + .@"enum" => |e| if (!e.is_exhaustive) @compileError("Selection's enum value needs to be exhaustive."), + else => @compileError("Selection's `Enum` type is not an `enum` type."), + } + return struct { + value: Enum, + configuration: Configuration, + width: u16, + + pub const Configuration = struct { + label: []const u8, + }; + + pub fn init(configuration: Configuration) @This() { + var max_len: usize = 0; + inline for (std.meta.fields(Enum)) |field| max_len = @max(max_len, field.name.len); + return .{ + .value = @enumFromInt(0), + .configuration = configuration, + .width = @intCast(max_len), + }; + } + + pub fn element(this: *@This()) Element(Event) { + return .{ + .ptr = this, + .vtable = &.{ + .handle = handle, + .content = content, + }, + }; + } + + fn handle(ctx: *anyopaque, event: Event) !void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + switch (event) { + .mouse => |mouse| if (mouse.y == 0) { + if (mouse.button == .left and mouse.kind == .release) { + // left button + if (mouse.x == this.configuration.label.len + 1) { + const next = if (@intFromEnum(this.value) > 0) + @intFromEnum(this.value) - 1 + else + std.meta.fields(Enum).len - 1; + this.value = @enumFromInt(next); + } + + // right button + if (mouse.x == this.configuration.label.len + 4 + this.width) { + const next = if (@intFromEnum(this.value) < std.meta.fields(Enum).len - 1) + @intFromEnum(this.value) + 1 + else + 0; + this.value = @enumFromInt(next); + } + } + + if (mouse.x > this.configuration.label.len and mouse.x < this.configuration.label.len + 4 + this.width) { + if (mouse.button == .wheel_down) { + const next = if (@intFromEnum(this.value) > 0) + @intFromEnum(this.value) - 1 + else + std.meta.fields(Enum).len - 1; + this.value = @enumFromInt(next); + } + + if (mouse.button == .wheel_up) { + const next = if (@intFromEnum(this.value) < std.meta.fields(Enum).len - 1) + @intFromEnum(this.value) + 1 + else + 0; + this.value = @enumFromInt(next); + } + } + }, + .key => |key| { + if (key.eql(.{ .cp = 'j' }) or key.eql(.{ .cp = input.Down }) or key.eql(.{ .cp = input.KpDown })) { + const next = if (@intFromEnum(this.value) < std.meta.fields(Enum).len - 1) + @intFromEnum(this.value) + 1 + else + 0; + this.value = @enumFromInt(next); + } + + if (key.eql(.{ .cp = 'k' }) or key.eql(.{ .cp = input.Up }) or key.eql(.{ .cp = input.KpUp })) { + const next = if (@intFromEnum(this.value) > 0) + @intFromEnum(this.value) - 1 + else + std.meta.fields(Enum).len - 1; + this.value = @enumFromInt(next); + } + + if (key.eql(.{ .cp = 'h' }) or key.eql(.{ .cp = input.Left }) or key.eql(.{ .cp = input.KpLeft })) { + const next = if (@intFromEnum(this.value) > 0) + @intFromEnum(this.value) - 1 + else + std.meta.fields(Enum).len - 1; + this.value = @enumFromInt(next); + } + + if (key.eql(.{ .cp = 'l' }) or key.eql(.{ .cp = input.Right }) or key.eql(.{ .cp = input.KpRight })) { + const next = if (@intFromEnum(this.value) < std.meta.fields(Enum).len - 1) + @intFromEnum(this.value) + 1 + else + 0; + this.value = @enumFromInt(next); + } + }, + else => {}, + } + } + + fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); + if (this.configuration.label.len + 4 + this.width >= cells.len) return Error.TooSmall; + + for (0.., this.configuration.label) |i, c| { + if (i == cells.len - 1) break; + + cells[i].cp = c; + } + cells[this.configuration.label.len + 1].cp = '◀'; + + const value = @tagName(this.value); + const offset = (this.width - value.len) / 2; // to center the value's text + for (this.configuration.label.len + 3 + offset.., value) |i, c| { + if (i == cells.len - 1) break; + + cells[i].cp = c; + } + cells[this.configuration.label.len + 4 + this.width].cp = '▶'; + } + }; + } + }; + return selection_struct.selection_fn; +} // Selection(Enum: type) pub fn Progress(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 @@ -809,7 +951,7 @@ pub fn Progress(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { } }; return progress_struct.progress_fn; -} +} // Progress(Event: type, Queue: type) const std = @import("std"); const assert = std.debug.assert; @@ -819,6 +961,7 @@ const input = @import("input.zig"); const Container = @import("container.zig").Container; const Cell = @import("cell.zig"); const Color = @import("color.zig").Color; +const Error = @import("error.zig").Error; const Mouse = input.Mouse; const Point = @import("point.zig").Point; @@ -1386,6 +1529,8 @@ test "button" { }); } +// TODO add test cases for `RadioButton` and `Selection` + test "progress" { const allocator = std.testing.allocator; const event = @import("event.zig");