SelectionGesture: release event

This commit is contained in:
Mitchell Hashimoto
2026-05-26 21:13:46 -07:00
parent 229f4c1f4f
commit 141c7d44d2
2 changed files with 94 additions and 8 deletions

View File

@@ -3815,6 +3815,30 @@ pub fn mouseButtonCallback(
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
// The selection gesture tracks whether a press became a drag by
// comparing the release cell to the original press cell. Resolve the
// release position and pin before notifying the gesture so later
// release handling can query that state.
const release_pos: ?apprt.CursorPos = self.rt_surface.getCursorPos() catch |err| pos: {
log.warn("error reading cursor position for mouse release err={}", .{err});
break :pos null;
};
// If we can't map the release position to a cell, pass null so the
// gesture can conservatively treat the release as having moved away
// from the pressed cell.
const release_pin: ?terminal.Pin = if (release_pos) |pos| pin: {
const release_vp = self.posToViewport(pos.x, pos.y);
break :pin self.io.terminal.screens.active.pages.pin(.{ .viewport = .{
.x = release_vp.x,
.y = release_vp.y,
} });
} else null;
self.mouse.selection_gesture.release(
self.renderer_state.terminal,
.{ .pin = release_pin },
);
// Stop selection scrolling when releasing the left mouse button
// but only when selection scrolling is active.
if (self.selection_scroll_active) {
@@ -3823,7 +3847,6 @@ pub fn mouseButtonCallback(
.locked,
);
}
self.mouse.selection_gesture.left_drag_autoscroll = .none;
// The selection clipboard is only updated for left-click drag when
// the left button is released. This is to avoid the clipboard
@@ -3842,10 +3865,10 @@ pub fn mouseButtonCallback(
// Handle link clicking. We want to do this before we do mouse
// reporting or any other mouse handling because a successfully
// clicked link will swallow the event.
if (self.mouse.over_link) {
if (self.mouse.over_link and !self.mouse.selection_gesture.left_click_dragged) {
// We are holding the renderer lock, but this should just be
// a cached value.
const pos = try self.rt_surface.getCursorPos();
const pos = release_pos orelse try self.rt_surface.getCursorPos();
if (self.processLinks(pos)) |processed| {
if (processed) return true;
} else |err| {
@@ -4139,11 +4162,12 @@ fn maybePromptClick(self: *Surface) !bool {
// prompt clicks because we can't move if we're not in a prompt!
if (!t.cursorIsAtPrompt()) return false;
// If we have a selection currently, then releasing the mouse
// completes the selection and we don't do prompt moving. I don't
// love this logic, I think it should be generalized to "if the
// mouse release was on a different cell than the mouse press" but
// our mouse state at the time of writing this doesn't support that.
// If the left click moved away from its pressed cell then releasing the
// mouse completes the drag gesture and we don't do prompt moving.
if (self.mouse.selection_gesture.left_click_dragged) return false;
// If we have a selection currently, then releasing the mouse completes
// the selection and we don't do prompt moving.
if (screen.selection != null) return false;
// Get the pin for our mouse click.

View File

@@ -32,6 +32,12 @@ left_click_time: ?std.time.Instant,
left_click_xpos: f64,
left_click_ypos: f64,
/// True once the active left-click gesture has moved away from the initially
/// pressed cell. This is reset on every press that starts or continues a
/// multi-click sequence, and is left available for callers to inspect while
/// handling the corresponding release.
left_click_dragged: bool,
/// The current autoscroll state for the active left-click drag gesture.
left_drag_autoscroll: Autoscroll,
@@ -61,6 +67,7 @@ pub const init: SelectionGesture = .{
.left_click_screen_generation = 0,
.left_click_xpos = 0,
.left_click_ypos = 0,
.left_click_dragged = false,
.left_drag_autoscroll = .none,
};
@@ -77,6 +84,7 @@ pub fn deinit(self: *SelectionGesture, t: *Terminal) void {
pub fn reset(self: *SelectionGesture, t: *Terminal) void {
self.left_click_count = 0;
self.left_click_time = null;
self.left_click_dragged = false;
self.left_drag_autoscroll = .none;
self.untrackPin(t);
}
@@ -193,6 +201,7 @@ pub fn drag(
// screen changed out from under us then we aren't actually
// clicking anymore.
const click_pin = self.validatedLeftClickPin(&t.screens) orelse return null;
if (!d.pin.eql(click_pin.*)) self.left_click_dragged = true;
// Determine if we should autoscroll. If our drag position is above
// the top, we go up. If its below the bottom we go down. Easy.
@@ -232,6 +241,34 @@ pub fn drag(
};
}
pub const Release = struct {
/// The cell where the release occurred, if the release position mapped to
/// a valid cell. This is used synchronously to update gesture state and is
/// not tracked.
pin: ?Pin,
};
/// Record a release event for the active left-click gesture.
pub fn release(
self: *SelectionGesture,
t: *Terminal,
r: Release,
) void {
if (self.left_click_count == 0) {
assert(self.left_drag_autoscroll == .none);
return;
}
if (r.pin) |release_pin| {
if (self.validatedLeftClickPin(&t.screens)) |click_pin| {
if (!release_pin.eql(click_pin.*)) self.left_click_dragged = true;
}
} else {
self.left_click_dragged = true;
}
self.left_drag_autoscroll = .none;
}
fn pressInitial(
self: *SelectionGesture,
t: *Terminal,
@@ -256,6 +293,7 @@ fn pressInitial(
self.left_click_xpos = p.xpos;
self.left_click_ypos = p.ypos;
self.left_click_time = p.time;
self.left_click_dragged = false;
self.left_drag_autoscroll = .none;
}
@@ -298,6 +336,7 @@ fn pressRepeat(
}
self.left_click_time = time;
self.left_click_dragged = false;
self.left_drag_autoscroll = .none;
self.left_click_count = @min(
self.left_click_count + 1,
@@ -988,6 +1027,7 @@ test "SelectionGesture press records initial click" {
try testing.expectEqual(time, gesture.left_click_time.?);
try testing.expectEqual(@as(f64, 1), gesture.left_click_xpos);
try testing.expectEqual(@as(f64, 2), gesture.left_click_ypos);
try testing.expectEqual(false, gesture.left_click_dragged);
}
test "SelectionGesture drag returns selection and records autoscroll" {
@@ -1003,6 +1043,7 @@ test "SelectionGesture drag returns selection and records autoscroll" {
const sel = gesture.drag(&t, testDrag(&t, 3, 1, 39, 50)).?;
try testing.expectEqual(.none, gesture.left_drag_autoscroll);
try testing.expectEqual(true, gesture.left_click_dragged);
try testing.expectEqualDeep(Selection.init(
t.screens.active.pages.pin(.{ .active = .{ .x = 1, .y = 1 } }).?,
@@ -1017,6 +1058,27 @@ test "SelectionGesture drag returns selection and records autoscroll" {
try testing.expectEqual(.down, gesture.left_drag_autoscroll);
}
test "SelectionGesture release clears autoscroll and records drag" {
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
defer t.deinit(testing.allocator);
var gesture: SelectionGesture = .init;
defer gesture.deinit(&t);
try gesture.press(&t, testPress(&t, 1, 1, try std.time.Instant.now()));
try testing.expectEqual(false, gesture.left_click_dragged);
_ = gesture.drag(&t, testDrag(&t, 1, 1, 10, 1));
try testing.expectEqual(.up, gesture.left_drag_autoscroll);
try testing.expectEqual(false, gesture.left_click_dragged);
gesture.release(&t, .{
.pin = testPin(&t, 2, 1),
});
try testing.expectEqual(.none, gesture.left_drag_autoscroll);
try testing.expectEqual(true, gesture.left_click_dragged);
}
test "SelectionGesture drag without press returns null" {
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
defer t.deinit(testing.allocator);