#![allow(clippy::unnecessary_cast)] use std::cell::{Cell, RefCell}; use objc2::rc::Retained; use objc2::runtime::{NSObjectProtocol, ProtocolObject}; use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass}; use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet, NSString}; use objc2_ui_kit::{ UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer, UIGestureRecognizerDelegate, UIGestureRecognizerState, UIKeyInput, UIPanGestureRecognizer, UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, UITextInputTraits, UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView, }; use super::app_state::{self, EventWrapper}; use super::window::WinitUIWindow; use super::FingerId; use crate::dpi::PhysicalPosition; use crate::event::{ ElementState, Event, FingerId as RootFingerId, Force, KeyEvent, Touch, TouchPhase, WindowEvent, }; use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKeyCode, PhysicalKey}; use crate::platform_impl::KeyEventExtra; use crate::window::{WindowAttributes, WindowId as RootWindowId}; pub struct WinitViewState { pinch_gesture_recognizer: RefCell>>, doubletap_gesture_recognizer: RefCell>>, rotation_gesture_recognizer: RefCell>>, pan_gesture_recognizer: RefCell>>, // for iOS delta references the start of the Gesture rotation_last_delta: Cell, pinch_last_delta: Cell, pan_last_delta: Cell, } declare_class!( pub(crate) struct WinitView; unsafe impl ClassType for WinitView { #[inherits(UIResponder, NSObject)] type Super = UIView; type Mutability = mutability::MainThreadOnly; const NAME: &'static str = "WinitUIView"; } impl DeclaredClass for WinitView { type Ivars = WinitViewState; } unsafe impl WinitView { #[method(drawRect:)] fn draw_rect(&self, rect: CGRect) { let mtm = MainThreadMarker::new().unwrap(); let window = self.window().unwrap(); app_state::handle_nonuser_event( mtm, EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.id()), event: WindowEvent::RedrawRequested, }), ); let _: () = unsafe { msg_send![super(self), drawRect: rect] }; } #[method(layoutSubviews)] fn layout_subviews(&self) { let mtm = MainThreadMarker::new().unwrap(); let _: () = unsafe { msg_send![super(self), layoutSubviews] }; let window = self.window().unwrap(); let window_bounds = window.bounds(); let screen = window.screen(); let screen_space = screen.coordinateSpace(); let screen_frame = self.convertRect_toCoordinateSpace(window_bounds, &screen_space); let scale_factor = screen.scale(); let size = crate::dpi::LogicalSize { width: screen_frame.size.width as f64, height: screen_frame.size.height as f64, } .to_physical(scale_factor as f64); // If the app is started in landscape, the view frame and window bounds can be mismatched. // The view frame will be in portrait and the window bounds in landscape. So apply the // window bounds to the view frame to make it consistent. let view_frame = self.frame(); if view_frame != window_bounds { self.setFrame(window_bounds); } app_state::handle_nonuser_event( mtm, EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.id()), event: WindowEvent::SurfaceResized(size), }), ); } #[method(setContentScaleFactor:)] fn set_content_scale_factor(&self, untrusted_scale_factor: CGFloat) { let mtm = MainThreadMarker::new().unwrap(); let _: () = unsafe { msg_send![super(self), setContentScaleFactor: untrusted_scale_factor] }; // `window` is null when `setContentScaleFactor` is invoked prior to `[UIWindow // makeKeyAndVisible]` at window creation time (either manually or internally by // UIKit when the `UIView` is first created), in which case we send no events here let window = match self.window() { Some(window) => window, None => return, }; // `setContentScaleFactor` may be called with a value of 0, which means "reset the // content scale factor to a device-specific default value", so we can't use the // parameter here. We can query the actual factor using the getter let scale_factor = self.contentScaleFactor(); assert!( !scale_factor.is_nan() && scale_factor.is_finite() && scale_factor.is_sign_positive() && scale_factor > 0.0, "invalid scale_factor set on UIView", ); let scale_factor = scale_factor as f64; let bounds = self.bounds(); let screen = window.screen(); let screen_space = screen.coordinateSpace(); let screen_frame = self.convertRect_toCoordinateSpace(bounds, &screen_space); let size = crate::dpi::LogicalSize { width: screen_frame.size.width as f64, height: screen_frame.size.height as f64, }; let window_id = RootWindowId(window.id()); app_state::handle_nonuser_events( mtm, std::iter::once(EventWrapper::ScaleFactorChanged( app_state::ScaleFactorChanged { window, scale_factor, suggested_size: size.to_physical(scale_factor), }, )) .chain(std::iter::once(EventWrapper::StaticEvent( Event::WindowEvent { window_id, event: WindowEvent::SurfaceResized(size.to_physical(scale_factor)), }, ))), ); } #[method(touchesBegan:withEvent:)] fn touches_began(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } #[method(touchesMoved:withEvent:)] fn touches_moved(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } #[method(touchesEnded:withEvent:)] fn touches_ended(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } #[method(touchesCancelled:withEvent:)] fn touches_cancelled(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } #[method(pinchGesture:)] fn pinch_gesture(&self, recognizer: &UIPinchGestureRecognizer) { let window = self.window().unwrap(); 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 => { 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), }; let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.id()), event: WindowEvent::PinchGesture { device_id: None, delta: delta as f64, phase, }, }); let mtm = MainThreadMarker::new().unwrap(); app_state::handle_nonuser_event(mtm, gesture_event); } #[method(doubleTapGesture:)] fn double_tap_gesture(&self, recognizer: &UITapGestureRecognizer) { let window = self.window().unwrap(); if recognizer.state() == UIGestureRecognizerState::Ended { let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.id()), event: WindowEvent::DoubleTapGesture { device_id: None, }, }); let mtm = MainThreadMarker::new().unwrap(); app_state::handle_nonuser_event(mtm, gesture_event); } } #[method(rotationGesture:)] fn rotation_gesture(&self, recognizer: &UIRotationGestureRecognizer) { let window = self.window().unwrap(); 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 => { 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), }; // 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: None, delta: -delta.to_degrees() as _, phase, }, }); 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(Some(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: None, delta: PhysicalPosition::new(dx as _, dy as _), phase, }, }); let mtm = MainThreadMarker::new().unwrap(); app_state::handle_nonuser_event(mtm, gesture_event); } #[method(canBecomeFirstResponder)] fn can_become_first_responder(&self) -> bool { true } } 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 } } unsafe impl UITextInputTraits for WinitView { } unsafe impl UIKeyInput for WinitView { #[method(hasText)] fn has_text(&self) -> bool { true } #[method(insertText:)] fn insert_text(&self, text: &NSString) { self.handle_insert_text(text) } #[method(deleteBackward)] fn delete_backward(&self) { self.handle_delete_backward() } } ); impl WinitView { pub(crate) fn new( mtm: MainThreadMarker, window_attributes: &WindowAttributes, frame: CGRect, ) -> Retained { let this = mtm.alloc().set_ivars(WinitViewState { 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: Retained = unsafe { msg_send_id![super(this), initWithFrame: frame] }; this.setMultipleTouchEnabled(true); if let Some(scale_factor) = window_attributes.platform_specific.scale_factor { this.setContentScaleFactor(scale_factor as _); } this } fn window(&self) -> Option> { // SAFETY: `WinitView`s are always installed in a `WinitUIWindow` (**self).window().map(|window| unsafe { Retained::cast(window) }) } pub(crate) fn recognize_pinch_gesture(&self, should_recognize: bool) { let mtm = MainThreadMarker::from(self); if should_recognize { if self.ivars().pinch_gesture_recognizer.borrow().is_none() { let pinch = unsafe { UIPinchGestureRecognizer::initWithTarget_action( mtm.alloc(), Some(self), Some(sel!(pinchGesture:)), ) }; pinch.setDelegate(Some(ProtocolObject::from_ref(self))); self.addGestureRecognizer(&pinch); self.ivars().pinch_gesture_recognizer.replace(Some(pinch)); } } else if let Some(recognizer) = self.ivars().pinch_gesture_recognizer.take() { self.removeGestureRecognizer(&recognizer); } } pub(crate) fn recognize_pan_gesture( &self, should_recognize: bool, minimum_number_of_touches: u8, maximum_number_of_touches: u8, ) { let mtm = MainThreadMarker::from(self); if should_recognize { if self.ivars().pan_gesture_recognizer.borrow().is_none() { let pan = unsafe { UIPanGestureRecognizer::initWithTarget_action( mtm.alloc(), Some(self), Some(sel!(panGesture:)), ) }; pan.setDelegate(Some(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) { let mtm = MainThreadMarker::from(self); if should_recognize { if self.ivars().doubletap_gesture_recognizer.borrow().is_none() { let tap = unsafe { UITapGestureRecognizer::initWithTarget_action( mtm.alloc(), Some(self), Some(sel!(doubleTapGesture:)), ) }; tap.setDelegate(Some(ProtocolObject::from_ref(self))); tap.setNumberOfTapsRequired(2); tap.setNumberOfTouchesRequired(1); self.addGestureRecognizer(&tap); self.ivars().doubletap_gesture_recognizer.replace(Some(tap)); } } else if let Some(recognizer) = self.ivars().doubletap_gesture_recognizer.take() { self.removeGestureRecognizer(&recognizer); } } pub(crate) fn recognize_rotation_gesture(&self, should_recognize: bool) { let mtm = MainThreadMarker::from(self); if should_recognize { if self.ivars().rotation_gesture_recognizer.borrow().is_none() { let rotation = unsafe { UIRotationGestureRecognizer::initWithTarget_action( mtm.alloc(), Some(self), Some(sel!(rotationGesture:)), ) }; rotation.setDelegate(Some(ProtocolObject::from_ref(self))); self.addGestureRecognizer(&rotation); self.ivars().rotation_gesture_recognizer.replace(Some(rotation)); } } else if let Some(recognizer) = self.ivars().rotation_gesture_recognizer.take() { self.removeGestureRecognizer(&recognizer); } } fn handle_touches(&self, touches: &NSSet) { let window = self.window().unwrap(); let mut touch_events = Vec::new(); let os_supports_force = app_state::os_capabilities().force_touch; for touch in touches { let logical_location = touch.locationInView(None); let touch_type = touch.r#type(); let force = if os_supports_force { let trait_collection = self.traitCollection(); let touch_capability = trait_collection.forceTouchCapability(); // Both the OS _and_ the device need to be checked for force touch support. if touch_capability == UIForceTouchCapability::Available || touch_type == UITouchType::Pencil { let force = touch.force(); let max_possible_force = touch.maximumPossibleForce(); let altitude_angle: Option = if touch_type == UITouchType::Pencil { let angle = touch.altitudeAngle(); Some(angle as _) } else { None }; Some(Force::Calibrated { force: force as _, max_possible_force: max_possible_force as _, altitude_angle, }) } else { None } } else { None }; let touch_id = touch as *const UITouch as usize; let phase = touch.phase(); let phase = match phase { UITouchPhase::Began => TouchPhase::Started, UITouchPhase::Moved => TouchPhase::Moved, // 2 is UITouchPhase::Stationary and is not expected here UITouchPhase::Ended => TouchPhase::Ended, UITouchPhase::Cancelled => TouchPhase::Cancelled, _ => panic!("unexpected touch phase: {phase:?}"), }; let physical_location = { let scale_factor = self.contentScaleFactor(); PhysicalPosition::from_logical::<(f64, f64), f64>( (logical_location.x as _, logical_location.y as _), scale_factor as f64, ) }; touch_events.push(EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.id()), event: WindowEvent::Touch(Touch { device_id: None, finger_id: RootFingerId(FingerId(touch_id)), location: physical_location, force, phase, }), })); } let mtm = MainThreadMarker::new().unwrap(); app_state::handle_nonuser_events(mtm, touch_events); } fn handle_insert_text(&self, text: &NSString) { let window = self.window().unwrap(); let window_id = RootWindowId(window.id()); let mtm = MainThreadMarker::new().unwrap(); // send individual events for each character app_state::handle_nonuser_events( mtm, text.to_string().chars().flat_map(|c| { let text = smol_str::SmolStr::from_iter([c]); // Emit both press and release events [ElementState::Pressed, ElementState::Released].map(|state| { EventWrapper::StaticEvent(Event::WindowEvent { window_id, event: WindowEvent::KeyboardInput { event: KeyEvent { text: if state == ElementState::Pressed { Some(text.clone()) } else { None }, state, location: KeyLocation::Standard, repeat: false, logical_key: Key::Character(text.clone()), physical_key: PhysicalKey::Unidentified( NativeKeyCode::Unidentified, ), platform_specific: KeyEventExtra {}, }, is_synthetic: false, device_id: None, }, }) }) }), ); } fn handle_delete_backward(&self) { let window = self.window().unwrap(); let window_id = RootWindowId(window.id()); let mtm = MainThreadMarker::new().unwrap(); app_state::handle_nonuser_events( mtm, [ElementState::Pressed, ElementState::Released].map(|state| { EventWrapper::StaticEvent(Event::WindowEvent { window_id, event: WindowEvent::KeyboardInput { device_id: None, event: KeyEvent { state, logical_key: Key::Named(NamedKey::Backspace), physical_key: PhysicalKey::Code(KeyCode::Backspace), platform_specific: KeyEventExtra {}, repeat: false, location: KeyLocation::Standard, text: None, }, is_synthetic: false, }, }) }), ); } }