示例#1
0
void RotationViewportAnchor::computeOrigins(
    const FloatSize& innerSize,
    IntPoint& mainFrameOffset,
    FloatPoint& visualViewportOffset) const {
  IntSize outerSize = layoutViewport().visibleContentRect().size();

  // Compute the viewport origins in CSS pixels relative to the document.
  FloatSize absVisualViewportOffset = m_normalizedVisualViewportOffset;
  absVisualViewportOffset.scale(outerSize.width(), outerSize.height());

  FloatPoint innerOrigin = getInnerOrigin(innerSize);
  FloatPoint outerOrigin = innerOrigin - absVisualViewportOffset;

  IntRect outerRect = IntRect(flooredIntPoint(outerOrigin), outerSize);
  FloatRect innerRect = FloatRect(innerOrigin, innerSize);

  moveToEncloseRect(outerRect, innerRect);

  outerRect.setLocation(IntPoint(
      layoutViewport().clampScrollOffset(toIntSize(outerRect.location()))));

  moveIntoRect(innerRect, outerRect);

  mainFrameOffset = outerRect.location();
  visualViewportOffset =
      FloatPoint(innerRect.location() - outerRect.location());
}
void RootFrameViewport::distributeScrollBetweenViewports(const DoublePoint& offset, ScrollType scrollType, ScrollBehavior behavior)
{
    // Make sure we use the scroll positions as reported by each viewport's ScrollAnimatorBase, since its
    // ScrollableArea's position may have the fractional part truncated off.
    DoublePoint oldPosition = scrollOffsetFromScrollAnimators();

    DoubleSize delta = offset - oldPosition;

    if (delta.isZero())
        return;

    DoublePoint targetPosition = visualViewport().clampScrollPosition(
        visualViewport().scrollAnimator().currentPosition() + delta);

    visualViewport().setScrollPosition(targetPosition, scrollType, behavior);

    // Scroll the secondary viewport if all of the scroll was not applied to the
    // primary viewport.
    DoublePoint updatedPosition = layoutViewport().scrollAnimator().currentPosition() + FloatPoint(targetPosition);
    DoubleSize applied = updatedPosition - oldPosition;
    delta -= applied;

    if (delta.isZero())
        return;

    targetPosition = layoutViewport().clampScrollPosition(layoutViewport().scrollAnimator().currentPosition() + delta);
    layoutViewport().setScrollPosition(targetPosition, scrollType, behavior);
}
void RootFrameViewport::setScrollOffset(const DoublePoint& offset, ScrollType scrollType)
{
    // Make sure we use the scroll positions as reported by each viewport's ScrollAnimator, since its
    // ScrollableArea's position may have the fractional part truncated off.
    DoublePoint oldPosition = scrollOffsetFromScrollAnimators();

    DoubleSize delta = offset - oldPosition;

    if (delta.isZero())
        return;

    ScrollableArea& primary = !m_invertScrollOrder ? layoutViewport() : visualViewport();
    ScrollableArea& secondary = !m_invertScrollOrder ? visualViewport() : layoutViewport();

    DoublePoint targetPosition = primary.clampScrollPosition(primary.scrollAnimator()->currentPosition() + delta);
    primary.setScrollPosition(targetPosition, scrollType);

    DoubleSize applied = scrollOffsetFromScrollAnimators() - oldPosition;
    delta -= applied;

    if (delta.isZero())
        return;

    targetPosition = secondary.clampScrollPosition(secondary.scrollAnimator()->currentPosition() + delta);
    secondary.setScrollPosition(targetPosition, scrollType);
}
示例#4
0
void RootFrameViewport::restoreToAnchor(const ScrollOffset& targetOffset) {
  // Clamp the scroll offset of each viewport now so that we force any invalid
  // offsets to become valid so we can compute the correct deltas.
  visualViewport().setScrollOffset(visualViewport().getScrollOffset(),
                                   ProgrammaticScroll);
  layoutViewport().setScrollOffset(layoutViewport().getScrollOffset(),
                                   ProgrammaticScroll);

  ScrollOffset delta = targetOffset - getScrollOffset();

  visualViewport().setScrollOffset(visualViewport().getScrollOffset() + delta,
                                   ProgrammaticScroll);

  delta = targetOffset - getScrollOffset();

  // Since the main thread FrameView has integer scroll offsets, scroll it to
  // the next pixel and then we'll scroll the visual viewport again to
  // compensate for the sub-pixel offset. We need this "overscroll" to ensure
  // the pixel of which we want to be partially in appears fully inside the
  // FrameView since the VisualViewport is bounded by the FrameView.
  IntSize layoutDelta = IntSize(
      delta.width() < 0 ? floor(delta.width()) : ceil(delta.width()),
      delta.height() < 0 ? floor(delta.height()) : ceil(delta.height()));

  layoutViewport().setScrollOffset(
      ScrollOffset(layoutViewport().scrollOffsetInt() + layoutDelta),
      ProgrammaticScroll);

  delta = targetOffset - getScrollOffset();
  visualViewport().setScrollOffset(visualViewport().getScrollOffset() + delta,
                                   ProgrammaticScroll);
}
示例#5
0
LayoutRect RootFrameViewport::scrollIntoView(const LayoutRect& rectInContent,
                                             const ScrollAlignment& alignX,
                                             const ScrollAlignment& alignY,
                                             ScrollType scrollType) {
  // We want to move the rect into the viewport that excludes the scrollbars so
  // we intersect the visual viewport with the scrollbar-excluded frameView
  // content rect. However, we don't use visibleContentRect directly since it
  // floors the scroll offset. Instead, we use ScrollAnimatorBase::currentOffset
  // and construct a LayoutRect from that.
  LayoutRect frameRectInContent =
      LayoutRect(FloatPoint(layoutViewport().scrollAnimator().currentOffset()),
                 FloatSize(layoutViewport().visibleContentRect().size()));
  LayoutRect visualRectInContent =
      LayoutRect(FloatPoint(scrollOffsetFromScrollAnimators()),
                 FloatSize(visualViewport().visibleContentRect().size()));

  // Intersect layout and visual rects to exclude the scrollbar from the view
  // rect.
  LayoutRect viewRectInContent =
      intersection(visualRectInContent, frameRectInContent);
  LayoutRect targetViewport = ScrollAlignment::getRectToExpose(
      viewRectInContent, rectInContent, alignX, alignY);
  if (targetViewport != viewRectInContent) {
    setScrollOffset(ScrollOffset(targetViewport.x(), targetViewport.y()),
                    scrollType);
  }

  // RootFrameViewport only changes the viewport relative to the document so we
  // can't change the input rect's location relative to the document origin.
  return rectInContent;
}
示例#6
0
void RootFrameViewport::setScrollOffset(const ScrollOffset& offset,
                                        ScrollType scrollType,
                                        ScrollBehavior scrollBehavior) {
  updateScrollAnimator();

  if (scrollBehavior == ScrollBehaviorAuto)
    scrollBehavior = scrollBehaviorStyle();

  if (scrollType == ProgrammaticScroll &&
      !layoutViewport().isProgrammaticallyScrollable())
    return;

  if (scrollType == AnchoringScroll) {
    distributeScrollBetweenViewports(offset, scrollType, scrollBehavior,
                                     LayoutViewport);
    return;
  }

  if (scrollBehavior == ScrollBehaviorSmooth) {
    distributeScrollBetweenViewports(offset, scrollType, scrollBehavior,
                                     VisualViewport);
    return;
  }

  ScrollOffset clampedOffset = clampScrollOffset(offset);
  ScrollableArea::setScrollOffset(clampedOffset, scrollType, scrollBehavior);
}
示例#7
0
void RotationViewportAnchor::setAnchor() {
  RootFrameViewport* rootFrameViewport =
      m_rootFrameView->getRootFrameViewport();
  DCHECK(rootFrameViewport);

  m_oldPageScaleFactor = m_visualViewport->scale();
  m_oldMinimumPageScaleFactor =
      m_pageScaleConstraintsSet.finalConstraints().minimumScale;

  // Save the absolute location in case we won't find the anchor node, we'll
  // fall back to that.
  m_visualViewportInDocument =
      FloatPoint(rootFrameViewport->visibleContentRect().location());

  m_anchorNode.clear();
  m_anchorNodeBounds = LayoutRect();
  m_anchorInNodeCoords = FloatSize();
  m_normalizedVisualViewportOffset = FloatSize();

  IntRect innerViewRect = rootFrameViewport->visibleContentRect();

  // Preserve origins at the absolute screen origin.
  if (innerViewRect.location() == IntPoint::zero() || innerViewRect.isEmpty())
    return;

  IntRect outerViewRect =
      layoutViewport().visibleContentRect(IncludeScrollbars);

  // Normalize by the size of the outer rect
  DCHECK(!outerViewRect.isEmpty());
  m_normalizedVisualViewportOffset = m_visualViewport->getScrollOffset();
  m_normalizedVisualViewportOffset.scale(1.0 / outerViewRect.width(),
                                         1.0 / outerViewRect.height());

  // Note, we specifically use the unscaled visual viewport size here as the
  // conversion into content-space below will apply the scale.
  FloatPoint anchorOffset(m_visualViewport->size());
  anchorOffset.scale(m_anchorInInnerViewCoords.width(),
                     m_anchorInInnerViewCoords.height());

  // Note, we specifically convert to the rootFrameView contents here, rather
  // than the layout viewport. That's because hit testing works from the
  // FrameView's content coordinates even if it's not the layout viewport.
  const FloatPoint anchorPointInContents = m_rootFrameView->frameToContents(
      m_visualViewport->viewportToRootFrame(anchorOffset));

  Node* node = findNonEmptyAnchorNode(flooredIntPoint(anchorPointInContents),
                                      innerViewRect,
                                      m_rootFrameView->frame().eventHandler());
  if (!node)
    return;

  m_anchorNode = node;
  m_anchorNodeBounds = node->boundingBox();
  m_anchorInNodeCoords =
      anchorPointInContents - FloatPoint(m_anchorNodeBounds.location());
  m_anchorInNodeCoords.scale(1.f / m_anchorNodeBounds.width(),
                             1.f / m_anchorNodeBounds.height());
}
void RootFrameViewport::setScrollPosition(const DoublePoint& position, ScrollType scrollType, ScrollBehavior scrollBehavior)
{
    updateScrollAnimator();

    // TODO(bokan): Support smooth scrolling the visual viewport.
    if (scrollBehavior == ScrollBehaviorAuto)
        scrollBehavior = scrollBehaviorStyle();
    if (scrollBehavior == ScrollBehaviorSmooth) {
        layoutViewport().setScrollPosition(position, scrollType, scrollBehavior);
        return;
    }

    if (scrollType == ProgrammaticScroll && !layoutViewport().isProgrammaticallyScrollable())
        return;

    DoublePoint clampedPosition = clampScrollPosition(position);
    ScrollableArea::setScrollPosition(clampedPosition, scrollType, scrollBehavior);
}
LayoutRect RootFrameViewport::scrollIntoView(const LayoutRect& rectInContent, const ScrollAlignment& alignX, const ScrollAlignment& alignY)
{
    // We want to move the rect into the viewport that excludes the scrollbars so we intersect
    // the visual viewport with the scrollbar-excluded frameView content rect. However, we don't
    // use visibleContentRect directly since it floors the scroll position. Instead, we use
    // FrameView::scrollPositionDouble and construct a LayoutRect from that (the FrameView size
    // is always integer sized.

    LayoutRect frameRectInContent = LayoutRect(
        layoutViewport().scrollPositionDouble(),
        layoutViewport().visibleContentRect().size());
    LayoutRect visualRectInContent = LayoutRect(
        layoutViewport().scrollPositionDouble() + toDoubleSize(visualViewport().scrollPositionDouble()),
        visualViewport().visibleContentRect().size());

    LayoutRect viewRectInContent = intersection(visualRectInContent, frameRectInContent);
    LayoutRect targetViewport =
        ScrollAlignment::getRectToExpose(viewRectInContent, rectInContent, alignX, alignY);

    // visualViewport.scrollIntoView will attempt to center the given rect within the viewport
    // so to prevent it from adjusting r's coordinates the rect must match the viewport's size
    // i.e. add the subtracted scrollbars from above back in.
    // FIXME: This is hacky and required because getRectToExpose doesn't naturally account
    // for the two viewports. crbug.com/449340.
    targetViewport.setSize(LayoutSize(visualViewport().visibleContentRect().size()));

    // Snap the visible rect to layout units to match the calculated target viewport rect.
    FloatRect visible =
        LayoutRect(visualViewport().scrollPositionDouble(), visualViewport().visibleContentRect().size());

    float centeringOffsetX = (visible.width() - targetViewport.width()) / 2;
    float centeringOffsetY = (visible.height() - targetViewport.height()) / 2;

    DoublePoint targetOffset(
        targetViewport.x() - centeringOffsetX,
        targetViewport.y() - centeringOffsetY);

    setScrollPosition(targetOffset, ProgrammaticScroll);

    // RootFrameViewport only changes the viewport relative to the document so we can't change the input
    // rect's location relative to the document origin.
    return rectInContent;
}
示例#10
0
LayoutRect RootFrameViewport::rootContentsToLayoutViewportContents(
    FrameView& rootFrameView,
    const LayoutRect& rect) const {
  LayoutRect ret(rect);

  // If the root FrameView is the layout viewport then coordinates in the
  // root FrameView's content space are already in the layout viewport's
  // content space.
  if (rootFrameView.layoutViewportScrollableArea() == &layoutViewport())
    return ret;

  // Make the given rect relative to the top of the layout viewport's content
  // by adding the scroll position.
  // TODO(bokan): This will have to be revisited if we ever remove the
  // restriction that a root scroller must be exactly screen filling.
  ret.move(LayoutSize(layoutViewport().getScrollOffset()));

  return ret;
}
示例#11
0
void RootFrameViewport::distributeScrollBetweenViewports(
    const ScrollOffset& offset,
    ScrollType scrollType,
    ScrollBehavior behavior,
    ViewportToScrollFirst scrollFirst) {
  // Make sure we use the scroll offsets as reported by each viewport's
  // ScrollAnimatorBase, since its ScrollableArea's offset may have the
  // fractional part truncated off.
  // TODO(szager): Now that scroll offsets are stored as floats, can we take the
  // scroll offset directly from the ScrollableArea's rather than the animators?
  ScrollOffset oldOffset = scrollOffsetFromScrollAnimators();

  ScrollOffset delta = offset - oldOffset;

  if (delta.isZero())
    return;

  ScrollableArea& primary =
      scrollFirst == VisualViewport ? visualViewport() : layoutViewport();
  ScrollableArea& secondary =
      scrollFirst == VisualViewport ? layoutViewport() : visualViewport();

  ScrollOffset targetOffset = primary.clampScrollOffset(
      primary.scrollAnimator().currentOffset() + delta);

  primary.setScrollOffset(targetOffset, scrollType, behavior);

  // Scroll the secondary viewport if all of the scroll was not applied to the
  // primary viewport.
  ScrollOffset updatedOffset =
      secondary.scrollAnimator().currentOffset() + FloatSize(targetOffset);
  ScrollOffset applied = updatedOffset - oldOffset;
  delta -= applied;

  if (delta.isZero())
    return;

  targetOffset = secondary.clampScrollOffset(
      secondary.scrollAnimator().currentOffset() + delta);
  secondary.setScrollOffset(targetOffset, scrollType, behavior);
}
ScrollResult RootFrameViewport::handleWheel(const PlatformWheelEvent& event)
{
    updateScrollAnimator();

    ScrollableArea& primary = !m_invertScrollOrder ? layoutViewport() : visualViewport();
    ScrollableArea& secondary = !m_invertScrollOrder ? visualViewport() : layoutViewport();

    ScrollResult viewScrollResult = primary.handleWheel(event);

    // The visual viewport will only accept pixel scrolls.
    if (!event.canScroll() || event.granularity() == ScrollByPageWheelEvent)
        return viewScrollResult;

    // TODO(sataya.m) : The delta in PlatformWheelEvent is negative when scrolling the
    // wheel towards the user, so negate it to get the scroll delta that should be applied
    // to the page. unusedScrollDelta computed in the ScrollResult is also negative. Say
    // there is WheelEvent({0, -10} and page scroll by 2px and unusedScrollDelta computed
    // is {0, -8}. Due to which we have to negate the unusedScrollDelta to obtain the expected
    // animation.Please address http://crbug.com/504389.
    DoublePoint oldOffset = secondary.scrollPositionDouble();
    DoublePoint locationDelta;
    if (viewScrollResult.didScroll()) {
        locationDelta = -DoublePoint(viewScrollResult.unusedScrollDeltaX, viewScrollResult.unusedScrollDeltaY);
    } else {
        if (event.railsMode() != PlatformEvent::RailsModeVertical)
            locationDelta.setX(-event.deltaX());
        if (event.railsMode() != PlatformEvent::RailsModeHorizontal)
            locationDelta.setY(-event.deltaY());
    }

    DoublePoint targetPosition = secondary.clampScrollPosition(
        secondary.scrollPositionDouble() + toDoubleSize(locationDelta));
    secondary.setScrollPosition(targetPosition, UserScroll);

    DoublePoint usedLocationDelta(secondary.scrollPositionDouble() - oldOffset);

    bool didScrollX = viewScrollResult.didScrollX || usedLocationDelta.x();
    bool didScrollY = viewScrollResult.didScrollY || usedLocationDelta.y();
    return ScrollResult(didScrollX, didScrollY, -viewScrollResult.unusedScrollDeltaX - usedLocationDelta.x(), -viewScrollResult.unusedScrollDeltaY - usedLocationDelta.y());
}
ScrollResultOneDimensional RootFrameViewport::userScroll(ScrollDirectionPhysical direction, ScrollGranularity granularity, float delta)
{
    updateScrollAnimator();

    ScrollbarOrientation orientation;

    if (direction == ScrollUp || direction == ScrollDown)
        orientation = VerticalScrollbar;
    else
        orientation = HorizontalScrollbar;

    if (layoutViewport().userInputScrollable(orientation) && visualViewport().userInputScrollable(orientation))
        return ScrollableArea::userScroll(direction, granularity, delta);

    if (visualViewport().userInputScrollable(orientation))
        return visualViewport().userScroll(direction, granularity, delta);

    if (layoutViewport().userInputScrollable(orientation))
        return layoutViewport().userScroll(direction, granularity, delta);

    return ScrollResultOneDimensional(false, delta);
}
void RootFrameViewport::setScrollPosition(const DoublePoint& position, ScrollType scrollType, ScrollBehavior scrollBehavior)
{
    updateScrollAnimator();

    if (scrollBehavior == ScrollBehaviorAuto)
        scrollBehavior = scrollBehaviorStyle();

    if (scrollType == ProgrammaticScroll && !layoutViewport().isProgrammaticallyScrollable())
        return;

    if (scrollBehavior == ScrollBehaviorSmooth) {
        distributeScrollBetweenViewports(position, scrollType, scrollBehavior);
        return;
    }

    DoublePoint clampedPosition = clampScrollPosition(position);
    ScrollableArea::setScrollPosition(clampedPosition, scrollType, scrollBehavior);
}
示例#15
0
void RotationViewportAnchor::restoreToAnchor() {
  float newPageScaleFactor =
      m_oldPageScaleFactor / m_oldMinimumPageScaleFactor *
      m_pageScaleConstraintsSet.finalConstraints().minimumScale;
  newPageScaleFactor =
      m_pageScaleConstraintsSet.finalConstraints().clampToConstraints(
          newPageScaleFactor);

  FloatSize visualViewportSize(m_visualViewport->size());
  visualViewportSize.scale(1 / newPageScaleFactor);

  IntPoint mainFrameOrigin;
  FloatPoint visualViewportOrigin;

  computeOrigins(visualViewportSize, mainFrameOrigin, visualViewportOrigin);

  layoutViewport().setScrollOffset(toScrollOffset(mainFrameOrigin),
                                   ProgrammaticScroll);

  // Set scale before location, since location can be clamped on setting scale.
  m_visualViewport->setScale(newPageScaleFactor);
  m_visualViewport->setLocation(visualViewportOrigin);
}
示例#16
0
bool RootFrameViewport::scrollAnimatorEnabled() const {
  return layoutViewport().scrollAnimatorEnabled();
}
示例#17
0
LayoutBox* RootFrameViewport::layoutBox() const {
  return layoutViewport().layoutBox();
}
示例#18
0
void RootFrameViewport::didUpdateVisualViewport() {
  if (RuntimeEnabledFeatures::scrollAnchoringEnabled()) {
    if (ScrollAnchor* anchor = layoutViewport().scrollAnchor())
      anchor->clear();
  }
}
示例#19
0
bool RootFrameViewport::shouldPlaceVerticalScrollbarOnLeft() const {
  return layoutViewport().shouldPlaceVerticalScrollbarOnLeft();
}
示例#20
0
void RootFrameViewport::clearScrollableArea() {
  ScrollableArea::clearScrollableArea();
  layoutViewport().clearScrollableArea();
  visualViewport().clearScrollableArea();
}
示例#21
0
void RootFrameViewport::cancelProgrammaticScrollAnimation() {
  ScrollableArea::cancelProgrammaticScrollAnimation();
  layoutViewport().cancelProgrammaticScrollAnimation();
  visualViewport().cancelProgrammaticScrollAnimation();
}
示例#22
0
void RootFrameViewport::updateCompositorScrollAnimations() {
  ScrollableArea::updateCompositorScrollAnimations();
  layoutViewport().updateCompositorScrollAnimations();
  visualViewport().updateCompositorScrollAnimations();
}
示例#23
0
void RootFrameViewport::serviceScrollAnimations(double monotonicTime) {
  ScrollableArea::serviceScrollAnimations(monotonicTime);
  layoutViewport().serviceScrollAnimations(monotonicTime);
  visualViewport().serviceScrollAnimations(monotonicTime);
}
示例#24
0
HostWindow* RootFrameViewport::getHostWindow() const {
  return layoutViewport().getHostWindow();
}
示例#25
0
bool RootFrameViewport::userInputScrollable(
    ScrollbarOrientation orientation) const {
  return visualViewport().userInputScrollable(orientation) ||
         layoutViewport().userInputScrollable(orientation);
}
示例#26
0
void RootFrameViewport::scrollControlWasSetNeedsPaintInvalidation() {
  layoutViewport().scrollControlWasSetNeedsPaintInvalidation();
}
示例#27
0
GraphicsLayer* RootFrameViewport::layerForContainer() const {
  return layoutViewport().layerForContainer();
}
示例#28
0
GraphicsLayer* RootFrameViewport::layerForVerticalScrollbar() const {
  return layoutViewport().layerForVerticalScrollbar();
}
示例#29
0
GraphicsLayer* RootFrameViewport::layerForScrollCorner() const {
  return layoutViewport().layerForScrollCorner();
}
示例#30
0
ScrollResult RootFrameViewport::userScroll(ScrollGranularity granularity,
                                           const FloatSize& delta) {
  // TODO(bokan/ymalik): Once smooth scrolling is permanently enabled we
  // should be able to remove this method override and use the base class
  // version: ScrollableArea::userScroll.

  updateScrollAnimator();

  // Distribute the scroll between the visual and layout viewport.

  float stepX = scrollStep(granularity, HorizontalScrollbar);
  float stepY = scrollStep(granularity, VerticalScrollbar);

  FloatSize pixelDelta(delta);
  pixelDelta.scale(stepX, stepY);

  // Precompute the amount of possible scrolling since, when animated,
  // ScrollAnimator::userScroll will report having consumed the total given
  // scroll delta, regardless of how much will actually scroll, but we need to
  // know how much to leave for the layout viewport.
  FloatSize visualConsumedDelta =
      visualViewport().scrollAnimator().computeDeltaToConsume(pixelDelta);

  // Split the remaining delta between scrollable and unscrollable axes of the
  // layout viewport. We only pass a delta to the scrollable axes and remember
  // how much was held back so we can add it to the unused delta in the
  // result.
  FloatSize layoutDelta = pixelDelta - visualConsumedDelta;
  FloatSize scrollableAxisDelta(
      layoutViewport().userInputScrollable(HorizontalScrollbar)
          ? layoutDelta.width()
          : 0,
      layoutViewport().userInputScrollable(VerticalScrollbar)
          ? layoutDelta.height()
          : 0);

  // If there won't be any scrolling, bail early so we don't produce any side
  // effects like cancelling existing animations.
  if (visualConsumedDelta.isZero() && scrollableAxisDelta.isZero()) {
    return ScrollResult(false, false, pixelDelta.width(), pixelDelta.height());
  }

  cancelProgrammaticScrollAnimation();

  // TODO(bokan): Why do we call userScroll on the animators directly and
  // not through the ScrollableAreas?
  ScrollResult visualResult = visualViewport().scrollAnimator().userScroll(
      granularity, visualConsumedDelta);

  if (visualConsumedDelta == pixelDelta)
    return visualResult;

  ScrollResult layoutResult = layoutViewport().scrollAnimator().userScroll(
      granularity, scrollableAxisDelta);

  // Remember to add any delta not used because of !userInputScrollable to the
  // unusedScrollDelta in the result.
  FloatSize unscrollableAxisDelta = layoutDelta - scrollableAxisDelta;

  return ScrollResult(
      visualResult.didScrollX || layoutResult.didScrollX,
      visualResult.didScrollY || layoutResult.didScrollY,
      layoutResult.unusedScrollDeltaX + unscrollableAxisDelta.width(),
      layoutResult.unusedScrollDeltaY + unscrollableAxisDelta.height());
}