diff --git a/image.c b/image.c index d3558ea6a..ab45429f0 100644 --- a/image.c +++ b/image.c @@ -1476,6 +1476,112 @@ image_cell_has_alpha(struct image *im, u_int x, u_int y) return (0); } +/* Add spans for one row of a placement between two source columns. */ +static void +image_extend_row(struct image_line *line, struct image_placement *placement, + u_int cx, u_int source_y, u_int old_end, u_int new_end) +{ + struct image *im = placement->image; + u_int x, run; + + for (x = old_end; x < new_end; x += run) { + if (!image_cell_has_alpha(im, x, source_y)) { + run = 1; + continue; + } + for (run = 1; x + run < new_end; run++) { + if (!image_cell_has_alpha(im, x + run, source_y)) + break; + } + image_span_add(line, placement, cx + x, run, x, source_y); + } +} + +/* + * Extend existing image placements to reveal more of their original width + * after a pane has grown wider. + * + * image_write() (below) only creates spans for as much of an image as fit + * in the pane at the time it was placed - the rest of the image's pixels + * are still retained (struct image is immutable and kept for as long as + * any placement references it), but nothing ever revisits that clipping + * decision, so a pane that was too narrow when an image was displayed + * stays clipped forever, even after growing wide enough to fit the rest. + * Unlike height, which recovers via ordinary scrollback (image_write() + * scrolls rather than clips when a placement is taller than the pane), + * there is no equivalent "scroll right" - this is the only way the extra + * width is ever recovered. + * + * For every grid row with image spans, this finds each distinct placement + * referenced there, works out how far its spans already reach (source_x + + * width) and its origin column (a span's x - source_x, which is the same + * for every span of the same placement), and adds spans for any newly + * revealed columns up to whichever is smaller: the image's own full width + * or the new pane width. + */ +void +image_grid_resize_width(struct grid *gd, u_int new_sx) +{ + struct grid_line *gl; + struct image_line *line; + struct image_span *span; + struct image_placement *placement; + struct image_placement *seen[64]; + u_int nseen, i, row, cx, end_x, avail; + u_int source_y; + int found; + + if (gd->images == NULL) + return; + for (row = 0; row < gd->hsize + gd->sy; row++) { + gl = &gd->linedata[row]; + line = gl->images; + if (line == NULL) + continue; + + nseen = 0; + TAILQ_FOREACH(span, &line->spans, line_entry) { + found = 0; + for (i = 0; i < nseen; i++) { + if (seen[i] == span->placement) { + found = 1; + break; + } + } + if (!found && nseen < nitems(seen)) + seen[nseen++] = span->placement; + } + + for (i = 0; i < nseen; i++) { + placement = seen[i]; + + cx = end_x = source_y = 0; + found = 0; + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (span->placement != placement) + continue; + if (!found) { + cx = span->x - span->source_x; + source_y = span->source_y; + found = 1; + } + if (span->source_x + span->sx > end_x) + end_x = span->source_x + span->sx; + } + if (!found || cx >= new_sx) + continue; + + avail = new_sx - cx; + if (avail > placement->image->sx) + avail = placement->image->sx; + if (avail <= end_x) + continue; + image_extend_row(line, placement, cx, source_y, end_x, + avail); + } + } +} + /* Place an image at the cursor using the supplied input semantics. */ static void image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, diff --git a/regress/image-resize-width-recovery.sh b/regress/image-resize-width-recovery.sh new file mode 100755 index 000000000..17c47a32c --- /dev/null +++ b/regress/image-resize-width-recovery.sh @@ -0,0 +1,110 @@ +#!/bin/sh + +# Regression test: an image wider than the pane at the time it was +# displayed must show more of itself once the pane grows wide enough, +# instead of staying clipped to its original width forever. +# +# image_write() (image.c) clips an image's width to whatever fit in the +# pane when it was first displayed, and never revisits that decision - +# unlike height, which recovers naturally through ordinary scrollback, +# there is no "scroll right", so the clipped columns were permanently +# discarded. window_pane_resize() (window.c) now calls +# image_grid_resize_width() after a pane grows wider, which extends each +# existing placement's spans - using the image's own retained, immutable +# pixel data - up to whichever is smaller: the image's full width or the +# new pane width. See tmux-image-redraw-known-bugs.md for the full +# write-up. +# +# This is checked via the SIXEL raster widths reported in the client's raw +# output before and after widening the window. The redraw triggered by the +# resize is damage-based (only the newly-uncovered columns are dirtied), so +# it does not redraw the whole row as one wider raster - it sends the +# already-correct clipped portion's width again untouched, plus a *second*, +# separate raster covering just the newly-added columns. So rather than +# looking for a single wider raster, this checks that the widths seen +# across both redraws, added together, account for the fixture's full +# pixel width - i.e. the previously-clipped remainder actually appeared. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 +} +fail() +{ + echo "$*" >&2 + cleanup + exit 1 +} + +cleanup + +TMP=$(mktemp) +trap "cleanup; rm -f $TMP" 0 1 15 + +FIXTURE=$(pwd)/monkey-2.sixel.txt +FULL=$(grep -oa '"1;1;[0-9]*;[0-9]*' $FIXTURE | head -1 | cut -d';' -f3) +[ -n "$FULL" ] || fail "could not read fixture's own raster width" + +# The fixture is 360px wide - narrow enough that a 12-column pane clips it, +# comfortably below the 360px it would take to show in full. +$TMUX new-session -d -s inner -x 12 -y 40 "cat '$FIXTURE'; exec sh" || exit 1 +sleep 0.5 + +[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0 +$TMUX set -as terminal-features ',*:sixel' || exit 1 + +# Start the outer session with a plain shell, then start capturing before +# triggering the attach - starting the attach as the outer pane's initial +# command would mean pipe-pane only starts after the attach-driven initial +# redraw (which sends the image) has already happened, missing it. +$TMUX2 new-session -d -x 12 -y 40 || exit 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." +# The outer client is just another tmux client receiving raw PTY output +# that happens to contain sixel DCS sequences - left alone, it would +# independently decode and place its own second, differently-sized image +# on top of the inner session's, confounding the raster-width measurement +# below. Disable sixel on the outer client so only the inner session's +# placement exists. +$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed" +$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed" +$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed" +$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed" +sleep 1 + +before=$(grep -oa '"1;1;[0-9]*;' $TMP | grep -o '[0-9]*' | sort -n | tail -1) +[ -n "$before" ] || fail "sanity: image never reached the client" +[ "$before" -lt 360 ] || fail "sanity: image was not clipped by the narrow pane ($before)" +: >$TMP + +# Widen the window well past the image's full width - resize the outer +# client, since the inner session's displayed size follows whatever +# terminal size its attached client actually has. +$TMUX2 resize-window -t "$OUTER" -x 30 -y 40 || fail "resize-window failed" +sleep 1 +$TMUX refresh-client || fail "refresh-client failed" +sleep 1 + +# The widths seen after the resize, added together (deduplicated - the +# same chunk may legitimately be retransmitted), should account for the +# fixture's full pixel width: this is expected to fail before the fix, +# where the newly-uncovered columns are never redrawn at all and only the +# original clipped width ever appears - see the header comment. +after_total=$(grep -oa '"1;1;[0-9]*;' $TMP | grep -o '[0-9]*' | sort -nu | + awk '{s+=$1} END{print s+0}') +[ "$after_total" -gt 0 ] || + fail "no image raster was sent at all after widening the window" +[ "$after_total" -ge $((FULL - 20)) ] || + fail "image stayed clipped at ${before}px after widening the window (widths summed to only ${after_total}px, expected close to ${FULL}px)" + +exit 0 diff --git a/tmux.h b/tmux.h index 532ea112e..16b605027 100644 --- a/tmux.h +++ b/tmux.h @@ -4295,6 +4295,7 @@ void image_grid_duplicate_lines(struct grid *, u_int, struct grid *, u_int, u_int); void image_grid_copy_area(struct grid *, u_int, u_int, struct grid *, u_int, u_int, u_int, u_int); +void image_grid_resize_width(struct grid *, u_int); int image_grid_line_has_images(const struct grid_line *); int image_grid_check_area(struct grid *, u_int, u_int, u_int, u_int); diff --git a/window.c b/window.c index 393446e4e..ba2aec0f9 100644 --- a/window.c +++ b/window.c @@ -1670,6 +1670,10 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) log_debug("%s: %%%u resize %ux%u", __func__, wp->id, sx, sy); screen_resize(&wp->base, sx, sy, wp->base.saved_grid == NULL); +#ifdef ENABLE_IMAGES + if (sx > r->osx) + image_grid_resize_width(wp->base.grid, sx); +#endif wme = TAILQ_FIRST(&wp->modes); if (wme != NULL && wme->mode->resize != NULL)