| // 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 |