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.