chromium/content/browser/renderer_host/render_widget_host_view_ios.mm

// Copyright 2023 The Chromium Authors
// 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/render_widget_host_view_ios.h"

#import <UIKit/UIKit.h>

#include <cstdint>

#include "base/command_line.h"
#include "build/ios_buildflags.h"
#include "components/input/render_widget_host_input_event_router.h"
#include "components/viz/common/features.h"
#include "components/viz/common/surfaces/frame_sink_id_allocator.h"
#include "content/browser/renderer_host/browser_compositor_ios.h"
#include "content/browser/renderer_host/input/motion_event_web.h"
#include "content/browser/renderer_host/input/synthetic_gesture_target_ios.h"
#include "content/browser/renderer_host/render_view_host_delegate_view.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_ios_uiview.h"
#include "content/browser/renderer_host/text_input_manager.h"
#include "content/common/content_switches_internal.h"
#include "content/common/input/events_helper.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/common/content_switches.h"
#include "ui/accelerated_widget_mac/display_ca_layer_tree.h"
#include "ui/base/ime/text_input_mode.h"
#include "ui/base/ime/text_input_type.h"
#include "ui/display/screen.h"
#include "ui/events/gesture_detection/gesture_provider_config_helper.h"
#include "ui/gfx/geometry/size_conversions.h"

@interface UIApplication (Testing)
- (BOOL)isRunningTests;
@end

@implementation UIApplication (Testing)
- (BOOL)isRunningTests {
  return NO;
}
@end

namespace {

// Used for setting the requested renderer size when testing.
constexpr gfx::Size kDefaultSizeForTesting = gfx::Size(800, 600);
constexpr gfx::Size KDefaultSizeForPreventResizingForTesting =
    gfx::Size(980, 735);

bool IsTesting() {
#if BUILDFLAG(IS_IOS_APP_EXTENSION)
  // This class shouldn't really be build with extension anyways.
  // Fix the build to avoid building browser code in extensions.
  return false;
#else
  return [[UIApplication sharedApplication] isRunningTests];
#endif
}

gfx::Rect GetDefaultSizeForTesting() {
  return base::CommandLine::ForCurrentProcess()->HasSwitch(
             switches::kPreventResizingContentsForTesting)
             ? gfx::Rect(KDefaultSizeForPreventResizingForTesting)
             : gfx::Rect(kDefaultSizeForTesting);
}

}  // namespace

