Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions src/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions src/internal/uistate/windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}) {
Expand Down
Loading