From 1af4bd04f9ad157edbfea30642250e854c5cb5d2 Mon Sep 17 00:00:00 2001 From: Raphael Date: Sun, 6 Nov 2022 18:59:43 +0800 Subject: [PATCH] feat(ui): add support to display a title in the border of a float (#20184) add "title" and "title_pos" keys to win config dict. --- runtime/doc/api.txt | 5 + src/nvim/api/keysets.lua | 2 + src/nvim/api/win_config.c | 118 +++++++++++++++++- src/nvim/buffer_defs.h | 12 ++ src/nvim/decoration.h | 1 - src/nvim/drawscreen.c | 31 +++++ src/nvim/extmark_defs.h | 2 + src/nvim/highlight_defs.h | 2 + src/nvim/highlight_group.c | 1 + src/nvim/window.c | 4 + test/functional/ui/cursor_spec.lua | 2 +- test/functional/ui/float_spec.lua | 194 +++++++++++++++++++++++++++++ 12 files changed, 371 insertions(+), 3 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 8928650a39cd3c..e2a5291f7f94fb 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3068,6 +3068,11 @@ nvim_open_win({buffer}, {enter}, {*config}) *nvim_open_win()* specified by character: [ {"+", "MyCorner"}, {"x", "MyBorder"} ]. + • title: Title (optional) in window border, String or list. + List is [text, highlight] tuples. if is string the default + highlight group is `FloatBorderTitle`. + • title_pos: Title position must set with title option. + value can be of `left` `center` `right` default is left. • noautocmd: If true then no buffer-related autocommand events such as |BufEnter|, |BufLeave| or |BufWinEnter| may fire from calling this function. diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index ea8949bd2c08f4..7e0d39957397df 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -81,6 +81,8 @@ return { "focusable"; "zindex"; "border"; + "title"; + "title_pos"; "style"; "noautocmd"; }; diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index 636b9566cece82..9f15e5a85bc8c6 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -2,14 +2,17 @@ // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com #include +#include #include #include #include +#include "nvim/api/extmark.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" #include "nvim/api/win_config.h" #include "nvim/ascii.h" +#include "nvim/buffer_defs.h" #include "nvim/drawscreen.h" #include "nvim/highlight_group.h" #include "nvim/option.h" @@ -134,6 +137,11 @@ /// By default, `FloatBorder` highlight is used, which links to `WinSeparator` /// when not defined. It could also be specified by character: /// [ {"+", "MyCorner"}, {"x", "MyBorder"} ]. +/// - title: Title (optional) in window border, String or list. +/// List is [text, highlight] tuples. if is string the default +/// highlight group is `FloatBorderTitle`. +/// - title_pos: Title position must set with title option. +/// value can be of `left` `center` `right` default is left. /// - noautocmd: If true then no buffer-related autocommand events such as /// |BufEnter|, |BufLeave| or |BufWinEnter| may fire from /// calling this function. @@ -273,6 +281,21 @@ Dictionary nvim_win_get_config(Window window, Error *err) } } PUT(rv, "border", ARRAY_OBJ(border)); + if (config->title) { + Array titles = ARRAY_DICT_INIT; + VirtText title_datas = config->title_chunks; + for (size_t i = 0; i < title_datas.size; i++) { + Array tuple = ARRAY_DICT_INIT; + ADD(tuple, CSTR_TO_OBJ((const char *)title_datas.items[i].text)); + if (title_datas.items[i].hl_id > 0) { + ADD(tuple, + STRING_OBJ(cstr_to_string((const char *)syn_id2name(title_datas.items[i].hl_id)))); + } + ADD(titles, ARRAY_OBJ(tuple)); + } + PUT(rv, "title", ARRAY_OBJ(titles)); + PUT(rv, "title_pos", INTEGER_OBJ(config->title_pos)); + } } } @@ -330,7 +353,75 @@ static bool parse_float_bufpos(Array bufpos, lpos_T *out) return true; } -static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) +static void parse_border_title(Object title, Object title_pos, FloatConfig *fconfig, Error *err) +{ + if (!parse_title_pos(title_pos, fconfig, err)) { + return; + } + + if (title.type == kObjectTypeString) { + if (title.data.string.size == 0) { + fconfig->title = false; + return; + } + int hl_id = syn_check_group(S_LEN("FloatBorderTitle")); + kv_push(fconfig->title_chunks, ((VirtTextChunk){ .text = xstrdup(title.data.string.data), + .hl_id = hl_id })); + fconfig->title_width = (int)mb_string2cells(title.data.string.data); + fconfig->title = true; + return; + } + + if (title.type != kObjectTypeArray) { + api_set_error(err, kErrorTypeValidation, "title must be string or array"); + return; + } + + if (title.type == kObjectTypeArray && title.data.array.size == 0) { + api_set_error(err, kErrorTypeValidation, "title cannot be an empty array"); + return; + } + + fconfig->title_width = 0; + fconfig->title_chunks = parse_virt_text(title.data.array, err, &fconfig->title_width); + + fconfig->title = true; + return; +} + +static bool parse_title_pos(Object title_pos, FloatConfig *fconfig, Error *err) +{ + if (!HAS_KEY(title_pos)) { + fconfig->title_pos = kAlignLeft; + return true; + } + + if (title_pos.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "title_pos must be string"); + return false; + } + + if (title_pos.data.string.size == 0) { + fconfig->title_pos = kAlignLeft; + return true; + } + + char *pos = title_pos.data.string.data; + + if (strequal(pos, "left")) { + fconfig->title_pos = kAlignLeft; + } else if (strequal(pos, "center")) { + fconfig->title_pos = kAlignCenter; + } else if (strequal(pos, "right")) { + fconfig->title_pos = kAlignRight; + } else { + api_set_error(err, kErrorTypeValidation, "invalid title_pos value"); + return false; + } + return true; +} + +static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) { struct { const char *name; @@ -414,6 +505,8 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) String str = style.data.string; if (str.size == 0 || strequal(str.data, "none")) { fconfig->border = false; + // title does not work with border equal none + fconfig->title = false; return; } for (size_t i = 0; defaults[i].name; i++) { @@ -603,6 +696,29 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig, return false; } + if (HAS_KEY(config->title_pos)) { + if (!HAS_KEY(config->title)) { + api_set_error(err, kErrorTypeException, "title_pos requires title to be set"); + return false; + } + } + + if (HAS_KEY(config->title)) { + // title only work with border + if (!HAS_KEY(config->border) && !fconfig->border) { + api_set_error(err, kErrorTypeException, "title requires border to be set"); + return false; + } + + if (fconfig->title) { + clear_virttext(&fconfig->title_chunks); + } + parse_border_title(config->title, config->title_pos, fconfig, err); + if (ERROR_SET(err)) { + return false; + } + } + if (HAS_KEY(config->border)) { parse_border_style(config->border, fconfig, err); if (ERROR_SET(err)) { diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 4e46a1aef2f4cf..2b4228985846d8 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -44,6 +44,8 @@ typedef struct { #include "klib/kvec.h" // for marktree #include "nvim/marktree.h" +// for float window title +#include "nvim/extmark_defs.h" #define GETFILE_SUCCESS(x) ((x) <= 0) #define MODIFIABLE(buf) (buf->b_p_ma) @@ -1048,6 +1050,12 @@ typedef enum { kWinStyleMinimal, /// Minimal UI: no number column, eob markers, etc } WinStyle; +typedef enum { + kAlignLeft = 0, + kAlignCenter = 1, + kAlignRight = 2, +} AlignTextPos; + typedef struct { Window window; lpos_T bufpos; @@ -1060,10 +1068,14 @@ typedef struct { int zindex; WinStyle style; bool border; + bool title; bool shadow; schar_T border_chars[8]; int border_hl_ids[8]; int border_attr[8]; + AlignTextPos title_pos; + VirtText title_chunks; + int title_width; bool noautocmd; } FloatConfig; diff --git a/src/nvim/decoration.h b/src/nvim/decoration.h index bdbfd72a81ca81..9ba621d7a4a5a5 100644 --- a/src/nvim/decoration.h +++ b/src/nvim/decoration.h @@ -28,7 +28,6 @@ typedef enum { EXTERN const char *const hl_mode_str[] INIT(= { "", "replace", "combine", "blend" }); -typedef kvec_t(VirtTextChunk) VirtText; #define VIRTTEXT_EMPTY ((VirtText)KV_INITIAL_VALUE) typedef kvec_t(struct virt_line { VirtText line; bool left_col; }) VirtLines; diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index adf52ef6e48175..30abcd1a31e952 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -60,11 +60,13 @@ #include #include "nvim/buffer.h" +#include "nvim/buffer_defs.h" #include "nvim/charset.h" #include "nvim/cmdexpand.h" #include "nvim/diff.h" #include "nvim/drawscreen.h" #include "nvim/ex_getln.h" +#include "nvim/extmark_defs.h" #include "nvim/grid.h" #include "nvim/highlight.h" #include "nvim/highlight_group.h" @@ -614,6 +616,20 @@ int update_screen(void) return OK; } +static void win_border_redr_title(win_T *wp, ScreenGrid *grid, int col) +{ + VirtText title_chunks = wp->w_float_config.title_chunks; + + for (size_t i = 0; i < title_chunks.size; i++) { + char *text = title_chunks.items[i].text; + int cell = (int)mb_string2cells(text); + int hl_id = title_chunks.items[i].hl_id; + int attr = hl_id ? syn_id2attr(hl_id) : 0; + grid_puts(grid, text, 0, col, attr); + col += cell; + } +} + static void win_redr_border(win_T *wp) { wp->w_redr_border = false; @@ -634,9 +650,24 @@ static void win_redr_border(win_T *wp) if (adj[3]) { grid_put_schar(grid, 0, 0, chars[0], attrs[0]); } + for (int i = 0; i < icol; i++) { grid_put_schar(grid, 0, i + adj[3], chars[1], attrs[1]); } + + if (wp->w_float_config.title) { + int title_col = 0; + int title_width = wp->w_float_config.title_width; + AlignTextPos title_pos = wp->w_float_config.title_pos; + + if (title_pos == kAlignCenter) { + title_col = (icol - title_width) / 2 + 1; + } else { + title_col = title_pos == kAlignLeft ? 1 : icol - title_width + 1; + } + + win_border_redr_title(wp, grid, title_col); + } if (adj[1]) { grid_put_schar(grid, 0, icol + adj[3], chars[2], attrs[2]); } diff --git a/src/nvim/extmark_defs.h b/src/nvim/extmark_defs.h index 9ef4ec23e501ce..51b60dcf6da41a 100644 --- a/src/nvim/extmark_defs.h +++ b/src/nvim/extmark_defs.h @@ -9,6 +9,8 @@ typedef struct { int hl_id; } VirtTextChunk; +typedef kvec_t(VirtTextChunk) VirtText; + typedef struct undo_object ExtmarkUndoObject; typedef kvec_t(ExtmarkUndoObject) extmark_undo_vec_t; diff --git a/src/nvim/highlight_defs.h b/src/nvim/highlight_defs.h index ffcb0f3f22c66b..d66bcca57fc511 100644 --- a/src/nvim/highlight_defs.h +++ b/src/nvim/highlight_defs.h @@ -114,6 +114,7 @@ typedef enum { HLF_WBR, // Window bars HLF_WBRNC, // Window bars of not-current windows HLF_CU, // Cursor + HLF_BTITLE, // Float Border Title HLF_COUNT, // MUST be the last one } hlf_T; @@ -178,6 +179,7 @@ EXTERN const char *hlf_names[] INIT(= { [HLF_WBR] = "WinBar", [HLF_WBRNC] = "WinBarNC", [HLF_CU] = "Cursor", + [HLF_BTITLE] = "FloatBorderTitle", }); EXTERN int highlight_attr[HLF_COUNT + 1]; // Highl. attr for each context. diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c index 9a09118939d01b..a83f4ea969e21b 100644 --- a/src/nvim/highlight_group.c +++ b/src/nvim/highlight_group.c @@ -131,6 +131,7 @@ static const char *highlight_init_both[] = { "default link MsgSeparator StatusLine", "default link NormalFloat Pmenu", "default link FloatBorder WinSeparator", + "default link FloatBorderTitle Title", "default FloatShadow blend=80 guibg=Black", "default FloatShadowThrough blend=100 guibg=Black", "RedrawDebugNormal cterm=reverse gui=reverse", diff --git a/src/nvim/window.c b/src/nvim/window.c index 10b366ce2346da..1cde433b0a3b13 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "nvim/api/private/helpers.h" #include "nvim/api/vim.h" @@ -5066,6 +5067,9 @@ static void win_free(win_T *wp, tabpage_T *tp) } } + // free the border title text + clear_virttext(&wp->w_float_config.title_chunks); + clear_matches(wp); free_jumplist(wp); diff --git a/test/functional/ui/cursor_spec.lua b/test/functional/ui/cursor_spec.lua index 03cd4bfd06750a..e261f0dfabb6b8 100644 --- a/test/functional/ui/cursor_spec.lua +++ b/test/functional/ui/cursor_spec.lua @@ -215,7 +215,7 @@ describe('ui/cursor', function() m.hl_id = 60 m.attr = {background = Screen.colors.DarkGray} end - if m.id_lm then m.id_lm = 61 end + if m.id_lm then m.id_lm = 62 end end -- Assert the new expectation. diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua index 9ef36cf5ae2218..cb90a902d794db 100644 --- a/test/functional/ui/float_spec.lua +++ b/test/functional/ui/float_spec.lua @@ -1716,6 +1716,200 @@ describe('float window', function() end end) + it('validates title title_pos', function() + local buf = meths.create_buf(false,false) + eq("title requires border to be set", + pcall_err(meths.open_win,buf, false, { + relative='editor', width=9, height=2, row=2, col=5, title='Title', + })) + eq("title_pos requires title to be set", + pcall_err(meths.open_win,buf, false, { + relative='editor', width=9, height=2, row=2, col=5, + border='single', title_pos='left', + })) + end) + + it('border with title', function() + local buf = meths.create_buf(false, false) + meths.buf_set_lines(buf, 0, -1, true, {' halloj! ', + ' BORDAA '}) + local win = meths.open_win(buf, false, { + relative='editor', width=9, height=2, row=2, col=5, border="double", + title = "Left",title_pos = "left", + }) + + 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 5 + {5:╔}{11:Left}{5:═════╗}| + {5:║}{1: halloj! }{5:║}| + {5:║}{1: BORDAA }{5:║}| + {5:╚═════════╝}| + ]], float_pos={ + [5] = { { id = 1002 }, "NW", 1, 2, 5, true } + }, win_viewport={ + [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1}; + [5] = {win = {id = 1002}, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 2}; + }} + else + screen:expect{grid=[[ + ^ | + {0:~ }| + {0:~ }{5:╔}{11:Left}{5:═════╗}{0: }| + {0:~ }{5:║}{1: halloj! }{5:║}{0: }| + {0:~ }{5:║}{1: BORDAA }{5:║}{0: }| + {0:~ }{5:╚═════════╝}{0: }| + | + ]]} + end + + meths.win_set_config(win, {title= "Center",title_pos="center"}) + 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 5 + {5:╔═}{11:Center}{5:══╗}| + {5:║}{1: halloj! }{5:║}| + {5:║}{1: BORDAA }{5:║}| + {5:╚═════════╝}| + ]], float_pos={ + [5] = { { id = 1002 }, "NW", 1, 2, 5, true } + }, win_viewport={ + [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1}; + [5] = {win = {id = 1002}, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 2}; + }} + else + screen:expect{grid=[[ + ^ | + {0:~ }| + {0:~ }{5:╔═}{11:Center}{5:══╗}{0: }| + {0:~ }{5:║}{1: halloj! }{5:║}{0: }| + {0:~ }{5:║}{1: BORDAA }{5:║}{0: }| + {0:~ }{5:╚═════════╝}{0: }| + | + ]]} + end + + meths.win_set_config(win, {title= "Right",title_pos="right"}) + 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 5 + {5:╔════}{11:Right}{5:╗}| + {5:║}{1: halloj! }{5:║}| + {5:║}{1: BORDAA }{5:║}| + {5:╚═════════╝}| + ]], float_pos={ + [5] = { { id = 1002 }, "NW", 1, 2, 5, true } + }, win_viewport={ + [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1}; + [5] = {win = {id = 1002}, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 2}; + }} + else + screen:expect{grid=[[ + ^ | + {0:~ }| + {0:~ }{5:╔════}{11:Right}{5:╗}{0: }| + {0:~ }{5:║}{1: halloj! }{5:║}{0: }| + {0:~ }{5:║}{1: BORDAA }{5:║}{0: }| + {0:~ }{5:╚═════════╝}{0: }| + | + ]]} + end + + meths.win_set_config(win, {title= { {"🦄"},{"BB"}},title_pos="right"}) + 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 5 + {5:╔═════}🦄BB{5:╗}| + {5:║}{1: halloj! }{5:║}| + {5:║}{1: BORDAA }{5:║}| + {5:╚═════════╝}| + ]], float_pos={ + [5] = { { id = 1002 }, "NW", 1, 2, 5, true } + }, win_viewport={ + [2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1}; + [5] = {win = {id = 1002}, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 2}; + }} + else + screen:expect{grid=[[ + ^ | + {0:~ }| + {0:~ }{5:╔═════}🦄BB{5:╗}{0: }| + {0:~ }{5:║}{1: halloj! }{5:║}{0: }| + {0:~ }{5:║}{1: BORDAA }{5:║}{0: }| + {0:~ }{5:╚═════════╝}{0: }| + | + ]]} + end + end) + it('terminates border on edge of viewport when window extends past viewport', function() local buf = meths.create_buf(false, false) meths.open_win(buf, false, {relative='editor', width=40, height=7, row=0, col=0, border="single"})