namespace content {

// This class holds strongly so we don't leak that in the header of the
// RenderWidgetHostViewIOS.
class UIViewHolder {
 public:
  RenderWidgetUIView* __strong view_;
};

///////////////////////////////////////////////////////////////////////////////
// RenderWidgetHostViewIOS, public:

RenderWidgetHostViewIOS::RenderWidgetHostViewIOS(RenderWidgetHost* widget)
    : RenderWidgetHostViewBase(widget),
      gesture_provider_(
          ui::GetGestureProviderConfig(
              ui::GestureProviderConfigType::CURRENT_PLATFORM,
              content::GetUIThreadTaskRunner({BrowserTaskType::kUserInput})),
          this) {
  ui_view_ = std::make_unique<UIViewHolder>();
  ui_view_->view_ =
      [[RenderWidgetUIView alloc] initWithWidget:weak_factory_.GetWeakPtr()];

  display_tree_ =
      std::make_unique<ui::DisplayCALayerTree>([ui_view_->view_ layer]);

  auto* screen = display::Screen::GetScreen();
  screen_infos_ =
      screen->GetScreenInfosNearestDisplay(screen->GetPrimaryDisplay().id());

  browser_compositor_ = std::make_unique<BrowserCompositorIOS>(
      (uint64_t)(__bridge void*)ui_view_->view_, this, host()->is_hidden(),
      host()->GetFrameSinkId());

  if (IsTesting()) {
    view_bounds_ = GetDefaultSizeForTesting();
    browser_compositor_->UpdateSurfaceFromUIView(GetViewBounds().size());
  }

  CHECK(host()->GetFrameSinkId().is_valid());

  // Let the page-level input event router know about our surface ID
  // namespace for surface-based hit testing.
  if (ShouldRouteEvents()) {
    host()->delegate()->GetInputEventRouter()->AddFrameSinkIdOwner(
        GetFrameSinkId(), this);
  }

  if (GetTextInputManager()) {
    text_input_manager_->AddObserver(this);
  }

  host()->render_frame_metadata_provider()->AddObserver(this);
  host()->SetView(this);
}

RenderWidgetHostViewIOS::~RenderWidgetHostViewIOS() = default;

void RenderWidgetHostViewIOS::Destroy() {
  [ui_view_->view_ removeView];
  host()->render_frame_metadata_provider()->RemoveObserver(this);
  if (text_input_manager_) {
    text_input_manager_->RemoveObserver(this);
  }
  browser_compositor_.reset();
  // Call this before the derived class is destroyed so that virtual function
  // calls back into `this` still work.
  NotifyObserversAboutShutdown();
  RenderWidgetHostViewBase::Destroy();
  delete this;
}

bool RenderWidgetHostViewIOS::IsSurfaceAvailableForCopy() {
  return browser_compositor_->GetDelegatedFrameHost()
      ->CanCopyFromCompositingSurface();
}

void RenderWidgetHostViewIOS::CopyFromSurface(
    const gfx::Rect& src_rect,
    const gfx::Size& dst_size,
    base::OnceCallback<void(const SkBitmap&)> callback) {
  base::WeakPtr<RenderWidgetHostImpl> popup_host;
  base::WeakPtr<DelegatedFrameHost> popup_frame_host;
  RenderWidgetHostViewBase::CopyMainAndPopupFromSurface(
      host()->GetWeakPtr(),
      browser_compositor_->GetDelegatedFrameHost()->GetWeakPtr(), popup_host,
      popup_frame_host, src_rect, dst_size, GetDeviceScaleFactor(),
      std::move(callback));
}

void RenderWidgetHostViewIOS::InitAsChild(gfx::NativeView parent_view) {}
void RenderWidgetHostViewIOS::SetSize(const gfx::Size& size) {}
void RenderWidgetHostViewIOS::SetBounds(const gfx::Rect& rect) {}
gfx::NativeView RenderWidgetHostViewIOS::GetNativeView() {
  return gfx::NativeView(ui_view_->view_);
}

gfx::NativeViewAccessible RenderWidgetHostViewIOS::GetNativeViewAccessible() {
  return ui_view_->view_;
}

gfx::NativeViewAccessible
RenderWidgetHostViewIOS::AccessibilityGetNativeViewAccessible() {
  return ui_view_->view_;
}

void RenderWidgetHostViewIOS::Focus() {
  // Ignore redundant calls, as they can cause unending loops of focus-setting.
  // crbug.com/998123, crbug.com/804184.
  if (is_first_responder_ || is_getting_focus_) {
    return;
  }

  base::AutoReset<bool> is_getting_focus_bit(&is_getting_focus_, true);
  [ui_view_->view_ becomeFirstResponder];
}

bool RenderWidgetHostViewIOS::HasFocus() {
  return is_first_responder_;
}

gfx::Rect RenderWidgetHostViewIOS::GetViewBounds() {
  return view_bounds_;
}
blink::mojom::PointerLockResult RenderWidgetHostViewIOS::LockPointer(bool) {
  return {};
}
blink::mojom::PointerLockResult RenderWidgetHostViewIOS::ChangePointerLock(
    bool) {
  return {};
}
void RenderWidgetHostViewIOS::UnlockPointer() {}

uint32_t RenderWidgetHostViewIOS::GetCaptureSequenceNumber() const {
  return latest_capture_sequence_number_;
}

void RenderWidgetHostViewIOS::EnsureSurfaceSynchronizedForWebTest() {
  ++latest_capture_sequence_number_;
  browser_compositor_->ForceNewSurfaceId();
}

void RenderWidgetHostViewIOS::TakeFallbackContentFrom(
    RenderWidgetHostView* view) {}

std::unique_ptr<SyntheticGestureTarget>
RenderWidgetHostViewIOS::CreateSyntheticGestureTarget() {
  RenderWidgetHostImpl* host =
      RenderWidgetHostImpl::From(GetRenderWidgetHost());
  return std::make_unique<SyntheticGestureTargetIOS>(host);
}

const viz::LocalSurfaceId& RenderWidgetHostViewIOS::GetLocalSurfaceId() const {
  return browser_compositor_->GetRendererLocalSurfaceId();
}

void RenderWidgetHostViewIOS::UpdateFrameSinkIdRegistration() {
  RenderWidgetHostViewBase::UpdateFrameSinkIdRegistration();
  browser_compositor_->GetDelegatedFrameHost()->SetIsFrameSinkIdOwner(
      is_frame_sink_id_owner());
}

const viz::FrameSinkId& RenderWidgetHostViewIOS::GetFrameSinkId() const {
  return browser_compositor_->GetDelegatedFrameHost()->frame_sink_id();
}

viz::FrameSinkId RenderWidgetHostViewIOS::GetRootFrameSinkId() {
  return browser_compositor_->GetRootFrameSinkId();
}

viz::SurfaceId RenderWidgetHostViewIOS::GetCurrentSurfaceId() const {
  // |browser_compositor_| could be null if this method is called during its
  // destruction.
  if (!browser_compositor_) {
    return viz::SurfaceId();
  }
  return browser_compositor_->GetDelegatedFrameHost()->GetCurrentSurfaceId();
}

void RenderWidgetHostViewIOS::InitAsPopup(
    RenderWidgetHostView* parent_host_view,
    const gfx::Rect& pos,
    const gfx::Rect& anchor_rect) {}
void RenderWidgetHostViewIOS::UpdateCursor(const ui::Cursor& cursor) {}
void RenderWidgetHostViewIOS::SetIsLoading(bool is_loading) {}

void RenderWidgetHostViewIOS::RenderProcessGone() {
  Destroy();
}

void RenderWidgetHostViewIOS::ShowWithVisibility(
    PageVisibilityState page_visibility) {
  if (IsTesting() && !is_visible_) {
    // There is some circularity in how UpdateScreenInfo works. The base class
    // sets up some state needed by the browser compositor. The base class also
    // depends on an update from the browser compositor. In practice this is a
    // non issue because the function is called many times and values converge,
    // but this is not necessarily the case in tests. This could be resolved
    // by rewriting UpdateScreenInfo to interleave the work (see the mac
    // implementation, eg), but for now we will simply may another call to the
    // base class.
    RenderWidgetHostViewBase::UpdateScreenInfo();
    UpdateScreenInfo();
  }
  is_visible_ = true;
  browser_compositor_->SetViewVisible(is_visible_);
  OnShowWithPageVisibility(page_visibility);
}

void RenderWidgetHostViewIOS::NotifyHostAndDelegateOnWasShown(
    blink::mojom::RecordContentToVisibleTimeRequestPtr visible_time_request) {
  // SetRenderWidgetHostIsHidden may cause a state transition that switches to
  // a new instance of DelegatedFrameHost and calls WasShown, which causes
  // HasSavedFrame to always return true. So cache the HasSavedFrame result
  // before the transition, and do not save this DelegatedFrameHost* locally.
  bool has_saved_frame =
      browser_compositor_->GetDelegatedFrameHost()->HasSavedFrame();

  browser_compositor_->SetRenderWidgetHostIsHidden(false);

  const bool renderer_should_record_presentation_time = !has_saved_frame;
  host()->WasShown(renderer_should_record_presentation_time
                       ? visible_time_request.Clone()
                       : blink::mojom::RecordContentToVisibleTimeRequestPtr());

  // If the frame for the renderer is already available, then the
  // tab-switching time is the presentation time for the browser-compositor.
  // SetRenderWidgetHostIsHidden above will show the DelegatedFrameHost
  // in this state, but doesn't include the presentation time request.
  if (has_saved_frame && visible_time_request) {
    browser_compositor_->GetDelegatedFrameHost()
        ->RequestSuccessfulPresentationTimeForNextFrame(
            std::move(visible_time_request));
  }
}

void RenderWidgetHostViewIOS::Hide() {
  is_visible_ = false;
  browser_compositor_->SetViewVisible(is_visible_);
  browser_compositor_->SetRenderWidgetHostIsHidden(true);
  if (!host() || host()->is_hidden()) {
    return;
  }

  // Inform the renderer that we are being hidden so it can reduce its resource
  // utilization.
  host()->WasHidden();
}

bool RenderWidgetHostViewIOS::IsShowing() {
  // In testing, `view_` is not attached to the window.
  if (IsTesting()) {
    return is_visible_;
  }
  return is_visible_ && [ui_view_->view_ window];
}

gfx::Rect RenderWidgetHostViewIOS::GetBoundsInRootWindow() {
  return GetViewBounds();
}

gfx::Size RenderWidgetHostViewIOS::GetRequestedRendererSize() {
  return GetViewBounds().size();
}

std::optional<DisplayFeature> RenderWidgetHostViewIOS::GetDisplayFeature() {
  return std::nullopt;
}
void RenderWidgetHostViewIOS::SetDisplayFeatureForTesting(
    const DisplayFeature* display_feature) {}
void RenderWidgetHostViewIOS::UpdateBackgroundColor() {}

void RenderWidgetHostViewIOS::
    RequestSuccessfulPresentationTimeFromHostOrDelegate(
        blink::mojom::RecordContentToVisibleTimeRequestPtr
            visible_time_request) {
  // No state transition here so don't use
  // has_saved_frame_before_state_transition.
  if (browser_compositor_->GetDelegatedFrameHost()->HasSavedFrame()) {
    // If the frame for the renderer is already available, then the
    // tab-switching time is the presentation time for the browser-compositor.
    browser_compositor_->GetDelegatedFrameHost()
        ->RequestSuccessfulPresentationTimeForNextFrame(
            std::move(visible_time_request));
  } else {
    host()->RequestSuccessfulPresentationTimeForNextFrame(
        std::move(visible_time_request));
  }
}
void RenderWidgetHostViewIOS::
    CancelSuccessfulPresentationTimeRequestForHostAndDelegate() {
  host()->CancelSuccessfulPresentationTimeRequest();
  browser_compositor_->GetDelegatedFrameHost()
      ->CancelSuccessfulPresentationTimeRequest();
}

SkColor RenderWidgetHostViewIOS::BrowserCompositorIOSGetGutterColor() {
  // When making an element on the page fullscreen the element's background
  // may not match the page's, so use black as the gutter color to avoid
  // flashes of brighter colors during the transition.
  if (host()->delegate() && host()->delegate()->IsFullscreen()) {
    return SK_ColorBLACK;
  }
  if (GetBackgroundColor()) {
    return *GetBackgroundColor();
  }
  return SK_ColorWHITE;
}

bool RenderWidgetHostViewIOS::OnBrowserCompositorSurfaceIdChanged() {
  return host()->SynchronizeVisualProperties();
}

void RenderWidgetHostViewIOS::OnFrameTokenChanged(
    uint32_t frame_token,
    base::TimeTicks activation_time) {
  OnFrameTokenChangedForView(frame_token, activation_time);
}

std::vector<viz::SurfaceId>
RenderWidgetHostViewIOS::CollectSurfaceIdsForEviction() {
  return {};
}

void RenderWidgetHostViewIOS::UpdateScreenInfo() {
  if (!IsTesting()) {
    browser_compositor_->UpdateSurfaceFromUIView(
        gfx::Rect([ui_view_->view_ bounds]).size());
  }
  RenderWidgetHostViewBase::UpdateScreenInfo();
}

void RenderWidgetHostViewIOS::OnSynchronizedDisplayPropertiesChanged(
    bool rotation) {
  host()->SynchronizeVisualProperties();
}

void RenderWidgetHostViewIOS::UpdateCALayerTree(
    const gfx::CALayerParams& ca_layer_params) {
  DCHECK(display_tree_);
  display_tree_->UpdateCALayerTree(ca_layer_params);
}

void RenderWidgetHostViewIOS::OnOldViewDidNavigatePreCommit() {
  if (base::FeatureList::IsEnabled(
          features::kInvalidateLocalSurfaceIdPreCommit)) {
    CHECK(browser_compositor_) << "Shouldn't be called during destruction!";
    browser_compositor_->DidNavigateMainFramePreCommit();
  }
}

void RenderWidgetHostViewIOS::OnNewViewDidNavigatePostCommit() {
  gesture_provider_.ResetDetection();
}

void RenderWidgetHostViewIOS::DidEnterBackForwardCache() {
  CHECK(browser_compositor_) << "Shouldn't be called during destruction!";
  browser_compositor_->DidEnterBackForwardCache();
  // If we have the fallback content timer running, force it to stop. Else, when
  // the page is restored the timer could also fire, setting whatever
  // `DelegatedFrameHost::first_local_surface_id_after_navigation_` as the
  // fallback to our Surfacelayer.
  //
  // This is safe for BFCache restore because we will supply specific fallback
  // surfaces for BFCache.
  //
  // We do not want to call this in `RWHImpl::WasHidden()` because in the case
  // of `Visibility::OCCLUDED` we still want to keep the timer running.
  //
  // Called after to prevent prematurely evict the BFCached surface.
  host()->ForceFirstFrameAfterNavigationTimeout();
}

void RenderWidgetHostViewIOS::DidNavigate() {
  browser_compositor_->DidNavigate();
}

viz::ScopedSurfaceIdAllocator
RenderWidgetHostViewIOS::DidUpdateVisualProperties(
    const cc::RenderFrameMetadata& metadata) {
  base::OnceCallback<void()> allocation_task = base::BindOnce(
      base::IgnoreResult(
          &RenderWidgetHostViewIOS::OnDidUpdateVisualPropertiesComplete),
      weak_factory_.GetWeakPtr(), metadata);
  return browser_compositor_->GetScopedRendererSurfaceIdAllocator(
      std::move(allocation_task));
}

void RenderWidgetHostViewIOS::OnDidUpdateVisualPropertiesComplete(
    const cc::RenderFrameMetadata& metadata) {
  browser_compositor_->UpdateSurfaceFromChild(
      host()->auto_resize_enabled(), metadata.device_scale_factor,
      metadata.viewport_size_in_pixels,
      metadata.local_surface_id.value_or(viz::LocalSurfaceId()));
}

void RenderWidgetHostViewIOS::InvalidateLocalSurfaceIdAndAllocationGroup() {
  browser_compositor_->InvalidateSurfaceAllocationGroup();
}

void RenderWidgetHostViewIOS::ClearFallbackSurfaceForCommitPending() {
  browser_compositor_->GetDelegatedFrameHost()
      ->ClearFallbackSurfaceForCommitPending();
  browser_compositor_->InvalidateLocalSurfaceIdOnEviction();
}

void RenderWidgetHostViewIOS::ResetFallbackToFirstNavigationSurface() {
  browser_compositor_->GetDelegatedFrameHost()
      ->ResetFallbackToFirstNavigationSurface();
}

bool RenderWidgetHostViewIOS::RequestRepaintForTesting() {
  return browser_compositor_->ForceNewSurfaceId();
}

void RenderWidgetHostViewIOS::TransformPointToRootSurface(gfx::PointF* point) {
  browser_compositor_->TransformPointToRootSurface(point);
}

bool RenderWidgetHostViewIOS::HasFallbackSurface() const {
  return browser_compositor_->GetDelegatedFrameHost()->HasFallbackSurface();
}

bool RenderWidgetHostViewIOS::TransformPointToCoordSpaceForView(
    const gfx::PointF& point,
    RenderWidgetHostViewInput* target_view,
    gfx::PointF* transformed_point) {
  if (target_view == this) {
    *transformed_point = point;
    return true;
  }

  return target_view->TransformPointToLocalCoordSpace(
      point, GetCurrentSurfaceId(), transformed_point);
}

display::ScreenInfo RenderWidgetHostViewIOS::GetCurrentScreenInfo() const {
  return screen_infos_.current();
}

void RenderWidgetHostViewIOS::SetCurrentDeviceScaleFactor(
    float device_scale_factor) {
  // TODO(crbug.com/40229152): does this need to be upscaled by
  // scale_override_for_capture_ for HiDPI capture mode?
  screen_infos_.mutable_current().device_scale_factor = device_scale_factor;
}

void RenderWidgetHostViewIOS::SetActive(bool active) {
  if (host()) {
    UpdateActiveState(active);
    if (active) {
      if (HasFocus()) {
        host()->Focus();
      }
    } else {
      host()->Blur();
    }
  }
  // if (HasFocus())
  //  SetTextInputActive(active);
  if (!active) {
    UnlockPointer();
  }
}

bool RenderWidgetHostViewIOS::ShouldRouteEvents() const {
  DCHECK(host());
  return host()->delegate() && host()->delegate()->GetInputEventRouter();
}

void RenderWidgetHostViewIOS::OnTouchEvent(blink::WebTouchEvent web_event) {
  ui::FilteredGestureProvider::TouchHandlingResult result =
      gesture_provider_.OnTouchEvent(MotionEventWeb(web_event));
  if (!result.succeeded) {
    return;
  }

  web_event.moved_beyond_slop_region = result.moved_beyond_slop_region;
  ui::LatencyInfo latency_info;
  latency_info.AddLatencyNumber(ui::INPUT_EVENT_LATENCY_UI_COMPONENT);
  if (ShouldRouteEvents()) {
    host()->delegate()->GetInputEventRouter()->RouteTouchEvent(this, &web_event,
                                                               latency_info);
  } else {
    host()->GetRenderInputRouter()->ForwardTouchEventWithLatencyInfo(
        web_event, latency_info);
  }
}

void RenderWidgetHostViewIOS::ProcessAckedTouchEvent(
    const input::TouchEventWithLatencyInfo& touch,
    blink::mojom::InputEventResultState ack_result) {
  const bool event_consumed =
      ack_result == blink::mojom::InputEventResultState::kConsumed;
  gesture_provider_.OnTouchEventAck(
      touch.event.unique_touch_event_id, event_consumed,
      InputEventResultStateIsSetBlocking(ack_result));
  if (touch.event.touch_start_or_first_touch_move && event_consumed &&
      ShouldRouteEvents()) {
    host()
        ->delegate()
        ->GetInputEventRouter()
        ->OnHandledTouchStartOrFirstTouchMove(
            touch.event.unique_touch_event_id);
  }
}

void RenderWidgetHostViewIOS::OnGestureEvent(
    const ui::GestureEventData& gesture) {
  if ((gesture.type() == ui::EventType::kGesturePinchBegin ||
       gesture.type() == ui::EventType::kGesturePinchUpdate ||
       gesture.type() == ui::EventType::kGesturePinchEnd) &&
      !IsPinchToZoomEnabled()) {
    return;
  }

  blink::WebGestureEvent web_gesture =
      ui::CreateWebGestureEventFromGestureEventData(gesture);
  SendGestureEvent(web_gesture);
}

bool RenderWidgetHostViewIOS::RequiresDoubleTapGestureEvents() const {
  return true;
}

void RenderWidgetHostViewIOS::SendGestureEvent(
    const blink::WebGestureEvent& event) {
  InjectGestureEvent(event, ui::LatencyInfo());
}

void RenderWidgetHostViewIOS::InjectTouchEvent(
    const blink::WebTouchEvent& event,
    const ui::LatencyInfo& latency_info) {
  ui::FilteredGestureProvider::TouchHandlingResult result =
      gesture_provider_.OnTouchEvent(MotionEventWeb(event));
  if (!result.succeeded) {
    return;
  }

  if (ShouldRouteEvents()) {
    blink::WebTouchEvent touch_event(event);
    host()->delegate()->GetInputEventRouter()->RouteTouchEvent(
        this, &touch_event, latency_info);
  } else {
    host()->GetRenderInputRouter()->ForwardTouchEventWithLatencyInfo(
        event, latency_info);
  }
}

void RenderWidgetHostViewIOS::InjectGestureEvent(
    const blink::WebGestureEvent& event,
    const ui::LatencyInfo& latency_info) {
  if (ShouldRouteEvents()) {
    blink::WebGestureEvent gesture_event(event);
    host()->delegate()->GetInputEventRouter()->RouteGestureEvent(
        this, &gesture_event, latency_info);
  } else {
    host()->GetRenderInputRouter()->ForwardGestureEventWithLatencyInfo(
        event, latency_info);
  }
}

void RenderWidgetHostViewIOS::InjectMouseEvent(
    const blink::WebMouseEvent& web_mouse,
    const ui::LatencyInfo& latency_info) {
  if (ShouldRouteEvents()) {
    blink::WebMouseEvent mouse_event(web_mouse);
    host()->delegate()->GetInputEventRouter()->RouteMouseEvent(
        this, &mouse_event, latency_info);
  } else {
    host()->ForwardMouseEventWithLatencyInfo(web_mouse, latency_info);
  }
}

void RenderWidgetHostViewIOS::InjectMouseWheelEvent(
    const blink::WebMouseWheelEvent& web_wheel,
    const ui::LatencyInfo& latency_info) {
  if (ShouldRouteEvents()) {
    blink::WebMouseWheelEvent mouse_wheel_event(web_wheel);
    host()->delegate()->GetInputEventRouter()->RouteMouseWheelEvent(
        this, &mouse_wheel_event, latency_info);
  } else {
    host()->ForwardWheelEventWithLatencyInfo(web_wheel, latency_info);
  }
}

bool RenderWidgetHostViewIOS::CanBecomeFirstResponderForTesting() const {
  return IsTesting() && !is_first_responder_ && is_getting_focus_;
}

bool RenderWidgetHostViewIOS::CanResignFirstResponderForTesting() const {
  return IsTesting() && is_first_responder_;
}

void RenderWidgetHostViewIOS::UpdateNativeViewTree(gfx::NativeView view) {
  if (view) {
    [ui_view_->view_ updateView:(UIScrollView*)view.Get()];
    UpdateFrameBounds();
  } else {
    [ui_view_->view_ removeView];
  }
}

void RenderWidgetHostViewIOS::ImeSetComposition(
    const std::u16string& text,
    const std::vector<ui::ImeTextSpan>& spans,
    const gfx::Range& replacement_range,
    int selection_start,
    int selection_end) {
  if (auto* widget_host = GetActiveWidget()) {
    widget_host->ImeSetComposition(text, spans, replacement_range,
                                   selection_start, selection_end);
  }
}

void RenderWidgetHostViewIOS::ImeCommitText(const std::u16string& text,
                                            const gfx::Range& replacement_range,
                                            int relative_position) {
  if (auto* widget_host = GetActiveWidget()) {
    widget_host->ImeCommitText(text, std::vector<ui::ImeTextSpan>(),
                               replacement_range, relative_position);
  }
}

void RenderWidgetHostViewIOS::ImeFinishComposingText(bool keep_selection) {
  if (auto* widget_host = GetActiveWidget()) {
    widget_host->ImeFinishComposingText(keep_selection);
  }
}

RenderWidgetHostImpl* RenderWidgetHostViewIOS::GetActiveWidget() {
  return text_input_manager_ ? text_input_manager_->GetActiveWidget() : nullptr;
}

void RenderWidgetHostViewIOS::OnFirstResponderChanged() {
  bool is_first_responder = [ui_view_->view_ isFirstResponder] ||
                            [[ui_view_->view_ textInput] isFirstResponder] ||
                            (IsTesting() && is_getting_focus_);

  if (is_first_responder_ == is_first_responder) {
    return;
  }
  is_first_responder_ = is_first_responder;

  if (is_first_responder_) {
    host()->GotFocus();
  } else {
    host()->LostFocus();
  }
}

void RenderWidgetHostViewIOS::OnUpdateTextInputStateCalled(
    TextInputManager* text_input_manager,
    RenderWidgetHostViewBase* updated_view,
    bool did_update_state) {
  if (text_input_manager->GetActiveWidget()) {
    [[ui_view_->view_ textInput]
        onUpdateTextInputState:*text_input_manager->GetTextInputState()
                    withBounds:[ui_view_->view_ bounds]];
  } else {
    // If there are no active widgets, the TextInputState.type should be
    // reported as none.
    [[ui_view_->view_ textInput]
        onUpdateTextInputState:ui::mojom::TextInputState()
                    withBounds:[ui_view_->view_ bounds]];
  }
}

void RenderWidgetHostViewIOS::OnTextSelectionChanged(
    TextInputManager* text_input_manager,
    RenderWidgetHostViewBase* updated_view) {
  DCHECK_EQ(GetTextInputManager(), text_input_manager);
  const TextInputManager::TextSelection* selection =
      text_input_manager->GetTextSelection(updated_view);
  if (selection && selection->selected_text().length()) {
    [ui_view_->view_.textInteraction refreshKeyboardUI];
    [ui_view_->view_.textInteraction textSelectionDisplayInteraction]
        .activated = YES;

    // This seems like a bug. BETextInput always sets the
    // textSelectionDisplayInteraction lolipop dot size to 16.5,16.5, expecting
    // the entire web content to be transformed down for some reason. Instead,
    // scale it down here with a very naive implementation.
    UITextSelectionDisplayInteraction* textSelectionDisplayInteraction =
        ui_view_->view_.textInteraction.textSelectionDisplayInteraction;
    NSArray<UIView<UITextSelectionHandleView>*>* handleViews =
        textSelectionDisplayInteraction.handleViews;

    CGFloat shrink = handleViews[0].subviews[0].frame.size.height / 20;
    shrink = std::max(std::min(shrink, 1.0), 0.65);
    handleViews[0].subviews[1].layer.transform =
        CATransform3DMakeScale(shrink, shrink, 1);

    shrink = handleViews[1].subviews[0].frame.size.height / 20;
    shrink = std::max(std::min(shrink, 1.0), 0.65);
    handleViews[1].subviews[1].layer.transform =
        CATransform3DMakeScale(shrink, shrink, 1);
  } else {
    [ui_view_->view_.textInteraction textSelectionDisplayInteraction]
        .activated = NO;
  }
}
void RenderWidgetHostViewIOS::OnSelectionBoundsChanged(
    TextInputManager* text_input_manager,
    RenderWidgetHostViewBase* updated_view) {
  [ui_view_->view_.textInteraction
          .textSelectionDisplayInteraction setNeedsSelectionUpdate];
}

ui::Compositor* RenderWidgetHostViewIOS::GetCompositor() {
  return browser_compositor_->GetCompositor();
}

void RenderWidgetHostViewIOS::GestureEventAck(
    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);

