Bug 145330 - Scroll-snap points should be triggered during programmatic scroll
Summary: Scroll-snap points should be triggered during programmatic scroll
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: Layout and Rendering (show other bugs)
Version: 528+ (Nightly build)
Hardware: All All
: P2 Normal
Assignee: Martin Robinson
URL:
Keywords: InRadar
Depends on:
Blocks: 218115 221302
  Show dependency treegraph
 
Reported: 2015-05-22 17:20 PDT by Brent Fulgham
Modified: 2021-06-21 20:23 PDT (History)
22 users (show)

See Also:


Attachments
Patch (84.30 KB, patch)
2021-01-11 02:34 PST, Martin Robinson
ews-feeder: commit-queue-
Details | Formatted Diff | Diff
Patch (86.06 KB, patch)
2021-01-11 03:00 PST, Martin Robinson
ews-feeder: commit-queue-
Details | Formatted Diff | Diff
Patch (92.31 KB, patch)
2021-01-11 05:21 PST, Martin Robinson
no flags Details | Formatted Diff | Diff
Patch (91.58 KB, patch)
2021-01-12 05:38 PST, Martin Robinson
no flags Details | Formatted Diff | Diff
Patch (104.32 KB, patch)
2021-01-13 03:45 PST, Martin Robinson
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Brent Fulgham 2015-05-22 17:20:27 PDT
I've recently made a set of bug fixes that help the engine keep track of the current snap point, even after programmatic scrolls.

For proper behavior, when a programmatic scroll occurs that will take us close to a snap point, we should drive an animation to that snap point. If the programmatic scroll was not sufficient to take us past, say, the midpoint between two snap points, we should roll back to the original snap point once the scroll operation completes.

However, this would break manually animated scrolling operations.

One possible answer to this problem is to say "Don't try to combine scroll-snap points and programmatic scrolling."
Comment 1 Brent Fulgham 2015-06-19 14:37:00 PDT
This is currently under discussion in the css working group. The current thought is that we should not invoke scroll snap points for programmatic scrolls, but we should add a 'snap' element to the ScrollOptions dictionary.
Comment 2 Radar WebKit Bug Importer 2015-06-19 14:37:26 PDT
<rdar://problem/21467780>
Comment 3 Martin Robinson 2021-01-11 02:34:15 PST
Created attachment 417363 [details]
Patch
Comment 4 Martin Robinson 2021-01-11 03:00:06 PST
Created attachment 417365 [details]
Patch
Comment 5 Martin Robinson 2021-01-11 05:21:32 PST
Created attachment 417371 [details]
Patch
Comment 6 Simon Fraser (smfr) 2021-01-11 10:47:35 PST
Comment on attachment 417371 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=417371&action=review

> Source/WebCore/dom/Element.cpp:991
> +    FloatPoint originalPosition = FloatPoint(
>          adjustForAbsoluteZoom(renderer->scrollLeft(), *renderer),
>          adjustForAbsoluteZoom(renderer->scrollTop(), *renderer)
>      );

The better way to write this is auto originalPosition = FloatPoint { ... }

> Source/WebCore/dom/Element.cpp:992
> +    ScrollToOptions scrollToOptions = normalizeNonFiniteCoordinatesOrFallBackTo(options, originalPosition.x(), originalPosition.y());

auto

> Source/WebCore/dom/Element.cpp:998
> +    SetScrollPositionOptions setScrollPositionOptions = SetScrollPositionOptions::createProgrammatic();

auto

> Source/WebCore/dom/Element.cpp:1003
> +    if (snapPointSelectionMethod == ScrollSnapPointSelectionMethod::Directional)
> +        setScrollPositionOptions.enableDirectionalScrollSnapping(originalPosition);
> +    if (useSmoothScrolling(scrollToOptions.behavior.valueOr(ScrollBehavior::Auto), this))
> +        setScrollPositionOptions.setAnimated();

Is this code that could live in a helper on ScrollableArea?

> Source/WebCore/dom/Element.cpp:1333
> +    auto options = SetScrollPositionOptions::createProgrammatic();
> +    if (useSmoothScrolling(ScrollBehavior::Auto, this))
> +        options.setAnimated();

Ditto. Like ScrollableArea::optionsForProgrammaticScroll().

> Source/WebCore/page/DOMWindow.cpp:1708
> +    SetScrollPositionOptions setScrollPositionOptions = SetScrollPositionOptions::createProgrammatic();

auto.

> Source/WebCore/page/FrameView.cpp:2331
> +    ScrollOffset snappedOffset = ceiledIntPoint(
> +        scrollAnimator().adjustScrollOffsetForSnappingIfNeeded(scrollOffsetFromPosition(scrollPosition), delta, originalPosition));

Weird wrapping

