Repository navigation
Smooth scrolling: mouse-wheel notches glide like Chrome's, with a setting - #1005
zeronsh[bot] merged 9 commits into
Conversation
6a8356e to
0eaaaf2
Compare
…ting A mouse-wheel notch moved content in one jump: GPUI hands non-precise wheels to containers as `ScrollDelta::Lines`, applied at once. The window root now takes each notch at the end of the capture phase and replays it over the following frames as fractional `Lines` slices, on Chrome's wheel curve (`cc::ScrollOffsetAnimationCurve` for `kMouseWheel`): ease-in-out, cubic-bezier(0.42, 0, 0.58, 1), over 200ms, shortening to 100ms for long moves. A notch in flight retargets at the current speed, so a spin is one continuous glide. Slices stay in lines, so every container keeps its own line height and a notch travels exactly as far as before. Precise input, notches with Ctrl, Alt, Cmd or Fn held, and content that eases notches itself (WebView2) stay raw; a press ends a glide. The in-app browser on macOS glides too. AppKit hands a page its wheel directly and WKWebView applies a tick at once, so a local event monitor holds the page's ticks and replays the same glide as precise pixel deltas, which WebKit routes like a trackpad's: to the scroller under the pointer, clamped at its edges. Settings → Appearance → Motion → Smooth scrolling, on by default. Reduced motion turns it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pin broke only when one wheel event moved the view more than 1px from the bottom, decided in a deferred check. A glided notch starts and ends with sub-pixel slices, and its slices arrive on frames, where the spring's step runs before that check: it pulled the view back and recorded the distance the check compared against. A notch up from the bottom of a chat drifted back down. Upward input over a scrollable transcript now breaks the pin on the spot, as use-stick-to-bottom's wheel handler does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`on_scroll_wheel` broke the pin for any upward wheel that bubbled to the transcript root, including one an overlay above the list let through (an image viewer at its pan limit, say) while the list never moved. The list's scroll handler now marks the event it took, and only that event can escape the pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
Only `browser::macos` reads it; elsewhere `gliding` was dead code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
X11 reports touchpads (and high-resolution wheels) as fractional `ScrollDelta::Lines`, so every touchpad event was held back and eased over 200ms. Outside macOS, where trackpads are flagged precise, only a whole-line delta is a wheel notch; fractional lines now pass through. The transcript's arrival-time pin escape is for wheel notches only: precise input keeps the distance test it had before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
No fixture calls fixture_wheel; it served a throwaway capture that was never committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
The Motion row joins the settings index under Appearance → Motion, with the words people reach for: mouse, wheel, glide, ease. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
0eaaaf2 to
bf9c2c1
Compare
Appshots is hidden off macOS and Linux, so search can't find its rows there and the test failed on Windows since zeronsh#1018. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Security audit: clean. No network, new hosts, build/CI changes or new crates (objc2-core-graphics was already locked); the only unsafe is AppKit monitor/timer FFI. Media is plain PNG/GIF/H.264 with no metadata or trailing bytes.
Review: precise input, fractional lines off macOS, modified notches and WebView2 pass through untouched. The frame loop stops once a glide lands (on_next_frame is only re-queued while active), so the display link parks. Reduced motion and pause-in-background both turn it off. Every wheel consumer in the app is proportional to its delta, so slicing a notch is safe. The settings row matches its Motion siblings.
Follow-ups pushed: rebased onto main, dropped the unused fixture_wheel hook, added the row to settings search (#1018), and gated #1018's Appshots test to the platforms that show Appshots (it failed on Windows). zeron-ui lib tests are green locally on Linux, on this head and merged with main; macOS and Windows CI are green on e3afc8a.
a_pick_keeps_the_keyboard_in_the_field searched for "destination" and expected Appshots' Destination row. Appshots has no page off macOS and Linux, so the search (rightly) finds none of it on Windows and the test failed there since #1018. #1005 gated the whole test to macOS and Linux; this runs the focus check on Windows again. - a_pick_keeps_the_keyboard_in_the_field drops that cfg gate and picks Files' Autosave, a row every platform shows, by Enter and by a click. - appshots_leaves_a_pick_the_keyboard keeps the Appshots case, the page that takes the keyboard as it opens, where Appshots exists; on Windows it holds that "appshot", "destination" and "capture" find no Appshots hit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
Closes #1004
The problem
A mouse-wheel notch moved content in one jump. GPUI hands non-precise wheels to containers as
ScrollDelta::Lines, and they apply them at once, so a 3-line notch scrolls the transcript 60px in a single frame. Chrome on macOS glides the same notch over 200ms. The in-app browser stepped too, becauseWKWebViewapplies a non-precise tick at once (120px).The change
Settings → Appearance → Motion → Smooth scrolling is on by default. It affects mouse wheels only, since trackpads already stream pixels. Reduce motion turns it off, and the row says so.
smooth_scrollwraps the window root. It takes each notch at the end of the capture phase and replays it over the next frames as fractionalLinesslices. Every container keeps its own line height, and a notch travels exactly as far as before: slices sit on a 1/4096-line grid, so they sum exactly.cc::ScrollOffsetAnimationCurve(kMouseWheel): ease-in-outcubic-bezier(0.42, 0, 0.58, 1)over 200ms, shortening to 100ms for long moves. A notch in flight retargets at the current velocity (UpdateTarget), so a spin is one continuous glide. One departure: a notch against the glide drops the rest of that axis instead of netting it into a target clamped to the scroller, because the root can't see scroller bounds.PageGlide) holds the page's ticks and replays the same glide as precise pixel deltas. WebKit routes those like a trackpad's: to the scroller under the pointer, clamped at its edges.Demo
Both takes get the same notches in the same window: three single notches, then a quick spin. Left: smoothing off (before). Right: on (this PR).
Full resolution at 60fps, real time then ½ speed: smooth-scroll-off-vs-on.mp4
With Reduce motion on:
Measured offsets for one 3-line notch:
WKWebView, pagescrollYafter the tick): +14ms 1px · +47ms 11 · +80ms 36 · +114ms 70 · +148ms 99 · +181ms 117 · +197ms 120. Before, all 120px landed at once.How the media was made
A throwaway fixture (not committed) ran the production
Shellagainst an isolated engine in a real window. It dispatched the sameScrollDelta::Linesnotches on the same schedule for both takes and captured every presented frame with its timestamp: about 120Hz while idle, about 60Hz while the transcript re-lays out each frame. The videos are resampled from those timestamps. The stills and frames are composited onto an opaque backdrop, because the frosted theme renders translucent.Tests
smooth_scroll(14 tests):browser::macos(2 tests): a tick replays in whole pixels to exactly its lines, and the replay is a precise event at the tick's window location.transcript::tests::a_glided_notch_leaves_the_bottom_and_stays_where_it_landsfails without the pin fix. The arrival-time escape applies to wheel notches only; precise input keeps its old distance test.settingsandshell: on by default, persisted assmoothScrolling.-p zeron-ui --lib --test voice_orb_geometryon this head (macOS): 1898 + 11 passed, 0 failed. Thebrowser-fixturefeature (library andbrowser-fixtureexample) builds.Not verified: a physical mouse in the macOS in-app browser, i.e. AppKit's real event routing into the monitor. The replay path was verified in Zeron's real
WKWebViewwith synthetic ticks, because this machine can't post real wheel events.🤖 Generated with Claude Code