From e68696f0baae66cd17d4a7f0f3d021ca906f49b3 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 29 Apr 2026 11:52:26 +0200 Subject: Delete image placements before window rerenders Window layout changes move image overlays before kitty-graphics recalculates terminal positions. Clearing last-row/last-col first loses the only geometry the backends can use to delete the old terminal placement, which leaves stale pixels over new split separators. Delete the current recorded image placement during window-change handling before clearing the cached position. Also make image-mode placement reuse explicit: zoom commands still reuse a PID to avoid flicker, while split/resize rerenders delete first. --- kitty-graphics.el | 80 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/kitty-graphics.el b/kitty-graphics.el index 0bed240..210d52b 100644 --- a/kitty-graphics.el +++ b/kitty-graphics.el @@ -1694,27 +1694,45 @@ then invalidates position caches and schedules a refresh." (defun kitty-gfx--on-window-change (_frame) "Handle window configuration change for image refresh. -Invalidates position caches and cell pixel size so the refresh -cycle re-places images correctly. Does NOT delete all placements -— that causes visible flicker. Uses a longer debounce than normal -refresh to let Emacs finish window layout transitions (e.g., when -closing a split, Emacs briefly shows two windows for the same -buffer before settling to one)." - (kitty-gfx--log "on-window-change: invalidating positions and cell size") +Invalidates cell pixel size, deletes stale image placements, then +clears image position caches so the refresh cycle re-places images +at their new positions. Uses a longer debounce than normal refresh +to let Emacs finish window layout transitions (e.g., when closing a +split, Emacs briefly shows two windows for the same buffer before +settling to one)." + (kitty-gfx--log "on-window-change: deleting stale placements and invalidating cell size") (setq kitty-gfx--cell-pixel-width nil kitty-gfx--cell-pixel-height nil) - ;; Reset position cache so images get re-placed at correct positions. + ;; Delete image placements before clearing their cached positions. + ;; Window splits/resizes can move an image from the middle of the old + ;; window to the center of the new pane(s). Some terminals do not + ;; reliably erase the old direct placement when it is re-placed at a + ;; different geometry, and Sixel is stateless and must be explicitly + ;; overwritten. If we clear the cache first, the backend no longer + ;; knows where to delete/erase the old placement, leaving artifacts + ;; over newly-created window separators. + ;; ;; Heading overlays PRESERVE their cache — the refresh cycle needs ;; old→new position comparison to erase multicell blocks properly. - ;; Images can clear cache because re-placing with the same PID is - ;; idempotent (the terminal replaces the old placement). - (dolist (buf (buffer-list)) - (with-current-buffer buf - (dolist (ov kitty-gfx--overlays) - (when (and (overlay-buffer ov) - (not (overlay-get ov 'kitty-gfx-heading))) - (overlay-put ov 'kitty-gfx-last-row nil) - (overlay-put ov 'kitty-gfx-last-col nil))))) + (kitty-gfx--sync-begin) + (unwind-protect + (dolist (buf (buffer-list)) + (with-current-buffer buf + (dolist (ov kitty-gfx--overlays) + (when (and (overlay-buffer ov) + (not (overlay-get ov 'kitty-gfx-heading))) + (let ((id (overlay-get ov 'kitty-gfx-id)) + (pid (overlay-get ov 'kitty-gfx-pid))) + (when (and id pid kitty-gfx--active-backend + (overlay-get ov 'kitty-gfx-last-row)) + (condition-case err + (funcall (kitty-gfx--backend-fn 'delete) ov id pid) + (error + (kitty-gfx--log "on-window-change: error deleting placement: %s" + (error-message-string err)))))) + (overlay-put ov 'kitty-gfx-last-row nil) + (overlay-put ov 'kitty-gfx-last-col nil))))) + (kitty-gfx--sync-end)) ;; Longer debounce: cancel any fast leading-edge cooldown and ;; schedule a 0.1s delayed refresh to let window layout settle. (when kitty-gfx--render-timer @@ -2647,8 +2665,13 @@ With an active region, restrict to it; otherwise clear the whole buffer." "Zoom scale factor for image-mode display. Values > 1.0 zoom in, < 1.0 zoom out.") -(defun kitty-gfx--image-mode-render () - "Render the current image file centered at current scale." +(defun kitty-gfx--image-mode-render (&optional reuse-placement) + "Render the current image file centered at current scale. +When REUSE-PLACEMENT is non-nil, reuse the old terminal placement +ID instead of deleting it first. This is useful for zoom commands +where the new placement immediately replaces the old one, but it is +intentionally not used for window size changes because stale pixels +can otherwise remain over newly-created window separators." (when-let* ((file (buffer-file-name))) (when (kitty-gfx--image-file-p file) (let* ((inhibit-read-only t) @@ -2656,15 +2679,18 @@ Values > 1.0 zoom in, < 1.0 zoom out.") (win-h (- (window-body-height) 2)) (max-cols (min win-w kitty-gfx-max-width)) (max-rows (min win-h kitty-gfx-max-height)) - ;; Save the old placement ID before removing overlays. - ;; Reusing it avoids delete+re-place glitches (WezTerm #5892). - (old-pid (when (car kitty-gfx--overlays) + ;; Save the old placement ID only when the caller explicitly + ;; wants to reuse it. Reusing avoids delete+re-place glitches + ;; for zoom commands (WezTerm #5892), but window splits/resizes + ;; must delete first so stale terminal pixels are cleared. + (old-pid (when (and reuse-placement (car kitty-gfx--overlays)) (overlay-get (car kitty-gfx--overlays) 'kitty-gfx-pid)))) (kitty-gfx--log "image-mode-render: file=%s scale=%.2f win=%dx%d max=%dx%d reuse-pid=%s" (file-name-nondirectory file) kitty-gfx--image-scale win-w win-h max-cols max-rows old-pid) - ;; Remove overlays but skip terminal-side delete when we have - ;; a PID to reuse (the new placement will atomically replace it). + ;; Remove overlays. When OLD-PID is non-nil, skip terminal-side + ;; delete so the new placement atomically replaces it; otherwise + ;; delete/erase the old placement before changing the buffer text. (dolist (ov (overlays-in (point-min) (point-max))) (when (overlay-get ov 'kitty-gfx) (kitty-gfx--remove-overlay ov old-pid))) @@ -2679,19 +2705,19 @@ Values > 1.0 zoom in, < 1.0 zoom out.") "Zoom in on the image in image-mode." (interactive) (setq kitty-gfx--image-scale (* kitty-gfx--image-scale 1.25)) - (kitty-gfx--image-mode-render)) + (kitty-gfx--image-mode-render t)) (defun kitty-gfx-image-decrease-size () "Zoom out on the image in image-mode." (interactive) (setq kitty-gfx--image-scale (max 0.1 (* kitty-gfx--image-scale 0.8))) - (kitty-gfx--image-mode-render)) + (kitty-gfx--image-mode-render t)) (defun kitty-gfx-image-reset-size () "Reset image zoom to default in image-mode." (interactive) (setq kitty-gfx--image-scale 1.0) - (kitty-gfx--image-mode-render)) + (kitty-gfx--image-mode-render t)) (defun kitty-gfx--image-mode-advice (orig-fn &rest args) "Around advice for `image-mode'." -- cgit v1.3.1