Skip to content

Commit fa35e25

Browse files
committed
feat: compact dropdown popover style
1 parent f20343f commit fa35e25

3 files changed

Lines changed: 100 additions & 5 deletions

File tree

lua/input-form/inputs/select.lua

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,17 +173,40 @@ function M:open_dropdown()
173173
local max_h = config.options.select.max_height
174174
local height = math.min(#lines, max_h)
175175

176-
-- Position the dropdown's top border immediately beneath the input's bottom
177-
-- border. Content origin row = (input content row) + (input bottom border = 1)
178-
-- + (dropdown top border = 1) + 1 = self._layout.row + 3.
176+
-- Prefer stitching the dropdown's top border into the select's bottom
177+
-- border for a compact, merged look:
178+
--
179+
-- ╭─ Label ─────╮
180+
-- │ Option 1 ⌃ │
181+
-- ├─────────────┤ <- shared row (dropdown's top border with T-junction
182+
-- │ Option 1 │ connectors, overlaid on the select's bottom)
183+
-- │ Option 2 │
184+
-- ╰─────────────╯
185+
--
186+
-- The dropdown is positioned so its top border row coincides with the
187+
-- select's bottom border row. With a higher zindex, the dropdown's top
188+
-- border (├─┤ / ╠═╣) wins, producing the visible T-junctions.
189+
local cfg_border = config.options.window.border
190+
local merged_border = utils.merged_top_border(cfg_border)
191+
local dropdown_row, dropdown_border
192+
if merged_border then
193+
dropdown_row = self._layout.row + 2
194+
dropdown_border = merged_border
195+
else
196+
-- Fallback for unmergeable borders (`"none"`, `"shadow"`, ...): keep the
197+
-- dropdown on its own, one row below the select.
198+
dropdown_row = self._layout.row + 3
199+
dropdown_border = cfg_border
200+
end
201+
179202
self.dropdown_win = vim.api.nvim_open_win(self.dropdown_buf, true, {
180203
relative = "editor",
181-
row = self._layout.row + 3,
204+
row = dropdown_row,
182205
col = self._layout.col,
183206
width = self._layout.width,
184207
height = height,
185208
style = "minimal",
186-
border = "rounded",
209+
border = dropdown_border,
187210
focusable = true,
188211
zindex = 100,
189212
})

lua/input-form/utils.lua

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,57 @@ end
4343
--- their UI plugins' exclusion lists as a fallback.
4444
M.FORM_FILETYPE = "input-form"
4545

46+
--- Character sets for the built-in border styles accepted by `nvim_open_win`.
47+
--- Order is clockwise from top-left: TL, T, TR, R, BR, B, BL, L.
48+
local BORDER_CHARS = {
49+
rounded = { "", "", "", "", "", "", "", "" },
50+
single = { "", "", "", "", "", "", "", "" },
51+
double = { "", "", "", "", "", "", "", "" },
52+
solid = { " ", " ", " ", " ", " ", " ", " ", " " },
53+
}
54+
55+
-- T-junction connectors used to replace the top corners when stitching two
56+
-- boxes together (the bottom of box A and the top of box B share a row).
57+
local MERGE_CONNECTORS = {
58+
rounded = { left = "", right = "" },
59+
single = { left = "", right = "" },
60+
double = { left = "", right = "" },
61+
solid = { left = " ", right = " " },
62+
}
63+
64+
--- Build an 8-element border array whose top row is a T-junction stitching
65+
--- into the bottom of a parent box above it. Accepts either one of the
66+
--- built-in border style names or an existing 8-element border array.
67+
---
68+
--- Returns `nil` for unrecognised / non-mergeable borders (e.g. `"none"`,
69+
--- `"shadow"`) so the caller can fall back to an unmerged layout.
70+
---@param border string|table
71+
---@return table|nil
72+
function M.merged_top_border(border)
73+
local chars, connectors
74+
if type(border) == "string" then
75+
chars = BORDER_CHARS[border]
76+
connectors = MERGE_CONNECTORS[border]
77+
elseif type(border) == "table" and #border == 8 then
78+
chars = vim.deepcopy(border)
79+
-- Best-effort fallback for custom arrays: use the straight T's.
80+
connectors = { left = "", right = "" }
81+
end
82+
if not chars or not connectors then
83+
return nil
84+
end
85+
return {
86+
connectors.left,
87+
chars[2],
88+
connectors.right,
89+
chars[4],
90+
chars[5],
91+
chars[6],
92+
chars[7],
93+
chars[8],
94+
}
95+
end
96+
4697
local _excluded_registered = false
4798

4899
-- Append `ft` to a list-shaped config field if missing.

tests/test_inputs_select.lua

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,27 @@ T["select input"]["uses custom chevrons from config"] = function()
113113
helpers.expect.no_match(open_line, "")
114114
end
115115

116+
T["select input"]["dropdown border merges into select's bottom border"] = function()
117+
-- `rounded` default → T-junctions are ├ and ┤.
118+
child.lua([[
119+
_G.t = _G.mk('a')
120+
_G.t:mount({ row = 5, col = 5, width = 30 })
121+
_G.t._layout = { row = 10, col = 5, width = 30 }
122+
_G.t:open_dropdown()
123+
]])
124+
local cfg = child.lua_get([[vim.api.nvim_win_get_config(_G.t.dropdown_win)]])
125+
-- Dropdown row must overlap the select's bottom border row (= layout.row + 2
126+
-- for the content origin, putting the top border at layout.row + 1).
127+
eq(cfg.row, 12)
128+
-- Border is an 8-element array with T-junctions in the top corners.
129+
eq(type(cfg.border), "table")
130+
-- nvim_win_get_config returns borders as { { char, hl_group }, ... }.
131+
local tl = type(cfg.border[1]) == "table" and cfg.border[1][1] or cfg.border[1]
132+
local tr = type(cfg.border[3]) == "table" and cfg.border[3][1] or cfg.border[3]
133+
eq(tl, "")
134+
eq(tr, "")
135+
end
136+
116137
T["select input"]["rejects empty options list"] = function()
117138
local ok = child.lua_get([[
118139
(function()

0 commit comments

Comments
 (0)