From ee88e38f13fbc86a7aafae1d17ad3cd4a1e761df Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 28 Dec 2022 18:36:32 +0100 Subject: [PATCH] Reduce amount of `unsafe` on iOS (#2579) * Use objc2::foundation CG types * Add safe abstraction over UIApplication * Add safe abstraction over UIDevice * Add safe abstraction over UIScreen * Add safe abstraction over UIWindow * Add safe abstraction over UIViewController * Add safe abstraction over UIView * Appease clippy --- src/platform/ios.rs | 4 +- src/platform_impl/ios/app_state.rs | 157 ++--- src/platform_impl/ios/event_loop.rs | 46 +- src/platform_impl/ios/ffi.rs | 185 +----- src/platform_impl/ios/mod.rs | 5 +- src/platform_impl/ios/monitor.rs | 225 +++---- src/platform_impl/ios/uikit/application.rs | 30 + .../ios/uikit/coordinate_space.rs | 11 + src/platform_impl/ios/uikit/device.rs | 25 + src/platform_impl/ios/uikit/event.rs | 11 + src/platform_impl/ios/uikit/mod.rs | 23 +- src/platform_impl/ios/uikit/screen.rs | 79 +++ src/platform_impl/ios/uikit/screen_mode.rs | 18 + src/platform_impl/ios/uikit/touch.rs | 64 ++ .../ios/uikit/trait_collection.rs | 32 + src/platform_impl/ios/uikit/view.rs | 81 ++- .../ios/uikit/view_controller.rs | 42 +- src/platform_impl/ios/uikit/window.rs | 26 +- src/platform_impl/ios/view.rs | 584 +++++++++--------- src/platform_impl/ios/window.rs | 362 +++++------ 20 files changed, 1048 insertions(+), 962 deletions(-) create mode 100644 src/platform_impl/ios/uikit/application.rs create mode 100644 src/platform_impl/ios/uikit/coordinate_space.rs create mode 100644 src/platform_impl/ios/uikit/device.rs create mode 100644 src/platform_impl/ios/uikit/event.rs create mode 100644 src/platform_impl/ios/uikit/screen.rs create mode 100644 src/platform_impl/ios/uikit/screen_mode.rs create mode 100644 src/platform_impl/ios/uikit/touch.rs create mode 100644 src/platform_impl/ios/uikit/trait_collection.rs diff --git a/src/platform/ios.rs b/src/platform/ios.rs index 59d1ffba..9b3159f9 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -1,5 +1,7 @@ use std::os::raw::c_void; +use objc2::rc::Id; + use crate::{ event_loop::EventLoop, monitor::{MonitorHandle, VideoMode}, @@ -239,7 +241,7 @@ pub trait MonitorHandleExtIOS { impl MonitorHandleExtIOS for MonitorHandle { #[inline] fn ui_screen(&self) -> *mut c_void { - self.inner.ui_screen() as _ + Id::as_ptr(self.inner.ui_screen()) as *mut c_void } #[inline] diff --git a/src/platform_impl/ios/app_state.rs b/src/platform_impl/ios/app_state.rs index 4972a559..4d957d6b 100644 --- a/src/platform_impl/ios/app_state.rs +++ b/src/platform_impl/ios/app_state.rs @@ -15,18 +15,19 @@ use core_foundation::runloop::{ kCFRunLoopCommonModes, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, }; -use objc2::foundation::{NSInteger, NSUInteger}; -use objc2::runtime::Object; +use objc2::foundation::{CGRect, CGSize, NSInteger}; +use objc2::rc::{Id, Shared}; use objc2::{class, msg_send, sel}; use once_cell::sync::Lazy; +use super::view::WinitUIWindow; use crate::{ dpi::LogicalSize, event::{Event, StartCause, WindowEvent}, event_loop::ControlFlow, platform_impl::platform::{ event_loop::{EventHandler, EventProxy, EventWrapper, Never}, - ffi::{id, CGRect, CGSize, NSOperatingSystemVersion}, + ffi::{id, NSOperatingSystemVersion}, }, window::WindowId as RootWindowId, }; @@ -65,25 +66,25 @@ impl Event<'static, Never> { #[must_use = "dropping `AppStateImpl` without inspecting it is probably a bug"] enum AppStateImpl { NotLaunched { - queued_windows: Vec, + queued_windows: Vec>, queued_events: Vec, - queued_gpu_redraws: HashSet, + queued_gpu_redraws: HashSet>, }, Launching { - queued_windows: Vec, + queued_windows: Vec>, queued_events: Vec, queued_event_handler: Box, - queued_gpu_redraws: HashSet, + queued_gpu_redraws: HashSet>, }, ProcessingEvents { event_handler: Box, - queued_gpu_redraws: HashSet, + queued_gpu_redraws: HashSet>, active_control_flow: ControlFlow, }, // special state to deal with reentrancy and prevent mutable aliasing. InUserCallback { queued_events: Vec, - queued_gpu_redraws: HashSet, + queued_gpu_redraws: HashSet>, }, ProcessingRedraws { event_handler: Box, @@ -106,28 +107,6 @@ struct AppState { waker: EventLoopWaker, } -impl Drop for AppState { - fn drop(&mut self) { - match self.state_mut() { - &mut AppStateImpl::NotLaunched { - ref mut queued_windows, - .. - } - | &mut AppStateImpl::Launching { - ref mut queued_windows, - .. - } => { - for &mut window in queued_windows { - unsafe { - let _: () = msg_send![window, release]; - } - } - } - _ => {} - } - } -} - impl AppState { // requires main thread unsafe fn get_mut() -> RefMut<'static, AppState> { @@ -223,7 +202,9 @@ impl AppState { }); } - fn did_finish_launching_transition(&mut self) -> (Vec, Vec) { + fn did_finish_launching_transition( + &mut self, + ) -> (Vec>, Vec) { let (windows, events, event_handler, queued_gpu_redraws) = match self.take_state() { AppStateImpl::Launching { queued_windows, @@ -380,7 +361,7 @@ impl AppState { } } - fn main_events_cleared_transition(&mut self) -> HashSet { + fn main_events_cleared_transition(&mut self) -> HashSet> { let (event_handler, queued_gpu_redraws, active_control_flow) = match self.take_state() { AppStateImpl::ProcessingEvents { event_handler, @@ -473,17 +454,13 @@ impl AppState { // requires main thread and window is a UIWindow // retains window -pub unsafe fn set_key_window(window: id) { - bug_assert!( - msg_send![window, isKindOfClass: class!(UIWindow)], - "set_key_window called with an incorrect type" - ); +pub(crate) unsafe fn set_key_window(window: &Id) { let mut this = AppState::get_mut(); match this.state_mut() { &mut AppStateImpl::NotLaunched { ref mut queued_windows, .. - } => return queued_windows.push(msg_send![window, retain]), + } => return queued_windows.push(window.clone()), &mut AppStateImpl::ProcessingEvents { .. } | &mut AppStateImpl::InUserCallback { .. } | &mut AppStateImpl::ProcessingRedraws { .. } => {} @@ -495,16 +472,12 @@ pub unsafe fn set_key_window(window: id) { } } drop(this); - msg_send![window, makeKeyAndVisible] + window.makeKeyAndVisible(); } // requires main thread and window is a UIWindow // retains window -pub unsafe fn queue_gl_or_metal_redraw(window: id) { - bug_assert!( - msg_send![window, isKindOfClass: class!(UIWindow)], - "set_key_window called with an incorrect type" - ); +pub(crate) unsafe fn queue_gl_or_metal_redraw(window: Id) { let mut this = AppState::get_mut(); match this.state_mut() { &mut AppStateImpl::NotLaunched { @@ -532,8 +505,6 @@ pub unsafe fn queue_gl_or_metal_redraw(window: id) { panic!("Attempt to create a `Window` after the app has terminated") } } - - drop(this); } // requires main thread @@ -560,30 +531,25 @@ pub unsafe fn did_finish_launching() { drop(this); for window in windows { - let count: NSUInteger = msg_send![window, retainCount]; - // make sure the window is still referenced - if count > 1 { - // Do a little screen dance here to account for windows being created before - // `UIApplicationMain` is called. This fixes visual issues such as being - // offcenter and sized incorrectly. Additionally, to fix orientation issues, we - // gotta reset the `rootViewController`. - // - // relevant iOS log: - // ``` - // [ApplicationLifecycle] Windows were created before application initialzation - // completed. This may result in incorrect visual appearance. - // ``` - let screen: id = msg_send![window, screen]; - let _: id = msg_send![screen, retain]; - let _: () = msg_send![window, setScreen:0 as id]; - let _: () = msg_send![window, setScreen: screen]; - let _: () = msg_send![screen, release]; - let controller: id = msg_send![window, rootViewController]; - let _: () = msg_send![window, setRootViewController:ptr::null::()]; - let _: () = msg_send![window, setRootViewController: controller]; - let _: () = msg_send![window, makeKeyAndVisible]; - } - let _: () = msg_send![window, release]; + // Do a little screen dance here to account for windows being created before + // `UIApplicationMain` is called. This fixes visual issues such as being + // offcenter and sized incorrectly. Additionally, to fix orientation issues, we + // gotta reset the `rootViewController`. + // + // relevant iOS log: + // ``` + // [ApplicationLifecycle] Windows were created before application initialzation + // completed. This may result in incorrect visual appearance. + // ``` + let screen = window.screen(); + let _: () = msg_send![&window, setScreen: 0 as id]; + window.setScreen(&screen); + + let controller = window.rootViewController(); + window.setRootViewController(None); + window.setRootViewController(controller.as_deref()); + + window.makeKeyAndVisible(); } let (windows, events) = AppState::get_mut().did_finish_launching_transition(); @@ -597,12 +563,7 @@ pub unsafe fn did_finish_launching() { // the above window dance hack, could possibly trigger new windows to be created. // we can just set those windows up normally, as they were created after didFinishLaunching for window in windows { - let count: NSUInteger = msg_send![window, retainCount]; - // make sure the window is still referenced - if count > 1 { - let _: () = msg_send![window, makeKeyAndVisible]; - } - let _: () = msg_send![window, release]; + window.makeKeyAndVisible(); } } @@ -620,12 +581,12 @@ pub unsafe fn handle_wakeup_transition() { } // requires main thread -pub unsafe fn handle_nonuser_event(event: EventWrapper) { +pub(crate) unsafe fn handle_nonuser_event(event: EventWrapper) { handle_nonuser_events(std::iter::once(event)) } // requires main thread -pub unsafe fn handle_nonuser_events>(events: I) { +pub(crate) unsafe fn handle_nonuser_events>(events: I) { let mut this = AppState::get_mut(); let (mut event_handler, active_control_flow, processing_redraws) = match this.try_user_callback_transition() { @@ -803,9 +764,7 @@ pub unsafe fn handle_main_events_cleared() { let mut redraw_events: Vec = this .main_events_cleared_transition() .into_iter() - .map(|window| { - EventWrapper::StaticEvent(Event::RedrawRequested(RootWindowId(window.into()))) - }) + .map(|window| EventWrapper::StaticEvent(Event::RedrawRequested(RootWindowId(window.id())))) .collect(); redraw_events.push(EventWrapper::StaticEvent(Event::RedrawEventsCleared)); @@ -838,13 +797,13 @@ fn handle_event_proxy( EventProxy::DpiChangedProxy { suggested_size, scale_factor, - window_id, + window, } => handle_hidpi_proxy( event_handler, control_flow, suggested_size, scale_factor, - window_id, + window, ), } } @@ -854,42 +813,36 @@ fn handle_hidpi_proxy( mut control_flow: ControlFlow, suggested_size: LogicalSize, scale_factor: f64, - window_id: id, + window: Id, ) { let mut size = suggested_size.to_physical(scale_factor); let new_inner_size = &mut size; let event = Event::WindowEvent { - window_id: RootWindowId(window_id.into()), + window_id: RootWindowId(window.id()), event: WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size, }, }; event_handler.handle_nonuser_event(event, &mut control_flow); - let (view, screen_frame) = get_view_and_screen_frame(window_id); + let (view, screen_frame) = get_view_and_screen_frame(&window); let physical_size = *new_inner_size; let logical_size = physical_size.to_logical(scale_factor); - let size = CGSize::new(logical_size); + let size = CGSize::new(logical_size.width, logical_size.height); let new_frame: CGRect = CGRect::new(screen_frame.origin, size); unsafe { let _: () = msg_send![view, setFrame: new_frame]; } } -fn get_view_and_screen_frame(window_id: id) -> (id, CGRect) { - unsafe { - let view_controller: id = msg_send![window_id, rootViewController]; - let view: id = msg_send![view_controller, view]; - let bounds: CGRect = msg_send![window_id, bounds]; - let screen: id = msg_send![window_id, screen]; - let screen_space: id = msg_send![screen, coordinateSpace]; - let screen_frame: CGRect = msg_send![ - window_id, - convertRect: bounds, - toCoordinateSpace: screen_space, - ]; - (view, screen_frame) - } +fn get_view_and_screen_frame(window: &WinitUIWindow) -> (id, CGRect) { + let view_controller = window.rootViewController().unwrap(); + let view: id = unsafe { msg_send![&view_controller, view] }; + let bounds = window.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = window.convertRect_toCoordinateSpace(bounds, &screen_space); + (view, screen_frame) } struct EventLoopWaker { diff --git a/src/platform_impl/ios/event_loop.rs b/src/platform_impl/ios/event_loop.rs index 5db3a6b8..de1feb42 100644 --- a/src/platform_impl/ios/event_loop.rs +++ b/src/platform_impl/ios/event_loop.rs @@ -14,10 +14,13 @@ use core_foundation::runloop::{ CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopSourceInvalidate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, }; -use objc2::runtime::Object; -use objc2::{class, msg_send, ClassType}; +use objc2::foundation::MainThreadMarker; +use objc2::rc::{Id, Shared}; +use objc2::ClassType; use raw_window_handle::{RawDisplayHandle, UiKitDisplayHandle}; +use super::uikit::{UIApplication, UIDevice, UIScreen}; +use super::view::WinitUIWindow; use crate::{ dpi::LogicalSize, event::Event, @@ -29,20 +32,20 @@ use crate::{ use crate::platform_impl::platform::{ app_state, - ffi::{id, nil, NSStringRust, UIApplicationMain, UIUserInterfaceIdiom}, + ffi::{nil, NSStringRust, UIApplicationMain}, monitor, view, MonitorHandle, }; #[derive(Debug)] -pub enum EventWrapper { +pub(crate) enum EventWrapper { StaticEvent(Event<'static, Never>), EventProxy(EventProxy), } #[derive(Debug, PartialEq)] -pub enum EventProxy { +pub(crate) enum EventProxy { DpiChangedProxy { - window_id: id, + window: Id, suggested_size: LogicalSize, scale_factor: f64, }, @@ -55,15 +58,13 @@ pub struct EventLoopWindowTarget { impl EventLoopWindowTarget { pub fn available_monitors(&self) -> VecDeque { - // guaranteed to be on main thread - unsafe { monitor::uiscreens() } + monitor::uiscreens(MainThreadMarker::new().unwrap()) } pub fn primary_monitor(&self) -> Option { - // guaranteed to be on main thread - let monitor = unsafe { monitor::main_uiscreen() }; - - Some(monitor) + Some(MonitorHandle::new(UIScreen::main( + MainThreadMarker::new().unwrap(), + ))) } pub fn raw_display_handle(&self) -> RawDisplayHandle { @@ -113,13 +114,10 @@ impl EventLoop { F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), { unsafe { - let application: *mut Object = msg_send![class!(UIApplication), sharedApplication]; - assert_eq!( - application, - ptr::null_mut(), + let _application = UIApplication::shared(MainThreadMarker::new().unwrap()).expect( "\ - `EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\n\ - Note: `EventLoop::run` calls `UIApplicationMain` on iOS" + `EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\n\ + Note: `EventLoop::run` calls `UIApplicationMain` on iOS", ); app_state::will_launch(Box::new(EventLoopHandler { f: event_handler, @@ -151,8 +149,9 @@ impl EventLoop { // EventLoopExtIOS impl EventLoop { pub fn idiom(&self) -> Idiom { - // guaranteed to be on main thread - unsafe { self::get_idiom() } + UIDevice::current(MainThreadMarker::new().unwrap()) + .userInterfaceIdiom() + .into() } } @@ -355,10 +354,3 @@ where } } } - -// must be called on main thread -pub unsafe fn get_idiom() -> Idiom { - let device: id = msg_send![class!(UIDevice), currentDevice]; - let raw_idiom: UIUserInterfaceIdiom = msg_send![device, userInterfaceIdiom]; - raw_idiom.into() -} diff --git a/src/platform_impl/ios/ffi.rs b/src/platform_impl/ios/ffi.rs index 7aef6a41..b6e52c9b 100644 --- a/src/platform_impl/ios/ffi.rs +++ b/src/platform_impl/ios/ffi.rs @@ -2,7 +2,6 @@ use std::convert::TryInto; use std::ffi::CString; -use std::ops::BitOr; use std::os::raw::{c_char, c_int}; use objc2::encode::{Encode, Encoding}; @@ -10,19 +9,11 @@ use objc2::foundation::{NSInteger, NSUInteger}; use objc2::runtime::Object; use objc2::{class, msg_send}; -use crate::{ - dpi::LogicalSize, - platform::ios::{Idiom, ScreenEdge, ValidOrientations}, -}; +use crate::platform::ios::{Idiom, ScreenEdge}; pub type id = *mut Object; pub const nil: id = 0 as id; -#[cfg(target_pointer_width = "32")] -pub type CGFloat = f32; -#[cfg(target_pointer_width = "64")] -pub type CGFloat = f64; - #[repr(C)] #[derive(Clone, Debug)] pub struct NSOperatingSystemVersion { @@ -42,116 +33,6 @@ unsafe impl Encode for NSOperatingSystemVersion { ); } -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct CGPoint { - pub x: CGFloat, - pub y: CGFloat, -} - -unsafe impl Encode for CGPoint { - const ENCODING: Encoding = Encoding::Struct("CGPoint", &[CGFloat::ENCODING, CGFloat::ENCODING]); -} - -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct CGSize { - pub width: CGFloat, - pub height: CGFloat, -} - -impl CGSize { - pub fn new(size: LogicalSize) -> CGSize { - CGSize { - width: size.width as _, - height: size.height as _, - } - } -} - -unsafe impl Encode for CGSize { - const ENCODING: Encoding = Encoding::Struct("CGSize", &[CGFloat::ENCODING, CGFloat::ENCODING]); -} - -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct CGRect { - pub origin: CGPoint, - pub size: CGSize, -} - -impl CGRect { - pub fn new(origin: CGPoint, size: CGSize) -> CGRect { - CGRect { origin, size } - } -} - -unsafe impl Encode for CGRect { - const ENCODING: Encoding = Encoding::Struct("CGRect", &[CGPoint::ENCODING, CGSize::ENCODING]); -} - -#[derive(Debug)] -#[allow(dead_code)] -#[repr(isize)] -pub enum UITouchPhase { - Began = 0, - Moved, - Stationary, - Ended, - Cancelled, -} - -unsafe impl Encode for UITouchPhase { - const ENCODING: Encoding = NSInteger::ENCODING; -} - -#[derive(Debug, PartialEq, Eq)] -#[allow(dead_code)] -#[repr(isize)] -pub enum UIForceTouchCapability { - Unknown = 0, - Unavailable, - Available, -} - -unsafe impl Encode for UIForceTouchCapability { - const ENCODING: Encoding = NSInteger::ENCODING; -} - -#[derive(Debug, PartialEq, Eq)] -#[allow(dead_code)] -#[repr(isize)] -pub enum UITouchType { - Direct = 0, - Indirect, - Pencil, -} - -unsafe impl Encode for UITouchType { - const ENCODING: Encoding = NSInteger::ENCODING; -} - -#[repr(C)] -#[derive(Debug, Clone)] -pub struct UIEdgeInsets { - pub top: CGFloat, - pub left: CGFloat, - pub bottom: CGFloat, - pub right: CGFloat, -} - -unsafe impl Encode for UIEdgeInsets { - const ENCODING: Encoding = Encoding::Struct( - "UIEdgeInsets", - &[ - CGFloat::ENCODING, - CGFloat::ENCODING, - CGFloat::ENCODING, - CGFloat::ENCODING, - ], - ); -} - #[repr(transparent)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct UIUserInterfaceIdiom(NSInteger); @@ -192,55 +73,6 @@ impl From for Idiom { } } -#[repr(transparent)] -#[derive(Clone, Copy, Debug)] -pub struct UIInterfaceOrientationMask(NSUInteger); - -unsafe impl Encode for UIInterfaceOrientationMask { - const ENCODING: Encoding = NSUInteger::ENCODING; -} - -impl UIInterfaceOrientationMask { - pub const Portrait: UIInterfaceOrientationMask = UIInterfaceOrientationMask(1 << 1); - pub const PortraitUpsideDown: UIInterfaceOrientationMask = UIInterfaceOrientationMask(1 << 2); - pub const LandscapeLeft: UIInterfaceOrientationMask = UIInterfaceOrientationMask(1 << 4); - pub const LandscapeRight: UIInterfaceOrientationMask = UIInterfaceOrientationMask(1 << 3); - pub const Landscape: UIInterfaceOrientationMask = - UIInterfaceOrientationMask(Self::LandscapeLeft.0 | Self::LandscapeRight.0); - pub const AllButUpsideDown: UIInterfaceOrientationMask = - UIInterfaceOrientationMask(Self::Landscape.0 | Self::Portrait.0); - pub const All: UIInterfaceOrientationMask = - UIInterfaceOrientationMask(Self::AllButUpsideDown.0 | Self::PortraitUpsideDown.0); -} - -impl BitOr for UIInterfaceOrientationMask { - type Output = Self; - - fn bitor(self, rhs: Self) -> Self { - UIInterfaceOrientationMask(self.0 | rhs.0) - } -} - -impl UIInterfaceOrientationMask { - pub fn from_valid_orientations_idiom( - valid_orientations: ValidOrientations, - idiom: Idiom, - ) -> UIInterfaceOrientationMask { - match (valid_orientations, idiom) { - (ValidOrientations::LandscapeAndPortrait, Idiom::Phone) => { - UIInterfaceOrientationMask::AllButUpsideDown - } - (ValidOrientations::LandscapeAndPortrait, _) => UIInterfaceOrientationMask::All, - (ValidOrientations::Landscape, _) => UIInterfaceOrientationMask::Landscape, - (ValidOrientations::Portrait, Idiom::Phone) => UIInterfaceOrientationMask::Portrait, - (ValidOrientations::Portrait, _) => { - UIInterfaceOrientationMask::Portrait - | UIInterfaceOrientationMask::PortraitUpsideDown - } - } - } -} - #[repr(transparent)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct UIRectEdge(NSUInteger); @@ -267,21 +99,6 @@ impl From for ScreenEdge { } } -#[repr(transparent)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct UIScreenOverscanCompensation(NSInteger); - -unsafe impl Encode for UIScreenOverscanCompensation { - const ENCODING: Encoding = NSInteger::ENCODING; -} - -#[allow(dead_code)] -impl UIScreenOverscanCompensation { - pub const Scale: UIScreenOverscanCompensation = UIScreenOverscanCompensation(0); - pub const InsetBounds: UIScreenOverscanCompensation = UIScreenOverscanCompensation(1); - pub const None: UIScreenOverscanCompensation = UIScreenOverscanCompensation(2); -} - #[link(name = "UIKit", kind = "framework")] extern "C" { pub fn UIApplicationMain( diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index be5e6fad..fc52ee95 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -87,18 +87,19 @@ pub(crate) use self::{ window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId}, }; +use self::uikit::UIScreen; pub(crate) use crate::icon::NoIcon as PlatformIcon; pub(self) use crate::platform_impl::Fullscreen; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DeviceId { - uiscreen: ffi::id, + uiscreen: *const UIScreen, } impl DeviceId { pub const unsafe fn dummy() -> Self { DeviceId { - uiscreen: std::ptr::null_mut(), + uiscreen: std::ptr::null(), } } } diff --git a/src/platform_impl/ios/monitor.rs b/src/platform_impl/ios/monitor.rs index 7f7e037b..f5273ebd 100644 --- a/src/platform_impl/ios/monitor.rs +++ b/src/platform_impl/ios/monitor.rs @@ -6,74 +6,43 @@ use std::{ ops::{Deref, DerefMut}, }; -use objc2::foundation::{NSInteger, NSUInteger}; -use objc2::{class, msg_send}; +use objc2::foundation::{MainThreadMarker, NSInteger}; +use objc2::rc::{Id, Shared}; +use super::uikit::{UIScreen, UIScreenMode}; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, monitor::VideoMode as RootVideoMode, - platform_impl::platform::{ - app_state, - ffi::{id, nil, CGFloat, CGRect, CGSize}, - }, + platform_impl::platform::app_state, }; -#[derive(Debug, PartialEq, Eq, Hash)] +// TODO(madsmtm): Remove or refactor this +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub(crate) struct ScreenModeSendSync(pub(crate) Id); + +unsafe impl Send for ScreenModeSendSync {} +unsafe impl Sync for ScreenModeSendSync {} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct VideoMode { pub(crate) size: (u32, u32), pub(crate) bit_depth: u16, pub(crate) refresh_rate_millihertz: u32, - pub(crate) screen_mode: NativeDisplayMode, + pub(crate) screen_mode: ScreenModeSendSync, pub(crate) monitor: MonitorHandle, } -#[derive(Debug, PartialEq, Eq, Hash)] -pub struct NativeDisplayMode(pub id); - -unsafe impl Send for NativeDisplayMode {} - -impl Drop for NativeDisplayMode { - fn drop(&mut self) { - unsafe { - let _: () = msg_send![self.0, release]; - } - } -} - -impl Clone for NativeDisplayMode { - fn clone(&self) -> Self { - unsafe { - let _: id = msg_send![self.0, retain]; - } - NativeDisplayMode(self.0) - } -} - -impl Clone for VideoMode { - fn clone(&self) -> VideoMode { - VideoMode { - size: self.size, - bit_depth: self.bit_depth, - refresh_rate_millihertz: self.refresh_rate_millihertz, - screen_mode: self.screen_mode.clone(), - monitor: self.monitor.clone(), - } - } -} - impl VideoMode { - unsafe fn retained_new(uiscreen: id, screen_mode: id) -> VideoMode { + fn new(uiscreen: Id, screen_mode: Id) -> VideoMode { assert_main_thread!("`VideoMode` can only be created on the main thread on iOS"); - let refresh_rate_millihertz = refresh_rate_millihertz(uiscreen); - let size: CGSize = msg_send![screen_mode, size]; - let screen_mode: id = msg_send![screen_mode, retain]; - let screen_mode = NativeDisplayMode(screen_mode); + let refresh_rate_millihertz = refresh_rate_millihertz(&uiscreen); + let size = screen_mode.size(); VideoMode { size: (size.width as u32, size.height as u32), bit_depth: 32, refresh_rate_millihertz, - screen_mode, - monitor: MonitorHandle::retained_new(uiscreen), + screen_mode: ScreenModeSendSync(screen_mode), + monitor: MonitorHandle::new(uiscreen), } } @@ -94,22 +63,27 @@ impl VideoMode { } } -#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, PartialEq, Eq, Hash)] pub struct Inner { - uiscreen: id, + uiscreen: Id, } -impl Drop for Inner { - fn drop(&mut self) { - unsafe { - let _: () = msg_send![self.uiscreen, release]; - } +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct MonitorHandle { + inner: Inner, +} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } -#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct MonitorHandle { - inner: Inner, +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // TODO: Make a better ordering + (self as *const Self).cmp(&(other as *const Self)) + } } impl Deref for MonitorHandle { @@ -131,12 +105,6 @@ impl DerefMut for MonitorHandle { unsafe impl Send for MonitorHandle {} unsafe impl Sync for MonitorHandle {} -impl Clone for MonitorHandle { - fn clone(&self) -> MonitorHandle { - MonitorHandle::retained_new(self.uiscreen) - } -} - impl Drop for MonitorHandle { fn drop(&mut self) { assert_main_thread!("`MonitorHandle` can only be dropped on the main thread on iOS"); @@ -167,12 +135,9 @@ impl fmt::Debug for MonitorHandle { } impl MonitorHandle { - pub fn retained_new(uiscreen: id) -> MonitorHandle { - assert_main_thread!("`MonitorHandle` can only be cloned on the main thread on iOS"); - unsafe { - let _: id = msg_send![uiscreen, retain]; - } - MonitorHandle { + pub(crate) fn new(uiscreen: Id) -> Self { + assert_main_thread!("`MonitorHandle` can only be created on the main thread on iOS"); + Self { inner: Inner { uiscreen }, } } @@ -180,70 +145,62 @@ impl MonitorHandle { impl Inner { pub fn name(&self) -> Option { - unsafe { - let main = main_uiscreen(); - if self.uiscreen == main.uiscreen { - Some("Primary".to_string()) - } else if self.uiscreen == mirrored_uiscreen(&main).uiscreen { - Some("Mirrored".to_string()) - } else { - uiscreens() - .iter() - .position(|rhs| rhs.uiscreen == self.uiscreen) - .map(|idx| idx.to_string()) - } + let main = UIScreen::main(MainThreadMarker::new().unwrap()); + if self.uiscreen == main { + Some("Primary".to_string()) + } else if self.uiscreen == main.mirroredScreen() { + Some("Mirrored".to_string()) + } else { + UIScreen::screens(MainThreadMarker::new().unwrap()) + .iter() + .position(|rhs| rhs == &*self.uiscreen) + .map(|idx| idx.to_string()) } } pub fn size(&self) -> PhysicalSize { - unsafe { - let bounds: CGRect = msg_send![self.ui_screen(), nativeBounds]; - PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32) - } + let bounds = self.uiscreen.nativeBounds(); + PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32) } pub fn position(&self) -> PhysicalPosition { - unsafe { - let bounds: CGRect = msg_send![self.ui_screen(), nativeBounds]; - (bounds.origin.x as f64, bounds.origin.y as f64).into() - } + let bounds = self.uiscreen.nativeBounds(); + (bounds.origin.x as f64, bounds.origin.y as f64).into() } pub fn scale_factor(&self) -> f64 { - unsafe { - let scale: CGFloat = msg_send![self.ui_screen(), nativeScale]; - scale as f64 - } + self.uiscreen.nativeScale() as f64 } pub fn refresh_rate_millihertz(&self) -> Option { - Some(refresh_rate_millihertz(self.uiscreen)) + Some(refresh_rate_millihertz(&self.uiscreen)) } pub fn video_modes(&self) -> impl Iterator { - let mut modes = BTreeSet::new(); - unsafe { - let available_modes: id = msg_send![self.uiscreen, availableModes]; - let available_mode_count: NSUInteger = msg_send![available_modes, count]; + // Use Ord impl of RootVideoMode + let modes: BTreeSet<_> = self + .uiscreen + .availableModes() + .into_iter() + .map(|mode| { + let mode: *const UIScreenMode = mode; + let mode = unsafe { Id::retain(mode as *mut UIScreenMode).unwrap() }; - for i in 0..available_mode_count { - let mode: id = msg_send![available_modes, objectAtIndex: i]; - // Use Ord impl of RootVideoMode - modes.insert(RootVideoMode { - video_mode: VideoMode::retained_new(self.uiscreen, mode), - }); - } - } + RootVideoMode { + video_mode: VideoMode::new(self.uiscreen.clone(), mode), + } + }) + .collect(); modes.into_iter().map(|mode| mode.video_mode) } } -fn refresh_rate_millihertz(uiscreen: id) -> u32 { - let refresh_rate_millihertz: NSInteger = unsafe { +fn refresh_rate_millihertz(uiscreen: &UIScreen) -> u32 { + let refresh_rate_millihertz: NSInteger = { let os_capabilities = app_state::os_capabilities(); if os_capabilities.maximum_frames_per_second { - msg_send![uiscreen, maximumFramesPerSecond] + uiscreen.maximumFramesPerSecond() } else { // https://developer.apple.com/library/archive/technotes/tn2460/_index.html // https://en.wikipedia.org/wiki/IPad_Pro#Model_comparison @@ -265,41 +222,25 @@ fn refresh_rate_millihertz(uiscreen: id) -> u32 { // MonitorHandleExtIOS impl Inner { - pub fn ui_screen(&self) -> id { - self.uiscreen + pub(crate) fn ui_screen(&self) -> &Id { + &self.uiscreen } pub fn preferred_video_mode(&self) -> VideoMode { - unsafe { - let mode: id = msg_send![self.uiscreen, preferredMode]; - VideoMode::retained_new(self.uiscreen, mode) - } + VideoMode::new( + self.uiscreen.clone(), + self.uiscreen.preferredMode().unwrap(), + ) } } -// requires being run on main thread -pub unsafe fn main_uiscreen() -> MonitorHandle { - let uiscreen: id = msg_send![class!(UIScreen), mainScreen]; - MonitorHandle::retained_new(uiscreen) -} - -// requires being run on main thread -unsafe fn mirrored_uiscreen(monitor: &MonitorHandle) -> MonitorHandle { - let uiscreen: id = msg_send![monitor.uiscreen, mirroredScreen]; - MonitorHandle::retained_new(uiscreen) -} - -// requires being run on main thread -pub unsafe fn uiscreens() -> VecDeque { - let screens: id = msg_send![class!(UIScreen), screens]; - let count: NSUInteger = msg_send![screens, count]; - let mut result = VecDeque::with_capacity(count as _); - let screens_enum: id = msg_send![screens, objectEnumerator]; - loop { - let screen: id = msg_send![screens_enum, nextObject]; - if screen == nil { - break result; - } - result.push_back(MonitorHandle::retained_new(screen)); - } +pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque { + UIScreen::screens(mtm) + .into_iter() + .map(|screen| { + let screen: *const UIScreen = screen; + let screen = unsafe { Id::retain(screen as *mut UIScreen).unwrap() }; + MonitorHandle::new(screen) + }) + .collect() } diff --git a/src/platform_impl/ios/uikit/application.rs b/src/platform_impl/ios/uikit/application.rs new file mode 100644 index 00000000..319a6b03 --- /dev/null +++ b/src/platform_impl/ios/uikit/application.rs @@ -0,0 +1,30 @@ +use objc2::foundation::{CGRect, MainThreadMarker, NSArray, NSObject}; +use objc2::rc::{Id, Shared}; +use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; + +use super::{UIResponder, UIWindow}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct UIApplication; + + unsafe impl ClassType for UIApplication { + #[inherits(NSObject)] + type Super = UIResponder; + } +); + +extern_methods!( + unsafe impl UIApplication { + pub fn shared(_mtm: MainThreadMarker) -> Option> { + unsafe { msg_send_id![Self::class(), sharedApplication] } + } + + pub fn windows(&self) -> Id, Shared> { + unsafe { msg_send_id![self, windows] } + } + + #[sel(statusBarFrame)] + pub fn statusBarFrame(&self) -> CGRect; + } +); diff --git a/src/platform_impl/ios/uikit/coordinate_space.rs b/src/platform_impl/ios/uikit/coordinate_space.rs new file mode 100644 index 00000000..d4d1c06e --- /dev/null +++ b/src/platform_impl/ios/uikit/coordinate_space.rs @@ -0,0 +1,11 @@ +use objc2::foundation::NSObject; +use objc2::{extern_class, ClassType}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct UICoordinateSpace; + + unsafe impl ClassType for UICoordinateSpace { + type Super = NSObject; + } +); diff --git a/src/platform_impl/ios/uikit/device.rs b/src/platform_impl/ios/uikit/device.rs new file mode 100644 index 00000000..9910025a --- /dev/null +++ b/src/platform_impl/ios/uikit/device.rs @@ -0,0 +1,25 @@ +use objc2::foundation::{MainThreadMarker, NSObject}; +use objc2::rc::{Id, Shared}; +use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; + +use super::super::ffi::UIUserInterfaceIdiom; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct UIDevice; + + unsafe impl ClassType for UIDevice { + type Super = NSObject; + } +); + +extern_methods!( + unsafe impl UIDevice { + pub fn current(_mtm: MainThreadMarker) -> Id { + unsafe { msg_send_id![Self::class(), currentDevice] } + } + + #[sel(userInterfaceIdiom)] + pub fn userInterfaceIdiom(&self) -> UIUserInterfaceIdiom; + } +); diff --git a/src/platform_impl/ios/uikit/event.rs b/src/platform_impl/ios/uikit/event.rs new file mode 100644 index 00000000..9ce24261 --- /dev/null +++ b/src/platform_impl/ios/uikit/event.rs @@ -0,0 +1,11 @@ +use objc2::foundation::NSObject; +use objc2::{extern_class, ClassType}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct UIEvent; + + unsafe impl ClassType for UIEvent { + type Super = NSObject; + } +); diff --git a/src/platform_impl/ios/uikit/mod.rs b/src/platform_impl/ios/uikit/mod.rs index 232f2cc6..ffd8ab91 100644 --- a/src/platform_impl/ios/uikit/mod.rs +++ b/src/platform_impl/ios/uikit/mod.rs @@ -1,11 +1,30 @@ #![deny(unsafe_op_in_unsafe_fn)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +mod application; +mod coordinate_space; +mod device; +mod event; mod responder; +mod screen; +mod screen_mode; +mod touch; +mod trait_collection; mod view; mod view_controller; mod window; +pub(crate) use self::application::UIApplication; +pub(crate) use self::coordinate_space::UICoordinateSpace; +pub(crate) use self::device::UIDevice; +pub(crate) use self::event::UIEvent; pub(crate) use self::responder::UIResponder; -pub(crate) use self::view::UIView; -pub(crate) use self::view_controller::UIViewController; +pub(crate) use self::screen::{UIScreen, UIScreenOverscanCompensation}; +pub(crate) use self::screen_mode::UIScreenMode; +pub(crate) use self::touch::{UITouch, UITouchPhase, UITouchType}; +pub(crate) use self::trait_collection::{UIForceTouchCapability, UITraitCollection}; +#[allow(unused_imports)] +pub(crate) use self::view::{UIEdgeInsets, UIView}; +pub(crate) use self::view_controller::{UIInterfaceOrientationMask, UIViewController}; pub(crate) use self::window::UIWindow; diff --git a/src/platform_impl/ios/uikit/screen.rs b/src/platform_impl/ios/uikit/screen.rs new file mode 100644 index 00000000..770b9237 --- /dev/null +++ b/src/platform_impl/ios/uikit/screen.rs @@ -0,0 +1,79 @@ +use objc2::encode::{Encode, Encoding}; +use objc2::foundation::{CGFloat, CGRect, MainThreadMarker, NSArray, NSInteger, NSObject}; +use objc2::rc::{Id, Shared}; +use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; + +use super::{UICoordinateSpace, UIScreenMode}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct UIScreen; + + unsafe impl ClassType for UIScreen { + type Super = NSObject; + } +); + +extern_methods!( + unsafe impl UIScreen { + pub fn main(_mtm: MainThreadMarker) -> Id { + unsafe { msg_send_id![Self::class(), mainScreen] } + } + + pub fn screens(_mtm: MainThreadMarker) -> Id, Shared> { + unsafe { msg_send_id![Self::class(), screens] } + } + + #[sel(bounds)] + pub fn bounds(&self) -> CGRect; + + #[sel(scale)] + pub fn scale(&self) -> CGFloat; + + #[sel(nativeBounds)] + pub fn nativeBounds(&self) -> CGRect; + + #[sel(nativeScale)] + pub fn nativeScale(&self) -> CGFloat; + + #[sel(maximumFramesPerSecond)] + pub fn maximumFramesPerSecond(&self) -> NSInteger; + + pub fn mirroredScreen(&self) -> Id { + unsafe { msg_send_id![Self::class(), mirroredScreen] } + } + + pub fn preferredMode(&self) -> Option> { + unsafe { msg_send_id![self, preferredMode] } + } + + #[sel(setCurrentMode:)] + pub fn setCurrentMode(&self, mode: Option<&UIScreenMode>); + + pub fn availableModes(&self) -> Id, Shared> { + unsafe { msg_send_id![self, availableModes] } + } + + #[sel(setOverscanCompensation:)] + pub fn setOverscanCompensation(&self, overscanCompensation: UIScreenOverscanCompensation); + + pub fn coordinateSpace(&self) -> Id { + unsafe { msg_send_id![self, coordinateSpace] } + } + } +); + +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct UIScreenOverscanCompensation(NSInteger); + +unsafe impl Encode for UIScreenOverscanCompensation { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +#[allow(dead_code)] +impl UIScreenOverscanCompensation { + pub const Scale: Self = Self(0); + pub const InsetBounds: Self = Self(1); + pub const None: Self = Self(2); +} diff --git a/src/platform_impl/ios/uikit/screen_mode.rs b/src/platform_impl/ios/uikit/screen_mode.rs new file mode 100644 index 00000000..1ef1a3dc --- /dev/null +++ b/src/platform_impl/ios/uikit/screen_mode.rs @@ -0,0 +1,18 @@ +use objc2::foundation::{CGSize, NSObject}; +use objc2::{extern_class, extern_methods, ClassType}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct UIScreenMode; + + unsafe impl ClassType for UIScreenMode { + type Super = NSObject; + } +); + +extern_methods!( + unsafe impl UIScreenMode { + #[sel(size)] + pub fn size(&self) -> CGSize; + } +); diff --git a/src/platform_impl/ios/uikit/touch.rs b/src/platform_impl/ios/uikit/touch.rs new file mode 100644 index 00000000..d0c7c2c7 --- /dev/null +++ b/src/platform_impl/ios/uikit/touch.rs @@ -0,0 +1,64 @@ +use objc2::encode::{Encode, Encoding}; +use objc2::foundation::{CGFloat, CGPoint, NSInteger, NSObject}; +use objc2::{extern_class, extern_methods, ClassType}; + +use super::UIView; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct UITouch; + + unsafe impl ClassType for UITouch { + type Super = NSObject; + } +); + +extern_methods!( + unsafe impl UITouch { + #[sel(locationInView:)] + pub fn locationInView(&self, view: Option<&UIView>) -> CGPoint; + + #[sel(type)] + pub fn type_(&self) -> UITouchType; + + #[sel(force)] + pub fn force(&self) -> CGFloat; + + #[sel(maximumPossibleForce)] + pub fn maximumPossibleForce(&self) -> CGFloat; + + #[sel(altitudeAngle)] + pub fn altitudeAngle(&self) -> CGFloat; + + #[sel(phase)] + pub fn phase(&self) -> UITouchPhase; + } +); + +#[derive(Debug, PartialEq, Eq)] +#[allow(dead_code)] +#[repr(isize)] +pub enum UITouchType { + Direct = 0, + Indirect, + Pencil, +} + +unsafe impl Encode for UITouchType { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +#[derive(Debug)] +#[allow(dead_code)] +#[repr(isize)] +pub enum UITouchPhase { + Began = 0, + Moved, + Stationary, + Ended, + Cancelled, +} + +unsafe impl Encode for UITouchPhase { + const ENCODING: Encoding = NSInteger::ENCODING; +} diff --git a/src/platform_impl/ios/uikit/trait_collection.rs b/src/platform_impl/ios/uikit/trait_collection.rs new file mode 100644 index 00000000..7ae58d2d --- /dev/null +++ b/src/platform_impl/ios/uikit/trait_collection.rs @@ -0,0 +1,32 @@ +use objc2::encode::{Encode, Encoding}; +use objc2::foundation::{NSInteger, NSObject}; +use objc2::{extern_class, extern_methods, ClassType}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct UITraitCollection; + + unsafe impl ClassType for UITraitCollection { + type Super = NSObject; + } +); + +extern_methods!( + unsafe impl UITraitCollection { + #[sel(forceTouchCapability)] + pub fn forceTouchCapability(&self) -> UIForceTouchCapability; + } +); + +#[derive(Debug, PartialEq, Eq)] +#[allow(dead_code)] +#[repr(isize)] +pub enum UIForceTouchCapability { + Unknown = 0, + Unavailable, + Available, +} + +unsafe impl Encode for UIForceTouchCapability { + const ENCODING: Encoding = NSInteger::ENCODING; +} diff --git a/src/platform_impl/ios/uikit/view.rs b/src/platform_impl/ios/uikit/view.rs index 4646845e..05c22956 100644 --- a/src/platform_impl/ios/uikit/view.rs +++ b/src/platform_impl/ios/uikit/view.rs @@ -1,7 +1,9 @@ -use objc2::foundation::NSObject; -use objc2::{extern_class, ClassType}; +use objc2::encode::{Encode, Encoding}; +use objc2::foundation::{CGFloat, CGRect, NSObject}; +use objc2::rc::{Id, Shared}; +use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; -use super::UIResponder; +use super::{UICoordinateSpace, UIResponder, UIViewController}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -12,3 +14,76 @@ extern_class!( type Super = UIResponder; } ); + +extern_methods!( + unsafe impl UIView { + #[sel(bounds)] + pub fn bounds(&self) -> CGRect; + + #[sel(setBounds:)] + pub fn setBounds(&self, value: CGRect); + + #[sel(frame)] + pub fn frame(&self) -> CGRect; + + #[sel(setFrame:)] + pub fn setFrame(&self, value: CGRect); + + #[sel(contentScaleFactor)] + pub fn contentScaleFactor(&self) -> CGFloat; + + #[sel(setContentScaleFactor:)] + pub fn setContentScaleFactor(&self, val: CGFloat); + + #[sel(setMultipleTouchEnabled:)] + pub fn setMultipleTouchEnabled(&self, val: bool); + + pub fn rootViewController(&self) -> Option> { + unsafe { msg_send_id![self, rootViewController] } + } + + #[sel(setRootViewController:)] + pub fn setRootViewController(&self, rootViewController: Option<&UIViewController>); + + #[sel(convertRect:toCoordinateSpace:)] + pub fn convertRect_toCoordinateSpace( + &self, + rect: CGRect, + coordinateSpace: &UICoordinateSpace, + ) -> CGRect; + + #[sel(convertRect:fromCoordinateSpace:)] + pub fn convertRect_fromCoordinateSpace( + &self, + rect: CGRect, + coordinateSpace: &UICoordinateSpace, + ) -> CGRect; + + #[sel(safeAreaInsets)] + pub fn safeAreaInsets(&self) -> UIEdgeInsets; + + #[sel(setNeedsDisplay)] + pub fn setNeedsDisplay(&self); + } +); + +#[repr(C)] +#[derive(Debug, Clone)] +pub struct UIEdgeInsets { + pub top: CGFloat, + pub left: CGFloat, + pub bottom: CGFloat, + pub right: CGFloat, +} + +unsafe impl Encode for UIEdgeInsets { + const ENCODING: Encoding = Encoding::Struct( + "UIEdgeInsets", + &[ + CGFloat::ENCODING, + CGFloat::ENCODING, + CGFloat::ENCODING, + CGFloat::ENCODING, + ], + ); +} diff --git a/src/platform_impl/ios/uikit/view_controller.rs b/src/platform_impl/ios/uikit/view_controller.rs index bacd1d97..b3665805 100644 --- a/src/platform_impl/ios/uikit/view_controller.rs +++ b/src/platform_impl/ios/uikit/view_controller.rs @@ -1,7 +1,8 @@ -use objc2::foundation::NSObject; -use objc2::{extern_class, ClassType}; +use objc2::encode::{Encode, Encoding}; +use objc2::foundation::{NSObject, NSUInteger}; +use objc2::{extern_class, extern_methods, ClassType}; -use super::UIResponder; +use super::{UIResponder, UIView}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] @@ -12,3 +13,38 @@ extern_class!( type Super = UIResponder; } ); + +extern_methods!( + unsafe impl UIViewController { + #[sel(attemptRotationToDeviceOrientation)] + pub fn attemptRotationToDeviceOrientation(); + + #[sel(setNeedsStatusBarAppearanceUpdate)] + pub fn setNeedsStatusBarAppearanceUpdate(&self); + + #[sel(setNeedsUpdateOfHomeIndicatorAutoHidden)] + pub fn setNeedsUpdateOfHomeIndicatorAutoHidden(&self); + + #[sel(setNeedsUpdateOfScreenEdgesDeferringSystemGestures)] + pub fn setNeedsUpdateOfScreenEdgesDeferringSystemGestures(&self); + + #[sel(setView:)] + pub fn setView(&self, view: Option<&UIView>); + } +); + +bitflags! { + pub struct UIInterfaceOrientationMask: NSUInteger { + const Portrait = 1 << 1; + const PortraitUpsideDown = 1 << 2; + const LandscapeRight = 1 << 3; + const LandscapeLeft = 1 << 4; + const Landscape = Self::LandscapeLeft.bits() | Self::LandscapeRight.bits(); + const AllButUpsideDown = Self::Landscape.bits() | Self::Portrait.bits(); + const All = Self::AllButUpsideDown.bits() | Self::PortraitUpsideDown.bits(); + } +} + +unsafe impl Encode for UIInterfaceOrientationMask { + const ENCODING: Encoding = NSUInteger::ENCODING; +} diff --git a/src/platform_impl/ios/uikit/window.rs b/src/platform_impl/ios/uikit/window.rs index dfd45494..190cf3c1 100644 --- a/src/platform_impl/ios/uikit/window.rs +++ b/src/platform_impl/ios/uikit/window.rs @@ -1,14 +1,32 @@ use objc2::foundation::NSObject; -use objc2::{extern_class, ClassType}; +use objc2::rc::{Id, Shared}; +use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; -use super::UIResponder; +use super::{UIResponder, UIScreen, UIView}; extern_class!( #[derive(Debug, PartialEq, Eq, Hash)] pub(crate) struct UIWindow; unsafe impl ClassType for UIWindow { - #[inherits(NSObject)] - type Super = UIResponder; + #[inherits(UIResponder, NSObject)] + type Super = UIView; + } +); + +extern_methods!( + unsafe impl UIWindow { + pub fn screen(&self) -> Id { + unsafe { msg_send_id![self, screen] } + } + + #[sel(setScreen:)] + pub fn setScreen(&self, screen: &UIScreen); + + #[sel(setHidden:)] + pub fn setHidden(&self, flag: bool); + + #[sel(makeKeyAndVisible)] + pub fn makeKeyAndVisible(&self); } ); diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs index bc27360f..04da35b0 100644 --- a/src/platform_impl/ios/view.rs +++ b/src/platform_impl/ios/view.rs @@ -1,19 +1,24 @@ #![allow(clippy::unnecessary_cast)] -use objc2::foundation::NSObject; -use objc2::{class, declare_class, msg_send, ClassType}; +use objc2::foundation::{CGFloat, CGRect, MainThreadMarker, NSObject, NSSet}; +use objc2::rc::{Id, Shared}; +use objc2::runtime::Class; +use objc2::{declare_class, extern_methods, msg_send, msg_send_id, ClassType}; -use super::uikit::{UIResponder, UIView, UIViewController, UIWindow}; +use super::uikit::{ + UIApplication, UIDevice, UIEvent, UIForceTouchCapability, UIInterfaceOrientationMask, + UIResponder, UITouch, UITouchPhase, UITouchType, UITraitCollection, UIView, UIViewController, + UIWindow, +}; +use super::window::WindowId; use crate::{ dpi::PhysicalPosition, event::{DeviceId as RootDeviceId, Event, Force, Touch, TouchPhase, WindowEvent}, + platform::ios::ValidOrientations, platform_impl::platform::{ app_state, - event_loop::{self, EventProxy, EventWrapper}, - ffi::{ - id, nil, CGFloat, CGPoint, CGRect, UIForceTouchCapability, UIInterfaceOrientationMask, - UIRectEdge, UITouchPhase, UITouchType, - }, + event_loop::{EventProxy, EventWrapper}, + ffi::{id, UIRectEdge, UIUserInterfaceIdiom}, window::PlatformSpecificWindowBuilderAttributes, DeviceId, Fullscreen, }, @@ -21,7 +26,7 @@ use crate::{ }; declare_class!( - struct WinitView {} + pub(crate) struct WinitView {} unsafe impl ClassType for WinitView { #[inherits(UIResponder, NSObject)] @@ -32,53 +37,47 @@ declare_class!( unsafe impl WinitView { #[sel(drawRect:)] fn draw_rect(&self, rect: CGRect) { + let window = self.window().unwrap(); unsafe { - let window: id = msg_send![self, window]; - assert!(!window.is_null()); app_state::handle_nonuser_events( std::iter::once(EventWrapper::StaticEvent(Event::RedrawRequested( - RootWindowId(window.into()), + RootWindowId(window.id()), ))) .chain(std::iter::once(EventWrapper::StaticEvent( Event::RedrawEventsCleared, ))), ); - let _: () = msg_send![super(self), drawRect: rect]; } + let _: () = unsafe { msg_send![super(self), drawRect: rect] }; } #[sel(layoutSubviews)] fn layout_subviews(&self) { + 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); + } + unsafe { - let _: () = msg_send![super(self), layoutSubviews]; - - let window: id = msg_send![self, window]; - assert!(!window.is_null()); - let window_bounds: CGRect = msg_send![window, bounds]; - let screen: id = msg_send![window, screen]; - let screen_space: id = msg_send![screen, coordinateSpace]; - let screen_frame: CGRect = msg_send![ - self, - convertRect: window_bounds, - toCoordinateSpace: screen_space, - ]; - let scale_factor: CGFloat = msg_send![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: CGRect = msg_send![self, frame]; - if view_frame != window_bounds { - let _: () = msg_send![self, setFrame: window_bounds]; - } - app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: RootWindowId(window.into()), + window_id: RootWindowId(window.id()), event: WindowEvent::Resized(size), })); } @@ -86,45 +85,47 @@ declare_class!( #[sel(setContentScaleFactor:)] fn set_content_scale_factor(&self, untrusted_scale_factor: CGFloat) { - unsafe { - let _: () = msg_send![super(self), setContentScaleFactor: untrusted_scale_factor]; + let _: () = + unsafe { msg_send![super(self), setContentScaleFactor: untrusted_scale_factor] }; - let window: id = msg_send![self, window]; - // `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 - if window.is_null() { - 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: CGFloat = msg_send![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 bounds: CGRect = msg_send![self, bounds]; - let screen: id = msg_send![window, screen]; - let screen_space: id = msg_send![screen, coordinateSpace]; - let screen_frame: CGRect = - msg_send![self, convertRect: bounds, toCoordinateSpace: screen_space]; - let size = crate::dpi::LogicalSize { - width: screen_frame.size.width as _, - height: screen_frame.size.height as _, - }; + // `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 _, + height: screen_frame.size.height as _, + }; + let window_id = RootWindowId(window.id()); + unsafe { app_state::handle_nonuser_events( std::iter::once(EventWrapper::EventProxy(EventProxy::DpiChangedProxy { - window_id: window, + window, scale_factor, suggested_size: size, })) .chain(std::iter::once(EventWrapper::StaticEvent( Event::WindowEvent { - window_id: RootWindowId(window.into()), + window_id, event: WindowEvent::Resized(size.to_physical(scale_factor)), }, ))), @@ -133,104 +134,134 @@ declare_class!( } #[sel(touchesBegan:withEvent:)] - fn touches_began(&self, touches: id, _: id) { + fn touches_began(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } #[sel(touchesMoved:withEvent:)] - fn touches_moved(&self, touches: id, _: id) { + fn touches_moved(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } #[sel(touchesEnded:withEvent:)] - fn touches_ended(&self, touches: id, _: id) { + fn touches_ended(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } #[sel(touchesCancelled:withEvent:)] - fn touches_cancelled(&self, touches: id, _: id) { + fn touches_cancelled(&self, touches: &NSSet, _event: Option<&UIEvent>) { self.handle_touches(touches) } } ); +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 + #[sel(layerClass)] + pub(crate) fn layerClass() -> &'static Class; + } +); + impl WinitView { - fn handle_touches(&self, touches: id) { - unsafe { - let window: id = msg_send![self, window]; - assert!(!window.is_null()); - let uiscreen: id = msg_send![window, screen]; - let touches_enum: id = msg_send![touches, objectEnumerator]; - let mut touch_events = Vec::new(); - let os_supports_force = app_state::os_capabilities().force_touch; - loop { - let touch: id = msg_send![touches_enum, nextObject]; - if touch == nil { - break; - } - let logical_location: CGPoint = msg_send![touch, locationInView: nil]; - let touch_type: UITouchType = msg_send![touch, type]; - let force = if os_supports_force { - let trait_collection: id = msg_send![self, traitCollection]; - let touch_capability: UIForceTouchCapability = - msg_send![trait_collection, forceTouchCapability]; - // Both the OS _and_ the device need to be checked for force touch support. - if touch_capability == UIForceTouchCapability::Available { - let force: CGFloat = msg_send![touch, force]; - let max_possible_force: CGFloat = msg_send![touch, maximumPossibleForce]; - let altitude_angle: Option = if touch_type == UITouchType::Pencil { - let angle: CGFloat = msg_send![touch, altitudeAngle]; - Some(angle as _) - } else { - None - }; - Some(Force::Calibrated { - force: force as _, - max_possible_force: max_possible_force as _, - altitude_angle, - }) + pub(crate) fn new( + _mtm: MainThreadMarker, + _window_attributes: &WindowAttributes, + platform_attributes: &PlatformSpecificWindowBuilderAttributes, + frame: CGRect, + ) -> Id { + let this: Id = + unsafe { msg_send_id![msg_send_id![Self::class(), alloc], initWithFrame: frame] }; + + this.setMultipleTouchEnabled(true); + + if let Some(scale_factor) = platform_attributes.scale_factor { + this.setContentScaleFactor(scale_factor as _); + } + + this + } + + fn handle_touches(&self, touches: &NSSet) { + let window = self.window().unwrap(); + let uiscreen = window.screen(); + 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 { + 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 - }; - let touch_id = touch as u64; - let phase: UITouchPhase = msg_send![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), - }; + } + } 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: CGFloat = msg_send![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.into()), - event: WindowEvent::Touch(Touch { - device_id: RootDeviceId(DeviceId { uiscreen }), - id: touch_id, - location: physical_location, - force, - 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: RootDeviceId(DeviceId { + uiscreen: Id::as_ptr(&uiscreen), }), - })); - } + id: touch_id, + location: physical_location, + force, + phase, + }), + })); + } + unsafe { app_state::handle_nonuser_events(touch_events); } } } declare_class!( - struct WinitViewController { + pub(crate) struct WinitViewController { _prefers_status_bar_hidden: bool, _prefers_home_indicator_auto_hidden: bool, _supported_orientations: UIInterfaceOrientationMask, @@ -259,9 +290,7 @@ declare_class!( #[sel(setPrefersStatusBarHidden:)] fn set_prefers_status_bar_hidden(&mut self, val: bool) { *self._prefers_status_bar_hidden = val; - unsafe { - let _: () = msg_send![self, setNeedsStatusBarAppearanceUpdate]; - } + self.setNeedsStatusBarAppearanceUpdate(); } #[sel(prefersHomeIndicatorAutoHidden)] @@ -274,9 +303,7 @@ declare_class!( *self._prefers_home_indicator_auto_hidden = val; let os_capabilities = app_state::os_capabilities(); if os_capabilities.home_indicator_hidden { - unsafe { - let _: () = msg_send![self, setNeedsUpdateOfHomeIndicatorAutoHidden]; - } + self.setNeedsUpdateOfHomeIndicatorAutoHidden(); } else { os_capabilities.home_indicator_hidden_err_msg("ignoring") } @@ -290,12 +317,7 @@ declare_class!( #[sel(setSupportedInterfaceOrientations:)] fn set_supported_orientations(&mut self, val: UIInterfaceOrientationMask) { *self._supported_orientations = val; - unsafe { - let _: () = msg_send![ - UIViewController::class(), - attemptRotationToDeviceOrientation - ]; - } + UIViewController::attemptRotationToDeviceOrientation(); } #[sel(preferredScreenEdgesDeferringSystemGestures)] @@ -308,9 +330,7 @@ declare_class!( *self._preferred_screen_edges_deferring_system_gestures = val; let os_capabilities = app_state::os_capabilities(); if os_capabilities.defer_system_gestures { - unsafe { - let _: () = msg_send![self, setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; - } + self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures(); } else { os_capabilities.defer_system_gestures_err_msg("ignoring") } @@ -318,8 +338,79 @@ declare_class!( } ); +extern_methods!( + #[allow(non_snake_case)] + unsafe impl WinitViewController { + #[sel(setPrefersStatusBarHidden:)] + pub(crate) fn setPrefersStatusBarHidden(&self, flag: bool); + + #[sel(setSupportedInterfaceOrientations:)] + pub(crate) fn setSupportedInterfaceOrientations(&self, val: UIInterfaceOrientationMask); + + #[sel(setPrefersHomeIndicatorAutoHidden:)] + pub(crate) fn setPrefersHomeIndicatorAutoHidden(&self, val: bool); + + #[sel(setPreferredScreenEdgesDeferringSystemGestures:)] + pub(crate) fn setPreferredScreenEdgesDeferringSystemGestures(&self, val: UIRectEdge); + } +); + +impl WinitViewController { + pub(crate) fn set_supported_interface_orientations( + &self, + mtm: MainThreadMarker, + valid_orientations: ValidOrientations, + ) { + let mask = match ( + valid_orientations, + UIDevice::current(mtm).userInterfaceIdiom(), + ) { + (ValidOrientations::LandscapeAndPortrait, UIUserInterfaceIdiom::Phone) => { + UIInterfaceOrientationMask::AllButUpsideDown + } + (ValidOrientations::LandscapeAndPortrait, _) => UIInterfaceOrientationMask::All, + (ValidOrientations::Landscape, _) => UIInterfaceOrientationMask::Landscape, + (ValidOrientations::Portrait, UIUserInterfaceIdiom::Phone) => { + UIInterfaceOrientationMask::Portrait + } + (ValidOrientations::Portrait, _) => { + UIInterfaceOrientationMask::Portrait + | UIInterfaceOrientationMask::PortraitUpsideDown + } + }; + self.setSupportedInterfaceOrientations(mask); + } + + pub(crate) fn new( + mtm: MainThreadMarker, + _window_attributes: &WindowAttributes, + platform_attributes: &PlatformSpecificWindowBuilderAttributes, + view: &UIView, + ) -> Id { + let this: Id = + unsafe { msg_send_id![msg_send_id![Self::class(), alloc], init] }; + + this.setPrefersStatusBarHidden(platform_attributes.prefers_status_bar_hidden); + + this.set_supported_interface_orientations(mtm, platform_attributes.valid_orientations); + + this.setPrefersHomeIndicatorAutoHidden(platform_attributes.prefers_home_indicator_hidden); + + this.setPreferredScreenEdgesDeferringSystemGestures( + platform_attributes + .preferred_screen_edges_deferring_system_gestures + .into(), + ); + + this.setView(Some(view)); + + this + } +} + declare_class!( - struct WinitUIWindow {} + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct WinitUIWindow {} unsafe impl ClassType for WinitUIWindow { #[inherits(UIResponder, NSObject)] @@ -331,128 +422,59 @@ declare_class!( fn become_key_window(&self) { unsafe { app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: RootWindowId((&*****self).into()), + window_id: RootWindowId(self.id()), event: WindowEvent::Focused(true), })); - let _: () = msg_send![super(self), becomeKeyWindow]; } + let _: () = unsafe { msg_send![super(self), becomeKeyWindow] }; } #[sel(resignKeyWindow)] fn resign_key_window(&self) { unsafe { app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: RootWindowId((&*****self).into()), + window_id: RootWindowId(self.id()), event: WindowEvent::Focused(false), })); - let _: () = msg_send![super(self), resignKeyWindow]; } + let _: () = unsafe { msg_send![super(self), resignKeyWindow] }; } } ); -// requires main thread -pub(crate) unsafe fn create_view( - _window_attributes: &WindowAttributes, - platform_attributes: &PlatformSpecificWindowBuilderAttributes, - frame: CGRect, -) -> id { - let view: id = msg_send![WinitView::class(), alloc]; - assert!(!view.is_null(), "Failed to create `UIView` instance"); - let view: id = msg_send![view, initWithFrame: frame]; - assert!(!view.is_null(), "Failed to initialize `UIView` instance"); - let _: () = msg_send![view, setMultipleTouchEnabled: true]; - if let Some(scale_factor) = platform_attributes.scale_factor { - let _: () = msg_send![view, setContentScaleFactor: scale_factor as CGFloat]; +impl WinitUIWindow { + pub(crate) fn new( + _mtm: MainThreadMarker, + window_attributes: &WindowAttributes, + _platform_attributes: &PlatformSpecificWindowBuilderAttributes, + frame: CGRect, + view_controller: &UIViewController, + ) -> Id { + let this: Id = + unsafe { msg_send_id![msg_send_id![Self::class(), alloc], initWithFrame: frame] }; + + this.setRootViewController(Some(view_controller)); + + match window_attributes.fullscreen { + Some(Fullscreen::Exclusive(ref video_mode)) => { + let monitor = video_mode.monitor(); + let screen = monitor.ui_screen(); + screen.setCurrentMode(Some(&video_mode.screen_mode.0)); + this.setScreen(screen); + } + Some(Fullscreen::Borderless(Some(ref monitor))) => { + let screen = monitor.ui_screen(); + this.setScreen(screen); + } + _ => (), + } + + this } - view -} - -// requires main thread -pub(crate) unsafe fn create_view_controller( - _window_attributes: &WindowAttributes, - platform_attributes: &PlatformSpecificWindowBuilderAttributes, - view: id, -) -> id { - let class = WinitViewController::class(); - - let view_controller: id = msg_send![class, alloc]; - assert!( - !view_controller.is_null(), - "Failed to create `UIViewController` instance" - ); - let view_controller: id = msg_send![view_controller, init]; - assert!( - !view_controller.is_null(), - "Failed to initialize `UIViewController` instance" - ); - let status_bar_hidden = platform_attributes.prefers_status_bar_hidden; - let idiom = event_loop::get_idiom(); - let supported_orientations = UIInterfaceOrientationMask::from_valid_orientations_idiom( - platform_attributes.valid_orientations, - idiom, - ); - let prefers_home_indicator_hidden = platform_attributes.prefers_home_indicator_hidden; - let edges: UIRectEdge = platform_attributes - .preferred_screen_edges_deferring_system_gestures - .into(); - let _: () = msg_send![ - view_controller, - setPrefersStatusBarHidden: status_bar_hidden - ]; - let _: () = msg_send![ - view_controller, - setSupportedInterfaceOrientations: supported_orientations - ]; - let _: () = msg_send![ - view_controller, - setPrefersHomeIndicatorAutoHidden: prefers_home_indicator_hidden - ]; - let _: () = msg_send![ - view_controller, - setPreferredScreenEdgesDeferringSystemGestures: edges - ]; - let _: () = msg_send![view_controller, setView: view]; - view_controller -} - -// requires main thread -pub(crate) unsafe fn create_window( - window_attributes: &WindowAttributes, - _platform_attributes: &PlatformSpecificWindowBuilderAttributes, - frame: CGRect, - view_controller: id, -) -> id { - let window: id = msg_send![WinitUIWindow::class(), alloc]; - assert!(!window.is_null(), "Failed to create `UIWindow` instance"); - let window: id = msg_send![window, initWithFrame: frame]; - assert!( - !window.is_null(), - "Failed to initialize `UIWindow` instance" - ); - let _: () = msg_send![window, setRootViewController: view_controller]; - match window_attributes.fullscreen { - Some(Fullscreen::Exclusive(ref video_mode)) => { - let uiscreen = video_mode.monitor().ui_screen() as id; - let _: () = msg_send![uiscreen, setCurrentMode: video_mode.screen_mode.0]; - msg_send![window, setScreen:video_mode.monitor().ui_screen()] - } - Some(Fullscreen::Borderless(ref monitor)) => { - let uiscreen: id = match &monitor { - Some(monitor) => monitor.ui_screen() as id, - None => { - let uiscreen: id = msg_send![window, screen]; - uiscreen - } - }; - - msg_send![window, setScreen: uiscreen] - } - None => (), + pub(crate) fn id(&self) -> WindowId { + (self as *const Self as usize as u64).into() } - - window } declare_class!( @@ -465,7 +487,7 @@ declare_class!( // UIApplicationDelegate protocol unsafe impl WinitApplicationDelegate { #[sel(application:didFinishLaunchingWithOptions:)] - fn did_finish_launching(&self, _: id, _: id) -> bool { + fn did_finish_launching(&self, _application: &UIApplication, _: id) -> bool { unsafe { app_state::did_finish_launching(); } @@ -473,40 +495,38 @@ declare_class!( } #[sel(applicationDidBecomeActive:)] - fn did_become_active(&self, _: id) { + fn did_become_active(&self, _application: &UIApplication) { unsafe { app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Resumed)) } } #[sel(applicationWillResignActive:)] - fn will_resign_active(&self, _: id) { + fn will_resign_active(&self, _application: &UIApplication) { unsafe { app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Suspended)) } } #[sel(applicationWillEnterForeground:)] - fn will_enter_foreground(&self, _: id) {} + fn will_enter_foreground(&self, _application: &UIApplication) {} #[sel(applicationDidEnterBackground:)] - fn did_enter_background(&self, _: id) {} + fn did_enter_background(&self, _application: &UIApplication) {} #[sel(applicationWillTerminate:)] - fn will_terminate(&self, _: id) { - unsafe { - let app: id = msg_send![class!(UIApplication), sharedApplication]; - let windows: id = msg_send![app, windows]; - let windows_enum: id = msg_send![windows, objectEnumerator]; - let mut events = Vec::new(); - loop { - let window: id = msg_send![windows_enum, nextObject]; - if window == nil { - break; - } - let is_winit_window = msg_send![window, isKindOfClass: WinitUIWindow::class()]; - if is_winit_window { - events.push(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: RootWindowId(window.into()), - event: WindowEvent::Destroyed, - })); - } + fn will_terminate(&self, application: &UIApplication) { + let mut events = Vec::new(); + for window in application.windows().iter() { + if window.is_kind_of::() { + // SAFETY: We just checked that the window is a `winit` window + let window = unsafe { + let ptr: *const UIWindow = window; + let ptr: *const WinitUIWindow = ptr.cast(); + &*ptr + }; + events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Destroyed, + })); } + } + unsafe { app_state::handle_nonuser_events(events); app_state::terminated(); } diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 12c0cbfd..466dacb6 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -5,10 +5,14 @@ use std::{ ops::{Deref, DerefMut}, }; -use objc2::runtime::{Class, Object}; +use objc2::foundation::{CGFloat, CGPoint, CGRect, CGSize, MainThreadMarker}; +use objc2::rc::{Id, Shared}; +use objc2::runtime::Object; use objc2::{class, msg_send}; use raw_window_handle::{RawDisplayHandle, RawWindowHandle, UiKitDisplayHandle, UiKitWindowHandle}; +use super::uikit::{UIApplication, UIScreen, UIScreenOverscanCompensation}; +use super::view::{WinitUIWindow, WinitView, WinitViewController}; use crate::{ dpi::{self, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, @@ -17,12 +21,9 @@ use crate::{ platform::ios::{ScreenEdge, ValidOrientations}, platform_impl::platform::{ app_state, - event_loop::{self, EventProxy, EventWrapper}, - ffi::{ - id, CGFloat, CGPoint, CGRect, CGSize, UIEdgeInsets, UIInterfaceOrientationMask, - UIRectEdge, UIScreenOverscanCompensation, - }, - monitor, view, EventLoopWindowTarget, Fullscreen, MonitorHandle, + event_loop::{EventProxy, EventWrapper}, + ffi::{id, UIRectEdge}, + monitor, EventLoopWindowTarget, Fullscreen, MonitorHandle, }, window::{ CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons, @@ -31,29 +32,19 @@ use crate::{ }; pub struct Inner { - pub window: id, - pub view_controller: id, - pub view: id, + pub(crate) window: Id, + pub(crate) view_controller: Id, + pub(crate) view: Id, gl_or_metal_backed: bool, } -impl Drop for Inner { - fn drop(&mut self) { - unsafe { - let _: () = msg_send![self.view, release]; - let _: () = msg_send![self.view_controller, release]; - let _: () = msg_send![self.window, release]; - } - } -} - impl Inner { pub fn set_title(&self, _title: &str) { debug!("`Window::set_title` is ignored on iOS") } pub fn set_visible(&self, visible: bool) { - unsafe { msg_send![self.window, setHidden: !visible] } + self.window.setHidden(!visible) } pub fn is_visible(&self) -> Option { @@ -72,9 +63,9 @@ impl Inner { // testing. // // https://developer.apple.com/documentation/uikit/uiview/1622437-setneedsdisplay?language=objc - app_state::queue_gl_or_metal_redraw(self.window); + app_state::queue_gl_or_metal_redraw(self.window.clone()); } else { - let _: () = msg_send![self.view, setNeedsDisplay]; + self.view.setNeedsDisplay(); } } } @@ -116,7 +107,7 @@ impl Inner { size: screen_frame.size, }; let bounds = self.rect_from_screen_space(new_screen_frame); - let _: () = msg_send![self.window, setBounds: bounds]; + self.window.setBounds(bounds); } } @@ -186,10 +177,7 @@ impl Inner { } pub fn scale_factor(&self) -> f64 { - unsafe { - let hidpi: CGFloat = msg_send![self.view, contentScaleFactor]; - hidpi as _ - } + self.view.contentScaleFactor() as _ } pub fn set_cursor_icon(&self, _cursor: CursorIcon) { @@ -230,40 +218,33 @@ impl Inner { } pub(crate) fn set_fullscreen(&self, monitor: Option) { - unsafe { - let uiscreen = match monitor { - Some(Fullscreen::Exclusive(video_mode)) => { - let uiscreen = video_mode.monitor.ui_screen() as id; - let _: () = msg_send![uiscreen, setCurrentMode: video_mode.screen_mode.0]; - uiscreen - } - Some(Fullscreen::Borderless(Some(monitor))) => monitor.ui_screen() as id, - Some(Fullscreen::Borderless(None)) => { - self.current_monitor_inner().ui_screen() as id - } - None => { - warn!("`Window::set_fullscreen(None)` ignored on iOS"); - return; - } - }; - - // this is pretty slow on iOS, so avoid doing it if we can - let current: id = msg_send![self.window, screen]; - if uiscreen != current { - let _: () = msg_send![self.window, setScreen: uiscreen]; + let uiscreen = match &monitor { + Some(Fullscreen::Exclusive(video_mode)) => { + let uiscreen = video_mode.monitor.ui_screen(); + uiscreen.setCurrentMode(Some(&video_mode.screen_mode.0)); + uiscreen.clone() } + Some(Fullscreen::Borderless(Some(monitor))) => monitor.ui_screen().clone(), + Some(Fullscreen::Borderless(None)) => self.current_monitor_inner().ui_screen().clone(), + None => { + warn!("`Window::set_fullscreen(None)` ignored on iOS"); + return; + } + }; - let bounds: CGRect = msg_send![uiscreen, bounds]; - let _: () = msg_send![self.window, setFrame: bounds]; - - // For external displays, we must disable overscan compensation or - // the displayed image will have giant black bars surrounding it on - // each side - let _: () = msg_send![ - uiscreen, - setOverscanCompensation: UIScreenOverscanCompensation::None - ]; + // this is pretty slow on iOS, so avoid doing it if we can + let current = self.window.screen(); + if uiscreen != current { + self.window.setScreen(&uiscreen); } + + let bounds = uiscreen.bounds(); + self.window.setFrame(bounds); + + // For external displays, we must disable overscan compensation or + // the displayed image will have giant black bars surrounding it on + // each side + uiscreen.setOverscanCompensation(UIScreenOverscanCompensation::None); } pub(crate) fn fullscreen(&self) -> Option { @@ -271,7 +252,7 @@ impl Inner { let monitor = self.current_monitor_inner(); let uiscreen = monitor.ui_screen(); let screen_space_bounds = self.screen_frame(); - let screen_bounds: CGRect = msg_send![uiscreen, bounds]; + let screen_bounds = uiscreen.bounds(); // TODO: track fullscreen instead of relying on brittle float comparisons if screen_space_bounds.origin.x == screen_bounds.origin.x @@ -321,10 +302,7 @@ impl Inner { // Allow directly accessing the current monitor internally without unwrapping. fn current_monitor_inner(&self) -> MonitorHandle { - unsafe { - let uiscreen: id = msg_send![self.window, screen]; - MonitorHandle::retained_new(uiscreen) - } + MonitorHandle::new(self.window.screen()) } pub fn current_monitor(&self) -> Option { @@ -332,23 +310,24 @@ impl Inner { } pub fn available_monitors(&self) -> VecDeque { - unsafe { monitor::uiscreens() } + monitor::uiscreens(MainThreadMarker::new().unwrap()) } pub fn primary_monitor(&self) -> Option { - let monitor = unsafe { monitor::main_uiscreen() }; - Some(monitor) + Some(MonitorHandle::new(UIScreen::main( + MainThreadMarker::new().unwrap(), + ))) } pub fn id(&self) -> WindowId { - self.window.into() + self.window.id() } pub fn raw_window_handle(&self) -> RawWindowHandle { let mut window_handle = UiKitWindowHandle::empty(); - window_handle.ui_window = self.window as _; - window_handle.ui_view = self.view as _; - window_handle.ui_view_controller = self.view_controller as _; + window_handle.ui_window = Id::as_ptr(&self.window) as _; + window_handle.ui_view = Id::as_ptr(&self.view) as _; + window_handle.ui_view_controller = Id::as_ptr(&self.view_controller) as _; RawWindowHandle::UiKit(window_handle) } @@ -407,6 +386,8 @@ impl Window { window_attributes: WindowAttributes, platform_attributes: PlatformSpecificWindowBuilderAttributes, ) -> Result { + let mtm = MainThreadMarker::new().unwrap(); + if window_attributes.min_inner_size.is_some() { warn!("`WindowAttributes::min_inner_size` is ignored on iOS"); } @@ -416,199 +397,162 @@ impl Window { // TODO: transparency, visible - unsafe { - let screen = match window_attributes.fullscreen { - Some(Fullscreen::Exclusive(ref video_mode)) => video_mode.monitor.ui_screen() as id, - Some(Fullscreen::Borderless(Some(ref monitor))) => monitor.ui_screen(), - Some(Fullscreen::Borderless(None)) | None => { - monitor::main_uiscreen().ui_screen() as id + let main_screen = UIScreen::main(mtm); + let screen = match window_attributes.fullscreen { + Some(Fullscreen::Exclusive(ref video_mode)) => video_mode.monitor.ui_screen(), + Some(Fullscreen::Borderless(Some(ref monitor))) => monitor.ui_screen(), + Some(Fullscreen::Borderless(None)) | None => &main_screen, + }; + + let screen_bounds = screen.bounds(); + + let frame = match window_attributes.inner_size { + Some(dim) => { + let scale_factor = screen.scale(); + let size = dim.to_logical::(scale_factor as f64); + CGRect { + origin: screen_bounds.origin, + size: CGSize { + width: size.width as _, + height: size.height as _, + }, } + } + None => screen_bounds, + }; + + let view = WinitView::new(mtm, &window_attributes, &platform_attributes, frame); + + let gl_or_metal_backed = unsafe { + let layer_class = WinitView::layerClass(); + let is_metal = msg_send![layer_class, isSubclassOfClass: class!(CAMetalLayer)]; + let is_gl = msg_send![layer_class, isSubclassOfClass: class!(CAEAGLLayer)]; + is_metal || is_gl + }; + + let view_controller = + WinitViewController::new(mtm, &window_attributes, &platform_attributes, &view); + let window = WinitUIWindow::new( + mtm, + &window_attributes, + &platform_attributes, + frame, + &view_controller, + ); + + unsafe { app_state::set_key_window(&window) }; + + // Like the Windows and macOS backends, we send a `ScaleFactorChanged` and `Resized` + // event on window creation if the DPI factor != 1.0 + let scale_factor = view.contentScaleFactor(); + let scale_factor = scale_factor as f64; + if scale_factor != 1.0 { + let bounds = view.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = view.convertRect_toCoordinateSpace(bounds, &screen_space); + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as _, + height: screen_frame.size.height as _, }; - - let screen_bounds: CGRect = msg_send![screen, bounds]; - - let frame = match window_attributes.inner_size { - Some(dim) => { - let scale_factor: CGFloat = msg_send![screen, scale]; - let size = dim.to_logical::(scale_factor as f64); - CGRect { - origin: screen_bounds.origin, - size: CGSize { - width: size.width as _, - height: size.height as _, - }, - } - } - None => screen_bounds, - }; - - let view = view::create_view(&window_attributes, &platform_attributes, frame); - - let gl_or_metal_backed = { - let view_class: *const Class = msg_send![view, class]; - let layer_class: *const Class = msg_send![view_class, layerClass]; - let is_metal = msg_send![layer_class, isSubclassOfClass: class!(CAMetalLayer)]; - let is_gl = msg_send![layer_class, isSubclassOfClass: class!(CAEAGLLayer)]; - is_metal || is_gl - }; - - let view_controller = - view::create_view_controller(&window_attributes, &platform_attributes, view); - let window = view::create_window( - &window_attributes, - &platform_attributes, - frame, - view_controller, - ); - - let result = Window { - inner: Inner { - window, - view_controller, - view, - gl_or_metal_backed, - }, - }; - app_state::set_key_window(window); - - // Like the Windows and macOS backends, we send a `ScaleFactorChanged` and `Resized` - // event on window creation if the DPI factor != 1.0 - let scale_factor: CGFloat = msg_send![view, contentScaleFactor]; - let scale_factor = scale_factor as f64; - if scale_factor != 1.0 { - let bounds: CGRect = msg_send![view, bounds]; - let screen: id = msg_send![window, screen]; - let screen_space: id = msg_send![screen, coordinateSpace]; - let screen_frame: CGRect = - msg_send![view, convertRect: bounds, toCoordinateSpace: screen_space]; - let size = crate::dpi::LogicalSize { - width: screen_frame.size.width as _, - height: screen_frame.size.height as _, - }; + let window_id = RootWindowId(window.id()); + unsafe { app_state::handle_nonuser_events( std::iter::once(EventWrapper::EventProxy(EventProxy::DpiChangedProxy { - window_id: window, + window: window.clone(), scale_factor, suggested_size: size, })) .chain(std::iter::once(EventWrapper::StaticEvent( Event::WindowEvent { - window_id: RootWindowId(window.into()), + window_id, event: WindowEvent::Resized(size.to_physical(scale_factor)), }, ))), ); } - - Ok(result) } + + Ok(Window { + inner: Inner { + window, + view_controller, + view, + gl_or_metal_backed, + }, + }) } } // WindowExtIOS impl Inner { pub fn ui_window(&self) -> id { - self.window + Id::as_ptr(&self.window) as id } pub fn ui_view_controller(&self) -> id { - self.view_controller + Id::as_ptr(&self.view_controller) as id } pub fn ui_view(&self) -> id { - self.view + Id::as_ptr(&self.view) as id } pub fn set_scale_factor(&self, scale_factor: f64) { - unsafe { - assert!( - dpi::validate_scale_factor(scale_factor), - "`WindowExtIOS::set_scale_factor` received an invalid hidpi factor" - ); - let scale_factor = scale_factor as CGFloat; - let _: () = msg_send![self.view, setContentScaleFactor: scale_factor]; - } + assert!( + dpi::validate_scale_factor(scale_factor), + "`WindowExtIOS::set_scale_factor` received an invalid hidpi factor" + ); + let scale_factor = scale_factor as CGFloat; + self.view.setContentScaleFactor(scale_factor); } pub fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { - unsafe { - let idiom = event_loop::get_idiom(); - let supported_orientations = UIInterfaceOrientationMask::from_valid_orientations_idiom( - valid_orientations, - idiom, - ); - msg_send![ - self.view_controller, - setSupportedInterfaceOrientations: supported_orientations - ] - } + self.view_controller.set_supported_interface_orientations( + MainThreadMarker::new().unwrap(), + valid_orientations, + ); } pub fn set_prefers_home_indicator_hidden(&self, hidden: bool) { - unsafe { - let _: () = msg_send![ - self.view_controller, - setPrefersHomeIndicatorAutoHidden: hidden, - ]; - } + self.view_controller + .setPrefersHomeIndicatorAutoHidden(hidden); } pub fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { let edges: UIRectEdge = edges.into(); - unsafe { - let _: () = msg_send![ - self.view_controller, - setPreferredScreenEdgesDeferringSystemGestures: edges - ]; - } + self.view_controller + .setPreferredScreenEdgesDeferringSystemGestures(edges); } pub fn set_prefers_status_bar_hidden(&self, hidden: bool) { - unsafe { - let _: () = msg_send![self.view_controller, setPrefersStatusBarHidden: hidden,]; - } + self.view_controller.setPrefersStatusBarHidden(hidden); } } impl Inner { // requires main thread unsafe fn screen_frame(&self) -> CGRect { - self.rect_to_screen_space(msg_send![self.window, bounds]) + self.rect_to_screen_space(self.window.bounds()) } // requires main thread unsafe fn rect_to_screen_space(&self, rect: CGRect) -> CGRect { - let screen: id = msg_send![self.window, screen]; - if !screen.is_null() { - let screen_space: id = msg_send![screen, coordinateSpace]; - msg_send![ - self.window, - convertRect: rect, - toCoordinateSpace: screen_space, - ] - } else { - rect - } + let screen_space = self.window.screen().coordinateSpace(); + self.window + .convertRect_toCoordinateSpace(rect, &screen_space) } // requires main thread unsafe fn rect_from_screen_space(&self, rect: CGRect) -> CGRect { - let screen: id = msg_send![self.window, screen]; - if !screen.is_null() { - let screen_space: id = msg_send![screen, coordinateSpace]; - msg_send![ - self.window, - convertRect: rect, - fromCoordinateSpace: screen_space, - ] - } else { - rect - } + let screen_space = self.window.screen().coordinateSpace(); + self.window + .convertRect_fromCoordinateSpace(rect, &screen_space) } // requires main thread unsafe fn safe_area_screen_space(&self) -> CGRect { - let bounds: CGRect = msg_send![self.window, bounds]; + let bounds = self.window.bounds(); if app_state::os_capabilities().safe_area { - let safe_area: UIEdgeInsets = msg_send![self.window, safeAreaInsets]; + let safe_area = self.window.safeAreaInsets(); let safe_bounds = CGRect { origin: CGPoint { x: bounds.origin.x + safe_area.left, @@ -622,13 +566,11 @@ impl Inner { self.rect_to_screen_space(safe_bounds) } else { let screen_frame = self.rect_to_screen_space(bounds); - let status_bar_frame: CGRect = { - let app: id = msg_send![class!(UIApplication), sharedApplication]; - assert!( - !app.is_null(), - "`Window::get_inner_position` cannot be called before `EventLoop::run` on iOS" + let status_bar_frame = { + let app = UIApplication::shared(MainThreadMarker::new().unwrap_unchecked()).expect( + "`Window::get_inner_position` cannot be called before `EventLoop::run` on iOS", ); - msg_send![app, statusBarFrame] + app.statusBarFrame() }; let (y, height) = if screen_frame.origin.y > status_bar_frame.size.height { (screen_frame.origin.y, screen_frame.size.height)