patch 9.2.0129: popup: wrong handling of wide-chars and opacity:0

Commit: 
https://github.com/vim/vim/commit/433bcf3becac2e4278f6f2cdeaa932a18e62d15f
Author: Yasuhiro Matsumoto <[email protected]>
Date:   Mon Mar 9 18:47:10 2026 +0000

    patch 9.2.0129: popup: wrong handling of wide-chars and opacity:0
    
    Problem:  popup: wrong handling of wide-chars and opacity:0
    Solution: Correctly handle opacity 0, correctly handle wide chars
              (Yasuhiro Matsumoto)
    
    closes: #19606
    
    Signed-off-by: Yasuhiro Matsumoto <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/src/highlight.c b/src/highlight.c
index 0bd5c6e4c..df34f0307 100644
--- a/src/highlight.c
+++ b/src/highlight.c
@@ -3120,6 +3120,10 @@ blend_colors(guicolor_T popup_color, guicolor_T 
bg_color, int blend_val)
     if (popup_color == INVALCOLOR)
        return INVALCOLOR;
 
+    // Fully transparent: use underlying color as-is.
+    if (blend_val >= 100)
+       return bg_color;
+
     r1 = (popup_color >> 16) & 0xFF;
     g1 = (popup_color >> 8) & 0xFF;
     b1 = popup_color & 0xFF;
@@ -3162,8 +3166,8 @@ hl_blend_attr(int char_attr, int popup_attr, int blend, 
int blend_fg UNUSED)
     // If both attrs are 0, return 0
     if (char_attr == 0 && popup_attr == 0)
        return 0;
-    if (blend >= 100)
-       return char_attr;  // Fully transparent, show background only
+    if (blend >= 100 && blend_fg)
+       return char_attr;  // Fully transparent for both fg and bg
 
 #ifdef FEAT_GUI
     if (gui.in_use)
@@ -3205,14 +3209,15 @@ hl_blend_attr(int char_attr, int popup_attr, int blend, 
int blend_fg UNUSED)
                    // blend_fg=FALSE: use popup foreground
                    new_en.ae_u.gui.fg_color = popup_aep->ae_u.gui.fg_color;
                }
-               // Blend background color
+               // Blend background color: blend popup bg toward underlying bg
                if (popup_aep->ae_u.gui.bg_color != INVALCOLOR)
                {
-                   // Always use popup background, fade to black based on blend
-                   int r = ((popup_aep->ae_u.gui.bg_color >> 16) & 0xFF) * 
(100 - blend) / 100;
-                   int g = ((popup_aep->ae_u.gui.bg_color >> 8) & 0xFF) * (100 
- blend) / 100;
-                   int b = (popup_aep->ae_u.gui.bg_color & 0xFF) * (100 - 
blend) / 100;
-                   new_en.ae_u.gui.bg_color = (r << 16) | (g << 8) | b;
+                   guicolor_T underlying_bg = INVALCOLOR;
+                   if (char_aep != NULL)
+                       underlying_bg = char_aep->ae_u.gui.bg_color;
+                   new_en.ae_u.gui.bg_color = blend_colors(
+                           popup_aep->ae_u.gui.bg_color,
+                           underlying_bg, blend);
                }
            }
        }
@@ -3269,11 +3274,13 @@ hl_blend_attr(int char_attr, int popup_attr, int blend, 
int blend_fg UNUSED)
                    new_en.ae_u.cterm.fg_rgb = popup_aep->ae_u.cterm.fg_rgb;
                if (popup_aep->ae_u.cterm.bg_rgb != INVALCOLOR)
                {
-                   // Always use popup background, fade to black based on blend
-                   int r = ((popup_aep->ae_u.cterm.bg_rgb >> 16) & 0xFF) * 
(100 - blend) / 100;
-                   int g = ((popup_aep->ae_u.cterm.bg_rgb >> 8) & 0xFF) * (100 
- blend) / 100;
-                   int b = (popup_aep->ae_u.cterm.bg_rgb & 0xFF) * (100 - 
blend) / 100;
-                   new_en.ae_u.cterm.bg_rgb = (r << 16) | (g << 8) | b;
+                   // Blend popup bg toward underlying bg
+                   guicolor_T underlying_bg = INVALCOLOR;
+                   if (char_aep != NULL)
+                       underlying_bg = char_aep->ae_u.cterm.bg_rgb;
+                   new_en.ae_u.cterm.bg_rgb = blend_colors(
+                           popup_aep->ae_u.cterm.bg_rgb,
+                           underlying_bg, blend);
                }
 #endif
            }
