From a2b92a5efbb4159d15311f015bb270f80de42e3a Mon Sep 17 00:00:00 2001 From: glepnir Date: Wed, 4 Feb 2026 23:09:50 +0800 Subject: [PATCH] feat(ui): show "empty" cursor behind unfocused floatwin #37624 Problem: There is no indication of when cursor is "behind" an unfocused, floating window. Solution: Set underline cursor to indicate when an unfocused floatwin is over the active cursor. --- src/nvim/ui.c | 27 ++ test/functional/ui/float_spec.lua | 411 ++++++++++++++++++++++++++++++ 2 files changed, 438 insertions(+) diff --git a/src/nvim/ui.c b/src/nvim/ui.c index 44ca7ea6bc..8ae16dd399 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -571,6 +571,17 @@ void ui_flush(void) // by nvim core, not the compositor) win_ui_flush(false); } + + // Show "empty box" (underline style) cursor if behind a floatwin. + if (!(State & MODE_CMDLINE)) { + bool cursor_obscured = ui_cursor_is_behind_floatwin(); + int new_idx = cursor_obscured ? SHAPE_IDX_R : cursor_get_mode_idx(); + if (ui_mode_idx != new_idx) { + ui_mode_idx = new_idx; + pending_mode_update = true; + } + } + if (pending_mode_info_update) { Arena arena = ARENA_EMPTY; Array style = mode_style_array(&arena); @@ -674,6 +685,22 @@ void ui_cursor_shape(void) conceal_check_cursor_line(); } +/// Check if the cursor is behind a floating window (only in compositor mode). +/// @return true if cursor is obscured by a float with higher zindex +static bool ui_cursor_is_behind_floatwin(void) +{ + if (!ui_comp_should_draw()) { + return false; + } + + int crow = curwin->w_winrow + curwin->w_winrow_off + curwin->w_wrow; + int ccol = curwin->w_wincol + curwin->w_wincol_off + + (curwin->w_p_rl ? curwin->w_view_width - curwin->w_wcol - 1 : curwin->w_wcol); + + ScreenGrid *top_grid = ui_comp_get_grid_at_coord(crow, ccol); + return top_grid != &curwin->w_grid_alloc && top_grid != &default_grid; +} + /// Returns true if the given UI extension is enabled. bool ui_has(UIExtension ext) { diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua index a9216270ed..ff2c2b9b73 100644 --- a/test/functional/ui/float_spec.lua +++ b/test/functional/ui/float_spec.lua @@ -11251,6 +11251,417 @@ describe('float window', function() eq('Vim(set):E474: Invalid argument: winborder=+,-,+,|,+,-,+,', pcall_err(command, [[set winborder=+,-,+,\|,+,-,+,]])) eq('Vim(set):E474: Invalid argument: winborder=custom', pcall_err(command, 'set winborder=custom')) end) + + it('cursor shape when the cursor is covered by a floating window', function() + local normal_win = api.nvim_get_current_win() + api.nvim_buf_set_lines(0, 0, -1, true, { 'one', 'two' }) + api.nvim_win_set_cursor(0, { 2, 2 }) + local buf = api.nvim_create_buf(false, false) + api.nvim_buf_set_lines(buf, 0, 0, true, { 'the only line' }) + local win = api.nvim_open_win(buf, false, { relative = 'editor', row = 0, col = 0, height = 2, width = 20 }) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:----------------------------------------]|*6 + [3:----------------------------------------]| + ## grid 2 + one | + tw^o | + {0:~ }|*4 + ## grid 3 + | + ## grid 4 + {1:the only line }| + {1: }| + ]], + win_pos = { + [2] = { height = 6, startcol = 0, startrow = 0, width = 40, win = 1000 }, + }, + float_pos = { + [4] = { 1001, 'NW', 1, 0, 0, true, 50, 1, 0, 0 }, + }, + mode = 'normal', + }) + else + screen:expect { + grid = [[ + {1:the only line } | + {1: ^ } | + {0:~ }|*4 + | + ]], + mode = 'replace', + } + end + api.nvim_buf_set_lines(0, 0, -1, true, { 'one', 'two', 'three' }) + feed('') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:----------------------------------------]|*6 + [3:----------------------------------------]| + ## grid 2 + one | + two | + th^ree | + {0:~ }|*3 + ## grid 3 + | + ## grid 4 + {1:the only line }| + {1: }| + ]], + win_pos = { + [2] = { height = 6, startcol = 0, startrow = 0, width = 40, win = 1000 }, + }, + float_pos = { + [4] = { 1001, 'NW', 1, 0, 0, true, 50, 1, 0, 0 }, + }, + mode = 'normal', + }) + else + screen:expect { mode = 'normal' } + end + -- Cursor shape on a lower z-index floating window + buf = api.nvim_create_buf(false, false) + api.nvim_buf_set_lines(buf, 0, 0, true, { 'highest' }) + local high_win = api.nvim_open_win(buf, false, { relative = 'editor', row = 0, col = 0, height = 2, width = 7, zindex = 150 }) + api.nvim_set_current_win(win) + api.nvim_win_set_cursor(win, { 2, 1 }) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:----------------------------------------]|*6 + [3:----------------------------------------]| + ## grid 2 + one | + two | + three | + {0:~ }|*3 + ## grid 3 + | + ## grid 4 + {1:the only line }| + {1:^ }| + ## grid 5 + {1:highest}| + {1: }| + ]], + win_pos = { + [2] = { height = 6, startcol = 0, startrow = 0, width = 40, win = 1000 }, + }, + float_pos = { + [5] = { 1002, 'NW', 1, 0, 0, true, 150, 2, 0, 0 }, + [4] = { 1001, 'NW', 1, 0, 0, true, 50, 1, 0, 0 }, + }, + mode = 'normal', + }) + else + screen:expect { mode = 'replace' } + end + + api.nvim_set_current_win(high_win) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:----------------------------------------]|*6 + [3:----------------------------------------]| + ## grid 2 + one | + two | + three | + {0:~ }|*3 + ## grid 3 + | + ## grid 4 + {1:the only line }| + {1: }| + ## grid 5 + {1:^highest}| + {1: }| + ]], + win_pos = { + [2] = { height = 6, startcol = 0, startrow = 0, width = 40, win = 1000 }, + }, + float_pos = { + [5] = { 1002, 'NW', 1, 0, 0, true, 150, 2, 0, 0 }, + [4] = { 1001, 'NW', 1, 0, 0, true, 50, 1, 0, 0 }, + }, + mode = 'normal', + }) + else + screen:expect { + grid = [[ + {1:^highesty line } | + {1: } | + three | + {0:~ }|*3 + | + ]], + mode = 'normal', + } + end + + buf = api.nvim_create_buf(false, false) + api.nvim_buf_set_lines(buf, 0, 0, true, { 'another' }) + api.nvim_open_win(buf, true, { relative = 'editor', row = 0, col = 0, height = 2, width = 7, zindex = 160 }) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:----------------------------------------]|*6 + [3:----------------------------------------]| + ## grid 2 + one | + two | + three | + {0:~ }|*3 + ## grid 3 + | + ## grid 4 + {1:the only line }| + {1: }| + ## grid 5 + {1:highest}| + {1: }| + ## grid 6 + {1:^another}| + {1: }| + ]], + win_pos = { + [2] = { height = 6, startcol = 0, startrow = 0, width = 40, win = 1000 }, + }, + float_pos = { + [4] = { 1001, 'NW', 1, 0, 0, true, 50, 1, 0, 0 }, + [5] = { 1002, 'NW', 1, 0, 0, true, 150, 2, 0, 0 }, + [6] = { 1003, 'NW', 1, 0, 0, true, 160, 3, 0, 0 }, + }, + mode = 'normal', + }) + else + screen:expect { + grid = [[ + {1:^anothery line } | + {1: } | + three | + {0:~ }|*3 + | + ]], + mode = 'normal', + } + end + api.nvim_set_current_win(normal_win) + command('only') + screen:try_resize(50, 20) + buf = api.nvim_create_buf(false, false) + api.nvim_buf_set_lines(buf, 0, -1, true, { 'x' }) + local float_win = api.nvim_open_win(buf, true, { + relative = 'editor', + width = 5, + height = 5, + row = 8, + col = 9, + border = 'single', + zindex = 1, + }) + local buf2 = api.nvim_create_buf(false, false) + api.nvim_open_win(buf2, false, { + relative = 'editor', + width = 10, + height = 10, + row = 0, + col = 0, + zindex = 2, + }) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:--------------------------------------------------]|*19 + [3:--------------------------------------------------]| + ## grid 2 + one | + two | + three | + {0:~ }|*16 + ## grid 3 + | + ## grid 7 + {5:┌─────┐}| + {5:│}{1:^x }{5:│}| + {5:│}{2:~ }{5:│}|*4 + {5:└─────┘}| + ## grid 8 + {1: }| + {2:~ }|*9 + ]], + win_pos = { + [2] = { height = 19, startcol = 0, startrow = 0, width = 50, win = 1000 }, + }, + float_pos = { + [7] = { 1004, 'NW', 1, 8, 9, true, 1, 1, 8, 9 }, + [8] = { 1005, 'NW', 1, 0, 0, true, 2, 2, 0, 0 }, + }, + mode = 'normal', + }) + else + screen:expect { + grid = [[ + {1: } | + {2:~ } |*2 + {2:~ }{0: }|*5 + {2:~ }{5:─────┐}{0: }| + {2:~ }{1:^x }{5:│}{0: }| + {0:~ }{5:│}{2:~ }{5:│}{0: }|*4 + {0:~ }{5:└─────┘}{0: }| + {0:~ }|*4 + | + ]], + mode = 'normal', + } + end + -- Move window + api.nvim_win_set_config(float_win, { relative = 'editor', row = 9, col = 8 }) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:--------------------------------------------------]|*19 + [3:--------------------------------------------------]| + ## grid 2 + one | + two | + three | + {0:~ }|*16 + ## grid 3 + | + ## grid 7 + {5:┌─────┐}| + {5:│}{1:^x }{5:│}| + {5:│}{2:~ }{5:│}|*4 + {5:└─────┘}| + ## grid 8 + {1: }| + {2:~ }|*9 + ]], + win_pos = { + [2] = { height = 19, startcol = 0, startrow = 0, width = 50, win = 1000 }, + }, + float_pos = { + [7] = { 1004, 'NW', 1, 9, 8, true, 1, 1, 9, 8 }, + [8] = { 1005, 'NW', 1, 0, 0, true, 2, 2, 0, 0 }, + }, + mode = 'normal', + }) + else + screen:expect { + grid = [[ + {1: } | + {2:~ } |*2 + {2:~ }{0: }|*6 + {2:~ }{5:────┐}{0: }| + {0:~ }{5:│}{1:^x }{5:│}{0: }| + {0:~ }{5:│}{2:~ }{5:│}{0: }|*4 + {0:~ }{5:└─────┘}{0: }| + {0:~ }|*3 + | + ]], + mode = 'normal', + } + end + + -- rightleft + api.nvim_win_set_config(float_win, { relative = 'editor', row = 8, col = 8 }) + command('set rightleft') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:--------------------------------------------------]|*19 + [3:--------------------------------------------------]| + ## grid 2 + one | + two | + three | + {0:~ }|*16 + ## grid 3 + | + ## grid 7 + {5:┌─────┐}| + {5:│}{1: ^x}{5:│}| + {5:│}{2: ~}{5:│}|*4 + {5:└─────┘}| + ## grid 8 + {1: }| + {2:~ }|*9 + ]], + win_pos = { + [2] = { height = 19, startcol = 0, startrow = 0, width = 50, win = 1000 }, + }, + float_pos = { + [7] = { 1004, 'NW', 1, 8, 8, true, 1, 1, 8, 8 }, + [8] = { 1005, 'NW', 1, 0, 0, true, 2, 2, 0, 0 }, + }, + mode = 'normal', + }) + else + screen:expect { + grid = [[ + {1: } | + {2:~ } |*2 + {2:~ }{0: }|*5 + {2:~ }{5:────┐}{0: }| + {2:~ }{1: ^x}{5:│}{0: }| + {0:~ }{5:│}{2: ~}{5:│}{0: }|*4 + {0:~ }{5:└─────┘}{0: }| + {0:~ }|*4 + | + ]], + mode = 'normal', + } + end + + command('set virtualedit=all') + fn.setpos('.', { 0, 1, 1, 4 }) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:--------------------------------------------------]|*19 + [3:--------------------------------------------------]| + ## grid 2 + one | + two | + three | + {0:~ }|*16 + ## grid 3 + | + ## grid 7 + {5:┌─────┐}| + {5:│}{1:^ x}{5:│}| + {5:│}{2: ~}{5:│}|*4 + {5:└─────┘}| + ## grid 8 + {1: }| + {2:~ }|*9 + ]], + win_pos = { + [2] = { height = 19, startcol = 0, startrow = 0, width = 50, win = 1000 }, + }, + float_pos = { + [7] = { 1004, 'NW', 1, 8, 8, true, 1, 1, 8, 8 }, + [8] = { 1005, 'NW', 1, 0, 0, true, 2, 2, 0, 0 }, + }, + mode = 'normal', + }) + else + screen:expect { mode = 'replace' } + end + end) end describe('with ext_multigrid and actual mouse grid', function()