Meh I'll figure out submodules later

This commit is contained in:
mustard 2025-09-16 01:01:02 +02:00
parent 4ca9d44a90
commit 8cb281f436
352 changed files with 66107 additions and 0 deletions

View file

@ -0,0 +1,307 @@
# Layout
Layout is a helper component for creating complex layout by automatically
handling the calculation for position and size of other components.
**Example**
```lua
local Layout = require("nui.layout")
local Popup = require("nui.popup")
local top_popup = Popup({ border = "double" })
local bottom_left_popup = Popup({ border = "single" })
local bottom_right_popup = Popup({ border = "single" })
local layout = Layout(
{
position = "50%",
size = {
width = 80,
height = 40,
},
},
Layout.Box({
Layout.Box(top_popup, { size = "40%" }),
Layout.Box({
Layout.Box(bottom_left_popup, { size = "50%" }),
Layout.Box(bottom_right_popup, { size = "50%" }),
}, { dir = "row", size = "60%" }),
}, { dir = "col" })
)
layout:mount()
```
_Signature:_ `Layout(options, box)` or `Layout(component, box)`
`component` can be `Popup` or `Split`.
## Options (for float layout)
### `anchor`
**Type:** `"NW"` / `"NE"` / `"SW"` / `"SE"`
Decides which corner of the layout to place at `position`.
---
### `relative`
**Type:** `string` or `table`
This option affects how `position` and `size` are calculated.
**Examples**
Relative to cursor on current window:
```lua
relative = "cursor",
```
Relative to the current editor screen:
```lua
relative = "editor",
```
Relative to the current window (_default_):
```lua
relative = "win",
```
Relative to the window with specific id:
```lua
relative = {
type = "win",
winid = 5,
},
```
Relative to the buffer position:
```lua
relative = {
type = "buf",
-- zero-indexed
position = {
row = 5,
col = 5,
},
},
```
---
### `position`
**Type:** `number` or `percentage string` or `table`
Position is calculated from the top-left corner.
If `position` is `number` or `percentage string`, it applies to both `row` and `col`.
Or you can pass a table to set them separately.
For `percentage string`, position is calculated according to the option `relative`.
If `relative` is set to `"buf"` or `"cursor"`, `percentage string` is not allowed.
**Examples**
```lua
position = 50,
```
```lua
position = "50%",
```
```lua
position = {
row = 30,
col = 20,
},
```
```lua
position = {
row = "20%",
col = "50%",
},
```
---
### `size`
**Type:** `number` or `percentage string` or `table`
Determines the size of the layout.
If `size` is `number` or `percentage string`, it applies to both `width` and `height`.
You can also pass a table to set them separately.
For `percentage string`, `size` is calculated according to the option `relative`.
If `relative` is set to `"buf"` or `"cursor"`, window size is considered.
Decimal `number` in `(0,1)` range is treated similar to `percentage string`. For
example: `0.5` is same as `"50%"`.
**Examples**
```lua
size = 50,
```
```lua
size = "50%",
```
```lua
size = 0.5,
```
```lua
size = {
width = 80,
height = 40,
},
```
```lua
size = {
width = "80%",
height = 0.6,
},
```
## Options (for split layout)
### `relative`
**Type:** `string` or `table`
This option affects how `size` is calculated.
**Examples**
Split current editor screen:
```lua
relative = "editor"
```
Split current window (_default_):
```lua
relative = "win"
```
Split window with specific id:
```lua
relative = {
type = "win",
winid = 42,
}
```
---
### `position`
**Type:** `"top" | "right"| "bottom" | "left"`.
---
### `size`
**Type:** `number` or `percentage string`
Determines the size of the layout.
For `percentage string`, size is calculated according to the option `relative`.
## Layout.Box
_Signature:_ `Layout.Box(box, options)`
**Parameters**
| Name | Type | Description |
| --------- | ------------------------------ | ----------------------------------------- |
| `box` | `Layout.Box[]` / nui component | list of `Layout.Box` or any nui component |
| `options` | `table` | box options |
`options` is a `table` having the following keys:
| Key | Type | Description |
| ------ | ----------------------------- | ------------------------------------------------------ |
| `dir` | `"col"` / `"row"` (_default_) | arrangement direction, only if `box` is `Layout.Box[]` |
| `grow` | `number` | growth factor to fill up the box free space |
| `size` | `number` / `string` / `table` | optional if `grow` is present |
## Methods
### `layout:mount`
_Signature:_ `layout:mount()`
Mounts the layout with all the components.
**Examples**
```lua
layout:mount()
```
### `layout:unmount`
_Signature:_ `layout:unmount()`
Unmounts the layout with all the components.
**Examples**
```lua
layout:unmount()
```
### `layout:hide`
_Signature:_ `layout:hide()`
Hides the layout with all the components. Preserves the buffer (related content, autocmds and keymaps).
### `layout:show`
_Signature:_ `layout:show()`
Shows the hidden layout with all the components.
### `layout:update`
_Signature:_ `layout:update(config, box?)` or `layout:update(box?)`
**Parameters**
`config` is a `table` having the following keys:
| Key | Type |
| ---------- | --------------------------------- |
| `anchor` | `"NW"` / `"NE"` / `"SW"` / `"SE"` |
| `relative` | `string` / `table` |
| `position` | `string` / `table` |
| `size` | `string` / `table` |
`box` is a `table` returned by `Layout.Box`.
They are the same options used for layout initialization.
## Wiki Page
You can find additional documentation/examples/guides/tips-n-tricks in
[nui.layout wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.layout).

