From 6de4533afb0032220188ac072ee38beab5e1033e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 1 Feb 2024 09:13:13 -0800 Subject: [PATCH] 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. --- src/Surface.zig | 26 ++++++++++++++++++++++++-- src/input/Link.zig | 5 +++++ src/renderer/Metal.zig | 6 +++--- src/renderer/OpenGL.zig | 6 +++--- src/renderer/link.zig | 2 +- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index eb0c178ec..f427d9071 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -854,7 +854,7 @@ fn modsChanged(self: *Surface, mods: input.Mods) void { { self.renderer_state.mutex.lock(); 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| { @@ -2372,6 +2372,9 @@ fn linkAtPos( 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. const line = self.io.terminal.screen.getLine(mouse_pt) orelse return null; @@ -2382,7 +2385,7 @@ fn linkAtPos( for (self.config.links) |link| { switch (link.highlight) { .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); @@ -2398,6 +2401,25 @@ fn linkAtPos( 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 /// given position. /// diff --git a/src/input/Link.zig b/src/input/Link.zig index cdd87ef02..79ea0cbed 100644 --- a/src/input/Link.zig +++ b/src/input/Link.zig @@ -36,6 +36,11 @@ pub const Highlight = union(enum) { /// hovering or always. For always, all links will be highlighted /// when the mods are pressed regardless of if the mouse is hovering /// 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, hover_mods: Mods, }; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 4a580ace9..a0fd8ea00 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1551,12 +1551,12 @@ fn rebuildCells( const arena_alloc = arena.allocator(); // 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, screen, - mouse.point orelse .{}, + mouse_pt, mouse.mods, - ); + ) else .{}; // Determine our x/y range for preedit. We don't want to render anything // here because we will render the preedit separately. diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 34a326379..608f4fdcb 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -987,12 +987,12 @@ pub fn rebuildCells( self.gl_cells_written = 0; // 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, screen, - mouse.point orelse .{}, + mouse_pt, mouse.mods, - ); + ) else .{}; // Determine our x/y range for preedit. We don't want to render anything // here because we will render the preedit separately. diff --git a/src/renderer/link.zig b/src/renderer/link.zig index 574d18e56..ee1532463 100644 --- a/src/renderer/link.zig +++ b/src/renderer/link.zig @@ -139,7 +139,7 @@ pub const MatchSet = struct { /// The matches. /// /// Important: this must be in left-to-right top-to-bottom order. - matches: []const terminal.Selection, + matches: []const terminal.Selection = &.{}, i: usize = 0, pub fn deinit(self: *MatchSet, alloc: Allocator) void {