blob: f7bce5958ef484b82715c7b7fc5081288f96a26a [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "core/frame/RootFrameViewport.h"
#include "core/frame/FrameView.h"
#include "core/layout/ScrollAlignment.h"
#include "core/layout/ScrollAnchor.h"
#include "platform/geometry/DoubleRect.h"
#include "platform/geometry/FloatRect.h"
#include "platform/geometry/LayoutRect.h"
namespace blink {
RootFrameViewport::RootFrameViewport(ScrollableArea& visualViewport,
ScrollableArea& layoutViewport)
: m_visualViewport(visualViewport) {
setLayoutViewport(layoutViewport);
}
void RootFrameViewport::setLayoutViewport(ScrollableArea& newLayoutViewport) {
if (m_layoutViewport.get() == &newLayoutViewport)
return;
if (m_layoutViewport && m_layoutViewport->scrollAnchor())
m_layoutViewport->scrollAnchor()->setScroller(m_layoutViewport.get());
m_layoutViewport = &newLayoutViewport;
if (m_layoutViewport->scrollAnchor())
m_layoutViewport->scrollAnchor()->setScroller(this);
}
ScrollableArea& RootFrameViewport::layoutViewport() const {
DCHECK(m_layoutViewport);
return *m_layoutViewport;
}
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().scrollOffset()));
return ret;
}
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().scrollOffset(),
ProgrammaticScroll);
layoutViewport().setScrollOffset(layoutViewport().scrollOffset(),
ProgrammaticScroll);
ScrollOffset delta = targetOffset - scrollOffset();
visualViewport().setScrollOffset(visualViewport().scrollOffset() + delta,
ProgrammaticScroll);
delta = targetOffset - scrollOffset();
// 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 - scrollOffset();
visualViewport().setScrollOffset(visualViewport().scrollOffset() + delta,
ProgrammaticScroll);
}
void RootFrameViewport::didUpdateVisualViewport() {
if (RuntimeEnabledFeatures::scrollAnchoringEnabled()) {
if (ScrollAnchor* anchor = layoutViewport().scrollAnchor())
anchor->clear();
}
}
LayoutBox* RootFrameViewport::layoutBox() const {
return layoutViewport().layoutBox();
}
void RootFrameViewport::updateScrollAnimator() {
scrollAnimator().setCurrentOffset(
toFloatSize(scrollOffsetFromScrollAnimators()));
}
ScrollOffset RootFrameViewport::scrollOffsetFromScrollAnimators() const {
return visualViewport().scrollAnimator().currentOffset() +
layoutViewport().scrollAnimator().currentOffset();
}
IntRect RootFrameViewport::visibleContentRect(
IncludeScrollbarsInRect scrollbarInclusion) const {
return IntRect(
IntPoint(scrollOffsetInt()),
visualViewport().visibleContentRect(scrollbarInclusion).size());
}
bool RootFrameViewport::shouldUseIntegerScrollOffset() const {
// Fractionals are floored in the ScrollAnimatorBase but it's important that
// the ScrollAnimators of the visual and layout viewports get the precise
// fractional number so never use integer scrolling for RootFrameViewport,
// we'll let the truncation happen in the subviewports.
return false;
}
bool RootFrameViewport::isActive() const {
return layoutViewport().isActive();
}
int RootFrameViewport::scrollSize(ScrollbarOrientation orientation) const {
IntSize scrollDimensions =
maximumScrollOffsetInt() - minimumScrollOffsetInt();
return (orientation == HorizontalScrollbar) ? scrollDimensions.width()
: scrollDimensions.height();
}
bool RootFrameViewport::isScrollCornerVisible() const {
return layoutViewport().isScrollCornerVisible();
}
IntRect RootFrameViewport::scrollCornerRect() const {
return layoutViewport().scrollCornerRect();
}
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);
}
ScrollBehavior RootFrameViewport::scrollBehaviorStyle() const {
return layoutViewport().scrollBehaviorStyle();
}
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;
}
void RootFrameViewport::updateScrollOffset(const ScrollOffset& offset,
ScrollType scrollType) {
distributeScrollBetweenViewports(offset, scrollType, ScrollBehaviorInstant,
VisualViewport);
}
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);
}
IntSize RootFrameViewport::scrollOffsetInt() const {
return flooredIntSize(scrollOffset());
}
ScrollOffset RootFrameViewport::scrollOffset() const {
return layoutViewport().scrollOffset() + visualViewport().scrollOffset();
}
IntSize RootFrameViewport::minimumScrollOffsetInt() const {
return IntSize(layoutViewport().minimumScrollOffsetInt() +
visualViewport().minimumScrollOffsetInt());
}
IntSize RootFrameViewport::maximumScrollOffsetInt() const {
return layoutViewport().maximumScrollOffsetInt() +
visualViewport().maximumScrollOffsetInt();
}
ScrollOffset RootFrameViewport::maximumScrollOffset() const {
return layoutViewport().maximumScrollOffset() +
visualViewport().maximumScrollOffset();
}
IntSize RootFrameViewport::contentsSize() const {
return layoutViewport().contentsSize();
}
bool RootFrameViewport::scrollbarsCanBeActive() const {
return layoutViewport().scrollbarsCanBeActive();
}
IntRect RootFrameViewport::scrollableAreaBoundingBox() const {
return layoutViewport().scrollableAreaBoundingBox();
}
bool RootFrameViewport::userInputScrollable(
ScrollbarOrientation orientation) const {
return visualViewport().userInputScrollable(orientation) ||
layoutViewport().userInputScrollable(orientation);
}
bool RootFrameViewport::shouldPlaceVerticalScrollbarOnLeft() const {
return layoutViewport().shouldPlaceVerticalScrollbarOnLeft();
}
void RootFrameViewport::scrollControlWasSetNeedsPaintInvalidation() {
layoutViewport().scrollControlWasSetNeedsPaintInvalidation();
}
GraphicsLayer* RootFrameViewport::layerForContainer() const {
return layoutViewport().layerForContainer();
}
GraphicsLayer* RootFrameViewport::layerForScrolling() const {
return layoutViewport().layerForScrolling();
}
GraphicsLayer* RootFrameViewport::layerForHorizontalScrollbar() const {
return layoutViewport().layerForHorizontalScrollbar();
}
GraphicsLayer* RootFrameViewport::layerForVerticalScrollbar() const {
return layoutViewport().layerForVerticalScrollbar();
}
GraphicsLayer* RootFrameViewport::layerForScrollCorner() const {
return layoutViewport().layerForScrollCorner();
}
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());
}
bool RootFrameViewport::scrollAnimatorEnabled() const {
return layoutViewport().scrollAnimatorEnabled();
}
HostWindow* RootFrameViewport::getHostWindow() const {
return layoutViewport().getHostWindow();
}
void RootFrameViewport::serviceScrollAnimations(double monotonicTime) {
ScrollableArea::serviceScrollAnimations(monotonicTime);
layoutViewport().serviceScrollAnimations(monotonicTime);
visualViewport().serviceScrollAnimations(monotonicTime);
}
void RootFrameViewport::updateCompositorScrollAnimations() {
ScrollableArea::updateCompositorScrollAnimations();
layoutViewport().updateCompositorScrollAnimations();
visualViewport().updateCompositorScrollAnimations();
}
void RootFrameViewport::cancelProgrammaticScrollAnimation() {
ScrollableArea::cancelProgrammaticScrollAnimation();
layoutViewport().cancelProgrammaticScrollAnimation();
visualViewport().cancelProgrammaticScrollAnimation();
}
Widget* RootFrameViewport::getWidget() {
return visualViewport().getWidget();
}
void RootFrameViewport::clearScrollAnimators() {
ScrollableArea::clearScrollAnimators();
layoutViewport().clearScrollAnimators();
visualViewport().clearScrollAnimators();
}
DEFINE_TRACE(RootFrameViewport) {
visitor->trace(m_visualViewport);
visitor->trace(m_layoutViewport);
ScrollableArea::trace(visitor);
}
} // namespace blink