View file

@ -0,0 +1,239 @@
local utils = require("nui.utils")
local layout_utils = require("nui.layout.utils")
local u = {
is_type = utils.is_type,
calculate_window_size = layout_utils.calculate_window_size,
}
local mod = {}
local function get_child_position(box, child, current_position, canvas_position)
local position = box.dir == "row" and {
row = canvas_position.row,
col = current_position.col,
} or {
col = canvas_position.col,
row = current_position.row,
}
if child.component then
local border = child.component.border
if border and border._.type == "complex" then
position.col = position.col + math.floor(border._.size_delta.width / 2 + 0.5)
position.row = position.row + math.floor(border._.size_delta.height / 2 + 0.5)
end
end
return position
end
---@param parent table Layout.Box
---@param child table Layout.Box
---@param container_size table
---@param growable_dimension_per_factor? number
local function get_child_size(parent, child, container_size, growable_dimension_per_factor)
local child_size = {
width = child.size.width,
height = child.size.height,
}
if child.grow and growable_dimension_per_factor then
if parent.dir == "col" then
child_size.height = math.floor(growable_dimension_per_factor * child.grow)
else
child_size.width = math.floor(growable_dimension_per_factor * child.grow)
end
end
local outer_size = u.calculate_window_size(child_size, container_size)
local inner_size = {
width = outer_size.width,
height = outer_size.height,
}
if child.component then
if child.component.border then
inner_size.width = inner_size.width - child.component.border._.size_delta.width
inner_size.height = inner_size.height - child.component.border._.size_delta.height
if inner_size.height <= 0 then
local height_adjustment = math.abs(inner_size.height) + 1
inner_size.height = inner_size.height + height_adjustment
outer_size.height = outer_size.height + height_adjustment
end
end
end
return outer_size, inner_size
end
function mod.process(box, meta)
-- luacov: disable
if box.mount or box.component or not box.box then
return error("invalid parameter: box")
end
-- luacov: enable
local container_size = meta.container_size
-- luacov: disable
if not u.is_type("number", container_size.width) or not u.is_type("number", container_size.height) then
return error("invalid value: box.size")
end
-- luacov: enable
local current_position = box.dir == "row" and {
col = meta.position.col,
row = 0,
} or {
col = 0,
row = meta.position.row,
}
local growable_child_factor = 0
for _, child in ipairs(box.box) do
if meta.process_growable_child or not child.grow then
local position = get_child_position(box, child, current_position, meta.position)
local outer_size, inner_size = get_child_size(box, child, container_size, meta.growable_dimension_per_factor)
if child.component then
child.component:set_layout({
size = inner_size,
relative = {
type = "win",
winid = meta.winid,
},
position = position,
})
else
mod.process(child, {
winid = meta.winid,
container_size = outer_size,
position = position,
})
end
current_position.col = current_position.col + outer_size.width
current_position.row = current_position.row + outer_size.height
end
if child.grow then
growable_child_factor = growable_child_factor + child.grow
end
end
if meta.process_growable_child or growable_child_factor == 0 then
return
end
local growable_width = container_size.width - current_position.col
local growable_height = container_size.height - current_position.row
local growable_dimension = box.dir == "col" and growable_height or growable_width
local growable_dimension_per_factor = growable_dimension / growable_child_factor
mod.process(box, {
winid = meta.winid,
container_size = meta.container_size,
position = meta.position,
process_growable_child = true,
growable_dimension_per_factor = growable_dimension_per_factor,
})
end
function mod.mount_box(box)
for _, child in ipairs(box.box) do
if child.component then
child.component:mount()
else
mod.mount_box(child)
end
end
end
---@param box table Layout.Box
function mod.show_box(box)
for _, child in ipairs(box.box) do
if child.component then
child.component:show()
else
mod.show_box(child)
end
end
end
function mod.unmount_box(box)
for _, child in ipairs(box.box) do
if child.component then
child.component:unmount()
else
mod.unmount_box(child)
end
end
end
---@param box table Layout.Box
function mod.hide_box(box)
for _, child in ipairs(box.box) do
if child.component then
child.component:hide()
else
mod.hide_box(child)
end
end
end
---@param box table Layout.Box
---@return table<string, table>
local function collect_box_components(box, components)
if not components then
components = {}
end
for _, child in ipairs(box.box) do
if child.component then
components[child.component._.id] = child.component
else
collect_box_components(child, components)
end
end
return components
end
---@param curr_box table Layout.Box
---@param prev_box table Layout.Box
function mod.process_box_change(curr_box, prev_box)
if curr_box == prev_box then
return
end
local curr_components = collect_box_components(curr_box)
local prev_components = collect_box_components(prev_box)
for id, component in pairs(curr_components) do
if not prev_components[id] then
if not component.winid then
if component._.mounted then
component:show()
else
component:mount()
end
end
end
end
for id, component in pairs(prev_components) do
if not curr_components[id] then
if component._.mounted then
if component.winid then
component:hide()
end
end
end
end
end
return mod

