a modern tui library written in zig
at main 6.6 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 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}