int leftcol;
int leftoff;
int has_scrollbar;
+ int topoff;
+ int bottomoff;
+ int leftclip;
+ int rightclip;
} popup_layout_T;
static poppos_entry_T poppos_entries[] = {
wp->w_popup_flags &= ~POPF_POSINVERT;
}
+ nr = dict_get_bool(dict, "clipwindow", -1);
+ if (nr != -1)
+ {
+ if (nr)
+ wp->w_popup_flags |= POPF_CLIPWINDOW;
+ else
+ wp->w_popup_flags &= ~POPF_CLIPWINDOW;
+ }
+
nr = dict_get_bool(dict, "resize", -1);
if (nr != -1)
{
+ wp->w_has_scrollbar;
}
+/*
+ * Return the host window used to clip popup "wp" when POPF_CLIPWINDOW is set,
+ * or NULL when no clipping should be applied (option off, or the host window
+ * is no longer valid). The textprop window is used as the host; popups not
+ * anchored to a textprop are not clipped.
+ */
+ static win_T *
+popup_get_clipwin(win_T *wp)
+{
+ if (!(wp->w_popup_flags & POPF_CLIPWINDOW))
+ return NULL;
+ if (win_valid(wp->w_popup_prop_win))
+ return wp->w_popup_prop_win;
+ return NULL;
+}
+
+// Per-popup clip geometry derived from w_popup_{top,bottom}off and
+// w_popup_{left,right}clip. Filled by popup_compute_clip().
+//
+// *_extra : original border+padding at each edge.
+// 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
+// `wp->w_popup_border[N] > 0 && wp->w_popup_*clip == 0`
+// with a single `cl.eff_border[N] > 0` test.
+// 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).
+typedef struct {
+ int top_extra;
+ int bot_extra;
+ int left_extra;
+ int right_extra;
+
+ int clip_top_content;
+ int clip_bot_content;
+ int clip_left_content;
+ int clip_right_content;
+
+ int eff_top_extra;
+ int eff_bot_extra;
+ int eff_left_extra;
+ int eff_right_extra;
+
+ int eff_border[4];
+ int eff_padding[4];
+
+ int eff_height;
+ int eff_width;
+} popup_clip_T;
+
+ static void
+popup_compute_clip(win_T *wp, popup_clip_T *cl)
+{
+ int h, w;
+
+ cl->top_extra = popup_top_extra(wp);
+ cl->bot_extra = wp->w_popup_padding[2] + wp->w_popup_border[2];
+ cl->left_extra = wp->w_popup_border[3] + wp->w_popup_padding[3];
+ cl->right_extra = wp->w_popup_border[1] + wp->w_popup_padding[1];
+
+ cl->clip_top_content = wp->w_popup_topoff - cl->top_extra;
+ if (cl->clip_top_content < 0)
+ cl->clip_top_content = 0;
+ cl->clip_bot_content = wp->w_popup_bottomoff - cl->bot_extra;
+ if (cl->clip_bot_content < 0)
+ cl->clip_bot_content = 0;
+ cl->clip_left_content = wp->w_popup_leftclip - cl->left_extra;
+ if (cl->clip_left_content < 0)
+ cl->clip_left_content = 0;
+ cl->clip_right_content = wp->w_popup_rightclip - cl->right_extra;
+ 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;
+
+ 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;
+ cl->eff_height = cl->eff_top_extra + h + cl->eff_bot_extra;
+
+ w = wp->w_width - cl->clip_left_content - cl->clip_right_content;
+ if (w < 0)
+ w = 0;
+ cl->eff_width = cl->eff_left_extra + w + cl->eff_right_extra;
+}
+
+// Snapshot of the popup window geometry that update_popups() temporarily
+// mutates so that win_update() draws within the host-window clip rectangle.
+// Saved before the clip is applied, restored after win_update() returns so
+// callers continue to see the popup's logical geometry.
+// Field names omit the "w_" prefix to avoid clashing with struct-field
+// macros like w_p_wrap (= w_onebuf_opt.wo_wrap).
+typedef struct {
+ int height;
+ int width;
+ int winrow;
+ int wincol;
+ int leftcol;
+ int p_wrap;
+ linenr_T topline;
+} popup_geom_save_T;
+
+ static void
+popup_geom_save(win_T *wp, popup_geom_save_T *sv)
+{
+ sv->height = wp->w_height;
+ sv->width = wp->w_width;
+ sv->winrow = wp->w_winrow;
+ sv->wincol = wp->w_wincol;
+ sv->leftcol = wp->w_leftcol;
+ sv->p_wrap = wp->w_p_wrap;
+ sv->topline = wp->w_topline;
+}
+
+ static void
+popup_geom_restore(win_T *wp, popup_geom_save_T *sv)
+{
+ wp->w_p_wrap = sv->p_wrap;
+ wp->w_leftcol = sv->leftcol;
+ wp->w_wincol = sv->wincol;
+ wp->w_winrow = sv->winrow;
+ wp->w_topline = sv->topline;
+ wp->w_width = sv->width;
+ wp->w_height = sv->height;
+}
+
+/*
+ * Compute a screen row for a textprop that has scrolled above the host
+ * window's top. textpos2screenpos() cannot return a row above topline, so we
+ * probe at topline to fill the screen_{scol,ccol,ecol} column mapping, then
+ * extrapolate a (possibly-negative) row by counting how many buffer lines lie
+ * between the prop and topline. The popup_topoff clip path then turns the
+ * negative row into a top-clip animation as the prop rolls off the top edge.
+ */
+ static void
+popup_screenpos_above_top(
+ win_T *prop_win,
+ pos_T *pos,
+ linenr_T prop_lnum,
+ int *screen_row,
+ int *screen_scol,
+ int *screen_ccol,
+ int *screen_ecol)
+{
+ pos_T probe = *pos;
+
+ probe.lnum = prop_win->w_topline;
+ textpos2screenpos(prop_win, &probe,
+ screen_row, screen_scol, screen_ccol, screen_ecol);
+ *screen_row = prop_win->w_winrow + 1
+ - (int)(prop_win->w_topline - prop_lnum);
+}
+
+/*
+ * Hide popup "wp" because its anchoring textprop is no longer reachable.
+ * Marks the popup as POPF_HIDDEN (no-op when already hidden) and schedules a
+ * redraw of the host window so any leftover decorations are cleared.
+ */
+ static void
+popup_hide_for_textprop(win_T *wp)
+{
+ if ((wp->w_popup_flags & POPF_HIDDEN) != 0)
+ return;
+ wp->w_popup_flags |= POPF_HIDDEN;
+ if (win_valid(wp->w_popup_prop_win))
+ redraw_win_later(wp->w_popup_prop_win, UPD_SOME_VALID);
+}
+
+/*
+ * For "clipwindow" popups: search the lines above prop_win->w_topline for the
+ * popup's anchoring textprop and report whether one was found. When
+ * "max_reach" is > 0, only the last "max_reach" lines before topline are
+ * scanned; pass 0 to scan all lines from line 1. Returns false when the
+ * popup is not "clipwindow", topline is already at line 1, or no prop matches.
+ */
+ static bool
+popup_find_prop_above_top(
+ win_T *wp,
+ win_T *prop_win,
+ int max_reach,
+ textprop_T *prop,
+ linenr_T *found_lnum)
+{
+ linenr_T first;
+
+ if (!(wp->w_popup_flags & POPF_CLIPWINDOW) || prop_win->w_topline <= 1)
+ return false;
+
+ first = max_reach > 0 ? prop_win->w_topline - max_reach : 1;
+ if (first < 1)
+ first = 1;
+ return find_prop_in_lines(prop_win,
+ wp->w_popup_prop_type, wp->w_popup_prop_id,
+ prop, found_lnum, first, prop_win->w_topline - 1);
+}
+
+/*
+ * Compute and assign w_popup_topoff/bottomoff/leftclip/rightclip from the
+ * host (textprop) window's content rectangle when POPF_CLIPWINDOW is set.
+ * The popup's logical geometry (w_winrow, w_height, w_width) is preserved;
+ * only the *off/clip fields record how much of each edge falls outside.
+ * Returns true when the popup has scrolled completely past one of the host
+ * edges, in which case the caller must hide it.
+ */
+ static bool
+popup_compute_clipwindow_offsets(win_T *wp)
+{
+ win_T *cw = popup_get_clipwin(wp);
+ int extra_h, extra_w;
+ int popup_top, popup_bottom, popup_left, popup_right;
+ int total_h, total_w;
+
+ if (cw == NULL)
+ return false;
+
+ extra_h = popup_top_extra(wp)
+ + wp->w_popup_padding[2] + wp->w_popup_border[2];
+ extra_w = popup_extra_width(wp);
+
+ popup_top = wp->w_winrow;
+ popup_bottom = wp->w_winrow + wp->w_height + extra_h;
+ popup_left = wp->w_wincol;
+ popup_right = wp->w_wincol + wp->w_width + extra_w;
+ total_h = wp->w_height + extra_h;
+ total_w = wp->w_width + extra_w;
+
+ if (popup_top < cw->w_winrow)
+ wp->w_popup_topoff = cw->w_winrow - popup_top;
+ if (popup_bottom > cw->w_winrow + cw->w_height)
+ wp->w_popup_bottomoff = popup_bottom - (cw->w_winrow + cw->w_height);
+ if (popup_left < cw->w_wincol)
+ wp->w_popup_leftclip = cw->w_wincol - popup_left;
+ if (popup_right > cw->w_wincol + cw->w_width)
+ wp->w_popup_rightclip = popup_right - (cw->w_wincol + cw->w_width);
+
+ return wp->w_popup_topoff >= total_h
+ || wp->w_popup_bottomoff >= total_h
+ || wp->w_popup_leftclip >= total_w
+ || wp->w_popup_rightclip >= total_w;
+}
+
+/*
+ * Mutate "wp"'s window geometry so win_update() draws only the rows/columns
+ * that fit within the host-window clip rectangle for "clipwindow" popups.
+ * The caller must save the original geometry with popup_geom_save() before
+ * this call and restore it with popup_geom_restore() after win_update().
+ *
+ * Vertical clip: shrink w_height by the clipped content rows; advance
+ * w_topline and w_winrow when rows are cut off the top so the first visible
+ * content row lands on the host's top edge.
+ *
+ * Horizontal clip: when the right side is clipped, just shrink w_width.
+ * When the left side is clipped, advance w_leftcol so the hidden buffer
+ * columns scroll off and shift w_wincol so the first visible column lands on
+ * the host's left edge. Disable wrap so the transient w_width reduction does
+ * not reflow wrapped lines: the popup's logical width is unchanged, we just
+ * want to truncate cells that fall outside the host at draw time.
+ */
+ static void
+popup_apply_winupdate_clip(win_T *wp, popup_clip_T *cl)
+{
+ if (wp->w_popup_topoff > 0 || wp->w_popup_bottomoff > 0)
+ {
+ wp->w_height -= cl->clip_top_content + cl->clip_bot_content;
+ if (wp->w_height < 0)
+ wp->w_height = 0;
+ if (cl->clip_top_content > 0)
+ {
+ wp->w_topline += cl->clip_top_content;
+ wp->w_winrow += cl->clip_top_content;
+ }
+ }
+ if (wp->w_popup_leftclip > 0 || wp->w_popup_rightclip > 0)
+ {
+ if (cl->clip_left_content > 0 || cl->clip_right_content > 0)
+ wp->w_p_wrap = FALSE;
+ if (cl->clip_right_content > 0)
+ {
+ wp->w_width -= cl->clip_right_content;
+ if (wp->w_width < 0)
+ wp->w_width = 0;
+ }
+ if (cl->clip_left_content > 0)
+ {
+ wp->w_leftcol += cl->clip_left_content;
+ wp->w_wincol += cl->clip_left_content;
+ wp->w_width -= cl->clip_left_content;
+ if (wp->w_width < 0)
+ wp->w_width = 0;
+ }
+ }
+}
+
/*
* Adjust the position and size of the popup to fit on the screen.
*/
wp->w_leftcol = 0;
wp->w_popup_leftoff = 0;
wp->w_popup_rightoff = 0;
+ wp->w_popup_topoff = 0;
+ wp->w_popup_bottomoff = 0;
+ wp->w_popup_leftclip = 0;
+ wp->w_popup_rightclip = 0;
// May need to update the "cursorline" highlighting, which may also change
// "topline"
int screen_ccol;
int screen_ecol;
- // Popup window is positioned relative to a text property.
+ // Popup window is positioned relative to a text property. With
+ // "clipwindow", keep the popup visible while the textprop has just
+ // scrolled above the host's top: extrapolate a negative screen_row
+ // from a prop above topline so the top-clip path can roll the popup
+ // off the top edge. Unhiding is done in check_popup_unhidden().
+ bool prop_above_top = false;
if (!find_visible_prop(prop_win,
wp->w_popup_prop_type, wp->w_popup_prop_id,
&prop, &prop_lnum))
{
- // Text property is no longer visible, hide the popup.
- // Unhiding the popup is done in check_popup_unhidden().
- if ((wp->w_popup_flags & POPF_HIDDEN) == 0)
+ if (popup_find_prop_above_top(wp, prop_win, 0,
+ &prop, &prop_lnum))
+ prop_above_top = true;
+ else
{
- wp->w_popup_flags |= POPF_HIDDEN;
- if (win_valid(wp->w_popup_prop_win))
- redraw_win_later(wp->w_popup_prop_win, UPD_SOME_VALID);
+ popup_hide_for_textprop(wp);
+ return;
}
- return;
}
// Compute the desired position from the position of the text
if (wp->w_popup_pos == POPPOS_TOPLEFT
|| wp->w_popup_pos == POPPOS_BOTLEFT)
pos.col += prop.tp_len - 1;
- textpos2screenpos(prop_win, &pos, &screen_row,
+ if (prop_above_top)
+ popup_screenpos_above_top(prop_win, &pos, prop_lnum, &screen_row,
+ &screen_scol, &screen_ccol, &screen_ecol);
+ else
+ textpos2screenpos(prop_win, &pos, &screen_row,
&screen_scol, &screen_ccol, &screen_ecol);
if (screen_scol == 0)
else if (wp->w_popup_pos == POPPOS_BOTRIGHT
|| wp->w_popup_pos == POPPOS_BOTLEFT)
{
- if ((wp->w_height + extra_height) <= wantline)
- // bottom aligned: may move down
+ if ((wp->w_height + extra_height) <= wantline
+ || (wp->w_popup_flags & POPF_CLIPWINDOW))
+ // bottom aligned: may move down. With "clipwindow" the popup
+ // keeps its natural position even if it overflows the screen,
+ // because the clip logic handles the overflow.
wp->w_winrow = wantline - (wp->w_height + extra_height);
else if (wantline * 2 >= Rows || !(wp->w_popup_flags & POPF_POSINVERT))
{
// make sure w_winrow is valid
if (wp->w_winrow >= Rows)
wp->w_winrow = Rows - 1;
- else if (wp->w_winrow < 0)
+ else if (wp->w_winrow < 0 && !(wp->w_popup_flags & POPF_CLIPWINDOW))
wp->w_winrow = 0;
if (wp->w_wincol + wp->w_width + extra_width
// Same for the bottom edge: shift up so the border/padding/shadow stays
// on screen, and clip the height if the popup is taller than the screen.
- if (wp->w_winrow + wp->w_height + extra_height > Rows)
- wp->w_winrow = Rows - wp->w_height - extra_height;
- if (wp->w_winrow < 0)
- wp->w_winrow = 0;
- if (wp->w_winrow + wp->w_height + extra_height > Rows)
- {
- int avail = Rows - wp->w_winrow - extra_height;
- wp->w_height = avail > 0 ? avail : 0;
+ // For "clipwindow" popups the host-window clip below handles overflow, so
+ // skip these screen-edge clamps -- otherwise a synthesised negative
+ // w_winrow (popup partially above the host's top edge) would be snapped
+ // back to 0 and defeat the top-clip animation.
+ if (!(wp->w_popup_flags & POPF_CLIPWINDOW))
+ {
+ if (wp->w_winrow + wp->w_height + extra_height > Rows)
+ wp->w_winrow = Rows - wp->w_height - extra_height;
+ if (wp->w_winrow < 0)
+ wp->w_winrow = 0;
+ if (wp->w_winrow + wp->w_height + extra_height > Rows)
+ {
+ int avail = Rows - wp->w_winrow - extra_height;
+ wp->w_height = avail > 0 ? avail : 0;
+ }
}
if (wp->w_height != org_layout.height)
win_comp_scroll(wp);
+ // Confine the popup to its host window for "clipwindow". The popup's
+ // logical geometry stays untouched; only w_popup_topoff/bottomoff/
+ // leftclip/rightclip record how many rows/columns of each edge fall
+ // outside the host so the drawing code can skip them. When the popup
+ // has fully scrolled past one of the host edges, hide it instead of
+ // leaving stray decorations behind.
+ if (popup_compute_clipwindow_offsets(wp))
+ {
+ popup_hide_for_textprop(wp);
+ return;
+ }
+
wp->w_popup_last_changedtick = CHANGEDTICK(wp->w_buffer);
if (win_valid(wp->w_popup_prop_win))
{
wp->w_popup_prop_changedtick =
CHANGEDTICK(wp->w_popup_prop_win->w_buffer);
wp->w_popup_prop_topline = wp->w_popup_prop_win->w_topline;
+ wp->w_popup_prop_winrow = wp->w_popup_prop_win->w_winrow;
+ wp->w_popup_prop_wincol = wp->w_popup_prop_win->w_wincol;
+ wp->w_popup_prop_width = wp->w_popup_prop_win->w_width;
+ wp->w_popup_prop_winheight = wp->w_popup_prop_win->w_height;
}
// Need to update popup_mask if the position or size changed.
layout->leftcol = wp->w_leftcol;
layout->leftoff = wp->w_popup_leftoff;
layout->has_scrollbar = wp->w_has_scrollbar;
+ layout->topoff = wp->w_popup_topoff;
+ layout->bottomoff = wp->w_popup_bottomoff;
+ layout->leftclip = wp->w_popup_leftclip;
+ layout->rightclip = wp->w_popup_rightclip;
}
/*
|| layout->leftoff != wp->w_popup_leftoff
|| layout->width != wp->w_width
|| layout->height != wp->w_height
- || layout->has_scrollbar != wp->w_has_scrollbar;
+ || layout->has_scrollbar != wp->w_has_scrollbar
+ || layout->topoff != wp->w_popup_topoff
+ || layout->bottomoff != wp->w_popup_bottomoff
+ || layout->leftclip != wp->w_popup_leftclip
+ || layout->rightclip != wp->w_popup_rightclip;
}
/*
dict_add_number(dict, "resize", (wp->w_popup_flags & POPF_RESIZE) != 0);
dict_add_number(dict, "posinvert",
(wp->w_popup_flags & POPF_POSINVERT) != 0);
+ dict_add_number(dict, "clipwindow",
+ (wp->w_popup_flags & POPF_CLIPWINDOW) != 0);
// Return opacity (0-100) by converting from internal blend value
dict_add_number(dict, "opacity",
(wp->w_popup_flags & POPF_OPACITY) ? 100 - wp->w_popup_blend : 100);
{
textprop_T prop;
linenr_T lnum;
+ bool found = false;
- if ((wp->w_popup_flags & POPF_HIDDEN_FORCE) == 0
- && find_visible_prop(wp->w_popup_prop_win,
- wp->w_popup_prop_type, wp->w_popup_prop_id,
- &prop, &lnum))
+ if ((wp->w_popup_flags & POPF_HIDDEN_FORCE) != 0)
+ return FALSE;
+ if (find_visible_prop(wp->w_popup_prop_win,
+ wp->w_popup_prop_type, wp->w_popup_prop_id,
+ &prop, &lnum))
+ found = true;
+ // The textprop may have scrolled just above the host window's top.
+ // Unhide the popup so popup_adjust_position() can roll it partially
+ // onto the host's top edge via the top-clip path. Limit the search
+ // to the popup's own height so we do not resurrect a popup whose
+ // prop is already further off-screen than the popup can extend.
+ else if (popup_find_prop_above_top(wp, wp->w_popup_prop_win,
+ popup_height(wp), &prop, &lnum))
+ found = true;
+ if (found)
{
wp->w_popup_flags &= ~POPF_HIDDEN;
wp->w_popup_prop_topline = 0; // force repositioning
if (win_valid(wp->w_popup_prop_win)
&& (wp->w_popup_prop_changedtick
!= CHANGEDTICK(wp->w_popup_prop_win->w_buffer)
- || wp->w_popup_prop_topline != wp->w_popup_prop_win->w_topline))
+ || wp->w_popup_prop_topline != wp->w_popup_prop_win->w_topline
+ || wp->w_popup_prop_winrow != wp->w_popup_prop_win->w_winrow
+ || wp->w_popup_prop_wincol != wp->w_popup_prop_win->w_wincol
+ || wp->w_popup_prop_width != wp->w_popup_prop_win->w_width
+ || wp->w_popup_prop_winheight != wp->w_popup_prop_win->w_height))
return TRUE;
// May need to adjust the width if the cursor moved.
}
width = popup_width(wp);
- height = popup_height(wp);
+ // Match the drawn extent computed by update_popups so that cells
+ // outside the clipped popup are not marked as popup-owned and the
+ // background window can draw through them.
+ if (wp->w_popup_topoff > 0 || wp->w_popup_bottomoff > 0)
+ {
+ popup_clip_T cl;
+
+ popup_compute_clip(wp, &cl);
+ height = cl.eff_height;
+ }
+ else
+ height = popup_height(wp);
popup_update_mask(wp, width, height);
// Popup with partial transparency do not block lower layers from
if ((wp->w_popup_flags & POPF_OPACITY) && wp->w_popup_blend > 0)
continue;
- for (line = wp->w_winrow;
- line < wp->w_winrow + height && line < screen_Rows; ++line)
- for (col = wp->w_wincol;
- col < wp->w_wincol + width - wp->w_popup_leftoff
- && col < screen_Columns; ++col)
- if (wp->w_zindex < POPUPMENU_ZINDEX
- && pum_visible()
- && pum_under_menu(line, col, FALSE))
- mask[line * screen_Columns + col] = POPUPMENU_ZINDEX;
- else if (wp->w_popup_mask_cells == NULL
- || !popup_masked(wp, width, height, col, line))
- mask[line * screen_Columns + col] = wp->w_zindex;
+ {
+ int mask_start = wp->w_winrow + wp->w_popup_topoff;
+ int mask_end = mask_start + height;
+ int mask_col_start = wp->w_wincol + wp->w_popup_leftclip;
+ int mask_col_end = wp->w_wincol + width - wp->w_popup_leftoff
+ - wp->w_popup_rightclip;
+
+ for (line = mask_start;
+ line < mask_end && line < screen_Rows; ++line)
+ for (col = mask_col_start;
+ col < mask_col_end && col < screen_Columns; ++col)
+ if (wp->w_zindex < POPUPMENU_ZINDEX
+ && pum_visible()
+ && pum_under_menu(line, col, FALSE))
+ mask[line * screen_Columns + col] = POPUPMENU_ZINDEX;
+ else if (wp->w_popup_mask_cells == NULL
+ || !popup_masked(wp, width, height, col, line))
+ mask[line * screen_Columns + col] = wp->w_zindex;
+ }
}
// Only check which lines are to be updated if not already
{
int title_len = 0;
int title_wincol;
+ popup_clip_T cl;
+
+ // Compute the clip geometry once per iteration; w_popup_*off/clip,
+ // w_height, w_width, w_popup_border and w_popup_padding are stable
+ // for the duration of this iteration (popup_apply_winupdate_clip()
+ // mutates w_height/w_width temporarily but the result is restored
+ // before any code below reads cl again).
+ popup_compute_clip(wp, &cl);
override_success = push_highlight_overrides(wp->w_hl, wp->w_hl_len);
// Draw the popup text, unless it's off screen.
if (wp->w_winrow < screen_Rows && wp->w_wincol < screen_Columns)
{
+ popup_geom_save_T saved;
+
+ popup_geom_save(wp, &saved);
+
// May need to update the "cursorline" highlighting, which may also
// change "topline"
if (wp->w_popup_last_curline != wp->w_cursor.lnum)
popup_highlight_curline(wp);
+ // Clip the buffer's drawn extent to the host window when
+ // "clipwindow" is set. The transient mutations are reverted by
+ // popup_geom_restore() so callers continue to see the popup's
+ // logical geometry via popup_getoptions/popup_getpos.
+ popup_apply_winupdate_clip(wp, &cl);
+
win_update(wp);
+ popup_geom_restore(wp, &saved);
+
// move the cursor into the visible lines, otherwise executing
// commands with win_execute() may cause the text to jump.
if (wp->w_cursor.lnum < wp->w_topline)
wp->w_winrow -= top_off;
wp->w_wincol -= left_extra;
+ // "clipwindow" with top-clip shifts all popup decorations down so the
+ // first visible row of the popup lands at the host window's top edge.
+ // Apply the shift before drawing borders/padding/etc. and restore at
+ // the end of this popup's iteration.
+ wp->w_winrow += wp->w_popup_topoff;
+
// Add offset for border and padding if not done already.
if ((wp->w_flags & WFLAG_WCOL_OFF_ADDED) == 0)
{
wp->w_flags |= WFLAG_WROW_OFF_ADDED;
}
- total_width = popup_width(wp) - wp->w_popup_rightoff;
- total_height = popup_height(wp);
+ // When clipped by "clipwindow", drop the border/padding slot at the
+ // clipped edge that we will not render, so the popup ends exactly on
+ // the last visible content row (no empty trailing side-border row)
+ // and starts on the first visible row when top-clipped. When
+ // unclipped, fall back to the full popup geometry (cl.eff_width
+ // excludes w_leftcol and the scrollbar, which popup_width() folds in).
+ if (wp->w_popup_leftclip > 0 || wp->w_popup_rightclip > 0)
+ total_width = cl.eff_width;
+ else
+ total_width = popup_width(wp) - wp->w_popup_rightoff;
+ if (total_width < 0)
+ total_width = 0;
+ if (wp->w_popup_topoff > 0 || wp->w_popup_bottomoff > 0)
+ total_height = cl.eff_height;
+ else
+ total_height = popup_height(wp);
popup_attr = get_win_attr(wp);
if (wp->w_winrow + total_height > cmdline_row)
wp->w_popup_border[0] > 0 ? border_attr[0] : popup_attr);
}
- wincol = wp->w_wincol - wp->w_popup_leftoff;
- top_padding = wp->w_popup_padding[0];
- if (wp->w_popup_border[0] > 0)
+ wincol = wp->w_wincol - wp->w_popup_leftoff + wp->w_popup_leftclip;
+ top_padding = cl.eff_padding[0];
+ if (cl.eff_border[0] > 0)
{
// top border; do not draw over the title
if (title_len > 0)
{
screen_fill(wp->w_winrow, wp->w_winrow + 1,
wincol < 0 ? 0 : wincol, title_wincol,
- wp->w_popup_border[3] != 0 && wp->w_popup_leftoff == 0
+ cl.eff_border[3] != 0 && wp->w_popup_leftoff == 0
? border_char[4] : border_char[0],
border_char[0], border_attr[0]);
screen_fill(wp->w_winrow, wp->w_winrow + 1,
{
screen_fill(wp->w_winrow, wp->w_winrow + 1,
wincol < 0 ? 0 : wincol, wincol + total_width,
- wp->w_popup_border[3] != 0 && wp->w_popup_leftoff == 0
+ cl.eff_border[3] != 0 && wp->w_popup_leftoff == 0
? border_char[4] : border_char[0],
border_char[0], border_attr[0]);
}
- if (wp->w_popup_border[1] > 0)
+ if (cl.eff_border[1] > 0)
{
buf[mb_char2bytes(border_char[5], buf)] = NUL;
screen_puts(buf, wp->w_winrow,
wincol + total_width - 1, border_attr[1]);
}
}
- else if (wp->w_popup_padding[0] == 0 && popup_top_extra(wp) > 0)
+ else if (cl.eff_padding[0] == 0 && popup_top_extra(wp) > 0
+ && wp->w_popup_topoff == 0)
top_padding = 1;
if (top_padding > 0 || wp->w_popup_padding[2] > 0)
attr_thumb = highlight_attr[HLF_PST];
}
- for (i = wp->w_popup_border[0];
- i < total_height - wp->w_popup_border[2]; ++i)
+ // The side-border loop spans the popup's drawn extent. cl.eff_border
+ // and cl.eff_padding collapse the clipped edges to 0 so the loop
+ // covers the full visible area without leaving an empty trailing row.
+ for (i = cl.eff_border[0]; i < total_height - cl.eff_border[2]; ++i)
{
int pad_left;
// left and right padding only needed next to the body
int do_padding =
- i >= wp->w_popup_border[0] + wp->w_popup_padding[0]
- && i < total_height - wp->w_popup_border[2]
- - wp->w_popup_padding[2];
+ i >= cl.eff_border[0] + cl.eff_padding[0]
+ && i < total_height - cl.eff_border[2] - cl.eff_padding[2];
row = wp->w_winrow + i;
// left border
- if (wp->w_popup_border[3] > 0 && wincol >= 0)
+ if (cl.eff_border[3] > 0 && wincol >= 0)
{
buf[mb_char2bytes(border_char[3], buf)] = NUL;
screen_puts(buf, row, wincol, border_attr[3]);
}
- if (do_padding && wp->w_popup_padding[3] > 0)
+ if (do_padding && cl.eff_padding[3] > 0)
{
int col = wincol + wp->w_popup_border[3];
screen_putchar(' ', row, scroll_col, popup_attr);
}
// right border
- if (wp->w_popup_border[1] > 0)
+ if (cl.eff_border[1] > 0)
{
buf[mb_char2bytes(border_char[1], buf)] = NUL;
screen_puts(buf, row, wincol + total_width - 1, border_attr[1]);
}
// right padding
- if (do_padding && wp->w_popup_padding[1] > 0)
+ if (do_padding && cl.eff_padding[1] > 0)
{
int pad_col_start = wincol + wp->w_popup_border[3]
+ wp->w_popup_padding[3] + wp->w_width + wp->w_leftcol;
}
}
- if (wp->w_popup_padding[2] > 0)
+ if (cl.eff_padding[2] > 0)
{
// bottom padding
row = wp->w_winrow + wp->w_popup_border[0]
padcol, padendcol, ' ', ' ', popup_attr);
}
- if (wp->w_popup_border[2] > 0)
+ if (cl.eff_border[2] > 0)
{
// bottom border
row = wp->w_winrow + total_height - 1;
screen_fill(row, row + 1,
wincol < 0 ? 0 : wincol,
wincol + total_width,
- wp->w_popup_border[3] != 0 && wp->w_popup_leftoff == 0
+ cl.eff_border[3] != 0 && wp->w_popup_leftoff == 0
? border_char[7] : border_char[2],
border_char[2], border_attr[2]);
- if (wp->w_popup_border[1] > 0)
+ if (cl.eff_border[1] > 0)
{
buf[mb_char2bytes(border_char[6], buf)] = NUL;
screen_puts(buf, row, wincol + total_width - 1, border_attr[2]);
}
}
- if (wp->w_popup_shadow)
+ if (wp->w_popup_shadow && wp->w_popup_bottomoff == 0)
{
// bottom shadow
row = wp->w_winrow + total_height;
if (override_success)
pop_highlight_overrides();
+
+ // Undo the topoff shift applied before drawing the borders so the
+ // next iteration sees the popup's logical winrow.
+ wp->w_winrow -= wp->w_popup_topoff;
}
#ifdef FEAT_PROP_POPUP
call prop_type_delete('textprop')
endfunc
+func Test_popup_clipwindow_option()
+ " Default: clipwindow is off.
+ let id = popup_create('TEST', #{})
+ call assert_equal(0, popup_getoptions(id).clipwindow)
+ call popup_close(id)
+
+ " popup_create() honours the option.
+ let id = popup_create('TEST', #{clipwindow: v:true})
+ call assert_equal(1, popup_getoptions(id).clipwindow)
+
+ " popup_setoptions() can toggle it off and on.
+ call popup_setoptions(id, #{clipwindow: v:false})
+ call assert_equal(0, popup_getoptions(id).clipwindow)
+ call popup_setoptions(id, #{clipwindow: v:true})
+ call assert_equal(1, popup_getoptions(id).clipwindow)
+
+ call popup_close(id)
+endfunc
+
+func Test_popup_clipwindow_hide_when_prop_off_screen()
+ " A "clipwindow" popup attached to a textprop should be hidden once the
+ " host window scrolls so the textprop is far enough off-screen that even
+ " the partially-clipped popup would no longer overlap, and unhidden again
+ " when the prop scrolls back into reach.
+ call prop_type_add('clipprop', {})
+ new
+ call setline(1, range(1, 200)->mapnew({_, v -> 'line ' .. v}))
+ call prop_add(5, 1, #{type: 'clipprop', length: 5})
+ let host = win_getid()
+
+ let id = popup_create('attached', #{
+ \ textprop: 'clipprop',
+ \ textpropwin: host,
+ \ line: -1,
+ \ wrap: v:false,
+ \ fixed: v:true,
+ \ clipwindow: v:true,
+ \ })
+ redraw
+ call assert_equal(1, popup_getpos(id).visible)
+
+ " Scroll the host so the prop is far below topline; popup hides.
+ call win_execute(host, 'normal! Gzb')
+ redraw
+ call assert_equal(0, popup_getpos(id).visible)
+
+ " Scroll back so the prop is on the first visible line; popup unhides.
+ call win_execute(host, 'normal! ggzt')
+ redraw
+ call assert_equal(1, popup_getpos(id).visible)
+
+ call popup_close(id)
+ bwipe!
+ call prop_type_delete('clipprop')
+endfunc
+
+func Test_popup_clipwindow_top_clip()
+ CheckScreendump
+
+ let lines =<< trim END
+ vim9script
+ set nowrap
+ :botright new
+ :resize 6
+ setline(1, range(1, 30)->mapnew((_, v) => 'host line ' .. v))
+ prop_type_add('clipprop', {})
+ prop_add(2, 1, {type: 'clipprop', length: 4})
+ popup_create(['popup A', 'popup B', 'popup C'], {
+ textprop: 'clipprop',
+ line: -4,
+ col: 0,
+ border: [],
+ padding: [0, 1, 0, 1],
+ highlight: 'PmenuSel',
+ wrap: false,
+ fixed: true,
+ posinvert: false,
+ clipwindow: true,
+ })
+ END
+ call writefile(lines, 'XtestPopupClipwindowTop', 'D')
+ let buf = RunVimInTerminal('-S XtestPopupClipwindowTop', #{rows: 14, cols: 40})
+ call VerifyScreenDump(buf, 'Test_popup_clipwindow_top_clip', {})
+
+ call StopVimInTerminal(buf)
+endfunc
+
+func Test_popup_clipwindow_bottom_clip()
+ CheckScreendump
+
+ let lines =<< trim END
+ vim9script
+ set nowrap
+ :topleft new
+ :resize 6
+ setline(1, range(1, 30)->mapnew((_, v) => 'host line ' .. v))
+ prop_type_add('clipprop', {})
+ prop_add(2, 1, {type: 'clipprop', length: 4})
+ popup_create(['popup A', 'popup B', 'popup C'], {
+ textprop: 'clipprop',
+ line: 1,
+ col: 0,
+ border: [],
+ padding: [0, 1, 0, 1],
+ highlight: 'PmenuSel',
+ wrap: false,
+ fixed: true,
+ posinvert: false,
+ clipwindow: true,
+ })
+ END
+ call writefile(lines, 'XtestPopupClipwindowBottom', 'D')
+ let buf = RunVimInTerminal('-S XtestPopupClipwindowBottom', #{rows: 14, cols: 40})
+ call VerifyScreenDump(buf, 'Test_popup_clipwindow_bottom_clip', {})
+
+ call StopVimInTerminal(buf)
+endfunc
+
+func Test_popup_clipwindow_left_clip()
+ CheckScreendump
+
+ let lines =<< trim END
+ vim9script
+ set nowrap
+ :vert botright new
+ :vert resize 22
+ set laststatus=0
+ setline(1, repeat(['host content line abcdef'], 20))
+ prop_type_add('clipprop', {})
+ prop_add(5, 6, {type: 'clipprop', length: 4})
+ popup_create(['popup A', 'popup B', 'popup C'], {
+ textprop: 'clipprop',
+ line: 0,
+ col: -10,
+ border: [],
+ padding: [0, 1, 0, 1],
+ highlight: 'PmenuSel',
+ wrap: false,
+ fixed: true,
+ posinvert: false,
+ clipwindow: true,
+ })
+ END
+ call writefile(lines, 'XtestPopupClipwindowLeft', 'D')
+ let buf = RunVimInTerminal('-S XtestPopupClipwindowLeft', #{rows: 14, cols: 50})
+ call VerifyScreenDump(buf, 'Test_popup_clipwindow_left_clip', {})
+
+ call StopVimInTerminal(buf)
+endfunc
+
+func Test_popup_clipwindow_right_clip()
+ CheckScreendump
+
+ let lines =<< trim END
+ vim9script
+ set nowrap
+ :vert topleft new
+ :vert resize 22
+ set laststatus=0
+ setline(1, repeat(['host content line abcdef'], 20))
+ prop_type_add('clipprop', {})
+ prop_add(5, 14, {type: 'clipprop', length: 4})
+ popup_create(['popup A', 'popup B', 'popup C'], {
+ textprop: 'clipprop',
+ line: 0,
+ col: 0,
+ border: [],
+ padding: [0, 1, 0, 1],
+ highlight: 'PmenuSel',
+ wrap: false,
+ fixed: true,
+ posinvert: false,
+ clipwindow: true,
+ })
+ END
+ call writefile(lines, 'XtestPopupClipwindowRight', 'D')
+ let buf = RunVimInTerminal('-S XtestPopupClipwindowRight', #{rows: 14, cols: 50})
+ call VerifyScreenDump(buf, 'Test_popup_clipwindow_right_clip', {})
+
+ call StopVimInTerminal(buf)
+endfunc
+
+func Test_popup_clipwindow_hidden()
+ CheckScreendump
+
+ let lines =<< trim END
+ vim9script
+ set nowrap
+ :topleft new
+ :resize 6
+ setline(1, range(1, 50)->mapnew((_, v) => 'host line ' .. v))
+ prop_type_add('clipprop', {})
+ prop_add(2, 1, {type: 'clipprop', length: 4})
+ popup_create(['popup A', 'popup B', 'popup C'], {
+ textprop: 'clipprop',
+ line: -4,
+ col: 0,
+ border: [],
+ padding: [0, 1, 0, 1],
+ highlight: 'PmenuSel',
+ wrap: false,
+ fixed: true,
+ posinvert: false,
+ clipwindow: true,
+ })
+ # Scroll the host so the textprop is far below topline; the popup is
+ # then hidden because the prop has scrolled out of the host window.
+ win_execute(win_getid(), 'normal! Gzb')
+ END
+ call writefile(lines, 'XtestPopupClipwindowHidden', 'D')
+ let buf = RunVimInTerminal('-S XtestPopupClipwindowHidden', #{rows: 14, cols: 40})
+ call VerifyScreenDump(buf, 'Test_popup_clipwindow_hidden', {})
+
+ call StopVimInTerminal(buf)
+endfunc
+
func Test_popup_prop_not_visible()
CheckScreendump