View file

@ -0,0 +1,573 @@
local Object = require("nui.object")
local Popup = require("nui.popup")
local Split = require("nui.split")
local utils = require("nui.utils")
local layout_utils = require("nui.layout.utils")
local float_layout = require("nui.layout.float")
local split_layout = require("nui.layout.split")
local split_utils = require("nui.split.utils")
local autocmd = require("nui.utils.autocmd")
local _ = utils._
local defaults = utils.defaults
local is_type = utils.is_type
local u = {
get_next_id = _.get_next_id,
position = layout_utils.position,
size = layout_utils.size,
split = split_utils,
update_layout_config = layout_utils.update_layout_config,
}
-- GitHub Issue: https://github.com/neovim/neovim/issues/18925
local function apply_workaround_for_float_relative_position_issue_18925(layout)
local winids_len = 1
local winids = { layout.winid }
local function collect_anchor_winids(box)
for _, child in ipairs(box.box) do
if child.component then
local border = child.component.border
if border and border.winid then
winids_len = winids_len + 1
winids[winids_len] = border.winid
end
else
collect_anchor_winids(child)
end
end
end
collect_anchor_winids(layout._.box)
vim.schedule(function()
-- check in case layout was immediately hidden or unmounted
if layout.winid == winids[1] and vim.api.nvim_win_is_valid(winids[1]) then
vim.cmd(
("noa call nvim_set_current_win(%s)\nnormal! jk\nredraw\n"):rep(winids_len):format(unpack(winids))
.. ("noa call nvim_set_current_win(%s)"):format(vim.api.nvim_get_current_win())
)
end
end)
end
---@param options nui_layout_options
local function merge_default_options(options)
options.relative = defaults(options.relative, "win")
return options
end
---@param options nui_layout_options
local function normalize_options(options)
options = _.normalize_layout_options(options)
return options
end
---@return boolean
local function is_box(object)
return object and (object.box or object.component)
end
---@return boolean
local function is_component(object)
return object and object.mount
end
local function is_component_mounted(component)
return is_type("number", component.winid)
end
---@param component NuiPopup|NuiSplit
local function get_layout_config_relative_to_component(component)
return {
relative = { type = "win", winid = component.winid },
position = { row = 0, col = 0 },
size = { width = "100%", height = "100%" },
}
end
---@param layout NuiLayout
---@param box table Layout.Box
local function wire_up_layout_components(layout, box)
for _, child in ipairs(box.box) do
if child.component then
autocmd.create({ "BufWipeout", "QuitPre" }, {
group = layout._.augroup.unmount,
buffer = child.component.bufnr,
callback = vim.schedule_wrap(function()
layout:unmount()
end),
}, child.component.bufnr)
autocmd.create("BufWinEnter", {
group = layout._.augroup.unmount,
buffer = child.component.bufnr,
callback = function()
local winid = child.component.winid
if layout._.type == "float" and not winid then
--[[
`BufWinEnter` does not contain window id and
it is fired before `nvim_open_win` returns
the window id.
--]]
winid = vim.fn.bufwinid(child.component.bufnr)
end
autocmd.create("WinClosed", {
group = layout._.augroup.hide,
nested = true,
pattern = tostring(winid),
callback = function()
layout:hide()
end,
}, child.component.bufnr)
end,
}, child.component.bufnr)
else
wire_up_layout_components(layout, child)
end
end
end
---@class nui_layout_options
---@field anchor? nui_layout_option_anchor
---@field relative? nui_layout_option_relative_type|nui_layout_option_relative
---@field position? number|string|nui_layout_option_position
---@field size? number|string|nui_layout_option_size
---@class NuiLayout
local Layout = Object("NuiLayout")
---@return '"float"'|'"split"' layout_type
local function get_layout_type(box)
for _, child in ipairs(box.box) do
if child.component and child.type then
return child.type
end
local type = get_layout_type(child)
if type then
return type
end
end
error("unexpected empty box")
end
---@param options nui_layout_options|NuiPopup|NuiSplit
---@param box NuiLayout.Box|NuiLayout.Box[]
function Layout:init(options, box)
local id = u.get_next_id()
box = Layout.Box(box)
local type = get_layout_type(box)
self._ = {
id = id,
type = type,
box = box,
loading = false,
mounted = false,
augroup = {
hide = string.format("%s_hide", id),
unmount = string.format("%s_unmount", id),
},
}
if type == "float" then
local container
if is_component(options) then
container = options --[[@as NuiPopup|NuiSplit]]
options = get_layout_config_relative_to_component(container)
else
---@cast options -NuiPopup, -NuiSplit
options = merge_default_options(options)
options = normalize_options(options)
end
self._[type] = {
container = container,
layout = {},
win_enter = false,
win_config = {
border = "none",
focusable = false,
style = "minimal",
anchor = options.anchor,
zindex = 49,
},
win_options = {
winblend = 100,
},
}
if not is_component(container) or is_component_mounted(container) then
self:update(options)
end
end
if type == "split" then
options = u.split.merge_default_options(options)
options = u.split.normalize_options(options)
self._[type] = {
layout = {},
position = options.position,
size = {},
win_config = {
pending_changes = {},
},
}
self:update(options)
end
end
function Layout:_process_layout()
local type = self._.type
if type == "float" then
local info = self._.float
float_layout.process(self._.box, {
winid = self.winid,
container_size = info.size,
position = {
row = 0,
col = 0,
},
})
return
end
if type == "split" then
local info = self._.split
split_layout.process(self._.box, {
position = info.position,
relative = info.relative,
container_size = info.size,
container_fallback_size = info.container_info.size,
})
end
end
function Layout:_open_window()
if self._.type == "float" then
local info = self._.float
self.winid = vim.api.nvim_open_win(self.bufnr, info.win_enter, info.win_config)
assert(self.winid, "failed to create popup window")
_.set_win_options(self.winid, info.win_options)
end
end
function Layout:_close_window()
if not self.winid then
return
end
if vim.api.nvim_win_is_valid(self.winid) then
vim.api.nvim_win_close(self.winid, true)
end
self.winid = nil
end
function Layout:mount()
if self._.loading or self._.mounted then
return
end
self._.loading = true
local type = self._.type
if type == "float" then
local info = self._.float
local container = info.container
if is_component(container) and not is_component_mounted(container) then
container:mount()
self:update(get_layout_config_relative_to_component(container))
end
if not self.bufnr then
self.bufnr = vim.api.nvim_create_buf(false, true)
assert(self.bufnr, "failed to create buffer")
end
self:_open_window()
end
self:_process_layout()
if type == "float" then
float_layout.mount_box(self._.box)
apply_workaround_for_float_relative_position_issue_18925(self)
end
if type == "split" then
split_layout.mount_box(self._.box)
end
self._.loading = false
self._.mounted = true
end
function Layout:unmount()
if self._.loading or not self._.mounted then
return
end
pcall(autocmd.delete_group, self._.augroup.hide)
pcall(autocmd.delete_group, self._.augroup.unmount)
self._.loading = true
local type = self._.type
if type == "float" then
float_layout.unmount_box(self._.box)
if self.bufnr then
if vim.api.nvim_buf_is_valid(self.bufnr) then
vim.api.nvim_buf_delete(self.bufnr, { force = true })
end
self.bufnr = nil
end
self:_close_window()
end
if type == "split" then
split_layout.unmount_box(self._.box)
end
self._.loading = false
self._.mounted = false
end
function Layout:hide()
if self._.loading or not self._.mounted then
return
end
self._.loading = true
pcall(autocmd.delete_group, self._.augroup.hide)
local type = self._.type
if type == "float" then
float_layout.hide_box(self._.box)
self:_close_window()
end
if type == "split" then
split_layout.hide_box(self._.box)
end
self._.loading = false
end
function Layout:show()
if self._.loading then
return
end
if not self._.mounted then
return self:mount()
end
self._.loading = true
autocmd.create_group(self._.augroup.hide, { clear = true })
local type = self._.type
if type == "float" then
self:_open_window()
end
self:_process_layout()
if type == "float" then
float_layout.show_box(self._.box)
apply_workaround_for_float_relative_position_issue_18925(self)
end
if type == "split" then
split_layout.show_box(self._.box)
end
self._.loading = false
end
---@param config? NuiLayout.Box|NuiLayout.Box[]|nui_layout_options
---@param box? NuiLayout.Box
function Layout:update(config, box)
config = config or {} --[[@as nui_layout_options]]
if not box and is_box(config) or is_box(config[1]) then
box = config --[=[@as NuiLayout.Box|NuiLayout.Box[]]=]
---@type nui_layout_options
config = {}
end
autocmd.create_group(self._.augroup.hide, { clear = true })
autocmd.create_group(self._.augroup.unmount, { clear = true })
local prev_box = self._.box
if box then
self._.box = Layout.Box(box)
self._.type = get_layout_type(self._.box)
end
if self._.type == "float" then
local info = self._.float
u.update_layout_config(info, config)
if self.winid then
vim.api.nvim_win_set_config(self.winid, info.win_config)
self:_process_layout()
float_layout.process_box_change(self._.box, prev_box)
apply_workaround_for_float_relative_position_issue_18925(self)
end
wire_up_layout_components(self, self._.box)
end
if self._.type == "split" then
local info = self._.split
local relative_winid = info.relative and info.relative.win
local prev_winid = vim.api.nvim_get_current_win()
if relative_winid then
vim.api.nvim_set_current_win(relative_winid)
end
local curr_box = self._.box
if prev_box ~= curr_box then
self._.box = prev_box
self:hide()
self._.box = curr_box
end
u.split.update_layout_config(info, config)
if prev_box == curr_box then
self:_process_layout()
else
self:show()
end
if vim.api.nvim_win_is_valid(prev_winid) then
vim.api.nvim_set_current_win(prev_winid)
end
wire_up_layout_components(self, self._.box)
end
end
---@class nui_layout_box_options
---@field dir? 'row'|'col'
---@field grow? integer
---@field size? number|string|table<'height'|'width', number|string>
---@class NuiLayout.Box
---@field type? 'float'|'split'
---@field component? NuiPopup|NuiSplit
---@field box? NuiLayout.Box[]
---@field grow? integer
---@field size? nui_layout_option_size
---@param box NuiPopup|NuiSplit|NuiLayout.Box|NuiLayout.Box[]
---@param options? nui_layout_box_options
---@return NuiLayout.Box
function Layout.Box(box, options)
options = options or {}
if is_box(box) then
return box --[[@as NuiLayout.Box]]
end
if box.mount then
local type
---@diagnostic disable: undefined-field
if box:is_instance_of(Popup) then
type = "float"
elseif box:is_instance_of(Split) then
type = "split"
end
---@diagnostic enable: undefined-field
if not type then
error("unsupported component")
end
return {
type = type,
component = box,
grow = options.grow,
size = options.size,
}
end
local dir = defaults(options.dir, "row")
-- normalize children size
for _, child in ipairs(box) do
if not child.grow and not child.size then
error("missing child.size")
end
if dir == "row" then
if type(child.size) ~= "table" then
---@diagnostic disable-next-line: assign-type-mismatch
child.size = { width = child.size }
end
if not child.size.height then
child.size.height = "100%"
end
elseif dir == "col" then
if not is_type("table", child.size) then
---@diagnostic disable-next-line: assign-type-mismatch
child.size = { height = child.size }
end
if not child.size.width then
child.size.width = "100%"
end
end
end
return {
box = box,
dir = dir,
grow = options.grow,
size = options.size,
}
end
-- luacheck: push no max comment line length
---@alias NuiLayout.constructor fun(options: nui_layout_options|NuiPopup|NuiSplit, box: NuiLayout.Box|NuiLayout.Box[]): NuiLayout
---@type NuiLayout|NuiLayout.constructor
local NuiLayout = Layout
-- luacheck: pop
return NuiLayout

