a modern tui library written in zig
at main 8.8 kB view raw
1const std = @import("std"); 2const vaxis = @import("../main.zig"); 3 4const Allocator = std.mem.Allocator; 5 6const vxfw = @import("vxfw.zig"); 7 8const SplitView = @This(); 9 10lhs: vxfw.Widget, 11rhs: vxfw.Widget, 12constrain: enum { lhs, rhs } = .lhs, 13style: vaxis.Style = .{}, 14/// min width for the constrained side 15min_width: u16 = 0, 16/// max width for the constrained side 17max_width: ?u16 = null, 18/// Target width to draw at 19width: u16, 20 21/// Used to calculate mouse events when our constraint is rhs 22last_max_width: ?u16 = null, 23 24// State 25pressed: bool = false, 26mouse_set: bool = false, 27 28pub fn widget(self: *const SplitView) vxfw.Widget { 29 return .{ 30 .userdata = @constCast(self), 31 .eventHandler = typeErasedEventHandler, 32 .drawFn = typeErasedDrawFn, 33 }; 34} 35 36fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 37 const self: *SplitView = @ptrCast(@alignCast(ptr)); 38 switch (event) { 39 .mouse_leave => { 40 self.pressed = false; 41 return; 42 }, 43 .mouse => {}, 44 else => return, 45 } 46 const mouse = event.mouse; 47 48 const separator_col: u16 = switch (self.constrain) { 49 .lhs => self.width, 50 .rhs => if (self.last_max_width) |max| 51 max -| self.width -| 1 52 else { 53 ctx.redraw = true; 54 return; 55 }, 56 }; 57 58 // If we are on the separator, we always set the mouse shape 59 if (mouse.col == separator_col) { 60 try ctx.setMouseShape(.@"ew-resize"); 61 self.mouse_set = true; 62 // Set pressed state if we are a left click 63 if (mouse.type == .press and mouse.button == .left) { 64 self.pressed = true; 65 } 66 } else if (self.mouse_set) { 67 // If we have set the mouse state and *aren't* over the separator, default the mouse state 68 try ctx.setMouseShape(.default); 69 self.mouse_set = false; 70 } 71 72 // On release, we reset state 73 if (mouse.type == .release) { 74 self.pressed = false; 75 self.mouse_set = false; 76 try ctx.setMouseShape(.default); 77 } 78 79 // If pressed, we always keep the mouse shape and we update the width 80 if (self.pressed) { 81 try ctx.setMouseShape(.@"ew-resize"); 82 switch (self.constrain) { 83 .lhs => { 84 self.width = @max(self.min_width, mouse.col); 85 if (self.max_width) |max| { 86 self.width = @min(self.width, max); 87 } 88 }, 89 .rhs => { 90 const last_max = self.last_max_width orelse return; 91 const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col); 92 self.width = @min(last_max -| self.min_width, last_max -| mouse_col -| 1); 93 if (self.max_width) |max| { 94 self.width = @max(self.width, max); 95 } 96 }, 97 } 98 ctx.consume_event = true; 99 } 100} 101 102fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 103 const self: *SplitView = @ptrCast(@alignCast(ptr)); 104 // Fills entire space 105 const max = ctx.max.size(); 106 // Constrain width to the max 107 self.width = @min(self.width, max.width); 108 self.last_max_width = max.width; 109 110 // The constrained side is equal to the width 111 const constrained_min: vxfw.Size = .{ .width = self.width, .height = max.height }; 112 const constrained_max = vxfw.MaxSize.fromSize(constrained_min); 113 114 const unconstrained_min: vxfw.Size = .{ .width = max.width -| self.width -| 1, .height = max.height }; 115 const unconstrained_max = vxfw.MaxSize.fromSize(unconstrained_min); 116 117 var children = try std.ArrayList(vxfw.SubSurface).initCapacity(ctx.arena, 2); 118 119 switch (self.constrain) { 120 .lhs => { 121 if (constrained_max.width.? > 0 and constrained_max.height.? > 0) { 122 const lhs_ctx = ctx.withConstraints(constrained_min, constrained_max); 123 const lhs_surface = try self.lhs.draw(lhs_ctx); 124 children.appendAssumeCapacity(.{ 125 .surface = lhs_surface, 126 .origin = .{ .row = 0, .col = 0 }, 127 }); 128 } 129 if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) { 130 const rhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max); 131 const rhs_surface = try self.rhs.draw(rhs_ctx); 132 children.appendAssumeCapacity(.{ 133 .surface = rhs_surface, 134 .origin = .{ .row = 0, .col = self.width + 1 }, 135 }); 136 } 137 var surface = try vxfw.Surface.initWithChildren( 138 ctx.arena, 139 self.widget(), 140 max, 141 children.items, 142 ); 143 for (0..max.height) |row| { 144 surface.writeCell(self.width, @intCast(row), .{ 145 .char = .{ .grapheme = "", .width = 1 }, 146 .style = self.style, 147 }); 148 } 149 return surface; 150 }, 151 .rhs => { 152 if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) { 153 const lhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max); 154 const lhs_surface = try self.lhs.draw(lhs_ctx); 155 children.appendAssumeCapacity(.{ 156 .surface = lhs_surface, 157 .origin = .{ .row = 0, .col = 0 }, 158 }); 159 } 160 if (constrained_max.width.? > 0 and constrained_max.height.? > 0) { 161 const rhs_ctx = ctx.withConstraints(constrained_min, constrained_max); 162 const rhs_surface = try self.rhs.draw(rhs_ctx); 163 children.appendAssumeCapacity(.{ 164 .surface = rhs_surface, 165 .origin = .{ .row = 0, .col = unconstrained_max.width.? + 1 }, 166 }); 167 } 168 var surface = try vxfw.Surface.initWithChildren( 169 ctx.arena, 170 self.widget(), 171 max, 172 children.items, 173 ); 174 for (0..max.height) |row| { 175 surface.writeCell(max.width -| self.width -| 1, @intCast(row), .{ 176 .char = .{ .grapheme = "", .width = 1 }, 177 .style = self.style, 178 }); 179 } 180 return surface; 181 }, 182 } 183} 184 185test SplitView { 186 // Boiler plate draw context 187 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 188 defer arena.deinit(); 189 vxfw.DrawContext.init(.unicode); 190 191 const draw_ctx: vxfw.DrawContext = .{ 192 .arena = arena.allocator(), 193 .min = .{}, 194 .max = .{ .width = 16, .height = 16 }, 195 .cell_size = .{ .width = 10, .height = 20 }, 196 }; 197 198 // Create LHS and RHS widgets 199 const lhs: vxfw.Text = .{ .text = "Left hand side" }; 200 const rhs: vxfw.Text = .{ .text = "Right hand side" }; 201 202 var split_view: SplitView = .{ 203 .lhs = lhs.widget(), 204 .rhs = rhs.widget(), 205 .width = 8, 206 }; 207 208 const split_widget = split_view.widget(); 209 { 210 const surface = try split_widget.draw(draw_ctx); 211 // SplitView expands to fill the space 212 try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 16, .height = 16 }), surface.size); 213 // It has two children 214 try std.testing.expectEqual(2, surface.children.len); 215 // The left child should have a width = SplitView.width 216 try std.testing.expectEqual(split_view.width, surface.children[0].surface.size.width); 217 } 218 219 // Send the widget a mouse press on the separator 220 var mouse: vaxis.Mouse = .{ 221 // The separator is at width 222 .col = @intCast(split_view.width), 223 .row = 0, 224 .type = .press, 225 .button = .left, 226 .mods = .{}, 227 }; 228 229 var ctx: vxfw.EventContext = .{ 230 .alloc = arena.allocator(), 231 .cmds = .empty, 232 }; 233 try split_widget.handleEvent(&ctx, .{ .mouse = mouse }); 234 // We should get a command to change the mouse shape 235 try std.testing.expect(ctx.cmds.items[0] == .set_mouse_shape); 236 try std.testing.expect(ctx.redraw); 237 try std.testing.expect(split_view.pressed); 238 239 // If we move the mouse, we should update the width 240 mouse.col = 2; 241 mouse.type = .drag; 242 try split_widget.handleEvent(&ctx, .{ .mouse = mouse }); 243 try std.testing.expect(ctx.redraw); 244 try std.testing.expect(split_view.pressed); 245 const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col); 246 try std.testing.expectEqual(mouse_col, split_view.width); 247} 248 249test "refAllDecls" { 250 std.testing.refAllDecls(@This()); 251}