diff --git a/src/event_loop.rs b/src/event_loop.rs index ab6839cdc6..15da177080 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -94,6 +94,10 @@ impl Default for ControlFlow { impl EventLoop<()> { /// Builds a new event loop with a `()` as the user event type. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. pub fn new() -> EventLoop<()> { EventLoop::<()>::new_user_event() } @@ -106,6 +110,10 @@ impl EventLoop { /// using an environment variable `WINIT_UNIX_BACKEND`. Legal values are `x11` and `wayland`. /// If it is not set, winit will try to connect to a wayland connection, and if it fails will /// fallback on x11. If this variable is set with any other value, winit will panic. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. pub fn new_user_event() -> EventLoop { EventLoop { event_loop: platform_impl::EventLoop::new(), diff --git a/src/platform/ios.rs b/src/platform/ios.rs index 7a2345b044..e7057c8247 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -2,7 +2,21 @@ use std::os::raw::c_void; -use {MonitorHandle, Window, WindowBuilder}; +use event_loop::EventLoop; +use monitor::MonitorHandle; +use window::{Window, WindowBuilder}; + +/// Additional methods on `EventLoop` that are specific to iOS. +pub trait EventLoopExtIOS { + /// Returns the idiom (phone/tablet/tv/etc) for the current device. + fn get_idiom(&self) -> Idiom; +} + +impl EventLoopExtIOS for EventLoop { + fn get_idiom(&self) -> Idiom { + self.event_loop.get_idiom() + } +} /// Additional methods on `Window` that are specific to iOS. pub trait WindowExtIOS { @@ -11,10 +25,25 @@ pub trait WindowExtIOS { /// The pointer will become invalid when the `Window` is destroyed. fn get_uiwindow(&self) -> *mut c_void; + /// Returns a pointer to the `UIViewController` that is used by this window. + /// + /// The pointer will become invalid when the `Window` is destroyed. + fn get_uiviewcontroller(&self) -> *mut c_void; + /// Returns a pointer to the `UIView` that is used by this window. /// /// The pointer will become invalid when the `Window` is destroyed. fn get_uiview(&self) -> *mut c_void; + + /// Sets the HiDpi factor used by this window. + /// + /// This translates to `-[UIWindow setContentScaleFactor:hidpi_factor]`. + fn set_hidpi_factor(&self, hidpi_factor: f64); + + /// Sets the valid orientations for screens showing this `Window`. + /// + /// On iPhones and iPods upside down portrait is never enabled. + fn set_valid_orientations(&self, valid_orientations: ValidOrientations); } impl WindowExtIOS for Window { @@ -23,10 +52,25 @@ impl WindowExtIOS for Window { self.window.get_uiwindow() as _ } + #[inline] + fn get_uiviewcontroller(&self) -> *mut c_void { + self.window.get_uiviewcontroller() as _ + } + #[inline] fn get_uiview(&self) -> *mut c_void { self.window.get_uiview() as _ } + + #[inline] + fn set_hidpi_factor(&self, hidpi_factor: f64) { + self.window.set_hidpi_factor(hidpi_factor) + } + + #[inline] + fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { + self.window.set_valid_orientations(valid_orientations) + } } /// Additional methods on `WindowBuilder` that are specific to iOS. @@ -35,6 +79,15 @@ pub trait WindowBuilderExtIOS { /// /// The class will be initialized by calling `[root_view initWithFrame:CGRect]` fn with_root_view_class(self, root_view_class: *const c_void) -> WindowBuilder; + + /// Sets the `contentScaleFactor` of the underlying `UIWindow` to `hidpi_factor`. + /// + /// The default value is device dependent, and it's recommended GLES or Metal applications set + /// this to `MonitorHandle::get_hidpi_factor()`. + fn with_hidpi_factor(self, hidpi_factor: f64) -> WindowBuilder; + + /// Sets the valid orientations for the `Window`. + fn with_valid_orientations(self, valid_orientations: ValidOrientations) -> WindowBuilder; } impl WindowBuilderExtIOS for WindowBuilder { @@ -43,6 +96,18 @@ impl WindowBuilderExtIOS for WindowBuilder { self.platform_specific.root_view_class = unsafe { &*(root_view_class as *const _) }; self } + + #[inline] + fn with_hidpi_factor(mut self, hidpi_factor: f64) -> WindowBuilder { + self.platform_specific.hidpi_factor = Some(hidpi_factor); + self + } + + #[inline] + fn with_valid_orientations(mut self, valid_orientations: ValidOrientations) -> WindowBuilder { + self.platform_specific.valid_orientations = valid_orientations; + self + } } /// Additional methods on `MonitorHandle` that are specific to iOS. @@ -57,3 +122,40 @@ impl MonitorHandleExtIOS for MonitorHandle { self.inner.get_uiscreen() as _ } } + +/// Valid orientations for a particular `Window`. +#[derive(Clone, Copy, Debug)] +pub enum ValidOrientations { + /// Excludes `PortraitUpsideDown` on iphone + LandscapeAndPortrait, + + Landscape, + + /// Excludes `PortraitUpsideDown` on iphone + Portrait, +} + +impl Default for ValidOrientations { + #[inline] + fn default() -> ValidOrientations { + ValidOrientations::LandscapeAndPortrait + } +} + +/// The device [idiom]. +/// +/// [idiom]: https://developer.apple.com/documentation/uikit/uidevice/1620037-userinterfaceidiom?language=objc +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Idiom { + Unspecified, + + /// iPhone and iPod touch. + Phone, + + /// iPad. + Pad, + + /// tvOS and Apple TV. + TV, + CarPlay, +} diff --git a/src/platform_impl/ios/app_state.rs b/src/platform_impl/ios/app_state.rs new file mode 100644 index 0000000000..295891d5b4 --- /dev/null +++ b/src/platform_impl/ios/app_state.rs @@ -0,0 +1,580 @@ +use std::{mem, ptr}; +use std::cell::{RefCell, RefMut}; +use std::mem::ManuallyDrop; +use std::os::raw::c_void; +use std::time::Instant; + +use event::{Event, StartCause}; +use event_loop::ControlFlow; + +use platform_impl::platform::event_loop::{EventHandler, Never}; +use platform_impl::platform::ffi::{ + id, + CFAbsoluteTimeGetCurrent, + CFRelease, + CFRunLoopAddTimer, + CFRunLoopGetMain, + CFRunLoopRef, + CFRunLoopTimerCreate, + CFRunLoopTimerInvalidate, + CFRunLoopTimerRef, + CFRunLoopTimerSetNextFireDate, + kCFRunLoopCommonModes, + NSUInteger, +}; + +macro_rules! bug { + ($msg:expr) => { + panic!("winit iOS bug, file an issue: {}", $msg) + }; +} + +// this is the state machine for the app lifecycle +#[derive(Debug)] +enum AppStateImpl { + NotLaunched { + queued_windows: Vec, + queued_events: Vec>, + }, + Launching { + queued_windows: Vec, + queued_events: Vec>, + queued_event_handler: Box, + }, + ProcessingEvents { + event_handler: Box, + active_control_flow: ControlFlow, + }, + // special state to deal with reentrancy and prevent mutable aliasing. + InUserCallback { + queued_events: Vec>, + }, + Waiting { + waiting_event_handler: Box, + start: Instant, + }, + PollFinished { + waiting_event_handler: Box, + }, + Terminated, +} + +impl Drop for AppStateImpl { + fn drop(&mut self) { + match self { + &mut AppStateImpl::NotLaunched { ref mut queued_windows, .. } | + &mut AppStateImpl::Launching { ref mut queued_windows, .. } => unsafe { + for &mut window in queued_windows { + let () = msg_send![window, release]; + } + } + _ => {} + } + } +} + +pub struct AppState { + app_state: AppStateImpl, + control_flow: ControlFlow, + waker: EventLoopWaker, +} + +impl AppState { + // requires main thread + unsafe fn get_mut() -> RefMut<'static, AppState> { + // basically everything in UIKit requires the main thread, so it's pointless to use the + // std::sync APIs. + // must be mut because plain `static` requires `Sync` + static mut APP_STATE: RefCell> = RefCell::new(None); + + if cfg!(debug_assertions) { + assert_main_thread!("bug in winit: `AppState::get_mut()` can only be called on the main thread"); + } + + let mut guard = APP_STATE.borrow_mut(); + if guard.is_none() { + #[inline(never)] + #[cold] + unsafe fn init_guard(guard: &mut RefMut<'static, Option>) { + let waker = EventLoopWaker::new(CFRunLoopGetMain()); + **guard = Some(AppState { + app_state: AppStateImpl::NotLaunched { + queued_windows: Vec::new(), + queued_events: Vec::new(), + }, + control_flow: ControlFlow::default(), + waker, + }); + } + init_guard(&mut guard) + } + RefMut::map(guard, |state| { + state.as_mut().unwrap() + }) + } + + // requires main thread and window is a UIWindow + // retains window + pub unsafe fn set_key_window(window: id) { + let mut this = AppState::get_mut(); + match &mut this.app_state { + &mut AppStateImpl::NotLaunched { ref mut queued_windows, .. } => { + queued_windows.push(window); + msg_send![window, retain]; + return; + } + &mut AppStateImpl::ProcessingEvents { .. } => {}, + &mut AppStateImpl::InUserCallback { .. } => {}, + &mut AppStateImpl::Terminated => panic!("Attempt to create a `Window` \ + after the app has terminated"), + app_state => unreachable!("unexpected state: {:#?}", app_state), // all other cases should be impossible + } + drop(this); + msg_send![window, makeKeyAndVisible] + } + + // requires main thread + pub unsafe fn will_launch(queued_event_handler: Box) { + let mut this = AppState::get_mut(); + let (queued_windows, queued_events) = match &mut this.app_state { + &mut AppStateImpl::NotLaunched { + ref mut queued_windows, + ref mut queued_events, + } => { + let windows = ptr::read(queued_windows); + let events = ptr::read(queued_events); + (windows, events) + } + _ => panic!("winit iOS expected the app to be in a `NotLaunched` \ + state, but was not - please file an issue"), + }; + ptr::write(&mut this.app_state, AppStateImpl::Launching { + queued_windows, + queued_events, + queued_event_handler, + }); + } + + // requires main thread + pub unsafe fn did_finish_launching() { + let mut this = AppState::get_mut(); + let windows = match &mut this.app_state { + &mut AppStateImpl::Launching { + ref mut queued_windows, + .. + } => mem::replace(queued_windows, Vec::new()), + _ => panic!( + "winit iOS expected the app to be in a `Launching` \ + state, but was not - please file an issue" + ), + }; + // have to drop RefMut because the window setup code below can trigger new events + 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 () = 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]; + } + + let mut this = AppState::get_mut(); + let (windows, events, event_handler) = match &mut this.app_state { + &mut AppStateImpl::Launching { + ref mut queued_windows, + ref mut queued_events, + ref mut queued_event_handler, + } => { + let windows = ptr::read(queued_windows); + let events = ptr::read(queued_events); + let event_handler = ptr::read(queued_event_handler); + (windows, events, event_handler) + } + _ => panic!("winit iOS expected the app to be in a `Launching` \ + state, but was not - please file an issue"), + }; + ptr::write(&mut this.app_state, AppStateImpl::ProcessingEvents { + event_handler, + active_control_flow: ControlFlow::Poll, + }); + drop(this); + + let events = std::iter::once(Event::NewEvents(StartCause::Init)).chain(events); + AppState::handle_nonuser_events(events); + + // 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]; + } + } + + // requires main thread + // AppState::did_finish_launching handles the special transition `Init` + pub unsafe fn handle_wakeup_transition() { + let mut this = AppState::get_mut(); + let event = match this.control_flow { + ControlFlow::Poll => { + let event_handler = match &mut this.app_state { + &mut AppStateImpl::NotLaunched { .. } | + &mut AppStateImpl::Launching { .. } => return, + &mut AppStateImpl::PollFinished { + ref mut waiting_event_handler, + } => ptr::read(waiting_event_handler), + _ => bug!("`EventHandler` unexpectedly started polling"), + }; + ptr::write(&mut this.app_state, AppStateImpl::ProcessingEvents { + event_handler, + active_control_flow: ControlFlow::Poll, + }); + Event::NewEvents(StartCause::Poll) + } + ControlFlow::Wait => { + let (event_handler, start) = match &mut this.app_state { + &mut AppStateImpl::NotLaunched { .. } | + &mut AppStateImpl::Launching { .. } => return, + &mut AppStateImpl::Waiting { + ref mut waiting_event_handler, + ref mut start, + } => (ptr::read(waiting_event_handler), *start), + _ => bug!("`EventHandler` unexpectedly woke up"), + }; + ptr::write(&mut this.app_state, AppStateImpl::ProcessingEvents { + event_handler, + active_control_flow: ControlFlow::Wait, + }); + Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: None, + }) + } + ControlFlow::WaitUntil(requested_resume) => { + let (event_handler, start) = match &mut this.app_state { + &mut AppStateImpl::NotLaunched { .. } | + &mut AppStateImpl::Launching { .. } => return, + &mut AppStateImpl::Waiting { + ref mut waiting_event_handler, + ref mut start, + } => (ptr::read(waiting_event_handler), *start), + _ => bug!("`EventHandler` unexpectedly woke up"), + }; + ptr::write(&mut this.app_state, AppStateImpl::ProcessingEvents { + event_handler, + active_control_flow: ControlFlow::WaitUntil(requested_resume), + }); + if Instant::now() >= requested_resume { + Event::NewEvents(StartCause::ResumeTimeReached { + start, + requested_resume, + }) + } else { + Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: Some(requested_resume), + }) + } + } + ControlFlow::Exit => bug!("unexpected controlflow `Exit`"), + }; + drop(this); + AppState::handle_nonuser_event(event) + } + + // requires main thread + pub unsafe fn handle_nonuser_event(event: Event) { + AppState::handle_nonuser_events(std::iter::once(event)) + } + + // requires main thread + pub unsafe fn handle_nonuser_events>>(events: I) { + let mut this = AppState::get_mut(); + let mut control_flow = this.control_flow; + let (mut event_handler, active_control_flow) = match &mut this.app_state { + &mut AppStateImpl::Launching { + ref mut queued_events, + .. + } + | &mut AppStateImpl::NotLaunched { + ref mut queued_events, + .. + } + | &mut AppStateImpl::InUserCallback { + ref mut queued_events, + .. + } => { + queued_events.extend(events); + return + } + &mut AppStateImpl::ProcessingEvents { + ref mut event_handler, + ref mut active_control_flow, + } => (ptr::read(event_handler), *active_control_flow), + &mut AppStateImpl::PollFinished { .. } + | &mut AppStateImpl::Waiting { .. } + | &mut AppStateImpl::Terminated => bug!("unexpected attempted to process an event"), + }; + ptr::write(&mut this.app_state, AppStateImpl::InUserCallback { + queued_events: Vec::new(), + }); + drop(this); + + for event in events { + event_handler.handle_nonuser_event(event, &mut control_flow) + } + loop { + let mut this = AppState::get_mut(); + let queued_events = match &mut this.app_state { + &mut AppStateImpl::InUserCallback { + ref mut queued_events, + } => mem::replace(queued_events, Vec::new()), + _ => bug!("unexpected `AppStateImpl`"), + }; + if queued_events.is_empty() { + this.app_state = AppStateImpl::ProcessingEvents { + event_handler, + active_control_flow, + }; + this.control_flow = control_flow; + break + } + drop(this); + for event in queued_events { + event_handler.handle_nonuser_event(event, &mut control_flow) + } + } + } + + // requires main thread + pub unsafe fn handle_user_events() { + let mut this = AppState::get_mut(); + let mut control_flow = this.control_flow; + let (mut event_handler, active_control_flow) = match &mut this.app_state { + &mut AppStateImpl::NotLaunched { .. } | &mut AppStateImpl::Launching { .. } => return, + &mut AppStateImpl::ProcessingEvents { + ref mut event_handler, + ref mut active_control_flow, + } => (ptr::read(event_handler), *active_control_flow), + &mut AppStateImpl::InUserCallback { .. } + | &mut AppStateImpl::PollFinished { .. } + | &mut AppStateImpl::Waiting { .. } + | &mut AppStateImpl::Terminated => bug!("unexpected attempted to process an event"), + }; + ptr::write(&mut this.app_state, AppStateImpl::InUserCallback { + queued_events: Vec::new(), + }); + drop(this); + + event_handler.handle_user_events(&mut control_flow); + loop { + let mut this = AppState::get_mut(); + let queued_events = match &mut this.app_state { + &mut AppStateImpl::InUserCallback { + ref mut queued_events, + } => mem::replace(queued_events, Vec::new()), + _ => bug!("unexpected `AppStateImpl`"), + }; + if queued_events.is_empty() { + this.app_state = AppStateImpl::ProcessingEvents { + event_handler, + active_control_flow, + }; + this.control_flow = control_flow; + break + } + drop(this); + for event in queued_events { + event_handler.handle_nonuser_event(event, &mut control_flow) + } + event_handler.handle_user_events(&mut control_flow); + } + } + + // requires main thread + pub unsafe fn handle_events_cleared() { + let mut this = AppState::get_mut(); + match &mut this.app_state { + &mut AppStateImpl::NotLaunched { .. } | &mut AppStateImpl::Launching { .. } => return, + &mut AppStateImpl::ProcessingEvents { .. } => {} + _ => unreachable!(), + }; + drop(this); + + AppState::handle_user_events(); + AppState::handle_nonuser_event(Event::EventsCleared); + + let mut this = AppState::get_mut(); + let (event_handler, old) = match &mut this.app_state { + &mut AppStateImpl::ProcessingEvents { + ref mut event_handler, + ref mut active_control_flow, + } => (ManuallyDrop::new(ptr::read(event_handler)), *active_control_flow), + _ => unreachable!(), + }; + + let new = this.control_flow; + match (old, new) { + (ControlFlow::Poll, ControlFlow::Poll) => { + ptr::write( + &mut this.app_state, + AppStateImpl::PollFinished { + waiting_event_handler: ManuallyDrop::into_inner(event_handler), + }, + ) + }, + (ControlFlow::Wait, ControlFlow::Wait) => { + let start = Instant::now(); + ptr::write( + &mut this.app_state, + AppStateImpl::Waiting { + waiting_event_handler: ManuallyDrop::into_inner(event_handler), + start, + }, + ) + }, + (ControlFlow::WaitUntil(old_instant), ControlFlow::WaitUntil(new_instant)) + if old_instant == new_instant => { + let start = Instant::now(); + ptr::write( + &mut this.app_state, + AppStateImpl::Waiting { + waiting_event_handler: ManuallyDrop::into_inner(event_handler), + start, + }, + ) + } + (_, ControlFlow::Wait) => { + let start = Instant::now(); + ptr::write( + &mut this.app_state, + AppStateImpl::Waiting { + waiting_event_handler: ManuallyDrop::into_inner(event_handler), + start, + }, + ); + this.waker.stop() + }, + (_, ControlFlow::WaitUntil(new_instant)) => { + let start = Instant::now(); + ptr::write( + &mut this.app_state, + AppStateImpl::Waiting { + waiting_event_handler: ManuallyDrop::into_inner(event_handler), + start, + }, + ); + this.waker.start_at(new_instant) + }, + (_, ControlFlow::Poll) => { + ptr::write( + &mut this.app_state, + AppStateImpl::PollFinished { + waiting_event_handler: ManuallyDrop::into_inner(event_handler), + }, + ); + this.waker.start() + }, + (_, ControlFlow::Exit) => { + // https://developer.apple.com/library/archive/qa/qa1561/_index.html + // it is not possible to quit an iOS app gracefully and programatically + warn!("`ControlFlow::Exit` ignored on iOS"); + this.control_flow = old + } + } + } + + pub fn terminated() { + let mut this = unsafe { AppState::get_mut() }; + let mut old = mem::replace(&mut this.app_state, AppStateImpl::Terminated); + let mut control_flow = this.control_flow; + if let AppStateImpl::ProcessingEvents { ref mut event_handler, .. } = old { + drop(this); + event_handler.handle_nonuser_event(Event::LoopDestroyed, &mut control_flow) + } else { + bug!("`LoopDestroyed` happened while not processing events") + } + } +} + +struct EventLoopWaker { + timer: CFRunLoopTimerRef, +} + +impl Drop for EventLoopWaker { + fn drop(&mut self) { + unsafe { + CFRunLoopTimerInvalidate(self.timer); + CFRelease(self.timer as _); + } + } +} + +impl EventLoopWaker { + fn new(rl: CFRunLoopRef) -> EventLoopWaker { + extern fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} + unsafe { + // create a timer with a 1microsec interval (1ns does not work) to mimic polling. + // it is initially setup with a first fire time really far into the + // future, but that gets changed to fire immediatley in did_finish_launching + let timer = CFRunLoopTimerCreate( + ptr::null_mut(), + std::f64::MAX, + 0.000_000_1, + 0, + 0, + wakeup_main_loop, + ptr::null_mut(), + ); + CFRunLoopAddTimer(rl, timer, kCFRunLoopCommonModes); + + EventLoopWaker { timer } + } + } + + fn stop(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MAX) } + } + + fn start(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MIN) } + } + + fn start_at(&mut self, instant: Instant) { + let now = Instant::now(); + if now >= instant { + self.start(); + } else { + unsafe { + let current = CFAbsoluteTimeGetCurrent(); + let duration = instant - now; + let fsecs = + duration.subsec_nanos() as f64 / 1_000_000_000.0 + duration.as_secs() as f64; + CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) + } + } + } +} \ No newline at end of file diff --git a/src/platform_impl/ios/event_loop.rs b/src/platform_impl/ios/event_loop.rs new file mode 100644 index 0000000000..76929980eb --- /dev/null +++ b/src/platform_impl/ios/event_loop.rs @@ -0,0 +1,334 @@ +use std::{mem, ptr}; +use std::collections::VecDeque; +use std::ffi::c_void; +use std::fmt::{self, Debug, Formatter}; +use std::marker::PhantomData; +use std::sync::mpsc::{self, Sender, Receiver}; + +use event::Event; +use event_loop::{ + ControlFlow, + EventLoopWindowTarget as RootEventLoopWindowTarget, + EventLoopClosed, +}; +use platform::ios::Idiom; + +use platform_impl::platform::app_state::AppState; +use platform_impl::platform::ffi::{ + id, + nil, + CFIndex, + CFRelease, + CFRunLoopActivity, + CFRunLoopAddObserver, + CFRunLoopAddSource, + CFRunLoopGetMain, + CFRunLoopObserverCreate, + CFRunLoopObserverRef, + CFRunLoopSourceContext, + CFRunLoopSourceCreate, + CFRunLoopSourceInvalidate, + CFRunLoopSourceRef, + CFRunLoopSourceSignal, + CFRunLoopWakeUp, + kCFRunLoopCommonModes, + kCFRunLoopDefaultMode, + kCFRunLoopEntry, + kCFRunLoopBeforeWaiting, + kCFRunLoopAfterWaiting, + kCFRunLoopExit, + NSOperatingSystemVersion, + NSString, + UIApplicationMain, + UIUserInterfaceIdiom, +}; +use platform_impl::platform::monitor; +use platform_impl::platform::MonitorHandle; +use platform_impl::platform::view; + +pub struct EventLoopWindowTarget { + receiver: Receiver, + sender_to_clone: Sender, + capabilities: Capabilities, +} + +impl EventLoopWindowTarget { + pub fn capabilities(&self) -> &Capabilities { + &self.capabilities + } +} + +pub struct EventLoop { + window_target: RootEventLoopWindowTarget, +} + +impl EventLoop { + pub fn new() -> EventLoop { + static mut SINGLETON_INIT: bool = false; + unsafe { + assert_main_thread!("`EventLoop` can only be created on the main thread on iOS"); + assert!(!SINGLETON_INIT, "Only one `EventLoop` is supported on iOS. \ + `EventLoopProxy` might be helpful"); + SINGLETON_INIT = true; + view::create_delegate_class(); + } + + let (sender_to_clone, receiver) = mpsc::channel(); + + // 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, + } + } + } + + pub fn run(self, event_handler: F) -> ! + where + F: 'static + FnMut(Event, &RootEventLoopWindowTarget, &mut ControlFlow) + { + unsafe { + let application: *mut c_void = msg_send![class!(UIApplication), sharedApplication]; + assert_eq!(application, ptr::null_mut(), "\ + `EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\n\ + Note: `EventLoop::run` calls `UIApplicationMain` on iOS"); + AppState::will_launch(Box::new(EventLoopHandler { + f: event_handler, + event_loop: self.window_target, + })); + + UIApplicationMain(0, ptr::null(), nil, NSString::alloc(nil).init_str("AppDelegate")); + unreachable!() + } + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.window_target.p.sender_to_clone.clone()) + } + + pub fn get_available_monitors(&self) -> VecDeque { + // guaranteed to be on main thread + unsafe { + monitor::uiscreens() + } + } + + pub fn get_primary_monitor(&self) -> MonitorHandle { + // guaranteed to be on main thread + unsafe { + monitor::main_uiscreen() + } + } + + pub fn window_target(&self) -> &RootEventLoopWindowTarget { + &self.window_target + } +} + +// EventLoopExtIOS +impl EventLoop { + pub fn get_idiom(&self) -> Idiom { + // guaranteed to be on main thread + unsafe { + self::get_idiom() + } + } +} + +pub struct EventLoopProxy { + sender: Sender, + source: CFRunLoopSourceRef, +} + +unsafe impl Send for EventLoopProxy {} +unsafe impl Sync for EventLoopProxy {} + +impl Clone for EventLoopProxy { + fn clone(&self) -> EventLoopProxy { + EventLoopProxy::new(self.sender.clone()) + } +} + +impl Drop for EventLoopProxy { + fn drop(&mut self) { + unsafe { + CFRunLoopSourceInvalidate(self.source); + CFRelease(self.source as _); + } + } +} + +impl EventLoopProxy { + fn new(sender: Sender) -> EventLoopProxy { + unsafe { + // just wakeup the eventloop + extern "C" fn event_loop_proxy_handler(_: *mut c_void) {} + + // adding a Source to the main CFRunLoop lets us wake it up and + // process user events through the normal OS EventLoop mechanisms. + let rl = CFRunLoopGetMain(); + // we want all the members of context to be zero/null, except one + let mut context: CFRunLoopSourceContext = mem::zeroed(); + context.perform = event_loop_proxy_handler; + let source = CFRunLoopSourceCreate( + ptr::null_mut(), + CFIndex::max_value() - 1, + &mut context, + ); + CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes); + CFRunLoopWakeUp(rl); + + EventLoopProxy { + sender, + source, + } + } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.sender.send(event).map_err(|_| EventLoopClosed)?; + unsafe { + // let the main thread know there's a new event + CFRunLoopSourceSignal(self.source); + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } + Ok(()) + } +} + +fn setup_control_flow_observers() { + unsafe { + // begin is queued with the highest priority to ensure it is processed before other observers + extern fn control_flow_begin_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + unsafe { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => AppState::handle_wakeup_transition(), + kCFRunLoopEntry => unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + } + } + + // end is queued with the lowest priority to ensure it is processed after other observers + // without that, LoopDestroyed will get sent after EventsCleared + extern fn control_flow_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + unsafe { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => AppState::handle_events_cleared(), + kCFRunLoopExit => unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + } + } + + let main_loop = CFRunLoopGetMain(); + let begin_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopEntry | kCFRunLoopAfterWaiting, + 1, // repeat = true + CFIndex::min_value(), + control_flow_begin_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, begin_observer, kCFRunLoopDefaultMode); + let end_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + 1, // repeat = true + CFIndex::max_value(), + control_flow_end_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, end_observer, kCFRunLoopDefaultMode); + } +} + +#[derive(Debug)] +pub enum Never {} + +pub trait EventHandler: Debug { + fn handle_nonuser_event(&mut self, event: Event, control_flow: &mut ControlFlow); + fn handle_user_events(&mut self, control_flow: &mut ControlFlow); +} + +struct EventLoopHandler { + f: F, + event_loop: RootEventLoopWindowTarget, +} + +impl Debug for EventLoopHandler { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.debug_struct("EventLoopHandler") + .field("event_loop", &self.event_loop) + .finish() + } +} + +impl EventHandler for EventLoopHandler +where + F: 'static + FnMut(Event, &RootEventLoopWindowTarget, &mut ControlFlow), + T: 'static, +{ + fn handle_nonuser_event(&mut self, event: Event, control_flow: &mut ControlFlow) { + (self.f)( + event.map_nonuser_event().unwrap(), + &self.event_loop, + control_flow, + ); + } + + fn handle_user_events(&mut self, control_flow: &mut ControlFlow) { + for event in self.event_loop.p.receiver.try_iter() { + (self.f)( + Event::UserEvent(event), + &self.event_loop, + control_flow, + ); + } + } +} + +// 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() +} + +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 } + } +} \ No newline at end of file diff --git a/src/platform_impl/ios/ffi.rs b/src/platform_impl/ios/ffi.rs index 6fd1a7cad0..8583dbd757 100644 --- a/src/platform_impl/ios/ffi.rs +++ b/src/platform_impl/ios/ffi.rs @@ -1,24 +1,33 @@ #![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] use std::ffi::CString; +use std::ops::BitOr; use std::os::raw::*; +use objc::{Encode, Encoding}; use objc::runtime::Object; +use platform::ios::{Idiom, ValidOrientations}; + pub type id = *mut Object; pub const nil: id = 0 as id; -pub type CFStringRef = *const c_void; -pub type CFTimeInterval = f64; -pub type Boolean = u32; - -pub const kCFRunLoopRunHandledSource: i32 = 4; - #[cfg(target_pointer_width = "32")] pub type CGFloat = f32; #[cfg(target_pointer_width = "64")] pub type CGFloat = f64; +pub type NSInteger = isize; +pub type NSUInteger = usize; + +#[repr(C)] +#[derive(Clone, Debug)] +pub struct NSOperatingSystemVersion { + pub major: NSInteger, + pub minor: NSInteger, + pub patch: NSInteger, +} + #[repr(C)] #[derive(Debug, Clone)] pub struct CGPoint { @@ -26,6 +35,13 @@ pub struct CGPoint { pub y: CGFloat, } +#[repr(C)] +#[derive(Debug, Clone)] +pub struct CGSize { + pub width: CGFloat, + pub height: CGFloat, +} + #[repr(C)] #[derive(Debug, Clone)] pub struct CGRect { @@ -33,20 +49,127 @@ pub struct CGRect { pub size: CGSize, } +unsafe impl Encode for CGRect { + fn encode() -> Encoding { + unsafe { + if cfg!(target_pointer_width = "32") { + Encoding::from_str("{CGRect={CGPoint=ff}{CGSize=ff}}") + } else if cfg!(target_pointer_width = "64") { + Encoding::from_str("{CGRect={CGPoint=dd}{CGSize=dd}}") + } else { + unimplemented!() + } + } + } +} +#[derive(Debug)] +#[allow(dead_code)] +#[repr(isize)] +pub enum UITouchPhase { + Began = 0, + Moved, + Stationary, + Ended, + Cancelled, +} + #[repr(C)] #[derive(Debug, Clone)] -pub struct CGSize { - pub width: CGFloat, - pub height: CGFloat, +pub struct UIEdgeInsets { + pub top: CGFloat, + pub left: CGFloat, + pub bottom: CGFloat, + pub right: CGFloat, +} + +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct UIUserInterfaceIdiom(NSInteger); + +unsafe impl Encode for UIUserInterfaceIdiom { + fn encode() -> Encoding { NSInteger::encode() } +} + +impl UIUserInterfaceIdiom { + pub const Unspecified: UIUserInterfaceIdiom = UIUserInterfaceIdiom(-1); + pub const Phone: UIUserInterfaceIdiom = UIUserInterfaceIdiom(0); + pub const Pad: UIUserInterfaceIdiom = UIUserInterfaceIdiom(1); + pub const TV: UIUserInterfaceIdiom = UIUserInterfaceIdiom(2); + pub const CarPlay: UIUserInterfaceIdiom = UIUserInterfaceIdiom(3); +} + +impl From for UIUserInterfaceIdiom { + fn from(idiom: Idiom) -> UIUserInterfaceIdiom { + match idiom { + Idiom::Unspecified => UIUserInterfaceIdiom::Unspecified, + Idiom::Phone => UIUserInterfaceIdiom::Phone, + Idiom::Pad => UIUserInterfaceIdiom::Pad, + Idiom::TV => UIUserInterfaceIdiom::TV, + Idiom::CarPlay => UIUserInterfaceIdiom::CarPlay, + } + } +} + +impl Into for UIUserInterfaceIdiom { + fn into(self) -> Idiom { + match self { + UIUserInterfaceIdiom::Unspecified => Idiom::Unspecified, + UIUserInterfaceIdiom::Phone => Idiom::Phone, + UIUserInterfaceIdiom::Pad => Idiom::Pad, + UIUserInterfaceIdiom::TV => Idiom::TV, + UIUserInterfaceIdiom::CarPlay => Idiom::CarPlay, + _ => unreachable!(), + } + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, Debug)] +pub struct UIInterfaceOrientationMask(NSUInteger); + +unsafe impl Encode for UIInterfaceOrientationMask { + fn encode() -> Encoding { NSUInteger::encode() } +} + +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, + } + } } #[link(name = "UIKit", kind = "framework")] #[link(name = "CoreFoundation", kind = "framework")] -#[link(name = "GlKit", kind = "framework")] extern { - pub static kCFRunLoopDefaultMode: CFStringRef; + pub static kCFRunLoopDefaultMode: CFRunLoopMode; + pub static kCFRunLoopCommonModes: CFRunLoopMode; - // int UIApplicationMain ( int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName ); pub fn UIApplicationMain( argc: c_int, argv: *const c_char, @@ -54,30 +177,114 @@ extern { delegateClassName: id, ) -> c_int; - // SInt32 CFRunLoopRunInMode ( CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled ); - pub fn CFRunLoopRunInMode( - mode: CFStringRef, - seconds: CFTimeInterval, - returnAfterSourceHandled: Boolean, - ) -> i32; -} + pub fn CFRunLoopGetMain() -> CFRunLoopRef; + pub fn CFRunLoopWakeUp(rl: CFRunLoopRef); -extern { - pub fn setjmp(env: *mut c_void) -> c_int; - pub fn longjmp(env: *mut c_void, val: c_int) -> !; + pub fn CFRunLoopObserverCreate( + allocator: CFAllocatorRef, + activities: CFOptionFlags, + repeats: Boolean, + order: CFIndex, + callout: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) -> CFRunLoopObserverRef; + pub fn CFRunLoopAddObserver( + rl: CFRunLoopRef, + observer: CFRunLoopObserverRef, + mode: CFRunLoopMode, + ); + + pub fn CFRunLoopTimerCreate( + allocator: CFAllocatorRef, + fireDate: CFAbsoluteTime, + interval: CFTimeInterval, + flags: CFOptionFlags, + order: CFIndex, + callout: CFRunLoopTimerCallBack, + context: *mut CFRunLoopTimerContext, + ) -> CFRunLoopTimerRef; + pub fn CFRunLoopAddTimer( + rl: CFRunLoopRef, + timer: CFRunLoopTimerRef, + mode: CFRunLoopMode, + ); + pub fn CFRunLoopTimerSetNextFireDate( + timer: CFRunLoopTimerRef, + fireDate: CFAbsoluteTime, + ); + pub fn CFRunLoopTimerInvalidate(time: CFRunLoopTimerRef); + + pub fn CFRunLoopSourceCreate( + allocator: CFAllocatorRef, + order: CFIndex, + context: *mut CFRunLoopSourceContext, + ) -> CFRunLoopSourceRef; + pub fn CFRunLoopAddSource( + rl: CFRunLoopRef, + source: CFRunLoopSourceRef, + mode: CFRunLoopMode, + ); + pub fn CFRunLoopSourceInvalidate(source: CFRunLoopSourceRef); + pub fn CFRunLoopSourceSignal(source: CFRunLoopSourceRef); + + pub fn CFAbsoluteTimeGetCurrent() -> CFAbsoluteTime; + pub fn CFRelease(cftype: *const c_void); } -// values taken from "setjmp.h" header in xcode iPhoneOS/iPhoneSimulator SDK -#[cfg(any(target_arch = "x86_64"))] -pub const JBLEN: usize = (9 * 2) + 3 + 16; -#[cfg(any(target_arch = "x86"))] -pub const JBLEN: usize = 18; -#[cfg(target_arch = "arm")] -pub const JBLEN: usize = 10 + 16 + 2; -#[cfg(target_arch = "aarch64")] -pub const JBLEN: usize = (14 + 8 + 2) * 2; +pub type Boolean = u8; +pub enum CFAllocator {} +pub type CFAllocatorRef = *mut CFAllocator; +pub enum CFRunLoop {} +pub type CFRunLoopRef = *mut CFRunLoop; +pub type CFRunLoopMode = CFStringRef; +pub enum CFRunLoopObserver {} +pub type CFRunLoopObserverRef = *mut CFRunLoopObserver; +pub enum CFRunLoopTimer {} +pub type CFRunLoopTimerRef = *mut CFRunLoopTimer; +pub enum CFRunLoopSource {} +pub type CFRunLoopSourceRef = *mut CFRunLoopSource; +pub enum CFString {} +pub type CFStringRef = *const CFString; + +pub type CFHashCode = c_ulong; +pub type CFIndex = c_long; +pub type CFOptionFlags = c_ulong; +pub type CFRunLoopActivity = CFOptionFlags; -pub type JmpBuf = [c_int; JBLEN]; +pub type CFAbsoluteTime = CFTimeInterval; +pub type CFTimeInterval = f64; + +pub const kCFRunLoopEntry: CFRunLoopActivity = 0; +pub const kCFRunLoopBeforeWaiting: CFRunLoopActivity = 1 << 5; +pub const kCFRunLoopAfterWaiting: CFRunLoopActivity = 1 << 6; +pub const kCFRunLoopExit: CFRunLoopActivity = 1 << 7; + +pub type CFRunLoopObserverCallBack = extern "C" fn( + observer: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + info: *mut c_void, +); +pub type CFRunLoopTimerCallBack = extern "C" fn( + timer: CFRunLoopTimerRef, + info: *mut c_void, +); + +pub enum CFRunLoopObserverContext {} +pub enum CFRunLoopTimerContext {} + +#[repr(C)] +pub struct CFRunLoopSourceContext { + pub version: CFIndex, + pub info: *mut c_void, + pub retain: extern "C" fn(*const c_void) -> *const c_void, + pub release: extern "C" fn(*const c_void), + pub copyDescription: extern "C" fn(*const c_void) -> CFStringRef, + pub equal: extern "C" fn(*const c_void, *const c_void) -> Boolean, + pub hash: extern "C" fn(*const c_void) -> CFHashCode, + pub schedule: extern "C" fn(*mut c_void, CFRunLoopRef, CFRunLoopMode), + pub cancel: extern "C" fn(*mut c_void, CFRunLoopRef, CFRunLoopMode), + pub perform: extern "C" fn(*mut c_void), +} pub trait NSString: Sized { unsafe fn alloc(_: Self) -> id { diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index ed8d74ba84..c73b224f6a 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -47,663 +47,55 @@ //! //! This is how those event are represented in winit: //! -//! - applicationDidBecomeActive is Focused(true) -//! - applicationWillResignActive is Focused(false) -//! - applicationDidEnterBackground is Suspended(true) -//! - applicationWillEnterForeground is Suspended(false) -//! - applicationWillTerminate is Destroyed +//! - applicationDidBecomeActive is Suspended(false) +//! - applicationWillResignActive is Suspended(true) +//! - applicationWillTerminate is LoopDestroyed //! -//! Keep in mind that after Destroyed event is received every attempt to draw with +//! Keep in mind that after LoopDestroyed event is received every attempt to draw with //! opengl will result in segfault. //! -//! Also note that app will not receive Destroyed event if suspended, it will be SIGKILL'ed +//! Also note that app may not receive the LoopDestroyed event if suspended; it might be SIGKILL'ed. #![cfg(target_os = "ios")] -use std::{fmt, mem, ptr}; -use std::cell::RefCell; -use std::collections::VecDeque; -use std::os::raw::*; -use std::sync::Arc; - -use objc::declare::ClassDecl; -use objc::runtime::{BOOL, Class, Object, Sel, YES}; - -use { - CreationError, - Event, - LogicalPosition, - LogicalSize, - MouseCursor, - PhysicalPosition, - PhysicalSize, - WindowAttributes, - WindowEvent, - WindowId as RootEventId, -}; -use events::{Touch, TouchPhase}; -use window::MonitorHandle as RootMonitorHandle; - -mod ffi; -use self::ffi::{ - CFTimeInterval, - CFRunLoopRunInMode, - CGFloat, - CGPoint, - CGRect, - id, - JBLEN, - JmpBuf, - kCFRunLoopDefaultMode, - kCFRunLoopRunHandledSource, - longjmp, - nil, - NSString, - setjmp, - UIApplicationMain, - }; - -static mut JMPBUF: Option> = None; - -pub struct Window { - _events_queue: Arc>>, - delegate_state: Box, -} - -unsafe impl Send for Window {} -unsafe impl Sync for Window {} - -#[derive(Debug)] -struct DelegateState { - window: id, - controller: id, - view: id, - size: LogicalSize, - scale: f64, -} - -impl DelegateState { - fn new(window: id, controller: id, view: id, size: LogicalSize, scale: f64) -> DelegateState { - DelegateState { - window, - controller, - view, - size, - scale, - } - } -} - -impl Drop for DelegateState { - fn drop(&mut self) { - unsafe { - let _: () = msg_send![self.window, release]; - let _: () = msg_send![self.controller, release]; - let _: () = msg_send![self.view, release]; - } - } -} - -#[derive(Clone)] -pub struct MonitorHandle; - -impl fmt::Debug for MonitorHandle { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - #[derive(Debug)] - struct MonitorHandle { - name: Option, - dimensions: PhysicalSize, - position: PhysicalPosition, - hidpi_factor: f64, - } - - let monitor_id_proxy = MonitorHandle { - name: self.get_name(), - dimensions: self.get_dimensions(), - position: self.get_position(), - hidpi_factor: self.get_hidpi_factor(), - }; - - monitor_id_proxy.fmt(f) - } -} - -impl MonitorHandle { - #[inline] - pub fn get_uiscreen(&self) -> id { - let class = class!(UIScreen); - unsafe { msg_send![class, mainScreen] } - } - - #[inline] - pub fn get_name(&self) -> Option { - Some("Primary".to_string()) - } - - #[inline] - pub fn get_dimensions(&self) -> PhysicalSize { - let bounds: CGRect = unsafe { msg_send![self.get_uiscreen(), nativeBounds] }; - (bounds.size.width as f64, bounds.size.height as f64).into() - } - - #[inline] - pub fn get_position(&self) -> PhysicalPosition { - // iOS assumes single screen - (0, 0).into() - } - - #[inline] - pub fn get_hidpi_factor(&self) -> f64 { - let scale: CGFloat = unsafe { msg_send![self.get_uiscreen(), nativeScale] }; - scale as f64 - } -} - -pub struct EventLoop { - events_queue: Arc>>, -} - -#[derive(Clone)] -pub struct EventLoopProxy; - -impl EventLoop { - pub fn new() -> EventLoop { - unsafe { - if !msg_send![class!(NSThread), isMainThread] { - panic!("`EventLoop` can only be created on the main thread on iOS"); - } - } - EventLoop { events_queue: Default::default() } - } - - #[inline] - pub fn get_available_monitors(&self) -> VecDeque { - let mut rb = VecDeque::with_capacity(1); - rb.push_back(MonitorHandle); - rb - } - - #[inline] - pub fn get_primary_monitor(&self) -> MonitorHandle { - MonitorHandle - } - - pub fn poll_events(&mut self, mut callback: F) - where F: FnMut(::Event) - { - if let Some(event) = self.events_queue.borrow_mut().pop_front() { - callback(event); - return; - } - - unsafe { - // jump hack, so we won't quit on willTerminate event before processing it - assert!(JMPBUF.is_some(), "`EventLoop::poll_events` must be called after window creation on iOS"); - if setjmp(mem::transmute_copy(&mut JMPBUF)) != 0 { - if let Some(event) = self.events_queue.borrow_mut().pop_front() { - callback(event); - return; - } - } +// TODO: (mtak-) UIKit requires main thread for virtually all function/method calls. This could be +// worked around in the future by using GCD (grand central dispatch) and/or caching of values like +// window size/position. +macro_rules! assert_main_thread { + ($($t:tt)*) => { + if !msg_send![class!(NSThread), isMainThread] { + panic!($($t)*); } - - unsafe { - // run runloop - let seconds: CFTimeInterval = 0.000002; - while CFRunLoopRunInMode(kCFRunLoopDefaultMode, seconds, 1) == kCFRunLoopRunHandledSource {} - } - - if let Some(event) = self.events_queue.borrow_mut().pop_front() { - callback(event) - } - } - - pub fn run_forever(&mut self, mut callback: F) - where F: FnMut(::Event) -> ::ControlFlow, - { - // Yeah that's a very bad implementation. - loop { - let mut control_flow = ::ControlFlow::Continue; - self.poll_events(|e| { - if let ::ControlFlow::Break = callback(e) { - control_flow = ::ControlFlow::Break; - } - }); - if let ::ControlFlow::Break = control_flow { - break; - } - ::std::thread::sleep(::std::time::Duration::from_millis(5)); - } - } - - pub fn create_proxy(&self) -> EventLoopProxy { - EventLoopProxy - } + }; } -impl EventLoopProxy { - pub fn wakeup(&self) -> Result<(), ::EventLoopClosed> { - unimplemented!() - } -} +mod app_state; +mod event_loop; +mod ffi; +mod monitor; +mod view; +mod window; + +pub use self::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; +pub use self::monitor::MonitorHandle; +pub use self::window::{ + PlatformSpecificWindowBuilderAttributes, + Window, + WindowId, +}; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct WindowId; - -impl WindowId { - pub unsafe fn dummy() -> Self { - WindowId - } +pub struct DeviceId { + uiscreen: ffi::id, } -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DeviceId; - impl DeviceId { pub unsafe fn dummy() -> Self { - DeviceId - } -} - -#[derive(Clone)] -pub struct PlatformSpecificWindowBuilderAttributes { - pub root_view_class: &'static Class, -} - -impl Default for PlatformSpecificWindowBuilderAttributes { - fn default() -> Self { - PlatformSpecificWindowBuilderAttributes { - root_view_class: class!(UIView), - } - } -} - -// TODO: AFAIK transparency is enabled by default on iOS, -// so to be consistent with other platforms we have to change that. -impl Window { - pub fn new( - ev: &EventLoop, - _attributes: WindowAttributes, - pl_attributes: PlatformSpecificWindowBuilderAttributes, - ) -> Result { - unsafe { - debug_assert!(mem::size_of_val(&JMPBUF) == mem::size_of::>()); - assert!(mem::replace(&mut JMPBUF, Some(Box::new([0; JBLEN]))).is_none(), "Only one `Window` is supported on iOS"); - } - - unsafe { - if setjmp(mem::transmute_copy(&mut JMPBUF)) != 0 { - let app_class = class!(UIApplication); - let app: id = msg_send![app_class, sharedApplication]; - let delegate: id = msg_send![app, delegate]; - let state: *mut c_void = *(&*delegate).get_ivar("winitState"); - let mut delegate_state = Box::from_raw(state as *mut DelegateState); - let events_queue = &*ev.events_queue; - (&mut *delegate).set_ivar("eventsQueue", mem::transmute::<_, *mut c_void>(events_queue)); - - // easiest? way to get access to PlatformSpecificWindowBuilderAttributes to configure the view - let rect: CGRect = msg_send![MonitorHandle.get_uiscreen(), bounds]; - - let uiview_class = class!(UIView); - let root_view_class = pl_attributes.root_view_class; - let is_uiview: BOOL = msg_send![root_view_class, isSubclassOfClass:uiview_class]; - assert!(is_uiview == YES, "`root_view_class` must inherit from `UIView`"); - - delegate_state.view = msg_send![root_view_class, alloc]; - assert!(!delegate_state.view.is_null(), "Failed to create `UIView` instance"); - delegate_state.view = msg_send![delegate_state.view, initWithFrame:rect]; - assert!(!delegate_state.view.is_null(), "Failed to initialize `UIView` instance"); - - let _: () = msg_send![delegate_state.controller, setView:delegate_state.view]; - let _: () = msg_send![delegate_state.window, makeKeyAndVisible]; - - return Ok(Window { - _events_queue: ev.events_queue.clone(), - delegate_state, - }); - } - } - - create_delegate_class(); - start_app(); - - panic!("Couldn't create `UIApplication`!") - } - - #[inline] - pub fn get_uiwindow(&self) -> id { - self.delegate_state.window - } - - #[inline] - pub fn get_uiview(&self) -> id { - self.delegate_state.view - } - - #[inline] - pub fn set_title(&self, _title: &str) { - // N/A - } - - #[inline] - pub fn show(&self) { - // N/A - } - - #[inline] - pub fn hide(&self) { - // N/A - } - - #[inline] - pub fn get_position(&self) -> Option { - // N/A - None - } - - #[inline] - pub fn get_inner_position(&self) -> Option { - // N/A - None - } - - #[inline] - pub fn set_position(&self, _position: LogicalPosition) { - // N/A - } - - #[inline] - pub fn get_inner_size(&self) -> Option { - Some(self.delegate_state.size) - } - - #[inline] - pub fn get_outer_size(&self) -> Option { - self.get_inner_size() - } - - #[inline] - pub fn set_inner_size(&self, _size: LogicalSize) { - // N/A - } - - #[inline] - pub fn set_min_dimensions(&self, _dimensions: Option) { - // N/A - } - - #[inline] - pub fn set_max_dimensions(&self, _dimensions: Option) { - // N/A - } - - #[inline] - pub fn set_resizable(&self, _resizable: bool) { - // N/A - } - - #[inline] - pub fn set_cursor(&self, _cursor: MouseCursor) { - // N/A - } - - #[inline] - pub fn grab_cursor(&self, _grab: bool) -> Result<(), String> { - Err("Cursor grabbing is not possible on iOS.".to_owned()) - } - - #[inline] - pub fn hide_cursor(&self, _hide: bool) { - // N/A - } - - #[inline] - pub fn get_hidpi_factor(&self) -> f64 { - self.delegate_state.scale - } - - #[inline] - pub fn set_cursor_position(&self, _position: LogicalPosition) -> Result<(), String> { - Err("Setting cursor position is not possible on iOS.".to_owned()) - } - - #[inline] - pub fn set_maximized(&self, _maximized: bool) { - // N/A - // iOS has single screen maximized apps so nothing to do - } - - #[inline] - pub fn get_fullscreen(&self) -> Option { - // N/A - // iOS has single screen maximized apps so nothing to do - None - } - - #[inline] - pub fn set_fullscreen(&self, _monitor: Option) { - // N/A - // iOS has single screen maximized apps so nothing to do - } - - #[inline] - pub fn set_decorations(&self, _decorations: bool) { - // N/A - } - - #[inline] - pub fn set_always_on_top(&self, _always_on_top: bool) { - // N/A - } - - #[inline] - pub fn set_window_icon(&self, _icon: Option<::Icon>) { - // N/A - } - - #[inline] - pub fn set_ime_spot(&self, _logical_spot: LogicalPosition) { - // N/A - } - - #[inline] - pub fn get_current_monitor(&self) -> RootMonitorHandle { - RootMonitorHandle { inner: MonitorHandle } - } - - #[inline] - pub fn get_available_monitors(&self) -> VecDeque { - let mut rb = VecDeque::with_capacity(1); - rb.push_back(MonitorHandle); - rb - } - - #[inline] - pub fn get_primary_monitor(&self) -> MonitorHandle { - MonitorHandle - } - - #[inline] - pub fn id(&self) -> WindowId { - WindowId - } -} - -fn create_delegate_class() { - extern fn did_finish_launching(this: &mut Object, _: Sel, _: id, _: id) -> BOOL { - let screen_class = class!(UIScreen); - let window_class = class!(UIWindow); - let controller_class = class!(UIViewController); - unsafe { - let main_screen: id = msg_send![screen_class, mainScreen]; - let bounds: CGRect = msg_send![main_screen, bounds]; - let scale: CGFloat = msg_send![main_screen, nativeScale]; - - let window: id = msg_send![window_class, alloc]; - let window: id = msg_send![window, initWithFrame:bounds.clone()]; - - let size = (bounds.size.width as f64, bounds.size.height as f64).into(); - - let view_controller: id = msg_send![controller_class, alloc]; - let view_controller: id = msg_send![view_controller, init]; - - let _: () = msg_send![window, setRootViewController:view_controller]; - - let state = Box::new(DelegateState::new(window, view_controller, ptr::null_mut(), size, scale as f64)); - let state_ptr: *mut DelegateState = mem::transmute(state); - this.set_ivar("winitState", state_ptr as *mut c_void); - - // The `UIView` is setup in `Window::new` which gets `longjmp`'ed to here. - // This makes it easier to configure the specific `UIView` type. - let _: () = msg_send![this, performSelector:sel!(postLaunch:) withObject:nil afterDelay:0.0]; - } - YES - } - - extern fn post_launch(_: &Object, _: Sel, _: id) { - unsafe { longjmp(mem::transmute_copy(&mut JMPBUF), 1); } - } - - extern fn did_become_active(this: &Object, _: Sel, _: id) { - unsafe { - let events_queue: *mut c_void = *this.get_ivar("eventsQueue"); - let events_queue = &*(events_queue as *const RefCell>); - events_queue.borrow_mut().push_back(Event::WindowEvent { - window_id: RootEventId(WindowId), - event: WindowEvent::Focused(true), - }); - } - } - - extern fn will_resign_active(this: &Object, _: Sel, _: id) { - unsafe { - let events_queue: *mut c_void = *this.get_ivar("eventsQueue"); - let events_queue = &*(events_queue as *const RefCell>); - events_queue.borrow_mut().push_back(Event::WindowEvent { - window_id: RootEventId(WindowId), - event: WindowEvent::Focused(false), - }); - } - } - - extern fn will_enter_foreground(this: &Object, _: Sel, _: id) { - unsafe { - let events_queue: *mut c_void = *this.get_ivar("eventsQueue"); - let events_queue = &*(events_queue as *const RefCell>); - events_queue.borrow_mut().push_back(Event::Suspended(false)); - } - } - - extern fn did_enter_background(this: &Object, _: Sel, _: id) { - unsafe { - let events_queue: *mut c_void = *this.get_ivar("eventsQueue"); - let events_queue = &*(events_queue as *const RefCell>); - events_queue.borrow_mut().push_back(Event::Suspended(true)); - } - } - - extern fn will_terminate(this: &Object, _: Sel, _: id) { - unsafe { - let events_queue: *mut c_void = *this.get_ivar("eventsQueue"); - let events_queue = &*(events_queue as *const RefCell>); - // push event to the front to garantee that we'll process it - // immidiatly after jump - events_queue.borrow_mut().push_front(Event::WindowEvent { - window_id: RootEventId(WindowId), - event: WindowEvent::Destroyed, - }); - longjmp(mem::transmute_copy(&mut JMPBUF), 1); - } - } - - extern fn handle_touches(this: &Object, _: Sel, touches: id, _:id) { - unsafe { - let events_queue: *mut c_void = *this.get_ivar("eventsQueue"); - let events_queue = &*(events_queue as *const RefCell>); - - let touches_enum: id = msg_send![touches, objectEnumerator]; - - loop { - let touch: id = msg_send![touches_enum, nextObject]; - if touch == nil { - break - } - let location: CGPoint = msg_send![touch, locationInView:nil]; - let touch_id = touch as u64; - let phase: i32 = msg_send![touch, phase]; - - events_queue.borrow_mut().push_back(Event::WindowEvent { - window_id: RootEventId(WindowId), - event: WindowEvent::Touch(Touch { - device_id: DEVICE_ID, - id: touch_id, - location: (location.x as f64, location.y as f64).into(), - phase: match phase { - 0 => TouchPhase::Started, - 1 => TouchPhase::Moved, - // 2 is UITouchPhaseStationary and is not expected here - 3 => TouchPhase::Ended, - 4 => TouchPhase::Cancelled, - _ => panic!("unexpected touch phase: {:?}", phase) - } - }), - }); - } + DeviceId { + uiscreen: std::ptr::null_mut(), } } - - let ui_responder = class!(UIResponder); - let mut decl = ClassDecl::new("AppDelegate", ui_responder).expect("Failed to declare class `AppDelegate`"); - - unsafe { - decl.add_method(sel!(application:didFinishLaunchingWithOptions:), - did_finish_launching as extern fn(&mut Object, Sel, id, id) -> BOOL); - - decl.add_method(sel!(applicationDidBecomeActive:), - did_become_active as extern fn(&Object, Sel, id)); - - decl.add_method(sel!(applicationWillResignActive:), - will_resign_active as extern fn(&Object, Sel, id)); - - decl.add_method(sel!(applicationWillEnterForeground:), - will_enter_foreground as extern fn(&Object, Sel, id)); - - decl.add_method(sel!(applicationDidEnterBackground:), - did_enter_background as extern fn(&Object, Sel, id)); - - decl.add_method(sel!(applicationWillTerminate:), - will_terminate as extern fn(&Object, Sel, id)); - - - decl.add_method(sel!(touchesBegan:withEvent:), - handle_touches as extern fn(this: &Object, _: Sel, _: id, _:id)); - - decl.add_method(sel!(touchesMoved:withEvent:), - handle_touches as extern fn(this: &Object, _: Sel, _: id, _:id)); - - decl.add_method(sel!(touchesEnded:withEvent:), - handle_touches as extern fn(this: &Object, _: Sel, _: id, _:id)); - - decl.add_method(sel!(touchesCancelled:withEvent:), - handle_touches as extern fn(this: &Object, _: Sel, _: id, _:id)); - - - decl.add_method(sel!(postLaunch:), - post_launch as extern fn(&Object, Sel, id)); - - decl.add_ivar::<*mut c_void>("winitState"); - decl.add_ivar::<*mut c_void>("eventsQueue"); - - decl.register(); - } -} - -#[inline] -fn start_app() { - unsafe { - UIApplicationMain(0, ptr::null(), nil, NSString::alloc(nil).init_str("AppDelegate")); - } } -// Constant device ID, to be removed when this backend is updated to report real device IDs. -const DEVICE_ID: ::DeviceId = ::DeviceId(DeviceId); +unsafe impl Send for DeviceId {} +unsafe impl Sync for DeviceId {} diff --git a/src/platform_impl/ios/monitor.rs b/src/platform_impl/ios/monitor.rs new file mode 100644 index 0000000000..8d01909397 --- /dev/null +++ b/src/platform_impl/ios/monitor.rs @@ -0,0 +1,171 @@ +use std::{ + collections::VecDeque, + fmt, + ops::{Deref, DerefMut}, +}; + +use dpi::{PhysicalPosition, PhysicalSize}; + +use platform_impl::platform::ffi::{ + id, + nil, + CGFloat, + CGRect, + NSUInteger, +}; + +pub struct Inner { + uiscreen: id, +} + +impl Drop for Inner { + fn drop(&mut self) { + unsafe { + let () = msg_send![self.uiscreen, release]; + } + } +} + +pub struct MonitorHandle { + inner: Inner, +} + +impl Deref for MonitorHandle { + type Target = Inner; + + fn deref(&self) -> &Inner { + unsafe { + assert_main_thread!("`MonitorHandle` methods can only be run on the main thread on iOS"); + } + &self.inner + } +} + +impl DerefMut for MonitorHandle { + fn deref_mut(&mut self) -> &mut Inner { + unsafe { + assert_main_thread!("`MonitorHandle` methods can only be run on the main thread on iOS"); + } + &mut self.inner + } +} + +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) { + unsafe { + assert_main_thread!("`MonitorHandle` can only be dropped on the main thread on iOS"); + } + } +} + +impl fmt::Debug for MonitorHandle { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + #[derive(Debug)] + struct MonitorHandle { + name: Option, + dimensions: PhysicalSize, + position: PhysicalPosition, + hidpi_factor: f64, + } + + let monitor_id_proxy = MonitorHandle { + name: self.get_name(), + dimensions: self.get_dimensions(), + position: self.get_position(), + hidpi_factor: self.get_hidpi_factor(), + }; + + monitor_id_proxy.fmt(f) + } +} + +impl MonitorHandle { + pub fn retained_new(uiscreen: id) -> MonitorHandle { + unsafe { + assert_main_thread!("`MonitorHandle` can only be cloned on the main thread on iOS"); + let () = msg_send![uiscreen, retain]; + } + MonitorHandle { inner: Inner { uiscreen } } + } +} + +impl Inner { + pub fn get_name(&self) -> Option { + unsafe { + if self.uiscreen == main_uiscreen().uiscreen { + Some("Primary".to_string()) + } else if self.uiscreen == mirrored_uiscreen().uiscreen { + Some("Mirrored".to_string()) + } else { + uiscreens() + .iter() + .position(|rhs| rhs.uiscreen == self.uiscreen) + .map(|idx| idx.to_string()) + } + } + } + + pub fn get_dimensions(&self) -> PhysicalSize { + unsafe { + let bounds: CGRect = msg_send![self.get_uiscreen(), nativeBounds]; + (bounds.size.width as f64, bounds.size.height as f64).into() + } + } + + pub fn get_position(&self) -> PhysicalPosition { + unsafe { + let bounds: CGRect = msg_send![self.get_uiscreen(), nativeBounds]; + (bounds.origin.x as f64, bounds.origin.y as f64).into() + } + } + + pub fn get_hidpi_factor(&self) -> f64 { + unsafe { + let scale: CGFloat = msg_send![self.get_uiscreen(), nativeScale]; + scale as f64 + } + } +} + +// MonitorHandleExtIOS +impl Inner { + pub fn get_uiscreen(&self) -> id { + self.uiscreen + } +} + +// 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() -> MonitorHandle { + let uiscreen: id = msg_send![class!(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)); + } +} diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs new file mode 100644 index 0000000000..72918f0ce6 --- /dev/null +++ b/src/platform_impl/ios/view.rs @@ -0,0 +1,395 @@ +use std::collections::HashMap; + +use objc::declare::ClassDecl; +use objc::runtime::{BOOL, Class, NO, Object, Sel, YES}; + +use event::{ + DeviceId as RootDeviceId, + Event, + Touch, + TouchPhase, + WindowEvent +}; +use platform::ios::MonitorHandleExtIOS; +use window::{WindowAttributes, WindowId as RootWindowId}; + +use platform_impl::platform::app_state::AppState; +use platform_impl::platform::DeviceId; +use platform_impl::platform::event_loop; +use platform_impl::platform::ffi::{ + id, + nil, + CGFloat, + CGPoint, + CGRect, + UIInterfaceOrientationMask, + UITouchPhase, +}; +use platform_impl::platform::window::{PlatformSpecificWindowBuilderAttributes}; + +// requires main thread +unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { + static mut CLASSES: Option> = None; + static mut ID: usize = 0; + + if CLASSES.is_none() { + CLASSES = Some(HashMap::default()); + } + + let classes = CLASSES.as_mut().unwrap(); + + classes.entry(root_view_class).or_insert_with(move || { + let uiview_class = class!(UIView); + let is_uiview: BOOL = msg_send![root_view_class, isSubclassOfClass:uiview_class]; + assert_eq!(is_uiview, YES, "`root_view_class` must inherit from `UIView`"); + + extern fn draw_rect(object: &Object, _: Sel, rect: CGRect) { + unsafe { + let window: id = msg_send![object, window]; + AppState::handle_nonuser_event(Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::RedrawRequested, + }); + let superclass: &'static Class = msg_send![object, superclass]; + let () = msg_send![super(object, superclass), drawRect: rect]; + } + } + + extern fn layout_subviews(object: &Object, _: Sel) { + unsafe { + let window: id = msg_send![object, window]; + let 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![object, convertRect:bounds toCoordinateSpace:screen_space]; + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width, + height: screen_frame.size.height, + }; + AppState::handle_nonuser_event(Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::Resized(size), + }); + let superclass: &'static Class = msg_send![object, superclass]; + let () = msg_send![super(object, superclass), layoutSubviews]; + } + } + + let mut decl = ClassDecl::new(&format!("WinitUIView{}", ID), root_view_class) + .expect("Failed to declare class `WinitUIView`"); + ID += 1; + decl.add_method(sel!(drawRect:), + draw_rect as extern fn(&Object, Sel, CGRect)); + decl.add_method(sel!(layoutSubviews), + layout_subviews as extern fn(&Object, Sel)); + decl.register() + }) +} + +// requires main thread +unsafe fn get_view_controller_class() -> &'static Class { + static mut CLASS: Option<&'static Class> = None; + if CLASS.is_none() { + let uiviewcontroller_class = class!(UIViewController); + + extern fn set_prefers_status_bar_hidden(object: &mut Object, _: Sel, hidden: BOOL) { + unsafe { + object.set_ivar::("_prefers_status_bar_hidden", hidden); + let () = msg_send![object, setNeedsStatusBarAppearanceUpdate]; + } + } + + extern fn prefers_status_bar_hidden(object: &Object, _: Sel) -> BOOL { + unsafe { + *object.get_ivar::("_prefers_status_bar_hidden") + } + } + + extern fn set_supported_orientations(object: &mut Object, _: Sel, orientations: UIInterfaceOrientationMask) { + unsafe { + object.set_ivar::("_supported_orientations", orientations); + let () = msg_send![class!(UIViewController), attemptRotationToDeviceOrientation]; + } + } + + extern fn supported_orientations(object: &Object, _: Sel) -> UIInterfaceOrientationMask { + unsafe { + *object.get_ivar::("_supported_orientations") + } + } + + extern fn should_autorotate(_: &Object, _: Sel) -> BOOL { + YES + } + + let mut decl = ClassDecl::new("WinitUIViewController", uiviewcontroller_class) + .expect("Failed to declare class `WinitUIViewController`"); + decl.add_ivar::("_prefers_status_bar_hidden"); + decl.add_ivar::("_supported_orientations"); + decl.add_method(sel!(setPrefersStatusBarHidden:), + set_prefers_status_bar_hidden as extern fn(&mut Object, Sel, BOOL)); + decl.add_method(sel!(prefersStatusBarHidden), + prefers_status_bar_hidden as extern fn(&Object, Sel) -> BOOL); + decl.add_method(sel!(setSupportedInterfaceOrientations:), + set_supported_orientations as extern fn(&mut Object, Sel, UIInterfaceOrientationMask)); + decl.add_method(sel!(supportedInterfaceOrientations), + supported_orientations as extern fn(&Object, Sel) -> UIInterfaceOrientationMask); + decl.add_method(sel!(shouldAutorotate), + should_autorotate as extern fn(&Object, Sel) -> BOOL); + CLASS = Some(decl.register()); + } + CLASS.unwrap() +} + +// requires main thread +unsafe fn get_window_class() -> &'static Class { + static mut CLASS: Option<&'static Class> = None; + if CLASS.is_none() { + let uiwindow_class = class!(UIWindow); + + extern fn become_key_window(object: &Object, _: Sel) { + unsafe { + AppState::handle_nonuser_event(Event::WindowEvent { + window_id: RootWindowId(object.into()), + event: WindowEvent::Focused(true), + }); + let () = msg_send![super(object, class!(UIWindow)), becomeKeyWindow]; + } + } + + extern fn resign_key_window(object: &Object, _: Sel) { + unsafe { + AppState::handle_nonuser_event(Event::WindowEvent { + window_id: RootWindowId(object.into()), + event: WindowEvent::Focused(false), + }); + let () = msg_send![super(object, class!(UIWindow)), resignKeyWindow]; + } + } + + extern fn handle_touches(object: &Object, _: Sel, touches: id, _:id) { + unsafe { + let uiscreen = msg_send![object, screen]; + let touches_enum: id = msg_send![touches, objectEnumerator]; + let mut touch_events = Vec::new(); + loop { + let touch: id = msg_send![touches_enum, nextObject]; + if touch == nil { + break + } + let location: CGPoint = msg_send![touch, locationInView:nil]; + 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), + }; + + touch_events.push(Event::WindowEvent { + window_id: RootWindowId(object.into()), + event: WindowEvent::Touch(Touch { + device_id: RootDeviceId(DeviceId { uiscreen }), + id: touch_id, + location: (location.x as f64, location.y as f64).into(), + phase, + }), + }); + } + AppState::handle_nonuser_events(touch_events); + } + } + + extern fn set_content_scale_factor(object: &mut Object, _: Sel, hidpi_factor: CGFloat) { + unsafe { + let () = msg_send![super(object, class!(UIWindow)), setContentScaleFactor:hidpi_factor]; + let view_controller: id = msg_send![object, rootViewController]; + let view: id = msg_send![view_controller, view]; + let () = msg_send![view, setContentScaleFactor:hidpi_factor]; + let bounds: CGRect = msg_send![object, bounds]; + let screen: id = msg_send![object, screen]; + let screen_space: id = msg_send![screen, coordinateSpace]; + let screen_frame: CGRect = msg_send![object, convertRect:bounds toCoordinateSpace:screen_space]; + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width, + height: screen_frame.size.height, + }; + AppState::handle_nonuser_events( + std::iter::once(Event::WindowEvent { + window_id: RootWindowId(object.into()), + event: WindowEvent::HiDpiFactorChanged(hidpi_factor as _), + }).chain(std::iter::once(Event::WindowEvent { + window_id: RootWindowId(object.into()), + event: WindowEvent::Resized(size), + })) + ); + } + } + + let mut decl = ClassDecl::new("WinitUIWindow", uiwindow_class) + .expect("Failed to declare class `WinitUIWindow`"); + decl.add_method(sel!(becomeKeyWindow), + become_key_window as extern fn(&Object, Sel)); + decl.add_method(sel!(resignKeyWindow), + resign_key_window as extern fn(&Object, Sel)); + + decl.add_method(sel!(touchesBegan:withEvent:), + handle_touches as extern fn(this: &Object, _: Sel, _: id, _:id)); + decl.add_method(sel!(touchesMoved:withEvent:), + handle_touches as extern fn(this: &Object, _: Sel, _: id, _:id)); + decl.add_method(sel!(touchesEnded:withEvent:), + handle_touches as extern fn(this: &Object, _: Sel, _: id, _:id)); + decl.add_method(sel!(touchesCancelled:withEvent:), + handle_touches as extern fn(this: &Object, _: Sel, _: id, _:id)); + + decl.add_method(sel!(setContentScaleFactor:), + set_content_scale_factor as extern fn(&mut Object, Sel, CGFloat)); + + CLASS = Some(decl.register()); + } + CLASS.unwrap() +} + +// requires main thread +pub unsafe fn create_view( + window_attributes: &WindowAttributes, + platform_attributes: &PlatformSpecificWindowBuilderAttributes, + frame: CGRect, +) -> id { + let class = get_view_class(platform_attributes.root_view_class); + + let view: id = msg_send![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"); + if window_attributes.multitouch { + let () = msg_send![view, setMultipleTouchEnabled:YES]; + } + + view +} + +// requires main thread +pub unsafe fn create_view_controller( + window_attributes: &WindowAttributes, + platform_attributes: &PlatformSpecificWindowBuilderAttributes, + view: id, +) -> id { + let class = get_view_controller_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 = if window_attributes.decorations { + NO + } else { + YES + }; + let idiom = event_loop::get_idiom(); + let supported_orientations = UIInterfaceOrientationMask::from_valid_orientations_idiom( + platform_attributes.valid_orientations, + idiom, + ); + let () = msg_send![view_controller, setPrefersStatusBarHidden:status_bar_hidden]; + let () = msg_send![view_controller, setSupportedInterfaceOrientations:supported_orientations]; + let () = msg_send![view_controller, setView:view]; + view_controller +} + +// requires main thread +pub unsafe fn create_window( + window_attributes: &WindowAttributes, + platform_attributes: &PlatformSpecificWindowBuilderAttributes, + frame: CGRect, + view_controller: id, +) -> id { + let class = get_window_class(); + + let window: id = msg_send![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]; + if let Some(hidpi_factor) = platform_attributes.hidpi_factor { + let () = msg_send![window, setContentScaleFactor:hidpi_factor as CGFloat]; + } + if let &Some(ref monitor) = &window_attributes.fullscreen { + let () = msg_send![window, setScreen:monitor.get_uiscreen()]; + } + + window +} + +pub fn create_delegate_class() { + extern fn did_finish_launching(_: &mut Object, _: Sel, _: id, _: id) -> BOOL { + unsafe { + AppState::did_finish_launching(); + } + YES + } + + extern fn did_become_active(_: &Object, _: Sel, _: id) { + unsafe { + AppState::handle_nonuser_event(Event::Suspended(false)) + } + } + + extern fn will_resign_active(_: &Object, _: Sel, _: id) { + unsafe { + AppState::handle_nonuser_event(Event::Suspended(true)) + } + } + + extern fn will_enter_foreground(_: &Object, _: Sel, _: id) {} + extern fn did_enter_background(_: &Object, _: Sel, _: id) {} + + extern fn will_terminate(_: &Object, _: Sel, _: 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: BOOL = msg_send![window, isKindOfClass:class!(WinitUIWindow)]; + if is_winit_window == YES { + events.push(Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::Destroyed, + }); + } + } + AppState::handle_nonuser_events(events); + AppState::terminated(); + } + } + + let ui_responder = class!(UIResponder); + let mut decl = ClassDecl::new("AppDelegate", ui_responder).expect("Failed to declare class `AppDelegate`"); + + unsafe { + decl.add_method(sel!(application:didFinishLaunchingWithOptions:), + did_finish_launching as extern fn(&mut Object, Sel, id, id) -> BOOL); + + decl.add_method(sel!(applicationDidBecomeActive:), + did_become_active as extern fn(&Object, Sel, id)); + decl.add_method(sel!(applicationWillResignActive:), + will_resign_active as extern fn(&Object, Sel, id)); + decl.add_method(sel!(applicationWillEnterForeground:), + will_enter_foreground as extern fn(&Object, Sel, id)); + decl.add_method(sel!(applicationDidEnterBackground:), + did_enter_background as extern fn(&Object, Sel, id)); + + decl.add_method(sel!(applicationWillTerminate:), + will_terminate as extern fn(&Object, Sel, id)); + + decl.register(); + } +} diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs new file mode 100644 index 0000000000..0d44c64f22 --- /dev/null +++ b/src/platform_impl/ios/window.rs @@ -0,0 +1,488 @@ +use std::{ + collections::VecDeque, + ops::{Deref, DerefMut}, +}; + +use objc::runtime::{Class, NO, Object, YES}; + +use dpi::{self, LogicalPosition, LogicalSize}; +use icon::Icon; +use monitor::MonitorHandle as RootMonitorHandle; +use platform::ios::{MonitorHandleExtIOS, ValidOrientations}; +use window::{ + CreationError, + MouseCursor, + WindowAttributes, +}; +use platform_impl::{ + platform::{ + app_state::AppState, + event_loop, + ffi::{ + id, + CGFloat, + CGPoint, + CGRect, + CGSize, + UIEdgeInsets, + UIInterfaceOrientationMask, + }, + monitor, + view, + EventLoopWindowTarget, + MonitorHandle + }, +}; + +pub struct Inner { + pub window: id, + pub view_controller: id, + pub view: id, + supports_safe_area: 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 show(&self) { + unsafe { + let () = msg_send![self.window, setHidden:NO]; + } + } + + pub fn hide(&self) { + unsafe { + let () = msg_send![self.window, setHidden:YES]; + } + } + + pub fn request_redraw(&self) { + unsafe { + let () = msg_send![self.view, setNeedsDisplay]; + } + } + + pub fn get_inner_position(&self) -> Option { + unsafe { + let safe_area = self.safe_area_screen_space(); + Some(LogicalPosition { + x: safe_area.origin.x, + y: safe_area.origin.y, + }) + } + } + + pub fn get_position(&self) -> Option { + unsafe { + let screen_frame = self.screen_frame(); + Some(LogicalPosition { + x: screen_frame.origin.x, + y: screen_frame.origin.y, + }) + } + } + + pub fn set_position(&self, position: LogicalPosition) { + unsafe { + let screen_frame = self.screen_frame(); + let new_screen_frame = CGRect { + origin: CGPoint { + x: position.x as _, + y: position.y as _, + }, + size: screen_frame.size, + }; + let bounds = self.from_screen_space(new_screen_frame); + let () = msg_send![self.window, setBounds:bounds]; + } + } + + pub fn get_inner_size(&self) -> Option { + unsafe { + let safe_area = self.safe_area_screen_space(); + Some(LogicalSize { + width: safe_area.size.width, + height: safe_area.size.height, + }) + } + } + + pub fn get_outer_size(&self) -> Option { + unsafe { + let screen_frame = self.screen_frame(); + Some(LogicalSize { + width: screen_frame.size.width, + height: screen_frame.size.height, + }) + } + } + + pub fn set_inner_size(&self, _size: LogicalSize) { + unimplemented!("not clear what `Window::set_inner_size` means on iOS"); + } + + pub fn set_min_dimensions(&self, _dimensions: Option) { + warn!("`Window::set_min_dimensions` is ignored on iOS") + } + + pub fn set_max_dimensions(&self, _dimensions: Option) { + warn!("`Window::set_max_dimensions` is ignored on iOS") + } + + pub fn set_resizable(&self, _resizable: bool) { + warn!("`Window::set_resizable` is ignored on iOS") + } + + pub fn get_hidpi_factor(&self) -> f64 { + unsafe { + let hidpi: CGFloat = msg_send![self.view, contentScaleFactor]; + hidpi as _ + } + } + + pub fn set_cursor(&self, _cursor: MouseCursor) { + debug!("`Window::set_cursor` ignored on iOS") + } + + pub fn set_cursor_position(&self, _position: LogicalPosition) -> Result<(), String> { + Err("Setting cursor position is not possible on iOS.".to_owned()) + } + + pub fn grab_cursor(&self, _grab: bool) -> Result<(), String> { + Err("Cursor grabbing is not possible on iOS.".to_owned()) + } + + pub fn hide_cursor(&self, _hide: bool) { + debug!("`Window::hide_cursor` is ignored on iOS") + } + + pub fn set_maximized(&self, _maximized: bool) { + warn!("`Window::set_maximized` is ignored on iOS") + } + + pub fn set_fullscreen(&self, monitor: Option) { + unsafe { + match monitor { + Some(monitor) => { + let uiscreen = monitor.get_uiscreen() as id; + let current: id = msg_send![self.window, screen]; + let bounds: CGRect = msg_send![uiscreen, bounds]; + + // this is pretty slow on iOS, so avoid doing it if we can + if uiscreen != current { + let () = msg_send![self.window, setScreen:uiscreen]; + } + let () = msg_send![self.window, setFrame:bounds]; + } + None => warn!("`Window::set_fullscreen(None)` ignored on iOS"), + } + } + } + + pub fn get_fullscreen(&self) -> Option { + unsafe { + let monitor = self.get_current_monitor(); + let uiscreen = monitor.inner.get_uiscreen(); + let screen_space_bounds = self.screen_frame(); + let screen_bounds: CGRect = msg_send![uiscreen, bounds]; + + // TODO: track fullscreen instead of relying on brittle float comparisons + if screen_space_bounds.origin.x == screen_bounds.origin.x + && screen_space_bounds.origin.y == screen_bounds.origin.y + && screen_space_bounds.size.width == screen_bounds.size.width + && screen_space_bounds.size.height == screen_bounds.size.height + { + Some(monitor) + } else { + None + } + } + } + + pub fn set_decorations(&self, decorations: bool) { + unsafe { + let status_bar_hidden = if decorations { NO } else { YES }; + let () = msg_send![self.view_controller, setPrefersStatusBarHidden:status_bar_hidden]; + } + } + + pub fn set_always_on_top(&self, _always_on_top: bool) { + warn!("`Window::set_always_on_top` is ignored on iOS") + } + + pub fn set_window_icon(&self, _icon: Option) { + warn!("`Window::set_window_icon` is ignored on iOS") + } + + pub fn set_ime_spot(&self, _position: LogicalPosition) { + warn!("`Window::set_ime_spot` is ignored on iOS") + } + + pub fn get_current_monitor(&self) -> RootMonitorHandle { + unsafe { + let uiscreen: id = msg_send![self.window, screen]; + RootMonitorHandle { inner: MonitorHandle::retained_new(uiscreen) } + } + } + + pub fn get_available_monitors(&self) -> VecDeque { + unsafe { + monitor::uiscreens() + } + } + + pub fn get_primary_monitor(&self) -> MonitorHandle { + unsafe { + monitor::main_uiscreen() + } + } + + pub fn id(&self) -> WindowId { + self.window.into() + } +} + +pub struct Window { + pub inner: Inner, +} + +impl Drop for Window { + fn drop(&mut self) { + unsafe { + assert_main_thread!("`Window::drop` can only be run on the main thread on iOS"); + } + } +} + +unsafe impl Send for Window {} +unsafe impl Sync for Window {} + +impl Deref for Window { + type Target = Inner; + + fn deref(&self) -> &Inner { + unsafe { + assert_main_thread!("`Window` methods can only be run on the main thread on iOS"); + } + &self.inner + } +} + +impl DerefMut for Window { + fn deref_mut(&mut self) -> &mut Inner { + unsafe { + assert_main_thread!("`Window` methods can only be run on the main thread on iOS"); + } + &mut self.inner + } +} + +impl Window { + pub fn new( + event_loop: &EventLoopWindowTarget, + window_attributes: WindowAttributes, + platform_attributes: PlatformSpecificWindowBuilderAttributes, + ) -> Result { + if let Some(_) = window_attributes.min_dimensions { + warn!("`WindowAttributes::min_dimensions` is ignored on iOS"); + } + if let Some(_) = window_attributes.max_dimensions { + warn!("`WindowAttributes::max_dimensions` is ignored on iOS"); + } + if window_attributes.always_on_top { + warn!("`WindowAttributes::always_on_top` is unsupported on iOS"); + } + // TODO: transparency, visible + + unsafe { + let screen = window_attributes.fullscreen + .as_ref() + .map(|screen| screen.get_uiscreen() as _) + .unwrap_or_else(|| monitor::main_uiscreen().get_uiscreen()); + let screen_bounds: CGRect = msg_send![screen, bounds]; + + let frame = match window_attributes.dimensions { + Some(dim) => CGRect { + origin: screen_bounds.origin, + size: CGSize { width: dim.width, height: dim.height }, + }, + None => screen_bounds, + }; + + let view = view::create_view(&window_attributes, &platform_attributes, frame.clone()); + 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 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); + Ok(result) + } + } +} + +// WindowExtIOS +impl Inner { + pub fn get_uiwindow(&self) -> id { self.window } + pub fn get_uiviewcontroller(&self) -> id { self.view_controller } + pub fn get_uiview(&self) -> id { self.view } + + pub fn set_hidpi_factor(&self, hidpi_factor: f64) { + unsafe { + assert!(dpi::validate_hidpi_factor(hidpi_factor), "`WindowExtIOS::set_hidpi_factor` received an invalid hidpi factor"); + let hidpi_factor = hidpi_factor as CGFloat; + let () = msg_send![self.view, setContentScaleFactor:hidpi_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]; + } + } +} + +impl Inner { + // requires main thread + unsafe fn screen_frame(&self) -> CGRect { + self.to_screen_space(msg_send![self.window, bounds]) + } + + // requires main thread + unsafe fn 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 + } + } + + // requires main thread + unsafe fn 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 + } + } + + // requires main thread + unsafe fn safe_area_screen_space(&self) -> CGRect { + let bounds: CGRect = msg_send![self.window, bounds]; + if self.supports_safe_area { + let safe_area: UIEdgeInsets = msg_send![self.window, safeAreaInsets]; + let safe_bounds = CGRect { + origin: CGPoint { + x: bounds.origin.x + safe_area.left, + y: bounds.origin.y + safe_area.top, + }, + size: CGSize { + width: bounds.size.width - safe_area.left - safe_area.right, + height: bounds.size.height - safe_area.top - safe_area.bottom, + }, + }; + self.to_screen_space(safe_bounds) + } else { + let screen_frame = self.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"); + msg_send![app, statusBarFrame] + }; + let (y, height) = if screen_frame.origin.y > status_bar_frame.size.height { + (screen_frame.origin.y, screen_frame.size.height) + } else { + let y = status_bar_frame.size.height; + let height = screen_frame.size.height - (status_bar_frame.size.height - screen_frame.origin.y); + (y, height) + }; + CGRect { + origin: CGPoint { + x: screen_frame.origin.x, + y, + }, + size: CGSize { + width: screen_frame.size.width, + height, + } + } + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId { + window: id, +} + +impl WindowId { + pub unsafe fn dummy() -> Self { + WindowId { + window: std::ptr::null_mut(), + } + } +} + +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} + +impl From<&Object> for WindowId { + fn from(window: &Object) -> WindowId { + WindowId { window: window as *const _ as _ } + } +} + +impl From<&mut Object> for WindowId { + fn from(window: &mut Object) -> WindowId { + WindowId { window: window as _ } + } +} + +impl From for WindowId { + fn from(window: id) -> WindowId { + WindowId { window } + } +} + +#[derive(Clone)] +pub struct PlatformSpecificWindowBuilderAttributes { + pub root_view_class: &'static Class, + pub hidpi_factor: Option, + pub valid_orientations: ValidOrientations, +} + +impl Default for PlatformSpecificWindowBuilderAttributes { + fn default() -> PlatformSpecificWindowBuilderAttributes { + PlatformSpecificWindowBuilderAttributes { + root_view_class: class!(UIView), + hidpi_factor: None, + valid_orientations: Default::default(), + } + } +} diff --git a/src/window.rs b/src/window.rs index 0bc9c08b3b..de0eceb22b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -330,6 +330,10 @@ impl Window { /// Modifies the title of the window. /// /// This is a no-op if the window has already been closed. + /// + /// ## Platform-specific + /// + /// - Has no effect on iOS. #[inline] pub fn set_title(&self, title: &str) { self.window.set_title(title) @@ -339,8 +343,8 @@ impl Window { /// /// ## Platform-specific /// - /// - Has no effect on Android - /// + /// - **Android:** Has no effect. + /// - **iOS:** Can only be called on the main thread. #[inline] pub fn show(&self) { self.window.show() @@ -350,8 +354,8 @@ impl Window { /// /// ## Platform-specific /// - /// - Has no effect on Android - /// + /// - **Android:** Has no effect. + /// - **iOS:** Can only be called on the main thread. #[inline] pub fn hide(&self) { self.window.hide() @@ -368,6 +372,10 @@ impl Window { /// * While processing `EventsCleared`. /// * While processing a `RedrawRequested` event that was sent during `EventsCleared` or any /// directly subsequent `RedrawRequested` event. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. pub fn request_redraw(&self) { self.window.request_redraw() } @@ -383,6 +391,11 @@ impl Window { /// of the visible screen region. /// /// Returns `None` if the window no longer exists. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the + /// window in the screen space coordinate system. #[inline] pub fn get_position(&self) -> Option { self.window.get_position() @@ -392,6 +405,13 @@ impl Window { /// top-left hand corner of the desktop. /// /// The same conditions that apply to `get_position` apply to this method. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the + /// window's [safe area] in the screen space coordinate system. + /// + /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc #[inline] pub fn get_inner_position(&self) -> Option { self.window.get_inner_position() @@ -402,6 +422,11 @@ impl Window { /// See `get_position` for more information about the coordinates. /// /// This is a no-op if the window has already been closed. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Sets the top left coordinates of the + /// window in the screen space coordinate system. #[inline] pub fn set_position(&self, position: LogicalPosition) { self.window.set_position(position) @@ -414,6 +439,13 @@ impl Window { /// Converting the returned `LogicalSize` to `PhysicalSize` produces the size your framebuffer should be. /// /// Returns `None` if the window no longer exists. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the `LogicalSize` of the window's + /// [safe area] in screen space coordinates. + /// + /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc #[inline] pub fn get_inner_size(&self) -> Option { self.window.get_inner_size() @@ -425,6 +457,11 @@ impl Window { /// use `get_inner_size` instead. /// /// Returns `None` if the window no longer exists. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the `LogicalSize` of the window in + /// screen space coordinates. #[inline] pub fn get_outer_size(&self) -> Option { self.window.get_outer_size() @@ -435,18 +472,31 @@ impl Window { /// See `get_inner_size` for more information about the values. /// /// This is a no-op if the window has already been closed. + /// + /// ## Platform-specific + /// + /// - **iOS:** Unimplemented. Currently this panics, as it's not clear what `set_inner_size` + /// would mean for iOS. #[inline] pub fn set_inner_size(&self, size: LogicalSize) { self.window.set_inner_size(size) } /// Sets a minimum dimension size for the window. + /// + /// ## Platform-specific + /// + /// - **iOS:** Has no effect. #[inline] pub fn set_min_dimensions(&self, dimensions: Option) { self.window.set_min_dimensions(dimensions) } /// Sets a maximum dimension size for the window. + /// + /// ## Platform-specific + /// + /// - **iOS:** Has no effect. #[inline] pub fn set_max_dimensions(&self, dimensions: Option) { self.window.set_max_dimensions(dimensions) @@ -462,6 +512,10 @@ impl Window { /// This only has an effect on desktop platforms. /// /// Due to a bug in XFCE, this has no effect on Xfwm. + /// + /// ## Platform-specific + /// + /// - **iOS:** Has no effect. #[inline] pub fn set_resizable(&self, resizable: bool) { self.window.set_resizable(resizable) @@ -479,19 +533,31 @@ impl Window { /// /// - **X11:** This respects Xft.dpi, and can be overridden using the `WINIT_HIDPI_FACTOR` environment variable. /// - **Android:** Always returns 1.0. + /// - **iOS:** Can only be called on the main thread. Returns the underlying `UIView`'s + /// [`contentScaleFactor`]. + /// + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc #[inline] pub fn get_hidpi_factor(&self) -> f64 { self.window.get_hidpi_factor() } /// Modifies the mouse cursor of the window. - /// Has no effect on Android. + /// + /// ## Platform-specific + /// + /// - **iOS:** Has no effect. + /// - **Android:** Has no effect. #[inline] pub fn set_cursor(&self, cursor: MouseCursor) { self.window.set_cursor(cursor); } /// Changes the position of the cursor in window coordinates. + /// + /// ## Platform-specific + /// + /// - **iOS:** Always returns an `Err`. #[inline] pub fn set_cursor_position(&self, position: LogicalPosition) -> Result<(), String> { self.window.set_cursor_position(position) @@ -501,9 +567,10 @@ impl Window { /// /// ## Platform-specific /// - /// On macOS, this presently merely locks the cursor in a fixed location, which looks visually awkward. - /// - /// This has no effect on Android or iOS. + /// - **macOS:** This presently merely locks the cursor in a fixed location, which looks visually + /// awkward. + /// - **Android:** Has no effect. + /// - **iOS:** Always returns an Err. #[inline] pub fn grab_cursor(&self, grab: bool) -> Result<(), String> { self.window.grab_cursor(grab) @@ -513,42 +580,65 @@ impl Window { /// /// ## Platform-specific /// - /// On Windows and X11, the cursor is only hidden within the confines of the window. - /// - /// On macOS, the cursor is hidden as long as the window has input focus, even if the cursor is outside of the - /// window. - /// - /// This has no effect on Android or iOS. + /// - **Windows:** The cursor is only hidden within the confines of the window. + /// - **X11:** The cursor is only hidden within the confines of the window. + /// - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + /// outside of the window. + /// - **iOS:** Has no effect. + /// - **Android:** Has no effect. #[inline] pub fn hide_cursor(&self, hide: bool) { self.window.hide_cursor(hide) } - /// Sets the window to maximized or back + /// Sets the window to maximized or back. + /// + /// ## Platform-specific + /// + /// - **iOS:** Has no effect. #[inline] pub fn set_maximized(&self, maximized: bool) { self.window.set_maximized(maximized) } - /// Sets the window to fullscreen or back + /// Sets the window to fullscreen or back. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. #[inline] pub fn set_fullscreen(&self, monitor: Option) { self.window.set_fullscreen(monitor) } /// Gets the window's current fullscreen state. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. #[inline] pub fn get_fullscreen(&self) -> Option { self.window.get_fullscreen() } /// Turn window decorations on or off. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Controls whether the status bar is hidden + /// via [`setPrefersStatusBarHidden`]. + /// + /// [`setPrefersStatusBarHidden`]: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc #[inline] pub fn set_decorations(&self, decorations: bool) { self.window.set_decorations(decorations) } /// Change whether or not the window will always be on top of other windows. + /// + /// ## Platform-specific + /// + /// - **iOS:** Has no effect. #[inline] pub fn set_always_on_top(&self, always_on_top: bool) { self.window.set_always_on_top(always_on_top) @@ -568,12 +658,20 @@ impl Window { } /// Sets location of IME candidate box in client area coordinates relative to the top left. + /// + /// ## Platform-specific + /// + /// **iOS:** Has no effect. #[inline] pub fn set_ime_spot(&self, position: LogicalPosition) { self.window.set_ime_spot(position) } /// Returns the monitor on which the window currently resides + /// + /// ## Platform-specific + /// + /// **iOS:** Can only be called on the main thread. #[inline] pub fn get_current_monitor(&self) -> MonitorHandle { self.window.get_current_monitor() @@ -582,6 +680,10 @@ impl Window { /// Returns the list of all the monitors available on the system. /// /// This is the same as `EventLoop::get_available_monitors`, and is provided for convenience. + /// + /// ## Platform-specific + /// + /// **iOS:** Can only be called on the main thread. #[inline] pub fn get_available_monitors(&self) -> AvailableMonitorsIter { let data = self.window.get_available_monitors(); @@ -591,6 +693,10 @@ impl Window { /// Returns the primary monitor of the system. /// /// This is the same as `EventLoop::get_primary_monitor`, and is provided for convenience. + /// + /// ## Platform-specific + /// + /// **iOS:** Can only be called on the main thread. #[inline] pub fn get_primary_monitor(&self) -> MonitorHandle { MonitorHandle { inner: self.window.get_primary_monitor() }