From 0c9e6702f7814bf88ae69baabea009a24f6acda2 Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Fri, 28 Aug 2026 22:34:48 -0700 Subject: [PATCH] v1 --- src/internal/app/app.go | 37 +++++++++++++++++++++++++++++++-- src/internal/uistate/windows.go | 25 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/internal/app/app.go b/src/internal/app/app.go index 719e8cb..5128d4b 100644 --- a/src/internal/app/app.go +++ b/src/internal/app/app.go @@ -71,6 +71,14 @@ type App struct { // policy decides what Alt means; see input.Policy. policy input.Policy + + // mouseBtn remembers which button name ("left", "right", "middle") + // was last pressed, so the corresponding Release event can reference + // the same button. Gio clears the button from e.Buttons before + // delivering the Release event, which makes MouseButtonFor return "" + // and silently drops every release — leaving Nvim stuck in + // mouse-held state. + mouseBtn string } // Options controls how the editor window starts. @@ -354,19 +362,44 @@ func (a *App) onPointer(e pointer.Event) { row := int(e.Position.Y) / a.fonts.Metrics.CellHeight mods := input.ModifierPrefix(a.mods.Modifiers(e.Modifiers)) + // With ext_multigrid, editor content lives on grids 2+ placed via + // win_pos, not on grid 1 (which is just chrome). Hit-test against + // the window placements to find the correct grid and translate to + // grid-relative coordinates so Nvim routes the event properly. + snap := a.state.Snapshot() + grid, gridRow, gridCol := 1, row, col + if g, gr, gc, ok := uistate.HitTest(snap.Windows, row, col); ok { + grid, gridRow, gridCol = g, gr, gc + } + if e.Kind == pointer.Scroll { if action, ok := input.ScrollDirection(e); ok { - a.proc.InputMouse("wheel", action, mods, 1, row, col) + a.proc.InputMouse("wheel", action, mods, grid, gridRow, gridCol) } return } button := input.MouseButtonFor(e) action := input.MouseAction(e.Kind) + + // Gio clears the released button from e.Buttons before delivering + // the Release event, so MouseButtonFor returns "" and the release + // would be silently dropped. Without a release Nvim thinks the + // button is still held, corrupting its mouse state. + if e.Kind == pointer.Press { + a.mouseBtn = button + } + if button == "" && e.Kind == pointer.Release { + button = a.mouseBtn + } + if e.Kind == pointer.Release { + a.mouseBtn = "" + } + if button == "" || action == "" { return } - a.proc.InputMouse(button, action, mods, 1, row, col) + a.proc.InputMouse(button, action, mods, grid, gridRow, gridCol) } // syncSize computes the grid size implied by the window's pixel size and diff --git a/src/internal/uistate/windows.go b/src/internal/uistate/windows.go index d473f49..604e188 100644 --- a/src/internal/uistate/windows.go +++ b/src/internal/uistate/windows.go @@ -62,6 +62,31 @@ func (w *WindowSet) ordered() []Placement { return out } +// HitTest finds the topmost visible grid at the given base-grid cell +// position and returns its grid ID and grid-relative (row, col). When +// ext_multigrid is active, editor content lives on grids 2+ placed via +// win_pos / win_float_pos; sending mouse events to grid 1 is wrong +// because Nvim interprets the coordinates relative to grid 1, which is +// just the chrome area, not the text. The caller should use the returned +// grid and coordinates instead of blindly passing grid=1 to +// nvim_input_mouse. +// +// Windows are checked in reverse z-order (topmost first) so that floats +// and overlays win over underlying splits. If no window contains the +// position, ok is false — the caller can fall back to grid=1. +func HitTest(windows []Placement, row, col int) (grid, gridRow, gridCol int, ok bool) { + // Walk the list backwards: ordered() puts later/higher z-index + // entries at the end, so the topmost visible window is last. + for i := len(windows) - 1; i >= 0; i-- { + p := windows[i] + if row >= p.Row && row < p.Row+p.Height && + col >= p.Col && col < p.Col+p.Width { + return p.GridID, row - p.Row, col - p.Col, true + } + } + return 0, 0, 0, false +} + // applyWinPos handles a normal (non-floating) window placement update: // [grid, win, start_row, start_col, width, height]. func (s *State) applyWinPos(args []interface{}) {