View file

@ -0,0 +1,262 @@
local utils = require("nui.utils")
local split_utils = require("nui.split.utils")
local u = {
is_type = utils.is_type,
split = split_utils,
set_win_options = utils._.set_win_options,
}
local mod = {}
---@param box_dir '"row"'|'"col"'
---@return nui_split_internal_position position
local function get_child_position(box_dir)
if box_dir == "row" then
return "right"
else
return "bottom"
end
end
---@param position nui_split_internal_position
---@param child { size: nui_layout_option_size, grow?: boolean }
---@param container_size { width?: number, height?: number }
---@param growable_dimension_per_factor? number
local function get_child_size(position, child, container_size, growable_dimension_per_factor)
local child_size
if position == "left" or position == "right" then
child_size = child.size.width
else
child_size = child.size.height
end
if child.grow and growable_dimension_per_factor then
child_size = math.floor(growable_dimension_per_factor * child.grow)
end
return u.split.calculate_window_size(position, child_size, container_size)
end
local function get_container_size(meta)
local size = meta.container_size
size.width = size.width or meta.container_fallback_size.width
size.height = size.height or meta.container_fallback_size.height
return size
end
function mod.process(box, meta)
-- luacov: disable
if box.mount or box.component or not box.box then
return error("invalid parameter: box")
end
-- luacov: enable
local container_size = get_container_size(meta)
-- luacov: disable
if not u.is_type("number", container_size.width) and not u.is_type("number", container_size.height) then
return error("invalid value: box.size")
end
-- luacov: enable
local consumed_size = {
width = 0,
height = 0,
}
local growable_child_factor = 0
for i, child in ipairs(box.box) do
if meta.process_growable_child or not child.grow then
local position = get_child_position(box.dir)
local relative = { type = "win" }
local size = get_child_size(position, child, container_size, meta.growable_dimension_per_factor)
consumed_size.width = consumed_size.width + (size.width or 0)
consumed_size.height = consumed_size.height + (size.height or 0)
if i == 1 then
position = meta.position
if meta.relative then
relative = meta.relative
end
if position == "left" or position == "right" then
size.width = container_size.width
else
size.height = container_size.height
end
end
if child.component then
child.component:update_layout({
position = position,
relative = relative,
size = size,
})
if i == 1 and child.component.winid then
if position == "left" or position == "right" then
vim.api.nvim_win_set_height(child.component.winid, size.height)
else
vim.api.nvim_win_set_width(child.component.winid, size.width)
end
end
else
mod.process(child, {
container_size = size,
container_fallback_size = container_size,
position = position,
})
end
end
if child.grow then
growable_child_factor = growable_child_factor + child.grow
end
end
if meta.process_growable_child or growable_child_factor == 0 then
return
end
local growable_width = container_size.width - consumed_size.width
local growable_height = container_size.height - consumed_size.height
local growable_dimension = box.dir == "col" and growable_height or growable_width
local growable_dimension_per_factor = growable_dimension / growable_child_factor
mod.process(box, {
container_size = meta.container_size,
container_fallback_size = meta.container_fallback_size,
position = meta.position,
process_growable_child = true,
growable_dimension_per_factor = growable_dimension_per_factor,
})
end
---@param box table Layout.Box
local function get_first_component(box)
if not box.box[1] then
return
end
if box.box[1].component then
return box.box[1].component
end
return get_first_component(box.box[1])
end
---@param box table Layout.Box
local function unset_win_options_fixsize(box)
for _, child in ipairs(box.box) do
if child.component then
local winfix = child.component._._layout_orig_winfixsize
if winfix then
child.component._.win_options.winfixwidth = winfix.winfixwidth
child.component._.win_options.winfixheight = winfix.winfixheight
child.component._._layout_orig_winfixsize = nil
end
u.set_win_options(child.component.winid, {
winfixwidth = child.component._.win_options.winfixwidth,
winfixheight = child.component._.win_options.winfixheight,
})
else
unset_win_options_fixsize(child)
end
end
end
---@param box table Layout.Box
---@param action '"mount"'|'"show"'
---@param meta? { initial_pass?: boolean }
local function do_action(box, action, meta)
meta = meta or { root = true }
for i, child in ipairs(box.box) do
if not meta.initial_pass or i == 1 then
if child.component then
child.component._._layout_orig_winfixsize = {
winfixwidth = child.component._.win_options.winfixwidth,
winfixheight = child.component._.win_options.winfixheight,
}
child.component._.win_options.winfixwidth = i ~= 1
child.component._.win_options.winfixheight = i == 1
if box.dir == "col" then
child.component._.win_options.winfixwidth = not child.component._.win_options.winfixwidth
child.component._.win_options.winfixheight = not child.component._.win_options.winfixheight
end
if child.component and not child.component.winid then
child.component._.relative.win = vim.api.nvim_get_current_win()
child.component._.win_config.win = child.component._.relative.win
end
child.component[action](child.component)
if action == "show" and not child.component._.mounted then
child.component:mount()
end
else
do_action(child, action, {
initial_pass = true,
})
end
end
end
if not meta.initial_pass then
for _, child in ipairs(box.box) do
if child.box then
local first_component = get_first_component(child)
if first_component and first_component.winid then
vim.api.nvim_set_current_win(first_component.winid)
end
do_action(child, action, {
initial_pass = false,
})
end
end
end
if meta.root then
unset_win_options_fixsize(box)
end
end
---@param box table Layout.Box
---@param meta? { initial_pass?: boolean }
function mod.mount_box(box, meta)
do_action(box, "mount", meta)
end
---@param box table Layout.Box
---@param meta? { initial_pass?: boolean }
function mod.show_box(box, meta)
do_action(box, "show", meta)
end
---@param box table Layout.Box
function mod.unmount_box(box)
for _, child in ipairs(box.box) do
if child.component then
child.component:unmount()
else
mod.unmount_box(child)
end
end
end
---@param box table Layout.Box
function mod.hide_box(box)
for _, child in ipairs(box.box) do
if child.component then
child.component:hide()
else
mod.hide_box(child)
end
end
end
return mod

