diff --git a/doc/windows.txt b/doc/windows.txt index e25c0e4..134e0e5 100644 --- a/doc/windows.txt +++ b/doc/windows.txt @@ -85,6 +85,17 @@ require("windows").setup({ help = 2, }, }, + autoheight = { -- only auto adjust height + enable = false, + winheight = 5, + filetype = { + help = 2, + }, + }, + autoboth = { -- scale both axis. + enable = false, + -- Uses: winwidth, winheight, filetype - from autowidth/autoheight + }, ignore = { -- |windows.ignore| buftype = { "quickfix" }, filetype = { "NvimTree", "neo-tree", "undotree", "gundo" } diff --git a/lua/windows/autoboth.lua b/lua/windows/autoboth.lua new file mode 100644 index 0000000..b44f5d5 --- /dev/null +++ b/lua/windows/autoboth.lua @@ -0,0 +1,136 @@ +local api = vim.api +local fn = vim.fn +local calc_layout = require('windows.calculate-layout') +local config = require('windows.config') +local cache = require('windows.cache') +local Window = require('windows.lib.api').Window +local resize_windows = require('windows.lib.resize-windows').resize_windows +local merge_resize_data = require('windows.lib.resize-windows').merge_resize_data +local tbl_is_empty = vim.tbl_isempty +local autocmd = vim.api.nvim_create_autocmd +local augroup = vim.api.nvim_create_augroup('windows.autoboth', {}) +local command = vim.api.nvim_create_user_command +local M = {} + +local curwin ---@type win.Window +local curbufnr ---@type integer + +---Flag for when a new window has been created. +---@type boolean +local new_window = false + +---To avoid multiple layout resizing in row, when several autocommands were +---triggered. +---@type boolean +M.resizing_request = false + +---@type win.ResizeWindowsAnimated | nil +local animation +if config.animation.enable then + local ResizeWindowsAnimated = require('windows.lib.resize-windows-animated') + animation = ResizeWindowsAnimated:new() +end + +local function setup_layout() + if not curwin or not M.resizing_request then + return + end + M.resizing_request = false + + local winsdata = calc_layout.autoboth(curwin) + if tbl_is_empty(winsdata) then return end + + if cache.maximized then + cache.maximized = nil + end + new_window = false + + if animation then + animation:load(winsdata) + animation:run() + else + resize_windows(winsdata) + end +end + +---Enable autoboth +function M.enable() + autocmd('BufWinEnter', { group = augroup, callback = function(ctx) + local win = Window(0) ---@type win.Window + if win:is_floating() + or (new_window and win:is_ignored()) + or win:get_type() == 'command' -- "[Command Line]" window + then + return + end + + M.resizing_request = true + + curbufnr = ctx.buf + setup_layout() + end }) + + autocmd('VimResized', { group = augroup, callback = function() + M.resizing_request = true + setup_layout() + end }) + + autocmd('WinEnter', { group = augroup, callback = function(ctx) + local win = Window(0) ---@type win.Window + if win:is_floating() + or win:is_ignored() + or (win == curwin and ctx.buf == curbufnr) + then + return + end + curwin = win + + M.resizing_request = true + + -- Defer resizing to handle the case when a new buffer is opened. + -- Then 'BufWinEnter' event will be fired after 'WinEnter'. + vim.defer_fn(setup_layout, 10) + end }) + + autocmd('WinNew', { group = augroup, callback = function() + new_window = true + end }) + + if animation then + autocmd('WinClosed', { group = augroup, callback = function(ctx) + ---Id of the closing window. + local id = tonumber(ctx.match) --[[@as integer]] + local win = Window(id) + + if not win:is_floating() then + animation:finish() + end + end }) + + autocmd('TabLeave', { group = augroup, callback = function() + animation:finish() + end }) + end +end + +---Disable autoboth +function M.disable() + api.nvim_clear_autocmds({ group = augroup }) +end + +---Toggle autoboth +function M.toggle() + if config.autoboth.enable then + M.disable() + config.autoboth.enable = false + else + M.enable() + config.autoboth.enable = true + end +end + +command('WindowsEnableAutoboth', M.enable, { bang = true }) +command('WindowsDisableAutoboth', M.disable, { bang = true }) +command('WindowsToggleAutoboth', M.toggle, { bang = true }) + +return M diff --git a/lua/windows/autoheight.lua b/lua/windows/autoheight.lua new file mode 100644 index 0000000..d65e28f --- /dev/null +++ b/lua/windows/autoheight.lua @@ -0,0 +1,141 @@ +local api = vim.api +local fn = vim.fn +local calc_layout = require('windows.calculate-layout') +local config = require('windows.config') +local cache = require('windows.cache') +local Window = require('windows.lib.api').Window +local resize_windows = require('windows.lib.resize-windows').resize_windows +local merge_resize_data = require('windows.lib.resize-windows').merge_resize_data +local tbl_is_empty = vim.tbl_isempty +local autocmd = vim.api.nvim_create_autocmd +local augroup = vim.api.nvim_create_augroup('windows.autoheight', {}) +local command = vim.api.nvim_create_user_command +local M = {} + +local curwin ---@type win.Window +local curbufnr ---@type integer + +---Flag for when a new window has been created. +---@type boolean +local new_window = false + +---To avoid multiple layout resizing in row, when several autocommands were +---triggered. +---@type boolean +M.resizing_request = false + +---@type win.ResizeWindowsAnimated | nil +local animation +if config.animation.enable then + local ResizeWindowsAnimated = require('windows.lib.resize-windows-animated') + animation = ResizeWindowsAnimated:new() +end + +local function setup_layout() + if not curwin or not M.resizing_request then + return + end + M.resizing_request = false + + local winsdata = calc_layout.autoheight(curwin) + if tbl_is_empty(winsdata) then return end + + if cache.maximized then + if cache.maximized.width then + local width_data = new_window and calc_layout.equalize_wins(true, false) + or cache.maximized.width + winsdata = merge_resize_data(winsdata, width_data) + end + cache.maximized = nil + end + new_window = false + + if animation then + animation:load(winsdata) + animation:run() + else + resize_windows(winsdata) + end +end + +---Enable autoheight +function M.enable() + autocmd('BufWinEnter', { group = augroup, callback = function(ctx) + local win = Window(0) ---@type win.Window + if win:is_floating() + or (new_window and win:is_ignored()) + or win:get_type() == 'command' -- "[Command Line]" window + then + return + end + + M.resizing_request = true + + curbufnr = ctx.buf + setup_layout() + end }) + + autocmd('VimResized', { group = augroup, callback = function() + M.resizing_request = true + setup_layout() + end }) + + autocmd('WinEnter', { group = augroup, callback = function(ctx) + local win = Window(0) ---@type win.Window + if win:is_floating() + or win:is_ignored() + or (win == curwin and ctx.buf == curbufnr) + then + return + end + curwin = win + + M.resizing_request = true + + -- Defer resizing to handle the case when a new buffer is opened. + -- Then 'BufWinEnter' event will be fired after 'WinEnter'. + vim.defer_fn(setup_layout, 10) + end }) + + autocmd('WinNew', { group = augroup, callback = function() + new_window = true + end }) + + if animation then + autocmd('WinClosed', { group = augroup, callback = function(ctx) + ---Id of the closing window. + local id = tonumber(ctx.match) --[[@as integer]] + local win = Window(id) + + if not win:is_floating() then + animation:finish() + end + end }) + + autocmd('TabLeave', { group = augroup, callback = function() + animation:finish() + end }) + end +end + +---Disable autoheight +function M.disable() + api.nvim_clear_autocmds({ group = augroup }) +end + +---Toggle autoheight +function M.toggle() + if config.autoheight.enable then + M.disable() + config.autoheight.enable = false + else + M.enable() + config.autoheight.enable = true + end +end + +command('WindowsEnableAutoheight', M.enable, { bang = true }) +command('WindowsDisableAutoheight', M.disable, { bang = true }) +command('WindowsToggleAutoheight', M.toggle, { bang = true }) + +return M diff --git a/lua/windows/calculate-layout.lua b/lua/windows/calculate-layout.lua index 9784912..4e407ea 100644 --- a/lua/windows/calculate-layout.lua +++ b/lua/windows/calculate-layout.lua @@ -2,7 +2,7 @@ local Frame = require('windows.lib.frame') local merge_resize_data = require('windows.lib.resize-windows').merge_resize_data local M = {} ----Calculate layout for auotwidth +---Calculate layout for autowidth ---@param curwin win.Window ---@return win.WinResizeData[] function M.autowidth(curwin) @@ -31,15 +31,38 @@ function M.autowidth(curwin) end local data = topFrame:get_data_for_width_resizing() + return data +end - -- -------------------------------------------------------- - -- local t = {}; - -- for _, d in ipairs(data) do - -- t[#t+1] = string.format('%d : %d', d.win.id, d.width) - -- end - -- print(table.concat(t, ' | ')) - -- -------------------------------------------------------- +---Calculate layout for autoheight +---@param curwin win.Window +---@return win.WinResizeData[] +function M.autoheight(curwin) + local topFrame = Frame() ---@type win.Frame + if topFrame.type == 'leaf' then + return {} + end + if curwin:is_valid() + and not curwin:is_floating() + and not curwin:get_option('winfixheight') + and not curwin:is_ignored() + then + local curwinLeaf = topFrame:find_window(curwin) + local topFrame_height = topFrame:get_height() + local curwin_wanted_height = curwin:get_wanted_height() + local topFrame_wanted_height = topFrame:get_min_height(curwin, curwin_wanted_height) + + if topFrame_wanted_height > topFrame_height then + topFrame:maximize_window(curwinLeaf, false, true) + else + topFrame:autoheight(curwinLeaf) + end + else + topFrame:equalize_windows(false, true) + end + + local data = topFrame:get_data_for_height_resizing() return data end @@ -63,6 +86,44 @@ function M.maximize_win(win, do_width, do_height) return width_data, height_data end +---Calculate layout for autoboth (width and height) +---@param curwin win.Window +---@return win.WinResizeData[] +function M.autoboth(curwin) + local topFrame = Frame() ---@type win.Frame + if topFrame.type == 'leaf' then + return {} + end + + if curwin:is_valid() + and not curwin:is_floating() + and not curwin:get_option('winfixwidth') + and not curwin:get_option('winfixheight') + and not curwin:is_ignored() + then + local curwinLeaf = topFrame:find_window(curwin) + local topFrame_width = topFrame:get_width() + local topFrame_height = topFrame:get_height() + local curwin_wanted_width = curwin:get_wanted_width() + local curwin_wanted_height = curwin:get_wanted_height() + local topFrame_wanted_width = topFrame:get_min_width(curwin, curwin_wanted_width) + local topFrame_wanted_height = topFrame:get_min_height(curwin, curwin_wanted_height) + + if topFrame_wanted_width > topFrame_width or topFrame_wanted_height > topFrame_height then + topFrame:maximize_window(curwinLeaf, true, true) + else + topFrame:autowidth(curwinLeaf) + topFrame:autoheight(curwinLeaf) + end + else + topFrame:equalize_windows(true, true) + end + + local width_data = topFrame:get_data_for_width_resizing() + local height_data = topFrame:get_data_for_height_resizing() + return merge_resize_data(width_data, height_data) +end + ---@param do_width boolean ---@param do_height boolean ---@return win.WinResizeData[] diff --git a/lua/windows/config.lua b/lua/windows/config.lua index 433192f..1e0a7af 100644 --- a/lua/windows/config.lua +++ b/lua/windows/config.lua @@ -11,12 +11,22 @@ local mt = {} ---@field ignore { buftype: table, filetype: table } local config = { autowidth = { - enable = true, -- false + enable = false, winwidth = 5, filetype = { help = 2, }, }, + autoheight = { + enable = false, + winheight = 5, + filetype = { + help = 1, + }, + }, + autoboth = { + enable = true, + }, ignore = { buftype = { 'quickfix' }, filetype = { @@ -57,4 +67,3 @@ end setmetatable(config, mt) return config - diff --git a/lua/windows/init.lua b/lua/windows/init.lua index 4c255ad..f85380b 100644 --- a/lua/windows/init.lua +++ b/lua/windows/init.lua @@ -10,9 +10,16 @@ function M.setup(input) config.animation.fps, config.animation.easing) end + if config.autoboth.enable then + require('windows.autoboth').enable() + else + if config.autowidth.enable then + require('windows.autowidth').enable() + end - if config.autowidth.enable then - require('windows.autowidth').enable() + if config.autoheight.enable then + require('windows.autoheight').enable() + end end require('windows.commands') diff --git a/lua/windows/lib/api.lua b/lua/windows/lib/api.lua index d92ba68..bc287ae 100644 --- a/lua/windows/lib/api.lua +++ b/lua/windows/lib/api.lua @@ -24,10 +24,10 @@ function Window:get_wanted_width() local buf = self:get_buffer() local ft = buf:get_option('filetype') - local w = config.autowidth.filetype[ft] or config.autowidth.winwidth + local autowidth = config.autowidth.filetype[ft] or config.autowidth.winwidth - if 0 < w and w < 1 then - return math.floor(w * vim.o.columns) + if 0 < autowidth and autowidth < 1 then -- use percentage of vim window width + return math.floor(autowidth * vim.o.columns) end -- Textwidth @@ -35,13 +35,40 @@ function Window:get_wanted_width() local tw = buf:get_option('textwidth') or 80 if tw == 0 then tw = 80 end - if 1 < w and w < 2 then - return math.floor(w * tw) + if 1 < autowidth and autowidth < 2 then + return math.floor(autowidth * tw) else - return tw + w + return tw + autowidth end end +local golden_ratio = 0.61 -- 1 / 1.61803398875 + +---@return integer height +function Window:get_wanted_height() + if self:get_option('winfixheight') then + return self:get_height() + end + + local buf = self:get_buffer() + local ft = buf:get_option('filetype') + local autoheight = config.autoheight.filetype[ft] or config.autoheight.winheight + + if 0 < autoheight and autoheight < 1 then -- use percentage of vim window height + return math.floor(autoheight * vim.o.lines) + end + + local gold_height = golden_ratio * vim.o.lines + local line_count = math.min(vim.api.nvim_buf_line_count(buf.id), gold_height) + if line_count == 0 then line_count = gold_height end + + if 1 < autoheight and autoheight < 2 then + return math.floor(autoheight * line_count) + else + return math.min(line_count + autoheight, gold_height) + end +end + ---@param l win.Window ---@param r win.Window function Window.__eq(l, r) diff --git a/lua/windows/lib/frame.lua b/lua/windows/lib/frame.lua index 5aa360c..6d7da9d 100644 --- a/lua/windows/lib/frame.lua +++ b/lua/windows/lib/frame.lua @@ -93,8 +93,15 @@ function Frame:initialize(layout, id, parent) end ---Calculate frame widths for autowidth functionality. ----@param curwinLeaf win.Frame +---@param curwinLeaf win.Frame|nil function Frame:autowidth(curwinLeaf) + -- Add a guard to handle nil curwinLeaf + if not curwinLeaf then + -- If the current window leaf is nil, fall back to equalize_windows + -- self:equalize_windows(true, false) + return + end + local curwin = curwinLeaf.win local curwinFrame = self:get_child_with_frame(curwinLeaf) @@ -190,6 +197,111 @@ function Frame:autowidth(curwinLeaf) end end +---Calculate frame heights for autoheight functionality. +---@param curwinLeaf win.Frame|nil +function Frame:autoheight(curwinLeaf) + -- Add a guard to handle nil curwinLeaf + if not curwinLeaf then + -- If the current window leaf is nil, fall back to equalize_windows + -- self:equalize_windows(false, true) + return + end + + local curwin = curwinLeaf.win + + local curwinFrame = self:get_child_with_frame(curwinLeaf) + + if self.type == 'row' then + local height = self.new_height + for _, frame in ipairs(self.children) do + frame.new_height = height + if frame.type ~= 'leaf' then + if frame == curwinFrame then + frame:autoheight(curwinLeaf) + else + frame:equalize_windows(false, true) + end + end + end + elseif self.type == 'col' then + local room = self.new_height + local topFrame_leafs = self:get_longest_column() + + local totwincount = #topFrame_leafs + + -- Exclude fixed height frames from consideration. + for _, frame in ipairs(self.children) do + if frame ~= curwinFrame and frame:is_fixed_height() then + local height = frame:get_height() + frame.new_height = height + room = room - height - 1 + frame:equalize_windows(false, true) + + totwincount = totwincount - #frame:get_longest_column() + end + end + + local curwin_wanted_height = curwin:get_wanted_height() + local wanted_height = curwinFrame:get_min_height(curwin, curwin_wanted_height) + + local n = #curwinFrame:get_longest_column() + local N = totwincount + local owed_height = round((room - N + 1) * n / N + n - 1) + + totwincount = totwincount - n + + local height = (wanted_height > owed_height) and wanted_height or owed_height + + -- Remove unnecessary windows "breathing", i.e. changing size in few cells. + if curwinFrame.type == 'leaf' then + local curwin_height = curwin:get_height() + if curwin_height - THRESHOLD < height and height <= curwin_height + THRESHOLD then + height = curwin_height + end + end + + curwinFrame.new_height = height + room = room - height - 1 + if curwinFrame.type ~= 'leaf' then + curwinFrame:autoheight(curwinLeaf) + end + + ---All children frames that are not curwinFrame and not fixed height. + local other_frames = {} ---@type win.Frame[] + for _, frame in ipairs(self.children) do + if frame ~= curwinFrame and not frame:is_fixed_height() then + table.insert(other_frames, frame) + end + end + + local Nf = #other_frames + for i, frame in ipairs(other_frames) do + if i == Nf then + frame.new_height = room + else + local n = #frame:get_longest_column() + local N = totwincount + local h = round((room - N + 1) * n / N + n - 1) + if frame.type == 'leaf' then + -- Remove unnecessary windows "breathing", i.e. changing size in + -- few cells. + local win_height = frame.win:get_height() + if win_height - THRESHOLD < h and h <= win_height + THRESHOLD then + h = win_height + end + end + frame.new_height = h + room = room - h - 1 + totwincount = totwincount - n + end + if frame.type ~= 'leaf' then + frame:equalize_windows(false, true) + end + end + end +end + + ---@param winLeaf win.Frame ---@param do_width boolean ---@param do_height boolean