a modern tui library written in zig
1const std = @import("std");
2const vaxis = @import("../main.zig");
3
4const vxfw = @import("vxfw.zig");
5
6const Allocator = std.mem.Allocator;
7
8const Center = @import("Center.zig");
9const Text = @import("Text.zig");
10
11const Button = @This();
12
13// User supplied values
14label: []const u8,
15onClick: *const fn (?*anyopaque, ctx: *vxfw.EventContext) anyerror!void,
16userdata: ?*anyopaque = null,
17
18// Styles
19style: struct {
20 default: vaxis.Style = .{ .reverse = true },
21 mouse_down: vaxis.Style = .{ .fg = .{ .index = 4 }, .reverse = true },
22 hover: vaxis.Style = .{ .fg = .{ .index = 3 }, .reverse = true },
23 focus: vaxis.Style = .{ .fg = .{ .index = 5 }, .reverse = true },
24} = .{},
25
26// State
27mouse_down: bool = false,
28has_mouse: bool = false,
29focused: bool = false,
30
31pub fn widget(self: *Button) vxfw.Widget {
32 return .{
33 .userdata = self,
34 .eventHandler = typeErasedEventHandler,
35 .drawFn = typeErasedDrawFn,
36 };
37}
38
39fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
40 const self: *Button = @ptrCast(@alignCast(ptr));
41 return self.handleEvent(ctx, event);
42}
43
44pub fn handleEvent(self: *Button, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
45 switch (event) {
46 .key_press => |key| {
47 if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
48 return self.doClick(ctx);
49 }
50 },
51 .mouse => |mouse| {
52 if (self.mouse_down and mouse.type == .release) {
53 self.mouse_down = false;
54 return self.doClick(ctx);
55 }
56 if (mouse.type == .press and mouse.button == .left) {
57 self.mouse_down = true;
58 return ctx.consumeAndRedraw();
59 }
60 return ctx.consumeEvent();
61 },
62 .mouse_enter => {
63 // implicit redraw
64 self.has_mouse = true;
65 try ctx.setMouseShape(.pointer);
66 return ctx.consumeAndRedraw();
67 },
68 .mouse_leave => {
69 self.has_mouse = false;
70 self.mouse_down = false;
71 // implicit redraw
72 try ctx.setMouseShape(.default);
73 },
74 .focus_in => {
75 self.focused = true;
76 ctx.redraw = true;
77 },
78 .focus_out => {
79 self.focused = false;
80 ctx.redraw = true;
81 },
82 else => {},
83 }
84}
85
86fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
87 const self: *Button = @ptrCast(@alignCast(ptr));
88 return self.draw(ctx);
89}
90
91pub fn draw(self: *Button, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
92 const style: vaxis.Style = if (self.mouse_down)
93 self.style.mouse_down
94 else if (self.has_mouse)
95 self.style.hover
96 else if (self.focused)
97 self.style.focus
98 else
99 self.style.default;
100
101 const text: Text = .{
102 .style = style,
103 .text = self.label,
104 .text_align = .center,
105 };
106
107 const center: Center = .{ .child = text.widget() };
108 const surf = try center.draw(ctx);
109
110 const button_surf = try vxfw.Surface.initWithChildren(ctx.arena, self.widget(), surf.size, surf.children);
111 @memset(button_surf.buffer, .{ .style = style });
112 return button_surf;
113}
114
115fn doClick(self: *Button, ctx: *vxfw.EventContext) anyerror!void {
116 try self.onClick(self.userdata, ctx);
117 ctx.consume_event = true;
118}
119
120test Button {
121 // Create some object which reacts to a button press
122 const Foo = struct {
123 count: u8,
124
125 fn onClick(ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
126 const foo: *@This() = @ptrCast(@alignCast(ptr));
127 foo.count +|= 1;
128 ctx.consumeAndRedraw();
129 }
130 };
131 var foo: Foo = .{ .count = 0 };
132
133 var button: Button = .{
134 .label = "Test Button",
135 .onClick = Foo.onClick,
136 .userdata = &foo,
137 };
138
139 // Event handlers need a context
140 var ctx: vxfw.EventContext = .{
141 .alloc = std.testing.allocator,
142 .cmds = .empty,
143 };
144 defer ctx.cmds.deinit(ctx.alloc);
145
146 // Get the widget interface
147 const b_widget = button.widget();
148
149 // Create a synthetic mouse event
150 var mouse_event: vaxis.Mouse = .{
151 .col = 0,
152 .row = 0,
153 .mods = .{},
154 .button = .left,
155 .type = .press,
156 };
157 // Send the button a mouse press event
158 try b_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
159
160 // A press alone doesn't trigger onClick
161 try std.testing.expectEqual(0, foo.count);
162
163 // Send the button a mouse release event. The onClick handler is called
164 mouse_event.type = .release;
165 try b_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
166 try std.testing.expectEqual(1, foo.count);
167
168 // Send it another press
169 mouse_event.type = .press;
170 try b_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
171
172 // Now the mouse leaves
173 try b_widget.handleEvent(&ctx, .mouse_leave);
174
175 // Then it comes back. We don't know it but the button was pressed outside of our widget. We
176 // receie the release event
177 mouse_event.type = .release;
178 try b_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
179
180 // But we didn't have the press registered, so we don't call onClick
181 try std.testing.expectEqual(1, foo.count);
182
183 // Now we receive an enter keypress. This also triggers the onClick handler
184 try b_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = vaxis.Key.enter } });
185 try std.testing.expectEqual(2, foo.count);
186
187 // Now we draw the button. Set up our context with some unicode data
188 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
189 defer arena.deinit();
190 vxfw.DrawContext.init(.unicode);
191
192 const draw_ctx: vxfw.DrawContext = .{
193 .arena = arena.allocator(),
194 .min = .{},
195 .max = .{ .width = 13, .height = 3 },
196 .cell_size = .{ .width = 10, .height = 20 },
197 };
198 const surface = try b_widget.draw(draw_ctx);
199
200 // The button should fill the available space.
201 try std.testing.expectEqual(surface.size.width, draw_ctx.max.width.?);
202 try std.testing.expectEqual(surface.size.height, draw_ctx.max.height.?);
203
204 // It should have one child, the label
205 try std.testing.expectEqual(1, surface.children.len);
206
207 // The label should be centered
208 try std.testing.expectEqual(1, surface.children[0].origin.row);
209 try std.testing.expectEqual(1, surface.children[0].origin.col);
210}
211
212test "refAllDecls" {
213 std.testing.refAllDecls(@This());
214}