Skip to content

fix(paginator): re-apply the settled scroll offset when WebKit clamps it - #73

Merged
chrox merged 1 commit into
mainfrom
fix/last-page-scroll-clamp
Aug 13, 2026
Merged

fix(paginator): re-apply the settled scroll offset when WebKit clamps it#73
chrox merged 1 commit into
mainfrom
fix/last-page-scroll-clamp

Conversation

@chrox

@chrox chrox commented Aug 13, 2026

Copy link
Copy Markdown
Member

cssAnimateScroll turns a page by putting transform: translateX(-delta) on every view element, then in its cleanup clears the transforms and immediately assigns the final scroll offset.

Translating the children shrinks the container's scrollable overflow by the animated distance, and WebKit on iOS 18 keeps clamping against that shrunken extent for the rest of the task, so the assignment lands short.

Mid-book the clamp sits far beyond the target and is invisible. On the last page of the book, where the target is exactly the maximum scroll offset, it lands a full page short and the page visibly snaps back, so the final page of long books cannot be reached at all.

Device trace from the failing turn (iPhone XR / iOS 18.5):

next     {"pages":305,"size":414,"rvs":126270,"sw":126270,"cw":414,"max":125856,"tgt":125856}
css-done {"req":125856,"act":125442,"max":125442,"rvs":126270}

max drops by exactly one page (414px) between issuing the turn and applying it, while the summed view widths (rvs) are unchanged.

Forcing a layout first is not enough: the extent it reports is restored, but the scroll clamp still uses the stale one for the rest of the task, measured on device:

clamp-retry {"end":125856,"after":125445,"sw":125859}   <- forced layout, still clamped
clamp-raf   {"end":125856,"after":125856}               <- next frame, lands

So the fix detects the short landing and re-applies on the next frame, which is past the compositor's animation teardown.

Verification

Verified on an iPhone XR / iOS 18.5 simulator with the EPUB from readest/readest#5663, reading straight through the book:

  • before: stuck on chapter 10's last page (3/4, 276/277)
  • after: reaches the final section, footer reads 277/277

Exactly one clamp event occurs in a 309-turn run, at the last page. Not reproducible on iOS 26.3, and not on other engines: every non-iOS platform takes the rafAnimateScroll path for a book that size, which writes the scroll offset directly each frame and never uses transforms.

cssAnimateScroll turns a page by translating every view element, then in its
cleanup clears the transforms and assigns the final scroll offset. Translating
the children shrinks the container's scrollable overflow by the animated
distance, and WebKit on iOS 18 keeps clamping against that shrunken extent for
the rest of the task, so the assignment lands short.

Mid-book the clamp sits far beyond the target and is invisible. On the last
page of the book, where the target is exactly the maximum scroll offset, it
lands a full page short and the page visibly snaps back, so the final page of
long books cannot be reached at all.

Forcing a layout first is not enough: the extent it reports is restored, but
the scroll clamp still uses the stale one. Detect the short landing and
re-apply on the next frame, which is past the compositor's animation teardown.

Verified on an iPhone XR / iOS 18.5 simulator with the EPUB from
readest/readest#5663: reading straight through now reaches 277/277 instead of
sticking at 276/277. Not reproducible on iOS 26.3.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chrox
chrox merged commit 887a0ae into main Aug 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant