// Copyright (c) 2012 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 "content/browser/renderer_host/overscroll_controller.h" #include #include "base/check_op.h" #include "base/command_line.h" #include "base/notreached.h" #include "base/numerics/ranges.h" #include "content/browser/renderer_host/overscroll_controller_delegate.h" #include "content/public/browser/overscroll_configuration.h" #include "content/public/common/content_features.h" #include "content/public/common/content_switches.h" namespace content { namespace { // Minimum amount of time after an actual scroll after which a pull-to-refresh // can start. constexpr base::TimeDelta kPullToRefreshCoolOffDelay = base::TimeDelta::FromMilliseconds(600); bool IsGestureEventFromTouchpad(const blink::WebInputEvent& event) { DCHECK(blink::WebInputEvent::IsGestureEventType(event.GetType())); const blink::WebGestureEvent& gesture = static_cast(event); return gesture.SourceDevice() == blink::WebGestureDevice::kTouchpad; } bool IsGestureEventFromAutoscroll(const blink::WebGestureEvent event) { return event.SourceDevice() == blink::WebGestureDevice::kSyntheticAutoscroll; } bool IsGestureScrollUpdateInertialEvent(const blink::WebInputEvent& event) { if (event.GetType() != blink::WebInputEvent::Type::kGestureScrollUpdate) return false; const blink::WebGestureEvent& gesture = static_cast(event); return gesture.data.scroll_update.inertial_phase == blink::WebGestureEvent::InertialPhaseState::kMomentum; } float ClampAbsoluteValue(float value, float max_abs) { DCHECK_LT(0.f, max_abs); return base::ClampToRange(value, -max_abs, max_abs); } } // namespace OverscrollController::OverscrollController() {} OverscrollController::~OverscrollController() {} bool OverscrollController::ShouldProcessEvent( const blink::WebInputEvent& event) { switch (event.GetType()) { case blink::WebInputEvent::Type::kGestureScrollBegin: case blink::WebInputEvent::Type::kGestureScrollUpdate: case blink::WebInputEvent::Type::kGestureScrollEnd: { const blink::WebGestureEvent& gesture = static_cast(event); // Gesture events with Autoscroll source don't cause overscrolling. if (IsGestureEventFromAutoscroll(gesture)) return false; ui::ScrollGranularity granularity; switch (event.GetType()) { case blink::WebInputEvent::Type::kGestureScrollBegin: granularity = gesture.data.scroll_begin.delta_hint_units; break; case blink::WebInputEvent::Type::kGestureScrollUpdate: granularity = gesture.data.scroll_update.delta_units; break; case blink::WebInputEvent::Type::kGestureScrollEnd: granularity = gesture.data.scroll_end.delta_units; break; default: granularity = ui::ScrollGranularity::kScrollByPixel; break; } return granularity == ui::ScrollGranularity::kScrollByPrecisePixel; } default: break; } return true; } bool OverscrollController::ShouldIgnoreInertialEvent( const blink::WebInputEvent& event) const { return ignore_following_inertial_events_ && IsGestureScrollUpdateInertialEvent(event); } bool OverscrollController::WillHandleEvent(const blink::WebInputEvent& event) { if (!ShouldProcessEvent(event)) return false; // TODO(mohsen): Consider filtering mouse-wheel events during overscroll. See // https://crbug.com/772106. if (event.GetType() == blink::WebInputEvent::Type::kMouseWheel) return false; if (event.GetType() == blink::WebInputEvent::Type::kGestureScrollBegin) { ignore_following_inertial_events_ = false; first_inertial_event_time_.reset(); time_since_last_ignored_scroll_ = event.TimeStamp() - last_ignored_scroll_time_; // Will handle events when processing ACKs to ensure the correct order. return false; } if (event.GetType() == blink::WebInputEvent::Type::kGestureScrollEnd) { if (scroll_state_ == ScrollState::CONTENT_CONSUMING || overscroll_ignored_) { last_ignored_scroll_time_ = event.TimeStamp(); } // Will handle events when processing ACKs to ensure the correct order. return false; } // Consume the scroll-update events if they are from a inertial scroll (fling) // event that completed an overscroll gesture. if (ShouldIgnoreInertialEvent(event)) return true; bool reset_scroll_state = false; if (scroll_state_ != ScrollState::NONE || overscroll_delta_x_ || overscroll_delta_y_) { switch (event.GetType()) { case blink::WebInputEvent::Type::kGestureFlingStart: reset_scroll_state = true; break; default: if (blink::WebInputEvent::IsMouseEventType(event.GetType()) || blink::WebInputEvent::IsKeyboardEventType(event.GetType())) { reset_scroll_state = true; } break; } } if (reset_scroll_state) ResetScrollState(); if (DispatchEventCompletesAction(event)) { CompleteAction(); // Let the event be dispatched to the renderer. return false; } if (overscroll_mode_ != OVERSCROLL_NONE && DispatchEventResetsState(event)) { SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE); // Let the event be dispatched to the renderer. return false; } if (overscroll_mode_ != OVERSCROLL_NONE) { // Consume the event only if it updates the overscroll state. if (ProcessEventForOverscroll(event)) return true; } else if (reset_scroll_state) { overscroll_delta_x_ = overscroll_delta_y_ = 0.f; } // In overscrolling state, consume scroll-update and fling-start events when // they do not contribute to overscroll in order to prevent content scroll. // TODO(bokan): This needs to account for behavior_ somehow since if the page // declares that it doesn't want an overscroll effect, we should allow // sending the scroll update events to generate DOM overscroll events. // https://crbug.com/1112183. return scroll_state_ == ScrollState::OVERSCROLLING && (event.GetType() == blink::WebInputEvent::Type::kGestureScrollUpdate || event.GetType() == blink::WebInputEvent::Type::kGestureFlingStart); } void OverscrollController::OnDidOverscroll( const ui::DidOverscrollParams& params) { // TODO(sunyunjia): We should also decide whether to trigger overscroll, // update scroll_state_ here. See https://crbug.com/799467. behavior_ = params.overscroll_behavior; } void OverscrollController::ReceivedEventACK(const blink::WebInputEvent& event, bool processed) { if (!ShouldProcessEvent(event)) return; // An inertial scroll (fling) event from a completed overscroll gesture // should not modify states below. if (ShouldIgnoreInertialEvent(event)) return; if (processed) { // If a scroll event is consumed by the page, i.e. some content on the page // has been scrolled, then there is not going to be an overscroll gesture, // until the current scroll ends, and a new scroll gesture starts. // Similarly, if a mouse-wheel event is consumed, probably the page has // implemented its own scroll-like behavior and no overscroll should happen. if (scroll_state_ == ScrollState::NONE && (event.GetType() == blink::WebInputEvent::Type::kGestureScrollUpdate || event.GetType() == blink::WebInputEvent::Type::kMouseWheel)) { scroll_state_ = ScrollState::CONTENT_CONSUMING; } // In overscrolling state, only return if we are in an overscroll mode; // otherwise, we would want to ProcessEventForOverscroll to let it start a // new overscroll mode. if (scroll_state_ != ScrollState::OVERSCROLLING || overscroll_mode_ != OVERSCROLL_NONE) { return; } } if (event.GetType() == blink::WebInputEvent::Type::kMouseWheel) return; ProcessEventForOverscroll(event); } void OverscrollController::Reset() { overscroll_mode_ = OVERSCROLL_NONE; overscroll_source_ = OverscrollSource::NONE; overscroll_delta_x_ = overscroll_delta_y_ = 0.f; ResetScrollState(); } void OverscrollController::Cancel() { SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE); overscroll_delta_x_ = overscroll_delta_y_ = 0.f; ResetScrollState(); } bool OverscrollController::DispatchEventCompletesAction( const blink::WebInputEvent& event) const { if (overscroll_mode_ == OVERSCROLL_NONE) return false; DCHECK_NE(OverscrollSource::NONE, overscroll_source_); // Complete the overscroll gesture if there was a mouse move or a scroll-end // after the threshold. if (event.GetType() != blink::WebInputEvent::Type::kMouseMove && event.GetType() != blink::WebInputEvent::Type::kGestureScrollEnd && event.GetType() != blink::WebInputEvent::Type::kGestureFlingStart && event.GetType() != blink::WebInputEvent::Type::kGestureScrollUpdate) return false; // Complete the overscroll gesture for inertial scroll (fling) event from // touchpad. if (event.GetType() == blink::WebInputEvent::Type::kGestureScrollUpdate) { if (overscroll_source_ != OverscrollSource::TOUCHPAD) return false; DCHECK(IsGestureEventFromTouchpad(event)); const blink::WebGestureEvent gesture_event = static_cast(event); if (gesture_event.data.scroll_update.inertial_phase != blink::WebGestureEvent::InertialPhaseState::kMomentum) return false; } if (event.GetType() == blink::WebInputEvent::Type::kGestureScrollEnd && overscroll_source_ == OverscrollSource::TOUCHPAD) { DCHECK(IsGestureEventFromTouchpad(event)); // Complete the action for a GSE with touchpad source only when it is in // momentumPhase. const blink::WebGestureEvent gesture_event = static_cast(event); if (gesture_event.data.scroll_end.inertial_phase != blink::WebGestureEvent::InertialPhaseState::kMomentum) return false; } if (!delegate_) return false; if (event.GetType() == blink::WebInputEvent::Type::kGestureFlingStart) { // Check to see if the fling is in the same direction of the overscroll. const blink::WebGestureEvent gesture = static_cast(event); switch (overscroll_mode_) { case OVERSCROLL_EAST: if (gesture.data.fling_start.velocity_x < 0) return false; break; case OVERSCROLL_WEST: if (gesture.data.fling_start.velocity_x > 0) return false; break; case OVERSCROLL_NORTH: if (gesture.data.fling_start.velocity_y > 0) return false; break; case OVERSCROLL_SOUTH: if (gesture.data.fling_start.velocity_y < 0) return false; break; case OVERSCROLL_NONE: NOTREACHED(); } } const gfx::Size size = delegate_->GetDisplaySize(); if (size.IsEmpty()) return false; const float delta = overscroll_mode_ == OVERSCROLL_WEST || overscroll_mode_ == OVERSCROLL_EAST ? overscroll_delta_x_ : overscroll_delta_y_; const float ratio = fabs(delta) / std::max(size.width(), size.height()); const float threshold = overscroll_source_ == OverscrollSource::TOUCHPAD ? OverscrollConfig::kCompleteTouchpadThresholdPercent : OverscrollConfig::kCompleteTouchscreenThresholdPercent; return ratio >= threshold; } bool OverscrollController::DispatchEventResetsState( const blink::WebInputEvent& event) const { switch (event.GetType()) { // GestureScrollBegin/End ACK will reset overscroll state when necessary. case blink::WebInputEvent::Type::kGestureScrollBegin: case blink::WebInputEvent::Type::kGestureScrollEnd: case blink::WebInputEvent::Type::kGestureScrollUpdate: case blink::WebInputEvent::Type::kGestureFlingCancel: return false; default: // Touch events can arrive during an overscroll gesture initiated by // touch-scrolling. These events should not reset the overscroll state. return !blink::WebInputEvent::IsTouchEventType(event.GetType()); } } bool OverscrollController::ProcessEventForOverscroll( const blink::WebInputEvent& event) { bool event_processed = false; switch (event.GetType()) { case blink::WebInputEvent::Type::kGestureScrollBegin: { if (overscroll_mode_ != OVERSCROLL_NONE) SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE); break; } case blink::WebInputEvent::Type::kGestureScrollEnd: { // Only reset the state on GestureScrollEnd generated from the touchpad // when the scrolling is in inertial state. const blink::WebGestureEvent gesture_event = static_cast(event); bool reset_scroll_state = !IsGestureEventFromTouchpad(event) || (gesture_event.data.scroll_end.inertial_phase == blink::WebGestureEvent::InertialPhaseState::kMomentum); if (reset_scroll_state) ResetScrollState(); if (DispatchEventCompletesAction(event)) { CompleteAction(); break; } if (!reset_scroll_state) break; if (overscroll_mode_ != OVERSCROLL_NONE) { SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE); } else { overscroll_delta_x_ = overscroll_delta_y_ = 0.f; } break; } case blink::WebInputEvent::Type::kGestureScrollUpdate: { const blink::WebGestureEvent& gesture = static_cast(event); bool is_gesture_scroll_update_inertial_event = IsGestureScrollUpdateInertialEvent(event); event_processed = ProcessOverscroll( gesture.data.scroll_update.delta_x, gesture.data.scroll_update.delta_y, gesture.SourceDevice() == blink::WebGestureDevice::kTouchpad, is_gesture_scroll_update_inertial_event); if (is_gesture_scroll_update_inertial_event) { // Record the timestamp of first inertial event. if (!first_inertial_event_time_) { first_inertial_event_time_ = event.TimeStamp(); break; } base::TimeDelta inertial_event_interval = event.TimeStamp() - first_inertial_event_time_.value(); if (inertial_event_interval >= OverscrollConfig::MaxInertialEventsBeforeOverscrollCancellation()) { ignore_following_inertial_events_ = true; // Reset overscroll state if fling didn't complete the overscroll // gesture within the first 20 inertial events. Cancel(); } } break; } case blink::WebInputEvent::Type::kGestureFlingStart: { const float kFlingVelocityThreshold = 1100.f; const blink::WebGestureEvent& gesture = static_cast(event); float velocity_x = gesture.data.fling_start.velocity_x; float velocity_y = gesture.data.fling_start.velocity_y; if (fabs(velocity_x) > kFlingVelocityThreshold) { if ((overscroll_mode_ == OVERSCROLL_WEST && velocity_x < 0) || (overscroll_mode_ == OVERSCROLL_EAST && velocity_x > 0)) { CompleteAction(); event_processed = true; break; } } else if (fabs(velocity_y) > kFlingVelocityThreshold) { if ((overscroll_mode_ == OVERSCROLL_NORTH && velocity_y < 0) || (overscroll_mode_ == OVERSCROLL_SOUTH && velocity_y > 0)) { CompleteAction(); event_processed = true; break; } } // Reset overscroll state if fling didn't complete the overscroll gesture. SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE); break; } default: DCHECK(blink::WebInputEvent::IsGestureEventType(event.GetType()) || blink::WebInputEvent::IsTouchEventType(event.GetType())) << "Received unexpected event: " << event.GetType(); } return event_processed; } bool OverscrollController::ProcessOverscroll(float delta_x, float delta_y, bool is_touchpad, bool is_inertial) { if (scroll_state_ == ScrollState::CONTENT_CONSUMING) return false; // Do not start overscroll for inertial events. if (overscroll_mode_ == OVERSCROLL_NONE && is_inertial) return false; overscroll_delta_x_ += delta_x; overscroll_delta_y_ += delta_y; const float start_threshold = is_touchpad ? OverscrollConfig::kStartTouchpadThresholdDips : OverscrollConfig::kStartTouchscreenThresholdDips; if (fabs(overscroll_delta_x_) <= start_threshold && fabs(overscroll_delta_y_) <= start_threshold) { SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE); return true; } if (delegate_) { base::Optional cap = delegate_->GetMaxOverscrollDelta(); if (cap) { DCHECK_LE(0.f, cap.value()); switch (overscroll_mode_) { case OVERSCROLL_WEST: case OVERSCROLL_EAST: overscroll_delta_x_ = ClampAbsoluteValue( overscroll_delta_x_, cap.value() + start_threshold); break; case OVERSCROLL_NORTH: case OVERSCROLL_SOUTH: overscroll_delta_y_ = ClampAbsoluteValue( overscroll_delta_y_, cap.value() + start_threshold); break; case OVERSCROLL_NONE: break; } } } // Compute the current overscroll direction. If the direction is different // from the current direction, then always switch to no-overscroll mode first // to make sure that subsequent scroll events go through to the page first. OverscrollMode new_mode = OVERSCROLL_NONE; const float kMinRatio = 2.5; if (fabs(overscroll_delta_x_) > start_threshold && fabs(overscroll_delta_x_) > fabs(overscroll_delta_y_) * kMinRatio) new_mode = overscroll_delta_x_ > 0.f ? OVERSCROLL_EAST : OVERSCROLL_WEST; else if (fabs(overscroll_delta_y_) > start_threshold && fabs(overscroll_delta_y_) > fabs(overscroll_delta_x_) * kMinRatio) new_mode = overscroll_delta_y_ > 0.f ? OVERSCROLL_SOUTH : OVERSCROLL_NORTH; // The horizontal overscroll is used for history navigation. Enable it for // touchpad only if TouchpadOverscrollHistoryNavigation is enabled. if ((new_mode == OVERSCROLL_EAST || new_mode == OVERSCROLL_WEST) && is_touchpad && !OverscrollConfig::TouchpadOverscrollHistoryNavigationEnabled()) { new_mode = OVERSCROLL_NONE; } // The vertical overscroll is used for pull-to-refresh. Enable it only if // pull-to-refresh is enabled. if (new_mode == OVERSCROLL_SOUTH || new_mode == OVERSCROLL_NORTH) { auto ptr_mode = OverscrollConfig::GetPullToRefreshMode(); if (ptr_mode == OverscrollConfig::PullToRefreshMode::kDisabled || (ptr_mode == OverscrollConfig::PullToRefreshMode::kEnabledTouchschreen && is_touchpad) || time_since_last_ignored_scroll_ < kPullToRefreshCoolOffDelay) { overscroll_ignored_ = true; new_mode = OVERSCROLL_NONE; } } if (overscroll_mode_ == OVERSCROLL_NONE) { SetOverscrollMode(new_mode, is_touchpad ? OverscrollSource::TOUCHPAD : OverscrollSource::TOUCHSCREEN); } else if (new_mode != overscroll_mode_) { SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE); } if (overscroll_mode_ == OVERSCROLL_NONE) return false; overscroll_ignored_ = false; // Tell the delegate about the overscroll update so that it can update // the display accordingly (e.g. show history preview etc.). if (delegate_) { // Do not include the threshold amount when sending the deltas to the // delegate. float delegate_delta_x = overscroll_delta_x_; if (fabs(delegate_delta_x) > start_threshold) { if (delegate_delta_x < 0) delegate_delta_x += start_threshold; else delegate_delta_x -= start_threshold; } else { delegate_delta_x = 0.f; } float delegate_delta_y = overscroll_delta_y_; if (fabs(delegate_delta_y) > start_threshold) { if (delegate_delta_y < 0) delegate_delta_y += start_threshold; else delegate_delta_y -= start_threshold; } else { delegate_delta_y = 0.f; } return delegate_->OnOverscrollUpdate(delegate_delta_x, delegate_delta_y); } return false; } void OverscrollController::CompleteAction() { ignore_following_inertial_events_ = true; if (delegate_) delegate_->OnOverscrollComplete(overscroll_mode_); Reset(); } void OverscrollController::SetOverscrollMode(OverscrollMode mode, OverscrollSource source) { if (overscroll_mode_ == mode) return; // If the mode changes to NONE, source is also NONE. DCHECK(mode != OVERSCROLL_NONE || source == OverscrollSource::NONE); // When setting to a non-NONE mode and there is a locked mode, don't set the // mode if the new mode is not the same as the locked mode. if (mode != OVERSCROLL_NONE && locked_mode_ != OVERSCROLL_NONE && mode != locked_mode_) { return; } OverscrollMode old_mode = overscroll_mode_; overscroll_mode_ = mode; overscroll_source_ = source; if (overscroll_mode_ == OVERSCROLL_NONE) { overscroll_delta_x_ = overscroll_delta_y_ = 0.f; } else { scroll_state_ = ScrollState::OVERSCROLLING; locked_mode_ = overscroll_mode_; } if (delegate_) { delegate_->OnOverscrollModeChange(old_mode, overscroll_mode_, source, behavior_); } } void OverscrollController::ResetScrollState() { scroll_state_ = ScrollState::NONE; locked_mode_ = OVERSCROLL_NONE; } } // namespace content