mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
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.
This commit is contained in:
@ -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{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user