fix(ui): apply 'pumborder' to mouse menu, fix overflow #36193

Problem:
Mouse popup menus (right-click context menus) do not respect the
'pumborder' option and could overflow screen boundaries when borders
were enabled near the edge.

Solution:
- Remove the mouse menu exclusion from border rendering.
- Add boundary check to shift menu left when border would exceed screen
  width, ensuring complete visibility of menu content and borders.
This commit is contained in:
glepnir
2026-03-20 01:11:35 +08:00
committed by GitHub
parent 4b0700c618
commit 531442ddd8
5 changed files with 169 additions and 17 deletions

View File

@@ -4903,6 +4903,9 @@ A jump table for the options with a short description can be found at |Q_op|.
valid values. |hl-PmenuBorder| is used for highlighting the border, and when
style is "shadow" the |hl-PmenuShadow| and |hl-PmenuShadowThrough| groups are used.
This option also applies to mouse popup menus when 'mousemodel' is set to
"popup" or "popup_setpos", which will display borders using the same style.
*'pumheight'* *'ph'*
'pumheight' 'ph' number (default 0)
global

View File

@@ -5108,6 +5108,9 @@ vim.go.pb = vim.go.pumblend
--- valid values. `hl-PmenuBorder` is used for highlighting the border, and when
--- style is "shadow" the `hl-PmenuShadow` and `hl-PmenuShadowThrough` groups are used.
---
--- This option also applies to mouse popup menus when 'mousemodel' is set to
--- "popup" or "popup_setpos", which will display borders using the same style.
---
--- @type string
vim.o.pumborder = ""
vim.go.pumborder = vim.o.pumborder

View File

@@ -6699,6 +6699,9 @@ local options = {
Defines the default border style of popupmenu windows. See 'winborder' for
valid values. |hl-PmenuBorder| is used for highlighting the border, and when
style is "shadow" the |hl-PmenuShadow| and |hl-PmenuShadowThrough| groups are used.
This option also applies to mouse popup menus when 'mousemodel' is set to
"popup" or "popup_setpos", which will display borders using the same style.
]=],
short_desc = N_('border of popupmenu'),
type = 'string',

View File

