| // 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 <algorithm> |
| |
| #include "base/command_line.h" |
| #include "base/logging.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<const blink::WebGestureEvent&>(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::kGestureScrollUpdate) |
| return false; |
| |
| const blink::WebGestureEvent& gesture = |
| static_cast<const blink::WebGestureEvent&>(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::kGestureScrollBegin: |
| case blink::WebInputEvent::kGestureScrollUpdate: |
| case blink::WebInputEvent::kGestureScrollEnd: { |
| const blink::WebGestureEvent& gesture = |
| static_cast<const blink::WebGestureEvent&>(event); |
| |
| // Gesture events with Autoscroll source don't cause overscrolling. |
| if (IsGestureEventFromAutoscroll(gesture)) |
| return false; |
| |
| ui::input_types::ScrollGranularity granularity; |
| switch (event.GetType()) { |
| case blink::WebInputEvent::kGestureScrollBegin: |
| granularity = gesture.data.scroll_begin.delta_hint_units; |
| break; |
| case blink::WebInputEvent::kGestureScrollUpdate: |
| granularity = gesture.data.scroll_update.delta_units; |
| break; |
| case blink::WebInputEvent::kGestureScrollEnd: |
| granularity = gesture.data.scroll_end.delta_units; |
| break; |
| default: |
| granularity = ui::input_types::ScrollGranularity::kScrollByPixel; |
| break; |
| } |
| |
| return granularity == |
| ui::input_types::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::kMouseWheel) |
| return false; |
| |
| if (event.GetType() == blink::WebInputEvent::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::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::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. |
| return scroll_state_ == ScrollState::OVERSCROLLING && |
| (event.GetType() == blink::WebInputEvent::kGestureScrollUpdate || |
| event.GetType() == blink::WebInputEvent::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::kGestureScrollUpdate || |
| event.GetType() == blink::WebInputEvent::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::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::kMouseMove && |
| event.GetType() != blink::WebInputEvent::kGestureScrollEnd && |
| event.GetType() != blink::WebInputEvent::kGestureFlingStart && |
| event.GetType() != blink::WebInputEvent::kGestureScrollUpdate) |
| return false; |
| |
| // Complete the overscroll gesture for inertial scroll (fling) event from |
| // touchpad. |
| if (event.GetType() == blink::WebInputEvent::kGestureScrollUpdate) { |
| if (overscroll_source_ != OverscrollSource::TOUCHPAD) |
| return false; |
| DCHECK(IsGestureEventFromTouchpad(event)); |
| const blink::WebGestureEvent gesture_event = |
| static_cast<const blink::WebGestureEvent&>(event); |
| if (gesture_event.data.scroll_update.inertial_phase != |
| blink::WebGestureEvent::InertialPhaseState::kMomentum) |
| return false; |
| } |
| |
| if (event.GetType() == blink::WebInputEvent::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<const blink::WebGestureEvent&>(event); |
| if (gesture_event.data.scroll_end.inertial_phase != |
| blink::WebGestureEvent::InertialPhaseState::kMomentum) |
| return false; |
| } |
| |
| if (!delegate_) |
| return false; |
| |
| if (event.GetType() == blink::WebInputEvent::kGestureFlingStart) { |
| // Check to see if the fling is in the same direction of the overscroll. |
| const blink::WebGestureEvent gesture = |
| static_cast<const blink::WebGestureEvent&>(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::kGestureScrollBegin: |
| case blink::WebInputEvent::kGestureScrollEnd: |
| case blink::WebInputEvent::kGestureScrollUpdate: |
| case blink::WebInputEvent::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::kGestureScrollBegin: { |
| if (overscroll_mode_ != OVERSCROLL_NONE) |
| SetOverscrollMode(OVERSCROLL_NONE, OverscrollSource::NONE); |
| break; |
| } |
| case blink::WebInputEvent::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<const blink::WebGestureEvent&>(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::kGestureScrollUpdate: { |
| const blink::WebGestureEvent& gesture = |
| static_cast<const blink::WebGestureEvent&>(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::kGestureFlingStart: { |
| const float kFlingVelocityThreshold = 1100.f; |
| const blink::WebGestureEvent& gesture = |
| static_cast<const blink::WebGestureEvent&>(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<float> 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 |