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