@@ -679,10 +679,7 @@ void pum_redraw(void)
}
int scroll_range = pum_size - pum_height;
// avoid set border for mouse menu
int mouse_menu = State != MODE_CMDLINE && pum_grid.zindex == kZIndexCmdlinePopupMenu;
if (!mouse_menu && fconfig.border) {
if (fconfig.border) {
grid_draw_border(&pum_grid, &fconfig, NULL, 0, NULL);
if (!fconfig.shadow) {
row++;
@@ -1437,18 +1434,21 @@ static void pum_position_at_mouse(int min_width)
pum_anchor_grid = grid;
}
if (max_row - row > pum_size || max_row - row > row - min_row) {
// Both width and height are 1 for shadow border, otherwise 2
int border_width = pum_border_width();
int border_height = border_width;
if (max_row - row > pum_size + border_height || max_row - row > row - min_row) {
// Enough space below the mouse row,
// or there is more space below the mouse row than above.
pum_above = false;
pum_row = row + 1;
if (pum_height > max_row - pum_row) {
pum_height = max_row - pum_row;
if (pum_height + border_height > max_row - pum_row) {
pum_height = max_row - pum_row - border_height;
}
} else {
// Show above the mouse row, reduce height if it does not fit.
pum_above = true;
pum_row = row - pum_size;
pum_row = row - pum_size - border_height;
if (pum_row < min_row) {
pum_height += pum_row - min_row;
pum_row = min_row;
@@ -1456,25 +1456,25 @@ static void pum_position_at_mouse(int min_width)
}
if (pum_rl) {
if (col - min_col + 1 >= pum_base_width
|| col - min_col + 1 > min_width) {
if (col - min_col + 1 >= pum_base_width + border_width
|| col - min_col + 1 > min_width + border_width) {
// Enough space to show at mouse column.
pum_col = col;
} else {
// Not enough space, left align with window.
pum_col = min_col + MIN(pum_base_width, min_width) - 1;
pum_col = min_col + MIN(pum_base_width + border_width, min_width + border_width) - 1;
}
pum_width = pum_col - min_col + 1;
pum_width = pum_col - min_col + 1 - border_width;
} else {
if (max_col - col >= pum_base_width
|| max_col - col > min_width) {
if (max_col - col >= pum_base_width + border_width
|| max_col - col > min_width + border_width) {
// Enough space to show at mouse column.
pum_col = col;
} else {
// Not enough space, right align with window.
pum_col = max_col - MIN(pum_base_width, min_width);
pum_col = max_col - MIN(pum_base_width + border_width, min_width + border_width);
}
pum_width = max_col - pum_col;
pum_width = max_col - pum_col - border_width;
}
pum_width = MIN(pum_width, pum_base_width + 1);
@@ -1492,7 +1492,10 @@ static void pum_select_mouse_pos(void)
}
if (grid == pum_grid.handle) {
pum_selected = row;
// Offset by 1 when border width is 2 (non-shadow border)
int border_offset = pum_border_width() == 2 ? 1 : 0;
int item = row - border_offset;
pum_selected = (item >= 0 && item < pum_height) ? item : -1;
return;
}

View File

@@ -9398,6 +9398,146 @@ describe('builtin popupmenu', function()
end
end)
end)
it("'pumborder' on mouse-menu displays completely within screen", function()
screen:try_resize(40, 12)
command('set pumborder=rounded')
-- Click near right edge to test boundary handling
if send_mouse_grid then
api.nvim_input_mouse('right', 'press', '', 2, 2, 35)
else
feed('<RightMouse><35,2>')
end
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*11
[3:----------------------------------------]|
## grid 2
^ |
{1:~ }|*10
## grid 3
|
## grid 4
{n:╭─────────────────────╮}|
{n:│ Inspect │}|
{n:│ │}|
{n:│ Paste │}|
{n:│ Select All │}|
{n:│ │}|
{n:│ How-to disable mouse│}|
{n:╰─────────────────────╯}|
]],
float_pos = {
[4] = { -1, 'NW', 2, 3, 17, false, 250, 2, 3, 17 },
},
})
else
screen:expect([[
^ |
{1:~ }|*2
{1:~ }{n:╭─────────────────────╮}|
{1:~ }{n:│ Inspect │}|
{1:~ }{n:│ │}|
{1:~ }{n:│ Paste │}|
{1:~ }{n:│ Select All │}|
{1:~ }{n:│ │}|
{1:~ }{n:│ How-to disable mouse│}|
{1:~ }{n:╰─────────────────────╯}|
|
]])
end
if send_mouse_grid then
api.nvim_input_mouse('move', '', '', 4, 1, 1)
else
feed('<MouseMove><25,4>')
end
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*11
[3:----------------------------------------]|
## grid 2
^ |
{1:~ }|*10
## grid 3
|
## grid 4
{n:╭─────────────────────╮}|
{n:│}{12: Inspect }{n:│}|
{n:│ │}|
{n:│ Paste │}|
{n:│ Select All │}|
{n:│ │}|
{n:│ How-to disable mouse│}|
{n:╰─────────────────────╯}|
]],
float_pos = {
[4] = { -1, 'NW', 2, 3, 17, false, 250, 2, 3, 17 },
},
})
else
screen:expect([[
^ |
{1:~ }|*2
{1:~ }{n:╭─────────────────────╮}|
{1:~ }{n:│}{12: Inspect }{n:│}|
{1:~ }{n:│ │}|
{1:~ }{n:│ Paste │}|
{1:~ }{n:│ Select All │}|
{1:~ }{n:│ │}|
{1:~ }{n:│ How-to disable mouse│}|
{1:~ }{n:╰─────────────────────╯}|
|
]])
end
-- when right-clicking on the bottom menu, it should appear above the mouse_row
if send_mouse_grid then
api.nvim_input_mouse('right', 'press', '', 2, 8, 20)
else
feed('<RightMouse><20,8>')
end
if multigrid then
screen:expect({
grid = [[
## grid 1
[2:----------------------------------------]|*11
[3:----------------------------------------]|
## grid 2
^ |
{1:~ }|*10
## grid 3
|
## grid 4
{n:╭─────────────────────╮}|
{n:│ Inspect │}|
{n:│ │}|
{n:│ Paste │}|
{n:│ Select All │}|
{n:│ │}|
{n:│ How-to disable mouse│}|
{n:╰─────────────────────╯}|
]],
float_pos = {
[4] = { -1, 'SW', 2, 6, 17, false, 250, 2, 0, 17 },
},
})
else
screen:expect([[
^ {n:╭─────────────────────╮}|
{1:~ }{n:│ Inspect │}|
{1:~ }{n:│ │}|
{1:~ }{n:│ Paste │}|
{1:~ }{n:│ Select All │}|
{1:~ }{n:│ │}|
{1:~ }{n:│ How-to disable mouse│}|
{1:~ }{n:╰─────────────────────╯}|
{1:~ }|*3
|
]])
end
end)
end
describe('with ext_multigrid and actual mouse grid', function()