mirror of
https://github.com/tmux/tmux.git
synced 2026-09-05 13:10:45 +00:00
image: extend existing placements when a pane grows wider
image_write() only ever 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 revisited that clipping decision, so a pane that was too narrow when an image was displayed stayed clipped forever, even after growing wide enough to fit the rest. Unlike height, which recovers via ordinary scrollback, there is no equivalent "scroll right" - this is the only way the extra width is ever recovered. image_grid_resize_width() finds every placement referenced in a grid's rows, works out how far its spans already reach and its origin column, and extends them (via a new image_extend_row(), factored out of image_write()'s own span-building loop) up to whichever is smaller: the image's own full width or the new pane width. window_pane_resize() calls it after a pane grows wider.
This commit is contained in:
106
image.c
106
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,
|
||||
|
||||
110
regress/image-resize-width-recovery.sh
Executable file
110
regress/image-resize-width-recovery.sh
Executable file
@@ -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
|
||||
1
tmux.h
1
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);
|
||||
|
||||
4
window.c
4
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)
|
||||
|
||||
Reference in New Issue
Block a user