patch 9.2.0492: popup: decoration wrongly drawn with clipping on border
Commit:
https://github.com/vim/vim/commit/3db4c3a20bd5e19320291dce11053e9e85994ebf
Author: Yasuhiro Matsumoto <
matt...@gmail.com>
Date: Sun May 17 09:27:04 2026 +0000
patch 9.2.0492: popup: decoration wrongly drawn with clipping on border
Problem: popup: clipwindow popups with border and padding could still
spill into the surrounding chrome of the host window
Solution: Consume the border first, then the padding, per edge; spill
any leftover clip into the opposite edge's decoration; derive
the bottom padding row from total_height; skip the scrollbar
branch for clipwindow popups (Yasuhiro Matsumoto).
closes: #20227
Signed-off-by: Yasuhiro Matsumoto <
matt...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/popupwin.c b/src/popupwin.c
index 772148f61..f122d7a0d 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -1356,15 +1356,15 @@ popup_get_clipwin(win_T *wp)
// clip_*_content : how many *content* rows/cols are clipped at each edge
// (border/padding is consumed first; the rest comes off
// w_height/w_width). >= 0.
-// eff_*_extra : 0 when that edge is clipped (border+padding gone),
-// otherwise the original *_extra.
// eff_border[],
// eff_padding[] : per-edge border/padding sizes (indexed [top,right,bot,left]
-// matching wp->w_popup_border / wp->w_popup_padding). At a
-// clipped edge they collapse to 0; elsewhere they keep the
-// original size. Drawing code can replace
+// matching wp->w_popup_border / wp->w_popup_padding). The
+// clip consumes the border first, then the padding, so when
+// only the border is clipped the padding still survives.
+// Drawing code can replace
// `wp->w_popup_border[N] > 0 && wp->w_popup_*clip == 0`
// with a single `cl.eff_border[N] > 0` test.
+// eff_*_extra : eff_border + eff_padding at that edge (visible decoration).
// eff_height : drawn extent = eff_top_extra + visible content + eff_bot_extra.
// eff_width : drawn extent = eff_left_extra + visible content + eff_right_extra
// (does NOT include w_leftcol or scrollbar; see callers).
@@ -1414,21 +1414,95 @@ popup_compute_clip(win_T *wp, popup_clip_T *cl)
if (cl->clip_right_content < 0)
cl->clip_right_content = 0;
- cl->eff_top_extra = wp->w_popup_topoff > 0 ? 0 : cl->top_extra;
- cl->eff_bot_extra = wp->w_popup_bottomoff > 0 ? 0 : cl->bot_extra;
- cl->eff_left_extra = wp->w_popup_leftclip > 0 ? 0 : cl->left_extra;
- cl->eff_right_extra = wp->w_popup_rightclip > 0 ? 0 : cl->right_extra;
+ // Border is consumed before padding: when only the border row/column is
+ // clipped, the adjacent padding row/column is still visible. Horizontal
+ // edges keep the previous all-or-nothing behaviour for now; the drawing
+ // code there still uses original w_popup_border / w_popup_padding offsets.
+ {
+ int clip = wp->w_popup_topoff;
+ int b = wp->w_popup_border[0];
+ int p = wp->w_popup_padding[0];
+ int rem;
- cl->eff_border[0] = wp->w_popup_topoff > 0 ? 0 : wp->w_popup_border[0];
+ if (clip >= b)
+ {
+ cl->eff_border[0] = 0;
+ rem = clip - b;
+ cl->eff_padding[0] = (rem >= p) ? 0 : p - rem;
+ }
+ else
+ {
+ cl->eff_border[0] = b;
+ cl->eff_padding[0] = p;
+ }
+ }
+ {
+ int clip = wp->w_popup_bottomoff;
+ int b = wp->w_popup_border[2];
+ int p = wp->w_popup_padding[2];
+ int rem;
+
+ if (clip >= b)
+ {
+ cl->eff_border[2] = 0;
+ rem = clip - b;
+ cl->eff_padding[2] = (rem >= p) ? 0 : p - rem;
+ }
+ else
+ {
+ cl->eff_border[2] = b;
+ cl->eff_padding[2] = p;
+ }
+ }
cl->eff_border[1] = wp->w_popup_rightclip > 0 ? 0 : wp->w_popup_border[1];
- cl->eff_border[2] = wp->w_popup_bottomoff > 0 ? 0 : wp->w_popup_border[2];
cl->eff_border[3] = wp->w_popup_leftclip > 0 ? 0 : wp->w_popup_border[3];
-
- cl->eff_padding[0] = wp->w_popup_topoff > 0 ? 0 : wp->w_popup_padding[0];
cl->eff_padding[1] = wp->w_popup_rightclip > 0 ? 0 : wp->w_popup_padding[1];
- cl->eff_padding[2] = wp->w_popup_bottomoff > 0 ? 0 : wp->w_popup_padding[2];
cl->eff_padding[3] = wp->w_popup_leftclip > 0 ? 0 : wp->w_popup_padding[3];
+ // When a clip on one edge runs past the content rows, the excess must
+ // eat into the OPPOSITE edge's decorations. Otherwise the surviving
+ // padding/border can land outside the host (e.g. a popup whose body is
+ // wholly below the host still drew its top padding onto the status row).
+ {
+ int excess = wp->w_popup_bottomoff - cl->bot_extra - wp->w_height;
+ if (excess > 0)
+ {
+ if (excess >= cl->eff_padding[0])
+ {
+ excess -= cl->eff_padding[0];
+ cl->eff_padding[0] = 0;
+ if (excess >= cl->eff_border[0])
+ cl->eff_border[0] = 0;
+ else
+ cl->eff_border[0] -= excess;
+ }
+ else
+ cl->eff_padding[0] -= excess;
+ }
+ }
+ {
+ int excess = wp->w_popup_topoff - cl->top_extra - wp->w_height;
+ if (excess > 0)
+ {
+ if (excess >= cl->eff_padding[2])
+ {
+ excess -= cl->eff_padding[2];
+ cl->eff_padding[2] = 0;
+ if (excess >= cl->eff_border[2])
+ cl->eff_border[2] = 0;
+ else
+ cl->eff_border[2] -= excess;
+ }
+ else
+ cl->eff_padding[2] -= excess;
+ }
+ }
+
+ cl->eff_top_extra = cl->eff_border[0] + cl->eff_padding[0];
+ cl->eff_bot_extra = cl->eff_border[2] + cl->eff_padding[2];
+ cl->eff_left_extra = wp->w_popup_leftclip > 0 ? 0 : cl->left_extra;
+ cl->eff_right_extra = wp->w_popup_rightclip > 0 ? 0 : cl->right_extra;
+
h = wp->w_height - cl->clip_top_content - cl->clip_bot_content;
if (h < 0)
h = 0;
@@ -2172,10 +2246,14 @@ popup_adjust_position(win_T *wp)
}
if (adjust_height_for_top_aligned && wp->w_want_scrollbar
+ && !(wp->w_popup_flags & POPF_CLIPWINDOW)
&& wp->w_winrow + wp->w_height + extra_height > Rows)
{
// Bottom of the popup goes below the last line, reduce the height and
- // add a scrollbar.
+ // add a scrollbar. For "clipwindow" popups the host-window clip
+ // already truncates the popup to fit inside the host, so we must not
+ // also force a scrollbar here -- that would widen the popup by one
+ // column the moment its decoration crossed the screen edge.
wp->w_height = Rows - wp->w_winrow - extra_height;
#ifdef FEAT_TERMINAL
if (wp->w_buffer->b_term == NULL || term_is_finished(wp->w_buffer))
@@ -6267,7 +6345,7 @@ update_popups(void (*win_update)(win_T *wp))
}
if (top_padding > 0)
{
- row = wp->w_winrow + wp->w_popup_border[0];
+ row = wp->w_winrow + cl.eff_border[0];
if (title_len > 0 && row == wp->w_winrow)
{
// top padding and no border; do not draw over the title
@@ -6432,14 +6510,20 @@ update_popups(void (*win_update)(win_T *wp))
if (cl.eff_padding[2] > 0)
{
- // bottom padding
- row = wp->w_winrow + wp->w_popup_border[0]
- + wp->w_popup_padding[0] + wp->w_height;
+ // bottom padding -- sits right after the visible content rows.
+ // Derive the row from total_height so it always lands inside the
+ // popup's drawn extent, including the corner case where the top
+ // clip consumes more rows than the content itself (so the visible
+ // content height is zero). A formula based on
+ // w_height - clip_top_content - clip_bot_content can go negative
+ // there and would draw the padding above w_winrow.
+ row = wp->w_winrow + total_height
+ - cl.eff_padding[2] - cl.eff_border[2];
if (screen_opacity_popup != NULL && saved_screen.lines != NULL)
- fill_opacity_padding(row, row + wp->w_popup_padding[2],
+ fill_opacity_padding(row, row + cl.eff_padding[2],
padcol, padendcol, &saved_screen);
else
- screen_fill(row, row + wp->w_popup_padding[2],
+ screen_fill(row, row + cl.eff_padding[2],
padcol, padendcol, ' ', ' ', popup_attr);
}
diff --git a/src/version.c b/src/version.c
index 7b05c7c45..501e951ab 100644
--- a/src/version.c
+++ b/src/version.c
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 492,
/**/
491,
/**/