Skip to content

input: Add scroll_bounce to stretch text past its ends and spring back - #3500

Open
shreeve wants to merge 1 commit into
longbridge:mainfrom
shreeve:feat/input-scroll-bounce
Open

shreeve wants to merge 1 commit into
longbridge:mainfrom
shreeve:feat/input-scroll-bounce

Conversation

@shreeve

@shreeve shreeve commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Description

A trackpad swipe or fling that runs into the end of a native macOS text view stretches the text past it and springs back. scroll_bounce(true) does the same for a multi-line input whose text is taller than it, using the spring and rubber-band curve of ScrollBounce.

The input scrolls itself, so it cannot sit inside ScrollBounce: that wrapper would move the input's frame along with the text, and it sees a packet only after the input has handled it. The input drives the same Physics instead. Scrolling stays exact: the stretch only moves where the text and gutter are drawn, inside the frame that clips them, and the scrollbar stays put.

  • A finger drags the edge and releases it to the spring; the momentum after a stretched release is left to the spring.
  • Momentum or a wheel flick that runs into the edge throws it instead: its packets measure speed rather than travel, so each adds to the edge's peak, up to 30px, and the spring carries it out and back. Physics gains a crate-private kick for that; pulling a 100px wheel step instead would jump the edge in one frame.
  • Unlike ScrollBounce, which leaves line-wheel input to its child, it also bounces on a mouse wheel: an input has no child to pass it to, and with a mouse the end of the text is otherwise a dead stop.
  • As with ScrollBounce, it is on by default on iOS and Android and off elsewhere. Under reduced motion the text stops at its ends without stretching.
  • The feel is ScrollBounce's default motion, plus the kick's fixed 0.3px of peak per pixel of travel. There is no ScrollBounceMotion setter on the input yet; one can follow if a host needs to tune it.

The Textarea and Editor stories turn it on, and the Textarea and Editor docs (English and Chinese) describe it. It builds on #3484, which keeps the scroll in the input at its end, so a stretch never scrolls the page behind it.

Public API

gpui-base (reached through TextareaState and EditorState in gpui-component)

  • InputBaseState<M: MultiLineMode>::scroll_bounce(self, scroll_bounce: bool) -> Self: whether text taller than the input stretches past its ends and springs back.
  • InputBaseState<M: MultiLineMode>::set_scroll_bounce(&mut self, scroll_bounce: bool, cx: &mut Context<Self>): change it later; turning it off drops any stretch at once.

Breaking Changes

On iOS and Android, textareas and editors whose text is taller than them now bounce at their ends by default, as ScrollBounce does there. To keep them still:

  TextareaState::new(window, cx)
+     .scroll_bounce(false)

How to Test

cargo test -p gpui-base --lib -- overscroll scroll_bounce
  • test_scroll_bounce_stretches_past_the_end_and_springs_back: a swipe past the end stretches the text, it springs back to 0, and the scroll offset stays exactly at the end.
  • test_scroll_bounce_stays_put_under_reduced_motion: the text still scrolls to its end, and the edge does not stretch.
  • test_scroll_bounce_is_off_by_default_on_the_desktop, test_scroll_bounce_leaves_text_that_fits and test_turning_scroll_bounce_off_drops_the_stretch pin the defaults and the setter.
  • overscroll::tests: pulling back unstretches before scrolling the text; a wheel flick throws the edge from where it is, never more than 15px a frame, peaks within 30px, ignores packets once it turns back, and settles; after a pause the next flick throws it again.
  • The existing scroll_bounce::tests still pass, so ScrollBounce is unchanged by sharing its spring.

AI-assisted: written with Claude Code and reviewed by me before submission.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes. (Textarea and Editor stories, macOS with a mouse wheel: the text bounces past either end and springs back; the frame, scrollbar and page stay put; nothing moves with Reduce motion on.)
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific). (Platform-specific only in its default on iOS and Android, which CI does not run; this commit passed CI on all three: run 38442010162.)

A trackpad swipe or a fling that runs into the end of a native macOS text view
stretches the text past it and springs back. `scroll_bounce(true)` does the
same for a multi-line input whose text is taller than it, with the spring and
rubber-band curve of `ScrollBounce`. The input scrolls itself, so it cannot sit
inside that wrapper, which would move its frame and only sees a packet after
the input has handled it; the input drives the shared `Physics` instead.
Scrolling stays exact: the stretch only moves where the text and gutter are
drawn, inside the frame that clips them, and the scrollbar stays put.

A finger drags the edge. Momentum or a mouse wheel flick that runs into it
throws it instead: its packets measure speed rather than travel, so each adds
to the edge's peak, capped, and the spring carries it out and back. `Physics`
gains `kick` for that. As with `ScrollBounce`, it is on by default on iOS and
Android and off elsewhere; under reduced motion the text stops at its ends
without stretching. The Textarea and Editor stories turn it on.
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