diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 34bc95c8e..ec3c25c6d 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -300,6 +300,7 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m) struct window_pane *wp; struct layout_cell *lc; int y, ly, x, lx; + int old_xoff, old_yoff, old_sx, old_sy; wp = cmd_mouse_pane(m, NULL, &wl); if (wp == NULL) { @@ -321,10 +322,17 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m) ly = m->statusat - 1; if (x != lx || y != ly) { + old_xoff = wp->xoff; + old_yoff = wp->yoff; + old_sx = wp->sx; + old_sy = wp->sy; + lc->g.xoff += x - lx; lc->g.yoff += y - ly; layout_fix_panes(w, NULL); - server_redraw_window(w); + + window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx, + old_sy); server_redraw_window_borders(w); } } diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index 01ddf424a..d2bc36430 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -237,6 +237,7 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, int y, ly, x, lx, sx, sy, new_sx, new_sy; int left, right; int new_xoff, new_yoff, resizes = 0; + int old_xoff, old_yoff, old_sx, old_sy; wp = cmd_mouse_pane(m, NULL, &wl); if (wp == NULL) { @@ -247,6 +248,10 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, lc = wp->layout_cell; sx = wp->sx; sy = wp->sy; + old_xoff = wp->xoff; + old_yoff = wp->yoff; + old_sx = (int)wp->sx; + old_sy = (int)wp->sy; left = wp->xoff - 1; right = wp->xoff + sx; if (window_pane_scrollbar_reserve(wp) && @@ -346,7 +351,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, } if (resizes != 0) { layout_fix_panes(w, NULL); - server_redraw_window(w); + window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx, + old_sy); server_redraw_window_borders(w); } } diff --git a/cmd-split-window.c b/cmd-split-window.c index 8beeac557..7546b313b 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -341,6 +341,7 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m) enum pane_lines lines; u_int sx, sy; int x, y, xoff, yoff, border; + int old_xoff, old_yoff, old_sx, old_sy; if (c->tty.mouse_last_pane == -1) return; @@ -396,8 +397,15 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m) if (sy < PANE_MINIMUM) sy = PANE_MINIMUM; + old_xoff = wp->xoff; + old_yoff = wp->yoff; + old_sx = wp->sx; + old_sy = wp->sy; + layout_set_size(lc, sx, sy, xoff, yoff); layout_fix_panes(w, NULL); - server_redraw_window(w); + + window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx, + old_sy); server_redraw_window_borders(w); } diff --git a/regress/floating-pane-drag-ghost.sh b/regress/floating-pane-drag-ghost.sh new file mode 100755 index 000000000..abc20633d --- /dev/null +++ b/regress/floating-pane-drag-ghost.sh @@ -0,0 +1,108 @@ +#!/bin/sh + +# Regression test for a floating-pane drag bug: cmd_resize_pane_redraw_floating() +# (cmd-resize-pane.c) reported damage for just a dragged floating pane's +# content rectangle, not the one-cell border frame drawn around it (see the +# "floating" case in screen-redraw.c, which draws that frame at +# xoff-1/yoff-1 through xoff+sx/yoff+sy - one cell outside the pane's own +# content area). Damage scoped to only the content area left the frame's +# previous position undrawn as the pane moved, so dragging it left a trail +# of un-erased border frames behind - visible as several "corners" stacked +# up rather than just the pane's current one. +# +# This bug has nothing to do with images - it reproduces with a plain +# floating pane and no image support required. + +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 +} + +# drag STARTCOL STARTROW ENDCOL ENDROW +# +# Write a plain (unmodified) SGR button-1 press, drag update and release at +# 1-based positions to the outer pane holding the inner client - this +# matches the default MouseDown1Border/MouseDrag1Border bindings used to +# move or resize a floating pane by its border. +drag() +{ + scol="$1" + srow="$2" + ecol="$3" + erow="$4" + + seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + +cleanup + +TMP=$(mktemp) +trap "cleanup; rm -f $TMP" 0 1 15 + +$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1 +$TMUX set -g mouse on +$TMUX set -g default-command 'sh -c "sleep 100"' + +FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) || + fail "new-pane -X -Y failed" +FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}') +FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}') +FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}') + +$TMUX2 new-session -d -x 60 -y 20 "$TMUX attach -t inner" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." + +# Sanity check: exactly one floating pane, so exactly one top-left corner, +# before dragging anything. +$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed" +n=$(grep -o '┌' $TMP | wc -l) +[ "$n" -eq 1 ] || fail "sanity: expected 1 corner before drag, found $n" + +# Drag the floating pane by its top border (row FTOP-1, some column within +# its width) down several rows in a few separate steps, then release. A +# single drag() call already does press/motion/release, so call it several +# times in a row to simulate a multi-step real drag. +GRABCOL=$((FLEFT + FWIDTH / 2)) +STARTROW=$FTOP +i=0 +while [ $i -lt 6 ]; do + newrow=$((STARTROW + i + 1)) + drag $((GRABCOL + 1)) $((STARTROW + i)) $((GRABCOL + 1)) $newrow + i=$((i + 1)) +done + +$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed" + +# Exactly one top-left corner should remain - the pane's current position. +# This is expected to fail before the fix: multiple corners (a trail of +# un-erased frames) would remain from the intermediate drag positions. +n=$(grep -o '┌' $TMP | wc -l) +[ "$n" -eq 1 ] || fail "expected exactly 1 corner after drag, found $n (ghost frames left behind)" + +exit 0 diff --git a/regress/floating-pane-drag-scrollbar-strip.sh b/regress/floating-pane-drag-scrollbar-strip.sh new file mode 100755 index 000000000..02c3252c3 --- /dev/null +++ b/regress/floating-pane-drag-scrollbar-strip.sh @@ -0,0 +1,140 @@ +#!/bin/sh + +# Regression test: dragging a floating pane across another pane's ordinary +# content must not redraw that other pane's scrollbar, unless the drag +# actually crosses the scrollbar's own strip. +# +# cmd_resize_pane_redraw_floating() (cmd-resize-pane.c) used to flag +# PANE_REDRAWSCROLLBAR on any pane whose whole *body* intersected the +# floating pane's old or new rectangle, rather than just its narrow +# scrollbar strip - so dragging a floating pane back and forth over an +# ordinary tiled pane's content (never touching its scrollbar) still +# needlessly redrew that pane's scrollbar on every motion step. See +# tmux-image-redraw-known-bugs.md for the full write-up. +# +# This is checked by giving the non-dragged pane a distinctive scrollbar +# colour and counting how many times its SGR code appears in the client's +# raw output while the floating pane is dragged vertically over that pane's +# body, well clear of its scrollbar column: with the fix, it should never +# reappear after the initial draw. + +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 +} + +# drag STARTCOL STARTROW ENDCOL ENDROW +drag() +{ + scol="$1" + srow="$2" + ecol="$3" + erow="$4" + + seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.5 +} + +cleanup + +TMP=$(mktemp) +trap "cleanup; rm -f $TMP" 0 1 15 + +$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1 +$TMUX set -g mouse on || fail "set mouse failed" +$TMUX set -g default-command 'sh -c "sleep 100"' || fail "set default-command failed" +$TMUX set -g pane-scrollbars on || fail "set pane-scrollbars failed" + +$TMUX split-window -h -t inner 'sh -c "sleep 100"' || fail "split-window failed" + +PANES=$($TMUX list-panes -t inner -F '#{pane_id} #{pane_left}') +LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1) +[ -n "$LEFT" ] || fail "could not identify left pane" + +# A distinctive scrollbar colour for the non-dragged (left) pane only. +$TMUX set -p -t "$LEFT" pane-scrollbars-style 'fg=colour201,bg=colour17' || + fail "set pane-scrollbars-style failed" + +ALEFT=$($TMUX display-message -p -t "$LEFT" '#{pane_left}') +ATOP=$($TMUX display-message -p -t "$LEFT" '#{pane_top}') +AWIDTH=$($TMUX display-message -p -t "$LEFT" '#{pane_width}') +AHEIGHT=$($TMUX display-message -p -t "$LEFT" '#{pane_height}') +[ "$AWIDTH" -gt 15 ] || fail "left pane too narrow for this test ($AWIDTH)" + +# A small floating pane placed well inside the left pane's content area, +# clear of its (right-hand) scrollbar column by several columns. +FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 8 -y 5 \ + -X $((ALEFT + 2)) -Y $((ATOP + 2))) || fail "new-pane -X -Y failed" +FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}') +FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}') +FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}') +[ $((FLEFT + FWIDTH + 3)) -lt $((ALEFT + AWIDTH)) ] || + fail "sanity: floating pane too close to the scrollbar column" + +# 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 draws the scrollbars) has already happened, missing it. +$TMUX2 new-session -d -x 60 -y 20 || exit 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." +$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 + +# Sanity check: the distinctive scrollbar colour reaches the client at all. +grep -qa '48;5;201' $TMP || fail "sanity: scrollbar colour never reached the client" +: >$TMP + +# Drag the floating pane straight up and down by its top border, staying at +# a fixed column the whole time - this never crosses the left pane's +# scrollbar strip, only its ordinary content. +GRABCOL=$((FLEFT + FWIDTH / 2)) +row=$FTOP +i=0 +while [ $i -lt 6 ]; do + newrow=$((row + 1)) + drag $GRABCOL $row $GRABCOL $newrow + row=$newrow + i=$((i + 1)) +done +i=0 +while [ $i -lt 6 ]; do + newrow=$((row - 1)) + drag $GRABCOL $row $GRABCOL $newrow + row=$newrow + i=$((i + 1)) +done + +# The scrollbar colour should never reappear - its geometry never changed, +# and the drag never crossed its column. This is expected to fail before +# the fix - see the header comment. +n=$(grep -ac '48;5;201' $TMP) +[ "$n" -eq 0 ] || + fail "left pane's scrollbar was redrawn $n times while dragging over its body only" + +exit 0