  UIScrollView* scrollView = (UIScrollView*)[ui_view_->view_ superview];
  switch (event.GetType()) {
    case blink::WebInputEvent::Type::kGestureScrollBegin:
      is_scrolling_ = true;
      if (host()->delegate()) {
        host()->delegate()->SetTopControlsGestureScrollInProgress(true);
      }
      [[scrollView delegate] scrollViewWillBeginDragging:scrollView];
      break;
    case blink::WebInputEvent::Type::kGestureScrollUpdate:
      // TODO(crbug.com/40274032): Since ScrollResultData has been removed from
      // GestureEventAck, the invocation of ApplyRootScrollOffsetChanged here
      // has also been eliminated for now. We should address the
      // GestureScrollUpdate event after examining how the bug implements
      // GestureEventAck.
      break;
    case blink::WebInputEvent::Type::kGestureScrollEnd: {
      // Make sure our cached view bounds gets updated.
      if (!IsTesting()) {
        view_bounds_ = gfx::Rect([ui_view_->view_ bounds]);
      }
      if (host()->delegate()) {
        host()->delegate()->SetTopControlsGestureScrollInProgress(false);
      }
      is_scrolling_ = false;
      CGPoint targetOffset = [scrollView contentOffset];
      [[scrollView delegate] scrollViewWillEndDragging:scrollView
                                          withVelocity:CGPoint()
                                   targetContentOffset:&targetOffset];
      [[scrollView delegate] scrollViewDidEndDragging:scrollView
                                       willDecelerate:NO];
      host()->SynchronizeVisualProperties();
      break;
    }
    default:
      break;
  }
}