View file

@ -0,0 +1,226 @@
local utils = require("nui.utils")
local _ = utils._
local defaults = utils.defaults
--luacheck: push no max line length
---@alias nui_layout_option_anchor "NW"|"NE"|"SW"|"SE"
---@alias nui_layout_option_relative_type "'cursor'"|"'editor'"|"'win'"|"'buf'"
---@alias nui_layout_option_relative { type: nui_layout_option_relative_type, winid?: number, position?: { row: number, col: number } }
---@alias nui_layout_option_position { row: number|string, col: number|string }
---@alias nui_layout_option_size { width: number|string, height: number|string }
---@alias nui_layout_internal_position { relative: "'cursor'"|"'editor'"|"'win'", win: number, bufpos?: number[], row: number, col: number }
---@alias nui_layout_container_info { relative: nui_layout_option_relative_type, size: { height: integer, width: integer }, type: "'editor'"|"'window'" }
--luacheck: pop
local mod_size = {}
local mod_position = {}
local mod = {
size = mod_size,
position = mod_position,
}
---@param position nui_layout_option_position
---@param size { width: number, height: number }
---@param container nui_layout_container_info
---@return { row: number, col: number }
function mod.calculate_window_position(position, size, container)
local row
local col
local is_percentage_allowed = not vim.tbl_contains({ "buf", "cursor" }, container.relative)
local percentage_error = string.format("position %% can not be used relative to %s", container.relative)
local r = utils.parse_number_input(position.row)
assert(r.value ~= nil, "invalid position.row")
if r.is_percentage then
assert(is_percentage_allowed, percentage_error)
row = math.floor((container.size.height - size.height) * r.value)
else
row = r.value
end
local c = utils.parse_number_input(position.col)
assert(c.value ~= nil, "invalid position.col")
if c.is_percentage then
assert(is_percentage_allowed, percentage_error)
col = math.floor((container.size.width - size.width) * c.value)
else
col = c.value
end
return {
row = row,
col = col,
}
end
---@param size { width: number|string, height: number|string }
---@param container_size { width: number, height: number }
---@return { width: number, height: number }
function mod.calculate_window_size(size, container_size)
local width = _.normalize_dimension(size.width, container_size.width)
assert(width, "invalid size.width")
local height = _.normalize_dimension(size.height, container_size.height)
assert(height, "invalid size.height")
return {
width = width,
height = height,
}
end
---@param position nui_layout_internal_position
---@return nui_layout_container_info
function mod.get_container_info(position)
local relative = position.relative
local winid = position.win == 0 and vim.api.nvim_get_current_win() or position.win
if relative == "editor" then
return {
relative = relative,
size = utils.get_editor_size(),
type = "editor",
winid = winid,
}
end
return {
relative = position.bufpos and "buf" or relative,
size = utils.get_window_size(position.win),
type = "window",
winid = winid,
}
end
---@param relative nui_layout_option_relative
---@param fallback_winid number
---@return nui_layout_internal_position
function mod.parse_relative(relative, fallback_winid)
local winid = defaults(relative.winid, fallback_winid)
if relative.type == "buf" then
return {
relative = "win",
win = winid,
bufpos = {
relative.position.row,
relative.position.col,
},
}
end
return {
relative = relative.type,
win = winid,
}
end
---@param component_internal table
---@param config nui_layout_options
function mod.update_layout_config(component_internal, config)
local internal = component_internal
local options = _.normalize_layout_options({
relative = config.relative,
size = config.size,
position = config.position,
})
local win_config = internal.win_config
if config.anchor then
win_config.anchor = config.anchor
end
if options.relative then
internal.layout.relative = options.relative
local fallback_winid = internal.position and internal.position.win
or internal.layout.relative.type == "cursor" and 0
or vim.api.nvim_get_current_win()
internal.position =
vim.tbl_extend("force", internal.position or {}, mod.parse_relative(internal.layout.relative, fallback_winid))
win_config.relative = internal.position.relative
win_config.win = internal.position.relative == "win" and internal.position.win or nil
win_config.bufpos = internal.position.bufpos
end
-- luacov: disable
if not win_config.relative then
return error("missing layout config: relative")
end
-- luacov: enable
local prev_container_size = internal.container_info and internal.container_info.size
internal.container_info = mod.get_container_info(internal.position)
local container_size_changed = not mod.size.are_same(internal.container_info.size, prev_container_size)
if
options.size
-- need_size_refresh
or (container_size_changed and internal.layout.size and mod.size.contains_percentage_string(internal.layout.size))
then
internal.layout.size = options.size or internal.layout.size
internal.size = mod.calculate_window_size(internal.layout.size, internal.container_info.size)
win_config.width = internal.size.width
win_config.height = internal.size.height
end
if not win_config.width or not win_config.height then
return error("missing layout config: size")
end
if
options.position
-- need_position_refresh
or (
container_size_changed
and internal.layout.position
and mod.position.contains_percentage_string(internal.layout.position)
)
then
internal.layout.position = options.position or internal.layout.position
internal.position = vim.tbl_extend(
"force",
internal.position,
mod.calculate_window_position(internal.layout.position, internal.size, internal.container_info)
)
win_config.row = internal.position.row
win_config.col = internal.position.col
end
if not win_config.row or not win_config.col then
return error("missing layout config: position")
end
end
---@param size_a nui_layout_option_size
---@param size_b? nui_layout_option_size
---@return boolean
function mod_size.are_same(size_a, size_b)
return size_b and size_a.width == size_b.width and size_a.height == size_b.height or false
end
---@param size nui_layout_option_size
---@return boolean
function mod_size.contains_percentage_string(size)
return type(size.width) == "string" or type(size.height) == "string"
end
---@param position nui_layout_option_position
---@return boolean
function mod_position.contains_percentage_string(position)
return type(position.row) == "string" or type(position.col) == "string"
end
return mod