From 088e1a924639fe13617b61db57100e271dc1f957 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sun, 13 Jul 2025 21:02:28 +0200 Subject: [PATCH] add(element/radio-button): RadioButton `Element` implementation This can be used to visualize the values of `bool`'s, which is relevant when creating form's based on `struct`'s automatically. --- build.zig | 2 + examples/elements/radio-button.zig | 98 ++++++++++++++++++++++++++++++ src/app.zig | 1 + src/element.zig | 64 +++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 examples/elements/radio-button.zig diff --git a/build.zig b/build.zig index 24d00e5..39860d3 100644 --- a/build.zig +++ b/build.zig @@ -11,6 +11,7 @@ pub fn build(b: *std.Build) void { button, input, progress, + radio_button, scrollable, // layouts: vertical, @@ -62,6 +63,7 @@ pub fn build(b: *std.Build) void { .button => "examples/elements/button.zig", .input => "examples/elements/input.zig", .progress => "examples/elements/progress.zig", + .radio_button => "examples/elements/radio-button.zig", .scrollable => "examples/elements/scrollable.zig", // layouts: .vertical => "examples/layouts/vertical.zig", diff --git a/examples/elements/radio-button.zig b/examples/elements/radio-button.zig new file mode 100644 index 0000000..54c4c6e --- /dev/null +++ b/examples/elements/radio-button.zig @@ -0,0 +1,98 @@ +const QuitText = struct { + const text = "Press ctrl+c to quit."; + + pub fn element(this: *@This()) App.Element { + return .{ .ptr = this, .vtable = &.{ .content = content } }; + } + + fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void { + _ = ctx; + assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); + + const row = 2; + const col = size.x / 2 -| (text.len / 2); + const anchor = (row * size.x) + col; + + for (text, 0..) |cp, idx| { + cells[anchor + idx].style.fg = .white; + cells[anchor + idx].style.bg = .black; + cells[anchor + idx].cp = cp; + + // NOTE do not write over the contents of this `Container`'s `Size` + if (anchor + idx == cells.len - 1) break; + } + } +}; + +pub fn main() !void { + errdefer |err| log.err("Application Error: {any}", .{err}); + + var gpa: std.heap.DebugAllocator(.{}) = .init; + defer if (gpa.deinit() == .leak) log.err("memory leak", .{}); + + const allocator = gpa.allocator(); + + var app: App = .init; + var renderer = zterm.Renderer.Buffered.init(allocator); + defer renderer.deinit(); + + var radiobutton: App.RadioButton = .init(false, .{ + .label = "Test Radio Button", + .style = .squared, + }); + var button: App.Button(.accept) = .init(&app.queue, .init(.default, "Button")); + var quit_text: QuitText = .{}; + + var container = try App.Container.init(allocator, .{ + .rectangle = .{ .fill = .grey }, + .layout = .{ .padding = .all(5) }, + }, quit_text.element()); + defer container.deinit(); + try container.append(try .init(allocator, .{}, radiobutton.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}); + + // event loop + while (true) { + const event = app.nextEvent(); + log.debug("received event: {s}", .{@tagName(event)}); + + // pre event handling + switch (event) { + .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(), + .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 => {}, + } + + container.handle(event) catch |err| app.postEvent(.{ + .err = .{ + .err = err, + .msg = "Container Event handling failed", + }, + }); + + // post event handling + switch (event) { + .quit => break, + else => {}, + } + + container.resize(try renderer.resize()); + container.reposition(.{}); + try renderer.render(@TypeOf(container), &container); + try renderer.flush(); + } +} + +pub const panic = App.panic_handler; +const log = std.log.scoped(.default); + +const std = @import("std"); +const assert = std.debug.assert; +const zterm = @import("zterm"); +const App = zterm.App(union(enum) { + accept, +}); diff --git a/src/app.zig b/src/app.zig index e9c000a..51db452 100644 --- a/src/app.zig +++ b/src/app.zig @@ -416,6 +416,7 @@ pub fn App(comptime E: type) type { pub const Button = element.Button(Event, Queue); pub const Input = element.Input(Event, Queue); pub const Progress = element.Progress(Event, Queue); + pub const RadioButton = element.RadioButton(Event); pub const Scrollable = element.Scrollable(Event); pub const Queue = queue.Queue(Event, 256); }; diff --git a/src/element.zig b/src/element.zig index b575d97..ed803d9 100644 --- a/src/element.zig +++ b/src/element.zig @@ -647,6 +647,70 @@ pub fn Button(Event: type, Queue: type) fn (meta.FieldEnum(Event)) type { return button_struct.button_fn; } +pub fn RadioButton(Event: type) type { + return struct { + configuration: Configuration, + value: bool, + + pub const Configuration = struct { + // TODO support more user control for colors (i.e. background, foreground, checked, unchecked, etc.) + color: Color = .default, + style: enum(u1) { + squared, + rounded, + } = .rounded, + label: []const u8, + }; + + pub fn init(initial_value: bool, configuration: Configuration) @This() { + return .{ + .value = initial_value, + .configuration = configuration, + }; + } + + pub fn element(this: *@This()) Element(Event) { + return .{ + .ptr = this, + .vtable = &.{ + .handle = handle, + .content = content, + }, + }; + } + + fn handle(ctx: *anyopaque, event: Event) !void { + var 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.value = !this.value; + }, + 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)); + + cells[0].cp = switch (this.configuration.style) { + .rounded => if (this.value) '●' else '○', + .squared => if (this.value) '■' else '□', + }; + cells[0].style.fg = this.configuration.color; + + for (2.., this.configuration.label) |idx, cp| { + cells[idx].style.fg = this.configuration.color; + cells[idx].cp = cp; + + // NOTE do not write over the contents of this `Container`'s `Size` + if (idx == cells.len - 1) break; + } + } + }; +} + 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 const progress_struct = struct {