#![allow(clippy::unnecessary_cast)] use std::cell::RefCell; use icrate::Foundation::{CGFloat, CGRect, MainThreadMarker, NSObject, NSSet}; use objc2::rc::Id; use objc2::runtime::AnyClass; use objc2::{ declare_class, extern_methods, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass, }; use super::app_state::{self, EventWrapper}; use super::uikit::{ UIEvent, UIForceTouchCapability, UIGestureRecognizerState, UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, UITouch, UITouchPhase, UITouchType, UITraitCollection, UIView, }; use super::window::WinitUIWindow; use crate::{ dpi::PhysicalPosition, event::{Event, Force, Touch, TouchPhase, WindowEvent}, platform_impl::platform::DEVICE_ID, window::{WindowAttributes, WindowId as RootWindowId}, }; pub struct WinitViewState { pinch_gesture_recognizer: RefCell>>, doubletap_gesture_recognizer: RefCell>>, rotation_gesture_recognizer: RefCell>>, } declare_class!( pub(crate) struct WinitView; unsafe impl ClassType for WinitView { #[inherits(UIResponder, NSObject)] type Super = UIView; type Mutability = mutability::InteriorMutable; 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::Resized(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::Resized(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 = match recognizer.state() { UIGestureRecognizerState::Began => TouchPhase::Started, UIGestureRecognizerState::Changed => TouchPhase::Moved, UIGestureRecognizerState::Ended => TouchPhase::Ended, UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { TouchPhase::Cancelled } state => panic!("unexpected recognizer state: {:?}", state), }; let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.id()), event: WindowEvent::PinchGesture { device_id: DEVICE_ID, delta: recognizer.velocity() as _, 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: DEVICE_ID, }, }); 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 = match recognizer.state() { UIGestureRecognizerState::Began => TouchPhase::Started, UIGestureRecognizerState::Changed => TouchPhase::Moved, UIGestureRecognizerState::Ended => TouchPhase::Ended, UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { TouchPhase::Cancelled } state => panic!("unexpected recognizer state: {:?}", state), }; // Flip the velocity to match macOS. let delta = -recognizer.velocity() as _; let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { window_id: RootWindowId(window.id()), event: WindowEvent::RotationGesture { device_id: DEVICE_ID, delta, phase, }, }); let mtm = MainThreadMarker::new().unwrap(); app_state::handle_nonuser_event(mtm, gesture_event); } } ); extern_methods!( #[allow(non_snake_case)] unsafe impl WinitView { fn window(&self) -> Option> { unsafe { msg_send_id![self, window] } } unsafe fn traitCollection(&self) -> Id { msg_send_id![self, traitCollection] } // TODO: Allow the user to customize this #[method(layerClass)] pub(crate) fn layerClass() -> &'static AnyClass; } ); impl WinitView { pub(crate) fn new( _mtm: MainThreadMarker, window_attributes: &WindowAttributes, frame: CGRect, ) -> Id { let this = Self::alloc().set_ivars(WinitViewState { pinch_gesture_recognizer: RefCell::new(None), doubletap_gesture_recognizer: RefCell::new(None), rotation_gesture_recognizer: RefCell::new(None), }); let this: Id = 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 } pub(crate) fn recognize_pinch_gesture(&self, should_recognize: bool) { if should_recognize { if self.ivars().pinch_gesture_recognizer.borrow().is_none() { let pinch: Id = unsafe { msg_send_id![UIPinchGestureRecognizer::alloc(), initWithTarget: self, action: sel!(pinchGesture:)] }; 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_doubletap_gesture(&self, should_recognize: bool) { if should_recognize { if self.ivars().doubletap_gesture_recognizer.borrow().is_none() { let tap: Id = unsafe { msg_send_id![UITapGestureRecognizer::alloc(), initWithTarget: self, action: sel!(doubleTapGesture:)] }; 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) { if should_recognize { if self.ivars().rotation_gesture_recognizer.borrow().is_none() { let rotation: Id = unsafe { msg_send_id![UIRotationGestureRecognizer::alloc(), initWithTarget: self, action: sel!(rotationGesture:)] }; 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.type_(); let force = if os_supports_force { let trait_collection = unsafe { 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 u64; 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 as i32), }; 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: DEVICE_ID, id: touch_id, location: physical_location, force, phase, }), })); } let mtm = MainThreadMarker::new().unwrap(); app_state::handle_nonuser_events(mtm, touch_events); } }