diff options
| author | user <user@clank> | 2026-08-27 15:09:02 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-27 15:09:02 +0200 |
| commit | 2ee881452aa4b19701a071f0333a15265e3b314e (patch) | |
| tree | 82fc4fc20ebcff8c27fbebe170534a9bc9213f6e /tests | |
| parent | macos-remote-build: tolerate notarization ticket propagation lag (diff) | |
| download | slosh-2ee881452aa4b19701a071f0333a15265e3b314e.tar.gz slosh-2ee881452aa4b19701a071f0333a15265e3b314e.tar.xz slosh-2ee881452aa4b19701a071f0333a15265e3b314e.zip | |
selection: drag past an edge scrolls the viewport under it
Text taller than the viewport was not selectable in one gesture: the
drag stopped dead at the pane's border. Two faults, one visible and one
waiting. The visible one: DRAG_SELECT motion subtracted the content
origin in uint16_t, so a pointer above or left of the pane underflowed,
the grid lookup failed, and the selection froze. The waiting one: the
anchor was a pair of *viewport* coordinates re-resolved on every extend,
so the moment anything scrolled mid-drag the anchor would have drifted
with the viewport, re-anchoring to whatever slid under the press.
The anchor is now a tracked grid ref -- the thing lib-vt documents as
being for 'long-lived anchors such as selections' -- so it follows its
cell across scrolling, pruning and reflow, and only the near end of the
selection moves. If a program flooding output prunes the anchored row
out of scrollback entirely, the selection keeps what it had; there is no
cell left to anchor a new answer to.
Motion clamps into the content rect (a pointer past an edge is pointing
at the nearest cell), and holding past the top or bottom edge scrolls on
a clock rather than on motion, because a resting pointer sends no events
and 'keep going' is exactly what resting it there means. app_tick() does
the step, app_next_deadline_ms() wakes the poll loop for it -- the same
mechanism toasts and hover guides already use -- and both front ends
call the tick, so the behaviour exists under the server and the headless
harness alike. Rows per step is how far past the edge the pointer sits:
one row past creeps, further hurries, so the hand already dragging is
also the throttle. The metronome is select_scroll_ms, a knob so the test
can configure the clock down instead of sleeping.
The test drags to one row above the pane and simply holds: the viewport
walks to the top of scrollback (clamping makes the endpoint exact), the
copy starts at a line that was never on screen with the press, and still
ends at the pressed row -- that last check is the tracked anchor's.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_clipboard.py | 49 |
1 files changed, 49 insertions, 0 deletions
diff --git a/tests/test_clipboard.py b/tests/test_clipboard.py index 9d1fab2..ca6a916 100644 --- a/tests/test_clipboard.py +++ b/tests/test_clipboard.py @@ -336,6 +336,54 @@ def test_a_word_in_scrollback_is_the_word_on_screen(): ) +def test_a_drag_held_past_the_edge_scrolls_the_selection(): + """More text than fits the viewport is still selectable with one drag: held + past the pane's top edge, the viewport scrolls under the button on the + `select_scroll_ms` clock, and the far end of the selection stays anchored to + the cell that was pressed -- a tracked grid ref, not a viewport coordinate, + which is what keeps the bottom lines in the copy after the viewport has + left them behind.""" + import tempfile + + g = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False) + g.write("select_scroll_ms 5\n") # the clock is configurable; wait less + g.close() + body = "; ".join("printf 'line%02d\\r\\n'" % i for i in range(1, 31)) + with Session(echoing(body), cols=40, rows=12, config=g.name) as s: + s.until_text("line30") + p = s.pane() + cx, cy, h = p["content_x"], p["content_y"], p["content_h"] + check( + "the first line has already left the viewport", + "line01" not in s.snapshot().screen(), + repr(s.snapshot().pane_line(p, 0)), + ) + + # press on the bottom row, drag one row above the pane, and hold + s.send(rf"\e[<0;{cx + 1};{cy + h}M") + s.send(rf"\e[<32;{cx + 1};{cy}M") + s.settle(400) # the hold: the auto-scroll clock does the walking + s.send(rf"\e[<0;{cx + 1};{cy}m") + s.settle(40) + + check( + "holding there scrolled the viewport to the top", + s.snapshot().pane_line(p, 0).startswith("line01"), + repr(s.snapshot().pane_line(p, 0)), + ) + clip = s.api("clipboard")["text"] or "" + check( + "the copy reaches text that was never on screen together", + clip.startswith("line01"), + repr(clip[:60]), + ) + check( + "and still ends where the press was anchored", + "line30" in clip, + repr(clip[-60:]), + ) + + def test_a_mouse_program_keeps_its_double_clicks(): """The same rule the drag has: a program that asked for the mouse gets the clicks, and shift is the way to select over one anyway. A word selection that @@ -560,5 +608,6 @@ if __name__ == "__main__": test_slow_clicks_are_two_clicks() test_the_separators_are_configurable() test_a_word_in_scrollback_is_the_word_on_screen() + test_a_drag_held_past_the_edge_scrolls_the_selection() test_a_mouse_program_keeps_its_double_clicks() sys.exit(report()) |
