diff --git a/lua/image/image.lua b/lua/image/image.lua index 31136d8..628c839 100644 --- a/lua/image/image.lua +++ b/lua/image/image.lua @@ -49,6 +49,7 @@ function Image:render(geometry) if not self.global_state.enabled then return end if geometry then self.geometry = vim.tbl_deep_extend("force", self.geometry, geometry) end + if self.global_state.rendering_paused then return end -- don't render if we are in the conmmand-line-window, in this case previously rendered images can -- be left in place diff --git a/lua/image/init.lua b/lua/image/init.lua index f547fb6..a3b7d1a 100644 --- a/lua/image/init.lua +++ b/lua/image/init.lua @@ -76,6 +76,7 @@ local state = { remote_cache = {}, tmp_dir = vim.fn.tempname(), disable_decorator_handling = false, + rendering_paused = false, hijacked_win_buf_images = {}, enabled = true, } @@ -384,22 +385,27 @@ api.setup = function(options) or (state.options.tmux_show_only_in_active_window and utils.tmux.is_tmux) then local images_to_restore_on_focus = {} + local focus_event_generation = 0 local initial_tmux_window_id = utils.tmux.get_window_id() local initial_tmux_session = utils.tmux.get_current_session() vim.api.nvim_create_autocmd("FocusLost", { group = group, callback = function() -- auto-clear images when windows and buffers change - -- bail if not enabled + focus_event_generation = focus_event_generation + 1 + local generation = focus_event_generation + if state.options.editor_only_render_when_focused then state.rendering_paused = true end if not state.enabled then return end vim.schedule(function() + if generation ~= focus_event_generation then return end log.debug("FocusLost") if state.options.editor_only_render_when_focused or (utils.tmux.is_tmux and utils.tmux.get_window_id() ~= initial_tmux_window_id) or (utils.tmux.is_tmux and utils.tmux.get_current_session() ~= initial_tmux_session) then + state.rendering_paused = true state.disable_decorator_handling = true local images = api.get_images() @@ -417,13 +423,14 @@ api.setup = function(options) vim.api.nvim_create_autocmd("FocusGained", { group = group, callback = function() -- auto-clear images when windows and buffers change - -- bail if not enabled + focus_event_generation = focus_event_generation + 1 + local was_rendering_paused = state.rendering_paused + state.rendering_paused = false + state.disable_decorator_handling = false if not state.enabled then return end log.debug("FocusGained") - state.disable_decorator_handling = false - vim.schedule_wrap(function() -- force rerender local images = api.get_images() @@ -431,12 +438,14 @@ api.setup = function(options) if current_image.is_rendered then current_image:clear() current_image:render() + elseif was_rendering_paused then + current_image:render() end end -- render images cleared on focus loss for _, current_image in ipairs(images_to_restore_on_focus) do - current_image:render() + if not current_image.is_rendered then current_image:render() end end images_to_restore_on_focus = {} end)() diff --git a/lua/types.lua b/lua/types.lua index e0a7c54..9e73a15 100644 --- a/lua/types.lua +++ b/lua/types.lua @@ -20,6 +20,7 @@ ---@field remote_cache { [string]: string } ---@field tmp_dir string ---@field disable_decorator_handling boolean +---@field rendering_paused boolean ---@field hijacked_win_buf_images { [string]: Image } ---@field processor ImageProcessor diff --git a/tests/core/focus_rendering_spec.lua b/tests/core/focus_rendering_spec.lua new file mode 100644 index 0000000..38a754b --- /dev/null +++ b/tests/core/focus_rendering_spec.lua @@ -0,0 +1,108 @@ +local unload_image_modules = function() + for module_name in pairs(package.loaded) do + if module_name == "image" or module_name:match("^image/") then package.loaded[module_name] = nil end + end +end + +local disabled_integrations = { + markdown = { enabled = false }, + asciidoc = { enabled = false }, + typst = { enabled = false }, + neorg = { enabled = false }, + rst = { enabled = false }, + syslang = { enabled = false }, + html = { enabled = false }, + css = { enabled = false }, + org = { enabled = false }, +} + +describe("focus-aware rendering", function() + local originals + local render_calls + local backend_state + + before_each(function() + unload_image_modules() + originals = { + create_autocmd = vim.api.nvim_create_autocmd, + schedule = vim.schedule, + schedule_wrap = vim.schedule_wrap, + uv = vim.uv, + } + render_calls = {} + backend_state = nil + if not vim.uv then vim.uv = vim.loop end + + package.loaded["image/renderer"] = { + render = function(image) + render_calls[#render_calls + 1] = image.id + image.global_state.images[image.id] = image + image.is_rendered = true + return true + end, + clear_cache_for_path = function() end, + } + package.loaded["image/backends/kitty"] = { + features = { crop = true }, + setup = function(state) + backend_state = state + end, + clear = function(id, shallow) + local image = backend_state.images[id] + if not image then return end + image.is_rendered = false + if not shallow then backend_state.images[id] = nil end + end, + render = function(image) + image.is_rendered = true + backend_state.images[image.id] = image + end, + } + end) + + after_each(function() + vim.api.nvim_create_autocmd = originals.create_autocmd + vim.schedule = originals.schedule + vim.schedule_wrap = originals.schedule_wrap + vim.uv = originals.uv + pcall(vim.api.nvim_del_augroup_by_name, "image.nvim") + local namespace = vim.api.nvim_get_namespaces()["image.nvim"] + if namespace then pcall(vim.api.nvim_set_decoration_provider, namespace, {}) end + unload_image_modules() + end) + + it("keeps images hidden while unfocused and restores them on focus gain", function() + local focus_lost + local focus_gained + vim.schedule = function(callback) + callback() + end + vim.schedule_wrap = function(callback) + return callback + end + vim.api.nvim_create_autocmd = function(events, options) + if events == "FocusLost" then focus_lost = options.callback end + if events == "FocusGained" then focus_gained = options.callback end + return originals.create_autocmd(events, options) + end + + local image = require("image") + image.setup({ + editor_only_render_when_focused = true, + integrations = disabled_integrations, + hijack_file_patterns = {}, + }) + vim.api.nvim_create_autocmd = originals.create_autocmd + + local current_image = image.from_file("tests/test_data/100x100.png") + current_image:render() + assert.are.same(1, #render_calls) + + focus_lost() + current_image:render() + assert.are.same(1, #render_calls) + + focus_gained() + assert.are.same(2, #render_calls) + end) +end)