Add UIGestureRecognizerDelegate and PanGestureRecogniser (#3597)

- Allow all gestures simultaneously recognized.
- Add PanGestureRecogniser with min/max number of touches.
- Fix sending delta values relative to Update instead to match macOS.
- Fix rotation gesture units from iOS to be in degrees instead of radians.

Co-authored-by: Mads Marquart <mads@marquart.dk>
This commit is contained in:
Joshua Pedrick 2024-04-27 09:55:04 -04:00 committed by GitHub
parent 94664ff687
commit fd477986de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 289 additions and 31 deletions

View file

@ -1,9 +1,13 @@
use objc2::encode::{Encode, Encoding};
use objc2::{extern_class, extern_methods, mutability, ClassType};
use objc2_foundation::{CGFloat, NSInteger, NSObject, NSUInteger};
use objc2::rc::Id;
use objc2::runtime::ProtocolObject;
use objc2::{extern_class, extern_methods, extern_protocol, mutability, ClassType, ProtocolType};
use objc2_foundation::{CGFloat, CGPoint, NSInteger, NSObject, NSObjectProtocol, NSUInteger};
use super::UIView;
// https://developer.apple.com/documentation/uikit/uigesturerecognizer
extern_class!(
/// [`UIGestureRecognizer`](https://developer.apple.com/documentation/uikit/uigesturerecognizer)
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIGestureRecognizer;
@ -17,6 +21,14 @@ extern_methods!(
unsafe impl UIGestureRecognizer {
#[method(state)]
pub fn state(&self) -> UIGestureRecognizerState;
/// [`delegate`](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624207-delegate?language=objc)
/// @property(nullable, nonatomic, weak) id<UIGestureRecognizerDelegate> delegate;
#[method(setDelegate:)]
pub fn setDelegate(&self, delegate: &ProtocolObject<dyn UIGestureRecognizerDelegate>);
#[method_id(delegate)]
pub fn delegate(&self) -> Id<ProtocolObject<dyn UIGestureRecognizerDelegate>>;
}
);
@ -24,7 +36,7 @@ unsafe impl Encode for UIGestureRecognizer {
const ENCODING: Encoding = Encoding::Object;
}
// https://developer.apple.com/documentation/uikit/uigesturerecognizer/state
// [`UIGestureRecognizerState`](https://developer.apple.com/documentation/uikit/uigesturerecognizer/state)
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UIGestureRecognizerState(NSInteger);
@ -43,7 +55,7 @@ impl UIGestureRecognizerState {
pub const Failed: Self = Self(5);
}
// https://developer.apple.com/documentation/uikit/uipinchgesturerecognizer
// [`UIPinchGestureRecognizer`](https://developer.apple.com/documentation/uikit/uipinchgesturerecognizer)
extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIPinchGestureRecognizer;
@ -68,8 +80,8 @@ unsafe impl Encode for UIPinchGestureRecognizer {
const ENCODING: Encoding = Encoding::Object;
}
// https://developer.apple.com/documentation/uikit/uirotationgesturerecognizer
extern_class!(
/// [`UIRotationGestureRecognizer`](https://developer.apple.com/documentation/uikit/uirotationgesturerecognizer)
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIRotationGestureRecognizer;
@ -93,8 +105,8 @@ unsafe impl Encode for UIRotationGestureRecognizer {
const ENCODING: Encoding = Encoding::Object;
}
// https://developer.apple.com/documentation/uikit/uitapgesturerecognizer
extern_class!(
/// [`UITapGestureRecognizer`](https://developer.apple.com/documentation/uikit/uitapgesturerecognizer)
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UITapGestureRecognizer;
@ -117,3 +129,48 @@ extern_methods!(
unsafe impl Encode for UITapGestureRecognizer {
const ENCODING: Encoding = Encoding::Object;
}
extern_class!(
/// [`UIPanGestureRecognizer`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer)
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct UIPanGestureRecognizer;
unsafe impl ClassType for UIPanGestureRecognizer {
type Super = UIGestureRecognizer;
type Mutability = mutability::InteriorMutable;
}
);
extern_methods!(
unsafe impl UIPanGestureRecognizer {
#[method(translationInView:)]
pub fn translationInView(&self, view: &UIView) -> CGPoint;
#[method(setTranslation:inView:)]
pub fn setTranslationInView(&self, translation: CGPoint, view: &UIView);
#[method(velocityInView:)]
pub fn velocityInView(&self, view: &UIView) -> CGPoint;
#[method(setMinimumNumberOfTouches:)]
pub fn setMinimumNumberOfTouches(&self, minimum_number_of_touches: NSUInteger);
#[method(minimumNumberOfTouches)]
pub fn minimumNumberOfTouches(&self) -> NSUInteger;
#[method(setMaximumNumberOfTouches:)]
pub fn setMaximumNumberOfTouches(&self, maximum_number_of_touches: NSUInteger);
#[method(maximumNumberOfTouches)]
pub fn maximumNumberOfTouches(&self) -> NSUInteger;
}
);
extern_protocol!(
/// (@protocol UIGestureRecognizerDelegate)[https://developer.apple.com/documentation/uikit/uigesturerecognizerdelegate?language=objc]
pub(crate) unsafe trait UIGestureRecognizerDelegate: NSObjectProtocol {}
unsafe impl ProtocolType for dyn UIGestureRecognizerDelegate {
const NAME: &'static str = "UIGestureRecognizerDelegate";
}
);

View file

@ -27,8 +27,9 @@ pub(crate) use self::device::{UIDevice, UIUserInterfaceIdiom};
pub(crate) use self::event::UIEvent;
pub(crate) use self::geometry::UIRectEdge;
pub(crate) use self::gesture_recognizer::{
UIGestureRecognizer, UIGestureRecognizerState, UIPinchGestureRecognizer,
UIRotationGestureRecognizer, UITapGestureRecognizer,
UIGestureRecognizer, UIGestureRecognizerDelegate, UIGestureRecognizerState,
UIPanGestureRecognizer, UIPinchGestureRecognizer, UIRotationGestureRecognizer,
UITapGestureRecognizer,
};
pub(crate) use self::responder::UIResponder;
pub(crate) use self::screen::{UIScreen, UIScreenOverscanCompensation};

View file

@ -1,18 +1,19 @@
#![allow(clippy::unnecessary_cast)]
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use objc2::rc::Id;
use objc2::runtime::AnyClass;
use objc2::runtime::{AnyClass, NSObjectProtocol, ProtocolObject};
use objc2::{
declare_class, extern_methods, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass,
};
use objc2_foundation::{CGFloat, CGRect, MainThreadMarker, NSObject, NSSet};
use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet};
use super::app_state::{self, EventWrapper};
use super::uikit::{
UIEvent, UIForceTouchCapability, UIGestureRecognizerState, UIPinchGestureRecognizer,
UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, UITouch, UITouchPhase,
UITouchType, UITraitCollection, UIView,
UIEvent, UIForceTouchCapability, UIGestureRecognizer, UIGestureRecognizerDelegate,
UIGestureRecognizerState, UIPanGestureRecognizer, UIPinchGestureRecognizer, UIResponder,
UIRotationGestureRecognizer, UITapGestureRecognizer, UITouch, UITouchPhase, UITouchType,
UITraitCollection, UIView,
};
use super::window::WinitUIWindow;
use crate::dpi::PhysicalPosition;
@ -24,6 +25,12 @@ pub struct WinitViewState {
pinch_gesture_recognizer: RefCell<Option<Id<UIPinchGestureRecognizer>>>,
doubletap_gesture_recognizer: RefCell<Option<Id<UITapGestureRecognizer>>>,
rotation_gesture_recognizer: RefCell<Option<Id<UIRotationGestureRecognizer>>>,
pan_gesture_recognizer: RefCell<Option<Id<UIPanGestureRecognizer>>>,
// for iOS delta references the start of the Gesture
rotation_last_delta: Cell<CGFloat>,
pinch_last_delta: Cell<CGFloat>,
pan_last_delta: Cell<CGPoint>,
}
declare_class!(
@ -165,12 +172,23 @@ declare_class!(
fn pinch_gesture(&self, recognizer: &UIPinchGestureRecognizer) {
let window = self.window().unwrap();
let phase = match recognizer.state() {
UIGestureRecognizerState::Began => TouchPhase::Started,
UIGestureRecognizerState::Changed => TouchPhase::Moved,
UIGestureRecognizerState::Ended => TouchPhase::Ended,
let (phase, delta) = match recognizer.state() {
UIGestureRecognizerState::Began => {
self.ivars().pinch_last_delta.set(recognizer.scale());
(TouchPhase::Started, 0.0)
}
UIGestureRecognizerState::Changed => {
let last_scale: f64 = self.ivars().pinch_last_delta.replace(recognizer.scale());
(TouchPhase::Moved, recognizer.scale() - last_scale)
}
UIGestureRecognizerState::Ended => {
let last_scale: f64 = self.ivars().pinch_last_delta.replace(0.0);
(TouchPhase::Moved, recognizer.scale() - last_scale)
}
UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
TouchPhase::Cancelled
self.ivars().rotation_last_delta.set(0.0);
// Pass -delta so that action is reversed
(TouchPhase::Cancelled, -recognizer.scale())
}
state => panic!("unexpected recognizer state: {:?}", state),
};
@ -179,7 +197,7 @@ declare_class!(
window_id: RootWindowId(window.id()),
event: WindowEvent::PinchGesture {
device_id: DEVICE_ID,
delta: recognizer.velocity() as _,
delta: delta as f64,
phase,
},
});
@ -209,23 +227,37 @@ declare_class!(
fn rotation_gesture(&self, recognizer: &UIRotationGestureRecognizer) {
let window = self.window().unwrap();
let phase = match recognizer.state() {
UIGestureRecognizerState::Began => TouchPhase::Started,
UIGestureRecognizerState::Changed => TouchPhase::Moved,
UIGestureRecognizerState::Ended => TouchPhase::Ended,
let (phase, delta) = match recognizer.state() {
UIGestureRecognizerState::Began => {
self.ivars().rotation_last_delta.set(0.0);
(TouchPhase::Started, 0.0)
}
UIGestureRecognizerState::Changed => {
let last_rotation = self.ivars().rotation_last_delta.replace(recognizer.rotation());
(TouchPhase::Moved, recognizer.rotation() - last_rotation)
}
UIGestureRecognizerState::Ended => {
let last_rotation = self.ivars().rotation_last_delta.replace(0.0);
(TouchPhase::Ended, recognizer.rotation() - last_rotation)
}
UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
TouchPhase::Cancelled
self.ivars().rotation_last_delta.set(0.0);
// Pass -delta so that action is reversed
(TouchPhase::Cancelled, -recognizer.rotation())
}
state => panic!("unexpected recognizer state: {:?}", state),
};
// Flip the velocity to match macOS.
let delta = -recognizer.velocity() as _;
// Make delta negative to match macos, convert to degrees
let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent {
window_id: RootWindowId(window.id()),
event: WindowEvent::RotationGesture {
device_id: DEVICE_ID,
delta,
delta: -delta.to_degrees() as _,
phase,
},
});
@ -233,6 +265,66 @@ declare_class!(
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_event(mtm, gesture_event);
}
#[method(panGesture:)]
fn pan_gesture(&self, recognizer: &UIPanGestureRecognizer) {
let window = self.window().unwrap();
let translation = recognizer.translationInView(self);
let (phase, dx, dy) = match recognizer.state() {
UIGestureRecognizerState::Began => {
self.ivars().pan_last_delta.set(translation);
(TouchPhase::Started, 0.0, 0.0)
}
UIGestureRecognizerState::Changed => {
let last_pan: CGPoint = self.ivars().pan_last_delta.replace(translation);
let dx = translation.x - last_pan.x;
let dy = translation.y - last_pan.y;
(TouchPhase::Moved, dx, dy)
}
UIGestureRecognizerState::Ended => {
let last_pan: CGPoint = self.ivars().pan_last_delta.replace(CGPoint{x:0.0, y:0.0});
let dx = translation.x - last_pan.x;
let dy = translation.y - last_pan.y;
(TouchPhase::Ended, dx, dy)
}
UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
let last_pan: CGPoint = self.ivars().pan_last_delta.replace(CGPoint{x:0.0, y:0.0});
// Pass -delta so that action is reversed
(TouchPhase::Cancelled, -last_pan.x, -last_pan.y)
}
state => panic!("unexpected recognizer state: {:?}", state),
};
let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent {
window_id: RootWindowId(window.id()),
event: WindowEvent::PanGesture {
device_id: DEVICE_ID,
delta: PhysicalPosition::new(dx as _, dy as _),
phase,
},
});
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_event(mtm, gesture_event);
}
}
unsafe impl NSObjectProtocol for WinitView {}
unsafe impl UIGestureRecognizerDelegate for WinitView {
#[method(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)]
fn should_recognize_simultaneously(&self, _gesture_recognizer: &UIGestureRecognizer, _other_gesture_recognizer: &UIGestureRecognizer) -> bool {
true
}
}
);
@ -263,6 +355,11 @@ impl WinitView {
pinch_gesture_recognizer: RefCell::new(None),
doubletap_gesture_recognizer: RefCell::new(None),
rotation_gesture_recognizer: RefCell::new(None),
pan_gesture_recognizer: RefCell::new(None),
rotation_last_delta: Cell::new(0.0),
pinch_last_delta: Cell::new(0.0),
pan_last_delta: Cell::new(CGPoint { x: 0.0, y: 0.0 }),
});
let this: Id<Self> = unsafe { msg_send_id![super(this), initWithFrame: frame] };
@ -281,6 +378,7 @@ impl WinitView {
let pinch: Id<UIPinchGestureRecognizer> = unsafe {
msg_send_id![UIPinchGestureRecognizer::alloc(), initWithTarget: self, action: sel!(pinchGesture:)]
};
pinch.setDelegate(ProtocolObject::from_ref(self));
self.addGestureRecognizer(&pinch);
self.ivars().pinch_gesture_recognizer.replace(Some(pinch));
}
@ -289,12 +387,35 @@ impl WinitView {
}
}
pub(crate) fn recognize_pan_gesture(
&self,
should_recognize: bool,
minimum_number_of_touches: u8,
maximum_number_of_touches: u8,
) {
if should_recognize {
if self.ivars().pan_gesture_recognizer.borrow().is_none() {
let pan: Id<UIPanGestureRecognizer> = unsafe {
msg_send_id![UIPanGestureRecognizer::alloc(), initWithTarget: self, action: sel!(panGesture:)]
};
pan.setDelegate(ProtocolObject::from_ref(self));
pan.setMinimumNumberOfTouches(minimum_number_of_touches as _);
pan.setMaximumNumberOfTouches(maximum_number_of_touches as _);
self.addGestureRecognizer(&pan);
self.ivars().pan_gesture_recognizer.replace(Some(pan));
}
} else if let Some(recognizer) = self.ivars().pan_gesture_recognizer.take() {
self.removeGestureRecognizer(&recognizer);
}
}
pub(crate) fn recognize_doubletap_gesture(&self, should_recognize: bool) {
if should_recognize {
if self.ivars().doubletap_gesture_recognizer.borrow().is_none() {
let tap: Id<UITapGestureRecognizer> = unsafe {
msg_send_id![UITapGestureRecognizer::alloc(), initWithTarget: self, action: sel!(doubleTapGesture:)]
};
tap.setDelegate(ProtocolObject::from_ref(self));
tap.setNumberOfTapsRequired(2);
tap.setNumberOfTouchesRequired(1);
self.addGestureRecognizer(&tap);
@ -311,6 +432,7 @@ impl WinitView {
let rotation: Id<UIRotationGestureRecognizer> = unsafe {
msg_send_id![UIRotationGestureRecognizer::alloc(), initWithTarget: self, action: sel!(rotationGesture:)]
};
rotation.setDelegate(ProtocolObject::from_ref(self));
self.addGestureRecognizer(&rotation);
self.ivars().rotation_gesture_recognizer.replace(Some(rotation));
}

View file

@ -619,6 +619,19 @@ impl Inner {
self.view.recognize_pinch_gesture(should_recognize);
}
pub fn recognize_pan_gesture(
&self,
should_recognize: bool,
minimum_number_of_touches: u8,
maximum_number_of_touches: u8,
) {
self.view.recognize_pan_gesture(
should_recognize,
minimum_number_of_touches,
maximum_number_of_touches,
);
}
pub fn recognize_doubletap_gesture(&self, should_recognize: bool) {
self.view.recognize_doubletap_gesture(should_recognize);
}