summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-16 12:18:01 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit2109098400fa37d7b448f232c848a0411c318591 (patch)
tree90955d94035e689eb3c112d3b635be7b00036dff /test
parent556c5b50bbfd4a6aaae192377d7810f610fc7d9e (diff)
downloadpardes-2109098400fa37d7b448f232c848a0411c318591.tar.gz
pardes-2109098400fa37d7b448f232c848a0411c318591.zip
Give macOS scrolling momentum
A swipe that was still moving when the fingers lifted stopped dead. The dial next to it has thrown properly since pardes_rotate_end, and this is that same curve on the scroll accumulator: velocity sampled between events off the monotonic clock, weighted toward the newest sample because a flick is decided by how the hand was moving when it left, and a fling that ramps up from zero at the floor rather than switching on at it. A coast stops at the end of a document rather than spinning its remaining velocity against the edge, which is why spendScroll now reports whether the pane moved and why paneAt is public. Only a step that delivered a press can report an edge — the many steps between two rows cross nothing. Nothing suppresses AppKit's own momentum, and nothing needs to: its momentum events are ordinary pardes_scroll calls, every one of which cancels the coast before spending its travel, so on a real trackpad the system takes the gesture over about a frame after the lift and the tail it ends on is below the floor. This is the path for devices AppKit does not fling for — and the only one a script can reach, since NSEvent phases have no public constructor. momentum.snap asserts both halves, which is why it needs the long scrollback: a flick with no document left proves nothing. A slow swipe is byte-identical after the release; a hard one coasts twenty-four rows further on its own, and a finger back on the pad stops it there. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'test')
-rw-r--r--test/macos-snapshots/momentum.golden186
-rw-r--r--test/macos-snapshots/momentum.snap73
-rw-r--r--test/macos_e2e.swift25
3 files changed, 284 insertions, 0 deletions
diff --git a/test/macos-snapshots/momentum.golden b/test/macos-snapshots/momentum.golden
new file mode 100644
index 00000000..b4471061
--- /dev/null
+++ b/test/macos-snapshots/momentum.golden
@@ -0,0 +1,186 @@
+== snap home grid=100x30 cursor=4,29
+|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill
+| New Tty Find Grep Joincol
+|$ /private/tmp/pardes-macos-e2e/momentum/cwd Tty Save Del Mode Filter Collapse
+| LINE 175
+| LINE 176
+| LINE 177
+| LINE 178
+| LINE 179
+| LINE 180
+| LINE 181
+| LINE 182
+| LINE 183
+| LINE 184
+| LINE 185
+| LINE 186
+| LINE 187
+| LINE 188
+| LINE 189
+| LINE 190
+| LINE 191
+| LINE 192
+| LINE 193
+| LINE 194
+| LINE 195
+| LINE 196
+| LINE 197
+| LINE 198
+| LINE 199
+| LINE 200
+| $
+== snap placed grid=100x30 cursor=0,0
+|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill
+| New Tty Find Grep Joincol
+|$ /private/tmp/pardes-macos-e2e/momentum/cwd Tty Save Del Mode Filter Collapse
+| LINE 174
+| LINE 175
+| LINE 176
+| LINE 177
+| LINE 178
+| LINE 179
+| LINE 180
+| LINE 181
+| LINE 182
+| LINE 183
+| LINE 184
+| LINE 185
+| LINE 186
+| LINE 187
+| LINE 188
+| LINE 189
+| LINE 190
+| LINE 191
+| LINE 192
+| LINE 193
+| LINE 194
+| LINE 195
+| LINE 196
+| LINE 197
+| LINE 198
+| LINE 199
+| LINE 200
+== snap placed_released grid=100x30 cursor=0,0
+|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill
+| New Tty Find Grep Joincol
+|$ /private/tmp/pardes-macos-e2e/momentum/cwd Tty Save Del Mode Filter Collapse
+| LINE 174
+| LINE 175
+| LINE 176
+| LINE 177
+| LINE 178
+| LINE 179
+| LINE 180
+| LINE 181
+| LINE 182
+| LINE 183
+| LINE 184
+| LINE 185
+| LINE 186
+| LINE 187
+| LINE 188
+| LINE 189
+| LINE 190
+| LINE 191
+| LINE 192
+| LINE 193
+| LINE 194
+| LINE 195
+| LINE 196
+| LINE 197
+| LINE 198
+| LINE 199
+| LINE 200
+== snap thrown grid=100x30 cursor=0,0
+|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill
+| New Tty Find Grep Joincol
+|$ /private/tmp/pardes-macos-e2e/momentum/cwd Tty Save Del Mode Filter Collapse
+| LINE 165
+| LINE 166
+| LINE 167
+| LINE 168
+| LINE 169
+| LINE 170
+| LINE 171
+| LINE 172
+| LINE 173
+| LINE 174
+| LINE 175
+| LINE 176
+| LINE 177
+| LINE 178
+| LINE 179
+| LINE 180
+| LINE 181
+| LINE 182
+| LINE 183
+| LINE 184
+| LINE 185
+| LINE 186
+| LINE 187
+| LINE 188
+| LINE 189
+| LINE 190
+| LINE 191
+== snap coasted grid=100x30 cursor=0,0
+|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill
+| New Tty Find Grep Joincol
+|$ /private/tmp/pardes-macos-e2e/momentum/cwd Tty Save Del Mode Filter Collapse
+| LINE 141
+| LINE 142
+| LINE 143
+| LINE 144
+| LINE 145
+| LINE 146
+| LINE 147
+| LINE 148
+| LINE 149
+| LINE 150
+| LINE 151
+| LINE 152
+| LINE 153
+| LINE 154
+| LINE 155
+| LINE 156
+| LINE 157
+| LINE 158
+| LINE 159
+| LINE 160
+| LINE 161
+| LINE 162
+| LINE 163
+| LINE 164
+| LINE 165
+| LINE 166
+| LINE 167
+== snap caught grid=100x30 cursor=0,0
+|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill
+| New Tty Find Grep Joincol
+|$ /private/tmp/pardes-macos-e2e/momentum/cwd Tty Save Del Mode Filter Collapse
+| LINE 141
+| LINE 142
+| LINE 143
+| LINE 144
+| LINE 145
+| LINE 146
+| LINE 147
+| LINE 148
+| LINE 149
+| LINE 150
+| LINE 151
+| LINE 152
+| LINE 153
+| LINE 154
+| LINE 155
+| LINE 156
+| LINE 157
+| LINE 158
+| LINE 159
+| LINE 160
+| LINE 161
+| LINE 162
+| LINE 163
+| LINE 164
+| LINE 165
+| LINE 166
+| LINE 167
diff --git a/test/macos-snapshots/momentum.snap b/test/macos-snapshots/momentum.snap
new file mode 100644
index 00000000..1c578e68
--- /dev/null
+++ b/test/macos-snapshots/momentum.snap
@@ -0,0 +1,73 @@
+# Scroll momentum: a swipe still moving when the fingers lift keeps scrolling,
+# in proportion to how hard it was thrown.
+#
+# The same shape as the dial's fling in rotate.snap, and asserted the same way,
+# because the contract is the same one: momentum ramps up from zero at the
+# floor (scroll_fling_floor in src/macos.zig, 8 rows/second) rather than
+# switching on at it. The `gap_ms` argument is what makes the two cases
+# different — it is a real sleep before the event, so libpardes measures a real
+# speed off its monotonic clock rather than reading back-to-back script lines
+# as an impossibly fast hand.
+#
+# Momentum on a real Apple trackpad comes from AppKit, not from here: its
+# momentum events are ordinary pardes_scroll calls and every one of them
+# cancels the coast. This is the path for the devices AppKit does not fling
+# for, and it is the only one a script can reach at all, since NSEvent phases
+# have no public constructor.
+start 30 100
+wait 8000 Newcol Joincol
+wait 8000 $
+stable 700 20000
+# Something long enough to coast ALONG. A flick that runs out of document has
+# nothing left to prove: at the top of a short scrollback the hardest possible
+# throw and no throw at all leave the same screen. Brace expansion rather than
+# seq, so the hermetic world needs nothing on PATH.
+text printf 'LINE %s\n' {1..200}
+key enter
+wait 10000 LINE 200
+stable 700 10000
+snap home
+
+# ---- PLACED ----
+#
+# Half a row every 100 ms is 5 rows/second, under the floor, so lifting the
+# fingers changes nothing: the swipe itself has moved what it moved and that is
+# where it stops. This is the case that has to hold — a slow deliberate scroll
+# that kept sliding afterwards would be unusable.
+scroll_begin
+scroll -0.5 10 10 0 100
+scroll -0.5 10 10 0 100
+scroll -0.5 10 10 0 100
+stable 700 15000
+snap placed
+scroll_end
+stable 700 15000
+# Byte-identical to `placed`, which is the whole assertion.
+snap placed_released
+
+# ---- THROWN ----
+#
+# The same hand moving six times as far in a twentieth of the time is hundreds
+# of rows a second, so the release keeps the view travelling after the fingers
+# are gone.
+#
+# `snap` and then `scroll_end` with NO `stable` between them, which is not
+# impatience: a release is only a throw if it arrives while the hand is still
+# moving, and a 700 ms wait here would be a hand that stopped — which is
+# exactly what PLACED above already proves.
+scroll_begin
+scroll -3 10 10 0 5
+scroll -3 10 10 0 5
+scroll -3 10 10 0 5
+snap thrown
+scroll_end
+stable 700 15000
+# Further back through the scrollback than `thrown`, and that distance is the
+# coast: nothing but the fling moved the view between these two captures.
+snap coasted
+
+# A finger back on the pad catches it. Begin re-zeros the velocity and the
+# bank, so a second throw cannot inherit the first one's speed.
+scroll_begin
+stable 700 15000
+snap caught
diff --git a/test/macos_e2e.swift b/test/macos_e2e.swift
index fbfa0e3e..43627a7e 100644
--- a/test/macos_e2e.swift
+++ b/test/macos_e2e.swift
@@ -790,6 +790,12 @@ private final class Driver: PardesViewDelegate {
view.drop([path], at: cell)
pump()
+ // `scroll <rows> <col> <row> [cols] [gap_ms]`. The optional gap is a
+ // real sleep BEFORE the event and means what it does for `rotate`:
+ // libpardes measures the release speed off the monotonic clock between
+ // calls, so back-to-back script lines read as an impossibly fast swipe.
+ // Pacing them is the only way a script can say a swipe was SLOW, which
+ // is the half of the momentum contract a fling test cannot show.
case "scroll":
let view = try live()
let rows = try double(args, 0, "scroll rows")
@@ -799,9 +805,28 @@ private final class Driver: PardesViewDelegate {
// other suites' scroll commands do.
var cols = 0.0
if args.count > 3 { cols = try double(args, 3, "scroll cols") }
+ if args.count > 4 {
+ Thread.sleep(forTimeInterval: try double(args, 4, "scroll gap_ms") / 1000)
+ }
view.scroll(rows: CGFloat(rows), cols: CGFloat(cols), at: cell)
pump()
+ // Fingers down and fingers up. Their own commands for the reason
+ // rotate_end is one: the fling is decided by the RELEASE SPEED, and
+ // NSEvent phases have no public constructor, so these entry points are
+ // the only way the momentum path is reachable from a script at all.
+ // The coast is then spent by idle's display clock, one step per
+ // animation frame; ordinary pumps do not move it.
+ case "scroll_begin":
+ let view = try live()
+ view.scrollBegin()
+ pump()
+
+ case "scroll_end":
+ let view = try live()
+ view.scrollEnd()
+ pump()
+
case "wheel":
let view = try live()
let button = try mouseButton(try token(args, 0, "wheel button"))