patch 9.2.0492: popup: decoration wrongly drawn with clipping on border

Commit: 
https://github.com/vim/vim/commit/3db4c3a20bd5e19320291dce11053e9e85994ebf
Author: Yasuhiro Matsumoto <[email protected]>
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 <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

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,
 /**/

-- 
-- 
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/E1wOfY0-009t2l-7W%40256bit.org.

Raspunde prin e-mail lui