From a39cee7ccb223e047cab8ebfd22a6b59caa1e4ec Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Mon, 30 Jun 2025 22:52:27 +0200 Subject: [PATCH] feat(element/button): add builtin `Element` implementation for buttons --- examples/elements/button.zig | 9 ++- src/app.zig | 3 +- src/element.zig | 125 +++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 4 deletions(-) diff --git a/examples/elements/button.zig b/examples/elements/button.zig index fd5cbe6..45772ef 100644 --- a/examples/elements/button.zig +++ b/examples/elements/button.zig @@ -89,6 +89,8 @@ pub fn main() !void { var clickable: Clickable = .{ .queue = &app.queue }; const element = clickable.element(); + var button: App.Button(.accept) = .init(&app.queue, .init(.default, "Button")); + var quit_text: QuitText = .{}; var container = try App.Container.init(allocator, .{ @@ -98,6 +100,7 @@ pub fn main() !void { defer container.deinit(); try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element)); + try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element())); try app.start(); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); @@ -110,9 +113,8 @@ pub fn main() !void { // pre event handling switch (event) { .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(), - .click => |button| { - log.info("Clicked with mouse using Button: {s}", .{button}); - }, + .click => |b| log.info("Clicked with mouse using Button: {s}", .{b}), + .accept => log.info("Clicked built-in button using the mouse", .{}), .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), else => {}, } @@ -145,4 +147,5 @@ const assert = std.debug.assert; const zterm = @import("zterm"); const App = zterm.App(union(enum) { click: [:0]const u8, + accept, }); diff --git a/src/app.zig b/src/app.zig index 80d214a..271cbcc 100644 --- a/src/app.zig +++ b/src/app.zig @@ -417,8 +417,9 @@ pub fn App(comptime E: type) type { pub const Container = @import("container.zig").Container(Event); pub const Element = element.Element(Event); pub const Alignment = element.Alignment(Event); - pub const Scrollable = element.Scrollable(Event); + pub const Button = element.Button(Event, Queue); pub const Input = element.Input(Event, Queue); + pub const Scrollable = element.Scrollable(Event); pub const Queue = queue.Queue(Event, 256); }; } diff --git a/src/element.zig b/src/element.zig index b47f79f..3c72895 100644 --- a/src/element.zig +++ b/src/element.zig @@ -574,6 +574,84 @@ pub fn Input(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { return input_struct.input_fn; } +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 + const button_struct = struct { + pub fn button_fn(accept_event: meta.FieldEnum(Event)) type { + { // 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 `void` is allowed."; + switch (@typeInfo(@FieldType(Event, @tagName(accept_event)))) { + .void => |_| {}, + else => @compileError(err_msg), + } + } + return struct { + queue: *Queue, + configuration: Configuration, + + /// Configuration for InputField's. + pub const Configuration = struct { + color: Color, + text: []const u8, + + pub fn init(color: Color, text: []const u8) @This() { + return .{ + .color = color, + .text = text, + }; + } + }; + + pub fn init(queue: *Queue, configuration: Configuration) @This() { + return .{ + .queue = queue, + .configuration = configuration, + }; + } + + 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) { + // TODO should this also support key presses to accept? + .mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) this.queue.push(accept_event), + 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)); + + // NOTE center text in the middle of the available cell slice + const row = size.y / 2 -| (this.configuration.text.len / 2); + const col = size.x / 2 -| (this.configuration.text.len / 2); + const anchor = (row * size.x) + col; + + for (0.., this.configuration.text) |idx, cp| { + cells[anchor + idx].style.fg = this.configuration.color; + cells[anchor + idx].style.emphasis = &.{.bold}; + cells[anchor + idx].cp = cp; + + // NOTE do not write over the contents of this `Container`'s `Size` + if (anchor + idx == cells.len - 1) break; + } + } + }; + } + }; + return button_struct.button_fn; +} + const std = @import("std"); const assert = std.debug.assert; const meta = std.meta; @@ -1101,3 +1179,50 @@ test "input element" { else => unreachable, } } + +test "button" { + const allocator = std.testing.allocator; + const event = @import("event.zig"); + const Event = event.mergeTaggedUnions(event.SystemEvent, union(enum) { + accept, + }); + const testing = @import("testing.zig"); + const Queue = @import("queue").Queue(Event, 256); + + const size: Point = .{ + .x = 30, + .y = 20, + }; + var queue: Queue = .{}; + + var container: Container(Event) = try .init(allocator, .{}, .{}); + defer container.deinit(); + + var button: Button(Event, Queue)(.accept) = .init(&queue, .init(.default, "Button")); + const button_container: Container(Event) = try .init(allocator, .{ + .rectangle = .{ .fill = .blue }, + }, button.element()); + try container.append(button_container); + + var renderer: testing.Renderer = .init(allocator, size); + defer renderer.deinit(); + + container.resize(size); + container.reposition(.{}); + try renderer.render(Container(Event), &container); + // try testing.expectEqualCells(.{}, renderer.size, @import("test/element/button.zon"), renderer.screen); + + // test the accepting of the `Element` + try container.handle(.{ + .mouse = .{ + .x = 5, + .y = 3, + .button = .left, + .kind = .release, + }, + }); + try std.testing.expect(switch (queue.pop()) { + .accept => true, + else => false, + }); +}