void RenderWidgetHostViewIOS::ChildDidAckGestureEvent(
    const blink::WebGestureEvent& event,
    blink::mojom::InputEventResultState ack_result) {
  // TODO(crbug.com/40274032): Since ScrollResultData has been removed from
  // GestureEventAck, the invocation of ApplyRootScrollOffsetChanged here has
  // also been eliminated for now. We should address the GestureScrollUpdate
  // event after examining how the bug implements GestureEventAck.
}

void RenderWidgetHostViewIOS::UpdateFrameBounds() {
  // UIScrollView* scrollView = (UIScrollView*)[ui_view_->view_ superview];
  gfx::PointF scrollOffset;
  if (last_root_scroll_offset_) {
    scrollOffset = *last_root_scroll_offset_;
  }
  CGRect parentBounds = [[ui_view_->view_ superview] bounds];
  gfx::SizeF viewportSize(parentBounds.size);

  CGRect frameBounds;
  frameBounds.origin = scrollOffset.ToCGPoint();
  frameBounds.size = viewportSize.ToCGSize();

  // If we are scrolling we don't resize the WebView immediately.
  if (!is_scrolling_ && !IsTesting()) {
    view_bounds_ = gfx::Rect(frameBounds);
  }
  [ui_view_->view_ setFrame:frameBounds];
}

