feat(float): add winborder option (#31074)

Problem:
There is currently no global option to define the default border style for floating windows. This leads to repetitive code when developers need consistent styling across multiple floating windows.

Solution:
Introduce a global option winborder to specify the default border style for floating windows. When a floating window is created without explicitly specifying a border style, the value of the winborder option will be used. This simplifies configuration and ensures consistency in floating window appearance.

Co-authored-by: Gregory Anders <greg@gpanders.com>
This commit is contained in:
glepnir
2025-03-19 05:05:35 +08:00
committed by GitHub
parent eefd72fff7
commit 62d9fab9af
12 changed files with 340 additions and 62 deletions

View File

@ -3493,28 +3493,20 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()*
`eob` flag of 'fillchars' to a space char, and clearing
the |hl-EndOfBuffer| region in 'winhighlight'.
• border: Style of (optional) window border. This can either
be a string or an array. The string values are
• "none": No border (default).
• "single": A single line box.
• "double": A double line box.
• "rounded": Like "single", but with rounded corners
("╭" etc.).
• "solid": Adds padding by a single whitespace cell.
• "shadow": A drop shadow effect by blending with the
background.
• If it is an array, it should have a length of eight or
any divisor of eight. The array will specify the eight
chars building up the border in a clockwise fashion
starting with the top-left corner. As an example, the
double box style could be specified as: >
[ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ].
be a string or an array. The string values are the same as
those described in 'winborder'. If it is an array, it
should have a length of eight or any divisor of eight. The
array will specify the eight chars building up the border
in a clockwise fashion starting with the top-left corner.
As an example, the double box style could be specified as: >
[ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ].
<
If the number of chars are less than eight, they will be
repeated. Thus an ASCII border could be specified as >
[ "/", "-", \"\\\\\", "|" ],
If the number of chars are less than eight, they will be
repeated. Thus an ASCII border could be specified as >
[ "/", "-", \"\\\\\", "|" ],
<
or all chars the same as >
[ "x" ].
or all chars the same as >
[ "x" ].
<
An empty string can be used to turn off a specific border,
for instance, >

View File

@ -348,6 +348,7 @@ OPTIONS
• 'messagesopt' configures |:messages| and |hit-enter| prompt.
• 'tabclose' controls which tab page to focus when closing a tab page.
• 'eventignorewin' to persistently ignore events in a window.
• 'winborder' sets the default border for |floating-windows|
PERFORMANCE

View File

@ -7150,6 +7150,18 @@ A jump table for the options with a short description can be found at |Q_op|.
UI-dependent. Works best with RGB colors. 'termguicolors'
*'winborder'*
'winborder' string (default "")
global
Defines the default border style of floating windows. The default value
is empty, which is equivalent to "none". Valid values include:
- "none": No border.
- "single": A single line box.
- "double": A double line box.
- "rounded": Like "single", but with rounded corners ("╭" etc.).
- "solid": Adds padding by a single whitespace cell.
- "shadow": A drop shadow effect by blending with the background.
*'window'* *'wi'*
'window' 'wi' number (default screen height - 1)
global

View File

@ -1797,17 +1797,11 @@ function vim.api.nvim_open_term(buffer, opts) end
--- 'fillchars' to a space char, and clearing the
--- `hl-EndOfBuffer` region in 'winhighlight'.
--- - border: Style of (optional) window border. This can either be a string
--- or an array. The string values are
--- - "none": No border (default).
--- - "single": A single line box.
--- - "double": A double line box.
--- - "rounded": Like "single", but with rounded corners ("╭" etc.).
--- - "solid": Adds padding by a single whitespace cell.
--- - "shadow": A drop shadow effect by blending with the background.
--- - If it is an array, it should have a length of eight or any divisor of
--- eight. The array will specify the eight chars building up the border
--- in a clockwise fashion starting with the top-left corner. As an
--- example, the double box style could be specified as:
--- or an array. The string values are the same as those described in 'winborder'.
--- If it is an array, it should have a length of eight or any divisor of
--- eight. The array will specify the eight chars building up the border
--- in a clockwise fashion starting with the top-left corner. As an
--- example, the double box style could be specified as:
--- ```
--- [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ].
--- ```

View File

@ -7832,6 +7832,19 @@ vim.o.winbl = vim.o.winblend
vim.wo.winblend = vim.o.winblend
vim.wo.winbl = vim.wo.winblend
--- Defines the default border style of floating windows. The default value
--- is empty, which is equivalent to "none". Valid values include:
--- - "none": No border.
--- - "single": A single line box.
--- - "double": A double line box.
--- - "rounded": Like "single", but with rounded corners ("╭" etc.).
--- - "solid": Adds padding by a single whitespace cell.
--- - "shadow": A drop shadow effect by blending with the background.
---
--- @type ''|'double'|'single'|'shadow'|'rounded'|'solid'|'none'
vim.o.winborder = ""
vim.go.winborder = vim.o.winborder
--- Window height used for `CTRL-F` and `CTRL-B` when there is only one
--- window and the value is smaller than 'lines' minus one. The screen
--- will scroll 'window' minus two lines, with a minimum of one.

View File

@ -6,17 +6,6 @@ local uv = vim.uv
local M = {}
local default_border = {
{ '', 'NormalFloat' },
{ '', 'NormalFloat' },
{ '', 'NormalFloat' },
{ ' ', 'NormalFloat' },
{ '', 'NormalFloat' },
{ '', 'NormalFloat' },
{ '', 'NormalFloat' },
{ ' ', 'NormalFloat' },
}
--- @param border string|(string|[string,string])[]
local function border_error(border)
error(
@ -43,7 +32,11 @@ local border_size = {
--- @return integer height
--- @return integer width
local function get_border_size(opts)
local border = opts and opts.border or default_border
local border = opts and opts.border or vim.o.winborder
if border == '' then
border = 'none'
end
if type(border) == 'string' then
if not border_size[border] then
@ -884,7 +877,7 @@ function M.make_floating_popup_options(width, height, opts)
or 'cursor',
style = 'minimal',
width = width,
border = opts.border or default_border,
border = opts.border,
zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1,
title = title,
title_pos = title_pos,

View File

@ -159,17 +159,11 @@
/// 'fillchars' to a space char, and clearing the
/// |hl-EndOfBuffer| region in 'winhighlight'.
/// - border: Style of (optional) window border. This can either be a string
/// or an array. The string values are
/// - "none": No border (default).
/// - "single": A single line box.
/// - "double": A double line box.
/// - "rounded": Like "single", but with rounded corners ("╭" etc.).
/// - "solid": Adds padding by a single whitespace cell.
/// - "shadow": A drop shadow effect by blending with the background.
/// - If it is an array, it should have a length of eight or any divisor of
/// eight. The array will specify the eight chars building up the border
/// in a clockwise fashion starting with the top-left corner. As an
/// example, the double box style could be specified as:
/// or an array. The string values are the same as those described in 'winborder'.
/// If it is an array, it should have a length of eight or any divisor of
/// eight. The array will specify the eight chars building up the border
/// in a clockwise fashion starting with the top-left corner. As an
/// example, the double box style could be specified as:
/// ```
/// [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ].
/// ```
@ -944,11 +938,11 @@ static void parse_border_style(Object style, WinConfig *fconfig, Error *err)
char chars[8][MAX_SCHAR_SIZE];
bool shadow_color;
} defaults[] = {
{ "double", { "", "", "", "", "", "", "", "" }, false },
{ "single", { "", "", "", "", "", "", "", "" }, false },
{ "shadow", { "", "", " ", " ", " ", " ", " ", "" }, true },
{ "rounded", { "", "", "", "", "", "", "", "" }, false },
{ "solid", { " ", " ", " ", " ", " ", " ", " ", " " }, false },
{ opt_winborder_values[1], { "", "", "", "", "", "", "", "" }, false },
{ opt_winborder_values[2], { "", "", "", "", "", "", "", "" }, false },
{ opt_winborder_values[3], { "", "", " ", " ", " ", " ", " ", "" }, true },
{ opt_winborder_values[4], { "", "", "", "", "", "", "", "" }, false },
{ opt_winborder_values[5], { " ", " ", " ", " ", " ", " ", " ", " " }, false },
{ NULL, { { NUL } }, false },
};
@ -1279,12 +1273,13 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco
}
}
if (HAS_KEY_X(config, border)) {
if (HAS_KEY_X(config, border) || *p_winbd != NUL) {
if (is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'border'");
goto fail;
}
parse_border_style(config->border, fconfig, err);
Object style = config->border.type != kObjectTypeNil ? config->border : CSTR_AS_OBJ(p_winbd);
parse_border_style(style, fconfig, err);
if (ERROR_SET(err)) {
goto fail;
}

View File

@ -566,6 +566,7 @@ EXTERN OptInt p_wcm; ///< 'wildcharm'
EXTERN int p_wic; ///< 'wildignorecase'
EXTERN char *p_wim; ///< 'wildmode'
EXTERN int p_wmnu; ///< 'wildmenu'
EXTERN char *p_winbd; ///< 'winborder'
EXTERN OptInt p_wh; ///< 'winheight'
EXTERN OptInt p_wmh; ///< 'winminheight'
EXTERN OptInt p_wmw; ///< 'winminwidth'

View File

@ -10188,6 +10188,26 @@ local options = {
short_desc = N_('Controls transparency level for floating windows'),
type = 'number',
},
{
defaults = { if_true = '' },
cb = 'did_set_winborder',
values = { '', 'double', 'single', 'shadow', 'rounded', 'solid', 'none' },
desc = [=[
Defines the default border style of floating windows. The default value
is empty, which is equivalent to "none". Valid values include:
- "none": No border.
- "single": A single line box.
- "double": A double line box.
- "rounded": Like "single", but with rounded corners ("╭" etc.).
- "solid": Adds padding by a single whitespace cell.
- "shadow": A drop shadow effect by blending with the background.
]=],
full_name = 'winborder',
scope = { 'global' },
short_desc = N_('border of floating window'),
type = 'string',
varname = 'p_winbd',
},
{
abbreviation = 'wi',
cb = 'did_set_window',

View File

@ -2005,6 +2005,15 @@ const char *did_set_winhighlight(optset_T *args)
return NULL;
}
/// The 'winborder' option is changed.
const char *did_set_winborder(optset_T *args)
{
if (opt_strings_flags(p_winbd, opt_winborder_values, NULL, true) != OK) {
return e_invarg;
}
return NULL;
}
int expand_set_winhighlight(optexpand_T *args, int *numMatches, char ***matches)
{
return expand_set_opt_generic(args, get_highlight_name, numMatches, matches);

View File

@ -9909,6 +9909,253 @@ describe('float window', function()
]])
eq({"%f", ""}, res)
end)
it('winborder option', function()
local buf = api.nvim_create_buf(false,false)
local config = {relative='editor', width=4, height=4, row=2, col=2}
command('set winborder=single')
api.nvim_open_win(buf, true, config)
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*6
[3:----------------------------------------]|
## grid 2
|
{0:~ }|*5
## grid 3
|
## grid 4
{5:┌────┐}|
{5:│}{1:^ }{5:│}|
{5:│}{2:~ }{5:│}|*3
{5:└────┘}|
]], float_pos={
[4] = {1001, "NW", 1, 2, 2, true, 50};
}, win_viewport={
[2] = {win = 1000, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
[4] = {win = 1001, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
}, win_viewport_margins={
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000
},
[4] = {
bottom = 1,
left = 1,
right = 1,
top = 1,
win = 1001
}
}
})
else
screen:expect({
grid = [[
{5:┌────┐} |
{0:~ }{5:│}{1:^ }{5:│}{0: }|
{0:~ }{5:│}{2:~ }{5:│}{0: }|*3
{0:~ }{5:└────┘}{0: }|
|
]]
})
end
command('fclose')
command('set winborder=double')
api.nvim_open_win(buf, true, config)
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*6
[3:----------------------------------------]|
## grid 2
|
{0:~ }|*5
## grid 3
|
## grid 5
{5:╔════╗}|
{5:║}{1:^ }{5:║}|
{5:║}{2:~ }{5:║}|*3
{5:╚════╝}|
]], float_pos={
[5] = {1002, "NW", 1, 2, 2, true, 50};
}, win_viewport={
[2] = {win = 1000, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
[5] = {win = 1002, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
}, win_viewport_margins={
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000
},
[5] = {
bottom = 1,
left = 1,
right = 1,
top = 1,
win = 1002
}
}
})
else
screen:expect({
grid = [[
{5:╔════╗} |
{0:~ }{5:║}{1:^ }{5:║}{0: }|
{0:~ }{5:║}{2:~ }{5:║}{0: }|*3
{0:~ }{5:╚════╝}{0: }|
|
]]
})
end
command('fclose!')
command('set winborder=none')
api.nvim_buf_set_lines(buf, 0, -1, false, {'none border'})
api.nvim_open_win(buf, true, config)
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*6
[3:----------------------------------------]|
## grid 2
|
{0:~ }|*5
## grid 3
|
## grid 6
{1:^none}|
{1: bor}|
{1:der }|
{2:~ }|
]],
win_pos = {
[2] = {
height = 6,
startcol = 0,
startrow = 0,
width = 40,
win = 1000
}
},
float_pos = {
[6] = {1003, "NW", 1, 2, 2, true, 50};
},
win_viewport = {
[2] = {win = 1000, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
[6] = {win = 1003, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000
},
[6] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1003
}
},
})
else
screen:expect([[
|
{0:~ }|
{0:~ }{1:^none}{0: }|
{0:~ }{1: bor}{0: }|
{0:~ }{1:der }{0: }|
{0:~ }{2:~ }{0: }|
|
]])
end
command('fclose!')
-- respect config.border
command('set winborder=rounded')
config.border = 'single'
api.nvim_open_win(buf, false, config)
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*6
[3:----------------------------------------]|
## grid 2
^ |
{0:~ }|*5
## grid 3
|
## grid 7
{5:┌────┐}|
{5:│}{1:none}{5:│}|
{5:│}{1: bor}{5:│}|
{5:│}{1:der }{5:│}|
{5:│}{2:~ }{5:│}|
{5:└────┘}|
]],
win_pos = {
[2] = {
height = 6,
startcol = 0,
startrow = 0,
width = 40,
win = 1000
}
},
float_pos = {
[7] = {1004, "NW", 1, 2, 2, true, 50};
},
win_viewport = {
[2] = {win = 1000, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
[7] = {win = 1004, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
},
win_viewport_margins = {
[2] = {
bottom = 0,
left = 0,
right = 0,
top = 0,
win = 1000
},
[7] = {
bottom = 1,
left = 1,
right = 1,
top = 1,
win = 1004
}
},
})
else
screen:expect([[
^ {5:┌────┐} |
{0:~ }{5:│}{1:none}{5:│}{0: }|
{0:~ }{5:│}{1: bor}{5:│}{0: }|
{0:~ }{5:│}{1:der }{5:│}{0: }|
{0:~ }{5:│}{2:~ }{5:│}{0: }|
{0:~ }{5:└────┘}{0: }|
|
]])
end
-- it is currently not supported.
eq('Vim(set):E474: Invalid argument: winborder=custom', pcall_err(command, 'set winborder=custom'))
end)
end
describe('with ext_multigrid', function()

View File

@ -75,6 +75,7 @@ let test_values = {
\ 'shada': [['', '''50', '"30'], ['xxx']],
\ 'termpastefilter': [['BS', 'HT', 'FF', 'ESC', 'DEL', 'C0', 'C1', 'C0,C1'],
\ ['xxx', 'C0,C1,xxx']],
\ 'winborder': [['rounded', 'none', 'single', 'solid'], ['xxx']],
\ 'winhighlight': [['', 'a:b', 'a:', 'a:b,c:d'],
\ ['a', ':', ':b', 'a:b:c', 'a:/', '/:b', ',', 'a:b,,', 'a:b,c']],
\