summaryrefslogtreecommitdiff
path: root/test/column_drag.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 16:50:50 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit9ce11bfeff73b96704e521b41cffe4625b02f103 (patch)
tree3aedcf15b6adc9c1ef4d1acff2146b7c352e6ba6 /test/column_drag.zig
parent96725011b43d7bf56ac02e710ea4ef4a76f05351 (diff)
downloadpardes-9ce11bfeff73b96704e521b41cffe4625b02f103.tar.gz
pardes-9ce11bfeff73b96704e521b41cffe4625b02f103.zip
Reorder columns using aligned and distinctly colored tag grips
Diffstat (limited to 'test/column_drag.zig')
-rw-r--r--test/column_drag.zig171
1 files changed, 171 insertions, 0 deletions
diff --git a/test/column_drag.zig b/test/column_drag.zig
new file mode 100644
index 00000000..341c8b70
--- /dev/null
+++ b/test/column_drag.zig
@@ -0,0 +1,171 @@
+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] - config.GUTTER - 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] - config.GUTTER, (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] + config.GUTTER, 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 + config.GUTTER, (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 + config.GUTTER, pardes.TOPBAR_H).style.font_role);
+ try std.testing.expectEqual(pardes.FontRole.tagline, surface.at(rect.x + config.GUTTER, pane_tag_y).style.font_role);
+ }
+ 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 } });
+}