chromium/components/input/web_input_event_builders_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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "components/input/web_input_event_builders_ios.h"

#import <UIKit/UIKit.h>

#include "third_party/blink/public/common/input/web_pointer_event.h"
#include "third_party/blink/public/common/input/web_touch_point.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event_utils.h"

namespace input {

namespace {

constexpr size_t MAX_POINTERS = blink::WebTouchEvent::kTouchesLengthCap;
static UITouch* g_active_touches[MAX_POINTERS] = {};

size_t GetTouchPointerId(UITouch* touch) {
  for (size_t i = 0; i < MAX_POINTERS; ++i) {
    if (g_active_touches[i] == touch) {
      return i + 1;
    }
  }
  return 0;
}

void AddUITouch(UITouch* touch) {
  CHECK(GetTouchPointerId(touch) == 0);
  for (size_t i = 0; i < MAX_POINTERS; ++i) {
    if (!g_active_touches[i]) {
      g_active_touches[i] = touch;
      return;
    }
  }
}

void RemoveUITouch(UITouch* touch) {
  for (size_t i = 0; i < MAX_POINTERS; ++i) {
    if (g_active_touches[i] == touch) {
      g_active_touches[i] = nil;
      return;
    }
  }
  CHECK(false);
}

int ModifiersFromEvent(UIEvent* event) {
  int modifiers = 0;
  UIKeyModifierFlags modifier_flags = [event modifierFlags];

  if (modifier_flags & UIKeyModifierControl) {
    modifiers |= blink::WebInputEvent::kControlKey;
  }
  if (modifier_flags & UIKeyModifierShift) {
    modifiers |= blink::WebInputEvent::kShiftKey;
  }
  if (modifier_flags & UIKeyModifierAlternate) {
    modifiers |= blink::WebInputEvent::kAltKey;
  }
  if (modifier_flags & UIKeyModifierCommand) {
    modifiers |= blink::WebInputEvent::kMetaKey;
  }
  if (modifier_flags & UIKeyModifierAlphaShift) {
    modifiers |= blink::WebInputEvent::kCapsLockOn;
  }

  return modifiers;
}

blink::WebTouchPoint::State ToWebTouchPointState(UITouch* event,
                                                 bool was_changed) {
  // We will get an event for each actual changed phase.
  if (!was_changed) {
    return blink::WebTouchPoint::State::kStateStationary;
  }

  switch ([event phase]) {
    case UITouchPhaseBegan:
    case UITouchPhaseRegionEntered:
      return blink::WebTouchPoint::State::kStatePressed;
    case UITouchPhaseMoved:
    case UITouchPhaseRegionMoved:
      return blink::WebTouchPoint::State::kStateMoved;
    case UITouchPhaseEnded:
    case UITouchPhaseRegionExited:
      return blink::WebTouchPoint::State::kStateReleased;
    case UITouchPhaseCancelled:
      return blink::WebTouchPoint::State::kStateCancelled;
    case UITouchPhaseStationary:
      return blink::WebTouchPoint::State::kStateStationary;
  }
  NOTREACHED_IN_MIGRATION() << "Invalid MotionEvent::Action.";
  return blink::WebTouchPoint::State::kStateUndefined;
}

void SetWebPointerPropertiesFromMotionEventData(
    blink::WebPointerProperties& webPointerProperties,
    int pointer_id,
    float pressure) {
  webPointerProperties.id = pointer_id;
  webPointerProperties.force = pressure;
  webPointerProperties.tilt_x = webPointerProperties.tilt_y = 0;
  webPointerProperties.twist = 0;
  webPointerProperties.tangential_pressure = 0;
  webPointerProperties.button = blink::WebPointerProperties::Button::kNoButton;
  webPointerProperties.pointer_type =
      blink::WebPointerProperties::PointerType::kTouch;
  // TODO(dtapuska): Support stylus.
}

blink::WebTouchPoint CreateWebTouchPoint(
    UIView* view,
    UITouch* event,
    bool was_changed,
    const std::optional<gfx::Vector2dF>& view_offset) {
  blink::WebTouchPoint touch;

  size_t pointer_index = GetTouchPointerId(event);
  CHECK(pointer_index != 0);

  SetWebPointerPropertiesFromMotionEventData(touch, pointer_index,
                                             [event force]);

  touch.state = ToWebTouchPointState(event, was_changed);
  gfx::PointF window_location = gfx::PointF([event locationInView:nil]);
  touch.SetPositionInScreen(window_location);

  gfx::PointF view_location;
  if (view_offset) {
    view_location = gfx::PointF(window_location);
    view_location += view_offset.value();
  } else {
    view_location = gfx::PointF([event locationInView:view]);
  }
  touch.SetPositionInWidget(view_location);

  float major_radius = event.majorRadius;
  float minor_radius = event.majorRadius;
  float orientation_deg = 0;

  DCHECK_GE(major_radius, 0);
  DCHECK_GE(minor_radius, 0);
  DCHECK_GE(major_radius, minor_radius);
  // Orientation lies in [-180, 180] for a stylus, and [-90, 90] for other
  // touchscreen inputs. There are exceptions on Android when a device is
  // rotated, yielding touch orientations in the range of [-180, 180].
  // Regardless, normalise to [-90, 90), allowing a small tolerance to account
  // for floating point conversion.
  // TODO(e_hakkinen): Also pass unaltered stylus orientation, avoiding loss of
  // quadrant information, see crbug.com/493728.
  DCHECK_GT(orientation_deg, -180.01f);
  DCHECK_LT(orientation_deg, 180.01f);
  if (orientation_deg >= 90.f) {
    orientation_deg -= 180.f;
  } else if (orientation_deg < -90.f) {
    orientation_deg += 180.f;
  }
  if (orientation_deg >= 0) {
    // The case orientation_deg == 0 is handled here on purpose: although the
    // 'else' block is equivalent in this case, we want to pass the 0 value
    // unchanged (and 0 is the default value for many devices that don't
    // report elliptical touches).
    touch.radius_x = minor_radius;
    touch.radius_y = major_radius;
    touch.rotation_angle = orientation_deg;
  } else {
    touch.radius_x = major_radius;
    touch.radius_y = minor_radius;
    touch.rotation_angle = orientation_deg + 90;
  }

  return touch;
}

}  // namespace

blink::WebKeyboardEvent WebKeyboardEventBuilder::Build(UIEvent*) {
  return blink::WebKeyboardEvent();
}

blink::WebGestureEvent WebGestureEventBuilder::Build(UIEvent*, UIView*) {
  return blink::WebGestureEvent();
}

blink::WebTouchEvent WebTouchEventBuilder::Build(
    blink::WebInputEvent::Type type,
    UITouch* touch,
    UIEvent* event,
    UIView* view,
    const std::optional<gfx::Vector2dF>& view_offset) {
  blink::WebTouchEvent result(type, ModifiersFromEvent(event),
                              ui::EventTimeStampFromSeconds([event timestamp]));
  // TODO(dtapuska): Enable
  //   ui::ComputeEventLatencyOS(event);
  result.dispatch_type =
      result.GetType() == blink::WebInputEvent::Type::kTouchCancel
          ? blink::WebInputEvent::DispatchType::kEventNonBlocking
          : blink::WebInputEvent::DispatchType::kBlocking;
  result.hovering = type == blink::WebInputEvent::Type::kTouchEnd;
  result.unique_touch_event_id = ui::GetNextTouchEventId();

  size_t touch_index = 0;
  if (type == blink::WebInputEvent::Type::kTouchStart) {
    AddUITouch(touch);
  }
  result.touches[touch_index] =
      CreateWebTouchPoint(view, touch, /*was_changed=*/true, view_offset);
  ++touch_index;
  if (type == blink::WebInputEvent::Type::kTouchCancel ||
      type == blink::WebInputEvent::Type::kTouchEnd) {
    RemoveUITouch(touch);
  }

  // We can't use `event.allTouches` here, because we need to generate a
  // WebTouchEvent for each touch point changing. But event.allTouches will have
  // it all already.
  for (size_t i = 0; i < MAX_POINTERS; ++i) {
    if (!g_active_touches[i] || g_active_touches[i] == touch) {
      continue;
    }
    result.touches[touch_index] = CreateWebTouchPoint(
        view, g_active_touches[i], /*was_changed=*/false, view_offset);
    ++touch_index;
  }
  result.touches_length = touch_index;
  DCHECK_GT(result.touches_length, 0U);

  return result;
}

}  // namespace input