neovim / neovim

Vim-fork focused on extensibility and usability
https://neovim.io
Other
79.92k stars 5.49k forks source link

getmousepos() reports wrong when using multigrid #27181

Open fredizzimo opened 5 months ago

fredizzimo commented 5 months ago

Problem

getmousepos() has several problems when multigrid is enabled.

  1. An unfocusable floating window gives a winid of 0
  2. A window border also gives winid 0, but I think I saw slightly different behaviour in the test code, where wincol and winrow becomes wrong instead. But maybe I messed something up there. In neovide we get a winid of 0 though.
  3. screenrow and screencol are window relative instead of screen relative

This causes this issue in Neovide, due to the scrollbar being unfocusable

Steps to reproduce

Run this test adapted from the regular getmousepos test

Code ```lua describe('ui/mouse/input', function() local screen before_each(function() clear() api.nvim_set_option_value('mouse', 'a', {}) api.nvim_set_option_value('list', true, {}) -- NB: this is weird, but mostly irrelevant to the test -- So I didn't bother to change it command('set listchars=eol:$') command('setl listchars=nbsp:x') screen = Screen.new(25, 5) screen:attach() screen:set_default_attr_ids({ [0] = { bold = true, foreground = Screen.colors.Blue }, [1] = { background = Screen.colors.LightGrey }, [2] = { bold = true }, [3] = { foreground = Screen.colors.Blue, background = Screen.colors.LightGrey, bold = true, }, [4] = { reverse = true }, [5] = { bold = true, reverse = true }, [6] = { foreground = Screen.colors.Grey100, background = Screen.colors.Red }, [7] = { bold = true, foreground = Screen.colors.SeaGreen4 }, [8] = { foreground = Screen.colors.Brown }, }) command('set mousemodel=extend') feed('itestingmousesupport and selection') screen:expect([[ testing | mouse | support and selectio^n | {0:~ }| | ]]) end) end) describe('ui/mouse/input/multigird', function() local screen before_each(function() clear() api.nvim_set_option_value('mouse', 'a', {}) api.nvim_set_option_value('list', true, {}) -- NB: this is weird, but mostly irrelevant to the test -- So I didn't bother to change it command('set listchars=eol:$') command('setl listchars=nbsp:x') screen = Screen.new(25, 5) screen:attach({ext_multigrid=true}) screen:set_default_attr_ids({ [0] = { bold = true, foreground = Screen.colors.Blue }, [1] = { background = Screen.colors.LightGrey }, [2] = { bold = true }, [3] = { foreground = Screen.colors.Blue, background = Screen.colors.LightGrey, bold = true, }, [4] = { reverse = true }, [5] = { bold = true, reverse = true }, [6] = { foreground = Screen.colors.Grey100, background = Screen.colors.Red }, [7] = { bold = true, foreground = Screen.colors.SeaGreen4 }, [8] = { foreground = Screen.colors.Brown }, }) command('set mousemodel=extend') feed('itestingmousesupport and selection') screen:expect{grid=[[ ## grid 1 [2:-------------------------]|*4 [3:-------------------------]| ## grid 2 testing | mouse | support and selectio^n | {0:~ }| ## grid 3 | ]], win_viewport={ [2] = {win = 1000, topline = 0, botline = 4, curline = 2, curcol = 20, linecount = 3, sum_scroll_delta = 0}; }} end) it('getmousepos()', function() local winwidth = api.nvim_get_option_value('winwidth', {}) -- Set winwidth=1 so that window sizes don't change. api.nvim_set_option_value('winwidth', 1, {}) command('tabedit') local tabpage = api.nvim_get_current_tabpage() insert('hello') command('vsplit') local opts = { relative = 'editor', width = 12, height = 1, col = 8, row = 1, anchor = 'NW', style = 'minimal', border = 'single', focusable = 1, } local float = api.nvim_open_win(api.nvim_get_current_buf(), false, opts) command('redraw') local screenstate = {grid=[[ ## grid 1 {9: + No Name] }{2: }{10:3}{2:+ o Name] }{9:X}| [5:------------]│[4:------------]|*2 {5:< Name] [+] }{4:< Name] [+] }| [3:-------------------------]| ## grid 2 (hidden) testing | mouse | support and selection | {0:~ }| ## grid 3 | ## grid 4 hello{0:$} | {0:~ }| ## grid 5 hell^o{0:$} | {0:~ }| ## grid 6 ┌────────────┐| │{11:hello }│| └────────────┘| ]], attr_ids={ [0] = {bold = true, foreground = Screen.colors.Blue1}; [1] = {background = Screen.colors.LightGray}; [2] = {bold = true}; [3] = {bold = true, foreground = Screen.colors.Blue1, background = Screen.colors.LightGray}; [4] = {reverse = true}; [5] = {bold = true, reverse = true}; [6] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}; [7] = {bold = true, foreground = Screen.colors.SeaGreen}; [8] = {foreground = Screen.colors.Brown}; [9] = {underline = true, background = Screen.colors.LightGray}; [10] = {bold = true, foreground = Screen.colors.Magenta1}; [11] = {background = Screen.colors.LightMagenta}; }, float_pos={ [6] = {1003, "NW", 1, 1, 8, true, 50}; }, win_viewport={ [2] = {win = 1000, topline = 0, botline = 4, curline = 2, curcol = 20, linecount = 3, sum_scroll_delta = 0}; [4] = {win = 1001, topline = 0, botline = 2, curline = 0, curcol = 4, linecount = 1, sum_scroll_delta = 0}; [5] = {win = 1002, topline = 0, botline = 2, curline = 0, curcol = 4, linecount = 1, sum_scroll_delta = 0}; [6] = {win = 1003, topline = 0, botline = 1, curline = 0, curcol = 4, linecount = 1, sum_scroll_delta = 0}; }, condition=function() eq({ [4] = { win = 1001, startrow = 1, startcol = 13, width = 12, height = 2 }, [5] = { win = 1002, startrow = 1, startcol = 0, width = 12, height = 2 }, }, screen.win_position) end} screen:expect(screenstate) local lines = api.nvim_get_option_value('lines', {}) local columns = api.nvim_get_option_value('columns', {}) local input_mouse = function(button, action, modifier, row, col) local grid = -1 if row == lines - 1 then grid = 3 elseif row >= opts.row and row <= opts.row + opts.height + 1 and col >= opts.col and col <= opts.col + opts.width + 1 then grid = 6 row = row - opts.row col = col - opts.col elseif col >= 12 then grid = 4 row = row - 1 col = col - 13 else grid = 5 row = row - 1 end api.nvim_input_mouse(button, action, modifier, grid, row, col) end -- Test that screenrow and screencol are set properly for all positions. for row = 0, lines - 1 do for col = 0, columns - 1 do -- Skip the X button that would close the tab. if row ~= 0 or col ~= columns - 1 then input_mouse('left', 'press', '', row, col) api.nvim_set_current_tabpage(tabpage) local mousepos = fn.getmousepos() -- FIXME: fix screenrow and screencol, they return window position instead of screen positions --eq(row + 1, mousepos.screenrow) --eq(col + 1, mousepos.screencol) -- All other values should be 0 when clicking on the command line. if row == lines - 1 then eq(0, mousepos.winid) eq(0, mousepos.winrow) eq(0, mousepos.wincol) eq(0, mousepos.line) eq(0, mousepos.column) eq(0, mousepos.coladd) end end end end -- Test that mouse position values are properly set for the floating window -- with a border. 1 is added to the height and width to account for the -- border. -- FIXME: The exact borders don't work --for win_row = 0, opts.height + 1 do for win_row = 0, opts.height do --for win_col = 0, opts.width + 1 do for win_col = 0, opts.width do local row = win_row + opts.row local col = win_col + opts.col input_mouse('left', 'press', '', row, col) local mousepos = fn.getmousepos() eq(float, mousepos.winid) eq(win_row + 1, mousepos.winrow) eq(win_col + 1, mousepos.wincol) local line = 0 local column = 0 local coladd = 0 if win_row > 0 and win_row < opts.height + 1 and win_col > 0 and win_col < opts.width + 1 then -- Because of border, win_row and win_col don't need to be -- incremented by 1. line = math.min(win_row, fn.line('$')) column = math.min(win_col, #fn.getline(line) + 1) coladd = win_col - column end eq(line, mousepos.line) eq(column, mousepos.column) eq(coladd, mousepos.coladd) end end -- Test that mouse position values are properly set for the floating -- window, after removing the border. opts.border = 'none' api.nvim_win_set_config(float, opts) command('redraw') for win_row = 0, opts.height - 1 do for win_col = 0, opts.width - 1 do local row = win_row + opts.row local col = win_col + opts.col input_mouse('left', 'press', '', row, col) local mousepos = fn.getmousepos() eq(float, mousepos.winid) eq(win_row + 1, mousepos.winrow) eq(win_col + 1, mousepos.wincol) local line = math.min(win_row + 1, fn.line('$')) local column = math.min(win_col + 1, #fn.getline(line) + 1) local coladd = win_col + 1 - column eq(line, mousepos.line) eq(column, mousepos.column) eq(coladd, mousepos.coladd) end end -- Test that mouse position values are properly set for ordinary windows. -- Set the float to be unfocusable instead of closing, to additionally test -- that getmousepos() does not consider unfocusable floats. (see discussion -- in PR #14937 for details). opts.focusable = false api.nvim_win_set_config(float, opts) command('redraw') for nr = 1, 2 do for win_row = 0, fn.winheight(nr) - 1 do for win_col = 0, fn.winwidth(nr) - 1 do local row = win_row + fn.win_screenpos(nr)[1] - 1 local col = win_col + fn.win_screenpos(nr)[2] - 1 input_mouse('left', 'press', '', row, col) local mousepos = fn.getmousepos() -- FIXME: Hitting the floating window does not work if col < opts.col or col > opts.col + opts.width + 1 then eq(fn.win_getid(nr), mousepos.winid) eq(win_row + 1, mousepos.winrow) eq(win_col + 1, mousepos.wincol) local line = math.min(win_row + 1, fn.line('$')) local column = math.min(win_col + 1, #fn.getline(line) + 1) local coladd = win_col + 1 - column eq(line, mousepos.line) eq(column, mousepos.column) eq(coladd, mousepos.coladd) end end end end -- Restore state and release mouse. command('tabclose!') api.nvim_set_option_value('winwidth', winwidth, {}) api.nvim_input_mouse('left', 'release', '', 0, 0, 0) end) end) ```

