patch 9.2.0469: popup: textprop-anchored popups bleed past host window edges

Problem:  A popup anchored to a text property in a split window is
          positioned relative to the screen and may extend into
          adjacent splits or off-screen regions.  There is no way to
          confine the popup to the window that contains the textprop.
Solution: Add the "clipwindow" popup option to allow clipping the text
          property popup to the host window (Yasuhiro Matsumoto).

Adds a "clipwindow" boolean option to popup_create()/popup_setoptions().
When set on a textprop-anchored popup, the popup's drawn extent is
confined to its host (textprop) window's content rectangle so the popup
no longer bleeds across a horizontal split's statusline (top/bottom) or
a vsplit's separator (right) into another window.

The popup keeps its full logical size and position; only the rows or
columns that fall outside the host window's content area are skipped
during drawing, so a popup that scrolls toward the host's edge looks
visually "cut off" without its borders being relocated.  popup_getoptions
and popup_getpos continue to report the unclipped geometry.

Implementation:

  - w_popup_topoff / w_popup_bottomoff record how many rows of the
    popup fall outside the host on each side.  popup_adjust_position()
    computes them from the host rectangle after the logical layout is
    finalised, and update_popups() and the popup-mask builder subtract
    them when emitting cells/borders/scrollbar and when marking
    popup-owned cells.  win_update() is bracketed by transient
    w_height/w_topline/w_winrow adjustments so the buffer's drawn
    content matches the visible row range.

  - w_popup_rightclip is the horizontal counterpart for the host's
    right edge: the right border, padding and content columns past
    the host are not drawn.  win_update() is bracketed by a transient
    w_width reduction so the buffer text is not written past the
    host's right edge either.

  - When the textprop scrolls just above the host window's top, the
    popup is kept visible by extending the prop search above topline
    (new helper find_prop_in_lines) and synthesising a negative
    screen_row so the top-clip path can roll the popup off the top.
    When the textprop has scrolled far enough that even the bottom
    border would overlap the host edge -- or when the popup would
    overflow the host's left edge at all -- the popup is hidden, and
    unhidden again once it comes back within range.

  - The "reduce-height" / "clamp winrow to 0" fallbacks in
    popup_adjust_position are bypassed for host-clipped popups so the
    popup keeps its natural anchored position instead of being
    snapped to the screen edge.

    Left-edge partial clipping is intentionally not supported: it
    would require shrinking the buffer width during win_update, which
    reflows wrapped lines and corrupts the displayed content; the
    popup is hidden instead.

