this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

vxfw: add Button widget

Add the Button widget. Button reacts to mouse presses and the "enter"
key when it has focus

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