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 <mattn.jp@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Yasuhiro Matsumoto
2026-03-09 18:47:10 +00:00
committed by Christian Brabandt
parent 1f1b86ac60
commit 433bcf3bec
7 changed files with 229 additions and 77 deletions
+20 -13
View File
@@ -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
}
+52 -16
View File
@@ -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
+99 -48
View File
@@ -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);
}
#endif
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)
@@ -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|
@@ -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|
+36
View File
@@ -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})
+2
View File
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
129,
/**/
128,
/**/