@@ -72,17 +72,39 @@ function M:_compute_layout()
7272
7373 local parent_inner_w = outer_width - 2 -- minus parent border
7474 local child_outer_w = parent_inner_w - pad_h * 2
75- local child_inner_w = child_outer_w - 2 -- minus child border
75+ local child_inner_w = child_outer_w - 2 -- minus child border (for bordered inputs)
76+
77+ -- Extra blank rows rendered above/below a checkbox (borderless input) so
78+ -- its glyph doesn't butt directly against an adjacent bordered input's
79+ -- border. Configurable via `style.checkbox.padding`.
80+ local cb_pad = (opts .style and opts .style .checkbox and opts .style .checkbox .padding ) or 0
7681
7782 local rows = {}
7883 local inner_h = pad_top
7984 for i , input in ipairs (self ._inputs ) do
8085 local h = input :height ()
86+ -- NB: avoid the `a and b or c` idiom — `is_bordered()` legitimately
87+ -- returns `false` and that must not get coerced back to the default.
88+ local bordered = true
89+ if type (input .is_bordered ) == " function" then
90+ bordered = input :is_bordered ()
91+ end
92+ local top_pad = (not bordered ) and cb_pad or 0
93+ local bot_pad = (not bordered ) and cb_pad or 0
94+ local outer_h = bordered and (h + 2 ) or (h + top_pad + bot_pad )
95+ -- Editor-row offset from `parent_row` to pass as `nvim_open_win`'s `row`
96+ -- parameter for this child. `row` refers to the window's OUTER top-left
97+ -- (i.e. the border origin for bordered windows, the content row for
98+ -- borderless windows). Parent_row is itself a border origin, so every
99+ -- child needs a `+1` to clear the parent's top border — matching the
100+ -- `+1` already applied on the column axis in `show()`.
101+ local content_offset = inner_h + 1 + top_pad
81102 table.insert (rows , {
82- top_border_offset = inner_h , -- row inside parent content where child's top border sits
103+ bordered = bordered ,
104+ content_row_offset = content_offset ,
83105 value_height = h ,
84106 })
85- inner_h = inner_h + h + 2 -- child's full outer height (content + 2 border rows)
107+ inner_h = inner_h + outer_h
86108 if i < # self ._inputs then
87109 inner_h = inner_h + sep
88110 end
@@ -104,6 +126,7 @@ function M:_compute_layout()
104126 parent_col = parent_col ,
105127 parent_inner_w = parent_inner_w ,
106128 parent_inner_h = inner_h ,
129+ child_outer_w = child_outer_w ,
107130 child_inner_w = child_inner_w ,
108131 pad_h = pad_h ,
109132 rows = rows ,
@@ -174,20 +197,27 @@ function M:show()
174197 " FloatFooter:InputFormHelp" ,
175198 }, " ," )
176199
177- -- Mount each input as its own bordered child floating window.
200+ -- Mount each input as its own floating window. Bordered inputs get their
201+ -- own border + label; borderless inputs (e.g. checkbox) render inline but
202+ -- still align their CONTENT column with the bordered siblings' content
203+ -- (not their border column) so everything lines up visually.
178204 local border = config .options .window .border
179205 for i , input in ipairs (self ._inputs ) do
180206 local r = layout .rows [i ]
181- -- Child's content origin: inside the parent content area, offset by the
182- -- row's top_border_offset plus one row for the child's own top border;
183- -- and one col inside the parent plus horizontal padding plus one for the
184- -- child's own left border.
185- input :mount ({
186- row = layout .parent_row + r .top_border_offset + 1 ,
187- col = layout .parent_col + layout .pad_h + 1 ,
207+ -- Bordered children get `+1` to clear the parent's left border; their
208+ -- content then sits at `+2`. Borderless children shift an extra column
209+ -- so their content column lines up with the bordered siblings' content
210+ -- column (not their border column).
211+ local col_offset = r .bordered and 1 or 2
212+ local mount_opts = {
213+ row = layout .parent_row + r .content_row_offset ,
214+ col = layout .parent_col + layout .pad_h + col_offset ,
188215 width = layout .child_inner_w ,
189- border = border ,
190- })
216+ }
217+ if r .bordered then
218+ mount_opts .border = border
219+ end
220+ input :mount (mount_opts )
191221 self :_install_keymaps (input )
192222 self :_install_validation (input )
193223 end
@@ -267,7 +297,8 @@ function M:_validate_all()
267297 if input .validator then
268298 input ._touched = true
269299 local err = input .validator (input :value ())
270- if err == " " then
300+ -- Only strings count as errors; nil / false / other types = no error.
301+ if type (err ) ~= " string" or err == " " then
271302 err = nil
272303 end
273304 input ._error = err
@@ -348,7 +379,8 @@ function M:_validate_input(input)
348379 return
349380 end
350381 local err = input .validator (input :value ())
351- if err == " " then
382+ -- Only strings count as errors; nil / false / other types = no error.
383+ if type (err ) ~= " string" or err == " " then
352384 err = nil
353385 end
354386 input ._error = err
@@ -362,6 +394,20 @@ function M:_render_validation(input)
362394 if not (win and vim .api .nvim_win_is_valid (win )) then
363395 return
364396 end
397+
398+ -- Borderless inputs (checkbox) render the error inline — they already read
399+ -- `self._error` from their own `_render_display()`.
400+ local bordered = true
401+ if type (input .is_bordered ) == " function" then
402+ bordered = input :is_bordered ()
403+ end
404+ if not bordered then
405+ if type (input ._render_display ) == " function" then
406+ input :_render_display ()
407+ end
408+ return
409+ end
410+
365411 local has_error = input ._error ~= nil
366412
367413 if has_error then
@@ -418,17 +464,21 @@ function M:_help_line()
418464 if nav then
419465 table.insert (parts , nav .. " navigate" )
420466 end
421- -- Only advertise open_select if the form actually has a select input.
422- local has_select = false
467+ -- Only advertise type-specific keys if the form actually has such an input.
468+ local has_select , has_checkbox = false , false
423469 for _ , input in ipairs (self ._inputs ) do
424470 if input .type == " select" then
425471 has_select = true
426- break
472+ elseif input .type == " checkbox" then
473+ has_checkbox = true
427474 end
428475 end
429476 if has_select then
430477 add (km .open_select , " open" )
431478 end
479+ if has_checkbox then
480+ add (km .toggle , " toggle" )
481+ end
432482 add (km .submit , " submit" )
433483 add (km .cancel , " cancel" )
434484 return table.concat (parts , " " )
@@ -487,6 +537,20 @@ function M:_install_keymaps(input)
487537 -- Block insert mode on the select display buffer.
488538 vim .keymap .set (" n" , " i" , " <Nop>" , { buffer = buf , nowait = true , silent = true })
489539 vim .keymap .set (" n" , " a" , " <Nop>" , { buffer = buf , nowait = true , silent = true })
540+ elseif input .type == " checkbox" then
541+ -- Toggle on the configured toggle key AND on open_select so users who
542+ -- prefer <CR> for all interactions get a single key for every field.
543+ map (" n" , km .toggle , function ()
544+ input :toggle ()
545+ end )
546+ if km .open_select and km .open_select ~= km .toggle then
547+ map (" n" , km .open_select , function ()
548+ input :toggle ()
549+ end )
550+ end
551+ -- Block insert mode on the checkbox display buffer.
552+ vim .keymap .set (" n" , " i" , " <Nop>" , { buffer = buf , nowait = true , silent = true })
553+ vim .keymap .set (" n" , " a" , " <Nop>" , { buffer = buf , nowait = true , silent = true })
490554 elseif input .type == " text" then
491555 -- Single-line text inputs must never contain newlines. <CR> in insert
492556 -- mode just exits insert mode (accepting the value) rather than inserting
0 commit comments