NOTE: I'm not fully sure that everything is completely correct, there might be one or two offset errors in my adapted code, as they are hard to detect when the actual code does not work properly.

Expected behavior

The tests should pass, even when the FIXME parts are fixed

Neovim version (nvim -v)

v0.9.5

Vim (not Nvim) behaves the same?

N/A

Operating system/version

Arch Linux

Terminal name/version

Alacritty/Neovide

$TERM environment variable

alacritty

Installation

Manually built

fredizzimo commented 5 months ago

It just occurred to me that this might not be solvable, due to this

If UI's are supposed to take control of floating windows, then Neovim won't be able to know which underlying window to actually report the mouse position for when the topmost window is unfocusable.

So the only way I can possibly see that this could work would to extend nvim_input_mouse to take both relative and absolute coordinates, so that screenrow and screencol can be reported correctly.

It would also be up to the UI, to ignore unfocusable windows, and always report the first focusable one. And to be consistent with the non-multigrid case, we also need report the window underneath when the border is clicked. But for that we need the information about them

EDIT: cliking a winbar works differently than the borders, the winbar click is reported to the window, while the border click goes to the window below when multigrid is disabled.

justinmk commented 5 months ago

nvim_input_mouse to take both relative and absolute coordinates, so that screenrow and screencol can be reported correctly.

SGTM. Except nvim_input_mouse doesn't have an opt arg :(

zeertzjq commented 5 months ago

And it doesn't really help if absolute coordinates are not mandatory :(

fredizzimo commented 3 months ago

If this gets merged, I think UIs have enough information to decide the target window themselves

We already know if the window is focusable or not, and will furthermore be able to check for border hits to simulate the standard nvim behaviour, and then send nvim_input_mouse to the actual target window.