diff --git a/src/popupwin.c b/src/popupwin.c
index a9325e837..63f9fe3f3 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -785,7 +785,13 @@ apply_general_options(win_T *wp, dict_T *dict)
     if (di != NULL)
     {
        nr = dict_get_number(dict, "opacity");
-       if (nr > 0 && nr < 100)
+       if (nr == 0)
+       {
+           // opacity: 0, fully transparent
+           wp->w_popup_flags |= POPF_OPACITY;
+           wp->w_popup_blend = 100;
+       }
+       else if (nr > 0 && nr < 100)
        {
            // opacity: 1-99, partially transparent
            // Convert to blend (0=opaque, 100=transparent)
@@ -4244,6 +4250,38 @@ popup_need_position_adjust(win_T *wp)
     return wp->w_cursor.lnum != wp->w_popup_last_curline;
 }
 
+/*
+ * Force background windows to redraw rows under an opacity popup.
+ */
+    static void
+redraw_win_under_opacity_popup(win_T *wp)
+{
+    int            height;
+    int            r;
+
+    if (!(wp->w_popup_flags & POPF_OPACITY) || wp->w_popup_blend <= 0
+           || (wp->w_popup_flags & POPF_HIDDEN))
+       return;
+
+    height = popup_height(wp);
+    for (r = wp->w_winrow;
+                      r < wp->w_winrow + height && r < screen_Rows; ++r)
+    {
+       int         line_cp = r;
+       int         col_cp = wp->w_wincol;
+       win_T       *twp;
+
+       twp = mouse_find_win(&line_cp, &col_cp, IGNORE_POPUP);
+       if (twp != NULL && line_cp < twp->w_height)
+       {
+           linenr_T lnum;
+
+           (void)mouse_comp_pos(twp, &line_cp, &col_cp, &lnum, NULL);
+           redrawWinline(twp, lnum);
+       }
+    }
+}
+
 /*
  * Update "popup_mask" if needed.
  * Also recomputes the popup size and positions.
@@ -4281,6 +4319,16 @@ may_update_popup_mask(int type)
        else if (popup_need_position_adjust(wp))
            popup_mask_refresh = TRUE;
 
+    // Force background windows to redraw rows under opacity popups.
+    // Opacity popups don't participate in popup_mask, so their area
+    // wouldn't normally be redrawn.  Without this, ScreenAttrs retains
+    // blended values from the previous cycle, causing blend accumulation.
+    // This must run every cycle, not just when popup_mask_refresh is set.
+    FOR_ALL_POPUPWINS(wp)
+       redraw_win_under_opacity_popup(wp);
+    FOR_ALL_POPUPWINS_IN_TAB(curtab, wp)
+       redraw_win_under_opacity_popup(wp);
+
     if (!popup_mask_refresh)
        return;
 
@@ -5310,21 +5358,9 @@ update_popups(void (*win_update)(win_T *wp))
     }
 
 #ifdef FEAT_PROP_POPUP
-    if (base_screenlines != NULL)
-    {
-       vim_free(base_screenlines);
-       base_screenlines = NULL;
-    }
-    if (base_screenattrs != NULL)
-    {
-       vim_free(base_screenattrs);
-       base_screenattrs = NULL;
-    }
-    if (base_screenlinesuc != NULL)
-    {
-       vim_free(base_screenlinesuc);
-       base_screenlinesuc = NULL;
-    }
+    VIM_CLEAR(base_screenlines);
+    VIM_CLEAR(base_screenattrs);
+    VIM_CLEAR(base_screenlinesuc);
     base_screen_rows = 0;
     base_screen_cols = 0;
 #endif
diff --git a/src/screen.c b/src/screen.c
index 01f9118d5..fc99ac372 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -452,6 +452,48 @@ skip_for_popup(int row, int col)
     return FALSE;
 }
 
+#ifdef FEAT_PROP_POPUP
+/*
+ * For a double-wide character at a popup boundary with opacity:0
+ * (blend==100), the two cells may have different underlying attrs.
+ * Pick the one without a background color to prevent color leaking.
+ */
+    static void
+resolve_wide_char_opacity_attrs(
+       int row, int col1, int col2,
+       sattr_T *attr1, sattr_T *attr2)
+{
+    int                bg1, bg2;
+    int                base1 = 0;
+    int                base2 = 0;
+    attrentry_T        *ae;
+
+    if (*attr1 == *attr2)
+       return;
+
+    popup_get_base_screen_cell(row, col1, NULL, &base1, NULL);
+    ae = syn_cterm_attr2entry(base1);
+# ifdef FEAT_TERMGUICOLORS
+    bg1 = (ae != NULL && !COLOR_INVALID(ae->ae_u.cterm.bg_rgb));
+# else
+    bg1 = (ae != NULL && ae->ae_u.cterm.bg_color != 0);
+# endif
+
+    popup_get_base_screen_cell(row, col2, NULL, &base2, NULL);
+    ae = syn_cterm_attr2entry(base2);
+# ifdef FEAT_TERMGUICOLORS
+    bg2 = (ae != NULL && !COLOR_INVALID(ae->ae_u.cterm.bg_rgb));
+# else
+    bg2 = (ae != NULL && ae->ae_u.cterm.bg_color != 0);
+# endif
+
+    if (bg1 && !bg2)
+       *attr1 = *attr2;
+    else if (!bg1 && bg2)
+       *attr2 = *attr1;
+}
+#endif
+
 /*
  * Move one "cooked" screen line to the screen, but only the characters that
  * have actually changed.  Handle insert/delete character.
@@ -605,40 +647,7 @@ screen_line(
            redraw_this = FALSE;
        // Check if the character is occluded by a popup.
        if (redraw_this && skip_for_popup(row, col + coloff))
-       {
-#ifdef FEAT_PROP_POPUP
-           // For transparent popup cells, update the background character
-           // so it shows through the popup.
-           if (screen_opacity_popup && screen_opacity_popup->w_popup_blend > 0)
-           {
-               ScreenLines[off_to] = ScreenLines[off_from];
-               ScreenAttrs[off_to] = ScreenAttrs[off_from];
-               if (enc_utf8)
-               {
-                   ScreenLinesUC[off_to] = ScreenLinesUC[off_from];
-                   if (ScreenLinesUC[off_from] != 0)
-                   {
-                       for (int i = 0; i < Screen_mco; ++i)
-                       ScreenLinesC[i][off_to] = ScreenLinesC[i][off_from];
-                   }
-               }
-               if (char_cells == 2)
-               {
-                   ScreenLines[off_to + 1] = ScreenLines[off_from + 1];
-                   ScreenAttrs[off_to + 1] = ScreenAttrs[off_from];
-               }
-               if (enc_dbcs == DBCS_JPNU)
-                   ScreenLines2[off_to] = ScreenLines2[off_from];
-
-               if (enc_dbcs != 0 && char_cells == 2)
-                   screen_char_2(off_to, row, col + coloff);
-               else
-                   screen_char(off_to, row, col + coloff);
-           }
-           else
-#endif
-               redraw_this = FALSE;
-       }
+           redraw_this = FALSE;
 
 #ifdef FEAT_PROP_POPUP
        // For popup with opacity windows: if drawing a space, show the
@@ -689,7 +698,10 @@ screen_line(
                    ScreenLines[off_to] = ' ';
                    if (enc_utf8)
                        ScreenLinesUC[off_to] = 0;
-                   ScreenAttrs[off_to] = hl_blend_attr(ScreenAttrs[off_to],
+                   int base_attr = ScreenAttrs[off_to];
+                   popup_get_base_screen_cell(row, col + coloff,
+                                               NULL, &base_attr, NULL);
+                   ScreenAttrs[off_to] = hl_blend_attr(base_attr,
                                                    combined, blend, TRUE);
                    screen_char(off_to, row, col + coloff);
                    opacity_blank = TRUE;
@@ -715,12 +727,26 @@ screen_line(
            int popup_attr = get_win_attr(screen_opacity_popup);
            int combined = hl_combine_attr(popup_attr, char_attr);
            int blend = screen_opacity_popup->w_popup_blend;
-           ScreenAttrs[off_to] = hl_blend_attr(ScreenAttrs[off_to],
-                                               combined, blend, TRUE);
+           int base_attr = ScreenAttrs[off_to];
+           popup_get_base_screen_cell(row, col + coloff,
+                                           NULL, &base_attr, NULL);
+           ScreenAttrs[off_to] = hl_blend_attr(base_attr,
+                                           combined, blend, TRUE);
            screen_char(off_to, row, col + coloff);
-           // For wide background character, also update the second cell.
+           // For wide background character, also update the second cell
+           // with its own base attr (it may be outside the popup area).
            if (bg_char_cells == 2)
-               ScreenAttrs[off_to + 1] = ScreenAttrs[off_to];
+           {
+               int base_attr2 = ScreenAttrs[off_to + 1];
+               popup_get_base_screen_cell(row, col + coloff + 1,
+                                               NULL, &base_attr2, NULL);
+               ScreenAttrs[off_to + 1] = hl_blend_attr(base_attr2,
+                                               combined, blend, TRUE);
+               if (blend == 100)
+                   resolve_wide_char_opacity_attrs(row,
+                           col + coloff, col + coloff + 1,
+                           &ScreenAttrs[off_to], &ScreenAttrs[off_to + 1]);
+           }
            redraw_this = FALSE;
        }
        // When a popup space overlaps the second half of a destroyed wide
@@ -741,8 +767,11 @@ screen_line(
            int combined = hl_combine_attr(popup_attr, char_attr);
            int blend = screen_opacity_popup->w_popup_blend;
            ScreenLines[off_to] = ' ';
-           ScreenAttrs[off_to] = hl_blend_attr(ScreenAttrs[off_to],
-                                               combined, blend, TRUE);
+           int base_attr = ScreenAttrs[off_to];
+           popup_get_base_screen_cell(row, col + coloff,
+                                           NULL, &base_attr, NULL);
+           ScreenAttrs[off_to] = hl_blend_attr(base_attr,
+                                           combined, blend, TRUE);
            screen_char(off_to, row, col + coloff);
            opacity_blank = TRUE;
            redraw_this = FALSE;
@@ -886,9 +915,10 @@ skip_opacity:
            if (gui.in_use && changed_this)
                redraw_next = TRUE;
 #endif
+
            ScreenAttrs[off_to] = ScreenAttrs[off_from];
 #ifdef FEAT_PROP_POPUP
-           // For popup with opacity text: blend background with default (0)
+           // For popup with opacity text: blend background with underlying.
            if (screen_opacity_popup != NULL
                    && (flags & SLF_POPUP)
                    && screen_opacity_popup->w_popup_blend > 0)
@@ -896,14 +926,35 @@ skip_opacity:
                int char_attr = ScreenAttrs[off_from];
                int popup_attr = get_win_attr(screen_opacity_popup);
                int blend = screen_opacity_popup->w_popup_blend;
-               // Combine popup window color with the character's own
-               // attribute (e.g. syntax highlighting) so that the
-               // character's foreground color is preserved.
                int combined = hl_combine_attr(popup_attr, char_attr);
-               ScreenAttrs[off_to] = hl_blend_attr(0, combined, blend, FALSE);
+               int underlying_attr = 0;
+
+               popup_get_base_screen_cell(row, col + coloff,
+                                               NULL, &underlying_attr, NULL);
+               ScreenAttrs[off_to] = hl_blend_attr(underlying_attr,
+                                               combined, blend, FALSE);
+
+               // For double-wide characters, the second cell may have a
+               // different underlying attr (e.g. at popup boundary),
+               // so blend it independently.
+               if (char_cells == 2)
+               {
+                   int underlying_attr2 = 0;
+
+                   popup_get_base_screen_cell(row, col + coloff + 1,
+                                               NULL, &underlying_attr2, NULL);
+                   ScreenAttrs[off_to + 1] = hl_blend_attr(
+                                       underlying_attr2, combined, blend,
+                                       FALSE);
+                   if (blend == 100)
+                       resolve_wide_char_opacity_attrs(row,
+                               col + coloff, col + coloff + 1,
+                               &ScreenAttrs[off_to],
+                               &ScreenAttrs[off_to + 1]);
+               }
            }
+           else
 #endif
-
            // For simplicity set the attributes of second half of a
            // double-wide character equal to the first half.
            if (char_cells == 2)
diff --git a/src/testdir/dumps/Test_popupwin_opacity_zero_01.dump 
b/src/testdir/dumps/Test_popupwin_opacity_zero_01.dump
new file mode 100644
index 000000000..17aa447f8
--- /dev/null
+++ b/src/testdir/dumps/Test_popupwin_opacity_zero_01.dump
@@ -0,0 +1,10 @@
+>b+0&#ffffff0|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|b|l|u|e|n|p|o|p|u|p|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|r|e|d| |p|o|p|u|p|h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+@57|1|,|1| @10|T|o|p| 
diff --git a/src/testdir/dumps/Test_popupwin_opacity_zero_02.dump 
b/src/testdir/dumps/Test_popupwin_opacity_zero_02.dump
new file mode 100644
index 000000000..03b9f4a59
--- /dev/null
+++ b/src/testdir/dumps/Test_popupwin_opacity_zero_02.dump
@@ -0,0 +1,10 @@
+>b+0&#ffffff0|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|b|l|u|e|n|p|o|p|u|p|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|r|e|d| |p|o|p|u|p|h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|b|a|c|k|g|r|o|u|n|d| |t|e|x|t| |h|e|r|e| @54
+|0| @55|1|,|1| @10|T|o|p| 
diff --git a/src/testdir/test_popupwin.vim b/src/testdir/test_popupwin.vim
index a2626a637..41cd6f0d0 100644
--- a/src/testdir/test_popupwin.vim
+++ b/src/testdir/test_popupwin.vim
@@ -4835,6 +4835,42 @@ func Test_popup_opacity_100_blocks_background()
   call StopVimInTerminal(buf)
 endfunc
 
+func Test_popup_opacity_zero()
+  CheckScreendump
+
+  let lines =<< trim END
+    call setline(1, repeat(['background text here'], 10))
+    hi BluePopup guibg=darkblue guifg=white
+    hi RedPopup guibg=darkred guifg=white
+
+    " Blue popup with opacity=50 (partially transparent)
+    call popup_create('blue popup', {
+        \ 'line': 3, 'col': 5,
+        \ 'highlight': 'BluePopup',
+        \ 'opacity': 50,
+        \ 'zindex': 1,
+        \ })
+
+    " Red popup with opacity=0 (fully transparent), overlapping the blue one
+    let g:pop_id = popup_create('red popup', {
+        \ 'line': 4, 'col': 8,
+        \ 'highlight': 'RedPopup',
+        \ 'opacity': 0,
+        \ 'zindex': 2,
+        \ })
+  END
+
+  call writefile(lines, 'XtestPopupOpacityZero', 'D')
+  let buf = RunVimInTerminal('-S XtestPopupOpacityZero', #{rows: 10})
+  call VerifyScreenDump(buf, 'Test_popupwin_opacity_zero_01', {})
+
+  call TermWait(buf, 50)
+  call term_sendkeys(buf, ":echo popup_getoptions(g:pop_id)['opacity']\<CR>")
+  call VerifyScreenDump(buf, 'Test_popupwin_opacity_zero_02', {})
+
+  call StopVimInTerminal(buf)
+endfunc
+
 func Test_popup_getwininfo_tabnr()
   tab split
   let winid1 = popup_create('sup', #{tabpage: 1})
diff --git a/src/version.c b/src/version.c
index 1526bc451..29f46f381 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    129,
 /**/
     128,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1vzfpj-00EpoZ-Tc%40256bit.org.

Raspunde prin e-mail lui