this repo has no description
13
fork

Configure Feed

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

at main 217 lines 6.8 kB view raw
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}