diff --git a/CHANGELOG.md b/CHANGELOG.md index b8594254..a3d7a299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - On iOS, add touch pressure information for touch events. - Implement `raw_window_handle::HasRawWindowHandle` for `Window` type on all supported platforms. - On macOS, fix the signature of `-[NSView drawRect:]`. +- On iOS, fix improper `msg_send` usage that was UB and/or would break if `!` is stabilized. # 0.20.0 Alpha 2 (2019-07-09) diff --git a/FEATURES.md b/FEATURES.md index fe8e50ea..e8927957 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -134,6 +134,7 @@ If your PR makes notable changes to Winit's features, please update this section * Base window size ### iOS +* `winit` has a minimum OS requirement of iOS 8 * Get the `UIWindow` object pointer * Get the `UIViewController` object pointer * Get the `UIView` object pointer diff --git a/src/event.rs b/src/event.rs index 401ed8b6..e226bd70 100644 --- a/src/event.rs +++ b/src/event.rs @@ -329,7 +329,7 @@ pub struct Touch { /// /// ## Platform-specific /// - /// - Only available on **iOS**. + /// - Only available on **iOS** 9.0+. pub force: Option, /// Unique identifier of a finger. pub id: u64, diff --git a/src/platform/ios.rs b/src/platform/ios.rs index fd69489a..9218204f 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -70,6 +70,8 @@ pub trait WindowExtIOS { /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc), /// and then calls /// [`-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc). + /// + /// This only has an effect on iOS 11.0+. fn set_prefers_home_indicator_hidden(&self, hidden: bool); /// Sets the screen edges for which the system gestures will take a lower priority than the @@ -79,6 +81,8 @@ pub trait WindowExtIOS { /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc), /// and then calls /// [`-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc). + /// + /// This only has an effect on iOS 11.0+. fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge); /// Sets whether the [`Window`] prefers the status bar hidden. @@ -167,6 +171,8 @@ pub trait WindowBuilderExtIOS { /// /// This sets the initial value returned by /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc). + /// + /// This only has an effect on iOS 11.0+. fn with_prefers_home_indicator_hidden(self, hidden: bool) -> WindowBuilder; /// Sets the screen edges for which the system gestures will take a lower priority than the @@ -174,6 +180,8 @@ pub trait WindowBuilderExtIOS { /// /// This sets the initial value returned by /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc). + /// + /// This only has an effect on iOS 11.0+. fn with_preferred_screen_edges_deferring_system_gestures( self, edges: ScreenEdge, diff --git a/src/platform_impl/ios/app_state.rs b/src/platform_impl/ios/app_state.rs index 32dae8ed..e8987e54 100644 --- a/src/platform_impl/ios/app_state.rs +++ b/src/platform_impl/ios/app_state.rs @@ -6,6 +6,8 @@ use std::{ time::Instant, }; +use objc::runtime::{BOOL, YES}; + use crate::{ event::{Event, StartCause}, event_loop::ControlFlow, @@ -16,7 +18,8 @@ use crate::platform_impl::platform::{ ffi::{ id, kCFRunLoopCommonModes, CFAbsoluteTimeGetCurrent, CFRelease, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, - CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, NSUInteger, + CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, NSInteger, NSOperatingSystemVersion, + NSUInteger, }, }; @@ -126,7 +129,7 @@ impl AppState { .. } => { queued_windows.push(window); - msg_send![window, retain]; + let _: id = msg_send![window, retain]; return; } &mut AppStateImpl::ProcessingEvents { .. } => {} @@ -199,7 +202,7 @@ impl AppState { // completed. This may result in incorrect visual appearance. // ``` let screen: id = msg_send![window, screen]; - let () = msg_send![screen, retain]; + 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]; @@ -618,3 +621,95 @@ impl EventLoopWaker { } } } + +macro_rules! os_capabilities { + ( + $( + $(#[$attr:meta])* + $error_name:ident: $objc_call:literal, + $name:ident: $major:literal-$minor:literal + ),* + $(,)* + ) => { + #[derive(Clone, Debug)] + pub struct OSCapabilities { + $( + pub $name: bool, + )* + + os_version: NSOperatingSystemVersion, + } + + impl From for OSCapabilities { + fn from(os_version: NSOperatingSystemVersion) -> OSCapabilities { + $(let $name = os_version.meets_requirements($major, $minor);)* + OSCapabilities { $($name,)* os_version, } + } + } + + impl OSCapabilities {$( + $(#[$attr])* + pub fn $error_name(&self, extra_msg: &str) { + log::warn!( + concat!("`", $objc_call, "` requires iOS {}.{}+. This device is running iOS {}.{}.{}. {}"), + $major, $minor, self.os_version.major, self.os_version.minor, self.os_version.patch, + extra_msg + ) + } + )*} + }; +} + +os_capabilities! { + /// https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + #[allow(unused)] // error message unused + safe_area_err_msg: "-[UIView safeAreaInsets]", + safe_area: 11-0, + /// https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc + home_indicator_hidden_err_msg: "-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]", + home_indicator_hidden: 11-0, + /// https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc + defer_system_gestures_err_msg: "-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystem]", + defer_system_gestures: 11-0, + /// https://developer.apple.com/documentation/uikit/uiscreen/2806814-maximumframespersecond?language=objc + maximum_frames_per_second_err_msg: "-[UIScreen maximumFramesPerSecond]", + maximum_frames_per_second: 10-3, + /// https://developer.apple.com/documentation/uikit/uitouch/1618110-force?language=objc + #[allow(unused)] // error message unused + force_touch_err_msg: "-[UITouch force]", + force_touch: 9-0, +} + +impl NSOperatingSystemVersion { + fn meets_requirements(&self, required_major: NSInteger, required_minor: NSInteger) -> bool { + (self.major, self.minor) >= (required_major, required_minor) + } +} + +pub fn os_capabilities() -> OSCapabilities { + lazy_static! { + static ref OS_CAPABILITIES: OSCapabilities = { + let version: NSOperatingSystemVersion = unsafe { + let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; + let atleast_ios_8: BOOL = msg_send![ + process_info, + respondsToSelector: sel!(operatingSystemVersion) + ]; + // winit requires atleast iOS 8 because no one has put the time into supporting earlier os versions. + // Older iOS versions are increasingly difficult to test. For example, Xcode 11 does not support + // debugging on devices with an iOS version of less than 8. Another example, in order to use an iOS + // simulator older than iOS 8, you must download an older version of Xcode (<9), and at least Xcode 7 + // has been tested to not even run on macOS 10.15 - Xcode 8 might? + // + // The minimum required iOS version is likely to grow in the future. + assert!( + atleast_ios_8 == YES, + "`winit` requires iOS version 8 or greater" + ); + msg_send![process_info, operatingSystemVersion] + }; + version.into() + }; + } + OS_CAPABILITIES.clone() +} diff --git a/src/platform_impl/ios/event_loop.rs b/src/platform_impl/ios/event_loop.rs index 956b910a..b5b56739 100644 --- a/src/platform_impl/ios/event_loop.rs +++ b/src/platform_impl/ios/event_loop.rs @@ -23,8 +23,7 @@ use crate::platform_impl::platform::{ CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopSourceInvalidate, CFRunLoopSourceRef, - CFRunLoopSourceSignal, CFRunLoopWakeUp, NSOperatingSystemVersion, NSString, - UIApplicationMain, UIUserInterfaceIdiom, + CFRunLoopSourceSignal, CFRunLoopWakeUp, NSString, UIApplicationMain, UIUserInterfaceIdiom, }, monitor, view, MonitorHandle, }; @@ -32,13 +31,6 @@ use crate::platform_impl::platform::{ pub struct EventLoopWindowTarget { receiver: Receiver, sender_to_clone: Sender, - capabilities: Capabilities, -} - -impl EventLoopWindowTarget { - pub fn capabilities(&self) -> &Capabilities { - &self.capabilities - } } pub struct EventLoop { @@ -64,18 +56,11 @@ impl EventLoop { // this line sets up the main run loop before `UIApplicationMain` setup_control_flow_observers(); - let version: NSOperatingSystemVersion = unsafe { - let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; - msg_send![process_info, operatingSystemVersion] - }; - let capabilities = version.into(); - EventLoop { window_target: RootEventLoopWindowTarget { p: EventLoopWindowTarget { receiver, sender_to_clone, - capabilities, }, _marker: PhantomData, }, @@ -296,20 +281,3 @@ pub unsafe fn get_idiom() -> Idiom { let raw_idiom: UIUserInterfaceIdiom = msg_send![device, userInterfaceIdiom]; raw_idiom.into() } - -pub struct Capabilities { - pub supports_safe_area: bool, -} - -impl From for Capabilities { - fn from(os_version: NSOperatingSystemVersion) -> Capabilities { - assert!( - os_version.major >= 8, - "`winit` current requires iOS version 8 or greater" - ); - - let supports_safe_area = os_version.major >= 11; - - Capabilities { supports_safe_area } - } -} diff --git a/src/platform_impl/ios/monitor.rs b/src/platform_impl/ios/monitor.rs index 96b6f7eb..14279f66 100644 --- a/src/platform_impl/ios/monitor.rs +++ b/src/platform_impl/ios/monitor.rs @@ -7,7 +7,10 @@ use std::{ use crate::{ dpi::{PhysicalPosition, PhysicalSize}, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, - platform_impl::platform::ffi::{id, nil, CGFloat, CGRect, CGSize, NSInteger, NSUInteger}, + platform_impl::platform::{ + app_state, + ffi::{id, nil, CGFloat, CGRect, CGSize, NSInteger, NSUInteger}, + }, }; #[derive(Debug, PartialEq, Eq, Hash)] @@ -35,7 +38,7 @@ impl Drop for VideoMode { fn drop(&mut self) { unsafe { assert_main_thread!("`VideoMode` can only be dropped on the main thread on iOS"); - msg_send![self.screen_mode, release]; + let () = msg_send![self.screen_mode, release]; } } } @@ -43,7 +46,23 @@ impl Drop for VideoMode { impl VideoMode { unsafe fn retained_new(uiscreen: id, screen_mode: id) -> VideoMode { assert_main_thread!("`VideoMode` can only be created on the main thread on iOS"); - let refresh_rate: NSInteger = msg_send![uiscreen, maximumFramesPerSecond]; + let os_capabilities = app_state::os_capabilities(); + let refresh_rate: NSInteger = if os_capabilities.maximum_frames_per_second { + msg_send![uiscreen, maximumFramesPerSecond] + } else { + // https://developer.apple.com/library/archive/technotes/tn2460/_index.html + // https://en.wikipedia.org/wiki/IPad_Pro#Model_comparison + // + // All iOS devices support 60 fps, and on devices where `maximumFramesPerSecond` is not + // supported, they are all guaranteed to have 60hz refresh rates. This does not + // correctly handle external displays. ProMotion displays support 120fps, but they were + // introduced at the same time as the `maximumFramesPerSecond` API. + // + // FIXME: earlier OSs could calculate the refresh rate using + // `-[CADisplayLink duration]`. + os_capabilities.maximum_frames_per_second_err_msg("defaulting to 60 fps"); + 60 + }; let size: CGSize = msg_send![screen_mode, size]; VideoMode { size: (size.width as u32, size.height as u32), diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs index dda1dbdf..e871ab46 100644 --- a/src/platform_impl/ios/view.rs +++ b/src/platform_impl/ios/view.rs @@ -9,7 +9,7 @@ use crate::{ event::{DeviceId as RootDeviceId, Event, Force, Touch, TouchPhase, WindowEvent}, platform::ios::MonitorHandleExtIOS, platform_impl::platform::{ - app_state::AppState, + app_state::{self, AppState, OSCapabilities}, event_loop, ffi::{ id, nil, CGFloat, CGPoint, CGRect, UIForceTouchCapability, UIInterfaceOrientationMask, @@ -27,24 +27,49 @@ macro_rules! add_property { $name:ident: $t:ty, $setter_name:ident: |$object:ident| $after_set:expr, $getter_name:ident, + ) => { + add_property!( + $decl, + $name: $t, + $setter_name: true, |_, _|{}; |$object| $after_set, + $getter_name, + ) + }; + ( + $decl:ident, + $name:ident: $t:ty, + $setter_name:ident: $capability:expr, $err:expr; |$object:ident| $after_set:expr, + $getter_name:ident, ) => { { const VAR_NAME: &'static str = concat!("_", stringify!($name)); $decl.add_ivar::<$t>(VAR_NAME); - #[allow(non_snake_case)] - extern "C" fn $setter_name($object: &mut Object, _: Sel, value: $t) { - unsafe { - $object.set_ivar::<$t>(VAR_NAME, value); + let setter = if $capability { + #[allow(non_snake_case)] + extern "C" fn $setter_name($object: &mut Object, _: Sel, value: $t) { + unsafe { + $object.set_ivar::<$t>(VAR_NAME, value); + } + $after_set } - $after_set - } + $setter_name + } else { + #[allow(non_snake_case)] + extern "C" fn $setter_name($object: &mut Object, _: Sel, value: $t) { + unsafe { + $object.set_ivar::<$t>(VAR_NAME, value); + } + $err(&app_state::os_capabilities(), "ignoring") + } + $setter_name + }; #[allow(non_snake_case)] extern "C" fn $getter_name($object: &Object, _: Sel) -> $t { unsafe { *$object.get_ivar::<$t>(VAR_NAME) } } $decl.add_method( sel!($setter_name:), - $setter_name as extern "C" fn(&mut Object, Sel, $t), + setter as extern "C" fn(&mut Object, Sel, $t), ); $decl.add_method( sel!($getter_name), @@ -125,6 +150,8 @@ unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { unsafe fn get_view_controller_class() -> &'static Class { static mut CLASS: Option<&'static Class> = None; if CLASS.is_none() { + let os_capabilities = app_state::os_capabilities(); + let uiviewcontroller_class = class!(UIViewController); extern "C" fn should_autorotate(_: &Object, _: Sel) -> BOOL { @@ -150,11 +177,14 @@ unsafe fn get_view_controller_class() -> &'static Class { add_property! { decl, prefers_home_indicator_auto_hidden: BOOL, - setPrefersHomeIndicatorAutoHidden: |object| { - unsafe { - let () = msg_send![object, setNeedsUpdateOfHomeIndicatorAutoHidden]; - } - }, + setPrefersHomeIndicatorAutoHidden: + os_capabilities.home_indicator_hidden, + OSCapabilities::home_indicator_hidden_err_msg; + |object| { + unsafe { + let () = msg_send![object, setNeedsUpdateOfHomeIndicatorAutoHidden]; + } + }, prefersHomeIndicatorAutoHidden, } add_property! { @@ -170,11 +200,14 @@ unsafe fn get_view_controller_class() -> &'static Class { add_property! { decl, preferred_screen_edges_deferring_system_gestures: UIRectEdge, - setPreferredScreenEdgesDeferringSystemGestures: |object| { - unsafe { - let () = msg_send![object, setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; - } - }, + setPreferredScreenEdgesDeferringSystemGestures: + os_capabilities.defer_system_gestures, + OSCapabilities::defer_system_gestures_err_msg; + |object| { + unsafe { + let () = msg_send![object, setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; + } + }, preferredScreenEdgesDeferringSystemGestures, } CLASS = Some(decl.register()); @@ -213,6 +246,7 @@ unsafe fn get_window_class() -> &'static Class { let uiscreen = msg_send![object, 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 { @@ -220,23 +254,29 @@ unsafe fn get_window_class() -> &'static Class { } let location: CGPoint = msg_send![touch, locationInView: nil]; let touch_type: UITouchType = msg_send![touch, type]; - let trait_collection: id = msg_send![object, traitCollection]; - let touch_capability: UIForceTouchCapability = - msg_send![trait_collection, forceTouchCapability]; - let force = 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 _) + let force = if os_supports_force { + let trait_collection: id = msg_send![object, 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, + }) } else { None - }; - Some(Force::Calibrated { - force: force as _, - max_possible_force: max_possible_force as _, - altitude_angle, - }) + } } else { None }; diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 46bc5c2e..cca2923d 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -13,7 +13,7 @@ use crate::{ monitor::MonitorHandle as RootMonitorHandle, platform::ios::{MonitorHandleExtIOS, ScreenEdge, ValidOrientations}, platform_impl::platform::{ - app_state::AppState, + app_state::{self, AppState}, event_loop, ffi::{ id, CGFloat, CGPoint, CGRect, CGSize, UIEdgeInsets, UIInterfaceOrientationMask, @@ -28,7 +28,6 @@ pub struct Inner { pub window: id, pub view_controller: id, pub view: id, - supports_safe_area: bool, } impl Drop for Inner { @@ -300,7 +299,7 @@ impl DerefMut for Window { impl Window { pub fn new( - event_loop: &EventLoopWindowTarget, + _event_loop: &EventLoopWindowTarget, window_attributes: WindowAttributes, platform_attributes: PlatformSpecificWindowBuilderAttributes, ) -> Result { @@ -347,14 +346,11 @@ impl Window { view_controller, ); - let supports_safe_area = event_loop.capabilities().supports_safe_area; - let result = Window { inner: Inner { window, view_controller, view, - supports_safe_area, }, }; AppState::set_key_window(window); @@ -396,7 +392,7 @@ impl Inner { msg_send![ self.view_controller, setSupportedInterfaceOrientations: supported_orientations - ]; + ] } } @@ -462,7 +458,7 @@ impl Inner { // requires main thread unsafe fn safe_area_screen_space(&self) -> CGRect { let bounds: CGRect = msg_send![self.window, bounds]; - if self.supports_safe_area { + if app_state::os_capabilities().safe_area { let safe_area: UIEdgeInsets = msg_send![self.window, safeAreaInsets]; let safe_bounds = CGRect { origin: CGPoint {