From 3362c8c89fc3c2cee6e893d1ff4b95308cd50e7c Mon Sep 17 00:00:00 2001 From: Hirohito Higashi Date: Fri, 11 Sep 2026 21:15:43 +0000 Subject: [PATCH] patch 9.2.1074: popup: image stays on the screen in another tab page Problem: The image of a tab-local popup window stays on the screen after switching to another tab page, while the border and the background of the popup are gone. The image backends keep an image until it is removed: a kitty placement and a GTK4 texture until they are deleted, GDI and cairo pixels until the area is repainted, sixel pixels until the screen is cleared. Solution: Remove the images of the popups local to a tab page when the tab page is left, they are emitted again when it is entered. (Hirohito Higashi). related: #21234 closes: #21273 Co-Authored-By: Claude Fable 5.1 Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- src/popupwin.c | 37 ++++++++++++ src/proto/popupwin.pro | 1 + src/testdir/test_popupwin.vim | 107 ++++++++++++++++++++++++++++++++++ src/version.c | 2 + src/window.c | 3 + 5 files changed, 150 insertions(+) diff --git a/src/popupwin.c b/src/popupwin.c index e7ad8f279e..aacde4b652 100644 --- a/src/popupwin.c +++ b/src/popupwin.c @@ -4645,6 +4645,43 @@ popup_close(int id, int force) return FAIL; } +#ifdef FEAT_IMAGE +/* + * Remove the images of the popups local to tabpage "tp", it is about to be + * left. The popups are not drawn in another tabpage, their images stay until + * they are removed. They are emitted again when the tabpage is entered. + */ + void +popup_leave_tabpage(tabpage_T *tp) +{ + win_T *wp; + + FOR_ALL_POPUPWINS_IN_TAB(tp, wp) + { + if ((wp->w_popup_flags & POPF_HIDDEN) || wp->w_popup_image_data == NULL) + continue; +# ifdef FEAT_IMAGE_KITTY + popup_image_clear_kitty(wp, false); +# endif +# ifdef FEAT_IMAGE_GDK + if (gui.in_use) + gui_gtk4_remove_image(wp); +# endif +# ifdef POPUP_IMAGE_CLEAR_GUI + popup_image_clear_gui(wp); +# endif +# ifdef FEAT_IMAGE_SIXEL + // Sixel pixels only go away with a clear of the screen. +# ifdef FEAT_GUI + if (!gui.in_use) +# endif + if (popup_image_backend() == IMAGE_BACKEND_SIXEL) + redraw_later_clear(); +# endif + } +} +#endif + /* * Close a popup window with Window-id "id" in tabpage "tp". */ diff --git a/src/proto/popupwin.pro b/src/proto/popupwin.pro index f29aef9d08..d61727d1ac 100644 --- a/src/proto/popupwin.pro +++ b/src/proto/popupwin.pro @@ -39,6 +39,7 @@ void f_popup_settext(typval_T *argvars, typval_T *rettv); void f_popup_setbuf(typval_T *argvars, typval_T *rettv); int error_if_popup_window(int also_with_term); int popup_close(int id, int force); +void popup_leave_tabpage(tabpage_T *tp); int popup_close_tabpage(tabpage_T *tp, int id, int force); void close_all_popups(int force); void f_popup_move(typval_T *argvars, typval_T *rettv); diff --git a/src/testdir/test_popupwin.vim b/src/testdir/test_popupwin.vim index b43bbae66e..271a6e7e8e 100644 --- a/src/testdir/test_popupwin.vim +++ b/src/testdir/test_popupwin.vim @@ -6310,6 +6310,113 @@ func Test_popup_image_clipwindow_scroll() call prop_type_delete('imgclipprop') endfunc +" The kitty and sixel image backends cannot be tested in a terminal window: +" it does not show the escape sequences Vim writes and does not answer the +" probe for the kitty graphics protocol. Run Vim on a pty as a job instead +" and look at what it writes there. + +" The sequences of the kitty graphics protocol Vim writes, "a=" is the action. +" See https://sw.kovidgoyal.net/kitty/graphics-protocol/ +" a=q: does the terminal support the protocol? +let s:kitty_probe = "\_Gi=31" +" What a terminal that supports the protocol answers, followed by the reply to +" the DA1 request that ends the probe. +let s:kitty_probe_answer = "\_Gi=31;OK\\\\[?62;4c" +" a=t: transmit the pixels of an image. +let s:kitty_transmit = "\_Ga=t," +" a=p: place a transmitted image on the screen. +let s:kitty_place = "\_Ga=p," +" a=d,d=i: delete the placement of an image, the terminal keeps the pixels. +let s:kitty_delete = "\_Ga=d,d=i," + +" The start of a sixel image, a DCS with the "q" command. +let s:sixel_start_pat = "\P[0-9;]*q" +" Clear the screen, the only way to remove sixel pixels on some terminals. +let s:clear_screen = "\[2J" + +" The script for the Vim on the pty: create a popup with an image. +let s:image_popup_script =<< trim END + let img = repeat([0xff, 0, 0], 16 * 32)->list2blob() + call popup_create('', #{image: #{data: img, width: 16, height: 32}}) + redraw +END + +" Collect what the Vim on the pty writes in s:pty_out. With "kitty" the probe +" is answered like a terminal that supports the protocol, otherwise Vim gets +" no answer and uses sixel. +func s:PtyOutput(job, msg, kitty) + let s:pty_out ..= a:msg + if a:kitty && !s:pty_answered && stridx(s:pty_out, s:kitty_probe) >= 0 + call ch_sendraw(a:job, s:kitty_probe_answer) + let s:pty_answered = 1 + endif +endfunc + +" Start Vim on a pty with the script written to XpopupImageTab. +func s:StartVimWithImageOnPty(kitty) + let s:pty_out = '' + let s:pty_answered = 0 + return job_start(GetVimCommandCleanTerm() .. ' -S XpopupImageTab', #{ + \ pty: 1, + \ out_mode: 'raw', + \ env: #{TERM: 'xterm', LINES: '24', COLUMNS: '80'}, + \ out_cb: {job, msg -> s:PtyOutput(job, msg, a:kitty)}, + \ }) +endfunc + +" Wait for the Vim on the pty to write "seq" after the first "start" bytes. +func s:WaitForPtyOutput(seq, start) + call WaitForAssert({-> assert_notequal(-1, + \ stridx(s:pty_out, a:seq, a:start))}) +endfunc + +func Test_popup_image_kitty_leave_tabpage() + CheckUnix + CheckFeature job + CheckFeature image_kitty + + call writefile(s:image_popup_script, 'XpopupImageTab', 'D') + let job = s:StartVimWithImageOnPty(1) + try + call s:WaitForPtyOutput(s:kitty_transmit, 0) + call s:WaitForPtyOutput(s:kitty_place, 0) + + " Leaving the tab page deletes the placement. + let start = len(s:pty_out) + call ch_sendraw(job, ":tabedit\") + call s:WaitForPtyOutput(s:kitty_delete, start) + + " Entering the tab page places the image again without transmitting it. + let start = len(s:pty_out) + call ch_sendraw(job, ":tabnext\") + call s:WaitForPtyOutput(s:kitty_place, start) + call assert_equal(-1, stridx(s:pty_out, s:kitty_transmit, start)) + finally + call job_stop(job, 'kill') + call WaitForAssert({-> assert_equal('dead', job_status(job))}) + endtry +endfunc + +func Test_popup_image_sixel_leave_tabpage() + CheckUnix + CheckFeature job + CheckFeature image_sixel + + call writefile(s:image_popup_script, 'XpopupImageTab', 'D') + let job = s:StartVimWithImageOnPty(0) + try + call WaitForAssert({-> assert_match(s:sixel_start_pat, s:pty_out)}) + + " Leaving the tab page clears the screen. + let start = len(s:pty_out) + call ch_sendraw(job, ":tabedit\") + call s:WaitForPtyOutput(s:clear_screen, start) + finally + call job_stop(job, 'kill') + call WaitForAssert({-> assert_equal('dead', job_status(job))}) + endtry +endfunc + func Test_popupwin_textprop_redraw() CheckScreendump diff --git a/src/version.c b/src/version.c index 6a81327c38..956539eb1d 100644 --- a/src/version.c +++ b/src/version.c @@ -763,6 +763,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 1074, /**/ 1073, /**/ diff --git a/src/window.c b/src/window.c index e1c108ff1a..85bb5ea460 100644 --- a/src/window.c +++ b/src/window.c @@ -5116,6 +5116,9 @@ leave_tabpage( return FAIL; } +#ifdef FEAT_IMAGE + popup_leave_tabpage(tp); +#endif reset_dragwin(); #if defined(FEAT_GUI) // Remove the scrollbars. They may be added back later.