Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253const std = @import("std");const vaxis = @import("../main.zig");
const Allocator = std.mem.Allocator;
const vxfw = @import("vxfw.zig");
const SplitView = @This();
lhs: vxfw.Widget,rhs: vxfw.Widget,constrain: enum { lhs, rhs } = .lhs,style: vaxis.Style = .{},/// min width for the constrained sidemin_width: u16 = 0,/// max width for the constrained sidemax_width: ?u16 = null,/// Target width to draw atwidth: u16,
/// Used to calculate mouse events when our constraint is rhslast_max_width: ?u16 = null,
// Statepressed: bool = false,mouse_set: bool = false,
pub fn widget(self: *const SplitView) vxfw.Widget { return .{ .userdata = @constCast(self), .eventHandler = typeErasedEventHandler, .drawFn = typeErasedDrawFn, };}
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { const self: *SplitView = @ptrCast(@alignCast(ptr)); switch (event) { .mouse_leave => { self.pressed = false; return; }, .mouse => {}, else => return, } const mouse = event.mouse;
const separator_col: u16 = switch (self.constrain) { .lhs => self.width, .rhs => if (self.last_max_width) |max| max -| self.width -| 1 else { ctx.redraw = true; return; }, };
// If we are on the separator, we always set the mouse shape if (mouse.col == separator_col) { try ctx.setMouseShape(.@"ew-resize"); self.mouse_set = true; // Set pressed state if we are a left click if (mouse.type == .press and mouse.button == .left) { self.pressed = true; } } else if (self.mouse_set) { // If we have set the mouse state and *aren't* over the separator, default the mouse state try ctx.setMouseShape(.default); self.mouse_set = false; }
// On release, we reset state if (mouse.type == .release) { self.pressed = false; self.mouse_set = false; try ctx.setMouseShape(.default); }
// If pressed, we always keep the mouse shape and we update the width if (self.pressed) { try ctx.setMouseShape(.@"ew-resize"); switch (self.constrain) { .lhs => { self.width = @max(self.min_width, mouse.col); if (self.max_width) |max| { self.width = @min(self.width, max); } }, .rhs => { const last_max = self.last_max_width orelse return; const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col); self.width = @min(last_max -| self.min_width, last_max -| mouse_col -| 1); if (self.max_width) |max| { self.width = @max(self.width, max); } }, } ctx.consume_event = true; }}
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { const self: *SplitView = @ptrCast(@alignCast(ptr)); // Fills entire space const max = ctx.max.size(); // Constrain width to the max self.width = @min(self.width, max.width); self.last_max_width = max.width;
// The constrained side is equal to the width const constrained_min: vxfw.Size = .{ .width = self.width, .height = max.height }; const constrained_max = vxfw.MaxSize.fromSize(constrained_min);
const unconstrained_min: vxfw.Size = .{ .width = max.width -| self.width -| 1, .height = max.height }; const unconstrained_max = vxfw.MaxSize.fromSize(unconstrained_min);
var children = try std.ArrayList(vxfw.SubSurface).initCapacity(ctx.arena, 2);
switch (self.constrain) { .lhs => { if (constrained_max.width.? > 0 and constrained_max.height.? > 0) { const lhs_ctx = ctx.withConstraints(constrained_min, constrained_max); const lhs_surface = try self.lhs.draw(lhs_ctx); children.appendAssumeCapacity(.{ .surface = lhs_surface, .origin = .{ .row = 0, .col = 0 }, }); } if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) { const rhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max); const rhs_surface = try self.rhs.draw(rhs_ctx); children.appendAssumeCapacity(.{ .surface = rhs_surface, .origin = .{ .row = 0, .col = self.width + 1 }, }); } var surface = try vxfw.Surface.initWithChildren( ctx.arena, self.widget(), max, children.items, ); for (0..max.height) |row| { surface.writeCell(self.width, @intCast(row), .{ .char = .{ .grapheme = "│", .width = 1 }, .style = self.style, }); } return surface; }, .rhs => { if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) { const lhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max); const lhs_surface = try self.lhs.draw(lhs_ctx); children.appendAssumeCapacity(.{ .surface = lhs_surface, .origin = .{ .row = 0, .col = 0 }, }); } if (constrained_max.width.? > 0 and constrained_max.height.? > 0) { const rhs_ctx = ctx.withConstraints(constrained_min, constrained_max); const rhs_surface = try self.rhs.draw(rhs_ctx); children.appendAssumeCapacity(.{ .surface = rhs_surface, .origin = .{ .row = 0, .col = unconstrained_max.width.? + 1 }, }); } var surface = try vxfw.Surface.initWithChildren( ctx.arena, self.widget(), max, children.items, ); for (0..max.height) |row| { surface.writeCell(max.width -| self.width -| 1, @intCast(row), .{ .char = .{ .grapheme = "│", .width = 1 }, .style = self.style, }); } return surface; }, }}
test SplitView { // Boiler plate draw context var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); vxfw.DrawContext.init(.unicode);
const draw_ctx: vxfw.DrawContext = .{ .arena = arena.allocator(), .min = .{}, .max = .{ .width = 16, .height = 16 }, .cell_size = .{ .width = 10, .height = 20 }, };
// Create LHS and RHS widgets const lhs: vxfw.Text = .{ .text = "Left hand side" }; const rhs: vxfw.Text = .{ .text = "Right hand side" };
var split_view: SplitView = .{ .lhs = lhs.widget(), .rhs = rhs.widget(), .width = 8, };
const split_widget = split_view.widget(); { const surface = try split_widget.draw(draw_ctx); // SplitView expands to fill the space try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 16, .height = 16 }), surface.size); // It has two children try std.testing.expectEqual(2, surface.children.len); // The left child should have a width = SplitView.width try std.testing.expectEqual(split_view.width, surface.children[0].surface.size.width); }
// Send the widget a mouse press on the separator var mouse: vaxis.Mouse = .{ // The separator is at width .col = @intCast(split_view.width), .row = 0, .type = .press, .button = .left, .mods = .{}, };
var ctx: vxfw.EventContext = .{ .io = std.testing.io, .alloc = arena.allocator(), .cmds = .empty, }; try split_widget.handleEvent(&ctx, .{ .mouse = mouse }); // We should get a command to change the mouse shape try std.testing.expect(ctx.cmds.items[0] == .set_mouse_shape); try std.testing.expect(ctx.redraw); try std.testing.expect(split_view.pressed);
// If we move the mouse, we should update the width mouse.col = 2; mouse.type = .drag; try split_widget.handleEvent(&ctx, .{ .mouse = mouse }); try std.testing.expect(ctx.redraw); try std.testing.expect(split_view.pressed); const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col); try std.testing.expectEqual(mouse_col, split_view.width);}
test "refAllDecls" { std.testing.refAllDecls(@This());}