├── LICENSE ├── README.md ├── doc └── mini-animate.txt └── lua └── mini └── animate.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Evgeni Chasnovski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [![GitHub license](https://badgen.net/github/license/echasnovski/mini.nvim)](https://github.com/echasnovski/mini.nvim/blob/main/LICENSE) 5 | 6 | 7 | ### Animate common Neovim actions 8 | 9 | See more details in [Features](#features) and [help file](doc/mini-animate.txt). 10 | 11 | --- 12 | 13 | ⦿ This is a part of [mini.nvim](https://github.com/echasnovski/mini.nvim) library. Please use [this link](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-animate.md) if you want to mention this module. 14 | 15 | ⦿ All contributions (issues, pull requests, discussions, etc.) are done inside of 'mini.nvim'. 16 | 17 | ⦿ See the repository page to learn about common design principles and configuration recipes. 18 | 19 | --- 20 | 21 | If you want to help this project grow but don't know where to start, check out [contributing guides of 'mini.nvim'](https://github.com/echasnovski/mini.nvim/blob/main/CONTRIBUTING.md) or leave a Github star for 'mini.nvim' project and/or any its standalone Git repositories. 22 | 23 | ## Demo 24 | 25 | https://user-images.githubusercontent.com/24854248/215829092-5aba4e8d-94a5-43da-8ef0-243bf0708f76.mp4 26 | 27 | ## Features 28 | 29 | - Works out of the box with a single `require('mini.animate').setup()`. No extra mappings or commands needed. 30 | - Animate **cursor movement** inside same buffer by showing customizable path. 31 | - Animate **scrolling** with a series of subscrolls ("smooth scrolling"). 32 | - Animate **window resize** by gradually changing sizes of all windows. 33 | - Animate **window open/close** with visually updating floating window. 34 | - Timings for all actions can be customized independently. 35 | - Action animations can be enabled/disabled independently. 36 | - All animations are asynchronous/non-blocking and trigger a targeted event which can be used to perform actions after animation is done. 37 | - `MiniAnimate.animate()` function which can be used to perform own animations. 38 | 39 | Notes: 40 | - Although all animations work in all supported versions of Neovim, scroll and resize animations have best experience with Neovim>=0.9. 41 | - Scroll and resize animations actually change Neovim state to achieve their effects and are asynchronous. This can cause following issues: 42 | - If you have remapped any movement operation to center after it is done (like with `nzvzz` or `zz`), you need to change those mappings. Either remove them or update to use `MiniAnimate.execute_after()` (see `:h MiniAnimate.config.scroll`) 43 | - Using mouse wheel to scroll can appear slower or can have visual jitter. This usually happens due to high number of wheel turns per second: each turn is taking over previous one to start new animation. To mitigate this, you can either modify 'mousescroll' option (set vertical scroll to 1 and use high turn speed or set to high value and use one turn at a time) or `config.scroll` to fine tune when/how scroll animation is done. 44 | 45 | ## Installation 46 | 47 | This plugin can be installed as part of 'mini.nvim' library (**recommended**) or as a standalone Git repository. 48 | 49 | There are two branches to install from: 50 | 51 | - `main` (default, **recommended**) will have latest development version of plugin. All changes since last stable release should be perceived as being in beta testing phase (meaning they already passed alpha-testing and are moderately settled). 52 | - `stable` will be updated only upon releases with code tested during public beta-testing phase in `main` branch. 53 | 54 | Here are code snippets for some common installation methods (use only one): 55 | 56 |
57 | With mini.deps 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
Github repoBranch Code snippet
'mini.nvim' library Main Follow recommended 'mini.deps' installation
Stable
Standalone plugin Main add('echasnovski/mini.animate')
Stable add({ source = 'echasnovski/mini.animate', checkout = 'stable' })
80 |
81 | 82 |
83 | With folke/lazy.nvim 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
Github repoBranch Code snippet
'mini.nvim' libraryMain { 'echasnovski/mini.nvim', version = false },
Stable { 'echasnovski/mini.nvim', version = '*' },
Standalone pluginMain { 'echasnovski/mini.animate', version = false },
Stable { 'echasnovski/mini.animate', version = '*' },
108 |
109 | 110 |
111 | With junegunn/vim-plug 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 |
Github repoBranch Code snippet
'mini.nvim' libraryMain Plug 'echasnovski/mini.nvim'
Stable Plug 'echasnovski/mini.nvim', { 'branch': 'stable' }
Standalone plugin Main Plug 'echasnovski/mini.animate'
Stable Plug 'echasnovski/mini.animate', { 'branch': 'stable' }
135 |
136 | 137 |
138 | 139 | **Important**: don't forget to call `require('mini.animate').setup()` to enable its functionality. 140 | 141 | **Note**: if you are on Windows, there might be problems with too long file paths (like `error: unable to create file : Filename too long`). Try doing one of the following: 142 | - Enable corresponding git global config value: `git config --system core.longpaths true`. Then try to reinstall. 143 | - Install plugin in other place with shorter path. 144 | 145 | ## Default config 146 | 147 | ```lua 148 | -- No need to copy this inside `setup()`. Will be used automatically. 149 | { 150 | -- Cursor path 151 | cursor = { 152 | -- Whether to enable this animation 153 | enable = true, 154 | 155 | -- Timing of animation (how steps will progress in time) 156 | timing = --, 157 | 158 | -- Path generator for visualized cursor movement 159 | path = --, 160 | }, 161 | 162 | -- Vertical scroll 163 | scroll = { 164 | -- Whether to enable this animation 165 | enable = true, 166 | 167 | -- Timing of animation (how steps will progress in time) 168 | timing = --, 169 | 170 | -- Subscroll generator based on total scroll 171 | subscroll = --, 172 | }, 173 | 174 | -- Window resize 175 | resize = { 176 | -- Whether to enable this animation 177 | enable = true, 178 | 179 | -- Timing of animation (how steps will progress in time) 180 | timing = --, 181 | 182 | -- Subresize generator for all steps of resize animations 183 | subresize = --, 184 | }, 185 | 186 | -- Window open 187 | open = { 188 | -- Whether to enable this animation 189 | enable = true, 190 | 191 | -- Timing of animation (how steps will progress in time) 192 | timing = --, 193 | 194 | -- Floating window config generator visualizing specific window 195 | winconfig = --, 196 | 197 | -- 'winblend' (window transparency) generator for floating window 198 | winblend = --, 199 | }, 200 | 201 | -- Window close 202 | close = { 203 | -- Whether to enable this animation 204 | enable = true, 205 | 206 | -- Timing of animation (how steps will progress in time) 207 | timing = --, 208 | 209 | -- Floating window config generator visualizing specific window 210 | winconfig = --, 211 | 212 | -- 'winblend' (window transparency) generator for floating window 213 | winblend = --, 214 | }, 215 | } 216 | ``` 217 | 218 | ## Similar plugins 219 | 220 | - [Neovide](https://neovide.dev/) (Neovim GUI, not a plugin) 221 | - [edluffy/specs.nvim](https://github.com/edluffy/specs.nvim) 222 | - [karb94/neoscroll.nvim](https://github.com/karb94/neoscroll.nvim) 223 | - [anuvyklack/windows.nvim](https://github.com/anuvyklack/windows.nvim) 224 | -------------------------------------------------------------------------------- /doc/mini-animate.txt: -------------------------------------------------------------------------------- 1 | *mini.animate* Animate common Neovim actions 2 | *MiniAnimate* 3 | 4 | MIT License Copyright (c) 2022 Evgeni Chasnovski 5 | 6 | ============================================================================== 7 | 8 | Features: 9 | - Works out of the box with a single `require('mini.animate').setup()`. 10 | No extra mappings or commands needed. 11 | 12 | - Animate cursor movement inside same buffer by showing customizable path. 13 | See |MiniAnimate.config.cursor| for more details. 14 | 15 | - Animate scrolling with a series of subscrolls ("smooth scrolling"). 16 | See |MiniAnimate.config.scroll| for more details. 17 | 18 | - Animate window resize by gradually changing sizes of all windows. 19 | See |MiniAnimate.config.resize| for more details. 20 | 21 | - Animate window open/close with visually updating floating window. 22 | See |MiniAnimate.config.open| and |MiniAnimate.config.close| for more details. 23 | 24 | - Timings for all actions can be customized independently. 25 | See |MiniAnimate-timing| for more details. 26 | 27 | - Action animations can be enabled/disabled independently. 28 | 29 | - All animations are asynchronous/non-blocking and trigger a targeted event 30 | which can be used to perform actions after animation is done. 31 | 32 | - |MiniAnimate.animate()| function which can be used to perform own animations. 33 | 34 | Notes: 35 | - Cursor movement is animated inside same window and buffer, not as cursor 36 | moves across the screen. 37 | 38 | - Scroll and resize animations are done with "side effects": they actually 39 | change the state of what is animated (window view and sizes 40 | respectively). This has a downside of possibly needing extra work to 41 | account for asynchronous nature of animation (like adjusting certain 42 | mappings, etc.). See |MiniAnimate.config.scroll| and 43 | |MiniAnimate.config.resize| for more details. 44 | 45 | - Although all animations work in all supported versions of Neovim, scroll 46 | and resize animations have best experience with Neovim>=0.9. This is due 47 | to updated implementation of |WinScrolled| event. 48 | 49 | # Setup ~ 50 | 51 | This module needs a setup with `require('mini.animate').setup({})` (replace 52 | `{}` with your `config` table). It will create global Lua table `MiniAnimate` 53 | which you can use for scripting or manually (with `:lua MiniAnimate.*`). 54 | 55 | See |MiniAnimate.config| for available config settings. 56 | 57 | You can override runtime config settings (like `config.modifiers`) locally 58 | to buffer inside `vim.b.minianimate_config` which should have same structure 59 | as `MiniAnimate.config`. See |mini.nvim-buffer-local-config| for more details. 60 | 61 | # Comparisons ~ 62 | 63 | - Neovide: 64 | - Neovide is a standalone GUI which has more control over its animations. 65 | While 'mini.animate' works inside terminal emulator (with all its 66 | limitations, like lack of pixel-size control over animations). 67 | - Neovide animates cursor movement across screen, while 'mini.animate' - 68 | as it moves across same buffer. 69 | - Neovide has fixed number of animation effects per action, while 70 | 'mini.animate' is fully customizable. 71 | - 'mini.animate' implements animations for window open/close, while 72 | Neovide does not. 73 | - 'edluffy/specs.nvim': 74 | - 'mini.animate' approaches cursor movement visualization via 75 | customizable path function (uses extmarks), while 'specs.nvim' can 76 | customize within its own visual effects (shading and floating 77 | window resizing). 78 | - 'karb94/neoscroll.nvim': 79 | - Scroll animation is triggered only inside dedicated mappings. 80 | 'mini.animate' animates scroll resulting from any window view change. 81 | - 'anuvyklack/windows.nvim': 82 | - Resize animation is done only within custom commands and mappings, 83 | while 'mini.animate' animates any resize out of the box (works 84 | similarly to 'windows.nvim' in Neovim>=0.9 with appropriate 85 | 'winheight' / 'winwidth' and 'winminheight' / 'winminwidth'). 86 | 87 | # Highlight groups ~ 88 | 89 | * `MiniAnimateCursor` - highlight of cursor during its animated movement. 90 | * `MiniAnimateNormalFloat` - highlight of floating window for `open` and 91 | `close` animations. 92 | 93 | To change any highlight group, modify it directly with |:highlight|. 94 | 95 | # Disabling ~ 96 | 97 | To disable, set `vim.g.minianimate_disable` (globally) or 98 | `vim.b.minianimate_disable` (for a buffer) to `true`. Considering high 99 | number of different scenarios and customization intentions, writing exact 100 | rules for disabling module's functionality is left to user. See 101 | |mini.nvim-disabling-recipes| for common recipes. 102 | 103 | ------------------------------------------------------------------------------ 104 | *MiniAnimate.setup()* 105 | `MiniAnimate.setup`({config}) 106 | Module setup 107 | 108 | Parameters ~ 109 | {config} `(table|nil)` Module config table. See |MiniAnimate.config|. 110 | 111 | Usage ~ 112 | >lua 113 | require('mini.animate').setup() -- use default config 114 | -- OR 115 | require('mini.animate').setup({}) -- replace {} with your config table 116 | < 117 | ------------------------------------------------------------------------------ 118 | *MiniAnimate.config* 119 | `MiniAnimate.config` 120 | Module config 121 | 122 | Default values: 123 | >lua 124 | MiniAnimate.config = { 125 | -- Cursor path 126 | cursor = { 127 | -- Whether to enable this animation 128 | enable = true, 129 | 130 | -- Timing of animation (how steps will progress in time) 131 | timing = --, 132 | 133 | -- Path generator for visualized cursor movement 134 | path = --, 135 | }, 136 | 137 | -- Vertical scroll 138 | scroll = { 139 | -- Whether to enable this animation 140 | enable = true, 141 | 142 | -- Timing of animation (how steps will progress in time) 143 | timing = --, 144 | 145 | -- Subscroll generator based on total scroll 146 | subscroll = --, 147 | }, 148 | 149 | -- Window resize 150 | resize = { 151 | -- Whether to enable this animation 152 | enable = true, 153 | 154 | -- Timing of animation (how steps will progress in time) 155 | timing = --, 156 | 157 | -- Subresize generator for all steps of resize animations 158 | subresize = --, 159 | }, 160 | 161 | -- Window open 162 | open = { 163 | -- Whether to enable this animation 164 | enable = true, 165 | 166 | -- Timing of animation (how steps will progress in time) 167 | timing = --, 168 | 169 | -- Floating window config generator visualizing specific window 170 | winconfig = --, 171 | 172 | -- 'winblend' (window transparency) generator for floating window 173 | winblend = --, 174 | }, 175 | 176 | -- Window close 177 | close = { 178 | -- Whether to enable this animation 179 | enable = true, 180 | 181 | -- Timing of animation (how steps will progress in time) 182 | timing = --, 183 | 184 | -- Floating window config generator visualizing specific window 185 | winconfig = --, 186 | 187 | -- 'winblend' (window transparency) generator for floating window 188 | winblend = --, 189 | }, 190 | } 191 | < 192 | # General ~ 193 | *MiniAnimate-timing* 194 | - Every animation is a non-blockingly scheduled series of specific actions. 195 | They are executed in a sequence of timed steps controlled by `timing` option. 196 | It is a callable which, given next and total step numbers, returns wait time 197 | (in ms). See |MiniAnimate.gen_timing| for builtin timing functions. 198 | See |MiniAnimate.animate()| for more details about animation process. 199 | 200 | - Every animation can be enabled/disabled independently by setting `enable` 201 | option to `true`/`false`. 202 | 203 | *MiniAnimate-done-event* 204 | - Every animation triggers custom |User| event when it is finished. It is 205 | named `MiniAnimateDoneXxx` with `Xxx` replaced by capitalized supported 206 | animation action name (like `MiniAnimateDoneCursor`). Use it to schedule 207 | some action after certain animation is completed. Alternatively, you can 208 | use |MiniAnimate.execute_after()| (usually preferred in mappings). 209 | 210 | - Each animation has its main step generator which defines how particular 211 | animation is done. They all are callables which take some input data and 212 | return an array of step data. Length of that array determines number of 213 | animation steps. Outputs `nil` and empty table result in no animation. 214 | 215 | *MiniAnimate.config.cursor* 216 | # Cursor ~ 217 | 218 | This animation is triggered for each movement of cursor inside same window 219 | and buffer. Its visualization step consists from placing single extmark (see 220 | |extmarks|) at certain position. This extmark contains single space and is 221 | highlighted with `MiniAnimateCursor` highlight group. 222 | 223 | Exact places of extmark and their number is controlled by `path` option. It 224 | is a callable which takes `destination` argument (2d integer point in 225 | `(line, col)` coordinates) and returns array of relative to `(0, 0)` places 226 | for extmark to be placed. Example: 227 | - Input `(2, -3)` means cursor jumped 2 lines forward and 3 columns backward. 228 | - Output `{ {0, 0 }, { 0, -1 }, { 0, -2 }, { 0, -3 }, { 1, -3 } }` means 229 | that path is first visualized along the initial line and then along final 230 | column. 231 | 232 | See |MiniAnimate.gen_path| for builtin path generators. 233 | 234 | Notes: 235 | - Input `destination` value is computed ignoring folds. This is by design 236 | as it helps better visualize distance between two cursor positions. 237 | - Outputs of path generator resulting in a place where extmark can't be 238 | placed are silently omitted during animation: this step won't show any 239 | visualization. 240 | 241 | Configuration example: >lua 242 | 243 | local animate = require('mini.animate') 244 | animate.setup({ 245 | cursor = { 246 | -- Animate for 200 milliseconds with linear easing 247 | timing = animate.gen_timing.linear({ duration = 200, unit = 'total' }), 248 | 249 | -- Animate with shortest line for any cursor move 250 | path = animate.gen_path.line({ 251 | predicate = function() return true end, 252 | }), 253 | } 254 | }) 255 | < 256 | After animation is done, `MiniAnimateDoneCursor` event is triggered. 257 | 258 | *MiniAnimate.config.scroll* 259 | # Scroll ~ 260 | 261 | This animation is triggered for each vertical scroll of current window. 262 | Its visualization step consists from performing a small subscroll which all 263 | in total will result into needed total scroll. 264 | 265 | Exact subscroll values and their number is controlled by `subscroll` option. 266 | It is a callable which takes `total_scroll` argument (single non-negative 267 | integer) and returns array of non-negative integers each representing the 268 | amount of lines needed to be scrolled inside corresponding step. All 269 | subscroll values should sum to input `total_scroll`. 270 | Example: 271 | - Input `5` means that total scroll consists from 5 lines (either up or down, 272 | which doesn't matter). 273 | - Output of `{ 1, 1, 1, 1, 1 }` means that there are 5 equal subscrolls. 274 | 275 | See |MiniAnimate.gen_subscroll| for builtin subscroll generators. 276 | 277 | Notes: 278 | - Input value of `total_scroll` is computed taking folds into account. 279 | - As scroll animation is essentially a precisely scheduled non-blocking 280 | subscrolls, this has two important interconnected consequences: 281 | - If another scroll is attempted during the animation, it is done based 282 | on the **currently visible** window view. Example: if user presses 283 | |CTRL-D| and then |CTRL-U| when animation is half done, window will not 284 | display the previous view half of 'scroll' above it. This especially 285 | affects mouse wheel scrolling, as each its turn results in a new scroll 286 | for number of lines defined by 'mousescroll'. Tweak it to your liking. 287 | - It breaks the use of several relative scrolling commands in the same 288 | command. Use |MiniAnimate.execute_after()| to schedule action after 289 | reaching target window view. 290 | Example: a useful `nnoremap n nzvzz` mapping (consecutive application 291 | of |n|, |zv|, and |zz|) should be expressed in the following way: >lua 292 | 293 | 'lua vim.cmd("normal! n"); ' .. 294 | 'MiniAnimate.execute_after("scroll", "normal! zvzz")' 295 | < 296 | - Default timing might conflict with scrolling via holding a key (like `j` or `k` 297 | with 'wrap' enabled) due to high key repeat rate: next scroll is done before 298 | first step of current one finishes. Resolve this by not scrolling like that 299 | or by ensuring maximum value of step duration to be lower than between 300 | repeated keys: set timing like `function(_, n) return math.min(250/n, 10) end` 301 | or use timing with constant step duration. 302 | - This animation works best with Neovim>=0.9 (after certain updates 303 | to |WinScrolled| event). 304 | 305 | Configuration example: >lua 306 | 307 | local animate = require('mini.animate') 308 | animate.setup({ 309 | scroll = { 310 | -- Animate for 200 milliseconds with linear easing 311 | timing = animate.gen_timing.linear({ duration = 200, unit = 'total' }), 312 | 313 | -- Animate equally but with at most 120 steps instead of default 60 314 | subscroll = animate.gen_subscroll.equal({ max_output_steps = 120 }), 315 | } 316 | }) 317 | < 318 | After animation is done, `MiniAnimateDoneScroll` event is triggered. 319 | 320 | *MiniAnimate.config.resize* 321 | # Resize ~ 322 | 323 | This animation is triggered for window resize while having same layout of 324 | same windows. For example, it won't trigger when window is opened/closed or 325 | after something like |CTRL-W_K|. Its visualization step consists from setting 326 | certain sizes to all visible windows (last step being for "true" final sizes). 327 | 328 | Exact window step sizes and their number is controlled by `subresize` option. 329 | It is a callable which takes `sizes_from` and `sizes_to` arguments (both 330 | tables with window id as keys and dimension table as values) and returns 331 | array of same shaped data. 332 | Example: 333 | - Input: >lua 334 | 335 | -- First 336 | { [1000] = {width = 7, height = 5}, [1001] = {width = 7, height = 10} } 337 | -- Second 338 | { [1000] = {width = 9, height = 5}, [1001] = {width = 5, height = 10} } 339 | -- Means window 1000 increased its width by 2 in expense of window 1001 340 | < 341 | - The following output demonstrates equal resizing: >lua 342 | 343 | { 344 | { [1000] = {width = 8, height = 5}, [1001] = {width = 6, height = 10} }, 345 | { [1000] = {width = 9, height = 5}, [1001] = {width = 5, height = 10} }, 346 | } 347 | < 348 | See |MiniAnimate.gen_subresize| for builtin subresize generators. 349 | 350 | Notes: 351 | 352 | - As resize animation is essentially a precisely scheduled non-blocking 353 | subresizes, this has two important interconnected consequences: 354 | - If another resize is attempted during the animation, it is done based 355 | on the **currently visible** window sizes. This might affect relative 356 | resizing. 357 | - It breaks the use of several relative resizing commands in the same 358 | command. Use |MiniAnimate.execute_after()| to schedule action after 359 | reaching target window sizes. 360 | - This animation works best with Neovim>=0.9 (after certain updates to 361 | |WinScrolled| event). For example, resize resulting from effect of 362 | 'winheight' / 'winwidth' will work properly. 363 | 364 | Configuration example: >lua 365 | 366 | local is_many_wins = function(sizes_from, sizes_to) 367 | return vim.tbl_count(sizes_from) >= 3 368 | end 369 | local animate = require('mini.animate') 370 | animate.setup({ 371 | resize = { 372 | -- Animate for 200 milliseconds with linear easing 373 | timing = animate.gen_timing.linear({ duration = 200, unit = 'total' }), 374 | 375 | -- Animate only if there are at least 3 windows 376 | subresize = animate.gen_subscroll.equal({ predicate = is_many_wins }), 377 | } 378 | }) 379 | < 380 | After animation is done, `MiniAnimateDoneResize` event is triggered. 381 | 382 | *MiniAnimate.config.open* *MiniAnimate.config.close* 383 | # Window open/close ~ 384 | 385 | These animations are similarly triggered for regular (non-floating) window 386 | open/close. Their visualization step consists from drawing empty floating 387 | window with customizable config and transparency. 388 | 389 | Exact window visualization characteristics are controlled by `winconfig` 390 | and `winblend` options. 391 | 392 | The `winconfig` option is a callable which takes window id (|window-ID|) as 393 | input and returns an array of floating window configs (as in `config` 394 | argument of |nvim_open_win()|). Its length determines number of animation steps. 395 | Example: 396 | - The following output results into two animation steps with second being 397 | upper left quarter of a first: >lua 398 | 399 | { 400 | { 401 | row = 0, col = 0, 402 | width = 10, height = 10, 403 | relative = 'editor', anchor = 'NW', focusable = false, 404 | zindex = 1, border = 'none', style = 'minimal', 405 | }, 406 | { 407 | row = 0, col = 0, 408 | width = 5, height = 5, 409 | relative = 'editor', anchor = 'NW', focusable = false, 410 | zindex = 1, border = 'none', style = 'minimal', 411 | }, 412 | } 413 | < 414 | The `winblend` option is similar to `timing` option: it is a callable 415 | which, given current and total step numbers, returns value of floating 416 | window's 'winblend' option. Note, that it is called for current step (so 417 | starts from 0), as opposed to `timing` which is called before step. 418 | Example: 419 | - Function `function(s, n) return 80 + 20 * s / n end` results in linear 420 | transition from `winblend` value of 80 to 100. 421 | 422 | See |MiniAnimate.gen_winconfig| for builtin window config generators. 423 | See |MiniAnimate.gen_winblend| for builtin window transparency generators. 424 | 425 | Configuration example: >lua 426 | 427 | local animate = require('mini.animate') 428 | animate.setup({ 429 | open = { 430 | -- Animate for 400 milliseconds with linear easing 431 | timing = animate.gen_timing.linear({ duration = 400, unit = 'total' }), 432 | 433 | -- Animate with wiping from nearest edge instead of default static one 434 | winconfig = animate.gen_winconfig.wipe({ direction = 'from_edge' }), 435 | 436 | -- Make bigger windows more transparent 437 | winblend = animate.gen_winblend.linear({ from = 80, to = 100 }), 438 | }, 439 | 440 | close = { 441 | -- Animate for 400 milliseconds with linear easing 442 | timing = animate.gen_timing.linear({ duration = 400, unit = 'total' }), 443 | 444 | -- Animate with wiping to nearest edge instead of default static one 445 | winconfig = animate.gen_winconfig.wipe({ direction = 'to_edge' }), 446 | 447 | -- Make bigger windows more transparent 448 | winblend = animate.gen_winblend.linear({ from = 100, to = 80 }), 449 | }, 450 | }) 451 | < 452 | After animation is done, `MiniAnimateDoneOpen` or `MiniAnimateDoneClose` 453 | event is triggered for `open` and `close` animation respectively. 454 | 455 | ------------------------------------------------------------------------------ 456 | *MiniAnimate.is_active()* 457 | `MiniAnimate.is_active`({animation_type}) 458 | Check animation activity 459 | 460 | Parameters ~ 461 | {animation_type} `(string)` One of supported animation types 462 | (entries of |MiniAnimate.config|, like `'cursor'`, etc.). 463 | 464 | Return ~ 465 | `(boolean)` Whether the animation is currently active. 466 | 467 | ------------------------------------------------------------------------------ 468 | *MiniAnimate.execute_after()* 469 | `MiniAnimate.execute_after`({animation_type}, {action}) 470 | Execute action after some animation is done 471 | 472 | Execute action immediately if animation is not active (checked with 473 | |MiniAnimate.is_active()|). Else, schedule its execution until after 474 | animation is done (on corresponding "done event", see 475 | |MiniAnimate-done-event|). 476 | 477 | Mostly meant to be used inside mappings. 478 | 479 | Example ~ 480 | 481 | A useful `nnoremap n nzvzz` mapping (consecutive application of |n|, |zv|, and |zz|) 482 | should be expressed in the following way: >lua 483 | 484 | 'lua vim.cmd("normal! n"); ' .. 485 | 'MiniAnimate.execute_after("scroll", "normal! zvzz")' 486 | < 487 | Parameters ~ 488 | {animation_type} `(string)` One of supported animation types 489 | (as in |MiniAnimate.is_active()|). 490 | {action} `(string|function)` Action to be executed. If string, executed as 491 | command (via |vim.cmd()|). 492 | 493 | ------------------------------------------------------------------------------ 494 | *MiniAnimate.animate()* 495 | `MiniAnimate.animate`({step_action}, {step_timing}, {opts}) 496 | Animate action 497 | 498 | This is equivalent to asynchronous execution of the following algorithm: 499 | - Call `step_action(0)` immediately after calling this function. Stop if 500 | action returned `false` or `nil`. 501 | - Wait `step_timing(1)` milliseconds. 502 | - Call `step_action(1)`. Stop if it returned `false` or `nil`. 503 | - Wait `step_timing(2)` milliseconds. 504 | - Call `step_action(2)`. Stop if it returned `false` or `nil`. 505 | - ... 506 | 507 | Notes: 508 | - Animation is also stopped on action error or if maximum number of steps 509 | is reached. 510 | - Asynchronous execution is done with |uv.new_timer()|. It only allows 511 | integer parts as repeat value. This has several implications: 512 | - Outputs of `step_timing()` are accumulated in order to preserve total 513 | execution time. 514 | - Any wait time less than 1 ms means that action will be executed 515 | immediately. 516 | 517 | Parameters ~ 518 | {step_action} `(function|table)` Callable which takes `step` (integer 0, 1, 2, 519 | etc. indicating current step) and executes some action. Its return value 520 | defines when animation should stop: values `false` and `nil` (equivalent 521 | to no explicit return) stop animation timer; any other continues it. 522 | {step_timing} `(function|table)` Callable which takes `step` (integer 1, 2, etc. 523 | indicating next step) and returns how many milliseconds to wait before 524 | executing this step action. 525 | {opts} `(table|nil)` Options. Possible fields: 526 | - - Maximum value of allowed step to execute. Default: 10000000. 527 | 528 | ------------------------------------------------------------------------------ 529 | *MiniAnimate.gen_timing* 530 | `MiniAnimate.gen_timing` 531 | Generate animation timing 532 | 533 | Each field corresponds to one family of progression which can be customized 534 | further by supplying appropriate arguments. 535 | 536 | This is a table with function elements. Call to actually get timing function. 537 | 538 | Example: >lua 539 | 540 | local animate = require('mini.animate') 541 | animate.setup({ 542 | cursor = { 543 | timing = animate.gen_timing.linear({ duration = 100, unit = 'total' }) 544 | }, 545 | }) 546 | < 547 | See also ~ 548 | |MiniIndentscope.gen_animation| for similar concept in 'mini.indentscope'. 549 | 550 | ------------------------------------------------------------------------------ 551 | *MiniAnimate.gen_timing.none()* 552 | `MiniAnimate.gen_timing.none`() 553 | Generate timing with no animation 554 | 555 | Show final result immediately. Usually better to use `enable` field in `config` 556 | if you want to disable animation. 557 | 558 | ------------------------------------------------------------------------------ 559 | *MiniAnimate.gen_timing.linear()* 560 | `MiniAnimate.gen_timing.linear`({opts}) 561 | Generate timing with linear progression 562 | 563 | Parameters ~ 564 | {opts} `(table|nil)` Options that control progression. Possible keys: 565 | - `(string)` - a subtype of progression. One of "in" 566 | (accelerating from zero speed), "out" (decelerating to zero speed), 567 | "in-out" (default; accelerating halfway, decelerating after). 568 | - `(number)` - duration (in ms) of a unit. Default: 20. 569 | - `(string)` - which unit's duration `opts.duration` controls. One 570 | of "step" (default; ensures average duration of step to be `opts.duration`) 571 | or "total" (ensures fixed total duration regardless of scope's range). 572 | 573 | Return ~ 574 | `(function)` Timing function (see |MiniAnimate-timing|). 575 | 576 | ------------------------------------------------------------------------------ 577 | *MiniAnimate.gen_timing.quadratic()* 578 | `MiniAnimate.gen_timing.quadratic`({opts}) 579 | Generate timing with quadratic progression 580 | 581 | Parameters ~ 582 | {opts} `(table|nil)` Options that control progression. Possible keys: 583 | - `(string)` - a subtype of progression. One of "in" 584 | (accelerating from zero speed), "out" (decelerating to zero speed), 585 | "in-out" (default; accelerating halfway, decelerating after). 586 | - `(number)` - duration (in ms) of a unit. Default: 20. 587 | - `(string)` - which unit's duration `opts.duration` controls. One 588 | of "step" (default; ensures average duration of step to be `opts.duration`) 589 | or "total" (ensures fixed total duration regardless of scope's range). 590 | 591 | Return ~ 592 | `(function)` Timing function (see |MiniAnimate-timing|). 593 | 594 | ------------------------------------------------------------------------------ 595 | *MiniAnimate.gen_timing.cubic()* 596 | `MiniAnimate.gen_timing.cubic`({opts}) 597 | Generate timing with cubic progression 598 | 599 | Parameters ~ 600 | {opts} `(table|nil)` Options that control progression. Possible keys: 601 | - `(string)` - a subtype of progression. One of "in" 602 | (accelerating from zero speed), "out" (decelerating to zero speed), 603 | "in-out" (default; accelerating halfway, decelerating after). 604 | - `(number)` - duration (in ms) of a unit. Default: 20. 605 | - `(string)` - which unit's duration `opts.duration` controls. One 606 | of "step" (default; ensures average duration of step to be `opts.duration`) 607 | or "total" (ensures fixed total duration regardless of scope's range). 608 | 609 | Return ~ 610 | `(function)` Timing function (see |MiniAnimate-timing|). 611 | 612 | ------------------------------------------------------------------------------ 613 | *MiniAnimate.gen_timing.quartic()* 614 | `MiniAnimate.gen_timing.quartic`({opts}) 615 | Generate timing with quartic progression 616 | 617 | Parameters ~ 618 | {opts} `(table|nil)` Options that control progression. Possible keys: 619 | - `(string)` - a subtype of progression. One of "in" 620 | (accelerating from zero speed), "out" (decelerating to zero speed), 621 | "in-out" (default; accelerating halfway, decelerating after). 622 | - `(number)` - duration (in ms) of a unit. Default: 20. 623 | - `(string)` - which unit's duration `opts.duration` controls. One 624 | of "step" (default; ensures average duration of step to be `opts.duration`) 625 | or "total" (ensures fixed total duration regardless of scope's range). 626 | 627 | Return ~ 628 | `(function)` Timing function (see |MiniAnimate-timing|). 629 | 630 | ------------------------------------------------------------------------------ 631 | *MiniAnimate.gen_timing.exponential()* 632 | `MiniAnimate.gen_timing.exponential`({opts}) 633 | Generate timing with exponential progression 634 | 635 | Parameters ~ 636 | {opts} `(table|nil)` Options that control progression. Possible keys: 637 | - `(string)` - a subtype of progression. One of "in" 638 | (accelerating from zero speed), "out" (decelerating to zero speed), 639 | "in-out" (default; accelerating halfway, decelerating after). 640 | - `(number)` - duration (in ms) of a unit. Default: 20. 641 | - `(string)` - which unit's duration `opts.duration` controls. One 642 | of "step" (default; ensures average duration of step to be `opts.duration`) 643 | or "total" (ensures fixed total duration regardless of scope's range). 644 | 645 | Return ~ 646 | `(function)` Timing function (see |MiniAnimate-timing|). 647 | 648 | ------------------------------------------------------------------------------ 649 | *MiniAnimate.gen_path* 650 | `MiniAnimate.gen_path` 651 | Generate cursor animation path 652 | 653 | For more information see |MiniAnimate.config.cursor|. 654 | 655 | This is a table with function elements. Call to actually get generator. 656 | 657 | Example: >lua 658 | 659 | local animate = require('mini.animate') 660 | animate.setup({ 661 | cursor = { 662 | -- Animate with line-column angle instead of shortest line 663 | path = animate.gen_path.angle(), 664 | } 665 | }) 666 | < 667 | ------------------------------------------------------------------------------ 668 | *MiniAnimate.gen_path.line()* 669 | `MiniAnimate.gen_path.line`({opts}) 670 | Generate path as shortest line 671 | 672 | Parameters ~ 673 | {opts} `(table|nil)` Options that control generator. Possible keys: 674 | - `(function)` - a callable which takes `destination` as input and 675 | returns boolean value indicating whether animation should be done. 676 | Default: `false` if `destination` is within one line of origin (reduces 677 | flickering), `true` otherwise. 678 | - `(number)` - maximum number of steps in output. 679 | Default: 1000. 680 | 681 | Return ~ 682 | `(function)` Path function (see |MiniAnimate.config.cursor|). 683 | 684 | ------------------------------------------------------------------------------ 685 | *MiniAnimate.gen_path.angle()* 686 | `MiniAnimate.gen_path.angle`({opts}) 687 | Generate path as line/column angle 688 | 689 | Parameters ~ 690 | {opts} `(table|nil)` Options that control generator. Possible keys: 691 | - `(function)` - a callable which takes `destination` as input and 692 | returns boolean value indicating whether animation should be done. 693 | Default: `false` if `destination` is within one line of origin (reduces 694 | flickering), `true` otherwise. 695 | - `(number)` - maximum number of steps per side in output. 696 | Default: 1000. 697 | - `(string)` - one of `"horizontal"` (default; animates 698 | across initial line first) or `"vertical"` (animates across initial 699 | column first). 700 | 701 | Return ~ 702 | `(function)` Path function (see |MiniAnimate.config.cursor|). 703 | 704 | ------------------------------------------------------------------------------ 705 | *MiniAnimate.gen_path.walls()* 706 | `MiniAnimate.gen_path.walls`({opts}) 707 | Generate path as closing walls at final position 708 | 709 | Parameters ~ 710 | {opts} `(table|nil)` Options that control generator. Possible keys: 711 | - `(function)` - a callable which takes `destination` as input and 712 | returns boolean value indicating whether animation should be done. 713 | Default: `false` if `destination` is within one line of origin (reduces 714 | flickering), `true` otherwise. 715 | - `(number)` - initial width of left and right walls. Default: 10. 716 | 717 | Return ~ 718 | `(function)` Path function (see |MiniAnimate.config.cursor|). 719 | 720 | ------------------------------------------------------------------------------ 721 | *MiniAnimate.gen_path.spiral()* 722 | `MiniAnimate.gen_path.spiral`({opts}) 723 | Generate path as diminishing spiral at final position 724 | 725 | Parameters ~ 726 | {opts} `(table|nil)` Options that control generator. Possible keys: 727 | - `(function)` - a callable which takes `destination` as input and 728 | returns boolean value indicating whether animation should be done. 729 | Default: `false` if `destination` is within one line of origin (reduces 730 | flickering), `true` otherwise. 731 | - `(number)` - initial width of spiral. Default: 2. 732 | 733 | Return ~ 734 | `(function)` Path function (see |MiniAnimate.config.cursor|). 735 | 736 | ------------------------------------------------------------------------------ 737 | *MiniAnimate.gen_subscroll* 738 | `MiniAnimate.gen_subscroll` 739 | Generate scroll animation subscroll 740 | 741 | For more information see |MiniAnimate.config.scroll|. 742 | 743 | This is a table with function elements. Call to actually get generator. 744 | 745 | Example: >lua 746 | 747 | local animate = require('mini.animate') 748 | animate.setup({ 749 | scroll = { 750 | -- Animate equally but with 120 maximum steps instead of default 60 751 | subscroll = animate.gen_subscroll.equal({ max_output_steps = 120 }), 752 | } 753 | }) 754 | < 755 | ------------------------------------------------------------------------------ 756 | *MiniAnimate.gen_subscroll.equal()* 757 | `MiniAnimate.gen_subscroll.equal`({opts}) 758 | Generate subscroll with equal steps 759 | 760 | Parameters ~ 761 | {opts} `(table|nil)` Options that control generator. Possible keys: 762 | - `(function)` - a callable which takes `total_scroll` as 763 | input and returns boolean value indicating whether animation should be 764 | done. Default: `false` if `total_scroll` is 1 or less (reduces 765 | unnecessary waiting), `true` otherwise. 766 | - `(number)` - maximum number of subscroll steps in output. 767 | Adjust this to reduce computations in expense of reduced smoothness. 768 | Default: 60. 769 | 770 | Return ~ 771 | `(function)` Subscroll function (see |MiniAnimate.config.scroll|). 772 | 773 | ------------------------------------------------------------------------------ 774 | *MiniAnimate.gen_subresize* 775 | `MiniAnimate.gen_subresize` 776 | Generate resize animation subresize 777 | 778 | For more information see |MiniAnimate.config.resize|. 779 | 780 | This is a table with function elements. Call to actually get generator. 781 | 782 | Example: >lua 783 | 784 | local is_many_wins = function(sizes_from, sizes_to) 785 | return vim.tbl_count(sizes_from) >= 3 786 | end 787 | local animate = require('mini.animate') 788 | animate.setup({ 789 | resize = { 790 | -- Animate only if there are at least 3 windows 791 | subresize = animate.gen_subresize.equal({ predicate = is_many_wins }), 792 | } 793 | }) 794 | < 795 | ------------------------------------------------------------------------------ 796 | *MiniAnimate.gen_subresize.equal()* 797 | `MiniAnimate.gen_subresize.equal`({opts}) 798 | Generate subresize with equal steps 799 | 800 | Parameters ~ 801 | {opts} `(table|nil)` Options that control generator. Possible keys: 802 | - `(function)` - a callable which takes `sizes_from` and 803 | `sizes_to` as input and returns boolean value indicating whether 804 | animation should be done. Default: always `true`. 805 | 806 | Return ~ 807 | `(function)` Subresize function (see |MiniAnimate.config.resize|). 808 | 809 | ------------------------------------------------------------------------------ 810 | *MiniAnimate.gen_winconfig* 811 | `MiniAnimate.gen_winconfig` 812 | Generate open/close animation winconfig 813 | 814 | For more information see |MiniAnimate.config.open| or |MiniAnimate.config.close|. 815 | 816 | This is a table with function elements. Call to actually get generator. 817 | 818 | Example: >lua 819 | 820 | local is_not_single_window = function(win_id) 821 | local tabpage_id = vim.api.nvim_win_get_tabpage(win_id) 822 | return #vim.api.nvim_tabpage_list_wins(tabpage_id) > 1 823 | end 824 | local animate = require('mini.animate') 825 | animate.setup({ 826 | open = { 827 | -- Animate with wiping from nearest edge instead of default static one 828 | -- and only if it is not a single window in tabpage 829 | winconfig = animate.gen_winconfig.wipe({ 830 | predicate = is_not_single_window, 831 | direction = 'from_edge', 832 | }), 833 | }, 834 | close = { 835 | -- Animate with wiping to nearest edge instead of default static one 836 | -- and only if it is not a single window in tabpage 837 | winconfig = animate.gen_winconfig.wipe({ 838 | predicate = is_not_single_window, 839 | direction = 'to_edge', 840 | }), 841 | }, 842 | }) 843 | < 844 | ------------------------------------------------------------------------------ 845 | *MiniAnimate.gen_winconfig.static()* 846 | `MiniAnimate.gen_winconfig.static`({opts}) 847 | Generate winconfig for static floating window 848 | 849 | This will result into floating window statically covering whole target 850 | window. 851 | 852 | Parameters ~ 853 | {opts} `(table|nil)` Options that control generator. Possible keys: 854 | - `(function)` - a callable which takes `win_id` as input and 855 | returns boolean value indicating whether animation should be done. 856 | Default: always `true`. 857 | - `(number)` - number of output steps, all with same config. 858 | Useful to tweak smoothness of transparency animation (done inside 859 | `winblend` config option). Default: 25. 860 | 861 | Return ~ 862 | `(function)` Winconfig function (see |MiniAnimate.config.open| 863 | or |MiniAnimate.config.close|). 864 | 865 | ------------------------------------------------------------------------------ 866 | *MiniAnimate.gen_winconfig.center()* 867 | `MiniAnimate.gen_winconfig.center`({opts}) 868 | Generate winconfig for center-focused animated floating window 869 | 870 | This will result into floating window growing from or shrinking to the 871 | target window center. 872 | 873 | Parameters ~ 874 | {opts} `(table|nil)` Options that control generator. Possible keys: 875 | - `(function)` - a callable which takes `win_id` as input and 876 | returns boolean value indicating whether animation should be done. 877 | Default: always `true`. 878 | - `(string)` - one of `"to_center"` (default; window will 879 | shrink from full coverage to center) or `"from_center"` (window will 880 | grow from center to full coverage). 881 | 882 | Return ~ 883 | `(function)` Winconfig function (see |MiniAnimate.config.open| 884 | or |MiniAnimate.config.close|). 885 | 886 | ------------------------------------------------------------------------------ 887 | *MiniAnimate.gen_winconfig.wipe()* 888 | `MiniAnimate.gen_winconfig.wipe`({opts}) 889 | Generate winconfig for wiping animated floating window 890 | 891 | This will result into floating window growing from or shrinking to the 892 | nearest edge. This also takes into account the split type of target window: 893 | vertically split window will progress towards vertical edge; horizontally - 894 | towards horizontal. 895 | 896 | Parameters ~ 897 | {opts} `(table|nil)` Options that control generator. Possible keys: 898 | - `(function)` - a callable which takes `win_id` as input and 899 | returns boolean value indicating whether animation should be done. 900 | Default: always `true`. 901 | - `(string)` - one of `"to_edge"` (default; window will 902 | shrink from full coverage to nearest edge) or `"from_edge"` (window 903 | will grow from edge to full coverage). 904 | 905 | Return ~ 906 | `(function)` Winconfig function (see |MiniAnimate.config.open| 907 | or |MiniAnimate.config.close|). 908 | 909 | ------------------------------------------------------------------------------ 910 | *MiniAnimate.gen_winblend* 911 | `MiniAnimate.gen_winblend` 912 | Generate open/close animation `winblend` progression 913 | 914 | For more information see |MiniAnimate.config.open| or |MiniAnimate.config.close|. 915 | 916 | This is a table with function elements. Call to actually get transparency 917 | function. 918 | 919 | Example: >lua 920 | 921 | local animate = require('mini.animate') 922 | animate.setup({ 923 | open = { 924 | -- Change transparency from 60 to 80 instead of default 80 to 100 925 | winblend = animate.gen_winblend.linear({ from = 60, to = 80 }), 926 | }, 927 | close = { 928 | -- Change transparency from 60 to 80 instead of default 80 to 100 929 | winblend = animate.gen_winblend.linear({ from = 60, to = 80 }), 930 | }, 931 | }) 932 | < 933 | ------------------------------------------------------------------------------ 934 | *MiniAnimate.gen_winblend.linear()* 935 | `MiniAnimate.gen_winblend.linear`({opts}) 936 | Generate linear `winblend` progression 937 | 938 | Parameters ~ 939 | {opts} `(table|nil)` Options that control generator. Possible keys: 940 | - `(number)` - initial value of 'winblend'. 941 | - `(number)` - final value of 'winblend'. 942 | 943 | Return ~ 944 | `(function)` Winblend function (see |MiniAnimate.config.open| 945 | or |MiniAnimate.config.close|). 946 | 947 | 948 | vim:tw=78:ts=8:noet:ft=help:norl: -------------------------------------------------------------------------------- /lua/mini/animate.lua: -------------------------------------------------------------------------------- 1 | --- *mini.animate* Animate common Neovim actions 2 | --- *MiniAnimate* 3 | --- 4 | --- MIT License Copyright (c) 2022 Evgeni Chasnovski 5 | --- 6 | --- ============================================================================== 7 | --- 8 | --- Features: 9 | --- - Works out of the box with a single `require('mini.animate').setup()`. 10 | --- No extra mappings or commands needed. 11 | --- 12 | --- - Animate cursor movement inside same buffer by showing customizable path. 13 | --- See |MiniAnimate.config.cursor| for more details. 14 | --- 15 | --- - Animate scrolling with a series of subscrolls ("smooth scrolling"). 16 | --- See |MiniAnimate.config.scroll| for more details. 17 | --- 18 | --- - Animate window resize by gradually changing sizes of all windows. 19 | --- See |MiniAnimate.config.resize| for more details. 20 | --- 21 | --- - Animate window open/close with visually updating floating window. 22 | --- See |MiniAnimate.config.open| and |MiniAnimate.config.close| for more details. 23 | --- 24 | --- - Timings for all actions can be customized independently. 25 | --- See |MiniAnimate-timing| for more details. 26 | --- 27 | --- - Action animations can be enabled/disabled independently. 28 | --- 29 | --- - All animations are asynchronous/non-blocking and trigger a targeted event 30 | --- which can be used to perform actions after animation is done. 31 | --- 32 | --- - |MiniAnimate.animate()| function which can be used to perform own animations. 33 | --- 34 | --- Notes: 35 | --- - Cursor movement is animated inside same window and buffer, not as cursor 36 | --- moves across the screen. 37 | --- 38 | --- - Scroll and resize animations are done with "side effects": they actually 39 | --- change the state of what is animated (window view and sizes 40 | --- respectively). This has a downside of possibly needing extra work to 41 | --- account for asynchronous nature of animation (like adjusting certain 42 | --- mappings, etc.). See |MiniAnimate.config.scroll| and 43 | --- |MiniAnimate.config.resize| for more details. 44 | --- 45 | --- - Although all animations work in all supported versions of Neovim, scroll 46 | --- and resize animations have best experience with Neovim>=0.9. This is due 47 | --- to updated implementation of |WinScrolled| event. 48 | --- 49 | --- # Setup ~ 50 | --- 51 | --- This module needs a setup with `require('mini.animate').setup({})` (replace 52 | --- `{}` with your `config` table). It will create global Lua table `MiniAnimate` 53 | --- which you can use for scripting or manually (with `:lua MiniAnimate.*`). 54 | --- 55 | --- See |MiniAnimate.config| for available config settings. 56 | --- 57 | --- You can override runtime config settings (like `config.modifiers`) locally 58 | --- to buffer inside `vim.b.minianimate_config` which should have same structure 59 | --- as `MiniAnimate.config`. See |mini.nvim-buffer-local-config| for more details. 60 | --- 61 | --- # Comparisons ~ 62 | --- 63 | --- - Neovide: 64 | --- - Neovide is a standalone GUI which has more control over its animations. 65 | --- While 'mini.animate' works inside terminal emulator (with all its 66 | --- limitations, like lack of pixel-size control over animations). 67 | --- - Neovide animates cursor movement across screen, while 'mini.animate' - 68 | --- as it moves across same buffer. 69 | --- - Neovide has fixed number of animation effects per action, while 70 | --- 'mini.animate' is fully customizable. 71 | --- - 'mini.animate' implements animations for window open/close, while 72 | --- Neovide does not. 73 | --- - 'edluffy/specs.nvim': 74 | --- - 'mini.animate' approaches cursor movement visualization via 75 | --- customizable path function (uses extmarks), while 'specs.nvim' can 76 | --- customize within its own visual effects (shading and floating 77 | --- window resizing). 78 | --- - 'karb94/neoscroll.nvim': 79 | --- - Scroll animation is triggered only inside dedicated mappings. 80 | --- 'mini.animate' animates scroll resulting from any window view change. 81 | --- - 'anuvyklack/windows.nvim': 82 | --- - Resize animation is done only within custom commands and mappings, 83 | --- while 'mini.animate' animates any resize out of the box (works 84 | --- similarly to 'windows.nvim' in Neovim>=0.9 with appropriate 85 | --- 'winheight' / 'winwidth' and 'winminheight' / 'winminwidth'). 86 | --- 87 | --- # Highlight groups ~ 88 | --- 89 | --- * `MiniAnimateCursor` - highlight of cursor during its animated movement. 90 | --- * `MiniAnimateNormalFloat` - highlight of floating window for `open` and 91 | --- `close` animations. 92 | --- 93 | --- To change any highlight group, modify it directly with |:highlight|. 94 | --- 95 | --- # Disabling ~ 96 | --- 97 | --- To disable, set `vim.g.minianimate_disable` (globally) or 98 | --- `vim.b.minianimate_disable` (for a buffer) to `true`. Considering high 99 | --- number of different scenarios and customization intentions, writing exact 100 | --- rules for disabling module's functionality is left to user. See 101 | --- |mini.nvim-disabling-recipes| for common recipes. 102 | 103 | ---@diagnostic disable:undefined-field 104 | 105 | -- Module definition ========================================================== 106 | local MiniAnimate = {} 107 | local H = {} 108 | 109 | --- Module setup 110 | --- 111 | ---@param config table|nil Module config table. See |MiniAnimate.config|. 112 | --- 113 | ---@usage >lua 114 | --- require('mini.animate').setup() -- use default config 115 | --- -- OR 116 | --- require('mini.animate').setup({}) -- replace {} with your config table 117 | --- < 118 | MiniAnimate.setup = function(config) 119 | -- TODO: Remove after Neovim=0.8 support is dropped 120 | if vim.fn.has('nvim-0.9') == 0 then 121 | vim.notify( 122 | '(mini.animate) Neovim<0.9 is soft deprecated (module works but not supported).' 123 | .. ' It will be deprecated after next "mini.nvim" release (module might not work).' 124 | .. ' Please update your Neovim version.' 125 | ) 126 | end 127 | 128 | -- Export module 129 | _G.MiniAnimate = MiniAnimate 130 | 131 | -- Setup config 132 | config = H.setup_config(config) 133 | 134 | -- Apply config 135 | H.apply_config(config) 136 | 137 | -- Define behavior 138 | H.create_autocommands() 139 | H.track_scroll_state() 140 | 141 | -- Create default highlighting 142 | H.create_default_hl() 143 | end 144 | 145 | --- Module config 146 | --- 147 | --- Default values: 148 | ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) 149 | ---@text # General ~ 150 | --- *MiniAnimate-timing* 151 | --- - Every animation is a non-blockingly scheduled series of specific actions. 152 | --- They are executed in a sequence of timed steps controlled by `timing` option. 153 | --- It is a callable which, given next and total step numbers, returns wait time 154 | --- (in ms). See |MiniAnimate.gen_timing| for builtin timing functions. 155 | --- See |MiniAnimate.animate()| for more details about animation process. 156 | --- 157 | --- - Every animation can be enabled/disabled independently by setting `enable` 158 | --- option to `true`/`false`. 159 | --- 160 | --- *MiniAnimate-done-event* 161 | --- - Every animation triggers custom |User| event when it is finished. It is 162 | --- named `MiniAnimateDoneXxx` with `Xxx` replaced by capitalized supported 163 | --- animation action name (like `MiniAnimateDoneCursor`). Use it to schedule 164 | --- some action after certain animation is completed. Alternatively, you can 165 | --- use |MiniAnimate.execute_after()| (usually preferred in mappings). 166 | --- 167 | --- - Each animation has its main step generator which defines how particular 168 | --- animation is done. They all are callables which take some input data and 169 | --- return an array of step data. Length of that array determines number of 170 | --- animation steps. Outputs `nil` and empty table result in no animation. 171 | --- 172 | --- *MiniAnimate.config.cursor* 173 | --- # Cursor ~ 174 | --- 175 | --- This animation is triggered for each movement of cursor inside same window 176 | --- and buffer. Its visualization step consists from placing single extmark (see 177 | --- |extmarks|) at certain position. This extmark contains single space and is 178 | --- highlighted with `MiniAnimateCursor` highlight group. 179 | --- 180 | --- Exact places of extmark and their number is controlled by `path` option. It 181 | --- is a callable which takes `destination` argument (2d integer point in 182 | --- `(line, col)` coordinates) and returns array of relative to `(0, 0)` places 183 | --- for extmark to be placed. Example: 184 | --- - Input `(2, -3)` means cursor jumped 2 lines forward and 3 columns backward. 185 | --- - Output `{ {0, 0 }, { 0, -1 }, { 0, -2 }, { 0, -3 }, { 1, -3 } }` means 186 | --- that path is first visualized along the initial line and then along final 187 | --- column. 188 | --- 189 | --- See |MiniAnimate.gen_path| for builtin path generators. 190 | --- 191 | --- Notes: 192 | --- - Input `destination` value is computed ignoring folds. This is by design 193 | --- as it helps better visualize distance between two cursor positions. 194 | --- - Outputs of path generator resulting in a place where extmark can't be 195 | --- placed are silently omitted during animation: this step won't show any 196 | --- visualization. 197 | --- 198 | --- Configuration example: >lua 199 | --- 200 | --- local animate = require('mini.animate') 201 | --- animate.setup({ 202 | --- cursor = { 203 | --- -- Animate for 200 milliseconds with linear easing 204 | --- timing = animate.gen_timing.linear({ duration = 200, unit = 'total' }), 205 | --- 206 | --- -- Animate with shortest line for any cursor move 207 | --- path = animate.gen_path.line({ 208 | --- predicate = function() return true end, 209 | --- }), 210 | --- } 211 | --- }) 212 | --- < 213 | --- After animation is done, `MiniAnimateDoneCursor` event is triggered. 214 | --- 215 | --- *MiniAnimate.config.scroll* 216 | --- # Scroll ~ 217 | --- 218 | --- This animation is triggered for each vertical scroll of current window. 219 | --- Its visualization step consists from performing a small subscroll which all 220 | --- in total will result into needed total scroll. 221 | --- 222 | --- Exact subscroll values and their number is controlled by `subscroll` option. 223 | --- It is a callable which takes `total_scroll` argument (single non-negative 224 | --- integer) and returns array of non-negative integers each representing the 225 | --- amount of lines needed to be scrolled inside corresponding step. All 226 | --- subscroll values should sum to input `total_scroll`. 227 | --- Example: 228 | --- - Input `5` means that total scroll consists from 5 lines (either up or down, 229 | --- which doesn't matter). 230 | --- - Output of `{ 1, 1, 1, 1, 1 }` means that there are 5 equal subscrolls. 231 | --- 232 | --- See |MiniAnimate.gen_subscroll| for builtin subscroll generators. 233 | --- 234 | --- Notes: 235 | --- - Input value of `total_scroll` is computed taking folds into account. 236 | --- - As scroll animation is essentially a precisely scheduled non-blocking 237 | --- subscrolls, this has two important interconnected consequences: 238 | --- - If another scroll is attempted during the animation, it is done based 239 | --- on the **currently visible** window view. Example: if user presses 240 | --- |CTRL-D| and then |CTRL-U| when animation is half done, window will not 241 | --- display the previous view half of 'scroll' above it. This especially 242 | --- affects mouse wheel scrolling, as each its turn results in a new scroll 243 | --- for number of lines defined by 'mousescroll'. Tweak it to your liking. 244 | --- - It breaks the use of several relative scrolling commands in the same 245 | --- command. Use |MiniAnimate.execute_after()| to schedule action after 246 | --- reaching target window view. 247 | --- Example: a useful `nnoremap n nzvzz` mapping (consecutive application 248 | --- of |n|, |zv|, and |zz|) should be expressed in the following way: >lua 249 | --- 250 | --- 'lua vim.cmd("normal! n"); ' .. 251 | --- 'MiniAnimate.execute_after("scroll", "normal! zvzz")' 252 | --- < 253 | --- - Default timing might conflict with scrolling via holding a key (like `j` or `k` 254 | --- with 'wrap' enabled) due to high key repeat rate: next scroll is done before 255 | --- first step of current one finishes. Resolve this by not scrolling like that 256 | --- or by ensuring maximum value of step duration to be lower than between 257 | --- repeated keys: set timing like `function(_, n) return math.min(250/n, 10) end` 258 | --- or use timing with constant step duration. 259 | --- - This animation works best with Neovim>=0.9 (after certain updates 260 | --- to |WinScrolled| event). 261 | --- 262 | --- Configuration example: >lua 263 | --- 264 | --- local animate = require('mini.animate') 265 | --- animate.setup({ 266 | --- scroll = { 267 | --- -- Animate for 200 milliseconds with linear easing 268 | --- timing = animate.gen_timing.linear({ duration = 200, unit = 'total' }), 269 | --- 270 | --- -- Animate equally but with at most 120 steps instead of default 60 271 | --- subscroll = animate.gen_subscroll.equal({ max_output_steps = 120 }), 272 | --- } 273 | --- }) 274 | --- < 275 | --- After animation is done, `MiniAnimateDoneScroll` event is triggered. 276 | --- 277 | --- *MiniAnimate.config.resize* 278 | --- # Resize ~ 279 | --- 280 | --- This animation is triggered for window resize while having same layout of 281 | --- same windows. For example, it won't trigger when window is opened/closed or 282 | --- after something like |CTRL-W_K|. Its visualization step consists from setting 283 | --- certain sizes to all visible windows (last step being for "true" final sizes). 284 | --- 285 | --- Exact window step sizes and their number is controlled by `subresize` option. 286 | --- It is a callable which takes `sizes_from` and `sizes_to` arguments (both 287 | --- tables with window id as keys and dimension table as values) and returns 288 | --- array of same shaped data. 289 | --- Example: 290 | --- - Input: >lua 291 | --- 292 | --- -- First 293 | --- { [1000] = {width = 7, height = 5}, [1001] = {width = 7, height = 10} } 294 | --- -- Second 295 | --- { [1000] = {width = 9, height = 5}, [1001] = {width = 5, height = 10} } 296 | --- -- Means window 1000 increased its width by 2 in expense of window 1001 297 | --- < 298 | --- - The following output demonstrates equal resizing: >lua 299 | --- 300 | --- { 301 | --- { [1000] = {width = 8, height = 5}, [1001] = {width = 6, height = 10} }, 302 | --- { [1000] = {width = 9, height = 5}, [1001] = {width = 5, height = 10} }, 303 | --- } 304 | --- < 305 | --- See |MiniAnimate.gen_subresize| for builtin subresize generators. 306 | --- 307 | --- Notes: 308 | --- 309 | --- - As resize animation is essentially a precisely scheduled non-blocking 310 | --- subresizes, this has two important interconnected consequences: 311 | --- - If another resize is attempted during the animation, it is done based 312 | --- on the **currently visible** window sizes. This might affect relative 313 | --- resizing. 314 | --- - It breaks the use of several relative resizing commands in the same 315 | --- command. Use |MiniAnimate.execute_after()| to schedule action after 316 | --- reaching target window sizes. 317 | --- - This animation works best with Neovim>=0.9 (after certain updates to 318 | --- |WinScrolled| event). For example, resize resulting from effect of 319 | --- 'winheight' / 'winwidth' will work properly. 320 | --- 321 | --- Configuration example: >lua 322 | --- 323 | --- local is_many_wins = function(sizes_from, sizes_to) 324 | --- return vim.tbl_count(sizes_from) >= 3 325 | --- end 326 | --- local animate = require('mini.animate') 327 | --- animate.setup({ 328 | --- resize = { 329 | --- -- Animate for 200 milliseconds with linear easing 330 | --- timing = animate.gen_timing.linear({ duration = 200, unit = 'total' }), 331 | --- 332 | --- -- Animate only if there are at least 3 windows 333 | --- subresize = animate.gen_subscroll.equal({ predicate = is_many_wins }), 334 | --- } 335 | --- }) 336 | --- < 337 | --- After animation is done, `MiniAnimateDoneResize` event is triggered. 338 | --- 339 | --- *MiniAnimate.config.open* *MiniAnimate.config.close* 340 | --- # Window open/close ~ 341 | --- 342 | --- These animations are similarly triggered for regular (non-floating) window 343 | --- open/close. Their visualization step consists from drawing empty floating 344 | --- window with customizable config and transparency. 345 | --- 346 | --- Exact window visualization characteristics are controlled by `winconfig` 347 | --- and `winblend` options. 348 | --- 349 | --- The `winconfig` option is a callable which takes window id (|window-ID|) as 350 | --- input and returns an array of floating window configs (as in `config` 351 | --- argument of |nvim_open_win()|). Its length determines number of animation steps. 352 | --- Example: 353 | --- - The following output results into two animation steps with second being 354 | --- upper left quarter of a first: >lua 355 | --- 356 | --- { 357 | --- { 358 | --- row = 0, col = 0, 359 | --- width = 10, height = 10, 360 | --- relative = 'editor', anchor = 'NW', focusable = false, 361 | --- zindex = 1, border = 'none', style = 'minimal', 362 | --- }, 363 | --- { 364 | --- row = 0, col = 0, 365 | --- width = 5, height = 5, 366 | --- relative = 'editor', anchor = 'NW', focusable = false, 367 | --- zindex = 1, border = 'none', style = 'minimal', 368 | --- }, 369 | --- } 370 | --- < 371 | --- The `winblend` option is similar to `timing` option: it is a callable 372 | --- which, given current and total step numbers, returns value of floating 373 | --- window's 'winblend' option. Note, that it is called for current step (so 374 | --- starts from 0), as opposed to `timing` which is called before step. 375 | --- Example: 376 | --- - Function `function(s, n) return 80 + 20 * s / n end` results in linear 377 | --- transition from `winblend` value of 80 to 100. 378 | --- 379 | --- See |MiniAnimate.gen_winconfig| for builtin window config generators. 380 | --- See |MiniAnimate.gen_winblend| for builtin window transparency generators. 381 | --- 382 | --- Configuration example: >lua 383 | --- 384 | --- local animate = require('mini.animate') 385 | --- animate.setup({ 386 | --- open = { 387 | --- -- Animate for 400 milliseconds with linear easing 388 | --- timing = animate.gen_timing.linear({ duration = 400, unit = 'total' }), 389 | --- 390 | --- -- Animate with wiping from nearest edge instead of default static one 391 | --- winconfig = animate.gen_winconfig.wipe({ direction = 'from_edge' }), 392 | --- 393 | --- -- Make bigger windows more transparent 394 | --- winblend = animate.gen_winblend.linear({ from = 80, to = 100 }), 395 | --- }, 396 | --- 397 | --- close = { 398 | --- -- Animate for 400 milliseconds with linear easing 399 | --- timing = animate.gen_timing.linear({ duration = 400, unit = 'total' }), 400 | --- 401 | --- -- Animate with wiping to nearest edge instead of default static one 402 | --- winconfig = animate.gen_winconfig.wipe({ direction = 'to_edge' }), 403 | --- 404 | --- -- Make bigger windows more transparent 405 | --- winblend = animate.gen_winblend.linear({ from = 100, to = 80 }), 406 | --- }, 407 | --- }) 408 | --- < 409 | --- After animation is done, `MiniAnimateDoneOpen` or `MiniAnimateDoneClose` 410 | --- event is triggered for `open` and `close` animation respectively. 411 | MiniAnimate.config = { 412 | -- Cursor path 413 | cursor = { 414 | -- Whether to enable this animation 415 | enable = true, 416 | 417 | -- Timing of animation (how steps will progress in time) 418 | --minidoc_replace_start timing = --, 419 | timing = function(_, n) return 250 / n end, 420 | --minidoc_replace_end 421 | 422 | -- Path generator for visualized cursor movement 423 | --minidoc_replace_start path = --, 424 | path = function(destination) 425 | return H.path_line(destination, { predicate = H.default_path_predicate, max_output_steps = 1000 }) 426 | end, 427 | --minidoc_replace_end 428 | }, 429 | 430 | -- Vertical scroll 431 | scroll = { 432 | -- Whether to enable this animation 433 | enable = true, 434 | 435 | -- Timing of animation (how steps will progress in time) 436 | --minidoc_replace_start timing = --, 437 | timing = function(_, n) return 250 / n end, 438 | --minidoc_replace_end 439 | 440 | -- Subscroll generator based on total scroll 441 | --minidoc_replace_start subscroll = --, 442 | subscroll = function(total_scroll) 443 | return H.subscroll_equal(total_scroll, { predicate = H.default_subscroll_predicate, max_output_steps = 60 }) 444 | end, 445 | --minidoc_replace_end 446 | }, 447 | 448 | -- Window resize 449 | resize = { 450 | -- Whether to enable this animation 451 | enable = true, 452 | 453 | -- Timing of animation (how steps will progress in time) 454 | --minidoc_replace_start timing = --, 455 | timing = function(_, n) return 250 / n end, 456 | --minidoc_replace_end 457 | 458 | -- Subresize generator for all steps of resize animations 459 | --minidoc_replace_start subresize = --, 460 | subresize = function(sizes_from, sizes_to) 461 | return H.subresize_equal(sizes_from, sizes_to, { predicate = H.default_subresize_predicate }) 462 | end, 463 | --minidoc_replace_end 464 | }, 465 | 466 | -- Window open 467 | open = { 468 | -- Whether to enable this animation 469 | enable = true, 470 | 471 | -- Timing of animation (how steps will progress in time) 472 | --minidoc_replace_start timing = --, 473 | timing = function(_, n) return 250 / n end, 474 | --minidoc_replace_end 475 | 476 | -- Floating window config generator visualizing specific window 477 | --minidoc_replace_start winconfig = --, 478 | winconfig = function(win_id) 479 | return H.winconfig_static(win_id, { predicate = H.default_winconfig_predicate, n_steps = 25 }) 480 | end, 481 | --minidoc_replace_end 482 | 483 | -- 'winblend' (window transparency) generator for floating window 484 | --minidoc_replace_start winblend = --, 485 | winblend = function(s, n) return 80 + 20 * (s / n) end, 486 | --minidoc_replace_end 487 | }, 488 | 489 | -- Window close 490 | close = { 491 | -- Whether to enable this animation 492 | enable = true, 493 | 494 | -- Timing of animation (how steps will progress in time) 495 | --minidoc_replace_start timing = --, 496 | timing = function(_, n) return 250 / n end, 497 | --minidoc_replace_end 498 | 499 | -- Floating window config generator visualizing specific window 500 | --minidoc_replace_start winconfig = --, 501 | winconfig = function(win_id) 502 | return H.winconfig_static(win_id, { predicate = H.default_winconfig_predicate, n_steps = 25 }) 503 | end, 504 | --minidoc_replace_end 505 | 506 | -- 'winblend' (window transparency) generator for floating window 507 | --minidoc_replace_start winblend = --, 508 | winblend = function(s, n) return 80 + 20 * (s / n) end, 509 | --minidoc_replace_end 510 | }, 511 | } 512 | --minidoc_afterlines_end 513 | 514 | -- Module functionality ======================================================= 515 | --- Check animation activity 516 | --- 517 | ---@param animation_type string One of supported animation types 518 | --- (entries of |MiniAnimate.config|, like `'cursor'`, etc.). 519 | --- 520 | ---@return boolean Whether the animation is currently active. 521 | MiniAnimate.is_active = function(animation_type) 522 | local res = H.cache[animation_type .. '_is_active'] 523 | if res == nil then H.error('Wrong `animation_type` for `is_active()`.') end 524 | return res 525 | end 526 | 527 | --- Execute action after some animation is done 528 | --- 529 | --- Execute action immediately if animation is not active (checked with 530 | --- |MiniAnimate.is_active()|). Else, schedule its execution until after 531 | --- animation is done (on corresponding "done event", see 532 | --- |MiniAnimate-done-event|). 533 | --- 534 | --- Mostly meant to be used inside mappings. 535 | --- 536 | --- Example ~ 537 | --- 538 | --- A useful `nnoremap n nzvzz` mapping (consecutive application of |n|, |zv|, and |zz|) 539 | --- should be expressed in the following way: >lua 540 | --- 541 | --- 'lua vim.cmd("normal! n"); ' .. 542 | --- 'MiniAnimate.execute_after("scroll", "normal! zvzz")' 543 | --- < 544 | ---@param animation_type string One of supported animation types 545 | --- (as in |MiniAnimate.is_active()|). 546 | ---@param action string|function Action to be executed. If string, executed as 547 | --- command (via |vim.cmd()|). 548 | MiniAnimate.execute_after = function(animation_type, action) 549 | local event_name = H.animation_done_events[animation_type] 550 | if event_name == nil then H.error('Wrong `animation_type` for `execute_after`.') end 551 | 552 | local callable = action 553 | if type(callable) == 'string' then callable = function() vim.cmd(action) end end 554 | if not vim.is_callable(callable) then 555 | H.error('Argument `action` of `execute_after()` should be string or callable.') 556 | end 557 | 558 | -- Schedule conditional action execution to allow animation to actually take 559 | -- effect. This helps creating more universal mappings, because some commands 560 | -- (like `n`) not always result into scrolling. 561 | vim.schedule(function() 562 | if MiniAnimate.is_active(animation_type) then 563 | vim.api.nvim_create_autocmd('User', { pattern = event_name, once = true, callback = callable }) 564 | else 565 | callable() 566 | end 567 | end) 568 | end 569 | 570 | -- Action (step 0) - wait (step 1) - action (step 1) - ... 571 | -- `step_action` should return `false` or `nil` (equivalent to not returning anything explicitly) in order to stop animation. 572 | --- Animate action 573 | --- 574 | --- This is equivalent to asynchronous execution of the following algorithm: 575 | --- - Call `step_action(0)` immediately after calling this function. Stop if 576 | --- action returned `false` or `nil`. 577 | --- - Wait `step_timing(1)` milliseconds. 578 | --- - Call `step_action(1)`. Stop if it returned `false` or `nil`. 579 | --- - Wait `step_timing(2)` milliseconds. 580 | --- - Call `step_action(2)`. Stop if it returned `false` or `nil`. 581 | --- - ... 582 | --- 583 | --- Notes: 584 | --- - Animation is also stopped on action error or if maximum number of steps 585 | --- is reached. 586 | --- - Asynchronous execution is done with |uv.new_timer()|. It only allows 587 | --- integer parts as repeat value. This has several implications: 588 | --- - Outputs of `step_timing()` are accumulated in order to preserve total 589 | --- execution time. 590 | --- - Any wait time less than 1 ms means that action will be executed 591 | --- immediately. 592 | --- 593 | ---@param step_action function|table Callable which takes `step` (integer 0, 1, 2, 594 | --- etc. indicating current step) and executes some action. Its return value 595 | --- defines when animation should stop: values `false` and `nil` (equivalent 596 | --- to no explicit return) stop animation timer; any other continues it. 597 | ---@param step_timing function|table Callable which takes `step` (integer 1, 2, etc. 598 | --- indicating next step) and returns how many milliseconds to wait before 599 | --- executing this step action. 600 | ---@param opts table|nil Options. Possible fields: 601 | --- - - Maximum value of allowed step to execute. Default: 10000000. 602 | MiniAnimate.animate = function(step_action, step_timing, opts) 603 | opts = vim.tbl_deep_extend('force', { max_steps = 10000000 }, opts or {}) 604 | 605 | local step, max_steps = 0, opts.max_steps 606 | local timer, wait_time = vim.loop.new_timer(), 0 607 | 608 | local draw_step 609 | draw_step = vim.schedule_wrap(function() 610 | local ok, should_continue = pcall(step_action, step) 611 | if not (ok and should_continue and step < max_steps) then 612 | timer:stop() 613 | return 614 | end 615 | 616 | step = step + 1 617 | wait_time = wait_time + step_timing(step) 618 | 619 | -- Repeat value of `timer` seems to be rounded down to milliseconds. This 620 | -- means that values less than 1 will lead to timer stop repeating. Instead 621 | -- call next step function directly. 622 | if wait_time < 1 then 623 | timer:set_repeat(0) 624 | -- Use `return` to make this proper "tail call" 625 | return draw_step() 626 | else 627 | timer:set_repeat(wait_time) 628 | wait_time = wait_time - timer:get_repeat() 629 | timer:again() 630 | end 631 | end) 632 | 633 | -- Start non-repeating timer without callback execution 634 | timer:start(10000000, 0, draw_step) 635 | 636 | -- Draw step zero (at origin) immediately 637 | draw_step() 638 | end 639 | 640 | --- Generate animation timing 641 | --- 642 | --- Each field corresponds to one family of progression which can be customized 643 | --- further by supplying appropriate arguments. 644 | --- 645 | --- This is a table with function elements. Call to actually get timing function. 646 | --- 647 | --- Example: >lua 648 | --- 649 | --- local animate = require('mini.animate') 650 | --- animate.setup({ 651 | --- cursor = { 652 | --- timing = animate.gen_timing.linear({ duration = 100, unit = 'total' }) 653 | --- }, 654 | --- }) 655 | --- < 656 | ---@seealso |MiniIndentscope.gen_animation| for similar concept in 'mini.indentscope'. 657 | MiniAnimate.gen_timing = {} 658 | 659 | ---@alias __animate_timing_opts table|nil Options that control progression. Possible keys: 660 | --- - `(string)` - a subtype of progression. One of "in" 661 | --- (accelerating from zero speed), "out" (decelerating to zero speed), 662 | --- "in-out" (default; accelerating halfway, decelerating after). 663 | --- - `(number)` - duration (in ms) of a unit. Default: 20. 664 | --- - `(string)` - which unit's duration `opts.duration` controls. One 665 | --- of "step" (default; ensures average duration of step to be `opts.duration`) 666 | --- or "total" (ensures fixed total duration regardless of scope's range). 667 | ---@alias __animate_timing_return function Timing function (see |MiniAnimate-timing|). 668 | 669 | --- Generate timing with no animation 670 | --- 671 | --- Show final result immediately. Usually better to use `enable` field in `config` 672 | --- if you want to disable animation. 673 | MiniAnimate.gen_timing.none = function() 674 | return function() return 0 end 675 | end 676 | 677 | --- Generate timing with linear progression 678 | --- 679 | ---@param opts __animate_timing_opts 680 | --- 681 | ---@return __animate_timing_return 682 | MiniAnimate.gen_timing.linear = function(opts) return H.timing_arithmetic(0, H.normalize_timing_opts(opts)) end 683 | 684 | --- Generate timing with quadratic progression 685 | --- 686 | ---@param opts __animate_timing_opts 687 | --- 688 | ---@return __animate_timing_return 689 | MiniAnimate.gen_timing.quadratic = function(opts) return H.timing_arithmetic(1, H.normalize_timing_opts(opts)) end 690 | 691 | --- Generate timing with cubic progression 692 | --- 693 | ---@param opts __animate_timing_opts 694 | --- 695 | ---@return __animate_timing_return 696 | MiniAnimate.gen_timing.cubic = function(opts) return H.timing_arithmetic(2, H.normalize_timing_opts(opts)) end 697 | 698 | --- Generate timing with quartic progression 699 | --- 700 | ---@param opts __animate_timing_opts 701 | --- 702 | ---@return __animate_timing_return 703 | MiniAnimate.gen_timing.quartic = function(opts) return H.timing_arithmetic(3, H.normalize_timing_opts(opts)) end 704 | 705 | --- Generate timing with exponential progression 706 | --- 707 | ---@param opts __animate_timing_opts 708 | --- 709 | ---@return __animate_timing_return 710 | MiniAnimate.gen_timing.exponential = function(opts) return H.timing_geometrical(H.normalize_timing_opts(opts)) end 711 | 712 | --- Generate cursor animation path 713 | --- 714 | --- For more information see |MiniAnimate.config.cursor|. 715 | --- 716 | --- This is a table with function elements. Call to actually get generator. 717 | --- 718 | --- Example: >lua 719 | --- 720 | --- local animate = require('mini.animate') 721 | --- animate.setup({ 722 | --- cursor = { 723 | --- -- Animate with line-column angle instead of shortest line 724 | --- path = animate.gen_path.angle(), 725 | --- } 726 | --- }) 727 | --- < 728 | MiniAnimate.gen_path = {} 729 | 730 | ---@alias __animate_path_opts_common table|nil Options that control generator. Possible keys: 731 | --- - `(function)` - a callable which takes `destination` as input and 732 | --- returns boolean value indicating whether animation should be done. 733 | --- Default: `false` if `destination` is within one line of origin (reduces 734 | --- flickering), `true` otherwise. 735 | ---@alias __animate_path_return function Path function (see |MiniAnimate.config.cursor|). 736 | 737 | --- Generate path as shortest line 738 | --- 739 | ---@param opts __animate_path_opts_common 740 | --- - `(number)` - maximum number of steps in output. 741 | --- Default: 1000. 742 | --- 743 | ---@return __animate_path_return 744 | MiniAnimate.gen_path.line = function(opts) 745 | opts = vim.tbl_deep_extend('force', { predicate = H.default_path_predicate, max_output_steps = 1000 }, opts or {}) 746 | 747 | return function(destination) return H.path_line(destination, opts) end 748 | end 749 | 750 | --- Generate path as line/column angle 751 | --- 752 | ---@param opts __animate_path_opts_common 753 | --- - `(number)` - maximum number of steps per side in output. 754 | --- Default: 1000. 755 | --- - `(string)` - one of `"horizontal"` (default; animates 756 | --- across initial line first) or `"vertical"` (animates across initial 757 | --- column first). 758 | --- 759 | ---@return __animate_path_return 760 | MiniAnimate.gen_path.angle = function(opts) 761 | local default_opts = { predicate = H.default_path_predicate, max_output_steps = 1000, first_direction = 'horizontal' } 762 | opts = vim.tbl_deep_extend('force', default_opts, opts or {}) 763 | 764 | local append_horizontal = function(res, dest_col, const_line) 765 | if dest_col == 0 then return end 766 | local n_steps = math.min(math.abs(dest_col), opts.max_output_steps) 767 | local coef = dest_col / n_steps 768 | for i = 0, n_steps - 1 do 769 | table.insert(res, { const_line, H.round(coef * i) }) 770 | end 771 | end 772 | 773 | local append_vertical = function(res, dest_line, const_col) 774 | if dest_line == 0 then return end 775 | local n_steps = math.min(math.abs(dest_line), opts.max_output_steps) 776 | local coef = dest_line / n_steps 777 | for i = 0, n_steps - 1 do 778 | table.insert(res, { H.round(coef * i), const_col }) 779 | end 780 | end 781 | 782 | return function(destination) 783 | -- Don't animate in case of false predicate 784 | if not opts.predicate(destination) then return {} end 785 | 786 | -- Travel along horizontal/vertical lines 787 | local res = {} 788 | if opts.first_direction == 'horizontal' then 789 | append_horizontal(res, destination[2], 0) 790 | append_vertical(res, destination[1], destination[2]) 791 | else 792 | append_vertical(res, destination[1], 0) 793 | append_horizontal(res, destination[2], destination[1]) 794 | end 795 | 796 | return res 797 | end 798 | end 799 | 800 | --- Generate path as closing walls at final position 801 | --- 802 | ---@param opts __animate_path_opts_common 803 | --- - `(number)` - initial width of left and right walls. Default: 10. 804 | --- 805 | ---@return __animate_path_return 806 | MiniAnimate.gen_path.walls = function(opts) 807 | opts = opts or {} 808 | local predicate = opts.predicate or H.default_path_predicate 809 | local width = opts.width or 10 810 | 811 | return function(destination) 812 | -- Don't animate in case of false predicate 813 | if not predicate(destination) then return {} end 814 | 815 | -- Don't animate in case of no movement 816 | if destination[1] == 0 and destination[2] == 0 then return {} end 817 | 818 | local dest_line, dest_col = destination[1], destination[2] 819 | local res = {} 820 | for i = width, 1, -1 do 821 | table.insert(res, { dest_line, dest_col + i }) 822 | table.insert(res, { dest_line, dest_col - i }) 823 | end 824 | return res 825 | end 826 | end 827 | 828 | --- Generate path as diminishing spiral at final position 829 | --- 830 | ---@param opts __animate_path_opts_common 831 | --- - `(number)` - initial width of spiral. Default: 2. 832 | --- 833 | ---@return __animate_path_return 834 | MiniAnimate.gen_path.spiral = function(opts) 835 | opts = opts or {} 836 | local predicate = opts.predicate or H.default_path_predicate 837 | local width = opts.width or 2 838 | 839 | local add_layer = function(res, w, destination) 840 | local dest_line, dest_col = destination[1], destination[2] 841 | --stylua: ignore start 842 | for j = -w, w-1 do table.insert(res, { dest_line - w, dest_col + j }) end 843 | for i = -w, w-1 do table.insert(res, { dest_line + i, dest_col + w }) end 844 | for j = -w, w-1 do table.insert(res, { dest_line + w, dest_col - j }) end 845 | for i = -w, w-1 do table.insert(res, { dest_line - i, dest_col - w }) end 846 | --stylua: ignore end 847 | end 848 | 849 | return function(destination) 850 | -- Don't animate in case of false predicate 851 | if not predicate(destination) then return {} end 852 | 853 | -- Don't animate in case of no movement 854 | if destination[1] == 0 and destination[2] == 0 then return {} end 855 | 856 | local res = {} 857 | for w = width, 1, -1 do 858 | add_layer(res, w, destination) 859 | end 860 | return res 861 | end 862 | end 863 | 864 | --- Generate scroll animation subscroll 865 | --- 866 | --- For more information see |MiniAnimate.config.scroll|. 867 | --- 868 | --- This is a table with function elements. Call to actually get generator. 869 | --- 870 | --- Example: >lua 871 | --- 872 | --- local animate = require('mini.animate') 873 | --- animate.setup({ 874 | --- scroll = { 875 | --- -- Animate equally but with 120 maximum steps instead of default 60 876 | --- subscroll = animate.gen_subscroll.equal({ max_output_steps = 120 }), 877 | --- } 878 | --- }) 879 | --- < 880 | MiniAnimate.gen_subscroll = {} 881 | 882 | --- Generate subscroll with equal steps 883 | --- 884 | ---@param opts table|nil Options that control generator. Possible keys: 885 | --- - `(function)` - a callable which takes `total_scroll` as 886 | --- input and returns boolean value indicating whether animation should be 887 | --- done. Default: `false` if `total_scroll` is 1 or less (reduces 888 | --- unnecessary waiting), `true` otherwise. 889 | --- - `(number)` - maximum number of subscroll steps in output. 890 | --- Adjust this to reduce computations in expense of reduced smoothness. 891 | --- Default: 60. 892 | --- 893 | ---@return function Subscroll function (see |MiniAnimate.config.scroll|). 894 | MiniAnimate.gen_subscroll.equal = function(opts) 895 | opts = vim.tbl_deep_extend('force', { predicate = H.default_subscroll_predicate, max_output_steps = 60 }, opts or {}) 896 | 897 | return function(total_scroll) return H.subscroll_equal(total_scroll, opts) end 898 | end 899 | 900 | --- Generate resize animation subresize 901 | --- 902 | --- For more information see |MiniAnimate.config.resize|. 903 | --- 904 | --- This is a table with function elements. Call to actually get generator. 905 | --- 906 | --- Example: >lua 907 | --- 908 | --- local is_many_wins = function(sizes_from, sizes_to) 909 | --- return vim.tbl_count(sizes_from) >= 3 910 | --- end 911 | --- local animate = require('mini.animate') 912 | --- animate.setup({ 913 | --- resize = { 914 | --- -- Animate only if there are at least 3 windows 915 | --- subresize = animate.gen_subresize.equal({ predicate = is_many_wins }), 916 | --- } 917 | --- }) 918 | --- < 919 | MiniAnimate.gen_subresize = {} 920 | 921 | --- Generate subresize with equal steps 922 | --- 923 | ---@param opts table|nil Options that control generator. Possible keys: 924 | --- - `(function)` - a callable which takes `sizes_from` and 925 | --- `sizes_to` as input and returns boolean value indicating whether 926 | --- animation should be done. Default: always `true`. 927 | --- 928 | ---@return function Subresize function (see |MiniAnimate.config.resize|). 929 | MiniAnimate.gen_subresize.equal = function(opts) 930 | opts = vim.tbl_deep_extend('force', { predicate = H.default_subresize_predicate }, opts or {}) 931 | 932 | return function(sizes_from, sizes_to) return H.subresize_equal(sizes_from, sizes_to, opts) end 933 | end 934 | 935 | --- Generate open/close animation winconfig 936 | --- 937 | --- For more information see |MiniAnimate.config.open| or |MiniAnimate.config.close|. 938 | --- 939 | --- This is a table with function elements. Call to actually get generator. 940 | --- 941 | --- Example: >lua 942 | --- 943 | --- local is_not_single_window = function(win_id) 944 | --- local tabpage_id = vim.api.nvim_win_get_tabpage(win_id) 945 | --- return #vim.api.nvim_tabpage_list_wins(tabpage_id) > 1 946 | --- end 947 | --- local animate = require('mini.animate') 948 | --- animate.setup({ 949 | --- open = { 950 | --- -- Animate with wiping from nearest edge instead of default static one 951 | --- -- and only if it is not a single window in tabpage 952 | --- winconfig = animate.gen_winconfig.wipe({ 953 | --- predicate = is_not_single_window, 954 | --- direction = 'from_edge', 955 | --- }), 956 | --- }, 957 | --- close = { 958 | --- -- Animate with wiping to nearest edge instead of default static one 959 | --- -- and only if it is not a single window in tabpage 960 | --- winconfig = animate.gen_winconfig.wipe({ 961 | --- predicate = is_not_single_window, 962 | --- direction = 'to_edge', 963 | --- }), 964 | --- }, 965 | --- }) 966 | --- < 967 | MiniAnimate.gen_winconfig = {} 968 | 969 | ---@alias __animate_winconfig_opts_common table|nil Options that control generator. Possible keys: 970 | --- - `(function)` - a callable which takes `win_id` as input and 971 | --- returns boolean value indicating whether animation should be done. 972 | --- Default: always `true`. 973 | ---@alias __animate_winconfig_return function Winconfig function (see |MiniAnimate.config.open| 974 | --- or |MiniAnimate.config.close|). 975 | 976 | --- Generate winconfig for static floating window 977 | --- 978 | --- This will result into floating window statically covering whole target 979 | --- window. 980 | --- 981 | ---@param opts __animate_winconfig_opts_common 982 | --- - `(number)` - number of output steps, all with same config. 983 | --- Useful to tweak smoothness of transparency animation (done inside 984 | --- `winblend` config option). Default: 25. 985 | --- 986 | ---@return __animate_winconfig_return 987 | MiniAnimate.gen_winconfig.static = function(opts) 988 | opts = vim.tbl_deep_extend('force', { predicate = H.default_winconfig_predicate, n_steps = 25 }, opts or {}) 989 | 990 | return function(win_id) return H.winconfig_static(win_id, opts) end 991 | end 992 | 993 | --- Generate winconfig for center-focused animated floating window 994 | --- 995 | --- This will result into floating window growing from or shrinking to the 996 | --- target window center. 997 | --- 998 | ---@param opts __animate_winconfig_opts_common 999 | --- - `(string)` - one of `"to_center"` (default; window will 1000 | --- shrink from full coverage to center) or `"from_center"` (window will 1001 | --- grow from center to full coverage). 1002 | --- 1003 | ---@return __animate_winconfig_return 1004 | MiniAnimate.gen_winconfig.center = function(opts) 1005 | opts = opts or {} 1006 | local predicate = opts.predicate or H.default_winconfig_predicate 1007 | local direction = opts.direction or 'to_center' 1008 | 1009 | return function(win_id) 1010 | -- Don't animate in case of false predicate 1011 | if not predicate(win_id) then return {} end 1012 | 1013 | local pos = vim.fn.win_screenpos(win_id) 1014 | local row, col = pos[1] - 1, pos[2] - 1 1015 | local height, width = vim.api.nvim_win_get_height(win_id), vim.api.nvim_win_get_width(win_id) 1016 | 1017 | local n_steps = math.max(height, width) 1018 | local res = {} 1019 | -- Progression should be between fully covering target window and minimal 1020 | -- dimensions in target window center. 1021 | for i = 1, n_steps do 1022 | local coef = (i - 1) / n_steps 1023 | 1024 | -- Reverse output if progression is from center 1025 | local res_ind = direction == 'to_center' and i or (n_steps - i + 1) 1026 | 1027 | --stylua: ignore 1028 | res[res_ind] = { 1029 | relative = 'editor', 1030 | anchor = 'NW', 1031 | row = H.round(row + 0.5 * coef * height), 1032 | col = H.round(col + 0.5 * coef * width), 1033 | width = math.ceil((1 - coef) * width), 1034 | height = math.ceil((1 - coef) * height), 1035 | focusable = false, 1036 | zindex = 1, 1037 | border = 'none', 1038 | style = 'minimal', 1039 | } 1040 | end 1041 | 1042 | return res 1043 | end 1044 | end 1045 | 1046 | --- Generate winconfig for wiping animated floating window 1047 | --- 1048 | --- This will result into floating window growing from or shrinking to the 1049 | --- nearest edge. This also takes into account the split type of target window: 1050 | --- vertically split window will progress towards vertical edge; horizontally - 1051 | --- towards horizontal. 1052 | --- 1053 | ---@param opts __animate_winconfig_opts_common 1054 | --- - `(string)` - one of `"to_edge"` (default; window will 1055 | --- shrink from full coverage to nearest edge) or `"from_edge"` (window 1056 | --- will grow from edge to full coverage). 1057 | --- 1058 | ---@return __animate_winconfig_return 1059 | MiniAnimate.gen_winconfig.wipe = function(opts) 1060 | opts = opts or {} 1061 | local predicate = opts.predicate or H.default_winconfig_predicate 1062 | local direction = opts.direction or 'to_edge' 1063 | 1064 | return function(win_id) 1065 | -- Don't animate in case of false predicate 1066 | if not predicate(win_id) then return {} end 1067 | 1068 | -- Get window data 1069 | local win_pos = vim.fn.win_screenpos(win_id) 1070 | local top_row, left_col = win_pos[1], win_pos[2] 1071 | local win_height, win_width = vim.api.nvim_win_get_height(win_id), vim.api.nvim_win_get_width(win_id) 1072 | 1073 | -- Compute progression data 1074 | local cur_row, cur_col = top_row, left_col 1075 | local cur_width, cur_height = win_width, win_height 1076 | 1077 | local increment_row, increment_col, increment_height, increment_width 1078 | local n_steps 1079 | 1080 | local win_container = H.get_window_parent_container(win_id) 1081 | --stylua: ignore 1082 | if win_container == 'col' then 1083 | -- Determine closest top/bottom screen edge and progress to it 1084 | local bottom_row = top_row + win_height - 1 1085 | local is_top_edge_closer = top_row < (vim.o.lines - bottom_row + 1) 1086 | 1087 | increment_row, increment_col = (is_top_edge_closer and 0 or 1), 0 1088 | increment_width, increment_height = 0, -1 1089 | n_steps = win_height 1090 | else 1091 | -- Determine closest left/right screen edge and progress to it 1092 | local right_col = left_col + win_width - 1 1093 | local is_left_edge_closer = left_col < (vim.o.columns - right_col + 1) 1094 | 1095 | increment_row, increment_col = 0, (is_left_edge_closer and 0 or 1) 1096 | increment_width, increment_height = -1, 0 1097 | n_steps = win_width 1098 | end 1099 | 1100 | -- Make step configs 1101 | local res = {} 1102 | for i = 1, n_steps do 1103 | -- Reverse output if progression is from edge 1104 | local res_ind = direction == 'to_edge' and i or (n_steps - i + 1) 1105 | res[res_ind] = { 1106 | relative = 'editor', 1107 | anchor = 'NW', 1108 | row = cur_row - 1, 1109 | col = cur_col - 1, 1110 | width = cur_width, 1111 | height = cur_height, 1112 | focusable = false, 1113 | zindex = 1, 1114 | border = 'none', 1115 | style = 'minimal', 1116 | } 1117 | cur_row = cur_row + increment_row 1118 | cur_col = cur_col + increment_col 1119 | cur_height = cur_height + increment_height 1120 | cur_width = cur_width + increment_width 1121 | end 1122 | return res 1123 | end 1124 | end 1125 | 1126 | --- Generate open/close animation `winblend` progression 1127 | --- 1128 | --- For more information see |MiniAnimate.config.open| or |MiniAnimate.config.close|. 1129 | --- 1130 | --- This is a table with function elements. Call to actually get transparency 1131 | --- function. 1132 | --- 1133 | --- Example: >lua 1134 | --- 1135 | --- local animate = require('mini.animate') 1136 | --- animate.setup({ 1137 | --- open = { 1138 | --- -- Change transparency from 60 to 80 instead of default 80 to 100 1139 | --- winblend = animate.gen_winblend.linear({ from = 60, to = 80 }), 1140 | --- }, 1141 | --- close = { 1142 | --- -- Change transparency from 60 to 80 instead of default 80 to 100 1143 | --- winblend = animate.gen_winblend.linear({ from = 60, to = 80 }), 1144 | --- }, 1145 | --- }) 1146 | --- < 1147 | MiniAnimate.gen_winblend = {} 1148 | 1149 | --- Generate linear `winblend` progression 1150 | --- 1151 | ---@param opts table|nil Options that control generator. Possible keys: 1152 | --- - `(number)` - initial value of 'winblend'. 1153 | --- - `(number)` - final value of 'winblend'. 1154 | --- 1155 | ---@return function Winblend function (see |MiniAnimate.config.open| 1156 | --- or |MiniAnimate.config.close|). 1157 | MiniAnimate.gen_winblend.linear = function(opts) 1158 | opts = opts or {} 1159 | local from = opts.from or 80 1160 | local to = opts.to or 100 1161 | local diff = to - from 1162 | 1163 | return function(s, n) return from + (s / n) * diff end 1164 | end 1165 | 1166 | -- Helper data ================================================================ 1167 | -- Module default config 1168 | H.default_config = vim.deepcopy(MiniAnimate.config) 1169 | 1170 | -- Cache for various operations 1171 | H.cache = { 1172 | -- Cursor move animation data 1173 | cursor_event_id = 0, 1174 | cursor_is_active = false, 1175 | cursor_state = { buf_id = nil, pos = {} }, 1176 | 1177 | -- Scroll animation data 1178 | scroll_event_id = 0, 1179 | scroll_is_active = false, 1180 | scroll_state = { buf_id = nil, win_id = nil, view = {}, cursor = {} }, 1181 | 1182 | -- Resize animation data 1183 | resize_event_id = 0, 1184 | resize_is_active = false, 1185 | resize_state = { layout = {}, sizes = {}, views = {} }, 1186 | 1187 | -- Window open animation data 1188 | open_event_id = 0, 1189 | open_is_active = false, 1190 | open_active_windows = {}, 1191 | 1192 | -- Window close animation data 1193 | close_event_id = 0, 1194 | close_is_active = false, 1195 | close_active_windows = {}, 1196 | } 1197 | 1198 | -- Namespaces for module operations 1199 | H.ns_id = { 1200 | -- Extmarks used to show cursor path 1201 | cursor = vim.api.nvim_create_namespace('MiniAnimateCursor'), 1202 | } 1203 | 1204 | -- Identifier of empty buffer used inside open/close animations 1205 | H.empty_buf_id = nil 1206 | 1207 | -- Names of `User` events triggered after certain type of animation is done 1208 | H.animation_done_events = { 1209 | cursor = 'MiniAnimateDoneCursor', 1210 | scroll = 'MiniAnimateDoneScroll', 1211 | resize = 'MiniAnimateDoneResize', 1212 | open = 'MiniAnimateDoneOpen', 1213 | close = 'MiniAnimateDoneClose', 1214 | } 1215 | 1216 | -- Helper functionality ======================================================= 1217 | -- Settings ------------------------------------------------------------------- 1218 | H.setup_config = function(config) 1219 | H.check_type('config', config, 'table', true) 1220 | config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {}) 1221 | 1222 | H.check_type('cursor', config.cursor, 'table') 1223 | H.check_type('cursor.enable', config.cursor.enable, 'boolean') 1224 | H.check_type('cursor.timing', config.cursor.timing, 'callable') 1225 | H.check_type('cursor.path', config.cursor.path, 'callable') 1226 | 1227 | H.check_type('scroll', config.scroll, 'table') 1228 | H.check_type('scroll.enable', config.scroll.enable, 'boolean') 1229 | H.check_type('scroll.timing', config.scroll.timing, 'callable') 1230 | H.check_type('scroll.subscroll', config.scroll.subscroll, 'callable') 1231 | 1232 | H.check_type('resize', config.resize, 'table') 1233 | H.check_type('resize.enable', config.resize.enable, 'boolean') 1234 | H.check_type('resize.timing', config.resize.timing, 'callable') 1235 | H.check_type('resize.subresize', config.resize.subresize, 'callable') 1236 | 1237 | H.check_type('open', config.open, 'table') 1238 | H.check_type('open.enable', config.open.enable, 'boolean') 1239 | H.check_type('open.timing', config.open.timing, 'callable') 1240 | H.check_type('open.winconfig', config.open.winconfig, 'callable') 1241 | H.check_type('open.winblend', config.open.winblend, 'callable') 1242 | 1243 | H.check_type('close', config.close, 'table') 1244 | H.check_type('close.enable', config.close.enable, 'boolean') 1245 | H.check_type('close.timing', config.close.timing, 'callable') 1246 | H.check_type('close.winconfig', config.close.winconfig, 'callable') 1247 | H.check_type('close.winblend', config.close.winblend, 'callable') 1248 | 1249 | return config 1250 | end 1251 | 1252 | H.apply_config = function(config) MiniAnimate.config = config end 1253 | 1254 | H.create_autocommands = function() 1255 | local gr = vim.api.nvim_create_augroup('MiniAnimate', {}) 1256 | 1257 | local au = function(event, pattern, callback, desc) 1258 | vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc }) 1259 | end 1260 | 1261 | au('CursorMoved', '*', H.auto_cursor, 'Animate cursor') 1262 | 1263 | au('WinScrolled', '*', function() 1264 | -- Inside `WinScrolled` first animate resize before scroll to avoid flicker 1265 | H.auto_resize() 1266 | H.auto_scroll() 1267 | end, 'Animate resize and animate scroll') 1268 | -- Track scroll state on buffer and window enter to animate its first scroll. 1269 | -- Use `vim.schedule_wrap()` to allow other immediate commands to change view 1270 | -- (like builtin cursor center on buffer change) to avoid unnecessary 1271 | -- animated scroll. 1272 | au({ 'BufEnter', 'WinEnter' }, '*', vim.schedule_wrap(H.track_scroll_state), 'Track scroll state') 1273 | -- Track immediately scroll state after leaving terminal mode. Otherwise it 1274 | -- will lead to scroll animation starting at latest non-Terminal mode view. 1275 | au('TermLeave', '*', H.track_scroll_state, 'Track scroll state') 1276 | -- Track scroll state (partially) on every cursor move to keep cursor 1277 | -- position up to date. This enables visually better cursor positioning 1278 | -- during scroll animation (convex progression from start cursor position to 1279 | -- end). Use `vim.schedule()` to make it affect state only after scroll is 1280 | -- done and cursor is already in correct final position. 1281 | au('CursorMoved', '*', vim.schedule_wrap(H.track_scroll_state_partial), 'Track partial scroll state') 1282 | au('CmdlineLeave', '*', H.on_cmdline_leave, 'On CmdlineLeave') 1283 | 1284 | -- Use `vim.schedule_wrap()` animation to get a window data used for 1285 | -- displaying (and not one after just opening). Useful for 'nvim-tree'. 1286 | au('WinNew', '*', vim.schedule_wrap(function() H.auto_openclose('open') end), 'Animate window open') 1287 | 1288 | au('WinClosed', '*', function() H.auto_openclose('close') end, 'Animate window close') 1289 | 1290 | au('ColorScheme', '*', H.create_default_hl, 'Ensure colors') 1291 | end 1292 | 1293 | H.create_default_hl = function() 1294 | vim.api.nvim_set_hl(0, 'MiniAnimateCursor', { default = true, reverse = true, nocombine = true }) 1295 | vim.api.nvim_set_hl(0, 'MiniAnimateNormalFloat', { default = true, link = 'NormalFloat' }) 1296 | end 1297 | 1298 | H.is_disabled = function() return vim.g.minianimate_disable == true or vim.b.minianimate_disable == true end 1299 | 1300 | H.get_config = function(config) 1301 | return vim.tbl_deep_extend('force', MiniAnimate.config, vim.b.minianimate_config or {}, config or {}) 1302 | end 1303 | 1304 | -- Autocommands --------------------------------------------------------------- 1305 | H.auto_cursor = function() 1306 | -- Don't animate if disabled 1307 | local cursor_config = H.get_config().cursor 1308 | if not cursor_config.enable or H.is_disabled() then 1309 | -- Reset state to not use an outdated one if enabled again 1310 | H.cache.cursor_state = { buf_id = nil, pos = {} } 1311 | return 1312 | end 1313 | 1314 | -- Don't animate if inside scroll animation 1315 | if H.cache.scroll_is_active then return end 1316 | 1317 | -- Update necessary information. NOTE: update state only on `CursorMoved` and 1318 | -- not inside every animation step (like in scroll animation) for performance 1319 | -- reasons: cursor movement is much more common action than scrolling. 1320 | local prev_state, new_state = H.cache.cursor_state, H.get_cursor_state() 1321 | H.cache.cursor_state = new_state 1322 | H.cache.cursor_event_id = H.cache.cursor_event_id + 1 1323 | 1324 | -- Don't animate if changed buffer 1325 | if new_state.buf_id ~= prev_state.buf_id then return end 1326 | 1327 | -- Make animation step data and possibly animate 1328 | local animate_step = H.make_cursor_step(prev_state, new_state, cursor_config) 1329 | if not animate_step then return end 1330 | 1331 | H.start_cursor() 1332 | MiniAnimate.animate(animate_step.step_action, animate_step.step_timing) 1333 | end 1334 | 1335 | H.auto_resize = function() 1336 | -- Don't animate if disabled 1337 | local resize_config = H.get_config().resize 1338 | if not resize_config.enable or H.is_disabled() then 1339 | -- Reset state to not use an outdated one if enabled again 1340 | H.cache.resize_state = { layout = {}, sizes = {}, views = {} } 1341 | return 1342 | end 1343 | 1344 | -- Don't animate if inside scroll animation. This reduces computations and 1345 | -- occasional flickering. 1346 | if H.cache.scroll_is_active then return end 1347 | 1348 | -- Update state. This also ensures that window views are up to date. 1349 | local prev_state, new_state = H.cache.resize_state, H.get_resize_state() 1350 | H.cache.resize_state = new_state 1351 | 1352 | -- Don't animate if there is nothing to animate (should be same layout but 1353 | -- different sizes). This also stops triggering animation on window scrolls. 1354 | local same_state = H.is_equal_resize_state(prev_state, new_state) 1355 | if not (same_state.layout and not same_state.sizes) then return end 1356 | 1357 | -- Register new event only in case there is something to animate 1358 | H.cache.resize_event_id = H.cache.resize_event_id + 1 1359 | 1360 | -- Make animation step data and possibly animate 1361 | local animate_step = H.make_resize_step(prev_state, new_state, resize_config) 1362 | if not animate_step then return end 1363 | 1364 | H.start_resize(prev_state) 1365 | MiniAnimate.animate(animate_step.step_action, animate_step.step_timing) 1366 | end 1367 | 1368 | H.auto_scroll = function() 1369 | -- Don't animate if disabled 1370 | local scroll_config = H.get_config().scroll 1371 | if not scroll_config.enable or H.is_disabled() then 1372 | -- Reset state to not use an outdated one if enabled again 1373 | H.cache.scroll_state = { buf_id = nil, win_id = nil, view = {}, cursor = {} } 1374 | return 1375 | end 1376 | 1377 | -- Get states 1378 | local prev_state, new_state = H.cache.scroll_state, H.get_scroll_state() 1379 | 1380 | -- Don't animate if nothing to animate. Mostly used to distinguish 1381 | -- `WinScrolled` resulting from module animation from the other ones. 1382 | local is_same_bufwin = new_state.buf_id == prev_state.buf_id and new_state.win_id == prev_state.win_id 1383 | local is_same_topline = new_state.view.topline == prev_state.view.topline 1384 | if is_same_topline and is_same_bufwin then return end 1385 | 1386 | -- Update necessary information 1387 | H.cache.scroll_state = new_state 1388 | H.cache.scroll_event_id = H.cache.scroll_event_id + 1 1389 | 1390 | -- Don't animate if changed buffer or window 1391 | if not is_same_bufwin then return end 1392 | 1393 | -- Don't animate if inside resize animation. This reduces computations and 1394 | -- occasional flickering. 1395 | if H.cache.resize_is_active then return end 1396 | 1397 | -- Make animation step data and possibly animate 1398 | local animate_step = H.make_scroll_step(prev_state, new_state, scroll_config) 1399 | if not animate_step then return end 1400 | 1401 | H.start_scroll(prev_state) 1402 | MiniAnimate.animate(animate_step.step_action, animate_step.step_timing) 1403 | end 1404 | 1405 | H.track_scroll_state = function() H.cache.scroll_state = H.get_scroll_state() end 1406 | 1407 | H.track_scroll_state_partial = function() 1408 | -- This not only improves computation load, but seems to be crucial for 1409 | -- a proper state tracking 1410 | if H.cache.scroll_is_active then return end 1411 | 1412 | H.cache.scroll_state.cursor = { line = vim.fn.line('.'), virtcol = vim.fn.virtcol('.') } 1413 | end 1414 | 1415 | H.on_cmdline_leave = function() 1416 | local cmd_type = vim.fn.getcmdtype() 1417 | local is_insearch = vim.o.incsearch and (cmd_type == '/' or cmd_type == '?') 1418 | if not is_insearch then return end 1419 | 1420 | -- Update scroll state so that there is no scroll animation after confirming 1421 | -- incremental search. Otherwise it leads to unnecessary animation from 1422 | -- initial scroll state to the one **already shown**. 1423 | H.track_scroll_state() 1424 | end 1425 | 1426 | H.auto_openclose = function(action_type) 1427 | action_type = action_type or 'open' 1428 | 1429 | -- Don't animate if disabled 1430 | local config = H.get_config()[action_type] 1431 | if not config.enable or H.is_disabled() then return end 1432 | 1433 | -- Get window id to act upon 1434 | local win_id 1435 | if action_type == 'close' then win_id = tonumber(vim.fn.expand('')) end 1436 | if action_type == 'open' then win_id = math.max(unpack(vim.api.nvim_list_wins())) end 1437 | 1438 | -- Don't animate if created window is not right (valid and not floating) 1439 | if win_id == nil or not vim.api.nvim_win_is_valid(win_id) then return end 1440 | if vim.api.nvim_win_get_config(win_id).relative ~= '' then return end 1441 | 1442 | -- Register new event only in case there is something to animate 1443 | local event_id_name = action_type .. '_event_id' 1444 | H.cache[event_id_name] = H.cache[event_id_name] + 1 1445 | 1446 | -- Make animation step data and possibly animate 1447 | local animate_step = H.make_openclose_step(action_type, win_id, config) 1448 | if not animate_step then return end 1449 | 1450 | H.start_openclose(action_type) 1451 | MiniAnimate.animate(animate_step.step_action, animate_step.step_timing) 1452 | end 1453 | 1454 | -- General animation ---------------------------------------------------------- 1455 | H.trigger_done_event = function(animation_type) vim.cmd('doautocmd User ' .. H.animation_done_events[animation_type]) end 1456 | 1457 | -- Cursor --------------------------------------------------------------------- 1458 | H.make_cursor_step = function(state_from, state_to, opts) 1459 | local pos_from, pos_to = state_from.pos, state_to.pos 1460 | local destination = { pos_to[1] - pos_from[1], pos_to[2] - pos_from[2] } 1461 | local path = opts.path(destination) 1462 | if path == nil or #path == 0 then return end 1463 | 1464 | local n_steps = #path 1465 | local timing = opts.timing 1466 | 1467 | -- Using explicit buffer id allows correct animation stop after buffer switch 1468 | local event_id, buf_id = H.cache.cursor_event_id, state_from.buf_id 1469 | 1470 | return { 1471 | step_action = function(step) 1472 | -- Undraw previous mark. Doing it before early return allows to clear 1473 | -- last animation mark. 1474 | H.undraw_cursor_mark(buf_id) 1475 | 1476 | -- Stop animation if another cursor movement is active. Don't use 1477 | -- `stop_cursor()` because it will also stop parallel animation. 1478 | if H.cache.cursor_event_id ~= event_id then return false end 1479 | 1480 | -- Don't draw outside of set number of steps or not inside current buffer 1481 | if n_steps <= step or vim.api.nvim_get_current_buf() ~= buf_id then return H.stop_cursor() end 1482 | 1483 | -- Draw cursor mark (starting from initial zero step) 1484 | local pos = path[step + 1] 1485 | H.draw_cursor_mark(pos_from[1] + pos[1], pos_from[2] + pos[2], buf_id) 1486 | return true 1487 | end, 1488 | step_timing = function(step) return timing(step, n_steps) end, 1489 | } 1490 | end 1491 | 1492 | H.get_cursor_state = function() 1493 | -- Use virtual column to respect position outside of line width and tabs 1494 | return { buf_id = vim.api.nvim_get_current_buf(), pos = { vim.fn.line('.'), vim.fn.virtcol('.') } } 1495 | end 1496 | 1497 | H.draw_cursor_mark = function(line, virt_col, buf_id) 1498 | -- Use only absolute coordinates. Allows to not draw outside of buffer. 1499 | if line <= 0 or virt_col <= 0 then return end 1500 | 1501 | -- Compute window column at which to place mark. Don't use explicit `col` 1502 | -- argument because it won't allow placing mark outside of text line. 1503 | local win_col = virt_col - vim.fn.winsaveview().leftcol 1504 | if win_col < 1 then return end 1505 | 1506 | -- Set extmark 1507 | local extmark_opts = { 1508 | id = 1, 1509 | hl_mode = 'combine', 1510 | priority = 1000, 1511 | right_gravity = false, 1512 | virt_text = { { ' ', 'MiniAnimateCursor' } }, 1513 | virt_text_win_col = win_col - 1, 1514 | virt_text_pos = 'overlay', 1515 | } 1516 | pcall(vim.api.nvim_buf_set_extmark, buf_id, H.ns_id.cursor, line - 1, 0, extmark_opts) 1517 | end 1518 | 1519 | H.undraw_cursor_mark = function(buf_id) pcall(vim.api.nvim_buf_del_extmark, buf_id, H.ns_id.cursor, 1) end 1520 | 1521 | H.start_cursor = function() 1522 | H.cache.cursor_is_active = true 1523 | return true 1524 | end 1525 | 1526 | H.stop_cursor = function() 1527 | H.cache.cursor_is_active = false 1528 | H.trigger_done_event('cursor') 1529 | return false 1530 | end 1531 | 1532 | -- Scroll --------------------------------------------------------------------- 1533 | H.make_scroll_step = function(state_from, state_to, opts) 1534 | -- Do not animate in Select mode because it resets it 1535 | local is_select_mode = ({ s = true, S = true, ['\19'] = true })[vim.fn.mode()] 1536 | if is_select_mode then return end 1537 | 1538 | -- Compute how subscrolling is done 1539 | local from_line, to_line = state_from.view.topline, state_to.view.topline 1540 | local total_scroll = H.get_n_visible_lines(from_line, to_line) - 1 1541 | local step_scrolls = opts.subscroll(total_scroll) 1542 | 1543 | -- Don't animate if no subscroll steps is returned 1544 | if step_scrolls == nil or #step_scrolls == 0 then return end 1545 | 1546 | -- Compute scrolling key ('\25' and '\5' are escaped '' and '') 1547 | local scroll_key = from_line < to_line and '\5' or '\25' 1548 | 1549 | -- Cache frequently accessed data 1550 | local from_cur_line, to_cur_line = state_from.cursor.line, state_to.cursor.line 1551 | local from_cur_virtcol, to_cur_virtcol = state_from.cursor.virtcol, state_to.cursor.virtcol 1552 | 1553 | local event_id, buf_id, win_id = H.cache.scroll_event_id, state_from.buf_id, state_from.win_id 1554 | local n_steps, timing = #step_scrolls, opts.timing 1555 | 1556 | return { 1557 | step_action = function(step) 1558 | -- Stop animation if another scroll is active. Don't use `stop_scroll()` 1559 | -- because it will stop parallel animation. 1560 | if H.cache.scroll_event_id ~= event_id then return false end 1561 | 1562 | -- Stop animation if jumped to different buffer or window. Don't restore 1563 | -- window view as it can only operate on current window. 1564 | local is_same_win_buf = vim.api.nvim_get_current_buf() == buf_id and vim.api.nvim_get_current_win() == win_id 1565 | if not is_same_win_buf then return H.stop_scroll() end 1566 | 1567 | -- Compute intermediate cursor position. This relies on `virtualedit=all` 1568 | -- to be able to place cursor anywhere on screen (has better animation; 1569 | -- at least for default equally spread subscrolls). 1570 | local coef = step / n_steps 1571 | local cursor_line = H.convex_point(from_cur_line, to_cur_line, coef) 1572 | local cursor_virtcol = H.convex_point(from_cur_virtcol, to_cur_virtcol, coef) 1573 | local cursor_data = { line = cursor_line, virtcol = cursor_virtcol } 1574 | 1575 | -- Perform scroll. Possibly stop on error. 1576 | local ok, _ = pcall(H.scroll_action, scroll_key, step_scrolls[step], cursor_data) 1577 | if not ok then return H.stop_scroll(state_to) end 1578 | 1579 | -- Update current scroll state for two reasons: 1580 | -- - Be able to distinguish manual `WinScrolled` event from one created 1581 | -- by `H.scroll_action()`. 1582 | -- - Be able to start manual scrolling at any animation step. 1583 | H.cache.scroll_state = H.get_scroll_state() 1584 | 1585 | -- Properly stop animation if step is too big 1586 | if n_steps <= step then return H.stop_scroll(state_to) end 1587 | 1588 | return true 1589 | end, 1590 | step_timing = function(step) return timing(step, n_steps) end, 1591 | } 1592 | end 1593 | 1594 | H.scroll_action = function(key, n, cursor_data) 1595 | -- Scroll. Allow supplying non-valid `n` for initial "scroll" which sets 1596 | -- cursor immediately, which reduces flicker. 1597 | if n ~= nil and n > 0 then 1598 | local command = string.format('normal! %d%s', n, key) 1599 | vim.cmd(command) 1600 | end 1601 | 1602 | -- Set cursor to properly handle cursor position 1603 | -- Computation of available top/bottom line depends on `scrolloff = 0` 1604 | -- because otherwise it will go out of bounds causing scroll overshoot with 1605 | -- later "bounce" back on view restore (see 1606 | -- https://github.com/echasnovski/mini.nvim/issues/177). 1607 | local top, bottom = vim.fn.line('w0'), vim.fn.line('w$') 1608 | local line = math.min(math.max(cursor_data.line, top), bottom) 1609 | 1610 | -- Cursor can only be set using byte column. To place it in the most correct 1611 | -- virtual column, tweak output of `virtcol2col()` 1612 | local virtcol = cursor_data.virtcol 1613 | local col = vim.fn.virtcol2col(0, line, virtcol) 1614 | -- - Correct for virtual column being outside of line's last virtual column 1615 | local virtcol_past_lineend = vim.fn.virtcol({ line, '$' }) 1616 | if virtcol_past_lineend <= virtcol then col = col + virtcol - virtcol_past_lineend + 1 end 1617 | 1618 | pcall(vim.api.nvim_win_set_cursor, 0, { line, col - 1 }) 1619 | end 1620 | 1621 | H.start_scroll = function(start_state) 1622 | H.cache.scroll_is_active = true 1623 | -- Disable scrolloff in order to be able to place cursor on top/bottom window 1624 | -- line inside scroll step. 1625 | -- Incorporating `vim.wo.scrolloff` in computation of available top and 1626 | -- bottom window lines works, but only in absence of folds. It gets tricky 1627 | -- otherwise, so disabling on scroll start and restore on scroll end is 1628 | -- better solution. 1629 | vim.wo.scrolloff = 0 1630 | -- Allow placing cursor anywhere on screen for better cursor placing 1631 | vim.wo.virtualedit = 'all' 1632 | 1633 | if start_state ~= nil then 1634 | vim.fn.winrestview(start_state.view) 1635 | -- Track state because `winrestview()` later triggers `WinScrolled`. 1636 | -- Otherwise mapping like `ulua _G.n = 0` (as in 'mini.bracketed') 1637 | -- can result into "inverted scroll": from destination to current state. 1638 | H.track_scroll_state() 1639 | end 1640 | 1641 | return true 1642 | end 1643 | 1644 | H.stop_scroll = function(end_state) 1645 | if end_state ~= nil then 1646 | vim.fn.winrestview(end_state.view) 1647 | H.track_scroll_state() 1648 | end 1649 | 1650 | vim.wo.scrolloff = end_state.scrolloff 1651 | vim.wo.virtualedit = end_state.virtualedit 1652 | 1653 | H.cache.scroll_is_active = false 1654 | H.trigger_done_event('scroll') 1655 | 1656 | return false 1657 | end 1658 | 1659 | H.get_scroll_state = function() 1660 | return { 1661 | buf_id = vim.api.nvim_get_current_buf(), 1662 | win_id = vim.api.nvim_get_current_win(), 1663 | view = vim.fn.winsaveview(), 1664 | cursor = { line = vim.fn.line('.'), virtcol = vim.fn.virtcol('.') }, 1665 | scrolloff = H.cache.scroll_is_active and H.cache.scroll_state.scrolloff or vim.wo.scrolloff, 1666 | virtualedit = H.cache.scroll_is_active and H.cache.scroll_state.virtualedit or vim.wo.virtualedit, 1667 | } 1668 | end 1669 | 1670 | -- Resize --------------------------------------------------------------------- 1671 | H.make_resize_step = function(state_from, state_to, opts) 1672 | -- Compute number of animation steps 1673 | local step_sizes = opts.subresize(state_from.sizes, state_to.sizes) 1674 | if step_sizes == nil or #step_sizes == 0 then return end 1675 | local n_steps = #step_sizes 1676 | 1677 | -- Create animation step 1678 | local event_id, timing = H.cache.resize_event_id, opts.timing 1679 | 1680 | return { 1681 | step_action = function(step) 1682 | -- Do nothing on initialization 1683 | if step == 0 then return true end 1684 | 1685 | -- Stop animation if another resize animation is active. Don't use 1686 | -- `stop_resize()` because it will also stop parallel animation. 1687 | if H.cache.resize_event_id ~= event_id then return false end 1688 | 1689 | -- Perform animation. Possibly stop on error. 1690 | -- Use `false` to not restore cursor position to avoid horizontal flicker 1691 | local ok, _ = pcall(H.apply_resize_state, { sizes = step_sizes[step] }, false) 1692 | if not ok then return H.stop_resize(state_to) end 1693 | 1694 | -- Properly stop animation if step is too big 1695 | if n_steps <= step then return H.stop_resize(state_to) end 1696 | 1697 | return true 1698 | end, 1699 | step_timing = function(step) return timing(step, n_steps) end, 1700 | } 1701 | end 1702 | 1703 | H.start_resize = function(start_state) 1704 | H.cache.resize_is_active = true 1705 | -- Don't restore cursor position to avoid horizontal flicker 1706 | if start_state ~= nil then H.apply_resize_state(start_state, false) end 1707 | return true 1708 | end 1709 | 1710 | H.stop_resize = function(end_state) 1711 | if end_state ~= nil then H.apply_resize_state(end_state, true) end 1712 | H.cache.resize_is_active = false 1713 | H.trigger_done_event('resize') 1714 | return false 1715 | end 1716 | 1717 | H.get_resize_state = function() 1718 | local layout = vim.fn.winlayout() 1719 | 1720 | local windows = H.get_layout_windows(layout) 1721 | local sizes, views = {}, {} 1722 | for _, win_id in ipairs(windows) do 1723 | sizes[win_id] = { height = vim.api.nvim_win_get_height(win_id), width = vim.api.nvim_win_get_width(win_id) } 1724 | views[win_id] = vim.api.nvim_win_call(win_id, function() return vim.fn.winsaveview() end) 1725 | end 1726 | 1727 | return { layout = layout, sizes = sizes, views = views } 1728 | end 1729 | 1730 | H.is_equal_resize_state = function(state_1, state_2) 1731 | return { 1732 | layout = vim.deep_equal(state_1.layout, state_2.layout), 1733 | sizes = vim.deep_equal(state_1.sizes, state_2.sizes), 1734 | } 1735 | end 1736 | 1737 | H.get_layout_windows = function(layout) 1738 | local res = {} 1739 | local traverse 1740 | traverse = function(l) 1741 | if l[1] == 'leaf' then 1742 | table.insert(res, l[2]) 1743 | return 1744 | end 1745 | for _, sub_l in ipairs(l[2]) do 1746 | traverse(sub_l) 1747 | end 1748 | end 1749 | traverse(layout) 1750 | 1751 | return res 1752 | end 1753 | 1754 | H.apply_resize_state = function(state, full_view) 1755 | -- Set window sizes while ensuring that 'cmdheight' will not change. Can 1756 | -- happen if changing height of window main row layout or increase terminal 1757 | -- height quickly (see #270) 1758 | local cache_cmdheight = vim.o.cmdheight 1759 | 1760 | for win_id, dims in pairs(state.sizes) do 1761 | vim.api.nvim_win_set_height(win_id, dims.height) 1762 | vim.api.nvim_win_set_width(win_id, dims.width) 1763 | end 1764 | 1765 | vim.o.cmdheight = cache_cmdheight 1766 | 1767 | -- Use `or {}` to allow states without `view` (mainly inside animation) 1768 | for win_id, view in pairs(state.views or {}) do 1769 | vim.api.nvim_win_call(win_id, function() 1770 | -- Allow to not restore full view. It mainly solves horizontal flickering 1771 | -- when resizing from small to big width and cursor is on the end of long 1772 | -- line. This is especially visible for Neovim>=0.9 and high 'winwidth'. 1773 | -- Example: `set winwidth=120 winheight=40` and hop between two 1774 | -- vertically split windows with cursor on `$` of long line. 1775 | if full_view then 1776 | vim.fn.winrestview(view) 1777 | return 1778 | end 1779 | 1780 | -- This triggers `CursorMoved` event, but nothing can be done 1781 | -- (`noautocmd` is of no use, see https://github.com/vim/vim/issues/2084) 1782 | pcall(vim.api.nvim_win_set_cursor, win_id, { view.lnum, view.leftcol }) 1783 | vim.fn.winrestview({ topline = view.topline, leftcol = view.leftcol }) 1784 | end) 1785 | end 1786 | 1787 | -- Update current resize state to be able to start another resize animation 1788 | -- at any current animation step. Recompute state to also capture `view`. 1789 | H.cache.resize_state = H.get_resize_state() 1790 | end 1791 | 1792 | -- Open/close ----------------------------------------------------------------- 1793 | H.make_openclose_step = function(action_type, win_id, config) 1794 | -- Compute winconfig progression 1795 | local step_winconfigs = config.winconfig(win_id) 1796 | if step_winconfigs == nil or #step_winconfigs == 0 then return end 1797 | 1798 | -- Produce animation steps. 1799 | local n_steps, event_id_name = #step_winconfigs, action_type .. '_event_id' 1800 | local timing, winblend, event_id = config.timing, config.winblend, H.cache[event_id_name] 1801 | local float_win_id 1802 | 1803 | return { 1804 | step_action = function(step) 1805 | -- Stop animation if another similar animation is active. Don't use 1806 | -- `stop_openclose()` because it will also stop parallel animation. 1807 | if H.cache[event_id_name] ~= event_id then 1808 | pcall(vim.api.nvim_win_close, float_win_id, true) 1809 | return false 1810 | end 1811 | 1812 | -- Stop animation if exceeded number of steps 1813 | if n_steps <= step then 1814 | pcall(vim.api.nvim_win_close, float_win_id, true) 1815 | return H.stop_openclose(action_type) 1816 | end 1817 | 1818 | -- Empty buffer should always be valid (might have been closed by user command) 1819 | if H.empty_buf_id == nil or not vim.api.nvim_buf_is_valid(H.empty_buf_id) then 1820 | H.empty_buf_id = vim.api.nvim_create_buf(false, true) 1821 | H.set_buf_name(H.empty_buf_id, 'open-close-scratch') 1822 | end 1823 | 1824 | -- Set step config to window. Possibly (re)open (it could have been 1825 | -- manually closed like after `:only`) 1826 | local float_config = step_winconfigs[step + 1] 1827 | if step == 0 or not vim.api.nvim_win_is_valid(float_win_id) then 1828 | float_win_id = vim.api.nvim_open_win(H.empty_buf_id, false, float_config) 1829 | vim.wo[float_win_id].winhighlight = 'Normal:MiniAnimateNormalFloat' 1830 | else 1831 | vim.api.nvim_win_set_config(float_win_id, float_config) 1832 | end 1833 | 1834 | vim.wo[float_win_id].winblend = H.round(winblend(step, n_steps)) 1835 | 1836 | return true 1837 | end, 1838 | step_timing = function(step) return timing(step, n_steps) end, 1839 | } 1840 | end 1841 | 1842 | H.start_openclose = function(action_type) 1843 | H.cache[action_type .. '_is_active'] = true 1844 | return true 1845 | end 1846 | 1847 | H.stop_openclose = function(action_type) 1848 | H.cache[action_type .. '_is_active'] = false 1849 | H.trigger_done_event(action_type) 1850 | return false 1851 | end 1852 | 1853 | -- Animation timings ---------------------------------------------------------- 1854 | H.normalize_timing_opts = function(x) 1855 | x = vim.tbl_deep_extend('force', H.get_config(), { easing = 'in-out', duration = 20, unit = 'step' }, x or {}) 1856 | H.validate_if(H.is_valid_timing_opts, x, 'opts') 1857 | return x 1858 | end 1859 | 1860 | H.is_valid_timing_opts = function(x) 1861 | if type(x.duration) ~= 'number' or x.duration < 0 then 1862 | return false, [[In `gen_timing` option `duration` should be a positive number.]] 1863 | end 1864 | 1865 | if not vim.tbl_contains({ 'in', 'out', 'in-out' }, x.easing) then 1866 | return false, [[In `gen_timing` option `easing` should be one of 'in', 'out', or 'in-out'.]] 1867 | end 1868 | 1869 | if not vim.tbl_contains({ 'total', 'step' }, x.unit) then 1870 | return false, [[In `gen_timing` option `unit` should be one of 'step' or 'total'.]] 1871 | end 1872 | 1873 | return true 1874 | end 1875 | 1876 | --- Imitate common power easing function 1877 | --- 1878 | --- Every step is preceded by waiting time decreasing/increasing in power 1879 | --- series fashion (`d` is "delta", ensures total duration time): 1880 | --- - "in": d*n^p; d*(n-1)^p; ... ; d*2^p; d*1^p 1881 | --- - "out": d*1^p; d*2^p; ... ; d*(n-1)^p; d*n^p 1882 | --- - "in-out": "in" until 0.5*n, "out" afterwards 1883 | --- 1884 | --- This way it imitates `power + 1` common easing function because animation 1885 | --- progression behaves as sum of `power` elements. 1886 | --- 1887 | ---@param power number Power of series. 1888 | ---@param opts table Options from `MiniAnimate.gen_timing` entry. 1889 | ---@private 1890 | H.timing_arithmetic = function(power, opts) 1891 | -- Sum of first `n_steps` natural numbers raised to `power` 1892 | local arith_power_sum = ({ 1893 | [0] = function(n_steps) return n_steps end, 1894 | [1] = function(n_steps) return n_steps * (n_steps + 1) / 2 end, 1895 | [2] = function(n_steps) return n_steps * (n_steps + 1) * (2 * n_steps + 1) / 6 end, 1896 | [3] = function(n_steps) return n_steps ^ 2 * (n_steps + 1) ^ 2 / 4 end, 1897 | })[power] 1898 | 1899 | -- Function which computes common delta so that overall duration will have 1900 | -- desired value (based on supplied `opts`) 1901 | local duration_unit, duration_value = opts.unit, opts.duration 1902 | local make_delta = function(n_steps, is_in_out) 1903 | local total_time = duration_unit == 'total' and duration_value or (duration_value * n_steps) 1904 | local total_parts 1905 | if is_in_out then 1906 | -- Examples: 1907 | -- - n_steps=5: 3^d, 2^d, 1^d, 2^d, 3^d 1908 | -- - n_steps=6: 3^d, 2^d, 1^d, 1^d, 2^d, 3^d 1909 | total_parts = 2 * arith_power_sum(math.ceil(0.5 * n_steps)) - (n_steps % 2 == 1 and 1 or 0) 1910 | else 1911 | total_parts = arith_power_sum(n_steps) 1912 | end 1913 | return total_time / total_parts 1914 | end 1915 | 1916 | return ({ 1917 | ['in'] = function(s, n) return make_delta(n) * (n - s + 1) ^ power end, 1918 | ['out'] = function(s, n) return make_delta(n) * s ^ power end, 1919 | ['in-out'] = function(s, n) 1920 | local n_half = math.ceil(0.5 * n) 1921 | local s_halved 1922 | if n % 2 == 0 then 1923 | s_halved = s <= n_half and (n_half - s + 1) or (s - n_half) 1924 | else 1925 | s_halved = s < n_half and (n_half - s + 1) or (s - n_half + 1) 1926 | end 1927 | return make_delta(n, true) * s_halved ^ power 1928 | end, 1929 | })[opts.easing] 1930 | end 1931 | 1932 | --- Imitate common exponential easing function 1933 | --- 1934 | --- Every step is preceded by waiting time decreasing/increasing in geometric 1935 | --- progression fashion (`d` is 'delta', ensures total duration time): 1936 | --- - 'in': (d-1)*d^(n-1); (d-1)*d^(n-2); ...; (d-1)*d^1; (d-1)*d^0 1937 | --- - 'out': (d-1)*d^0; (d-1)*d^1; ...; (d-1)*d^(n-2); (d-1)*d^(n-1) 1938 | --- - 'in-out': 'in' until 0.5*n, 'out' afterwards 1939 | --- 1940 | ---@param opts table Options from `MiniAnimate.gen_timing` entry. 1941 | ---@private 1942 | H.timing_geometrical = function(opts) 1943 | -- Function which computes common delta so that overall duration will have 1944 | -- desired value (based on supplied `opts`) 1945 | local duration_unit, duration_value = opts.unit, opts.duration 1946 | local make_delta = function(n_steps, is_in_out) 1947 | local total_time = duration_unit == 'step' and (duration_value * n_steps) or duration_value 1948 | -- Exact solution to avoid possible (bad) approximation 1949 | if n_steps == 1 then return total_time + 1 end 1950 | if is_in_out then 1951 | local n_half = math.ceil(0.5 * n_steps) 1952 | if n_steps % 2 == 1 then total_time = total_time + math.pow(0.5 * total_time + 1, 1 / n_half) - 1 end 1953 | return math.pow(0.5 * total_time + 1, 1 / n_half) 1954 | end 1955 | return math.pow(total_time + 1, 1 / n_steps) 1956 | end 1957 | 1958 | return ({ 1959 | ['in'] = function(s, n) 1960 | local delta = make_delta(n) 1961 | return (delta - 1) * delta ^ (n - s) 1962 | end, 1963 | ['out'] = function(s, n) 1964 | local delta = make_delta(n) 1965 | return (delta - 1) * delta ^ (s - 1) 1966 | end, 1967 | ['in-out'] = function(s, n) 1968 | local n_half, delta = math.ceil(0.5 * n), make_delta(n, true) 1969 | local s_halved 1970 | if n % 2 == 0 then 1971 | s_halved = s <= n_half and (n_half - s) or (s - n_half - 1) 1972 | else 1973 | s_halved = s < n_half and (n_half - s) or (s - n_half) 1974 | end 1975 | return (delta - 1) * delta ^ s_halved 1976 | end, 1977 | })[opts.easing] 1978 | end 1979 | 1980 | -- Animation path ------------------------------------------------------------- 1981 | H.path_line = function(destination, opts) 1982 | -- Don't animate in case of false predicate 1983 | if not opts.predicate(destination) then return {} end 1984 | 1985 | -- Travel along the biggest horizontal/vertical difference, but stop one 1986 | -- step before destination 1987 | local l, c = destination[1], destination[2] 1988 | local l_abs, c_abs = math.abs(l), math.abs(c) 1989 | local max_diff = math.min(math.max(l_abs, c_abs), opts.max_output_steps) 1990 | 1991 | local res = {} 1992 | for i = 0, max_diff - 1 do 1993 | local prop = i / max_diff 1994 | table.insert(res, { H.round(prop * l), H.round(prop * c) }) 1995 | end 1996 | return res 1997 | end 1998 | 1999 | H.default_path_predicate = function(destination) return destination[1] < -1 or 1 < destination[1] end 2000 | 2001 | -- Animation subscroll -------------------------------------------------------- 2002 | H.subscroll_equal = function(total_scroll, opts) 2003 | -- Don't animate in case of false predicate 2004 | if not opts.predicate(total_scroll) then return {} end 2005 | 2006 | -- Make equal steps, but no more than `max_output_steps` 2007 | local n_steps = math.min(total_scroll, opts.max_output_steps) 2008 | local res, coef = {}, total_scroll / n_steps 2009 | for i = 1, n_steps do 2010 | res[i] = math.floor(i * coef) - math.floor((i - 1) * coef) 2011 | end 2012 | return res 2013 | end 2014 | 2015 | H.default_subscroll_predicate = function(total_scroll) return total_scroll > 1 end 2016 | 2017 | -- Animation subresize -------------------------------------------------------- 2018 | H.subresize_equal = function(sizes_from, sizes_to, opts) 2019 | -- Don't animate in case of false predicate 2020 | if not opts.predicate(sizes_from, sizes_to) then return {} end 2021 | 2022 | -- Don't animate single window 2023 | if #vim.tbl_keys(sizes_from) == 1 then return {} end 2024 | 2025 | -- Compute number of steps 2026 | local n_steps = 0 2027 | for win_id, dims_from in pairs(sizes_from) do 2028 | local height_absidff = math.abs(sizes_to[win_id].height - dims_from.height) 2029 | local width_absidff = math.abs(sizes_to[win_id].width - dims_from.width) 2030 | n_steps = math.max(n_steps, height_absidff, width_absidff) 2031 | end 2032 | if n_steps <= 1 then return {} end 2033 | 2034 | -- Make subresize array 2035 | local res = {} 2036 | for i = 1, n_steps do 2037 | local coef = i / n_steps 2038 | local sub_res = {} 2039 | for win_id, dims_from in pairs(sizes_from) do 2040 | sub_res[win_id] = { 2041 | height = H.convex_point(dims_from.height, sizes_to[win_id].height, coef), 2042 | width = H.convex_point(dims_from.width, sizes_to[win_id].width, coef), 2043 | } 2044 | end 2045 | res[i] = sub_res 2046 | end 2047 | 2048 | return res 2049 | end 2050 | 2051 | H.default_subresize_predicate = function(sizes_from, sizes_to) return true end 2052 | 2053 | -- Animation winconfig -------------------------------------------------------- 2054 | H.winconfig_static = function(win_id, opts) 2055 | -- Don't animate in case of false predicate 2056 | if not opts.predicate(win_id) then return {} end 2057 | 2058 | local pos = vim.fn.win_screenpos(win_id) 2059 | local width, height = vim.api.nvim_win_get_width(win_id), vim.api.nvim_win_get_height(win_id) 2060 | local res = {} 2061 | for i = 1, opts.n_steps do 2062 | --stylua: ignore 2063 | res[i] = { 2064 | relative = 'editor', 2065 | anchor = 'NW', 2066 | row = pos[1] - 1, 2067 | col = pos[2] - 1, 2068 | width = width, 2069 | height = height, 2070 | focusable = false, 2071 | zindex = 1, 2072 | border = 'none', 2073 | style = 'minimal', 2074 | } 2075 | end 2076 | return res 2077 | end 2078 | 2079 | H.get_window_parent_container = function(win_id) 2080 | local f 2081 | f = function(layout, parent_container) 2082 | local container, second = layout[1], layout[2] 2083 | if container == 'leaf' then 2084 | if second == win_id then return parent_container end 2085 | return 2086 | end 2087 | 2088 | for _, sub_layout in ipairs(second) do 2089 | local res = f(sub_layout, container) 2090 | if res ~= nil then return res end 2091 | end 2092 | end 2093 | 2094 | -- Important to get layout of tabpage window actually belongs to (as it can 2095 | -- already be not current tabpage) 2096 | -- NOTE: `winlayout()` takes tabpage number (non unique), not tabpage id 2097 | local tabpage_id = vim.api.nvim_win_get_tabpage(win_id) 2098 | local tabpage_nr = vim.api.nvim_tabpage_get_number(tabpage_id) 2099 | return f(vim.fn.winlayout(tabpage_nr), 'single') 2100 | end 2101 | 2102 | H.default_winconfig_predicate = function(win_id) return true end 2103 | 2104 | -- Utilities ------------------------------------------------------------------ 2105 | H.error = function(msg) error('(mini.animate) ' .. msg, 0) end 2106 | 2107 | H.check_type = function(name, val, ref, allow_nil) 2108 | if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end 2109 | H.error(string.format('`%s` should be %s, not %s', name, ref, type(val))) 2110 | end 2111 | 2112 | H.set_buf_name = function(buf_id, name) vim.api.nvim_buf_set_name(buf_id, 'minianimate://' .. buf_id .. '/' .. name) end 2113 | 2114 | H.validate_if = function(predicate, x, x_name) 2115 | local is_valid, msg = predicate(x, x_name) 2116 | if not is_valid then H.error(msg) end 2117 | end 2118 | 2119 | H.get_n_visible_lines = function(from_line, to_line) 2120 | local min_line, max_line = math.min(from_line, to_line), math.max(from_line, to_line) 2121 | 2122 | -- If `max_line` is inside fold, scrol should stop on the fold (not after) 2123 | local max_line_fold_start = vim.fn.foldclosed(max_line) 2124 | local target_line = max_line_fold_start == -1 and max_line or max_line_fold_start 2125 | 2126 | local i, res = min_line, 1 2127 | while i < target_line do 2128 | res = res + 1 2129 | local end_fold_line = vim.fn.foldclosedend(i) 2130 | i = (end_fold_line == -1 and i or end_fold_line) + 1 2131 | end 2132 | return res 2133 | end 2134 | 2135 | H.round = function(x) return math.floor(x + 0.5) end 2136 | 2137 | H.convex_point = function(x, y, coef) return H.round((1 - coef) * x + coef * y) end 2138 | 2139 | return MiniAnimate 2140 | --------------------------------------------------------------------------------