| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/input/child_frame_input_helper.h" |
| |
| #include "base/trace_event/trace_event.h" |
| #include "components/input/features.h" |
| #include "components/input/render_input_router.h" |
| #include "components/input/render_widget_host_input_event_router.h" |
| #include "third_party/blink/public/common/frame/frame_visual_properties.h" |
| |
| namespace input { |
| |
| ChildFrameInputHelper::~ChildFrameInputHelper() = default; |
| |
| ChildFrameInputHelper::ChildFrameInputHelper(RenderWidgetHostViewInput* view, |
| Delegate* delegate) |
| : view_(view), delegate_(delegate) { |
| CHECK(view); |
| } |
| |
| void ChildFrameInputHelper::NotifyHitTestRegionUpdated( |
| const viz::AggregatedHitTestRegion& region) { |
| std::optional<gfx::RectF> screen_rect = |
| region.transform.InverseMapRect(gfx::RectF(region.rect)); |
| if (!screen_rect) { |
| last_stable_screen_rect_ = gfx::RectF(); |
| last_stable_screen_rect_for_iov2_ = gfx::RectF(); |
| screen_rect_stable_since_ = base::TimeTicks::Now(); |
| screen_rect_stable_since_for_iov2_ = base::TimeTicks::Now(); |
| return; |
| } |
| |
| // Convert to DIP |
| screen_rect->Scale(1. / view_->GetDeviceScaleFactor()); |
| |
| // Movement as a proportion of frame size |
| double horizontal_movement = |
| screen_rect->width() |
| ? std::abs(last_stable_screen_rect_.x() - screen_rect->x()) / |
| screen_rect->width() |
| : 0.0; |
| double vertical_movement = |
| screen_rect->height() |
| ? std::abs(last_stable_screen_rect_.y() - screen_rect->y()) / |
| screen_rect->height() |
| : 0.0; |
| if ((ToRoundedSize(screen_rect->size()) != |
| ToRoundedSize(last_stable_screen_rect_.size())) || |
| horizontal_movement > |
| blink::FrameVisualProperties::MaxChildFrameScreenRectMovement() || |
| vertical_movement > |
| blink::FrameVisualProperties::MaxChildFrameScreenRectMovement()) { |
| last_stable_screen_rect_ = *screen_rect; |
| screen_rect_stable_since_ = base::TimeTicks::Now(); |
| } |
| // The legacy logic is based on manhattan distance. |
| if ((ToRoundedSize(screen_rect->size()) != |
| ToRoundedSize(last_stable_screen_rect_for_iov2_.size())) || |
| (std::abs(last_stable_screen_rect_for_iov2_.x() - screen_rect->x()) + |
| std::abs(last_stable_screen_rect_for_iov2_.y() - screen_rect->y()) > |
| blink::FrameVisualProperties:: |
| MaxChildFrameScreenRectMovementForIOv2())) { |
| last_stable_screen_rect_for_iov2_ = *screen_rect; |
| screen_rect_stable_since_for_iov2_ = base::TimeTicks::Now(); |
| } |
| } |
| |
| bool ChildFrameInputHelper::ScreenRectIsUnstableFor( |
| const blink::WebInputEvent& event) { |
| // Some tests generate events with artificial timestamps; ignore these. |
| if (event.TimeStamp() < screen_rect_stable_since_) { |
| return false; |
| } |
| if (event.TimeStamp() - |
| base::Milliseconds( |
| blink::FrameVisualProperties::MinScreenRectStableTimeMs()) < |
| screen_rect_stable_since_) { |
| return true; |
| } |
| if (auto* parent = view_->GetParentViewInput()) { |
| return parent->ScreenRectIsUnstableFor(event); |
| } |
| return false; |
| } |
| |
| bool ChildFrameInputHelper::ScreenRectIsUnstableForIOv2For( |
| const blink::WebInputEvent& event) { |
| // Some tests generate events with artificial timestamps; ignore these. |
| if (event.TimeStamp() < screen_rect_stable_since_for_iov2_) { |
| return false; |
| } |
| if (event.TimeStamp() - |
| base::Milliseconds(blink::FrameVisualProperties:: |
| MinScreenRectStableTimeMsForIOv2()) < |
| screen_rect_stable_since_for_iov2_) { |
| return true; |
| } |
| if (RenderWidgetHostViewInput* parent = view_->GetParentViewInput()) { |
| return parent->ScreenRectIsUnstableForIOv2For(event); |
| } |
| return false; |
| } |
| |
| gfx::PointF ChildFrameInputHelper::TransformPointToRootCoordSpaceF( |
| const gfx::PointF& point) { |
| return TransformPointToRootCoordSpace(point); |
| } |
| |
| gfx::PointF ChildFrameInputHelper::TransformPointToRootCoordSpace( |
| const gfx::PointF& point) { |
| if (!delegate_) { |
| return point; |
| } |
| |
| gfx::PointF transformed_point; |
| TransformPointToCoordSpaceForView(point, delegate_->GetRootViewInput(), |
| view_->GetFrameSinkId(), |
| &transformed_point); |
| return transformed_point; |
| } |
| |
| gfx::PointF ChildFrameInputHelper::TransformRootPointToViewCoordSpace( |
| const gfx::PointF& point) { |
| if (!delegate_) { |
| return point; |
| } |
| |
| auto* root_rwhv = delegate_->GetRootViewInput(); |
| if (!root_rwhv) { |
| return point; |
| } |
| |
| gfx::PointF transformed_point; |
| if (!root_rwhv->TransformPointToCoordSpaceForView(point, view_, |
| &transformed_point)) { |
| return point; |
| } |
| return transformed_point; |
| } |
| |
| bool ChildFrameInputHelper::TransformPointToCoordSpaceForView( |
| const gfx::PointF& point, |
| input::RenderWidgetHostViewInput* target_view, |
| gfx::PointF* transformed_point) { |
| if (target_view == view_) { |
| *transformed_point = point; |
| return true; |
| } |
| |
| return TransformPointToCoordSpaceForView( |
| point, target_view, view_->GetFrameSinkId(), transformed_point); |
| } |
| |
| bool ChildFrameInputHelper::TransformPointToCoordSpaceForView( |
| const gfx::PointF& point, |
| input::RenderWidgetHostViewInput* target_view, |
| const viz::FrameSinkId& local_frame_sink_id, |
| gfx::PointF* transformed_point) { |
| if (!delegate_) { |
| return false; |
| } |
| |
| RenderWidgetHostViewInput* root_view = delegate_->GetRootViewInput(); |
| if (!root_view) { |
| return false; |
| } |
| |
| // It is possible that neither the original surface or target surface is an |
| // ancestor of the other in the RenderWidgetHostView tree (e.g. they could |
| // be siblings). To account for this, the point is first transformed into the |
| // root coordinate space and then the root is asked to perform the conversion. |
| if (!root_view->TransformPointToLocalCoordSpace(point, local_frame_sink_id, |
| transformed_point)) { |
| return false; |
| } |
| |
| if (target_view == root_view) { |
| return true; |
| } |
| |
| return root_view->TransformPointToCoordSpaceForView( |
| *transformed_point, target_view, transformed_point); |
| } |
| |
| void ChildFrameInputHelper::TransformPointToRootSurface(gfx::PointF* point) { |
| // This function is called by RenderWidgetHostInputEventRouter only for |
| // root-views. |
| NOTREACHED(); |
| } |
| |
| blink::mojom::InputEventResultState ChildFrameInputHelper::FilterInputEvent( |
| const blink::WebInputEvent& input_event) { |
| // A child renderer should never receive a GesturePinch event. Pinch events |
| // can still be targeted to a child, but they must be processed without |
| // sending the pinch event to the child (e.g. touchpad pinch synthesizes |
| // wheel events to send to the child renderer). |
| if (blink::WebInputEvent::IsPinchGestureEventType(input_event.GetType())) { |
| const blink::WebGestureEvent& gesture_event = |
| static_cast<const blink::WebGestureEvent&>(input_event); |
| // Touchscreen pinch events may be targeted to a child in order to have the |
| // child's TouchActionFilter filter them, but we may encounter |
| // https://crbug.com/771330 which would let the pinch events through. |
| if (gesture_event.SourceDevice() == blink::WebGestureDevice::kTouchscreen) { |
| return blink::mojom::InputEventResultState::kConsumed; |
| } |
| DUMP_WILL_BE_NOTREACHED(); |
| } |
| |
| if (input_event.GetType() == blink::WebInputEvent::Type::kGestureFlingStart) { |
| const blink::WebGestureEvent& gesture_event = |
| static_cast<const blink::WebGestureEvent&>(input_event); |
| // Zero-velocity touchpad flings are an Aura-specific signal that the |
| // touchpad scroll has ended, and should not be forwarded to the renderer. |
| if (gesture_event.SourceDevice() == blink::WebGestureDevice::kTouchpad && |
| !gesture_event.data.fling_start.velocity_x && |
| !gesture_event.data.fling_start.velocity_y) { |
| // Here we indicate that there was no consumer for this event, as |
| // otherwise the fling animation system will try to run an animation |
| // and will also expect a notification when the fling ends. Since |
| // CrOS just uses the GestureFlingStart with zero-velocity as a means |
| // of indicating that touchpad scroll has ended, we don't actually want |
| // a fling animation. |
| // Note: this event handling is modeled on similar code in |
| // TenderWidgetHostViewAura::FilterInputEvent(). |
| return blink::mojom::InputEventResultState::kNoConsumerExists; |
| } |
| } |
| |
| if (is_scroll_sequence_bubbling_ && |
| (input_event.GetType() == |
| blink::WebInputEvent::Type::kGestureScrollUpdate) && |
| delegate_) { |
| // If we're bubbling, then to preserve latching behaviour, the child should |
| // not consume this event. If the child has added its viewport to the scroll |
| // chain, then any GSU events we send to the renderer could be consumed, |
| // even though we intend for them to be bubbled. So we immediately bubble |
| // any scroll updates without giving the child a chance to consume them. |
| // If the child has not added its viewport to the scroll chain, then we |
| // know that it will not attempt to consume the rest of the scroll |
| // sequence. |
| return blink::mojom::InputEventResultState::kNoConsumerExists; |
| } |
| |
| return blink::mojom::InputEventResultState::kNotConsumed; |
| } |
| |
| void ChildFrameInputHelper::StopFlingingIfNecessary( |
| const blink::WebGestureEvent& event, |
| blink::mojom::InputEventResultState ack_result) { |
| // In case of scroll bubbling the target view is in charge of stopping the |
| // fling if needed. |
| if (is_scroll_sequence_bubbling_) { |
| return; |
| } |
| |
| // Delegates to RenderWidgetHostViewInput to stop flinging if the GSU event |
| // with momentum phase was not consumed by the renderer. |
| view_->RenderWidgetHostViewInput::StopFlingingIfNecessary(event, ack_result); |
| } |
| |
| void ChildFrameInputHelper::GestureEventAckHelper( |
| const blink::WebGestureEvent& event, |
| blink::mojom::InputEventResultSource ack_source, |
| blink::mojom::InputEventResultState ack_result) { |
| // Stop flinging if a GSU event with momentum phase is sent to the renderer |
| // but not consumed. |
| StopFlingingIfNecessary(event, ack_result); |
| |
| if (event.IsTouchpadZoomEvent()) { |
| ProcessTouchpadZoomEventAckInRoot(event, ack_source, ack_result); |
| } |
| |
| // GestureScrollBegin is a blocking event; It is forwarded for bubbling if |
| // its ack is not consumed. For the rest of the scroll events |
| // (GestureScrollUpdate, GestureScrollEnd) are bubbled if the |
| // GestureScrollBegin was bubbled. If the browser consumed the event, the |
| // event was filtered and shouldn't affect the state of scroll bubbling. |
| bool event_filtered = |
| ack_source == blink::mojom::InputEventResultSource::kBrowser && |
| ack_result == blink::mojom::InputEventResultState::kConsumed; |
| |
| // TODO(crbug.com/346629231): Remove flag guard once this lands. Prior to the |
| // fix this section was always entered. |
| if (!event_filtered || |
| !base::FeatureList::IsEnabled(input::features::kScrollBubblingFix)) { |
| if (event.GetType() == blink::WebInputEvent::Type::kGestureScrollBegin) { |
| DCHECK(!is_scroll_sequence_bubbling_); |
| is_scroll_sequence_bubbling_ = |
| ack_result == blink::mojom::InputEventResultState::kNotConsumed || |
| ack_result == blink::mojom::InputEventResultState::kNoConsumerExists; |
| } |
| |
| if (is_scroll_sequence_bubbling_ && |
| (event.GetType() == blink::WebInputEvent::Type::kGestureScrollBegin || |
| event.GetType() == blink::WebInputEvent::Type::kGestureScrollUpdate || |
| event.GetType() == blink::WebInputEvent::Type::kGestureScrollEnd)) { |
| const bool can_continue = BubbleScrollEvent(event); |
| if (event.GetType() == blink::WebInputEvent::Type::kGestureScrollEnd || |
| !can_continue) { |
| is_scroll_sequence_bubbling_ = false; |
| } |
| } |
| } |
| |
| TRACE_EVENT_INSTANT0("input", "Did_Ack_To_Frame_Connector", |
| TRACE_EVENT_SCOPE_THREAD); |
| DidAckGestureEvent(event, ack_result); |
| } |
| |
| void ChildFrameInputHelper::ForwardTouchpadZoomEventIfNecessary( |
| const blink::WebGestureEvent& event, |
| blink::mojom::InputEventResultState ack_result) { |
| // ACKs of synthetic wheel events for touchpad pinch or double tap are |
| // processed in the root RWHV. |
| NOTREACHED(); |
| } |
| |
| void ChildFrameInputHelper::ProcessTouchpadZoomEventAckInRoot( |
| const blink::WebGestureEvent& event, |
| blink::mojom::InputEventResultSource ack_source, |
| blink::mojom::InputEventResultState ack_result) { |
| DCHECK(event.IsTouchpadZoomEvent()); |
| if (!delegate_) { |
| return; |
| } |
| auto* root_view = delegate_->GetRootViewInput(); |
| if (!root_view) { |
| return; |
| } |
| |
| blink::WebGestureEvent root_event(event); |
| const gfx::PointF root_point = |
| TransformPointToRootCoordSpaceF(event.PositionInWidget()); |
| root_event.SetPositionInWidget(root_point); |
| root_view->GestureEventAck(root_event, ack_source, ack_result); |
| } |
| |
| bool ChildFrameInputHelper::BubbleScrollEvent( |
| const blink::WebGestureEvent& event) { |
| TRACE_EVENT1("input", "ChildFrameInputHelper::BubbleScrollEvent", "type", |
| blink::WebInputEvent::GetName(event.GetType())); |
| DCHECK(event.GetType() == blink::WebInputEvent::Type::kGestureScrollBegin || |
| event.GetType() == blink::WebInputEvent::Type::kGestureScrollUpdate || |
| event.GetType() == blink::WebInputEvent::Type::kGestureScrollEnd); |
| |
| if (!delegate_) { |
| return false; |
| } |
| auto* parent_view = delegate_->GetParentViewInput(); |
| |
| if (!parent_view) { |
| return false; |
| } |
| |
| auto* event_router = parent_view->GetViewRenderInputRouter() |
| ->delegate() |
| ->GetInputEventRouter(); |
| |
| // We will only convert the coordinates back to the root here. The |
| // RenderWidgetHostInputEventRouter will determine which ancestor view will |
| // receive a resent gesture event, so it will be responsible for converting to |
| // the coordinates of the target view. |
| blink::WebGestureEvent resent_gesture_event(event); |
| const gfx::PointF root_point = |
| view_->TransformPointToRootCoordSpaceF(event.PositionInWidget()); |
| resent_gesture_event.SetPositionInWidget(root_point); |
| // When a gesture event is bubbled to the parent frame, set the allowed touch |
| // action of the parent frame to Auto so that this gesture event is allowed. |
| parent_view->GetViewRenderInputRouter() |
| ->input_router() |
| ->ForceSetTouchActionAuto(); |
| |
| TRACE_EVENT_INSTANT0("input", "Did_Bubble_To_InputEventRouter", |
| TRACE_EVENT_SCOPE_THREAD); |
| return event_router->BubbleScrollEvent(parent_view, view_, |
| resent_gesture_event); |
| } |
| |
| void ChildFrameInputHelper::DidAckGestureEvent( |
| const blink::WebGestureEvent& event, |
| blink::mojom::InputEventResultState ack_result) { |
| if (!delegate_) { |
| return; |
| } |
| |
| auto* root_view = delegate_->GetRootViewInput(); |
| if (!root_view) { |
| return; |
| } |
| |
| root_view->ChildDidAckGestureEvent(event, ack_result); |
| } |
| |
| } // namespace input |