blob: 29a9fb74365fc646a1c95f799759c656f2cb6ab0 [file] [log] [blame]
/*
* Copyright (c) 2010, Google Inc. All rights reserved.
* Copyright (C) 2008, 2011 Apple Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/core/scroll/scrollable_area.h"
#include "build/build_config.h"
#include "cc/input/main_thread_scrolling_reason.h"
#include "cc/input/scroll_utils.h"
#include "cc/input/scrollbar.h"
#include "cc/input/snap_selection_strategy.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/core/animation/scroll_timeline.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/visual_viewport.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/layout/layout_shift_tracker.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
#include "third_party/blink/renderer/core/paint/paint_timing_detector.h"
#include "third_party/blink/renderer/core/scroll/programmatic_scroll_animator.h"
#include "third_party/blink/renderer/core/scroll/scroll_animator_base.h"
#include "third_party/blink/renderer/core/scroll/scrollbar_theme.h"
#include "third_party/blink/renderer/core/scroll/smooth_scroll_sequencer.h"
#include "third_party/blink/renderer/platform/graphics/graphics_layer.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
namespace blink {
int ScrollableArea::PixelsPerLineStep(LocalFrame* frame) {
if (!frame)
return cc::kPixelsPerLineStep;
return frame->GetPage()->GetChromeClient().WindowToViewportScalar(
frame, cc::kPixelsPerLineStep);
}
float ScrollableArea::MinFractionToStepWhenPaging() {
return cc::kMinFractionToStepWhenPaging;
}
int ScrollableArea::MaxOverlapBetweenPages() const {
return GetPageScrollbarTheme().MaxOverlapBetweenPages();
}
// static
float ScrollableArea::DirectionBasedScrollDelta(ScrollGranularity granularity) {
return (granularity == ScrollGranularity::kScrollByPercentage)
? cc::kPercentDeltaForDirectionalScroll
: 1;
}
// static
mojom::blink::ScrollBehavior ScrollableArea::DetermineScrollBehavior(
mojom::blink::ScrollBehavior behavior_from_param,
mojom::blink::ScrollBehavior behavior_from_style) {
if (behavior_from_param == mojom::blink::ScrollBehavior::kSmooth)
return mojom::blink::ScrollBehavior::kSmooth;
if (behavior_from_param == mojom::blink::ScrollBehavior::kAuto &&
behavior_from_style == mojom::blink::ScrollBehavior::kSmooth) {
return mojom::blink::ScrollBehavior::kSmooth;
}
return mojom::blink::ScrollBehavior::kInstant;
}
ScrollableArea::ScrollableArea()
: scrollbar_overlay_color_theme_(kScrollbarOverlayColorThemeDark),
horizontal_scrollbar_needs_paint_invalidation_(false),
vertical_scrollbar_needs_paint_invalidation_(false),
scroll_corner_needs_paint_invalidation_(false),
scrollbars_hidden_if_overlay_(true),
scrollbar_captured_(false),
mouse_over_scrollbar_(false),
has_been_disposed_(false),
needs_show_scrollbar_layers_(false),
uses_composited_scrolling_(false) {}
ScrollableArea::~ScrollableArea() = default;
void ScrollableArea::Dispose() {
if (HasBeenDisposed())
return;
DisposeImpl();
fade_overlay_scrollbars_timer_.reset();
has_been_disposed_ = true;
}
void ScrollableArea::ClearScrollableArea() {
#if defined(OS_MAC)
if (scroll_animator_)
scroll_animator_->Dispose();
#endif
scroll_animator_.Clear();
programmatic_scroll_animator_.Clear();
if (fade_overlay_scrollbars_timer_)
fade_overlay_scrollbars_timer_->Stop();
}
ScrollAnimatorBase& ScrollableArea::GetScrollAnimator() const {
if (!scroll_animator_)
scroll_animator_ =
ScrollAnimatorBase::Create(const_cast<ScrollableArea*>(this));
return *scroll_animator_;
}
ProgrammaticScrollAnimator& ScrollableArea::GetProgrammaticScrollAnimator()
const {
if (!programmatic_scroll_animator_) {
programmatic_scroll_animator_ =
MakeGarbageCollected<ProgrammaticScrollAnimator>(
const_cast<ScrollableArea*>(this));
}
return *programmatic_scroll_animator_;
}
ScrollbarOrientation ScrollableArea::ScrollbarOrientationFromDirection(
ScrollDirectionPhysical direction) const {
return (direction == kScrollUp || direction == kScrollDown)
? kVerticalScrollbar
: kHorizontalScrollbar;
}
float ScrollableArea::ScrollStep(ScrollGranularity granularity,
ScrollbarOrientation orientation) const {
switch (granularity) {
case ScrollGranularity::kScrollByLine:
return LineStep(orientation);
case ScrollGranularity::kScrollByPage:
return PageStep(orientation);
case ScrollGranularity::kScrollByDocument:
return DocumentStep(orientation);
case ScrollGranularity::kScrollByPixel:
case ScrollGranularity::kScrollByPrecisePixel:
return PixelStep(orientation);
case ScrollGranularity::kScrollByPercentage:
return PercentageStep(orientation);
default:
NOTREACHED();
return 0.0f;
}
}
ScrollOffset ScrollableArea::ResolveScrollDelta(ScrollGranularity granularity,
const ScrollOffset& delta) {
gfx::SizeF step(ScrollStep(granularity, kHorizontalScrollbar),
ScrollStep(granularity, kVerticalScrollbar));
if (granularity == ScrollGranularity::kScrollByPercentage) {
LocalFrame* local_frame = GetLayoutBox()->GetFrame();
DCHECK(local_frame);
gfx::SizeF viewport = gfx::SizeF(
FloatSize(local_frame->GetPage()->GetVisualViewport().Size()));
// Convert to screen coordinates (physical pixels).
float page_scale_factor = local_frame->GetPage()->PageScaleFactor();
step.Scale(page_scale_factor);
gfx::Vector2dF pixel_delta =
cc::ScrollUtils::ResolveScrollPercentageToPixels(gfx::Vector2dF(delta),
step, viewport);
// Rescale back to rootframe coordinates.
pixel_delta.Scale(1 / page_scale_factor);
return ScrollOffset(pixel_delta.x(), pixel_delta.y());
}
return delta.ScaledBy(step.width(), step.height());
}
ScrollResult ScrollableArea::UserScroll(ScrollGranularity granularity,
const ScrollOffset& delta,
ScrollCallback on_finish) {
TRACE_EVENT2("input", "ScrollableArea::UserScroll", "x", delta.Width(), "y",
delta.Height());
if (on_finish)
RegisterScrollCompleteCallback(std::move(on_finish));
base::ScopedClosureRunner run_on_return(WTF::Bind(
&ScrollableArea::RunScrollCompleteCallbacks, WrapWeakPersistent(this)));
ScrollOffset pixel_delta = ResolveScrollDelta(granularity, delta);
ScrollOffset scrollable_axis_delta(
UserInputScrollable(kHorizontalScrollbar) ? pixel_delta.Width() : 0,
UserInputScrollable(kVerticalScrollbar) ? pixel_delta.Height() : 0);
if (scrollable_axis_delta.IsZero()) {
return ScrollResult(false, false, pixel_delta.Width(),
pixel_delta.Height());
}
CancelProgrammaticScrollAnimation();
if (SmoothScrollSequencer* sequencer = GetSmoothScrollSequencer())
sequencer->AbortAnimations();
ScrollResult result = GetScrollAnimator().UserScroll(
granularity, scrollable_axis_delta, run_on_return.Release());
// Delta that wasn't scrolled because the axis is !userInputScrollable
// should count as unusedScrollDelta.
ScrollOffset unscrollable_axis_delta = pixel_delta - scrollable_axis_delta;
result.unused_scroll_delta_x += unscrollable_axis_delta.Width();
result.unused_scroll_delta_y += unscrollable_axis_delta.Height();
return result;
}
void ScrollableArea::SetScrollOffset(const ScrollOffset& offset,
mojom::blink::ScrollType scroll_type,
mojom::blink::ScrollBehavior behavior,
ScrollCallback on_finish) {
if (on_finish)
RegisterScrollCompleteCallback(std::move(on_finish));
base::ScopedClosureRunner run_on_return(WTF::Bind(
&ScrollableArea::RunScrollCompleteCallbacks, WrapWeakPersistent(this)));
if (SmoothScrollSequencer* sequencer = GetSmoothScrollSequencer()) {
if (sequencer->FilterNewScrollOrAbortCurrent(scroll_type)) {
return;
}
}
ScrollOffset clamped_offset = ClampScrollOffset(offset);
if (clamped_offset == GetScrollOffset()) {
return;
}
TRACE_EVENT2("blink", "ScrollableArea::SetScrollOffset", "cur_x",
GetScrollOffset().Width(), "cur_y", GetScrollOffset().Height());
TRACE_EVENT_INSTANT1("blink", "Type", TRACE_EVENT_SCOPE_THREAD, "type",
scroll_type);
TRACE_EVENT_INSTANT1("blink", "Behavior", TRACE_EVENT_SCOPE_THREAD,
"behavior", behavior);
if (behavior == mojom::blink::ScrollBehavior::kAuto)
behavior = ScrollBehaviorStyle();
switch (scroll_type) {
case mojom::blink::ScrollType::kCompositor:
ScrollOffsetChanged(clamped_offset, scroll_type);
break;
case mojom::blink::ScrollType::kClamping:
GetScrollAnimator().AdjustAnimationAndSetScrollOffset(clamped_offset,
scroll_type);
break;
case mojom::blink::ScrollType::kAnchoring:
GetScrollAnimator().AdjustAnimationAndSetScrollOffset(clamped_offset,
scroll_type);
break;
case mojom::blink::ScrollType::kProgrammatic:
ProgrammaticScrollHelper(clamped_offset, behavior, false,
run_on_return.Release());
break;
case mojom::blink::ScrollType::kSequenced:
ProgrammaticScrollHelper(clamped_offset, behavior, true,
run_on_return.Release());
break;
case mojom::blink::ScrollType::kUser:
UserScrollHelper(clamped_offset, behavior);
break;
default:
NOTREACHED();
}
}
void ScrollableArea::SetScrollOffset(const ScrollOffset& offset,
mojom::blink::ScrollType type,
mojom::blink::ScrollBehavior behavior) {
SetScrollOffset(offset, type, behavior, ScrollCallback());
}
void ScrollableArea::ScrollBy(const ScrollOffset& delta,
mojom::blink::ScrollType type,
mojom::blink::ScrollBehavior behavior) {
SetScrollOffset(GetScrollOffset() + delta, type, behavior);
}
void ScrollableArea::ProgrammaticScrollHelper(
const ScrollOffset& offset,
mojom::blink::ScrollBehavior scroll_behavior,
bool is_sequenced_scroll,
ScrollCallback on_finish) {
CancelScrollAnimation();
ScrollCallback callback = std::move(on_finish);
callback = ScrollCallback(WTF::Bind(
[](ScrollCallback original_callback,
WeakPersistent<ScrollableArea> area) {
if (area)
area->OnScrollFinished();
if (original_callback)
std::move(original_callback).Run();
},
std::move(callback), WrapWeakPersistent(this)));
if (scroll_behavior == mojom::blink::ScrollBehavior::kSmooth &&
ScrollAnimatorEnabled()) {
GetProgrammaticScrollAnimator().AnimateToOffset(offset, is_sequenced_scroll,
std::move(callback));
} else {
GetProgrammaticScrollAnimator().ScrollToOffsetWithoutAnimation(
offset, is_sequenced_scroll);
if (callback)
std::move(callback).Run();
}
}
void ScrollableArea::UserScrollHelper(
const ScrollOffset& offset,
mojom::blink::ScrollBehavior scroll_behavior) {
CancelProgrammaticScrollAnimation();
if (SmoothScrollSequencer* sequencer = GetSmoothScrollSequencer())
sequencer->AbortAnimations();
float x = UserInputScrollable(kHorizontalScrollbar)
? offset.Width()
: GetScrollAnimator().CurrentOffset().Width();
float y = UserInputScrollable(kVerticalScrollbar)
? offset.Height()
: GetScrollAnimator().CurrentOffset().Height();
// Smooth user scrolls (keyboard, wheel clicks) are handled via the userScroll
// method.
// TODO(bokan): The userScroll method should probably be modified to call this
// method and ScrollAnimatorBase to have a simpler
// animateToOffset method like the ProgrammaticScrollAnimator.
DCHECK_EQ(scroll_behavior, mojom::blink::ScrollBehavior::kInstant);
GetScrollAnimator().ScrollToOffsetWithoutAnimation(ScrollOffset(x, y));
}
PhysicalRect ScrollableArea::ScrollIntoView(
const PhysicalRect& rect_in_absolute,
const mojom::blink::ScrollIntoViewParamsPtr& params) {
// TODO(bokan): This should really be implemented here but ScrollAlignment is
// in Core which is a dependency violation.
NOTREACHED();
return PhysicalRect();
}
void ScrollableArea::ScrollOffsetChanged(const ScrollOffset& offset,
mojom::blink::ScrollType scroll_type) {
TRACE_EVENT2("input", "ScrollableArea::scrollOffsetChanged", "x",
offset.Width(), "y", offset.Height());
TRACE_EVENT_INSTANT1("input", "Type", TRACE_EVENT_SCOPE_THREAD, "type",
scroll_type);
ScrollOffset old_offset = GetScrollOffset();
ScrollOffset truncated_offset = ShouldUseIntegerScrollOffset()
? ScrollOffset(FlooredIntSize(offset))
: offset;
// Tell the derived class to scroll its contents.
UpdateScrollOffset(truncated_offset, scroll_type);
// If the layout object has been detached as a result of updating the scroll
// this object will be cleaned up shortly.
if (HasBeenDisposed())
return;
// Tell the scrollbars to update their thumb postions.
// If the scrollbar does not have its own layer, it must always be
// invalidated to reflect the new thumb offset, even if the theme did not
// invalidate any individual part.
if (Scrollbar* horizontal_scrollbar = this->HorizontalScrollbar())
horizontal_scrollbar->OffsetDidChange(scroll_type);
if (Scrollbar* vertical_scrollbar = this->VerticalScrollbar())
vertical_scrollbar->OffsetDidChange(scroll_type);
ScrollOffset delta = GetScrollOffset() - old_offset;
// TODO(skobes): Should we exit sooner when the offset has not changed?
bool offset_changed = !delta.IsZero();
if (offset_changed) {
GetScrollAnimator().NotifyContentAreaScrolled(
GetScrollOffset() - old_offset, scroll_type);
}
if (GetLayoutBox()) {
if (offset_changed && GetLayoutBox()->GetFrameView() &&
GetLayoutBox()
->GetFrameView()
->GetPaintTimingDetector()
.NeedToNotifyInputOrScroll()) {
GetLayoutBox()->GetFrameView()->GetPaintTimingDetector().NotifyScroll(
scroll_type);
}
}
if (offset_changed && GetLayoutBox() && GetLayoutBox()->GetFrameView()) {
GetLayoutBox()->GetFrameView()->GetLayoutShiftTracker().NotifyScroll(
scroll_type, delta);
}
GetScrollAnimator().SetCurrentOffset(offset);
}
bool ScrollableArea::ScrollBehaviorFromString(
const String& behavior_string,
mojom::blink::ScrollBehavior& behavior) {
if (behavior_string == "auto")
behavior = mojom::blink::ScrollBehavior::kAuto;
else if (behavior_string == "instant")
behavior = mojom::blink::ScrollBehavior::kInstant;
else if (behavior_string == "smooth")
behavior = mojom::blink::ScrollBehavior::kSmooth;
else
return false;
return true;
}
// NOTE: Only called from Internals for testing.
void ScrollableArea::UpdateScrollOffsetFromInternals(const IntSize& offset) {
ScrollOffsetChanged(ScrollOffset(offset),
mojom::blink::ScrollType::kProgrammatic);
}
void ScrollableArea::RegisterScrollCompleteCallback(ScrollCallback callback) {
DCHECK(!HasBeenDisposed());
pending_scroll_complete_callbacks_.push_back(std::move(callback));
}
void ScrollableArea::RunScrollCompleteCallbacks() {
Vector<ScrollCallback> callbacks(
std::move(pending_scroll_complete_callbacks_));
for (auto& callback : callbacks)
std::move(callback).Run();
}
void ScrollableArea::ContentAreaWillPaint() const {
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->ContentAreaWillPaint();
}
void ScrollableArea::MouseEnteredContentArea() const {
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->MouseEnteredContentArea();
}
void ScrollableArea::MouseExitedContentArea() const {
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->MouseExitedContentArea();
}
void ScrollableArea::MouseMovedInContentArea() const {
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->MouseMovedInContentArea();
}
void ScrollableArea::MouseEnteredScrollbar(Scrollbar& scrollbar) {
mouse_over_scrollbar_ = true;
GetScrollAnimator().MouseEnteredScrollbar(scrollbar);
ShowNonMacOverlayScrollbars();
if (fade_overlay_scrollbars_timer_)
fade_overlay_scrollbars_timer_->Stop();
}
void ScrollableArea::MouseExitedScrollbar(Scrollbar& scrollbar) {
mouse_over_scrollbar_ = false;
GetScrollAnimator().MouseExitedScrollbar(scrollbar);
if (HasOverlayScrollbars() && !scrollbars_hidden_if_overlay_) {
// This will kick off the fade out timer.
ShowNonMacOverlayScrollbars();
}
}
void ScrollableArea::MouseCapturedScrollbar() {
scrollbar_captured_ = true;
ShowNonMacOverlayScrollbars();
if (fade_overlay_scrollbars_timer_)
fade_overlay_scrollbars_timer_->Stop();
}
void ScrollableArea::MouseReleasedScrollbar() {
scrollbar_captured_ = false;
// This will kick off the fade out timer.
ShowNonMacOverlayScrollbars();
}
void ScrollableArea::ContentAreaDidShow() const {
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->ContentAreaDidShow();
}
void ScrollableArea::ContentAreaDidHide() const {
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->ContentAreaDidHide();
}
void ScrollableArea::FinishCurrentScrollAnimations() const {
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->FinishCurrentScrollAnimations();
}
void ScrollableArea::DidAddScrollbar(Scrollbar& scrollbar,
ScrollbarOrientation orientation) {
if (orientation == kVerticalScrollbar)
GetScrollAnimator().DidAddVerticalScrollbar(scrollbar);
else
GetScrollAnimator().DidAddHorizontalScrollbar(scrollbar);
// <rdar://problem/9797253> AppKit resets the scrollbar's style when you
// attach a scrollbar
SetScrollbarOverlayColorTheme(GetScrollbarOverlayColorTheme());
}
void ScrollableArea::WillRemoveScrollbar(Scrollbar& scrollbar,
ScrollbarOrientation orientation) {
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator()) {
if (orientation == kVerticalScrollbar)
scroll_animator->WillRemoveVerticalScrollbar(scrollbar);
else
scroll_animator->WillRemoveHorizontalScrollbar(scrollbar);
}
}
void ScrollableArea::ContentsResized() {
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->ContentsResized();
}
void ScrollableArea::InvalidateScrollTimeline() {
if (auto* layout_box = GetLayoutBox()) {
if (auto* node = layout_box->GetNode())
ScrollTimeline::Invalidate(node);
}
}
bool ScrollableArea::HasOverlayScrollbars() const {
Scrollbar* v_scrollbar = VerticalScrollbar();
if (v_scrollbar && v_scrollbar->IsOverlayScrollbar())
return true;
Scrollbar* h_scrollbar = HorizontalScrollbar();
return h_scrollbar && h_scrollbar->IsOverlayScrollbar();
}
void ScrollableArea::SetScrollbarOverlayColorTheme(
ScrollbarOverlayColorTheme overlay_theme) {
scrollbar_overlay_color_theme_ = overlay_theme;
if (Scrollbar* scrollbar = HorizontalScrollbar()) {
GetPageScrollbarTheme().UpdateScrollbarOverlayColorTheme(*scrollbar);
scrollbar->SetNeedsPaintInvalidation(kAllParts);
}
if (Scrollbar* scrollbar = VerticalScrollbar()) {
GetPageScrollbarTheme().UpdateScrollbarOverlayColorTheme(*scrollbar);
scrollbar->SetNeedsPaintInvalidation(kAllParts);
}
}
void ScrollableArea::RecalculateScrollbarOverlayColorTheme(
Color background_color) {
ScrollbarOverlayColorTheme old_overlay_theme =
GetScrollbarOverlayColorTheme();
ScrollbarOverlayColorTheme overlay_theme = kScrollbarOverlayColorThemeDark;
// Reduce the background color from RGB to a lightness value
// and determine which scrollbar style to use based on a lightness
// heuristic.
double hue, saturation, lightness;
background_color.GetHSL(hue, saturation, lightness);
if (lightness <= .5 && background_color.Alpha())
overlay_theme = kScrollbarOverlayColorThemeLight;
if (old_overlay_theme != overlay_theme)
SetScrollbarOverlayColorTheme(overlay_theme);
}
void ScrollableArea::SetScrollbarNeedsPaintInvalidation(
ScrollbarOrientation orientation) {
if (orientation == kHorizontalScrollbar) {
if (cc::Layer* layer = LayerForHorizontalScrollbar())
layer->SetNeedsDisplay();
horizontal_scrollbar_needs_paint_invalidation_ = true;
} else {
if (cc::Layer* layer = LayerForVerticalScrollbar())
layer->SetNeedsDisplay();
vertical_scrollbar_needs_paint_invalidation_ = true;
}
ScrollControlWasSetNeedsPaintInvalidation();
}
void ScrollableArea::SetScrollCornerNeedsPaintInvalidation() {
if (cc::Layer* layer = LayerForScrollCorner()) {
layer->SetNeedsDisplay();
return;
}
scroll_corner_needs_paint_invalidation_ = true;
ScrollControlWasSetNeedsPaintInvalidation();
}
bool ScrollableArea::HasLayerForHorizontalScrollbar() const {
return LayerForHorizontalScrollbar();
}
bool ScrollableArea::HasLayerForVerticalScrollbar() const {
return LayerForVerticalScrollbar();
}
bool ScrollableArea::HasLayerForScrollCorner() const {
return LayerForScrollCorner();
}
void ScrollableArea::LayerForScrollingDidChange(
CompositorAnimationTimeline* timeline) {
if (ProgrammaticScrollAnimator* programmatic_scroll_animator =
ExistingProgrammaticScrollAnimator())
programmatic_scroll_animator->LayerForCompositedScrollingDidChange(
timeline);
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->LayerForCompositedScrollingDidChange(timeline);
}
void ScrollableArea::ServiceScrollAnimations(double monotonic_time) {
bool requires_animation_service = false;
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator()) {
scroll_animator->TickAnimation(monotonic_time);
if (scroll_animator->HasAnimationThatRequiresService())
requires_animation_service = true;
}
if (ProgrammaticScrollAnimator* programmatic_scroll_animator =
ExistingProgrammaticScrollAnimator()) {
programmatic_scroll_animator->TickAnimation(monotonic_time);
if (programmatic_scroll_animator->HasAnimationThatRequiresService())
requires_animation_service = true;
}
if (!requires_animation_service)
DeregisterForAnimation();
}
void ScrollableArea::UpdateCompositorScrollAnimations() {
if (ProgrammaticScrollAnimator* programmatic_scroll_animator =
ExistingProgrammaticScrollAnimator())
programmatic_scroll_animator->UpdateCompositorAnimations();
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->UpdateCompositorAnimations();
}
void ScrollableArea::CancelScrollAnimation() {
if (ScrollAnimatorBase* scroll_animator = ExistingScrollAnimator())
scroll_animator->CancelAnimation();
}
void ScrollableArea::CancelProgrammaticScrollAnimation() {
if (ProgrammaticScrollAnimator* programmatic_scroll_animator =
ExistingProgrammaticScrollAnimator())
programmatic_scroll_animator->CancelAnimation();
}
bool ScrollableArea::ScrollbarsHiddenIfOverlay() const {
return HasOverlayScrollbars() && scrollbars_hidden_if_overlay_;
}
void ScrollableArea::SetScrollbarsHiddenForTesting(bool hidden) {
// If scrollable area has been disposed, we can not get the page scrollbar
// theme setting. Should early return here.
if (HasBeenDisposed())
return;
SetScrollbarsHiddenIfOverlayInternal(hidden);
}
void ScrollableArea::SetScrollbarsHiddenFromExternalAnimator(bool hidden) {
// If scrollable area has been disposed, we can not get the page scrollbar
// theme setting. Should early return here.
if (HasBeenDisposed())
return;
DCHECK(!GetPageScrollbarTheme().BlinkControlsOverlayVisibility());
SetScrollbarsHiddenIfOverlayInternal(hidden);
}
void ScrollableArea::SetScrollbarsHiddenIfOverlay(bool hidden) {
// If scrollable area has been disposed, we can not get the page scrollbar
// theme setting. Should early return here.
if (HasBeenDisposed())
return;
DCHECK(GetPageScrollbarTheme().BlinkControlsOverlayVisibility());
SetScrollbarsHiddenIfOverlayInternal(hidden);
}
void ScrollableArea::SetScrollbarsHiddenIfOverlayInternal(bool hidden) {
if (!GetPageScrollbarTheme().UsesOverlayScrollbars())
return;
if (scrollbars_hidden_if_overlay_ == static_cast<unsigned>(hidden))
return;
scrollbars_hidden_if_overlay_ = hidden;
ScrollbarVisibilityChanged();
}
void ScrollableArea::FadeOverlayScrollbarsTimerFired(TimerBase*) {
SetScrollbarsHiddenIfOverlay(true);
}
void ScrollableArea::ShowNonMacOverlayScrollbars() {
if (!GetPageScrollbarTheme().UsesOverlayScrollbars() ||
!GetPageScrollbarTheme().BlinkControlsOverlayVisibility())
return;
// Don't do this for composited scrollbars. These scrollbars are handled
// by separate code in cc::ScrollbarAnimationController.
if (LayerForVerticalScrollbar() || LayerForHorizontalScrollbar())
return;
SetScrollbarsHiddenIfOverlay(false);
needs_show_scrollbar_layers_ = true;
const base::TimeDelta time_until_disable =
GetPageScrollbarTheme().OverlayScrollbarFadeOutDelay() +
GetPageScrollbarTheme().OverlayScrollbarFadeOutDuration();
// If the overlay scrollbars don't fade out, don't do anything. This is the
// case for the mock overlays used in tests (and also Mac but its scrollbars
// are animated by OS APIs and so we've already early-out'ed above). We also
// don't fade out overlay scrollbar for popup since we don't create
// compositor for popup and thus they don't appear on hover so users without
// a wheel can't scroll if they fade out.
if (time_until_disable.is_zero() || GetChromeClient()->IsPopup())
return;
if (!fade_overlay_scrollbars_timer_) {
fade_overlay_scrollbars_timer_.reset(new TaskRunnerTimer<ScrollableArea>(
ThreadScheduler::Current()->CompositorTaskRunner(), this,
&ScrollableArea::FadeOverlayScrollbarsTimerFired));
}
if (!scrollbar_captured_ && !mouse_over_scrollbar_) {
fade_overlay_scrollbars_timer_->StartOneShot(time_until_disable, FROM_HERE);
}
}
Node* ScrollableArea::EventTargetNode() const {
const LayoutBox* box = GetLayoutBox();
Node* node = box->GetNode();
if (!node && box->Parent() && box->Parent()->IsLayoutNGFieldset())
node = box->Parent()->GetNode();
if (node && IsA<Element>(node))
DCHECK_EQ(box, To<Element>(node)->GetLayoutBoxForScrolling());
return node;
}
const Document* ScrollableArea::GetDocument() const {
if (auto* box = GetLayoutBox())
return &box->GetDocument();
return nullptr;
}
IntSize ScrollableArea::ClampScrollOffset(const IntSize& scroll_offset) const {
return scroll_offset.ShrunkTo(MaximumScrollOffsetInt())
.ExpandedTo(MinimumScrollOffsetInt());
}
ScrollOffset ScrollableArea::ClampScrollOffset(
const ScrollOffset& scroll_offset) const {
return scroll_offset.ShrunkTo(MaximumScrollOffset())
.ExpandedTo(MinimumScrollOffset());
}
int ScrollableArea::LineStep(ScrollbarOrientation) const {
return PixelsPerLineStep(GetLayoutBox()->GetFrame());
}
int ScrollableArea::PageStep(ScrollbarOrientation orientation) const {
// Paging scroll operations should take scroll-padding into account [1]. So we
// use the snapport rect to calculate the page step instead of the visible
// rect.
// [1] https://drafts.csswg.org/css-scroll-snap/#scroll-padding
IntSize snapport_size =
VisibleScrollSnapportRect(kExcludeScrollbars).PixelSnappedSize();
int length = (orientation == kHorizontalScrollbar) ? snapport_size.Width()
: snapport_size.Height();
int min_page_step =
static_cast<float>(length) * MinFractionToStepWhenPaging();
int page_step = std::max(min_page_step, length - MaxOverlapBetweenPages());
return std::max(page_step, 1);
}
int ScrollableArea::DocumentStep(ScrollbarOrientation orientation) const {
return ScrollSize(orientation);
}
float ScrollableArea::PixelStep(ScrollbarOrientation) const {
return 1;
}
float ScrollableArea::PercentageStep(ScrollbarOrientation orientation) const {
int percent_basis =
(orientation == ScrollbarOrientation::kHorizontalScrollbar)
? VisibleWidth()
: VisibleHeight();
return static_cast<float>(percent_basis);
}
int ScrollableArea::VerticalScrollbarWidth(
OverlayScrollbarClipBehavior behavior) const {
DCHECK_EQ(behavior, kIgnoreOverlayScrollbarSize);
if (Scrollbar* vertical_bar = VerticalScrollbar())
return !vertical_bar->IsOverlayScrollbar() ? vertical_bar->Width() : 0;
return 0;
}
int ScrollableArea::HorizontalScrollbarHeight(
OverlayScrollbarClipBehavior behavior) const {
DCHECK_EQ(behavior, kIgnoreOverlayScrollbarSize);
if (Scrollbar* horizontal_bar = HorizontalScrollbar())
return !horizontal_bar->IsOverlayScrollbar() ? horizontal_bar->Height() : 0;
return 0;
}
FloatQuad ScrollableArea::LocalToVisibleContentQuad(const FloatQuad& quad,
const LayoutObject*,
unsigned) const {
FloatQuad result(quad);
result.Move(-GetScrollOffset());
return result;
}
IntSize ScrollableArea::ExcludeScrollbars(const IntSize& size) const {
return IntSize(std::max(0, size.Width() - VerticalScrollbarWidth()),
std::max(0, size.Height() - HorizontalScrollbarHeight()));
}
void ScrollableArea::DidScroll(const FloatPoint& position) {
ScrollOffset new_offset(ScrollPositionToOffset(position));
SetScrollOffset(new_offset, mojom::blink::ScrollType::kCompositor);
}
CompositorElementId ScrollableArea::GetScrollbarElementId(
ScrollbarOrientation orientation) {
CompositorElementId scrollable_element_id = GetScrollElementId();
DCHECK(scrollable_element_id);
CompositorElementIdNamespace element_id_namespace =
orientation == kHorizontalScrollbar
? CompositorElementIdNamespace::kHorizontalScrollbar
: CompositorElementIdNamespace::kVerticalScrollbar;
return CompositorElementIdFromUniqueObjectId(
scrollable_element_id.GetStableId(), element_id_namespace);
}
void ScrollableArea::OnScrollFinished() {
if (GetLayoutBox()) {
if (RuntimeEnabledFeatures::OverscrollCustomizationEnabled()) {
if (Node* node = EventTargetNode())
node->GetDocument().EnqueueScrollEndEventForNode(node);
}
GetLayoutBox()
->GetFrame()
->LocalFrameRoot()
.GetEventHandler()
.MarkHoverStateDirty();
}
}
void ScrollableArea::SnapAfterScrollbarScrolling(
ScrollbarOrientation orientation) {
SnapAtCurrentPosition(orientation == kHorizontalScrollbar,
orientation == kVerticalScrollbar);
}
bool ScrollableArea::SnapAtCurrentPosition(
bool scrolled_x,
bool scrolled_y,
base::ScopedClosureRunner on_finish) {
DCHECK(IsRootFrameViewport() || !GetLayoutBox()->IsGlobalRootScroller());
FloatPoint current_position = ScrollPosition();
return SnapForEndPosition(current_position, scrolled_x, scrolled_y,
std::move(on_finish));
}
bool ScrollableArea::SnapForEndPosition(const FloatPoint& end_position,
bool scrolled_x,
bool scrolled_y,
base::ScopedClosureRunner on_finish) {
DCHECK(IsRootFrameViewport() || !GetLayoutBox()->IsGlobalRootScroller());
std::unique_ptr<cc::SnapSelectionStrategy> strategy =
cc::SnapSelectionStrategy::CreateForEndPosition(
gfx::ScrollOffset(end_position), scrolled_x, scrolled_y);
return PerformSnapping(*strategy, mojom::blink::ScrollBehavior::kSmooth,
std::move(on_finish));
}
bool ScrollableArea::SnapForDirection(const ScrollOffset& delta,
base::ScopedClosureRunner on_finish) {
DCHECK(IsRootFrameViewport() || !GetLayoutBox()->IsGlobalRootScroller());
FloatPoint current_position = ScrollPosition();
std::unique_ptr<cc::SnapSelectionStrategy> strategy =
cc::SnapSelectionStrategy::CreateForDirection(
gfx::ScrollOffset(current_position),
gfx::ScrollOffset(delta.Width(), delta.Height()),
RuntimeEnabledFeatures::FractionalScrollOffsetsEnabled());
return PerformSnapping(*strategy, mojom::blink::ScrollBehavior::kSmooth,
std::move(on_finish));
}
bool ScrollableArea::SnapForEndAndDirection(const ScrollOffset& delta) {
DCHECK(IsRootFrameViewport() || !GetLayoutBox()->IsGlobalRootScroller());
FloatPoint current_position = ScrollPosition();
std::unique_ptr<cc::SnapSelectionStrategy> strategy =
cc::SnapSelectionStrategy::CreateForEndAndDirection(
gfx::ScrollOffset(current_position),
gfx::ScrollOffset(delta.Width(), delta.Height()),
RuntimeEnabledFeatures::FractionalScrollOffsetsEnabled());
return PerformSnapping(*strategy);
}
void ScrollableArea::SnapAfterLayout() {
const cc::SnapContainerData* container_data = GetSnapContainerData();
if (!container_data || !container_data->size())
return;
FloatPoint current_position = ScrollPosition();
std::unique_ptr<cc::SnapSelectionStrategy> strategy =
cc::SnapSelectionStrategy::CreateForTargetElement(
gfx::ScrollOffset(current_position));
PerformSnapping(*strategy, mojom::blink::ScrollBehavior::kInstant);
}
bool ScrollableArea::PerformSnapping(
const cc::SnapSelectionStrategy& strategy,
mojom::blink::ScrollBehavior scroll_behavior,
base::ScopedClosureRunner on_finish) {
base::Optional<FloatPoint> snap_point = GetSnapPositionAndSetTarget(strategy);
if (!snap_point)
return false;
CancelScrollAnimation();
CancelProgrammaticScrollAnimation();
SetScrollOffset(ScrollPositionToOffset(snap_point.value()),
mojom::blink::ScrollType::kProgrammatic, scroll_behavior,
on_finish.Release());
return true;
}
void ScrollableArea::Trace(Visitor* visitor) const {
visitor->Trace(scroll_animator_);
visitor->Trace(programmatic_scroll_animator_);
}
void ScrollableArea::InjectGestureScrollEvent(
WebGestureDevice device,
ScrollOffset delta,
ScrollGranularity granularity,
WebInputEvent::Type gesture_type) const {
// All ScrollableArea's have a layout box, except for the VisualViewport.
// We shouldn't be injecting scrolls for the visual viewport scrollbar, since
// it is not hit-testable.
DCHECK(GetLayoutBox());
if (granularity == ScrollGranularity::kScrollByPrecisePixel ||
granularity == ScrollGranularity::kScrollByPixel) {
// Pixel-based deltas need to be scaled up by the input event scale factor,
// since the GSUs will be scaled down by that factor when being handled.
float scale = 1;
LocalFrameView* root_view =
GetLayoutBox()->GetFrame()->LocalFrameRoot().View();
if (root_view)
scale = root_view->InputEventsScaleFactor();
delta.Scale(scale);
}
GetChromeClient()->InjectGestureScrollEvent(
*GetLayoutBox()->GetFrame(), device,
gfx::Vector2dF(delta.Width(), delta.Height()), granularity,
GetScrollElementId(), gesture_type);
}
ScrollableArea* ScrollableArea::GetForScrolling(const LayoutBox* layout_box) {
if (!layout_box)
return nullptr;
if (!layout_box->IsGlobalRootScroller()) {
if (const auto* element = DynamicTo<Element>(layout_box->GetNode())) {
if (auto* scrolling_box = element->GetLayoutBoxForScrolling())
return scrolling_box->GetScrollableArea();
}
return layout_box->GetScrollableArea();
}
// The global root scroller should be scrolled by the root frame view's
// ScrollableArea.
LocalFrame& root_frame = layout_box->GetFrame()->LocalFrameRoot();
return root_frame.View()->GetScrollableArea();
}
float ScrollableArea::ScaleFromDIP() const {
auto* client = GetChromeClient();
auto* document = GetDocument();
if (client && document)
return client->WindowToViewportScalar(document->GetFrame(), 1.0f);
return 1.0f;
}
} // namespace blink