commit a2b92a5efbb4159d15311f015bb270f80de42e3a
parent 379e307148e08dd28cc98ea429ea3e686fd57966
Author: glepnir <glephunter@gmail.com>
Date: Wed, 4 Feb 2026 23:09:50 +0800
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.
Diffstat:
2 files changed, 438 insertions(+), 0 deletions(-)
diff --git 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
@@ -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('<Down>')
+ 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()