From 3db4c3a20bd5e19320291dce11053e9e85994ebf Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sun, 17 May 2026 09:27:04 +0000 Subject: [PATCH] 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 Signed-off-by: Christian Brabandt --- src/popupwin.c | 132 ++++++++++++++++++++++++++++++++++++++++--------- src/version.c | 2 + 2 files changed, 110 insertions(+), 24 deletions(-) diff --git a/src/popupwin.c b/src/popupwin.c index 772148f61d..f122d7a0de 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; + // 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; + + 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[3] = wp->w_popup_leftclip > 0 ? 0 : wp->w_popup_border[3]; + cl->eff_padding[1] = wp->w_popup_rightclip > 0 ? 0 : wp->w_popup_padding[1]; + 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; - cl->eff_border[0] = wp->w_popup_topoff > 0 ? 0 : wp->w_popup_border[0]; - 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]; - 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 7b05c7c451..501e951ab2 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, /**/