Skip to content

Commit 9d9d781

Browse files
authored
Mouse drag while clicked should cancel any mouse link actions (#7080)
Fixes #7077 This follows pretty standard behavior across native or popular applications on both platforms macOS and Linux. The basic behavior is that if you do a mouse down event and then drag the mouse beyond the current character, then any mouse up actions are canceled (beyond emiting the event itself). This fixes a specific scenario where you could do the following: 1. Click anywhere (mouse down) 2. Drag over a valid link 3. Press command/control (to activate the link) 4. Release the mouse button (mouse up) 5. The link is triggered Now, step 3 and step 5 do not happen. Links are not even highlighted in this scenario. This matches iTerm2 on macOS which has a similar command-to-activate-links behavior. ## Demo https://github.com/user-attachments/assets/f79767b1-78fd-432b-af46-28194b816747
2 parents f7394c0 + 6d3f97e commit 9d9d781

File tree

1 file changed

+60
-24
lines changed

1 file changed

+60
-24
lines changed

src/Surface.zig

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,49 +1031,84 @@ fn mouseRefreshLinks(
10311031
// If the position is outside our viewport, do nothing
10321032
if (pos.x < 0 or pos.y < 0) return;
10331033

1034+
// Update the last point that we checked for links so we don't
1035+
// recheck if the mouse moves some pixels to the same point.
10341036
self.mouse.link_point = pos_vp;
10351037

1036-
if (try self.linkAtPos(pos)) |link| {
1037-
self.renderer_state.mouse.point = pos_vp;
1038-
self.mouse.over_link = true;
1039-
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
1040-
_ = try self.rt_app.performAction(
1041-
.{ .surface = self },
1042-
.mouse_shape,
1043-
.pointer,
1044-
);
1038+
// We use an arena for everything below to make things easy to clean up.
1039+
// In the case we don't do any allocs this is very cheap to setup
1040+
// (effectively just struct init).
1041+
var arena = ArenaAllocator.init(self.alloc);
1042+
defer arena.deinit();
1043+
const alloc = arena.allocator();
1044+
1045+
// Get our link at the current position. This returns null if there
1046+
// isn't a link OR if we shouldn't be showing links for some reason
1047+
// (see further comments for cases).
1048+
const link_: ?apprt.action.MouseOverLink = link: {
1049+
// If we clicked and our mouse moved cells then we never
1050+
// highlight links until the mouse is unclicked. This follows
1051+
// standard macOS and Linux behavior where a click and drag cancels
1052+
// mouse actions.
1053+
const left_idx = @intFromEnum(input.MouseButton.left);
1054+
if (self.mouse.click_state[left_idx] == .press) click: {
1055+
const pin = self.mouse.left_click_pin orelse break :click;
1056+
const click_pt = self.io.terminal.screen.pages.pointFromPin(
1057+
.viewport,
1058+
pin.*,
1059+
) orelse break :click;
1060+
1061+
if (!click_pt.coord().eql(pos_vp)) {
1062+
log.debug("mouse moved while left click held, ignoring link hover", .{});
1063+
break :link null;
1064+
}
1065+
}
10451066

1067+
const link = (try self.linkAtPos(pos)) orelse break :link null;
10461068
switch (link[0]) {
10471069
.open => {
1048-
const str = try self.io.terminal.screen.selectionString(self.alloc, .{
1070+
const str = try self.io.terminal.screen.selectionString(alloc, .{
10491071
.sel = link[1],
10501072
.trim = false,
10511073
});
1052-
defer self.alloc.free(str);
1053-
_ = try self.rt_app.performAction(
1054-
.{ .surface = self },
1055-
.mouse_over_link,
1056-
.{ .url = str },
1057-
);
1074+
break :link .{ .url = str };
10581075
},
10591076

1060-
._open_osc8 => link: {
1077+
._open_osc8 => {
10611078
// Show the URL in the status bar
10621079
const pin = link[1].start();
10631080
const uri = self.osc8URI(pin) orelse {
10641081
log.warn("failed to get URI for OSC8 hyperlink", .{});
1065-
break :link;
1082+
break :link null;
10661083
};
1067-
_ = try self.rt_app.performAction(
1068-
.{ .surface = self },
1069-
.mouse_over_link,
1070-
.{ .url = uri },
1071-
);
1084+
break :link .{ .url = uri };
10721085
},
10731086
}
1087+
};
10741088

1089+
// If we found a link, setup our internal state and notify the
1090+
// apprt so it can highlight it.
1091+
if (link_) |link| {
1092+
self.renderer_state.mouse.point = pos_vp;
1093+
self.mouse.over_link = true;
1094+
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
1095+
_ = try self.rt_app.performAction(
1096+
.{ .surface = self },
1097+
.mouse_shape,
1098+
.pointer,
1099+
);
1100+
_ = try self.rt_app.performAction(
1101+
.{ .surface = self },
1102+
.mouse_over_link,
1103+
link,
1104+
);
10751105
try self.queueRender();
1076-
} else if (over_link) {
1106+
return;
1107+
}
1108+
1109+
// No link, if we're previously over a link then we need to clear
1110+
// the over-link apprt state.
1111+
if (over_link) {
10771112
_ = try self.rt_app.performAction(
10781113
.{ .surface = self },
10791114
.mouse_shape,
@@ -1085,6 +1120,7 @@ fn mouseRefreshLinks(
10851120
.{ .url = "" },
10861121
);
10871122
try self.queueRender();
1123+
return;
10881124
}
10891125
}
10901126

0 commit comments

Comments
 (0)