> Source/WebCore/platform/ScrollTypes.h:257
> +enum class ScrollSnapPointSelectionMethod : bool {

I prefer uint8_t rather than bool.

> Source/WebCore/platform/ScrollTypes.h:270
> +struct SetScrollPositionOptions {

Having "Set" in the name makes this read like a function at the call sites. Maybe ScrollPositionChangeOptions?

> Source/WebCore/platform/cocoa/ScrollController.mm:918
> +    Optional<LayoutUnit> originalPositionInLayoutUnits = WTF::nullopt;

No need to initialize.

> Source/WebCore/rendering/RenderLayer.cpp:2849
> +            SetScrollPositionOptions setScrollPositionOptions = SetScrollPositionOptions::createProgrammatic();
> +            if (shouldUseAnimatedScroll(box->element(), options.behavior))
> +                setScrollPositionOptions.setAnimated();

More sharable code.

> Source/WebCore/rendering/RenderLayer.cpp:2884
> +                SetScrollPositionOptions setScrollPositionOptions = SetScrollPositionOptions::createProgrammatic();
> +                if (shouldUseAnimatedScroll(element, options.behavior))
> +                    setScrollPositionOptions.setAnimated();

Ditto

> Source/WebCore/rendering/RenderLayer.cpp:2935
> +                SetScrollPositionOptions setScrollPositionOptions = SetScrollPositionOptions::createProgrammatic();
> +                if (shouldUseAnimatedScroll(element, options.behavior))
> +                    setScrollPositionOptions.setAnimated();

Ditto
Comment 7 Martin Robinson 2021-01-12 05:38:27 PST
Created attachment 417448 [details]
Patch
Comment 8 Martin Robinson 2021-01-12 05:42:27 PST
(In reply to Simon Fraser (smfr) from comment #6)
> Comment on attachment 417371 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=417371&action=review

Thanks for the review!

> 
> > Source/WebCore/dom/Element.cpp:991
> > +    FloatPoint originalPosition = FloatPoint(
> >          adjustForAbsoluteZoom(renderer->scrollLeft(), *renderer),
> >          adjustForAbsoluteZoom(renderer->scrollTop(), *renderer)
> >      );
> 
> The better way to write this is auto originalPosition = FloatPoint { ... }

I gave this a shot, but it seems that things inside curly initialisers and casts are subject to errors due to narrowing int into float. I've switched this to:

FloatPoint originalPosition(...)

 
> > Source/WebCore/dom/Element.cpp:992
> > +    ScrollToOptions scrollToOptions = normalizeNonFiniteCoordinatesOrFallBackTo(options, originalPosition.x(), originalPosition.y());
> 
> auto

Fixed.

> 
> > Source/WebCore/dom/Element.cpp:998
> > +    SetScrollPositionOptions setScrollPositionOptions = SetScrollPositionOptions::createProgrammatic();
> 
> auto

Fixed.

> 
> > Source/WebCore/dom/Element.cpp:1003
> > +    if (snapPointSelectionMethod == ScrollSnapPointSelectionMethod::Directional)
> > +        setScrollPositionOptions.enableDirectionalScrollSnapping(originalPosition);
> > +    if (useSmoothScrolling(scrollToOptions.behavior.valueOr(ScrollBehavior::Auto), this))
> > +        setScrollPositionOptions.setAnimated();
> 
> Is this code that could live in a helper on ScrollableArea?

It's a little tricky to do a simple helper in ScrollableArea, but I reworked this a quite a bit and now I think there isn't any repeated code, apart from the call to useSmoothScrolling (which takes a different argument).
> 
> > Source/WebCore/dom/Element.cpp:1333
> > +    auto options = SetScrollPositionOptions::createProgrammatic();
> > +    if (useSmoothScrolling(ScrollBehavior::Auto, this))
> > +        options.setAnimated();
> 
> Ditto. Like ScrollableArea::optionsForProgrammaticScroll().
> 
> > Source/WebCore/page/DOMWindow.cpp:1708
> > +    SetScrollPositionOptions setScrollPositionOptions = SetScrollPositionOptions::createProgrammatic();
> 
> auto.

Fixed.

> 
> > Source/WebCore/page/FrameView.cpp:2331
> > +    ScrollOffset snappedOffset = ceiledIntPoint(
> > +        scrollAnimator().adjustScrollOffsetForSnappingIfNeeded(scrollOffsetFromPosition(scrollPosition), delta, originalPosition));
> 
> Weird wrapping

Fixed.

> 
> > Source/WebCore/platform/ScrollTypes.h:257
> > +enum class ScrollSnapPointSelectionMethod : bool {
> 
> I prefer uint8_t rather than bool.

Fixed.

> 
> > Source/WebCore/platform/ScrollTypes.h:270
> > +struct SetScrollPositionOptions {
> 
> Having "Set" in the name makes this read like a function at the call sites.
> Maybe ScrollPositionChangeOptions?

Sure. I'm not at all attached to the original name. I've renamed it to ScrollPositionChangeOptions.

> 
> > Source/WebCore/platform/cocoa/ScrollController.mm:918
> > +    Optional<LayoutUnit> originalPositionInLayoutUnits = WTF::nullopt;
> 
> No need to initialize.

Fixed.

> 
> > Source/WebCore/rendering/RenderLayer.cpp:2849
> > +            SetScrollPositionOptions setScrollPositionOptions = SetScrollPositionOptions::createProgrammatic();
> > +            if (shouldUseAnimatedScroll(box->element(), options.behavior))
> > +                setScrollPositionOptions.setAnimated();
> 
> More sharable code.

Fixed.
Comment 9 Simon Fraser (smfr) 2021-01-12 09:52:44 PST
Comment on attachment 417448 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=417448&action=review

> Source/WebCore/platform/ScrollAnimator.cpp:375
> +    Optional<float> originalX, originalY;
> +    FloatSize velocity;
> +    if (method == ScrollSnapPointSelectionMethod::Directional) {
> +        FloatSize scrollOrigin = toFloatSize(m_scrollableArea.scrollOrigin());
> +        originalX = currentPosition().x();
> +        originalY = currentPosition().y();
> +        auto newPosition = ScrollableArea::scrollPositionFromOffset(offset, scrollOrigin);
> +        velocity = { newPosition.x() - currentPosition().x(), newPosition.y() - currentPosition().y() };
> +    }
> +
> +    FloatPoint newOffset = offset;
> +    newOffset.setX(m_scrollController.adjustScrollDestination(ScrollEventAxis::Horizontal, newOffset.x(), velocity.width(), originalX));
> +    newOffset.setY(m_scrollController.adjustScrollDestination(ScrollEventAxis::Vertical, newOffset.y(), velocity.height(), originalY));

There's an unsetting mix of scrollOffset and scrollPosition here. originalX/originalY are positions (please name them thus), and are passed into m_scrollController.adjustScrollDestination(), but I can't tell if that actually treats them as positions or offsets. Please test in a horizontal RTL scroller.

> Source/WebCore/platform/cocoa/ScrollController.mm:921
> +    LayoutUnit offset = closestSnapOffset(snapState.snapOffsetsForAxis(axis), snapState.snapOffsetRangesForAxis(axis), LayoutUnit(destination / m_client.pageScaleFactor()), velocity, snapIndex, originalPositionInLayoutUnits);

Does closestSnapOffset() treat originalPositionInLayoutUnits as a position or offset?

> Source/WebCore/rendering/RenderLayer.cpp:2645
> +    IntPoint snappedOffset = ceiledIntPoint(scrollAnimator().adjustScrollOffsetForSnappingIfNeeded(clampedScrollOffset, options.snapPointSelectionMethod));

IntPoint -> ScrollOffset
Comment 10 Martin Robinson 2021-01-13 03:45:26 PST
Created attachment 417520 [details]
Patch
Comment 11 Martin Robinson 2021-01-13 03:52:58 PST
(In reply to Simon Fraser (smfr) from comment #9)
> Comment on attachment 417448 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=417448&action=review
> 
> > Source/WebCore/platform/ScrollAnimator.cpp:375
> > +    Optional<float> originalX, originalY;
> > +    FloatSize velocity;
> > +    if (method == ScrollSnapPointSelectionMethod::Directional) {
> > +        FloatSize scrollOrigin = toFloatSize(m_scrollableArea.scrollOrigin());
> > +        originalX = currentPosition().x();
> > +        originalY = currentPosition().y();
> > +        auto newPosition = ScrollableArea::scrollPositionFromOffset(offset, scrollOrigin);
> > +        velocity = { newPosition.x() - currentPosition().x(), newPosition.y() - currentPosition().y() };
> > +    }
> > +
> > +    FloatPoint newOffset = offset;
> > +    newOffset.setX(m_scrollController.adjustScrollDestination(ScrollEventAxis::Horizontal, newOffset.x(), velocity.width(), originalX));
> > +    newOffset.setY(m_scrollController.adjustScrollDestination(ScrollEventAxis::Vertical, newOffset.y(), velocity.height(), originalY));
> 
> There's an unsetting mix of scrollOffset and scrollPosition here.
> originalX/originalY are positions (please name them thus), and are passed
> into m_scrollController.adjustScrollDestination(), but I can't tell if that
> actually treats them as positions or offsets. Please test in a horizontal
> RTL scroller.

Agreed. After looking at this again, I think we should always be passing offsets here. I've done that and have modified all of the arguments and variable names down the call chain to make it clear that these are scroll offets. Additionally, I've written a test that verifies the correct scroll snap behavior for RTL scrollers when scrolling from script. That test is part of the patch now.

> > Source/WebCore/rendering/RenderLayer.cpp:2645
> > +    IntPoint snappedOffset = ceiledIntPoint(scrollAnimator().adjustScrollOffsetForSnappingIfNeeded(clampedScrollOffset, options.snapPointSelectionMethod));
> 
> IntPoint -> ScrollOffset

Fixed.
Comment 12 EWS 2021-01-13 06:18:08 PST
Committed r271439: <https://trac.webkit.org/changeset/271439>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 417520 [details].
Comment 13 Darin Adler 2021-02-02 15:30:06 PST
Comment on attachment 417520 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=417520&action=review

> Source/WebCore/page/ScrollBehavior.cpp:41
> +    // FIXME: Should we use document()->scrollingElement()?
> +    // See https://bugs.webkit.org/show_bug.cgi?id=205059
> +    if (associatedElement == associatedElement->document().scrollingElement())
> +        associatedElement = associatedElement->document().documentElement();

This is dereferencing associatedElement *before* it is null checked below.