core: handle mouse capture events with link highlighting

Fixes #1416

At a high level, the issue is that when mouse capture is enabled (i.e. in
neovim), "shift" escapes the capture. So "cmd+shift" is equal to "cmd"
which doesn't get sent to the TUI program and so on. For link
highlighting which now requires "cmd" (super) is held, we were sending
"cmd+shift" to the renderer so we weren't checking for links.

So the core of this commit is respecting this scenario and stripping the
shift modifier.

This commit also found that when the mouse wasn't over a link, we were
always checking and highlighting links on line one of the visible
screen. This bug is fixed and should also result in a very slight
performance improvement on rendering in all cases.
This commit is contained in:
Mitchell Hashimoto
2024-02-01 09:13:13 -08:00
parent 61b964b958
commit 6de4533afb
5 changed files with 36 additions and 9 deletions

View File

@ -854,7 +854,7 @@ fn modsChanged(self: *Surface, mods: input.Mods) void {
{ {
self.renderer_state.mutex.lock(); self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock(); defer self.renderer_state.mutex.unlock();
self.renderer_state.mouse.mods = mods; self.renderer_state.mouse.mods = self.mouseModsWithCapture(self.mouse.mods);
} }
self.queueRender() catch |err| { self.queueRender() catch |err| {
@ -2372,6 +2372,9 @@ fn linkAtPos(
break :mouse_pt viewport_point.toScreen(&self.io.terminal.screen); break :mouse_pt viewport_point.toScreen(&self.io.terminal.screen);
}; };
// Get our comparison mods
const mouse_mods = self.mouseModsWithCapture(self.mouse.mods);
// Get the line we're hovering over. // Get the line we're hovering over.
const line = self.io.terminal.screen.getLine(mouse_pt) orelse const line = self.io.terminal.screen.getLine(mouse_pt) orelse
return null; return null;
@ -2382,7 +2385,7 @@ fn linkAtPos(
for (self.config.links) |link| { for (self.config.links) |link| {
switch (link.highlight) { switch (link.highlight) {
.always, .hover => {}, .always, .hover => {},
.always_mods, .hover_mods => |v| if (!v.equal(self.mouse.mods)) continue, .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue,
} }
var it = strmap.searchIterator(link.regex); var it = strmap.searchIterator(link.regex);
@ -2398,6 +2401,25 @@ fn linkAtPos(
return null; return null;
} }
/// This returns the mouse mods to consider for link highlighting or
/// other purposes taking into account when shift is pressed for releasing
/// the mouse from capture.
///
/// The renderer state mutex must be held.
fn mouseModsWithCapture(self: *Surface, mods: input.Mods) input.Mods {
// In any of these scenarios, whatever mods are set (even shift)
// are preserved.
if (self.io.terminal.flags.mouse_event == .none) return mods;
if (!mods.shift) return mods;
if (self.mouseShiftCapture(false)) return mods;
// We have mouse capture, shift set, and we're not allowed to capture
// shift, so we can clear shift.
var final = mods;
final.shift = false;
return final;
}
/// Attempt to invoke the action of any link that is under the /// Attempt to invoke the action of any link that is under the
/// given position. /// given position.
/// ///

View File

@ -36,6 +36,11 @@ pub const Highlight = union(enum) {
/// hovering or always. For always, all links will be highlighted /// hovering or always. For always, all links will be highlighted
/// when the mods are pressed regardless of if the mouse is hovering /// when the mods are pressed regardless of if the mouse is hovering
/// over them. /// over them.
///
/// Note that if "shift" is specified here, this will NEVER match in
/// TUI programs that capture mouse events. "Shift" with mouse capture
/// escapes the mouse capture but strips the "shift" so it can't be
/// detected.
always_mods: Mods, always_mods: Mods,
hover_mods: Mods, hover_mods: Mods,
}; };

View File

@ -1551,12 +1551,12 @@ fn rebuildCells(
const arena_alloc = arena.allocator(); const arena_alloc = arena.allocator();
// Create our match set for the links. // Create our match set for the links.
var link_match_set = try self.config.links.matchSet( var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
arena_alloc, arena_alloc,
screen, screen,
mouse.point orelse .{}, mouse_pt,
mouse.mods, mouse.mods,
); ) else .{};
// Determine our x/y range for preedit. We don't want to render anything // Determine our x/y range for preedit. We don't want to render anything
// here because we will render the preedit separately. // here because we will render the preedit separately.

View File

@ -987,12 +987,12 @@ pub fn rebuildCells(
self.gl_cells_written = 0; self.gl_cells_written = 0;
// Create our match set for the links. // Create our match set for the links.
var link_match_set = try self.config.links.matchSet( var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
arena_alloc, arena_alloc,
screen, screen,
mouse.point orelse .{}, mouse_pt,
mouse.mods, mouse.mods,
); ) else .{};
// Determine our x/y range for preedit. We don't want to render anything // Determine our x/y range for preedit. We don't want to render anything
// here because we will render the preedit separately. // here because we will render the preedit separately.

View File

@ -139,7 +139,7 @@ pub const MatchSet = struct {
/// The matches. /// The matches.
/// ///
/// Important: this must be in left-to-right top-to-bottom order. /// Important: this must be in left-to-right top-to-bottom order.
matches: []const terminal.Selection, matches: []const terminal.Selection = &.{},
i: usize = 0, i: usize = 0,
pub fn deinit(self: *MatchSet, alloc: Allocator) void { pub fn deinit(self: *MatchSet, alloc: Allocator) void {