From edb5864a29ec0d1049c4674afb75c8279301446c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Linse?= Date: Sat, 1 May 2021 13:29:34 +0200 Subject: [PATCH] floats: z-index --- src/nvim/api/private/helpers.c | 10 +++- src/nvim/api/ui_events.in.h | 3 +- src/nvim/api/vim.c | 9 ++++ src/nvim/buffer_defs.h | 2 + src/nvim/grid_defs.h | 14 +++++- src/nvim/message.c | 1 + src/nvim/popupmnu.c | 6 ++- src/nvim/ui_compositor.c | 22 +++----- src/nvim/window.c | 5 +- test/functional/ui/float_spec.lua | 83 +++++++++++++++++++++++++++---- test/functional/ui/screen.lua | 29 +++++++++-- 11 files changed, 148 insertions(+), 36 deletions(-) diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 24ba6110c4..0f7008e150 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -1909,7 +1909,7 @@ bool parse_float_config(Dictionary config, FloatConfig *fconfig, bool reconf, } else if (strequal(key, "height")) { has_height = true; if (val.type == kObjectTypeInteger && val.data.integer > 0) { - fconfig->height= (int)val.data.integer; + fconfig->height = (int)val.data.integer; } else { api_set_error(err, kErrorTypeValidation, "'height' key must be a positive Integer"); @@ -1983,6 +1983,14 @@ bool parse_float_config(Dictionary config, FloatConfig *fconfig, bool reconf, "'focusable' key must be Boolean"); return false; } + } else if (strequal(key, "zindex")) { + if (val.type == kObjectTypeInteger && val.data.integer > 0) { + fconfig->zindex = (int)val.data.integer; + } else { + api_set_error(err, kErrorTypeValidation, + "'zindex' key must be a positive Integer"); + return false; + } } else if (!strcmp(key, "border")) { parse_border_style(val, fconfig, err); if (ERROR_SET(err)) { diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h index e934d5dc92..11e21a88ea 100644 --- a/src/nvim/api/ui_events.in.h +++ b/src/nvim/api/ui_events.in.h @@ -106,7 +106,8 @@ void win_pos(Integer grid, Window win, Integer startrow, Integer startcol, Integer width, Integer height) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; void win_float_pos(Integer grid, Window win, String anchor, Integer anchor_grid, - Float anchor_row, Float anchor_col, Boolean focusable) + Float anchor_row, Float anchor_col, Boolean focusable, + Integer zindex) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; void win_external_pos(Integer grid, Window win) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index e0eebd079f..e9a0b0df2e 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1417,6 +1417,15 @@ void nvim_chan_send(Integer chan, String data, Error *err) /// - `external`: GUI should display the window as an external /// top-level window. Currently accepts no other positioning /// configuration together with this. +/// - `zindex`: Stacking order. floats with higher `zindex` go on top on +/// floats with lower indices. Must be larger than zero. The +/// following screen elements have hard-coded z-indices: +/// - 100: insert completion popupmenu +/// - 200: message scrollback +/// - 250: cmdline completion popupmenu (when wildoptions+=pum) +/// The default value for floats are 50. In general, values below 100 are +/// recommended, unless there is a good reason to overshadow builtin +/// elements. /// - `style`: Configure the appearance of the window. Currently only takes /// one non-empty value: /// - "minimal" Nvim will display the window with many UI options diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index dd24db910e..0c839ba12a 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -1083,6 +1083,7 @@ typedef struct { FloatRelative relative; bool external; bool focusable; + int zindex; WinStyle style; bool border; bool shadow; @@ -1096,6 +1097,7 @@ typedef struct { .row = 0, .col = 0, .anchor = 0, \ .relative = 0, .external = false, \ .focusable = true, \ + .zindex = kZIndexFloatDefault, \ .style = kWinStyleUnused }) // Structure to store last cursor position and topline. Used by check_lnums() diff --git a/src/nvim/grid_defs.h b/src/nvim/grid_defs.h index 3b34af46e4..724363674c 100644 --- a/src/nvim/grid_defs.h +++ b/src/nvim/grid_defs.h @@ -13,6 +13,15 @@ typedef char_u schar_T[(MAX_MCO+1) * 4 + 1]; typedef int sattr_T; +enum { + kZIndexDefaultGrid = 0, + kZIndexFloatDefault = 50, + kZIndexPopupMenu = 100, + kZIndexMessages = 200, + kZIndexCmdlinePopupMenu = 250, +}; + + /// ScreenGrid represents a resizable rectuangular grid displayed by UI clients. /// /// chars[] contains the UTF-8 text that is currently displayed on the grid. @@ -73,6 +82,9 @@ struct ScreenGrid { // whether the grid can be focused with mouse clicks. bool focusable; + // z-index: the order in the stack of grids. + int zindex; + // Below is state owned by the compositor. Should generally not be set/read // outside this module, except for specific compatibilty hacks @@ -96,7 +108,7 @@ struct ScreenGrid { }; #define SCREEN_GRID_INIT { 0, NULL, NULL, NULL, NULL, NULL, 0, 0, false, \ - false, 0, 0, NULL, false, true, \ + false, 0, 0, NULL, false, true, 0, \ 0, 0, 0, 0, 0, false } #endif // NVIM_GRID_DEFS_H diff --git a/src/nvim/message.c b/src/nvim/message.c index a34b895033..ec5dabbbc0 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -165,6 +165,7 @@ void msg_grid_validate(void) // TODO(bfredl): eventually should be set to "invalid". I e all callers // will use the grid including clear to EOS if necessary. grid_alloc(&msg_grid, Rows, Columns, false, true); + msg_grid.zindex = kZIndexMessages; xfree(msg_grid.dirty_col); msg_grid.dirty_col = xcalloc(Rows, sizeof(*msg_grid.dirty_col)); diff --git a/src/nvim/popupmnu.c b/src/nvim/popupmnu.c index 32c9750628..7d452d6797 100644 --- a/src/nvim/popupmnu.c +++ b/src/nvim/popupmnu.c @@ -421,6 +421,10 @@ void pum_redraw(void) } grid_assign_handle(&pum_grid); + + pum_grid.zindex = ((State == CMDLINE) + ? kZIndexCmdlinePopupMenu : kZIndexPopupMenu); + bool moved = ui_comp_put_grid(&pum_grid, pum_row, pum_col-col_off, pum_height, grid_width, false, true); bool invalid_grid = moved || pum_invalid; @@ -439,7 +443,7 @@ void pum_redraw(void) int row_off = pum_above ? pum_height : 0; ui_call_win_float_pos(pum_grid.handle, -1, cstr_to_string(anchor), pum_anchor_grid, pum_row-row_off, pum_col-col_off, - false); + false, pum_grid.zindex); } diff --git a/src/nvim/ui_compositor.c b/src/nvim/ui_compositor.c index c1e4a40ef2..1ec5189795 100644 --- a/src/nvim/ui_compositor.c +++ b/src/nvim/ui_compositor.c @@ -165,22 +165,13 @@ bool ui_comp_put_grid(ScreenGrid *grid, int row, int col, int height, int width, } #endif - // TODO(bfredl): this is pretty ad-hoc, add a proper z-order/priority - // scheme. For now: - // - msg_grid is always on top. - // - pum_grid is on top of all windows but not msg_grid. Except for when - // wildoptions=pum, and completing the cmdline with scrolled messages, - // then the pum has to be drawn over the scrolled messages. size_t insert_at = kv_size(layers); - bool cmd_completion = (grid == &pum_grid && (State & CMDLINE) - && (wop_flags & WOP_PUM)); - if (kv_A(layers, insert_at-1) == &msg_grid && !cmd_completion) { - insert_at--; - } - if (kv_A(layers, insert_at-1) == &pum_grid && (grid != &msg_grid)) { + while (insert_at > 0 && kv_A(layers, insert_at-1)->zindex > grid->zindex) { insert_at--; } + if (curwin && kv_A(layers, insert_at-1) == &curwin->w_grid_alloc + && kv_A(layers, insert_at-1)->zindex == grid->zindex && !on_top) { insert_at--; } @@ -279,12 +270,11 @@ static void ui_comp_grid_cursor_goto(UI *ui, Integer grid_handle, // should configure all grids before entering win_update() if (curgrid != &default_grid) { size_t new_index = kv_size(layers)-1; - if (kv_A(layers, new_index) == &msg_grid) { - new_index--; - } - if (kv_A(layers, new_index) == &pum_grid) { + + while (new_index > 1 && kv_A(layers, new_index)->zindex > curgrid->zindex) { new_index--; } + if (curgrid->comp_index < new_index) { ui_comp_raise_grid(curgrid, new_index); } diff --git a/src/nvim/window.c b/src/nvim/window.c index d4d00c0a71..0b28e8aa76 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -763,10 +763,13 @@ void ui_ext_win_position(win_T *wp) } api_clear_error(&dummy); } + + wp->w_grid_alloc.zindex = wp->w_float_config.zindex; if (ui_has(kUIMultigrid)) { String anchor = cstr_to_string(float_anchor_str[c.anchor]); ui_call_win_float_pos(wp->w_grid_alloc.handle, wp->handle, anchor, - grid->handle, row, col, c.focusable); + grid->handle, row, col, c.focusable, + wp->w_grid_alloc.zindex); } else { // TODO(bfredl): ideally, compositor should work like any multigrid UI // and use standard win_pos events. diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua index 66aaf0c941..fe24db08e6 100644 --- a/test/functional/ui/float_spec.lua +++ b/test/functional/ui/float_spec.lua @@ -1077,8 +1077,8 @@ describe('float window', function() {1: abb }| {13: acc }| ]], float_pos={ - [5] = { { id = 1002 }, "NW", 1, 0, 5, true }, - [6] = { { id = -1 }, "NW", 5, 4, 0, false } + [5] = { { id = 1002 }, "NW", 1, 0, 5, true, 50 }, + [6] = { { id = -1 }, "NW", 5, 4, 0, false, 100 } }, win_viewport={ [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0}; [5] = {win = {id = 1002}, topline = 0, botline = 3, curline = 2, curcol = 3}; @@ -2755,8 +2755,8 @@ describe('float window', function() {1: word }| {1: longtext }| ]], float_pos={ - [4] = {{ id = 1001 }, "NW", 1, 2, 5, true}, - [5] = {{ id = -1 }, "NW", 4, 1, 1, false} + [4] = {{ id = 1001 }, "NW", 1, 2, 5, true, 50}, + [5] = {{ id = -1 }, "NW", 4, 1, 1, false, 100} }} else screen:expect([[ @@ -2842,8 +2842,8 @@ describe('float window', function() {1:yy }| {1:zz }| ]], float_pos={ - [4] = {{ id = 1001 }, "NW", 1, 2, 5, true}, - [5] = {{ id = -1 }, "NW", 2, 1, 0, false} + [4] = {{ id = 1001 }, "NW", 1, 2, 5, true, 50}, + [5] = {{ id = -1 }, "NW", 2, 1, 0, false, 100} }} else screen:expect([[ @@ -3104,7 +3104,7 @@ describe('float window', function() {1:word }| {1:longtext }| ]], float_pos={ - [4] = {{id = -1}, "NW", 2, 1, 0, false}} + [4] = {{id = -1}, "NW", 2, 1, 0, false, 100}} } else screen:expect([[ @@ -3148,8 +3148,8 @@ describe('float window', function() {15:some info }| {15:about item }| ]], float_pos={ - [4] = {{id = -1}, "NW", 2, 1, 0, false}, - [6] = {{id = 1002}, "NW", 2, 1, 12, true}, + [4] = {{id = -1}, "NW", 2, 1, 0, false, 100}, + [6] = {{id = 1002}, "NW", 2, 1, 12, true, 50}, }} else screen:expect([[ @@ -3263,7 +3263,7 @@ describe('float window', function() {1:word }| {1:longtext }| ]], float_pos={ - [4] = {{id = -1}, "NW", 2, 1, 0, false}, + [4] = {{id = -1}, "NW", 2, 1, 0, false, 100}, }} else screen:expect([[ @@ -6296,6 +6296,69 @@ describe('float window', function() ]]} end end) + + it('can use z-index', function() + local buf = meths.create_buf(false,false) + local win1 = meths.open_win(buf, false, {relative='editor', width=20, height=3, row=1, col=5, zindex=30}) + meths.win_set_option(win1, "winhl", "Normal:ErrorMsg,EndOfBuffer:ErrorMsg") + local win2 = meths.open_win(buf, false, {relative='editor', width=20, height=3, row=2, col=6, zindex=50}) + meths.win_set_option(win2, "winhl", "Normal:Search,EndOfBuffer:Search") + local win3 = meths.open_win(buf, false, {relative='editor', width=20, height=3, row=3, col=7, zindex=40}) + meths.win_set_option(win3, "winhl", "Normal:Question,EndOfBuffer:Question") + + if multigrid then + screen:expect{grid=[[ + ## grid 1 + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [3:----------------------------------------]| + ## grid 2 + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + ## grid 3 + | + ## grid 4 + {7: }| + {7:~ }| + {7:~ }| + ## grid 5 + {17: }| + {17:~ }| + {17:~ }| + ## grid 6 + {8: }| + {8:~ }| + {8:~ }| + ]], float_pos={ + [4] = {{id = 1001}, "NW", 1, 1, 5, true, 30}; + [5] = {{id = 1002}, "NW", 1, 2, 6, true, 50}; + [6] = {{id = 1003}, "NW", 1, 3, 7, true, 40}; + }, win_viewport={ + [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [4] = {win = {id = 1001}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [5] = {win = {id = 1002}, topline = 0, botline = 2, curline = 0, curcol = 0}; + [6] = {win = {id = 1003}, topline = 0, botline = 2, curline = 0, curcol = 0}; + }} + else + screen:expect{grid=[[ + ^ | + {0:~ }{7: }{0: }| + {0:~ }{7:~}{17: }{0: }| + {0:~ }{7:~}{17:~ }{8: }{0: }| + {0:~ }{17:~ }{8: }{0: }| + {0:~ }{8:~ }{0: }| + | + ]]} + end + end) end describe('with ext_multigrid', function() diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua index fcf6926433..f73d051857 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -429,6 +429,15 @@ screen:redraw_debug() to show all intermediate screen states. ]]) extstate.win_viewport = nil end + if expected.float_pos then + expected.float_pos = deepcopy(expected.float_pos) + for _, v in pairs(expected.float_pos) do + if not v.external and v[7] == nil then + v[7] = 50 + end + end + end + -- Convert assertion errors into invalid screen state descriptions. for _, k in ipairs(concat_tables(ext_keys, {'mode', 'mouse_enabled'})) do -- Empty states are considered the default and need not be mentioned. @@ -1287,6 +1296,11 @@ function Screen:get_snapshot(attrs, ignore) end local function fmt_ext_state(name, state) + local function remove_all_metatables(item, path) + if path[#path] ~= inspect.METATABLE then + return item + end + end if name == "win_viewport" then local str = "{\n" for k,v in pairs(state) do @@ -1295,13 +1309,18 @@ local function fmt_ext_state(name, state) ..", curcol = "..v.curcol.."};\n") end return str .. "}" + elseif name == "float_pos" then + local str = "{\n" + for k,v in pairs(state) do + str = str.." ["..k.."] = {{id = "..v[1].id.."}" + for i = 2, #v do + str = str..", "..inspect(v[i]) + end + str = str .. "};\n" + end + return str .. "}" else -- TODO(bfredl): improve formatting of more states - local function remove_all_metatables(item, path) - if path[#path] ~= inspect.METATABLE then - return item - end - end return inspect(state,{process=remove_all_metatables}) end end