summaryrefslogtreecommitdiff
path: root/src/macos/Sources
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 /src/macos/Sources
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 'src/macos/Sources')
-rw-r--r--src/macos/Sources/PardesView.swift34
1 files changed, 34 insertions, 0 deletions
diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift
index 5df1fd73..ba3677e7 100644
--- a/src/macos/Sources/PardesView.swift
+++ b/src/macos/Sources/PardesView.swift
@@ -1842,6 +1842,20 @@ final class PardesView: NSView {
fed()
}
+ /// Fingers down and fingers up on a precise scroll. Entry points of their
+ /// own for the same reason rotateEnd is one: NSEvent phases have no public
+ /// constructor, and a fling nothing can synthesize is a fling nothing can
+ /// assert.
+ func scrollBegin() {
+ pardes_scroll_begin()
+ fed()
+ }
+
+ func scrollEnd() {
+ pardes_scroll_end()
+ fed()
+ }
+
func rotate(degrees: CGFloat) {
guard degrees != 0 else { return }
pardes_rotate(Float(degrees))
@@ -2086,12 +2100,32 @@ final class PardesView: NSView {
return
}
if event.hasPreciseScrollingDeltas {
+ // Phases first: a finger back on the pad has to catch a coast before
+ // its own travel is spent, or the first millimetre of the new swipe
+ // would be added to the old fling instead of replacing it.
+ //
+ // Momentum events carry `phase == .none` and a momentumPhase
+ // instead, so neither branch here fires for them — they are just
+ // more travel, and libpardes lets them take the gesture over. That
+ // is deliberate: AppKit's fling is the one the rest of the system
+ // uses, and a second fling of our own underneath it would scroll
+ // everything at double speed. `pardes_scroll_end` exists for the
+ // devices AppKit does NOT fling for.
+ if event.phase == .began { scrollBegin() }
// The core scrolls a cell at a time, so libpardes accumulates the
// sub-cell travel and spends it as wheel presses — which is why the
// cell has to travel with the delta.
scroll(rows: -event.scrollingDeltaY / cellHeight,
cols: -event.scrollingDeltaX / cellWidth,
at: at)
+ switch event.phase {
+ case .ended: scrollEnd()
+ // A gesture the system took away should not be thrown. scrollBegin
+ // is the catching half of the pair — coast stopped, bank and
+ // velocity cleared — which is exactly what a cancellation means.
+ case .cancelled: scrollBegin()
+ default: break
+ }
} else {
// AppKit's sign is the opposite of the DOM's: positive deltaY means the
// content moved down, which is a scroll back through history.