closes: #20166

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-05-10 18:53:57 +00:00
committed by Christian Brabandt
parent ef1ecc3b61
commit e3d9929109
15 changed files with 891 additions and 72 deletions
+35 -1
View File
@@ -1,4 +1,4 @@
*popup.txt* For Vim version 9.2. Last change: 2026 May 01
*popup.txt* For Vim version 9.2. Last change: 2026 May 10
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -712,6 +712,15 @@ The second argument of |popup_create()| is a dictionary with options:
when "textprop" is present.
textpropid Used to identify the text property when "textprop" is
present. Use zero to reset.
clipwindow Only used when "textprop" is set. When TRUE the popup
is kept within the window containing the text
property: if the text property scrolls past that
window's top, bottom, left or right edge, the popup
is clipped at that edge instead of being drawn
outside it. Once the text property has scrolled out
of the window the popup is hidden.
Default FALSE.
See |popup-clipwindow|.
fixed When FALSE (the default), and:
- "pos" is "botleft" or "topleft", and
- the popup would be truncated at the right edge of
@@ -949,6 +958,31 @@ If the window for which the popup was defined is closed, the popup is closed.
If the popup cannot fit in the desired position, it may show at a nearby
position.
CLIP TEXTPROP POPUP TO HOST WINDOW *popup-clipwindow*
When the popup is anchored to a text property in a split window, the popup is
by default drawn relative to the whole screen and may extend past the edges of
the window that contains the text property (the "host window"). Setting
"clipwindow" to TRUE keeps the popup within window's content area:
parts of the popup that fall outside the window are clipped, and the popup is
hidden once the text property has scrolled entirely past one of the edges.
Example: a tall popup anchored above the cursor that should never spill into
the window below the split: >
call popup_create(body, #{
\ textprop: 'marker',
\ textpropid: id,
\ pos: 'topleft',
\ line: -1, col: 0,
\ posinvert: v:false,
\ clipwindow: v:true,
\ })
<
With "posinvert" left at its default (TRUE) the popup may be flipped to the
opposite side of the text property when there is no room; set it to FALSE to
keep the requested side and rely on "clipwindow" to clip the overflow.
Some hints:
- To avoid collision with other plugins the text property type name has to be
unique. You can also use the "bufnr" item to make it local to a buffer.
+1
View File
@@ -9791,6 +9791,7 @@ popt-option print.txt /*popt-option*
popup popup.txt /*popup*
popup-buffer popup.txt /*popup-buffer*
popup-callback popup.txt /*popup-callback*
popup-clipwindow popup.txt /*popup-clipwindow*
popup-close popup.txt /*popup-close*
popup-examples popup.txt /*popup-examples*
popup-filter popup.txt /*popup-filter*
+2 -1
View File
@@ -1,4 +1,4 @@
*version9.txt* For Vim version 9.2. Last change: 2026 May 05
*version9.txt* For Vim version 9.2. Last change: 2026 May 10
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52588,6 +52588,7 @@ Popups ~
- 'previewpopup' supports the same values as 'completepopup' (except for
"align").
- Support "opacity" setting for 'completepopup' option.
- Support for clipping textproperty popups |popup-clipwindow|.
Diff mode ~
---------
+516 -61
View File
@@ -70,6 +70,10 @@ typedef struct {
int leftcol;
int leftoff;
int has_scrollbar;
int topoff;
int bottomoff;
int leftclip;
int rightclip;
} popup_layout_T;
static poppos_entry_T poppos_entries[] = {
@@ -838,6 +842,15 @@ apply_general_options(win_T *wp, dict_T *dict)
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)
{
@@ -1320,6 +1333,320 @@ popup_extra_width(win_T *wp)
+ 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.
*/
@@ -1361,6 +1688,10 @@ popup_adjust_position(win_T *wp)
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"
@@ -1378,20 +1709,24 @@ popup_adjust_position(win_T *wp)
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
@@ -1401,7 +1736,11 @@ popup_adjust_position(win_T *wp)
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)
@@ -1771,8 +2110,11 @@ popup_adjust_position(win_T *wp)
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))
{
@@ -1838,7 +2180,7 @@ popup_adjust_position(win_T *wp)
// 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
@@ -1863,25 +2205,48 @@ popup_adjust_position(win_T *wp)
// 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)
// 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))
{
int avail = Rows - wp->w_winrow - extra_height;
wp->w_height = avail > 0 ? avail : 0;
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.
@@ -3554,6 +3919,10 @@ popup_save_layout(win_T *wp, popup_layout_T *layout)
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;
}
/*
@@ -3568,7 +3937,11 @@ popup_layout_changed(win_T *wp, popup_layout_T *layout)
|| 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;
}
/*
@@ -4298,6 +4671,8 @@ f_popup_getoptions(typval_T *argvars, typval_T *rettv)
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);
@@ -4765,11 +5140,23 @@ check_popup_unhidden(win_T *wp)
{
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
@@ -4793,7 +5180,11 @@ popup_need_position_adjust(win_T *wp)
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.
@@ -5086,7 +5477,18 @@ may_update_popup_mask(int type)
}
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
@@ -5095,18 +5497,25 @@ may_update_popup_mask(int type)
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
@@ -5528,6 +5937,14 @@ update_popups(void (*win_update)(win_T *wp))
{
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);
@@ -5613,13 +6030,25 @@ update_popups(void (*win_update)(win_T *wp))
// 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)
@@ -5631,6 +6060,12 @@ update_popups(void (*win_update)(win_T *wp))
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)
{
@@ -5643,8 +6078,22 @@ update_popups(void (*win_update)(win_T *wp))
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)
@@ -5723,16 +6172,16 @@ update_popups(void (*win_update)(win_T *wp))
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,
@@ -5743,18 +6192,19 @@ update_popups(void (*win_update)(win_T *wp))
{
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)
@@ -5844,25 +6294,26 @@ update_popups(void (*win_update)(win_T *wp))
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];
@@ -5899,13 +6350,13 @@ update_popups(void (*win_update)(win_T *wp))
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;
@@ -5932,7 +6383,7 @@ update_popups(void (*win_update)(win_T *wp))
}
}
if (wp->w_popup_padding[2] > 0)
if (cl.eff_padding[2] > 0)
{
// bottom padding
row = wp->w_winrow + wp->w_popup_border[0]
@@ -5945,24 +6396,24 @@ update_popups(void (*win_update)(win_T *wp))
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;
@@ -5996,6 +6447,10 @@ update_popups(void (*win_update)(win_T *wp))
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
+1
View File
@@ -16,6 +16,7 @@ int get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change);
int prop_count_above_below(buf_T *buf, linenr_T lnum);
int count_props(linenr_T lnum, int only_starting, int last_line);
void sort_text_props(buf_T *buf, textprop_T *props, int *idxs, int count);
bool find_prop_in_lines(win_T *wp, int type_id, int id, textprop_T *prop, linenr_T *found_lnum, linenr_T first_lnum, linenr_T last_lnum);
bool find_visible_prop(win_T *wp, int type_id, int id, textprop_T *prop, linenr_T *found_lnum);
char_u *props_add_count_header(char_u *line, int line_len, int textlen, int *new_len);
void add_text_props(linenr_T lnum, textprop_T *text_props, int text_prop_count);
+16
View File
@@ -4197,6 +4197,14 @@ struct window_S
int w_popup_leftoff; // columns left of the screen
int w_popup_rightoff; // columns right of the screen
int w_popup_topoff; // rows above the host window's top
// when "clipwindow" is set
int w_popup_bottomoff; // rows below the host window's bottom
// when "clipwindow" is set
int w_popup_leftclip; // columns left of the host window's left
// when "clipwindow" is set
int w_popup_rightclip; // columns right of the host window's right
// when "clipwindow" is set
varnumber_T w_popup_last_changedtick; // b:changedtick of popup buffer
// when position was computed
varnumber_T w_popup_prop_changedtick; // b:changedtick of buffer with
@@ -4205,6 +4213,14 @@ struct window_S
int w_popup_prop_topline; // w_topline of window with
// w_popup_prop_type when position was
// computed
int w_popup_prop_winrow; // w_winrow of host window when
// position was computed
int w_popup_prop_wincol; // w_wincol of host window when
// position was computed
int w_popup_prop_width; // w_width of host window when
// position was computed
int w_popup_prop_winheight; // w_height of host window when
// position was computed
linenr_T w_popup_last_curline; // last known w_cursor.lnum of window
// with "cursorline" set
callback_T w_close_cb; // popup close callback
+14
View File
@@ -0,0 +1,14 @@
>h+0&#ffffff0|o|s|t| |l|i|n|e| |1| @28
|h|o|s|t| |l|i|n|e| |2| @28
|h|o|s|t| |l|i|n|e| |3| @28
|h|o|s|t|╔+0#0000001#e0e0e08|═@8|╗| +0#0000000#ffffff0@24
|h|o|s|t|║+0#0000001#e0e0e08| |p|o|p|u|p| |A| |║| +0#0000000#ffffff0@24
|h|o|s|t|║+0#0000001#e0e0e08| |p|o|p|u|p| |B| |║| +0#0000000#ffffff0@24
|[+3&&|N|o| |N|a|m|e|]| |[|+|]| @8|1|,|1| @11|T|o|p
| +0&&@39
|~+0#4040ff13&| @38
|~| @38
|~| @38
|~| @38
|[+1#0000000&|N|o| |N|a|m|e|]| @12|0|,|0|-|1| @9|A|l@1
| +0&&@39
+14
View File
@@ -0,0 +1,14 @@
|h+0&#ffffff0|o|s|t| |l|i|n|e| |4|5| @27
|h|o|s|t| |l|i|n|e| |4|6| @27
|h|o|s|t| |l|i|n|e| |4|7| @27
|h|o|s|t| |l|i|n|e| |4|8| @27
|h|o|s|t| |l|i|n|e| |4|9| @27
>h|o|s|t| |l|i|n|e| |5|0| @27
|[+3&&|N|o| |N|a|m|e|]| |[|+|]| @8|5|0|,|1| @10|B|o|t
| +0&&@39
|~+0#4040ff13&| @38
|~| @38
|~| @38
|~| @38
|[+1#0000000&|N|o| |N|a|m|e|]| @12|0|,|0|-|1| @9|A|l@1
| +0&&@39
+14
View File
@@ -0,0 +1,14 @@
| +0&#ffffff0@26||+1&&>h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&|═+0#0000001#e0e0e08@7|╗| +0#0000000#ffffff0|n|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&| +0&&|p+0#0000001#e0e0e08|o|p|u|p| |A|║| |n+0#0000000#ffffff0|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&| +0&&|p+0#0000001#e0e0e08|o|p|u|p| |B|║| |n+0#0000000#ffffff0|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&| +0&&|p+0#0000001#e0e0e08|o|p|u|p| |C|║| |n+0#0000000#ffffff0|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&|═+0#0000001#e0e0e08@7|╝| +0#0000000#ffffff0|n|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d
|~+0#4040ff13&| @25||+1#0000000&|h+0&&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d
| @31|1|,|1| @10|T|o|p|
+14
View File
@@ -0,0 +1,14 @@
>h+0&#ffffff0|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&| +0&&@26
|h|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e|╔+0#0000001#e0e0e08|═@3||+1#0000000#ffffff0|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e|║+0#0000001#e0e0e08| |p|o|p||+1#0000000#ffffff0|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e|║+0#0000001#e0e0e08| |p|o|p||+1#0000000#ffffff0|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e|║+0#0000001#e0e0e08| |p|o|p||+1#0000000#ffffff0|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e|╚+0#0000001#e0e0e08|═@3||+1#0000000#ffffff0|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25
|h+0#0000000&|o|s|t| |c|o|n|t|e|n|t| |l|i|n|e| |a|b|c|d||+1&&|~+0#4040ff13&| @25
| +0#0000000&@31|1|,|1| @10|T|o|p|
+14
View File
@@ -0,0 +1,14 @@
| +0&#ffffff0@39
|~+0#4040ff13&| @38
|~| @38
|~| @38
|~| @38
|[+1#0000000&|N|o| |N|a|m|e|]| @12|0|,|0|-|1| @9|A|l@1
>h+0&&|o|s|t|║+0#0000001#e0e0e08| |p|o|p|u|p| |B| |║| +0#0000000#ffffff0@24
|h|o|s|t|║+0#0000001#e0e0e08| |p|o|p|u|p| |C| |║| +0#0000000#ffffff0@24
|h|o|s|t|╚+0#0000001#e0e0e08|═@8|╝| +0#0000000#ffffff0@24
|h|o|s|t| |l|i|n|e| |4| @28
|h|o|s|t| |l|i|n|e| |5| @28
|h|o|s|t| |l|i|n|e| |6| @28
|[+3&&|N|o| |N|a|m|e|]| |[|+|]| @8|1|,|1| @11|T|o|p
| +0&&@39
+216
View File
@@ -4522,6 +4522,222 @@ func Test_popup_setoptions_other_tab()
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
+31 -9
View File
@@ -1396,25 +1396,28 @@ sort_text_props(
}
/*
* Find text property "type_id" in the visible lines of window "wp".
* Match "id" when it is > 0.
* Returns false when not found.
* Find text property "type_id" in lines [first_lnum, last_lnum] of window
* "wp"'s buffer. Match "id" when it is > 0. Returns false when not found.
*/
bool
find_visible_prop(
find_prop_in_lines(
win_T *wp,
int type_id,
int id,
textprop_T *prop,
linenr_T *found_lnum)
linenr_T *found_lnum,
linenr_T first_lnum,
linenr_T last_lnum)
{
// return when "type_id" no longer exists
if (text_prop_type_by_id(wp->w_buffer, type_id) == NULL)
return false;
// w_botline may not have been updated yet.
validate_botline_win(wp);
for (linenr_T lnum = wp->w_topline; lnum < wp->w_botline; ++lnum)
if (first_lnum < 1)
first_lnum = 1;
if (last_lnum > wp->w_buffer->b_ml.ml_line_count)
last_lnum = wp->w_buffer->b_ml.ml_line_count;
for (linenr_T lnum = first_lnum; lnum <= last_lnum; ++lnum)
{
char_u *props;
int count = get_text_props(wp->w_buffer, lnum, &props, FALSE);
@@ -1432,6 +1435,25 @@ find_visible_prop(
return false;
}
/*
* Find text property "type_id" in the visible lines of window "wp".
* Match "id" when it is > 0.
* Returns false when not found.
*/
bool
find_visible_prop(
win_T *wp,
int type_id,
int id,
textprop_T *prop,
linenr_T *found_lnum)
{
// w_botline may not have been updated yet.
validate_botline_win(wp);
return find_prop_in_lines(wp, type_id, id, prop, found_lnum,
wp->w_topline, wp->w_botline - 1);
}
/*
* Set the text properties for line "lnum" to "tps" array with "count" entries.
* If "count" is zero text properties are removed.
+2
View File
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
469,
/**/
468,
/**/
+1
View File
@@ -690,6 +690,7 @@ extern int (*dyn_libintl_wputenv)(const wchar_t *envstring);
#define POPF_INFO_MENU 0x400 // align info popup with popup menu
#define POPF_POSINVERT 0x800 // vertical position can be inverted
#define POPF_OPACITY 0x1000 // popup has opacity/transparency setting
#define POPF_CLIPWINDOW 0x2000 // confine popup to its host window's rect
// flags used in w_popup_handled
#define POPUP_HANDLED_1 0x01 // used by mouse_find_win()