server-client, popup, screen-redraw: fix damage-system regressions

Fixes the four bugs caught by the regression tests added in aed1209c
(popup-drag-status-line.sh, popup-drag-wide-character.sh,
popup-drag-pane-prompt.sh, switch-client-redraw.sh), based on fixes
from Michael K. Darling (github.com/darlingm/tmux, pr5516-regression-
fixes), reviewed and adapted:

- server-client.c: server_client_set_session()'s check for whether the
  client's window actually changed compared old->curw to s->curw, but
  when old == s these read the same, already-updated field, so a
  same-session window switch was never detected. Compare against the
  client's own cached redraw scene instead (redraw_client_has_window(),
  new in screen-redraw.c/tmux.h). Taken from darlingm as-is.

- popup.c: popup_damage() only translated a popup's client-coordinate
  rectangle into window coordinates, so a popup dragged across the
  status line never triggered a status-line redraw once it moved away -
  status_redraw()'s own "skip if content unchanged" optimization
  suppressed it, since only the popup moved, not the status content.
  Now detects overlap with the status line and forces a redraw via the
  existing (previously unused) CLIENT_REDRAWSTATUSALWAYS flag, and
  properly clips the reported rectangle to the pane area for
  status-at-top/bottom/off. Taken from darlingm as-is.

- screen-redraw.c: redraw_draw_damage_rect() clipped a span to a damage
  rectangle's raw geometric edges, which have no idea what's in the
  grid, so a clip edge could land mid-character and tear a wide
  character in half. Added redraw_damage_grow_span_clip(): widen the
  clip by one cell on each edge that isn't already at the span's own
  boundary. Reimplemented simpler than darlingm's version (which walked
  grid cells per span type via a switch and direct grid lookups) -
  since no grid cell is ever wider than two columns, an unconditional
  one-cell margin is always enough to pull a split character back in,
  with no need to inspect grid content at all.

- screen-redraw.c: redraw_draw_damage_rect() also never re-overlaid a
  pane's active in-pane prompt after drawing its underlying content, so
  damage crossing a prompt row erased it until an unrelated redraw
  restored it. Factored the existing full-redraw prompt-building code
  into a shared redraw_make_pane_prompt() helper and added
  redraw_damage_draw_pane_prompt(), which recomposes the prompt over
  the drawn range. Taken from darlingm as-is.

All 9 regression tests in regress/ now pass. redraw_damage_grow_span_clip
was verified independently by disabling it and confirming
popup-drag-wide-character.sh reproduces its original failure.

Co-Authored-By: Michael K. Darling <darlingm@gmail.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Michael Grant
2026-08-25 10:25:29 +01:00
parent aed1209c02
commit 48e33179d4
4 changed files with 137 additions and 29 deletions

46
popup.c
View File

@@ -321,32 +321,50 @@ popup_resize_cb(__unused struct client *c, void *data)
}
/*
* Report damage on the current window for a popup's rectangle, given in raw
* client/tty coordinates - translated into window coordinates by removing
* any top status lines and adding the client's pan/window offset, the same
* Report damage for a popup's rectangle, given in raw client/tty
* coordinates. Status-line cells are outside the window scene - they have no
* corresponding window content and redraw_damage_window() can't reach them -
* so if the popup's rectangle overlaps the status line, force it to redraw
* separately. The rest of the rectangle is clipped to the actual pane area
* (above or below the status line, whichever side it's on) before being
* translated into window coordinates and reported the normal way, the same
* way mouse coordinates are translated elsewhere (e.g. cmd-join-pane.c,
* cmd-split-window.c). redraw_damage_window() safely clips or drops
* anything that ends up out of the window's own bounds (a popup can cover
* the status line, which has no corresponding window content), so this
* does not need to be exact for those edge cases.
* cmd-split-window.c).
*/
static void
popup_damage(struct client *c, u_int px, u_int py, u_int sx, u_int sy)
{
struct window *w;
u_int ox, oy, osx, osy, wy;
int statusat;
u_int ox, oy, osx, osy, lines, top, bottom, y0, y1;
if (c->session == NULL)
return;
w = c->session->curw->window;
tty_window_offset(&c->tty, &ox, &oy, &osx, &osy);
if (status_at_line(c) == 0 && py >= status_line_size(c))
wy = py - status_line_size(c);
else
wy = py;
lines = status_line_size(c);
statusat = status_at_line(c);
if (statusat >= 0 && py < (u_int)statusat + lines &&
py + sy > (u_int)statusat)
c->flags |= (CLIENT_REDRAWSTATUS|CLIENT_REDRAWSTATUSALWAYS);
redraw_damage_window(w, px + ox, wy + oy, sx, sy);
if (statusat == 0) {
top = lines;
bottom = c->tty.sy;
} else if (statusat > 0) {
top = 0;
bottom = statusat;
} else {
top = 0;
bottom = c->tty.sy;
}
y0 = (py > top) ? py : top;
y1 = (py + sy < bottom) ? py + sy : bottom;
if (y0 >= y1)
return;
tty_window_offset(&c->tty, &ox, &oy, &osx, &osy);
redraw_damage_window(w, px + ox, y0 - top + oy, sx, y1 - y0);
}
static void

