From df142e08adc80af38d0a92c1e982fce9bc372f2b Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Thu, 7 Dec 2023 18:46:38 -0800 Subject: [PATCH] Selection: fix bottom-right/top-left rectangle selections This fixes an issue where selections from the bottom-right to the top-left (or top-left to bottom-right), in addition to some single-line rectangle selections, were not working. This works by handling situations where only one of the x or y axes in the start or end points may need to be flipped to get the correct top-left or bottom-right of a selection. We call these kinds of orientations "mirrored", like you were looking in a mirror. This also adds a small bit of logic that keeps these kinds of motions in rectangle selection from selecting the character before or after it. This has the current side-effect of anchoring a rectangle selection to the original characters if you change directions during the selection, something I will look at in a later commit. Finally, this also removes rectangle select on double-click. I thought this might be a good idea, but word select in rectangle mode really does not work (the effect seems pretty erratic), and it's not implemented in Kitty either. Fixes #1008. --- src/Surface.zig | 6 +- src/terminal/Selection.zig | 344 ++++++++++++++++++++++++++++++++++++- 2 files changed, 344 insertions(+), 6 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index d8d6c3967..b249df41b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2300,13 +2300,11 @@ fn dragLeftClickDouble( self.setSelection(.{ .start = word_current.start, .end = word_start.end, - .rectangle = ctrlOrSuper(self.mouse.mods) and self.mouse.mods.alt, }); } else { self.setSelection(.{ .start = word_start.start, .end = word_current.end, - .rectangle = ctrlOrSuper(self.mouse.mods) and self.mouse.mods.alt, }); } } @@ -2409,7 +2407,7 @@ fn dragLeftClickSingle( // - Inverse logic for a point after the start. const click_point = self.mouse.left_click_point; const start: terminal.point.ScreenPoint = if (screen_point.before(click_point)) start: { - if (cell_start_xpos >= cell_xboundary) { + if ((ctrlOrSuper(self.mouse.mods) and self.mouse.mods.alt) or cell_start_xpos >= cell_xboundary) { break :start click_point; } else { break :start if (click_point.x > 0) terminal.point.ScreenPoint{ @@ -2421,7 +2419,7 @@ fn dragLeftClickSingle( }; } } else start: { - if (cell_start_xpos < cell_xboundary) { + if ((ctrlOrSuper(self.mouse.mods) and self.mouse.mods.alt) or cell_start_xpos < cell_xboundary) { break :start click_point; } else { break :start if (click_point.x < self.io.terminal.screen.cols - 1) terminal.point.ScreenPoint{ diff --git a/src/terminal/Selection.zig b/src/terminal/Selection.zig index 61c8e4c9f..ea507b571 100644 --- a/src/terminal/Selection.zig +++ b/src/terminal/Selection.zig @@ -156,6 +156,8 @@ pub fn topLeft(self: Selection) ScreenPoint { return switch (self.order()) { .forward => self.start, .reverse => self.end, + .mirrored_forward => .{ .x = self.end.x, .y = self.start.y }, + .mirrored_reverse => .{ .x = self.start.x, .y = self.end.y }, }; } @@ -164,10 +166,15 @@ pub fn bottomRight(self: Selection) ScreenPoint { return switch (self.order()) { .forward => self.end, .reverse => self.start, + .mirrored_forward => .{ .x = self.start.x, .y = self.end.y }, + .mirrored_reverse => .{ .x = self.end.x, .y = self.start.y }, }; } /// Returns the selection in the given order. +/// +/// Note that only forward and reverse are useful desired orders for this +/// function. All other orders act as if forward order was desired. pub fn ordered(self: Selection, desired: Order) Selection { if (self.order() == desired) return self; const tl = self.topLeft(); @@ -175,13 +182,40 @@ pub fn ordered(self: Selection, desired: Order) Selection { return switch (desired) { .forward => .{ .start = tl, .end = br, .rectangle = self.rectangle }, .reverse => .{ .start = br, .end = tl, .rectangle = self.rectangle }, + else => .{ .start = tl, .end = br, .rectangle = self.rectangle }, }; } -/// The order of the selection (whether it is selecting forward or back). -pub const Order = enum { forward, reverse }; +/// The order of the selection: +/// +/// * forward: start(x, y) is before end(x, y) (top-left to bottom-right). +/// * reverse: end(x, y) is before start(x, y) (bottom-right to top-left). +/// * mirrored_[forward|reverse]: special, rectangle selections only (see below). +/// +/// For regular selections, the above also holds for top-right to bottom-left +/// (forward) and bottom-left to top-right (reverse). However, for rectangle +/// selections, both of these selections are *mirrored* as orientation +/// operations only flip the x or y axis, not both. Depending on the y axis +/// direction, this is either mirrored_forward or mirrored_reverse. +/// +pub const Order = enum { forward, reverse, mirrored_forward, mirrored_reverse }; fn order(self: Selection) Order { + if (self.rectangle) { + // Reverse (also handles single-column) + if (self.start.y > self.end.y and self.start.x >= self.end.x) return .reverse; + if (self.start.y >= self.end.y and self.start.x > self.end.x) return .reverse; + + // Mirror, bottom-left to top-right + if (self.start.y > self.end.y and self.start.x < self.end.x) return .mirrored_reverse; + + // Mirror, top-right to bottom-left + if (self.start.y < self.end.y and self.start.x > self.end.x) return .mirrored_forward; + + // Forward + return .forward; + } + if (self.start.y < self.end.y) return .forward; if (self.start.y > self.end.y) return .reverse; if (self.start.x <= self.end.x) return .forward; @@ -409,3 +443,309 @@ test "Selection: within" { try testing.expect(!sel.within(.{ .x = 0, .y = 0 }, .{ .x = 4, .y = 1 })); } } + +test "Selection: order, standard" { + const testing = std.testing; + { + // forward, multi-line + const sel: Selection = .{ + .start = .{ .x = 2, .y = 1 }, + .end = .{ .x = 2, .y = 2 }, + }; + + try testing.expect(sel.order() == .forward); + } + { + // reverse, multi-line + const sel: Selection = .{ + .start = .{ .x = 2, .y = 2 }, + .end = .{ .x = 2, .y = 1 }, + }; + + try testing.expect(sel.order() == .reverse); + } + { + // forward, same-line + const sel: Selection = .{ + .start = .{ .x = 2, .y = 1 }, + .end = .{ .x = 3, .y = 1 }, + }; + + try testing.expect(sel.order() == .forward); + } + { + // forward, single char + const sel: Selection = .{ + .start = .{ .x = 2, .y = 1 }, + .end = .{ .x = 2, .y = 1 }, + }; + + try testing.expect(sel.order() == .forward); + } + { + // reverse, single line + const sel: Selection = .{ + .start = .{ .x = 2, .y = 1 }, + .end = .{ .x = 1, .y = 1 }, + }; + + try testing.expect(sel.order() == .reverse); + } +} + +test "Selection: order, rectangle" { + const testing = std.testing; + // Conventions: + // TL - top left + // BL - bottom left + // TR - top right + // BR - bottom right + { + // forward (TL -> BR) + const sel: Selection = .{ + .start = .{ .x = 1, .y = 1 }, + .end = .{ .x = 2, .y = 2 }, + .rectangle = true, + }; + + try testing.expect(sel.order() == .forward); + } + { + // reverse (BR -> TL) + const sel: Selection = .{ + .start = .{ .x = 2, .y = 2 }, + .end = .{ .x = 1, .y = 1 }, + .rectangle = true, + }; + + try testing.expect(sel.order() == .reverse); + } + { + // mirrored_forward (TR -> BL) + const sel: Selection = .{ + .start = .{ .x = 3, .y = 1 }, + .end = .{ .x = 1, .y = 3 }, + .rectangle = true, + }; + + try testing.expect(sel.order() == .mirrored_forward); + } + { + // mirrored_reverse (BL -> TR) + const sel: Selection = .{ + .start = .{ .x = 1, .y = 3 }, + .end = .{ .x = 3, .y = 1 }, + .rectangle = true, + }; + + try testing.expect(sel.order() == .mirrored_reverse); + } + { + // forward, single line (left -> right ) + const sel: Selection = .{ + .start = .{ .x = 1, .y = 1 }, + .end = .{ .x = 3, .y = 1 }, + .rectangle = true, + }; + + try testing.expect(sel.order() == .forward); + } + { + // reverse, single line (right -> left) + const sel: Selection = .{ + .start = .{ .x = 3, .y = 1 }, + .end = .{ .x = 1, .y = 1 }, + .rectangle = true, + }; + + try testing.expect(sel.order() == .reverse); + } + { + // forward, single column (top -> bottom) + const sel: Selection = .{ + .start = .{ .x = 2, .y = 1 }, + .end = .{ .x = 2, .y = 3 }, + .rectangle = true, + }; + + try testing.expect(sel.order() == .forward); + } + { + // reverse, single column (bottom -> top) + const sel: Selection = .{ + .start = .{ .x = 2, .y = 3 }, + .end = .{ .x = 2, .y = 1 }, + .rectangle = true, + }; + + try testing.expect(sel.order() == .reverse); + } + { + // forward, single cell + const sel: Selection = .{ + .start = .{ .x = 1, .y = 1 }, + .end = .{ .x = 1, .y = 1 }, + .rectangle = true, + }; + + try testing.expect(sel.order() == .forward); + } +} + +test "topLeft" { + const testing = std.testing; + { + // forward + const sel: Selection = .{ + .start = .{ .x = 1, .y = 1 }, + .end = .{ .x = 3, .y = 1 }, + }; + const expected: ScreenPoint = .{ .x = 1, .y = 1 }; + try testing.expectEqual(sel.topLeft(), expected); + } + { + // reverse + const sel: Selection = .{ + .start = .{ .x = 3, .y = 1 }, + .end = .{ .x = 1, .y = 1 }, + }; + const expected: ScreenPoint = .{ .x = 1, .y = 1 }; + try testing.expectEqual(sel.topLeft(), expected); + } + { + // mirrored_forward + const sel: Selection = .{ + .start = .{ .x = 3, .y = 1 }, + .end = .{ .x = 1, .y = 3 }, + .rectangle = true, + }; + const expected: ScreenPoint = .{ .x = 1, .y = 1 }; + try testing.expectEqual(sel.topLeft(), expected); + } + { + // mirrored_reverse + const sel: Selection = .{ + .start = .{ .x = 1, .y = 3 }, + .end = .{ .x = 3, .y = 1 }, + .rectangle = true, + }; + const expected: ScreenPoint = .{ .x = 1, .y = 1 }; + try testing.expectEqual(sel.topLeft(), expected); + } +} + +test "bottomRight" { + const testing = std.testing; + { + // forward + const sel: Selection = .{ + .start = .{ .x = 1, .y = 1 }, + .end = .{ .x = 3, .y = 1 }, + }; + const expected: ScreenPoint = .{ .x = 3, .y = 1 }; + try testing.expectEqual(sel.bottomRight(), expected); + } + { + // reverse + const sel: Selection = .{ + .start = .{ .x = 3, .y = 1 }, + .end = .{ .x = 1, .y = 1 }, + }; + const expected: ScreenPoint = .{ .x = 3, .y = 1 }; + try testing.expectEqual(sel.bottomRight(), expected); + } + { + // mirrored_forward + const sel: Selection = .{ + .start = .{ .x = 3, .y = 1 }, + .end = .{ .x = 1, .y = 3 }, + .rectangle = true, + }; + const expected: ScreenPoint = .{ .x = 3, .y = 3 }; + try testing.expectEqual(sel.bottomRight(), expected); + } + { + // mirrored_reverse + const sel: Selection = .{ + .start = .{ .x = 1, .y = 3 }, + .end = .{ .x = 3, .y = 1 }, + .rectangle = true, + }; + const expected: ScreenPoint = .{ .x = 3, .y = 3 }; + try testing.expectEqual(sel.bottomRight(), expected); + } +} + +test "ordered" { + const testing = std.testing; + { + // forward + const sel: Selection = .{ + .start = .{ .x = 1, .y = 1 }, + .end = .{ .x = 3, .y = 1 }, + }; + const sel_reverse: Selection = .{ + .start = .{ .x = 3, .y = 1 }, + .end = .{ .x = 1, .y = 1 }, + }; + try testing.expectEqual(sel.ordered(.forward), sel); + try testing.expectEqual(sel.ordered(.reverse), sel_reverse); + try testing.expectEqual(sel.ordered(.mirrored_reverse), sel); + } + { + // reverse + const sel: Selection = .{ + .start = .{ .x = 3, .y = 1 }, + .end = .{ .x = 1, .y = 1 }, + }; + const sel_forward: Selection = .{ + .start = .{ .x = 1, .y = 1 }, + .end = .{ .x = 3, .y = 1 }, + }; + try testing.expectEqual(sel.ordered(.forward), sel_forward); + try testing.expectEqual(sel.ordered(.reverse), sel); + try testing.expectEqual(sel.ordered(.mirrored_forward), sel_forward); + } + { + // mirrored_forward + const sel: Selection = .{ + .start = .{ .x = 3, .y = 1 }, + .end = .{ .x = 1, .y = 3 }, + .rectangle = true, + }; + const sel_forward: Selection = .{ + .start = .{ .x = 1, .y = 1 }, + .end = .{ .x = 3, .y = 3 }, + .rectangle = true, + }; + const sel_reverse: Selection = .{ + .start = .{ .x = 3, .y = 3 }, + .end = .{ .x = 1, .y = 1 }, + .rectangle = true, + }; + try testing.expectEqual(sel.ordered(.forward), sel_forward); + try testing.expectEqual(sel.ordered(.reverse), sel_reverse); + try testing.expectEqual(sel.ordered(.mirrored_reverse), sel_forward); + } + { + // mirrored_reverse + const sel: Selection = .{ + .start = .{ .x = 1, .y = 3 }, + .end = .{ .x = 3, .y = 1 }, + .rectangle = true, + }; + const sel_forward: Selection = .{ + .start = .{ .x = 1, .y = 1 }, + .end = .{ .x = 3, .y = 3 }, + .rectangle = true, + }; + const sel_reverse: Selection = .{ + .start = .{ .x = 3, .y = 3 }, + .end = .{ .x = 1, .y = 1 }, + .rectangle = true, + }; + try testing.expectEqual(sel.ordered(.forward), sel_forward); + try testing.expectEqual(sel.ordered(.reverse), sel_reverse); + try testing.expectEqual(sel.ordered(.mirrored_forward), sel_forward); + } +}