this repo has no description
13
fork

Configure Feed

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

vxfw: add SplitView widget

+201 -1
+2 -1
src/vxfw/App.zig
··· 63 63 if (!vx.state.in_band_resize) try loop.init(); 64 64 } 65 65 66 - // HACK: Ghostty is reporting incorrect pixel screen size 66 + // NOTE: We don't use pixel mouse anywhere 67 67 vx.caps.sgr_pixels = false; 68 68 try vx.setMouseMode(tty.anyWriter(), true); 69 69 ··· 402 402 } 403 403 404 404 fn deinit(self: *FocusHandler) void { 405 + self.path_to_focused.deinit(); 405 406 self.arena.deinit(); 406 407 } 407 408
+198
src/vxfw/SplitView.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("../main.zig"); 3 + 4 + const Allocator = std.mem.Allocator; 5 + 6 + const vxfw = @import("vxfw.zig"); 7 + 8 + const SplitView = @This(); 9 + 10 + lhs: vxfw.Widget, 11 + rhs: vxfw.Widget, 12 + constrain: enum { lhs, rhs } = .lhs, 13 + style: vaxis.Style = .{}, 14 + /// min width for the constrained side 15 + min_width: u16 = 0, 16 + /// max width for the constrained side 17 + max_width: ?u16 = null, 18 + /// Target width to draw at 19 + width: u16, 20 + 21 + /// Statically allocated children 22 + children: [2]vxfw.SubSurface = undefined, 23 + 24 + // State 25 + pressed: bool = false, 26 + mouse_set: bool = false, 27 + 28 + pub fn widget(self: *const SplitView) vxfw.Widget { 29 + return .{ 30 + .userdata = @constCast(self), 31 + .eventHandler = typeErasedEventHandler, 32 + .drawFn = typeErasedDrawFn, 33 + }; 34 + } 35 + 36 + fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 37 + const self: *SplitView = @ptrCast(@alignCast(ptr)); 38 + if (event != .mouse) return; 39 + const mouse = event.mouse; 40 + 41 + const separator_col: u16 = switch (self.constrain) { 42 + .lhs => self.width + 1, 43 + .rhs => self.width -| 1, 44 + }; 45 + 46 + // If we are on the separator, we always set the mouse shape 47 + if (mouse.col == separator_col) { 48 + try ctx.setMouseShape(.@"ew-resize"); 49 + self.mouse_set = true; 50 + // Set pressed state if we are a left click 51 + if (mouse.type == .press and mouse.button == .left) { 52 + self.pressed = true; 53 + } 54 + } else if (self.mouse_set) { 55 + // If we have set the mouse state and *aren't* over the separator, default the mouse state 56 + try ctx.setMouseShape(.default); 57 + self.mouse_set = false; 58 + } 59 + 60 + // On release, we reset state 61 + if (mouse.type == .release) { 62 + self.pressed = false; 63 + self.mouse_set = false; 64 + try ctx.setMouseShape(.default); 65 + } 66 + 67 + // If pressed, we always keep the mouse shape and we update the width 68 + if (self.pressed) { 69 + try ctx.setMouseShape(.@"ew-resize"); 70 + self.width = @max(self.min_width, mouse.col -| 1); 71 + if (self.max_width) |max| { 72 + self.width = @min(self.width, max); 73 + } 74 + } 75 + } 76 + 77 + fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 78 + const self: *SplitView = @ptrCast(@alignCast(ptr)); 79 + // Fills entire space 80 + const max = ctx.max.size(); 81 + // Constrain width to the max 82 + self.width = @min(self.width, max.width); 83 + 84 + // The constrained side is equal to the width 85 + const constrained_min: vxfw.Size = .{ .width = self.width, .height = max.height }; 86 + const constrained_max: vxfw.MaxSize = .{ .width = self.width, .height = max.height }; 87 + 88 + const unconstrained_min: vxfw.Size = .{ .width = max.width - self.width - 2, .height = max.height }; 89 + const unconstrained_max: vxfw.MaxSize = .{ .width = max.width - self.width - 2, .height = max.height }; 90 + 91 + switch (self.constrain) { 92 + .lhs => { 93 + const lhs_ctx = ctx.withConstraints(constrained_min, constrained_max); 94 + const lhs_surface = try self.lhs.draw(lhs_ctx); 95 + 96 + self.children[0] = .{ 97 + .surface = lhs_surface, 98 + .origin = .{ .row = 0, .col = 0 }, 99 + }; 100 + const rhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max); 101 + const rhs_surface = try self.rhs.draw(rhs_ctx); 102 + self.children[1] = .{ 103 + .surface = rhs_surface, 104 + .origin = .{ .row = 0, .col = self.width + 2 }, 105 + }; 106 + }, 107 + .rhs => { 108 + const lhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max); 109 + const lhs_surface = try self.lhs.draw(lhs_ctx); 110 + self.children[0] = .{ 111 + .surface = lhs_surface, 112 + .origin = .{ .row = 0, .col = 0 }, 113 + }; 114 + const rhs_ctx = ctx.withConstraints(constrained_min, constrained_max); 115 + const rhs_surface = try self.rhs.draw(rhs_ctx); 116 + self.children[1] = .{ 117 + .surface = rhs_surface, 118 + .origin = .{ .row = 0, .col = self.width + 2 }, 119 + }; 120 + }, 121 + } 122 + 123 + var surface = try vxfw.Surface.initWithChildren(ctx.arena, self.widget(), max, &self.children); 124 + surface.handles_mouse = true; 125 + for (0..max.height) |row| { 126 + surface.writeCell(self.width + 1, @intCast(row), .{ 127 + .char = .{ .grapheme = "│", .width = 1 }, 128 + .style = self.style, 129 + }); 130 + } 131 + return surface; 132 + } 133 + 134 + test SplitView { 135 + // Boiler plate draw context 136 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 137 + defer arena.deinit(); 138 + const ucd = try vaxis.Unicode.init(arena.allocator()); 139 + vxfw.DrawContext.init(&ucd, .unicode); 140 + 141 + const draw_ctx: vxfw.DrawContext = .{ 142 + .arena = arena.allocator(), 143 + .min = .{}, 144 + .max = .{ .width = 16, .height = 16 }, 145 + }; 146 + 147 + // Create LHS and RHS widgets 148 + const lhs: vxfw.Text = .{ .text = "Left hand side" }; 149 + const rhs: vxfw.Text = .{ .text = "Right hand side" }; 150 + 151 + var split_view: SplitView = .{ 152 + .lhs = lhs.widget(), 153 + .rhs = rhs.widget(), 154 + .width = 8, 155 + }; 156 + 157 + const split_widget = split_view.widget(); 158 + { 159 + const surface = try split_widget.draw(draw_ctx); 160 + // SplitView expands to fill the space 161 + try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 16, .height = 16 }), surface.size); 162 + // It has two children 163 + try std.testing.expectEqual(2, surface.children.len); 164 + // The left child should have a width = SplitView.width 165 + try std.testing.expectEqual(split_view.width, surface.children[0].surface.size.width); 166 + } 167 + 168 + // Send the widget a mouse press on the separator 169 + var mouse: vaxis.Mouse = .{ 170 + // The separator is width + 1 171 + .col = split_view.width + 1, 172 + .row = 0, 173 + .type = .press, 174 + .button = .left, 175 + .mods = .{}, 176 + }; 177 + 178 + var ctx: vxfw.EventContext = .{ 179 + .cmds = std.ArrayList(vxfw.Command).init(arena.allocator()), 180 + }; 181 + try split_widget.handleEvent(&ctx, .{ .mouse = mouse }); 182 + // We should get a command to change the mouse shape 183 + try std.testing.expect(ctx.cmds.items[0] == .set_mouse_shape); 184 + try std.testing.expect(ctx.redraw); 185 + try std.testing.expect(split_view.pressed); 186 + 187 + // If we move the mouse, we should update the width 188 + mouse.col = 2; 189 + mouse.type = .drag; 190 + try split_widget.handleEvent(&ctx, .{ .mouse = mouse }); 191 + try std.testing.expect(ctx.redraw); 192 + try std.testing.expect(split_view.pressed); 193 + try std.testing.expectEqual(mouse.col - 1, split_view.width); 194 + } 195 + 196 + test "refAllDecls" { 197 + std.testing.refAllDecls(@This()); 198 + }
+1
src/vxfw/vxfw.zig
··· 19 19 pub const Padding = @import("Padding.zig"); 20 20 pub const RichText = @import("RichText.zig"); 21 21 pub const SizedBox = @import("SizedBox.zig"); 22 + pub const SplitView = @import("SplitView.zig"); 22 23 pub const Spinner = @import("Spinner.zig"); 23 24 pub const Text = @import("Text.zig"); 24 25 pub const TextField = @import("TextField.zig");