From aa6b52eff9b3950cfd2429345a1edf14bf029dff Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Tue, 25 Aug 2026 12:14:41 +0100 Subject: [PATCH] window: redraw border status after moving floating panes --- regress/image-border-status-wipe.sh | 194 +++++++++++++++++++--------- window.c | 1 + 2 files changed, 131 insertions(+), 64 deletions(-) diff --git a/regress/image-border-status-wipe.sh b/regress/image-border-status-wipe.sh index 17228cacc..bfaeabc6d 100644 --- a/regress/image-border-status-wipe.sh +++ b/regress/image-border-status-wipe.sh @@ -1,86 +1,152 @@ #!/bin/sh -# Regression test for a KNOWN, NOT YET FIXED bug - this test currently -# FAILS, and is expected to keep failing until the general damage-tracking -# redraw work lands (IMAGE-REDRAW-PLAN.md, steps 2-4). Left failing -# deliberately rather than skipped, so `make` in regress/ shows accurate, -# live status of whether this is fixed yet. +# Regression test for a border-status caching bug: window_make_pane_status() +# (window-border.c) gates the physical redraw of a pane's border-status +# title on a logical content diff (grid_compare against a cached copy), not +# on whether the physical screen cells were disturbed by something else in +# the meantime - such as a floating pane's own border, drawn on top of a +# tiled pane's border-status row, sliding across it and then away again. +# See tmux-image-redraw-known-bugs.md ("border-status text cache ignores +# physical damage") for the full write-up. # -# On a client detected as SIXEL-capable, entering copy-mode on a pane and -# moving the cursor can blank a floating pane's border-status title text -# (and can also disturb window-level border/title chrome), without -# redrawing it back in. Root cause: window_make_pane_status() -# (window-border.c) gates the physical redraw of border-status text on a -# logical content diff (grid_compare against a cached copy), not on -# whether the physical screen cells were disturbed by something else in -# the meantime. See tmux-image-redraw-known-bugs.md ("border-status text -# cache ignores physical damage") and IMAGE-REDRAW-DISCUSSION.md for the -# full write-up. +# Reproduction: a tiled pane with pane-border-status on has a floating pane +# dragged, by mouse, from directly over its border-status row to somewhere +# else. The tiled pane's title should reappear once the floating pane has +# moved off it; without the fix it stays blank. # -# A proper damage-tracking compositor gates redraw on "was this region -# touched", not "did the logical content change", so the disturbing erase -# would itself register as damage and force recomposition regardless of -# content diff. Design requirement for that work: border/status/scrollbar -# spans must participate in the same damage-driven composition as pane -# content, or this bug survives the rewrite unchanged. -# -# No mouse is required to reproduce this - entering copy-mode (matching a -# PageUp key binding) and moving the cursor once is enough. +# The drag starts and ends away from row 0 rather than grabbing the +# floating pane's border while it is already sitting on row 0: when a +# floating pane's own border-status row exactly coincides with the tiled +# pane's row 0, mouse hit-testing on that row attributes clicks to the +# tiled pane, not the floating one on top of it (an unrelated tmux quirk, +# not what this test is about). That only matters for the initial press, +# though - once a drag is under way, further motion events go straight to +# the already-bound per-pane callback without re-resolving which pane owns +# the coordinates, so starting the grab on an unambiguous row and dragging +# through row 0 works fine. PATH=/bin:/usr/bin TERM=screen LC_ALL=C.UTF-8 export TERM LC_ALL -CONF=$(mktemp) -cat > "$CONF" </dev/null -$TMUX2 kill-server 2>/dev/null +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 COL ROW [ROW ...] +# +# Write one continuous mouse-1 gesture at column COL: a press at the first +# ROW, a drag update at each subsequent ROW in turn, and a release at the +# last ROW - all 1-based positions sent 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. +# +# This must be one continuous press/drag/.../release gesture, not several +# separate drag() calls chained together: releasing clears the per-pane +# drag callback binding, so a later press has to be re-hit-tested from +# scratch, which only works reliably away from row 0 (see the header +# comment). A drag already under way is not re-hit-tested per motion +# event, so it can safely pass through row 0 as an intermediate waypoint. +drag() +{ + col="$1" + shift + + row="$1" + shift + seq=$(printf '\033[<0;%s;%sM' "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + + while [ $# -gt 1 ]; do + row="$1" + shift + seq=$(printf '\033[<32;%s;%sM' "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.15 + done + + row="$1" + seq=$(printf '\033[<0;%s;%sm' "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + +cleanup TMP=$(mktemp) -trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP $CONF" 0 1 15 +trap "cleanup; rm -f $TMP" 0 1 15 -$TMUX2 new-session -d -x 60 -y 20 "sh -c 'i=0; while [ \$i -lt 30 ]; do printf \"TILE%02d\\n\" \$i; i=\$((i + 1)); done; exec sleep 100'" -$TMUX2 new-pane -x20 -y6 -X8 -Y3 -T FLOATTITLE "sh -c 'printf FLOATCONTENT; exec sleep 100'" -[ "$($TMUX2 display-message -p '#{image_support}')" = 0 ] && exit 0 -$TMUX2 set -as terminal-features ',*:sixel' +$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1 +$TMUX set -g mouse on +$TMUX set -g pane-border-status top +$TMUX set -g default-command 'sh -c "sleep 100"' -$TMUX new-session -d -x 60 -y 20 -$TMUX send -l "$TMUX2 attach" -$TMUX send Enter +BASE=$($TMUX list-panes -F '#{pane_id}' | head -1) +[ -n "$BASE" ] || fail "No base pane." +$TMUX select-pane -t "$BASE" -T TILEDTITLE || fail "set base title failed" + +# Float a pane well clear of row 0 to start - its own top border must not +# coincide with the base pane's border-status row for the initial mouse +# press to unambiguously hit it (see the header comment). -x/-y are the +# pane's outer size including its 2-cell border frame, so request 2 more +# than the desired 14x3 content area (see layout_floating_args_parse()). +FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5 -T FLOATTITLE) || + 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: the floating pane's OWN border row shows its title before -# anything disturbs it. (A bare 'FLOATTITLE' grep on the whole capture is -# not enough - the outer client's title bar separately reflects the pane -# title via an unrelated OSC title escape, regardless of this bug.) -$TMUX capturep -p >$TMP || exit 1 -grep '┌' $TMP | grep -q 'FLOATTITLE' || exit 1 +# Sanity check: the base pane's title is visible now (the float starts +# well clear of row 0). +$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed" +head -1 $TMP | grep -q TILEDTITLE || + fail "base title not visible before the drag - test setup is wrong" -# Enter copy-mode on the (active) floating pane, matching the PageUp -# binding, then move the cursor once, matching the Up binding. -$TMUX2 copy-mode -u -t%1 -sleep 1 -$TMUX2 send-keys -t%1 -X cursor-up -sleep 1 +# Drag the float up by its top border (an unambiguous row - not row 0) so +# it ends up covering row 0, hiding the base pane's title behind its own +# border, then continue the same drag back down again so it ends clear of +# row 0 once more - one continuous gesture the whole way (see drag()'s +# comment for why). +GRABCOL=$((FLEFT + FWIDTH / 2)) +waypoints="$FTOP" +row=$FTOP +while [ "$row" != 1 ]; do + row=$((row - 1)) + waypoints="$waypoints $row" +done +while [ "$row" != "$FTOP" ]; do + row=$((row + 1)) + waypoints="$waypoints $row" +done +drag $((GRABCOL + 1)) $waypoints +drag $((GRABCOL + 1)) 1 $((GRABCOL + 1)) "$FTOP" -$TMUX capturep -p >$TMP || exit 1 +$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed" -# The border frame and its title text should both still be there. This is -# expected to fail today - see the header comment. -BORDERLINE=$(grep '┌' $TMP) -[ -n "$BORDERLINE" ] || exit 1 -echo "$BORDERLINE" | grep -q 'FLOATTITLE' || exit 1 +# The base pane's title should be visible again now the floating pane has +# moved off its border-status row. This is expected to fail before the fix +# - see the header comment. +head -1 $TMP | grep -q TILEDTITLE || + fail "base pane title still blank after floating pane moved away" exit 0 + diff --git a/window.c b/window.c index ba2aec0f9..a8bcc656e 100644 --- a/window.c +++ b/window.c @@ -3016,4 +3016,5 @@ window_pane_redraw_floating(struct window *w, struct window_pane *wp, /* Session status formats may depend on the pane's new geometry. */ server_status_window(w); + server_redraw_window(w); }