View File

@@ -1073,6 +1073,13 @@ redraw_free_scene(struct redraw_scene *scene)
free(scene);
}
/* Does a client's cached scene show this window? */
int
redraw_client_has_window(struct client *c, struct window *w)
{
return (c->redraw_scene != NULL && c->redraw_scene->w == w);
}
/* Mark a window's cached redraw scenes as out of date. */
void
redraw_invalidate_scene(struct window *w)
@@ -1758,6 +1765,24 @@ redraw_set_draw_context(struct redraw_draw_ctx *dctx,
dctx->flags |= REDRAW_ISOLATES;
}
/* Build a pane prompt into a one-line screen. */
static void
redraw_make_pane_prompt(struct window_pane *wp, struct screen *screen)
{
struct screen_write_ctx ctx;
struct prompt_draw_data pdd;
screen_init(screen, wp->sx, 1, 0);
screen_write_start(&ctx, screen);
pdd.ctx = &ctx;
pdd.cursor_x = &wp->prompt_cx;
pdd.area_x = 0;
pdd.area_width = wp->sx;
pdd.prompt_line = 0;
prompt_draw(wp->prompt, &pdd);
screen_write_stop(&ctx);
}
/* Draw a pane's prompt over its content. */
static void
redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp)
@@ -1766,8 +1791,6 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp)
struct client *c = scene->c;
struct tty *tty = &c->tty;
struct screen screen;
struct screen_write_ctx ctx;
struct prompt_draw_data pdd;
int ox = scene->ox, oy = scene->oy;
int sx = scene->sx, sy = scene->sy;
int line, cy, px, offset, width, wy;
@@ -1800,16 +1823,7 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp)
if (px + width > sx)
width = sx - px;
screen_init(&screen, wp->sx, 1, 0);
screen_write_start(&ctx, &screen);
pdd.ctx = &ctx;
pdd.cursor_x = &wp->prompt_cx;
pdd.area_x = 0;
pdd.area_width = wp->sx;
pdd.prompt_line = 0;
prompt_draw(wp->prompt, &pdd);
screen_write_stop(&ctx);
redraw_make_pane_prompt(wp, &screen);
tty_draw_line(tty, &screen, 0, offset, width, px, cy, NULL);
screen_free(&screen);
}
@@ -1841,7 +1855,8 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
redraw = status_prompt_redraw(c);
else
redraw = status_redraw(c);
if (!redraw && !REDRAW_IS_ALL(flags)) {
if (!redraw && (~c->flags & CLIENT_REDRAWSTATUSALWAYS) &&
!REDRAW_IS_ALL(flags)) {
flags &= ~REDRAW_STATUS;
if (flags == 0)
return;
@@ -2014,7 +2029,7 @@ redraw_screen(struct client *c)
} else {
if (c->flags & CLIENT_REDRAWBORDERS)
flags |= (REDRAW_PANE_BORDER|REDRAW_PANE_STATUS);
if (c->flags & CLIENT_REDRAWSTATUS)
if (c->flags & (CLIENT_REDRAWSTATUS|CLIENT_REDRAWSTATUSALWAYS))
flags |= (REDRAW_STATUS|REDRAW_PANE_STATUS);
if (c->flags & CLIENT_REDRAWOVERLAY)
flags |= REDRAW_OVERLAY;
@@ -2071,6 +2086,64 @@ redraw_damage_refresh_status(struct redraw_draw_ctx *dctx,
wp->flags |= PANE_NEWSTATUS;
}
/*
* Grow a clipped span range by one cell on either edge that isn't already at
* the span's own boundary. A clip edge that lands mid-character (this is a
* damage rectangle, so its edges are geometric and have no idea what's in
* the grid) may be sitting on the second, padding half of a wide character
* whose first half falls just outside the requested range - growing by one
* cell is always enough to pull the whole character back in, since no grid
* cell is ever wider than two columns, and clamping to the span's own x and
* width keeps this from bleeding into a neighbouring span.
*/
static void
redraw_damage_grow_span_clip(struct redraw_span *span, u_int *xp, u_int *endp)
{
if (*xp > span->x)
(*xp)--;
if (*endp < span->x + span->width)
(*endp)++;
}
/* Recompose a pane's prompt over a damaged section of its display row. */
static void
redraw_damage_draw_pane_prompt(struct redraw_draw_ctx *dctx,
struct redraw_span *span, u_int y, u_int x, u_int n)
{
struct redraw_scene *scene = dctx->scene;
struct window_pane *wp = span->data.p.wp;
struct tty *tty = &scene->c->tty;
struct visible_ranges *r;
struct visible_range *rr;
struct screen screen;
u_int i, px, width, prompt_y;
if (wp->prompt == NULL || wp->sx == 0 || wp->sy == 0)
return;
if (dctx->flags & REDRAW_STATUS_TOP)
prompt_y = 0;
else
prompt_y = wp->sy - 1;
if (span->data.p.py != prompt_y)
return;
redraw_make_pane_prompt(wp, &screen);
r = tty_check_overlay_range(tty, x, y, n);
for (i = 0; i < r->used; i++) {
rr = &r->ranges[i];
if (rr->nx == 0)
continue;
px = span->data.p.px + (rr->px - span->x);
if (px >= screen_size_x(&screen))
continue;
width = rr->nx;
if (width > screen_size_x(&screen) - px)
width = screen_size_x(&screen) - px;
tty_draw_line(tty, &screen, px, 0, width, rr->px, y, NULL);
}
screen_free(&screen);
}
/*
* Compose exactly the cells within a damaged rectangle (already in this
* client's own scene coordinates), rather than a whole pane. For each row
@@ -2114,8 +2187,15 @@ redraw_draw_damage_rect(struct redraw_draw_ctx *dctx, u_int x, u_int y,
redraw_damage_refresh_status(dctx,
span->data.st.wp);
}
redraw_damage_grow_span_clip(span, &clip_x,
&clip_end);
redraw_draw_span(dctx, span, cy, clip_x,
clip_end - clip_x);
if (type == REDRAW_SPAN_PANE) {
redraw_damage_draw_pane_prompt(dctx,
span, cy, clip_x,
clip_end - clip_x);
}
}
}
}

View File

@@ -488,8 +488,17 @@ server_client_set_session(struct client *c, struct session *s)
* just an active-pane change, already handled narrowly by
* window_set_active_pane() and window_redraw_active_switch()
* before this is reached.
*
* old and s may be the same session object, whose curw was
* already updated to the new window before this function was
* called - old->curw and s->curw would then read the same,
* already-current value, so comparing them can never detect
* a same-session window change. Compare against the client's
* own cached scene instead, which only reflects what it has
* actually drawn.
*/
if (old == NULL || old != s || old->curw != s->curw)
if (old == NULL || old != s ||
!redraw_client_has_window(c, s->curw->window))
server_redraw_client(c);
}

1
tmux.h
View File

@@ -3658,6 +3658,7 @@ void redraw_screen(struct client *);
void redraw_pane(struct client *, struct window_pane *);
void redraw_pane_scrollbar(struct client *, struct window_pane *);
void redraw_free_scene(struct redraw_scene *);
int redraw_client_has_window(struct client *, struct window *);
void redraw_invalidate_scene(struct window *);
void redraw_invalidate_all_scenes(void);
void redraw_damage_window(struct window *, u_int, u_int, u_int, u_int);