this repo has no description
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 self.width = @min(last_max -| self.min_width, last_max -| mouse.col -| 1);
92 if (self.max_width) |max| {
93 self.width = @max(self.width, max);
94 }
95 },
96 }
97 ctx.consume_event = true;
98 }
99}
100
101fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
102 const self: *SplitView = @ptrCast(@alignCast(ptr));
103 // Fills entire space
104 const max = ctx.max.size();
105 // Constrain width to the max
106 self.width = @min(self.width, max.width);
107 self.last_max_width = max.width;
108
109 // The constrained side is equal to the width
110 const constrained_min: vxfw.Size = .{ .width = self.width, .height = max.height };
111 const constrained_max = vxfw.MaxSize.fromSize(constrained_min);
112
113 const unconstrained_min: vxfw.Size = .{ .width = max.width -| self.width -| 1, .height = max.height };
114 const unconstrained_max = vxfw.MaxSize.fromSize(unconstrained_min);
115
116 var children = try std.ArrayList(vxfw.SubSurface).initCapacity(ctx.arena, 2);
117
118 switch (self.constrain) {
119 .lhs => {
120 if (constrained_max.width.? > 0 and constrained_max.height.? > 0) {
121 const lhs_ctx = ctx.withConstraints(constrained_min, constrained_max);
122 const lhs_surface = try self.lhs.draw(lhs_ctx);
123 children.appendAssumeCapacity(.{
124 .surface = lhs_surface,
125 .origin = .{ .row = 0, .col = 0 },
126 });
127 }
128 if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) {
129 const rhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max);
130 const rhs_surface = try self.rhs.draw(rhs_ctx);
131 children.appendAssumeCapacity(.{
132 .surface = rhs_surface,
133 .origin = .{ .row = 0, .col = self.width + 1 },
134 });
135 }
136 var surface = try vxfw.Surface.initWithChildren(
137 ctx.arena,
138 self.widget(),
139 max,
140 children.items,
141 );
142 for (0..max.height) |row| {
143 surface.writeCell(self.width, @intCast(row), .{
144 .char = .{ .grapheme = "│", .width = 1 },
145 .style = self.style,
146 });
147 }
148 return surface;
149 },
150 .rhs => {
151 if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) {
152 const lhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max);
153 const lhs_surface = try self.lhs.draw(lhs_ctx);
154 children.appendAssumeCapacity(.{
155 .surface = lhs_surface,
156 .origin = .{ .row = 0, .col = 0 },
157 });
158 }
159 if (constrained_max.width.? > 0 and constrained_max.height.? > 0) {
160 const rhs_ctx = ctx.withConstraints(constrained_min, constrained_max);
161 const rhs_surface = try self.rhs.draw(rhs_ctx);
162 children.appendAssumeCapacity(.{
163 .surface = rhs_surface,
164 .origin = .{ .row = 0, .col = unconstrained_max.width.? + 1 },
165 });
166 }
167 var surface = try vxfw.Surface.initWithChildren(
168 ctx.arena,
169 self.widget(),
170 max,
171 children.items,
172 );
173 for (0..max.height) |row| {
174 surface.writeCell(max.width -| self.width -| 1, @intCast(row), .{
175 .char = .{ .grapheme = "│", .width = 1 },
176 .style = self.style,
177 });
178 }
179 return surface;
180 },
181 }
182}
183
184test SplitView {
185 // Boiler plate draw context
186 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
187 defer arena.deinit();
188 const ucd = try vaxis.Unicode.init(arena.allocator());
189 vxfw.DrawContext.init(&ucd, .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 = 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 try std.testing.expectEqual(mouse.col, split_view.width);
246}
247
248test "refAllDecls" {
249 std.testing.refAllDecls(@This());
250}