void RenderWidgetHostViewIOS::ApplyRootScrollOffsetChanged(
    const gfx::PointF& root_scroll_offset,
    bool force) {
  if (last_root_scroll_offset_ != root_scroll_offset || force) {
    last_root_scroll_offset_ = root_scroll_offset;
    UpdateFrameBounds();
    UIScrollView* scrollView = (UIScrollView*)[ui_view_->view_ superview];
    [scrollView setContentOffset:root_scroll_offset.ToCGPoint()];
    [[scrollView delegate] scrollViewDidScroll:scrollView];
  } else {
    UpdateFrameBounds();
  }
}

void RenderWidgetHostViewIOS::OnRenderFrameMetadataChangedBeforeActivation(
    const cc::RenderFrameMetadata& metadata) {
  UIScrollView* scrollView = (UIScrollView*)[ui_view_->view_ superview];
  CGSize newContentSize = metadata.root_layer_size.ToCGSize();
  if (!CGSizeEqualToSize([scrollView contentSize], newContentSize)) {
    [scrollView setContentSize:newContentSize];
  }
  if (metadata.root_scroll_offset) {
    ApplyRootScrollOffsetChanged(*metadata.root_scroll_offset, /*force=*/false);
  }
}

void RenderWidgetHostViewIOS::ContentInsetChanged() {
  if (last_root_scroll_offset_) {
    ApplyRootScrollOffsetChanged(*last_root_scroll_offset_, /*force=*/true);
  }
  if (!is_scrolling_) {
    host()->SynchronizeVisualProperties();
  }
}

gfx::Size RenderWidgetHostViewIOS::GetCompositorViewportPixelSize() {
  return gfx::ScaleToCeiledSize(
      IsTesting() ? GetRequestedRendererSize() : GetScreenInfo().rect.size(),
      GetDeviceScaleFactor());
}

}  // namespace content