feat: compact dropdown popover style

This commit is contained in:
2026-04-05 10:15:20 +03:00
parent f20343f312
commit fa35e25686
3 changed files with 100 additions and 5 deletions

View File

@@ -173,17 +173,40 @@ function M:open_dropdown()
local max_h = config.options.select.max_height
local height = math.min(#lines, max_h)
-- Position the dropdown's top border immediately beneath the input's bottom
-- border. Content origin row = (input content row) + (input bottom border = 1)
-- + (dropdown top border = 1) + 1 = self._layout.row + 3.
-- Prefer stitching the dropdown's top border into the select's bottom
-- border for a compact, merged look:
--
-- ╭─ Label ─────╮
-- │ Option 1 ⌃ │
-- ├─────────────┤ <- shared row (dropdown's top border with T-junction
-- │ Option 1 │ connectors, overlaid on the select's bottom)
-- │ Option 2 │
-- ╰─────────────╯
--
-- The dropdown is positioned so its top border row coincides with the
-- select's bottom border row. With a higher zindex, the dropdown's top
-- border (├─┤ / ╠═╣) wins, producing the visible T-junctions.
local cfg_border = config.options.window.border
local merged_border = utils.merged_top_border(cfg_border)
local dropdown_row, dropdown_border
if merged_border then
dropdown_row = self._layout.row + 2
dropdown_border = merged_border
else
-- Fallback for unmergeable borders (`"none"`, `"shadow"`, ...): keep the
-- dropdown on its own, one row below the select.
dropdown_row = self._layout.row + 3
dropdown_border = cfg_border
end
self.dropdown_win = vim.api.nvim_open_win(self.dropdown_buf, true, {
relative = "editor",
row = self._layout.row + 3,
row = dropdown_row,
col = self._layout.col,
width = self._layout.width,
height = height,
style = "minimal",
border = "rounded",
border = dropdown_border,
focusable = true,
zindex = 100,
})

View File

@@ -43,6 +43,57 @@ end
--- their UI plugins' exclusion lists as a fallback.
M.FORM_FILETYPE = "input-form"
--- Character sets for the built-in border styles accepted by `nvim_open_win`.
--- Order is clockwise from top-left: TL, T, TR, R, BR, B, BL, L.
local BORDER_CHARS = {
rounded = { "", "", "", "", "", "", "", "" },
single = { "", "", "", "", "", "", "", "" },
double = { "", "", "", "", "", "", "", "" },
solid = { " ", " ", " ", " ", " ", " ", " ", " " },
}
-- T-junction connectors used to replace the top corners when stitching two
-- boxes together (the bottom of box A and the top of box B share a row).
local MERGE_CONNECTORS = {
rounded = { left = "", right = "" },
single = { left = "", right = "" },
double = { left = "", right = "" },
solid = { left = " ", right = " " },
}
--- Build an 8-element border array whose top row is a T-junction stitching
--- into the bottom of a parent box above it. Accepts either one of the
--- built-in border style names or an existing 8-element border array.
---
--- Returns `nil` for unrecognised / non-mergeable borders (e.g. `"none"`,
--- `"shadow"`) so the caller can fall back to an unmerged layout.
---@param border string|table
---@return table|nil
function M.merged_top_border(border)
local chars, connectors
if type(border) == "string" then
chars = BORDER_CHARS[border]
connectors = MERGE_CONNECTORS[border]
elseif type(border) == "table" and #border == 8 then
chars = vim.deepcopy(border)
-- Best-effort fallback for custom arrays: use the straight T's.
connectors = { left = "", right = "" }
end
if not chars or not connectors then
return nil
end
return {
connectors.left,
chars[2],
connectors.right,
chars[4],
chars[5],
chars[6],
chars[7],
chars[8],
}
end
local _excluded_registered = false
-- Append `ft` to a list-shaped config field if missing.

View File

@@ -113,6 +113,27 @@ T["select input"]["uses custom chevrons from config"] = function()
helpers.expect.no_match(open_line, "")
end
T["select input"]["dropdown border merges into select's bottom border"] = function()
-- `rounded` default → T-junctions are ├ and ┤.
child.lua([[
_G.t = _G.mk('a')
_G.t:mount({ row = 5, col = 5, width = 30 })
_G.t._layout = { row = 10, col = 5, width = 30 }
_G.t:open_dropdown()
]])
local cfg = child.lua_get([[vim.api.nvim_win_get_config(_G.t.dropdown_win)]])
-- Dropdown row must overlap the select's bottom border row (= layout.row + 2
-- for the content origin, putting the top border at layout.row + 1).
eq(cfg.row, 12)
-- Border is an 8-element array with T-junctions in the top corners.
eq(type(cfg.border), "table")
-- nvim_win_get_config returns borders as { { char, hl_group }, ... }.
local tl = type(cfg.border[1]) == "table" and cfg.border[1][1] or cfg.border[1]
local tr = type(cfg.border[3]) == "table" and cfg.border[3][1] or cfg.border[3]
eq(tl, "")
eq(tr, "")
end
T["select input"]["rejects empty options list"] = function()
local ok = child.lua_get([[
(function()