const std = @import("std"); const pardes = @import("pardes"); const layout = pardes.layout; const config = pardes.config; const Pardes = pardes.Pardes; fn mouse(p: *Pardes, kind: pardes.Mouse.Kind, col: u16) void { p.update(.{ .mouse = .{ .button = .left, .kind = kind, .col = col, .row = pardes.TOPBAR_H } }); } fn tagLayer(p: *Pardes, column: usize) !*pardes.TagLayer { for (&p.surface.tag_layers) |*layer| { if (layer.cols > 0 and layer.kind == .column and layer.id == column) return layer; } return error.MissingColumnTagLayer; } fn headerMouse(layer: *const pardes.TagLayer, display_col: u16, kind: pardes.Mouse.Kind) pardes.Mouse { const x = @as(f32, @floatFromInt(layer.viewport.x)) * 10 + (@as(f32, @floatFromInt(display_col)) + 0.5) * 5; const y = (@as(f32, @floatFromInt(layer.viewport.y)) + 0.5) * 20; return .{ .button = .left, .kind = kind, .col = @intFromFloat(x / 10), .row = @intFromFloat(y / 20), .tag_hit = layer.hitAt(x, y, 10, 20, 5, 10) }; } const Snapshot = struct { weight: u64, serial: u32, panes: [pardes.MAX_PANES]usize = undefined, count: usize, tag: pardes.TagLine, }; fn expectOrder(p: *Pardes, before: []const Snapshot, order: []const usize) !void { try std.testing.expectEqual(order.len, p.ncol); for (order, 0..) |original, column| { const saved = before[original]; try std.testing.expectEqual(saved.weight, p.col_weight[column]); try std.testing.expectEqual(saved.serial, p.col_serial[column]); try std.testing.expectEqual(saved.count, p.col_n[column]); try std.testing.expectEqualSlices(usize, saved.panes[0..saved.count], p.col_panes[column][0..p.col_n[column]]); try std.testing.expectEqualStrings(saved.tag.slice(""), p.col_tags[column].slice("")); } } test "column grip drag moves only its interval and keeps tag caret with the column" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 180, .rows = 30 }); defer p.deinit(); _ = try p.setTestFile("body\n"); p.presentation.enabled = false; p.settings.column_tags = true; p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 }; for (0..3) |_| { try std.testing.expect(p.executeBuiltinLine(p.active, "New")); const id = p.active; try std.testing.expect(layout.splitColumn(p, id, id, false)); } p.active = p.col_panes[1][0]; try std.testing.expect(p.executeBuiltinLine(p.active, "New")); const active = p.active; try std.testing.expectEqual(@as(usize, 2), p.col_n[1]); for ([_]u64{ 2, 3, 5, 7 }, 0..) |weight, column| { p.col_weight[column] = weight; try std.testing.expect(p.col_tags[column].set(([_][]const u8{ "zero column header", "one column header", "two column header", "three column header" })[column])); } p.update(.tick); var frame = std.heap.ArenaAllocator.init(p.gpa); defer frame.deinit(); _ = try p.render(frame.allocator()); var before: [4]Snapshot = undefined; for (&before, 0..) |*saved, column| { saved.* = .{ .weight = p.col_weight[column], .serial = p.col_serial[column], .count = p.col_n[column], .tag = p.col_tags[column] }; @memcpy(saved.panes[0..saved.count], p.col_panes[column][0..saved.count]); } const layer = try tagLayer(p, 1); const stale = headerMouse(layer, 4, .press); p.update(.{ .mouse = headerMouse(layer, 10, .press) }); p.update(.{ .mouse = headerMouse(layer, 10, .release) }); try std.testing.expectEqual(@as(?usize, 1), p.header_column); const caret = p.topbar_col; const anchor = p.header_anchor; const scroll = p.header_scroll; const target = p.col_x[3] + p.col_w[3] - 2; const last_rect = p.rects[p.col_panes[3][0]]; const last_columns = (try tagLayer(p, 3)).cols; mouse(p, .press, p.col_x[1]); try std.testing.expect(p.drag == .column_move); mouse(p, .drag, target); _ = try p.render(frame.allocator()); try std.testing.expectEqual(p.col_w[3] - pardes.TAG_TEXT_INSET - 1, (try tagLayer(p, 3)).viewport.w); try std.testing.expectEqual(last_rect, p.rects[p.col_panes[3][0]]); try std.testing.expectEqual(last_columns, (try tagLayer(p, 3)).cols); try expectOrder(p, &before, &.{ 0, 1, 2, 3 }); mouse(p, .release, target); try expectOrder(p, &before, &.{ 0, 2, 3, 1 }); try std.testing.expectEqual(active, p.active); try std.testing.expectEqual(@as(?usize, 3), p.header_column); try std.testing.expectEqual(caret, p.topbar_col); try std.testing.expectEqual(anchor, p.header_anchor); try std.testing.expectEqual(scroll, p.header_scroll); // The old tag envelope must not edit the new occupant of its old index, // including the interval before hosts request another rendered frame. p.update(.{ .mouse = stale }); try std.testing.expectEqual(@as(?usize, 3), p.header_column); try std.testing.expectEqual(caret, p.topbar_col); _ = try p.render(frame.allocator()); try std.testing.expectEqual(before[1].serial, (try tagLayer(p, 3)).serial); try std.testing.expectEqual(p.col_w[3] - pardes.TAG_TEXT_INSET, (try tagLayer(p, 3)).viewport.w); mouse(p, .press, p.col_x[3]); const back = p.col_x[1] + config.GUTTER; mouse(p, .drag, back); mouse(p, .release, back); try expectOrder(p, &before, &.{ 0, 1, 2, 3 }); try std.testing.expectEqual(@as(?usize, 1), p.header_column); try std.testing.expectEqual(caret, p.topbar_col); try std.testing.expectEqual(active, p.active); // A structural change between press and release must not retarget the // in-flight drag to whichever column now occupies the original index. mouse(p, .press, p.col_x[1]); layout.reorderColumn(p, 1, 2); mouse(p, .release, p.col_x[0] + config.GUTTER); try expectOrder(p, &before, &.{ 1, 0, 2, 3 }); try std.testing.expectEqual(@as(?usize, 0), p.header_column); try std.testing.expect(p.drag == .none); } test "column grip is separate from compact text and the resize seam" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 24 }); defer p.deinit(); _ = try p.setTestFile("body\n"); p.presentation.enabled = false; p.settings.column_tags = true; p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 }; try std.testing.expect(p.executeBuiltinLine(0, "New")); try std.testing.expect(layout.splitColumn(p, p.active, p.active, false)); p.update(.tick); var frame = std.heap.ArenaAllocator.init(p.gpa); defer frame.deinit(); _ = try p.render(frame.allocator()); const layer = try tagLayer(p, 1); try std.testing.expectEqual(p.col_x[1] + pardes.TAG_TEXT_INSET, layer.viewport.x); const surface = &p.surface; for (0..p.ncol) |column| { const pane_id = p.col_panes[column][0]; const rect = p.rects[pane_id]; try std.testing.expectEqual(p.col_x[column], rect.x); try std.testing.expectEqual(rect.x + pardes.TAG_TEXT_INSET, (try tagLayer(p, column)).viewport.x); for (p.surface.tagLayers()) |pane_layer| { if (pane_layer.cols > 0 and pane_layer.kind == .pane and pane_layer.id == pane_id) try std.testing.expectEqual((try tagLayer(p, column)).viewport.x, pane_layer.viewport.x); } const pane_tag_y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y; for (0..config.GUTTER) |offset| { const x = rect.x + @as(u16, @intCast(offset)); try std.testing.expectEqual(surface.at(x, pane_tag_y).style.font_role, surface.at(x, pardes.TOPBAR_H).style.font_role); } try std.testing.expectEqual(pardes.FontRole.tagline, surface.at(rect.x + pardes.TAG_TEXT_INSET, pardes.TOPBAR_H).style.font_role); try std.testing.expectEqual(pardes.FontRole.tagline, surface.at(rect.x + pardes.TAG_TEXT_INSET, pane_tag_y).style.font_role); } const gap_x = p.col_x[1] + config.GUTTER; try std.testing.expectEqualStrings(" ", p.surface.at(gap_x, pardes.TOPBAR_H).grapheme()); mouse(p, .press, gap_x); try std.testing.expect(p.drag != .column_move); try std.testing.expect(!p.header_drag); mouse(p, .release, gap_x); p.update(.{ .mouse = headerMouse(layer, 0, .press) }); try std.testing.expect(p.drag != .column_move); try std.testing.expect(p.header_drag); p.update(.{ .mouse = headerMouse(layer, 0, .release) }); try std.testing.expectEqual(@as(?u16, 0), p.topbar_col); mouse(p, .press, p.col_x[1] + config.GUTTER - 1); try std.testing.expect(p.drag == .column_move); mouse(p, .release, p.col_x[1] + config.GUTTER - 1); const seam = p.col_x[0] + p.col_w[0] - 1; const body_row = p.rects[p.col_panes[0][0]].y + pardes.BOX_H + 1; p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = seam, .row = body_row } }); try std.testing.expect(p.drag == .border_v); p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = seam, .row = body_row } }); } test "column grips stay blank and muted across focus hover and drag" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 20 }); defer p.deinit(); _ = try p.setTestFile("left\n"); p.presentation.enabled = false; p.settings.column_tags = true; p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 }; try std.testing.expect(p.executeBuiltinLine(0, "New")); try std.testing.expect(layout.splitColumn(p, p.active, p.active, false)); var frame = std.heap.ArenaAllocator.init(p.gpa); defer frame.deinit(); for ([_]bool{ false, true }) |focus_tint| { p.settings.focus_tint = focus_tint; for (0..p.ncol) |active_column| { p.active = p.col_panes[active_column][0]; const x = p.col_x[active_column]; p.update(.{ .mouse = .{ .button = .none, .kind = .motion, .col = x, .row = pardes.TOPBAR_H } }); for ([_]bool{ false, true }) |dragging| { if (dragging) mouse(p, .press, x); _ = frame.reset(.retain_capacity); const surface = try p.render(frame.allocator()); const muted: pardes.Color = .{ .rgb = p.chromeTheme().column_box_dim }; for (0..p.ncol) |column| { for (0..config.GUTTER) |offset| { const cell = surface.at(p.col_x[column] + @as(u16, @intCast(offset)), pardes.TOPBAR_H); try std.testing.expectEqualStrings(" ", cell.grapheme()); try std.testing.expectEqual(muted, cell.style.bg); try std.testing.expectEqual(pardes.FontRole.tagline, cell.style.font_role); } try std.testing.expectEqual(p.col_x[column] + pardes.TAG_TEXT_INSET, (try tagLayer(p, column)).viewport.x); } if (dragging) mouse(p, .release, x); } } } }