Move iOS and macOS implementations into new apple module (#3756)
Move iOS and macOS implementations to a shared folder called `apple`, to allow us to reduce the code-duplication between these platforms in the future. The folder structure is now: - `src/platform_impl/apple/` - `appkit/` - `uikit/` - `example_shared_file.rs` - `mod.rs` * Add preliminary support for tvOS, watchOS and visionOS * Reduce duplication in Cargo.toml when specifying dependencies
This commit is contained in:
parent
ecb887e5c3
commit
9d5412ffe1
28 changed files with 52 additions and 59 deletions
60
src/platform_impl/apple/uikit/app_delegate.rs
Normal file
60
src/platform_impl/apple/uikit/app_delegate.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use objc2::{declare_class, mutability, ClassType, DeclaredClass};
|
||||
use objc2_foundation::{MainThreadMarker, NSObject};
|
||||
use objc2_ui_kit::UIApplication;
|
||||
|
||||
use super::app_state::{self, send_occluded_event_for_all_windows, EventWrapper};
|
||||
use crate::event::Event;
|
||||
|
||||
declare_class!(
|
||||
pub struct AppDelegate;
|
||||
|
||||
unsafe impl ClassType for AppDelegate {
|
||||
type Super = NSObject;
|
||||
type Mutability = mutability::InteriorMutable;
|
||||
const NAME: &'static str = "WinitApplicationDelegate";
|
||||
}
|
||||
|
||||
impl DeclaredClass for AppDelegate {}
|
||||
|
||||
// UIApplicationDelegate protocol
|
||||
unsafe impl AppDelegate {
|
||||
#[method(application:didFinishLaunchingWithOptions:)]
|
||||
fn did_finish_launching(&self, _application: &UIApplication, _: *mut NSObject) -> bool {
|
||||
app_state::did_finish_launching(MainThreadMarker::new().unwrap());
|
||||
true
|
||||
}
|
||||
|
||||
#[method(applicationDidBecomeActive:)]
|
||||
fn did_become_active(&self, _application: &UIApplication) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Resumed))
|
||||
}
|
||||
|
||||
#[method(applicationWillResignActive:)]
|
||||
fn will_resign_active(&self, _application: &UIApplication) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Suspended))
|
||||
}
|
||||
|
||||
#[method(applicationWillEnterForeground:)]
|
||||
fn will_enter_foreground(&self, application: &UIApplication) {
|
||||
send_occluded_event_for_all_windows(application, false);
|
||||
}
|
||||
|
||||
#[method(applicationDidEnterBackground:)]
|
||||
fn did_enter_background(&self, application: &UIApplication) {
|
||||
send_occluded_event_for_all_windows(application, true);
|
||||
}
|
||||
|
||||
#[method(applicationWillTerminate:)]
|
||||
fn will_terminate(&self, application: &UIApplication) {
|
||||
app_state::terminated(application);
|
||||
}
|
||||
|
||||
#[method(applicationDidReceiveMemoryWarning:)]
|
||||
fn did_receive_memory_warning(&self, _application: &UIApplication) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::MemoryWarning))
|
||||
}
|
||||
}
|
||||
);
|
||||
919
src/platform_impl/apple/uikit/app_state.rs
Normal file
919
src/platform_impl/apple/uikit/app_state.rs
Normal file
|
|
@ -0,0 +1,919 @@
|
|||
#![deny(unused_results)]
|
||||
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::collections::HashSet;
|
||||
use std::os::raw::c_void;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::time::Instant;
|
||||
use std::{fmt, mem, ptr};
|
||||
|
||||
use core_foundation::base::CFRelease;
|
||||
use core_foundation::date::CFAbsoluteTimeGetCurrent;
|
||||
use core_foundation::runloop::{
|
||||
kCFRunLoopCommonModes, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate,
|
||||
CFRunLoopTimerInvalidate, CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate,
|
||||
};
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::AnyObject;
|
||||
use objc2::{msg_send, sel};
|
||||
use objc2_foundation::{
|
||||
CGRect, CGSize, MainThreadMarker, NSInteger, NSObjectProtocol, NSOperatingSystemVersion,
|
||||
NSProcessInfo,
|
||||
};
|
||||
use objc2_ui_kit::{UIApplication, UICoordinateSpace, UIView, UIWindow};
|
||||
|
||||
use super::window::WinitUIWindow;
|
||||
use crate::dpi::PhysicalSize;
|
||||
use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent};
|
||||
use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow};
|
||||
use crate::window::WindowId as RootWindowId;
|
||||
|
||||
macro_rules! bug {
|
||||
($($msg:tt)*) => {
|
||||
panic!("winit iOS bug, file an issue: {}", format!($($msg)*))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! bug_assert {
|
||||
($test:expr, $($msg:tt)*) => {
|
||||
assert!($test, "winit iOS bug, file an issue: {}", format!($($msg)*))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) struct EventLoopHandler {
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) handler: Box<dyn FnMut(Event, &RootActiveEventLoop)>,
|
||||
pub(crate) event_loop: RootActiveEventLoop,
|
||||
}
|
||||
|
||||
impl fmt::Debug for EventLoopHandler {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("EventLoopHandler")
|
||||
.field("handler", &"...")
|
||||
.field("event_loop", &self.event_loop)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventLoopHandler {
|
||||
fn handle_event(&mut self, event: Event) {
|
||||
(self.handler)(event, &self.event_loop)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum EventWrapper {
|
||||
StaticEvent(Event),
|
||||
ScaleFactorChanged(ScaleFactorChanged),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScaleFactorChanged {
|
||||
pub(super) window: Retained<WinitUIWindow>,
|
||||
pub(super) suggested_size: PhysicalSize<u32>,
|
||||
pub(super) scale_factor: f64,
|
||||
}
|
||||
|
||||
enum UserCallbackTransitionResult<'a> {
|
||||
Success {
|
||||
handler: EventLoopHandler,
|
||||
active_control_flow: ControlFlow,
|
||||
processing_redraws: bool,
|
||||
},
|
||||
ReentrancyPrevented {
|
||||
queued_events: &'a mut Vec<EventWrapper>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Event {
|
||||
fn is_redraw(&self) -> bool {
|
||||
matches!(self, Event::WindowEvent { event: WindowEvent::RedrawRequested, .. })
|
||||
}
|
||||
}
|
||||
|
||||
// this is the state machine for the app lifecycle
|
||||
#[derive(Debug)]
|
||||
#[must_use = "dropping `AppStateImpl` without inspecting it is probably a bug"]
|
||||
enum AppStateImpl {
|
||||
NotLaunched {
|
||||
queued_windows: Vec<Retained<WinitUIWindow>>,
|
||||
queued_events: Vec<EventWrapper>,
|
||||
queued_gpu_redraws: HashSet<Retained<WinitUIWindow>>,
|
||||
},
|
||||
Launching {
|
||||
queued_windows: Vec<Retained<WinitUIWindow>>,
|
||||
queued_events: Vec<EventWrapper>,
|
||||
queued_handler: EventLoopHandler,
|
||||
queued_gpu_redraws: HashSet<Retained<WinitUIWindow>>,
|
||||
},
|
||||
ProcessingEvents {
|
||||
handler: EventLoopHandler,
|
||||
queued_gpu_redraws: HashSet<Retained<WinitUIWindow>>,
|
||||
active_control_flow: ControlFlow,
|
||||
},
|
||||
// special state to deal with reentrancy and prevent mutable aliasing.
|
||||
InUserCallback {
|
||||
queued_events: Vec<EventWrapper>,
|
||||
queued_gpu_redraws: HashSet<Retained<WinitUIWindow>>,
|
||||
},
|
||||
ProcessingRedraws {
|
||||
handler: EventLoopHandler,
|
||||
active_control_flow: ControlFlow,
|
||||
},
|
||||
Waiting {
|
||||
waiting_handler: EventLoopHandler,
|
||||
start: Instant,
|
||||
},
|
||||
PollFinished {
|
||||
waiting_handler: EventLoopHandler,
|
||||
},
|
||||
Terminated,
|
||||
}
|
||||
|
||||
pub(crate) struct AppState {
|
||||
// This should never be `None`, except for briefly during a state transition.
|
||||
app_state: Option<AppStateImpl>,
|
||||
control_flow: ControlFlow,
|
||||
waker: EventLoopWaker,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub(crate) fn get_mut(_mtm: MainThreadMarker) -> 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<Option<AppState>> = RefCell::new(None);
|
||||
|
||||
let mut guard = unsafe { APP_STATE.borrow_mut() };
|
||||
if guard.is_none() {
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
fn init_guard(guard: &mut RefMut<'static, Option<AppState>>) {
|
||||
let waker = EventLoopWaker::new(unsafe { CFRunLoopGetMain() });
|
||||
**guard = Some(AppState {
|
||||
app_state: Some(AppStateImpl::NotLaunched {
|
||||
queued_windows: Vec::new(),
|
||||
queued_events: Vec::new(),
|
||||
queued_gpu_redraws: HashSet::new(),
|
||||
}),
|
||||
control_flow: ControlFlow::default(),
|
||||
waker,
|
||||
});
|
||||
}
|
||||
init_guard(&mut guard);
|
||||
}
|
||||
RefMut::map(guard, |state| state.as_mut().unwrap())
|
||||
}
|
||||
|
||||
fn state(&self) -> &AppStateImpl {
|
||||
match &self.app_state {
|
||||
Some(ref state) => state,
|
||||
None => bug!("`AppState` previously failed a state transition"),
|
||||
}
|
||||
}
|
||||
|
||||
fn state_mut(&mut self) -> &mut AppStateImpl {
|
||||
match &mut self.app_state {
|
||||
Some(ref mut state) => state,
|
||||
None => bug!("`AppState` previously failed a state transition"),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_state(&mut self) -> AppStateImpl {
|
||||
match self.app_state.take() {
|
||||
Some(state) => state,
|
||||
None => bug!("`AppState` previously failed a state transition"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_state(&mut self, new_state: AppStateImpl) {
|
||||
bug_assert!(
|
||||
self.app_state.is_none(),
|
||||
"attempted to set an `AppState` without calling `take_state` first {:?}",
|
||||
self.app_state
|
||||
);
|
||||
self.app_state = Some(new_state)
|
||||
}
|
||||
|
||||
fn replace_state(&mut self, new_state: AppStateImpl) -> AppStateImpl {
|
||||
match &mut self.app_state {
|
||||
Some(ref mut state) => mem::replace(state, new_state),
|
||||
None => bug!("`AppState` previously failed a state transition"),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_launched(&self) -> bool {
|
||||
!matches!(self.state(), AppStateImpl::NotLaunched { .. } | AppStateImpl::Launching { .. })
|
||||
}
|
||||
|
||||
fn has_terminated(&self) -> bool {
|
||||
matches!(self.state(), AppStateImpl::Terminated)
|
||||
}
|
||||
|
||||
fn will_launch_transition(&mut self, queued_handler: EventLoopHandler) {
|
||||
let (queued_windows, queued_events, queued_gpu_redraws) = match self.take_state() {
|
||||
AppStateImpl::NotLaunched { queued_windows, queued_events, queued_gpu_redraws } => {
|
||||
(queued_windows, queued_events, queued_gpu_redraws)
|
||||
},
|
||||
s => bug!("unexpected state {:?}", s),
|
||||
};
|
||||
self.set_state(AppStateImpl::Launching {
|
||||
queued_windows,
|
||||
queued_events,
|
||||
queued_handler,
|
||||
queued_gpu_redraws,
|
||||
});
|
||||
}
|
||||
|
||||
fn did_finish_launching_transition(
|
||||
&mut self,
|
||||
) -> (Vec<Retained<WinitUIWindow>>, Vec<EventWrapper>) {
|
||||
let (windows, events, handler, queued_gpu_redraws) = match self.take_state() {
|
||||
AppStateImpl::Launching {
|
||||
queued_windows,
|
||||
queued_events,
|
||||
queued_handler,
|
||||
queued_gpu_redraws,
|
||||
} => (queued_windows, queued_events, queued_handler, queued_gpu_redraws),
|
||||
s => bug!("unexpected state {:?}", s),
|
||||
};
|
||||
self.set_state(AppStateImpl::ProcessingEvents {
|
||||
handler,
|
||||
active_control_flow: self.control_flow,
|
||||
queued_gpu_redraws,
|
||||
});
|
||||
(windows, events)
|
||||
}
|
||||
|
||||
fn wakeup_transition(&mut self) -> Option<EventWrapper> {
|
||||
// before `AppState::did_finish_launching` is called, pretend there is no running
|
||||
// event loop.
|
||||
if !self.has_launched() || self.has_terminated() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (handler, event) = match (self.control_flow, self.take_state()) {
|
||||
(ControlFlow::Poll, AppStateImpl::PollFinished { waiting_handler }) => {
|
||||
(waiting_handler, EventWrapper::StaticEvent(Event::NewEvents(StartCause::Poll)))
|
||||
},
|
||||
(ControlFlow::Wait, AppStateImpl::Waiting { waiting_handler, start }) => (
|
||||
waiting_handler,
|
||||
EventWrapper::StaticEvent(Event::NewEvents(StartCause::WaitCancelled {
|
||||
start,
|
||||
requested_resume: None,
|
||||
})),
|
||||
),
|
||||
(
|
||||
ControlFlow::WaitUntil(requested_resume),
|
||||
AppStateImpl::Waiting { waiting_handler, start },
|
||||
) => {
|
||||
let event = if Instant::now() >= requested_resume {
|
||||
EventWrapper::StaticEvent(Event::NewEvents(StartCause::ResumeTimeReached {
|
||||
start,
|
||||
requested_resume,
|
||||
}))
|
||||
} else {
|
||||
EventWrapper::StaticEvent(Event::NewEvents(StartCause::WaitCancelled {
|
||||
start,
|
||||
requested_resume: Some(requested_resume),
|
||||
}))
|
||||
};
|
||||
(waiting_handler, event)
|
||||
},
|
||||
s => bug!("`EventHandler` unexpectedly woke up {:?}", s),
|
||||
};
|
||||
|
||||
self.set_state(AppStateImpl::ProcessingEvents {
|
||||
handler,
|
||||
queued_gpu_redraws: Default::default(),
|
||||
active_control_flow: self.control_flow,
|
||||
});
|
||||
Some(event)
|
||||
}
|
||||
|
||||
fn try_user_callback_transition(&mut self) -> UserCallbackTransitionResult<'_> {
|
||||
// If we're not able to process an event due to recursion or `Init` not having been sent out
|
||||
// yet, then queue the events up.
|
||||
match self.state_mut() {
|
||||
&mut AppStateImpl::Launching { ref mut queued_events, .. }
|
||||
| &mut AppStateImpl::NotLaunched { ref mut queued_events, .. }
|
||||
| &mut AppStateImpl::InUserCallback { ref mut queued_events, .. } => {
|
||||
// A lifetime cast: early returns are not currently handled well with NLL, but
|
||||
// polonius handles them well. This transmute is a safe workaround.
|
||||
return unsafe {
|
||||
mem::transmute::<
|
||||
UserCallbackTransitionResult<'_>,
|
||||
UserCallbackTransitionResult<'_>,
|
||||
>(UserCallbackTransitionResult::ReentrancyPrevented {
|
||||
queued_events,
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
&mut AppStateImpl::ProcessingEvents { .. }
|
||||
| &mut AppStateImpl::ProcessingRedraws { .. } => {},
|
||||
|
||||
s @ &mut AppStateImpl::PollFinished { .. }
|
||||
| s @ &mut AppStateImpl::Waiting { .. }
|
||||
| s @ &mut AppStateImpl::Terminated => {
|
||||
bug!("unexpected attempted to process an event {:?}", s)
|
||||
},
|
||||
}
|
||||
|
||||
let (handler, queued_gpu_redraws, active_control_flow, processing_redraws) = match self
|
||||
.take_state()
|
||||
{
|
||||
AppStateImpl::Launching { .. }
|
||||
| AppStateImpl::NotLaunched { .. }
|
||||
| AppStateImpl::InUserCallback { .. } => unreachable!(),
|
||||
AppStateImpl::ProcessingEvents { handler, queued_gpu_redraws, active_control_flow } => {
|
||||
(handler, queued_gpu_redraws, active_control_flow, false)
|
||||
},
|
||||
AppStateImpl::ProcessingRedraws { handler, active_control_flow } => {
|
||||
(handler, Default::default(), active_control_flow, true)
|
||||
},
|
||||
AppStateImpl::PollFinished { .. }
|
||||
| AppStateImpl::Waiting { .. }
|
||||
| AppStateImpl::Terminated => unreachable!(),
|
||||
};
|
||||
self.set_state(AppStateImpl::InUserCallback {
|
||||
queued_events: Vec::new(),
|
||||
queued_gpu_redraws,
|
||||
});
|
||||
UserCallbackTransitionResult::Success { handler, active_control_flow, processing_redraws }
|
||||
}
|
||||
|
||||
fn main_events_cleared_transition(&mut self) -> HashSet<Retained<WinitUIWindow>> {
|
||||
let (handler, queued_gpu_redraws, active_control_flow) = match self.take_state() {
|
||||
AppStateImpl::ProcessingEvents { handler, queued_gpu_redraws, active_control_flow } => {
|
||||
(handler, queued_gpu_redraws, active_control_flow)
|
||||
},
|
||||
s => bug!("unexpected state {:?}", s),
|
||||
};
|
||||
self.set_state(AppStateImpl::ProcessingRedraws { handler, active_control_flow });
|
||||
queued_gpu_redraws
|
||||
}
|
||||
|
||||
fn events_cleared_transition(&mut self) {
|
||||
if !self.has_launched() || self.has_terminated() {
|
||||
return;
|
||||
}
|
||||
let (waiting_handler, old) = match self.take_state() {
|
||||
AppStateImpl::ProcessingRedraws { handler, active_control_flow } => {
|
||||
(handler, active_control_flow)
|
||||
},
|
||||
s => bug!("unexpected state {:?}", s),
|
||||
};
|
||||
|
||||
let new = self.control_flow;
|
||||
match (old, new) {
|
||||
(ControlFlow::Wait, ControlFlow::Wait) => {
|
||||
let start = Instant::now();
|
||||
self.set_state(AppStateImpl::Waiting { waiting_handler, start });
|
||||
},
|
||||
(ControlFlow::WaitUntil(old_instant), ControlFlow::WaitUntil(new_instant))
|
||||
if old_instant == new_instant =>
|
||||
{
|
||||
let start = Instant::now();
|
||||
self.set_state(AppStateImpl::Waiting { waiting_handler, start });
|
||||
},
|
||||
(_, ControlFlow::Wait) => {
|
||||
let start = Instant::now();
|
||||
self.set_state(AppStateImpl::Waiting { waiting_handler, start });
|
||||
self.waker.stop()
|
||||
},
|
||||
(_, ControlFlow::WaitUntil(new_instant)) => {
|
||||
let start = Instant::now();
|
||||
self.set_state(AppStateImpl::Waiting { waiting_handler, start });
|
||||
self.waker.start_at(new_instant)
|
||||
},
|
||||
// Unlike on macOS, handle Poll to Poll transition here to call the waker
|
||||
(_, ControlFlow::Poll) => {
|
||||
self.set_state(AppStateImpl::PollFinished { waiting_handler });
|
||||
self.waker.start()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn terminated_transition(&mut self) -> EventLoopHandler {
|
||||
match self.replace_state(AppStateImpl::Terminated) {
|
||||
AppStateImpl::ProcessingEvents { handler, .. } => handler,
|
||||
s => bug!("`LoopExiting` happened while not processing events {:?}", s),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_control_flow(&mut self, control_flow: ControlFlow) {
|
||||
self.control_flow = control_flow;
|
||||
}
|
||||
|
||||
pub(crate) fn control_flow(&self) -> ControlFlow {
|
||||
self.control_flow
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_key_window(mtm: MainThreadMarker, window: &Retained<WinitUIWindow>) {
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
match this.state_mut() {
|
||||
&mut AppStateImpl::NotLaunched { ref mut queued_windows, .. } => {
|
||||
return queued_windows.push(window.clone())
|
||||
},
|
||||
&mut AppStateImpl::ProcessingEvents { .. }
|
||||
| &mut AppStateImpl::InUserCallback { .. }
|
||||
| &mut AppStateImpl::ProcessingRedraws { .. } => {},
|
||||
s @ &mut AppStateImpl::Launching { .. }
|
||||
| s @ &mut AppStateImpl::Waiting { .. }
|
||||
| s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s),
|
||||
&mut AppStateImpl::Terminated => {
|
||||
panic!("Attempt to create a `Window` after the app has terminated")
|
||||
},
|
||||
}
|
||||
drop(this);
|
||||
window.makeKeyAndVisible();
|
||||
}
|
||||
|
||||
pub(crate) fn queue_gl_or_metal_redraw(mtm: MainThreadMarker, window: Retained<WinitUIWindow>) {
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
match this.state_mut() {
|
||||
&mut AppStateImpl::NotLaunched { ref mut queued_gpu_redraws, .. }
|
||||
| &mut AppStateImpl::Launching { ref mut queued_gpu_redraws, .. }
|
||||
| &mut AppStateImpl::ProcessingEvents { ref mut queued_gpu_redraws, .. }
|
||||
| &mut AppStateImpl::InUserCallback { ref mut queued_gpu_redraws, .. } => {
|
||||
let _ = queued_gpu_redraws.insert(window);
|
||||
},
|
||||
s @ &mut AppStateImpl::ProcessingRedraws { .. }
|
||||
| s @ &mut AppStateImpl::Waiting { .. }
|
||||
| s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s),
|
||||
&mut AppStateImpl::Terminated => {
|
||||
panic!("Attempt to create a `Window` after the app has terminated")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn will_launch(mtm: MainThreadMarker, queued_handler: EventLoopHandler) {
|
||||
AppState::get_mut(mtm).will_launch_transition(queued_handler)
|
||||
}
|
||||
|
||||
pub fn did_finish_launching(mtm: MainThreadMarker) {
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
let windows = match this.state_mut() {
|
||||
AppStateImpl::Launching { queued_windows, .. } => mem::take(queued_windows),
|
||||
s => bug!("unexpected state {:?}", s),
|
||||
};
|
||||
|
||||
this.waker.start();
|
||||
|
||||
// have to drop RefMut because the window setup code below can trigger new events
|
||||
drop(this);
|
||||
|
||||
for window in windows {
|
||||
// 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 initialization
|
||||
// completed. This may result in incorrect visual appearance.
|
||||
// ```
|
||||
let screen = window.screen();
|
||||
let _: () = unsafe { msg_send![&window, setScreen: ptr::null::<AnyObject>()] };
|
||||
window.setScreen(&screen);
|
||||
|
||||
let controller = window.rootViewController();
|
||||
window.setRootViewController(None);
|
||||
window.setRootViewController(controller.as_deref());
|
||||
|
||||
window.makeKeyAndVisible();
|
||||
}
|
||||
|
||||
let (windows, events) = AppState::get_mut(mtm).did_finish_launching_transition();
|
||||
|
||||
let events = std::iter::once(EventWrapper::StaticEvent(Event::NewEvents(StartCause::Init)))
|
||||
.chain(events);
|
||||
handle_nonuser_events(mtm, 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 {
|
||||
window.makeKeyAndVisible();
|
||||
}
|
||||
}
|
||||
|
||||
// AppState::did_finish_launching handles the special transition `Init`
|
||||
pub fn handle_wakeup_transition(mtm: MainThreadMarker) {
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
let wakeup_event = match this.wakeup_transition() {
|
||||
None => return,
|
||||
Some(wakeup_event) => wakeup_event,
|
||||
};
|
||||
drop(this);
|
||||
|
||||
handle_nonuser_event(mtm, wakeup_event)
|
||||
}
|
||||
|
||||
pub(crate) fn handle_nonuser_event(mtm: MainThreadMarker, event: EventWrapper) {
|
||||
handle_nonuser_events(mtm, std::iter::once(event))
|
||||
}
|
||||
|
||||
pub(crate) fn handle_nonuser_events<I: IntoIterator<Item = EventWrapper>>(
|
||||
mtm: MainThreadMarker,
|
||||
events: I,
|
||||
) {
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
if this.has_terminated() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (mut handler, active_control_flow, processing_redraws) =
|
||||
match this.try_user_callback_transition() {
|
||||
UserCallbackTransitionResult::ReentrancyPrevented { queued_events } => {
|
||||
queued_events.extend(events);
|
||||
return;
|
||||
},
|
||||
UserCallbackTransitionResult::Success {
|
||||
handler,
|
||||
active_control_flow,
|
||||
processing_redraws,
|
||||
} => (handler, active_control_flow, processing_redraws),
|
||||
};
|
||||
drop(this);
|
||||
|
||||
for wrapper in events {
|
||||
match wrapper {
|
||||
EventWrapper::StaticEvent(event) => {
|
||||
if !processing_redraws && event.is_redraw() {
|
||||
tracing::info!("processing `RedrawRequested` during the main event loop");
|
||||
} else if processing_redraws && !event.is_redraw() {
|
||||
tracing::warn!(
|
||||
"processing non `RedrawRequested` event after the main event loop: {:#?}",
|
||||
event
|
||||
);
|
||||
}
|
||||
handler.handle_event(event)
|
||||
},
|
||||
EventWrapper::ScaleFactorChanged(event) => handle_hidpi_proxy(&mut handler, event),
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
let queued_events = match this.state_mut() {
|
||||
&mut AppStateImpl::InUserCallback { ref mut queued_events, queued_gpu_redraws: _ } => {
|
||||
mem::take(queued_events)
|
||||
},
|
||||
s => bug!("unexpected state {:?}", s),
|
||||
};
|
||||
if queued_events.is_empty() {
|
||||
let queued_gpu_redraws = match this.take_state() {
|
||||
AppStateImpl::InUserCallback { queued_events: _, queued_gpu_redraws } => {
|
||||
queued_gpu_redraws
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
this.app_state = Some(if processing_redraws {
|
||||
bug_assert!(
|
||||
queued_gpu_redraws.is_empty(),
|
||||
"redraw queued while processing redraws"
|
||||
);
|
||||
AppStateImpl::ProcessingRedraws { handler, active_control_flow }
|
||||
} else {
|
||||
AppStateImpl::ProcessingEvents { handler, queued_gpu_redraws, active_control_flow }
|
||||
});
|
||||
break;
|
||||
}
|
||||
drop(this);
|
||||
|
||||
for wrapper in queued_events {
|
||||
match wrapper {
|
||||
EventWrapper::StaticEvent(event) => {
|
||||
if !processing_redraws && event.is_redraw() {
|
||||
tracing::info!("processing `RedrawRequested` during the main event loop");
|
||||
} else if processing_redraws && !event.is_redraw() {
|
||||
tracing::warn!(
|
||||
"processing non-`RedrawRequested` event after the main event loop: \
|
||||
{:#?}",
|
||||
event
|
||||
);
|
||||
}
|
||||
handler.handle_event(event)
|
||||
},
|
||||
EventWrapper::ScaleFactorChanged(event) => handle_hidpi_proxy(&mut handler, event),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_user_events(mtm: MainThreadMarker) {
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
let (mut handler, active_control_flow, processing_redraws) =
|
||||
match this.try_user_callback_transition() {
|
||||
UserCallbackTransitionResult::ReentrancyPrevented { .. } => {
|
||||
bug!("unexpected attempted to process an event")
|
||||
},
|
||||
UserCallbackTransitionResult::Success {
|
||||
handler,
|
||||
active_control_flow,
|
||||
processing_redraws,
|
||||
} => (handler, active_control_flow, processing_redraws),
|
||||
};
|
||||
if processing_redraws {
|
||||
bug!("user events attempted to be sent out while `ProcessingRedraws`");
|
||||
}
|
||||
drop(this);
|
||||
|
||||
handler.handle_event(Event::UserWakeUp);
|
||||
|
||||
loop {
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
let queued_events = match this.state_mut() {
|
||||
&mut AppStateImpl::InUserCallback { ref mut queued_events, queued_gpu_redraws: _ } => {
|
||||
mem::take(queued_events)
|
||||
},
|
||||
s => bug!("unexpected state {:?}", s),
|
||||
};
|
||||
if queued_events.is_empty() {
|
||||
let queued_gpu_redraws = match this.take_state() {
|
||||
AppStateImpl::InUserCallback { queued_events: _, queued_gpu_redraws } => {
|
||||
queued_gpu_redraws
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
this.app_state = Some(AppStateImpl::ProcessingEvents {
|
||||
handler,
|
||||
queued_gpu_redraws,
|
||||
active_control_flow,
|
||||
});
|
||||
break;
|
||||
}
|
||||
drop(this);
|
||||
|
||||
for wrapper in queued_events {
|
||||
match wrapper {
|
||||
EventWrapper::StaticEvent(event) => handler.handle_event(event),
|
||||
EventWrapper::ScaleFactorChanged(event) => handle_hidpi_proxy(&mut handler, event),
|
||||
}
|
||||
}
|
||||
|
||||
handler.handle_event(Event::UserWakeUp);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn send_occluded_event_for_all_windows(application: &UIApplication, occluded: bool) {
|
||||
let mtm = MainThreadMarker::from(application);
|
||||
|
||||
let mut events = Vec::new();
|
||||
#[allow(deprecated)]
|
||||
for window in application.windows().iter() {
|
||||
if window.is_kind_of::<WinitUIWindow>() {
|
||||
// SAFETY: We just checked that the window is a `winit` window
|
||||
let window = unsafe {
|
||||
let ptr: *const UIWindow = window;
|
||||
let ptr: *const WinitUIWindow = ptr.cast();
|
||||
&*ptr
|
||||
};
|
||||
events.push(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::Occluded(occluded),
|
||||
}));
|
||||
}
|
||||
}
|
||||
handle_nonuser_events(mtm, events);
|
||||
}
|
||||
|
||||
pub fn handle_main_events_cleared(mtm: MainThreadMarker) {
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
if !this.has_launched() || this.has_terminated() {
|
||||
return;
|
||||
}
|
||||
match this.state_mut() {
|
||||
AppStateImpl::ProcessingEvents { .. } => {},
|
||||
_ => bug!("`ProcessingRedraws` happened unexpectedly"),
|
||||
};
|
||||
drop(this);
|
||||
|
||||
handle_user_events(mtm);
|
||||
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
let redraw_events: Vec<EventWrapper> = this
|
||||
.main_events_cleared_transition()
|
||||
.into_iter()
|
||||
.map(|window| {
|
||||
EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::RedrawRequested,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
drop(this);
|
||||
|
||||
handle_nonuser_events(mtm, redraw_events);
|
||||
handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::AboutToWait));
|
||||
}
|
||||
|
||||
pub fn handle_events_cleared(mtm: MainThreadMarker) {
|
||||
AppState::get_mut(mtm).events_cleared_transition();
|
||||
}
|
||||
|
||||
pub(crate) fn terminated(application: &UIApplication) {
|
||||
let mtm = MainThreadMarker::from(application);
|
||||
|
||||
let mut events = Vec::new();
|
||||
#[allow(deprecated)]
|
||||
for window in application.windows().iter() {
|
||||
if window.is_kind_of::<WinitUIWindow>() {
|
||||
// SAFETY: We just checked that the window is a `winit` window
|
||||
let window = unsafe {
|
||||
let ptr: *const UIWindow = window;
|
||||
let ptr: *const WinitUIWindow = ptr.cast();
|
||||
&*ptr
|
||||
};
|
||||
events.push(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::Destroyed,
|
||||
}));
|
||||
}
|
||||
}
|
||||
handle_nonuser_events(mtm, events);
|
||||
|
||||
let mut this = AppState::get_mut(mtm);
|
||||
let mut handler = this.terminated_transition();
|
||||
drop(this);
|
||||
|
||||
handler.handle_event(Event::LoopExiting)
|
||||
}
|
||||
|
||||
fn handle_hidpi_proxy(handler: &mut EventLoopHandler, event: ScaleFactorChanged) {
|
||||
let ScaleFactorChanged { suggested_size, scale_factor, window } = event;
|
||||
let new_inner_size = Arc::new(Mutex::new(suggested_size));
|
||||
let event = Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::ScaleFactorChanged {
|
||||
scale_factor,
|
||||
inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)),
|
||||
},
|
||||
};
|
||||
handler.handle_event(event);
|
||||
let (view, screen_frame) = get_view_and_screen_frame(&window);
|
||||
let physical_size = *new_inner_size.lock().unwrap();
|
||||
drop(new_inner_size);
|
||||
let logical_size = physical_size.to_logical(scale_factor);
|
||||
let size = CGSize::new(logical_size.width, logical_size.height);
|
||||
let new_frame: CGRect = CGRect::new(screen_frame.origin, size);
|
||||
view.setFrame(new_frame);
|
||||
}
|
||||
|
||||
fn get_view_and_screen_frame(window: &WinitUIWindow) -> (Retained<UIView>, CGRect) {
|
||||
let view_controller = window.rootViewController().unwrap();
|
||||
let view = view_controller.view().unwrap();
|
||||
let bounds = window.bounds();
|
||||
let screen = window.screen();
|
||||
let screen_space = screen.coordinateSpace();
|
||||
let screen_frame = window.convertRect_toCoordinateSpace(bounds, &screen_space);
|
||||
(view, screen_frame)
|
||||
}
|
||||
|
||||
struct EventLoopWaker {
|
||||
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 "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {}
|
||||
unsafe {
|
||||
// Create a timer with a 0.1µs 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 immediately in did_finish_launching
|
||||
let timer = CFRunLoopTimerCreate(
|
||||
ptr::null_mut(),
|
||||
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, f64::MAX) }
|
||||
}
|
||||
|
||||
fn start(&mut self) {
|
||||
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! os_capabilities {
|
||||
(
|
||||
$(
|
||||
$(#[$attr:meta])*
|
||||
$error_name:ident: $objc_call:literal,
|
||||
$name:ident: $major:literal-$minor:literal
|
||||
),*
|
||||
$(,)*
|
||||
) => {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OSCapabilities {
|
||||
$(
|
||||
pub $name: bool,
|
||||
)*
|
||||
|
||||
os_version: NSOperatingSystemVersion,
|
||||
}
|
||||
|
||||
impl OSCapabilities {
|
||||
fn from_os_version(os_version: NSOperatingSystemVersion) -> Self {
|
||||
$(let $name = meets_requirements(os_version, $major, $minor);)*
|
||||
Self { $($name,)* os_version, }
|
||||
}
|
||||
}
|
||||
|
||||
impl OSCapabilities {$(
|
||||
$(#[$attr])*
|
||||
pub fn $error_name(&self, extra_msg: &str) {
|
||||
tracing::warn!(
|
||||
concat!("`", $objc_call, "` requires iOS {}.{}+. This device is running iOS {}.{}.{}. {}"),
|
||||
$major, $minor, self.os_version.majorVersion, self.os_version.minorVersion, self.os_version.patchVersion,
|
||||
extra_msg
|
||||
)
|
||||
}
|
||||
)*}
|
||||
};
|
||||
}
|
||||
|
||||
os_capabilities! {
|
||||
/// <https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc>
|
||||
#[allow(unused)] // error message unused
|
||||
safe_area_err_msg: "-[UIView safeAreaInsets]",
|
||||
safe_area: 11-0,
|
||||
/// <https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc>
|
||||
home_indicator_hidden_err_msg: "-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]",
|
||||
home_indicator_hidden: 11-0,
|
||||
/// <https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc>
|
||||
defer_system_gestures_err_msg: "-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystem]",
|
||||
defer_system_gestures: 11-0,
|
||||
/// <https://developer.apple.com/documentation/uikit/uiscreen/2806814-maximumframespersecond?language=objc>
|
||||
maximum_frames_per_second_err_msg: "-[UIScreen maximumFramesPerSecond]",
|
||||
maximum_frames_per_second: 10-3,
|
||||
/// <https://developer.apple.com/documentation/uikit/uitouch/1618110-force?language=objc>
|
||||
#[allow(unused)] // error message unused
|
||||
force_touch_err_msg: "-[UITouch force]",
|
||||
force_touch: 9-0,
|
||||
}
|
||||
|
||||
fn meets_requirements(
|
||||
version: NSOperatingSystemVersion,
|
||||
required_major: NSInteger,
|
||||
required_minor: NSInteger,
|
||||
) -> bool {
|
||||
(version.majorVersion, version.minorVersion) >= (required_major, required_minor)
|
||||
}
|
||||
|
||||
fn get_version() -> NSOperatingSystemVersion {
|
||||
let process_info = NSProcessInfo::processInfo();
|
||||
let atleast_ios_8 = process_info.respondsToSelector(sel!(operatingSystemVersion));
|
||||
// Winit requires atleast iOS 8 because no one has put the time into supporting earlier os
|
||||
// versions. Older iOS versions are increasingly difficult to test. For example, Xcode 11 does
|
||||
// not support debugging on devices with an iOS version of less than 8. Another example, in
|
||||
// order to use an iOS simulator older than iOS 8, you must download an older version of Xcode
|
||||
// (<9), and at least Xcode 7 has been tested to not even run on macOS 10.15 - Xcode 8 might?
|
||||
//
|
||||
// The minimum required iOS version is likely to grow in the future.
|
||||
assert!(atleast_ios_8, "`winit` requires iOS version 8 or greater");
|
||||
process_info.operatingSystemVersion()
|
||||
}
|
||||
|
||||
pub fn os_capabilities() -> OSCapabilities {
|
||||
// Cache the version lookup for efficiency
|
||||
static OS_CAPABILITIES: OnceLock<OSCapabilities> = OnceLock::new();
|
||||
OS_CAPABILITIES.get_or_init(|| OSCapabilities::from_os_version(get_version())).clone()
|
||||
}
|
||||
391
src/platform_impl/apple/uikit/event_loop.rs
Normal file
391
src/platform_impl/apple/uikit/event_loop.rs
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::marker::PhantomData;
|
||||
use std::ptr::{self, NonNull};
|
||||
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use core_foundation::base::{CFIndex, CFRelease};
|
||||
use core_foundation::runloop::{
|
||||
kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, kCFRunLoopDefaultMode,
|
||||
kCFRunLoopExit, CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddSource, CFRunLoopGetMain,
|
||||
CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopSourceContext, CFRunLoopSourceCreate,
|
||||
CFRunLoopSourceInvalidate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp,
|
||||
};
|
||||
use objc2::rc::Retained;
|
||||
use objc2::{msg_send_id, ClassType};
|
||||
use objc2_foundation::{MainThreadMarker, NSString};
|
||||
use objc2_ui_kit::{UIApplication, UIApplicationMain, UIDevice, UIScreen, UIUserInterfaceIdiom};
|
||||
|
||||
use super::app_state::EventLoopHandler;
|
||||
use crate::application::ApplicationHandler;
|
||||
use crate::error::EventLoopError;
|
||||
use crate::event::Event;
|
||||
use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents};
|
||||
use crate::platform::ios::Idiom;
|
||||
use crate::window::{CustomCursor, CustomCursorSource};
|
||||
|
||||
use super::app_delegate::AppDelegate;
|
||||
use super::app_state::AppState;
|
||||
use super::{app_state, monitor, MonitorHandle};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ActiveEventLoop {
|
||||
pub(super) mtm: MainThreadMarker,
|
||||
}
|
||||
|
||||
impl ActiveEventLoop {
|
||||
pub fn create_custom_cursor(&self, source: CustomCursorSource) -> CustomCursor {
|
||||
let _ = source.inner;
|
||||
CustomCursor { inner: super::PlatformCustomCursor }
|
||||
}
|
||||
|
||||
pub fn available_monitors(&self) -> VecDeque<MonitorHandle> {
|
||||
monitor::uiscreens(self.mtm)
|
||||
}
|
||||
|
||||
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
|
||||
#[allow(deprecated)]
|
||||
Some(MonitorHandle::new(UIScreen::mainScreen(self.mtm)))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn listen_device_events(&self, _allowed: DeviceEvents) {}
|
||||
|
||||
#[cfg(feature = "rwh_05")]
|
||||
#[inline]
|
||||
pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle {
|
||||
rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty())
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_06")]
|
||||
#[inline]
|
||||
pub fn raw_display_handle_rwh_06(
|
||||
&self,
|
||||
) -> Result<rwh_06::RawDisplayHandle, rwh_06::HandleError> {
|
||||
Ok(rwh_06::RawDisplayHandle::UiKit(rwh_06::UiKitDisplayHandle::new()))
|
||||
}
|
||||
|
||||
pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) {
|
||||
AppState::get_mut(self.mtm).set_control_flow(control_flow)
|
||||
}
|
||||
|
||||
pub(crate) fn control_flow(&self) -> ControlFlow {
|
||||
AppState::get_mut(self.mtm).control_flow()
|
||||
}
|
||||
|
||||
pub(crate) fn exit(&self) {
|
||||
// https://developer.apple.com/library/archive/qa/qa1561/_index.html
|
||||
// it is not possible to quit an iOS app gracefully and programmatically
|
||||
tracing::warn!("`ControlFlow::Exit` ignored on iOS");
|
||||
}
|
||||
|
||||
pub(crate) fn exiting(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle {
|
||||
OwnedDisplayHandle
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct OwnedDisplayHandle;
|
||||
|
||||
impl OwnedDisplayHandle {
|
||||
#[cfg(feature = "rwh_05")]
|
||||
#[inline]
|
||||
pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle {
|
||||
rwh_05::UiKitDisplayHandle::empty().into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_06")]
|
||||
#[inline]
|
||||
pub fn raw_display_handle_rwh_06(
|
||||
&self,
|
||||
) -> Result<rwh_06::RawDisplayHandle, rwh_06::HandleError> {
|
||||
Ok(rwh_06::UiKitDisplayHandle::new().into())
|
||||
}
|
||||
}
|
||||
|
||||
fn map_user_event<A: ApplicationHandler>(
|
||||
app: &mut A,
|
||||
proxy_wake_up: Arc<AtomicBool>,
|
||||
) -> impl FnMut(Event, &RootActiveEventLoop) + '_ {
|
||||
move |event, window_target| match event {
|
||||
Event::NewEvents(cause) => app.new_events(window_target, cause),
|
||||
Event::WindowEvent { window_id, event } => {
|
||||
app.window_event(window_target, window_id, event)
|
||||
},
|
||||
Event::DeviceEvent { device_id, event } => {
|
||||
app.device_event(window_target, device_id, event)
|
||||
},
|
||||
Event::UserWakeUp => {
|
||||
if proxy_wake_up.swap(false, AtomicOrdering::Relaxed) {
|
||||
app.proxy_wake_up(window_target);
|
||||
}
|
||||
},
|
||||
Event::Suspended => app.suspended(window_target),
|
||||
Event::Resumed => app.resumed(window_target),
|
||||
Event::AboutToWait => app.about_to_wait(window_target),
|
||||
Event::LoopExiting => app.exiting(window_target),
|
||||
Event::MemoryWarning => app.memory_warning(window_target),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventLoop {
|
||||
mtm: MainThreadMarker,
|
||||
proxy_wake_up: Arc<AtomicBool>,
|
||||
window_target: RootActiveEventLoop,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct PlatformSpecificEventLoopAttributes {}
|
||||
|
||||
impl EventLoop {
|
||||
pub(crate) fn new(
|
||||
_: &PlatformSpecificEventLoopAttributes,
|
||||
) -> Result<EventLoop, EventLoopError> {
|
||||
let mtm = MainThreadMarker::new()
|
||||
.expect("On iOS, `EventLoop` must be created on the main thread");
|
||||
|
||||
static mut SINGLETON_INIT: bool = false;
|
||||
unsafe {
|
||||
assert!(
|
||||
!SINGLETON_INIT,
|
||||
"Only one `EventLoop` is supported on iOS. `EventLoopProxy` might be helpful"
|
||||
);
|
||||
SINGLETON_INIT = true;
|
||||
}
|
||||
|
||||
// this line sets up the main run loop before `UIApplicationMain`
|
||||
setup_control_flow_observers();
|
||||
|
||||
let proxy_wake_up = Arc::new(AtomicBool::new(false));
|
||||
|
||||
Ok(EventLoop {
|
||||
mtm,
|
||||
proxy_wake_up,
|
||||
window_target: RootActiveEventLoop { p: ActiveEventLoop { mtm }, _marker: PhantomData },
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_app<A: ApplicationHandler>(self, app: &mut A) -> ! {
|
||||
let application: Option<Retained<UIApplication>> =
|
||||
unsafe { msg_send_id![UIApplication::class(), sharedApplication] };
|
||||
assert!(
|
||||
application.is_none(),
|
||||
"\
|
||||
`EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\nNote: \
|
||||
`EventLoop::run_app` calls `UIApplicationMain` on iOS",
|
||||
);
|
||||
|
||||
let handler = map_user_event(app, self.proxy_wake_up.clone());
|
||||
|
||||
let handler = unsafe {
|
||||
std::mem::transmute::<
|
||||
Box<dyn FnMut(Event, &RootActiveEventLoop)>,
|
||||
Box<dyn FnMut(Event, &RootActiveEventLoop)>,
|
||||
>(Box::new(handler))
|
||||
};
|
||||
|
||||
let handler = EventLoopHandler { handler, event_loop: self.window_target };
|
||||
|
||||
app_state::will_launch(self.mtm, handler);
|
||||
|
||||
// Ensure application delegate is initialized
|
||||
let _ = AppDelegate::class();
|
||||
|
||||
extern "C" {
|
||||
// These functions are in crt_externs.h.
|
||||
fn _NSGetArgc() -> *mut c_int;
|
||||
fn _NSGetArgv() -> *mut *mut *mut c_char;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
UIApplicationMain(
|
||||
*_NSGetArgc(),
|
||||
NonNull::new(*_NSGetArgv()).unwrap(),
|
||||
None,
|
||||
Some(&NSString::from_str(AppDelegate::NAME)),
|
||||
)
|
||||
};
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn create_proxy(&self) -> EventLoopProxy {
|
||||
EventLoopProxy::new(self.proxy_wake_up.clone())
|
||||
}
|
||||
|
||||
pub fn window_target(&self) -> &RootActiveEventLoop {
|
||||
&self.window_target
|
||||
}
|
||||
}
|
||||
|
||||
// EventLoopExtIOS
|
||||
impl EventLoop {
|
||||
pub fn idiom(&self) -> Idiom {
|
||||
match UIDevice::currentDevice(self.mtm).userInterfaceIdiom() {
|
||||
UIUserInterfaceIdiom::Unspecified => Idiom::Unspecified,
|
||||
UIUserInterfaceIdiom::Phone => Idiom::Phone,
|
||||
UIUserInterfaceIdiom::Pad => Idiom::Pad,
|
||||
UIUserInterfaceIdiom::TV => Idiom::TV,
|
||||
UIUserInterfaceIdiom::CarPlay => Idiom::CarPlay,
|
||||
_ => Idiom::Unspecified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventLoopProxy {
|
||||
proxy_wake_up: Arc<AtomicBool>,
|
||||
source: CFRunLoopSourceRef,
|
||||
}
|
||||
|
||||
unsafe impl Send for EventLoopProxy {}
|
||||
unsafe impl Sync for EventLoopProxy {}
|
||||
|
||||
impl Clone for EventLoopProxy {
|
||||
fn clone(&self) -> EventLoopProxy {
|
||||
EventLoopProxy::new(self.proxy_wake_up.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EventLoopProxy {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
CFRunLoopSourceInvalidate(self.source);
|
||||
CFRelease(self.source as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventLoopProxy {
|
||||
fn new(proxy_wake_up: Arc<AtomicBool>) -> EventLoopProxy {
|
||||
unsafe {
|
||||
// just wake up the eventloop
|
||||
extern "C" fn event_loop_proxy_handler(_: *const 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();
|
||||
let mut context = CFRunLoopSourceContext {
|
||||
version: 0,
|
||||
info: ptr::null_mut(),
|
||||
retain: None,
|
||||
release: None,
|
||||
copyDescription: None,
|
||||
equal: None,
|
||||
hash: None,
|
||||
schedule: None,
|
||||
cancel: None,
|
||||
perform: event_loop_proxy_handler,
|
||||
};
|
||||
let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context);
|
||||
CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes);
|
||||
CFRunLoopWakeUp(rl);
|
||||
|
||||
EventLoopProxy { proxy_wake_up, source }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wake_up(&self) {
|
||||
self.proxy_wake_up.store(true, AtomicOrdering::Relaxed);
|
||||
unsafe {
|
||||
// let the main thread know there's a new event
|
||||
CFRunLoopSourceSignal(self.source);
|
||||
let rl = CFRunLoopGetMain();
|
||||
CFRunLoopWakeUp(rl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_control_flow_observers() {
|
||||
unsafe {
|
||||
// begin is queued with the highest priority to ensure it is processed before other
|
||||
// observers
|
||||
extern "C" fn control_flow_begin_handler(
|
||||
_: CFRunLoopObserverRef,
|
||||
activity: CFRunLoopActivity,
|
||||
_: *mut c_void,
|
||||
) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
#[allow(non_upper_case_globals)]
|
||||
match activity {
|
||||
kCFRunLoopAfterWaiting => app_state::handle_wakeup_transition(mtm),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// Core Animation registers its `CFRunLoopObserver` that performs drawing operations in
|
||||
// `CA::Transaction::ensure_implicit` with a priority of `0x1e8480`. We set the main_end
|
||||
// priority to be 0, in order to send AboutToWait before RedrawRequested. This value was
|
||||
// chosen conservatively to guard against apple using different priorities for their redraw
|
||||
// observers in different OS's or on different devices. If it so happens that it's too
|
||||
// conservative, the main symptom would be non-redraw events coming in after `AboutToWait`.
|
||||
//
|
||||
// The value of `0x1e8480` was determined by inspecting stack traces and the associated
|
||||
// registers for every `CFRunLoopAddObserver` call on an iPad Air 2 running iOS 11.4.
|
||||
//
|
||||
// Also tested to be `0x1e8480` on iPhone 8, iOS 13 beta 4.
|
||||
extern "C" fn control_flow_main_end_handler(
|
||||
_: CFRunLoopObserverRef,
|
||||
activity: CFRunLoopActivity,
|
||||
_: *mut c_void,
|
||||
) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
#[allow(non_upper_case_globals)]
|
||||
match activity {
|
||||
kCFRunLoopBeforeWaiting => app_state::handle_main_events_cleared(mtm),
|
||||
kCFRunLoopExit => {}, // may happen when running on macOS
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// end is queued with the lowest priority to ensure it is processed after other observers
|
||||
extern "C" fn control_flow_end_handler(
|
||||
_: CFRunLoopObserverRef,
|
||||
activity: CFRunLoopActivity,
|
||||
_: *mut c_void,
|
||||
) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
#[allow(non_upper_case_globals)]
|
||||
match activity {
|
||||
kCFRunLoopBeforeWaiting => app_state::handle_events_cleared(mtm),
|
||||
kCFRunLoopExit => {}, // may happen when running on macOS
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
let main_loop = CFRunLoopGetMain();
|
||||
|
||||
let begin_observer = CFRunLoopObserverCreate(
|
||||
ptr::null_mut(),
|
||||
kCFRunLoopAfterWaiting,
|
||||
1, // repeat = true
|
||||
CFIndex::MIN,
|
||||
control_flow_begin_handler,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
CFRunLoopAddObserver(main_loop, begin_observer, kCFRunLoopDefaultMode);
|
||||
|
||||
let main_end_observer = CFRunLoopObserverCreate(
|
||||
ptr::null_mut(),
|
||||
kCFRunLoopExit | kCFRunLoopBeforeWaiting,
|
||||
1, // repeat = true
|
||||
0, // see comment on `control_flow_main_end_handler`
|
||||
control_flow_main_end_handler,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
CFRunLoopAddObserver(main_loop, main_end_observer, kCFRunLoopDefaultMode);
|
||||
|
||||
let end_observer = CFRunLoopObserverCreate(
|
||||
ptr::null_mut(),
|
||||
kCFRunLoopExit | kCFRunLoopBeforeWaiting,
|
||||
1, // repeat = true
|
||||
CFIndex::MAX,
|
||||
control_flow_end_handler,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
CFRunLoopAddObserver(main_loop, end_observer, kCFRunLoopDefaultMode);
|
||||
}
|
||||
}
|
||||
52
src/platform_impl/apple/uikit/mod.rs
Normal file
52
src/platform_impl/apple/uikit/mod.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#![allow(clippy::let_unit_value)]
|
||||
|
||||
mod app_delegate;
|
||||
mod app_state;
|
||||
mod event_loop;
|
||||
mod monitor;
|
||||
mod view;
|
||||
mod view_controller;
|
||||
mod window;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::event::DeviceId as RootDeviceId;
|
||||
|
||||
pub(crate) use self::event_loop::{
|
||||
ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle,
|
||||
PlatformSpecificEventLoopAttributes,
|
||||
};
|
||||
pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle};
|
||||
pub(crate) use self::window::{PlatformSpecificWindowAttributes, Window, WindowId};
|
||||
pub(crate) use crate::cursor::{
|
||||
NoCustomCursor as PlatformCustomCursor, NoCustomCursor as PlatformCustomCursorSource,
|
||||
};
|
||||
pub(crate) use crate::icon::NoIcon as PlatformIcon;
|
||||
pub(crate) use crate::platform_impl::Fullscreen;
|
||||
|
||||
/// There is no way to detect which device that performed a certain event in
|
||||
/// UIKit (i.e. you can't differentiate between different external keyboards,
|
||||
/// or whether it was the main touchscreen, assistive technologies, or some
|
||||
/// other pointer device that caused a touch event).
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DeviceId;
|
||||
|
||||
impl DeviceId {
|
||||
pub const unsafe fn dummy() -> Self {
|
||||
DeviceId
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct KeyEventExtra {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OsError {}
|
||||
|
||||
impl fmt::Display for OsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "os error")
|
||||
}
|
||||
}
|
||||
244
src/platform_impl/apple/uikit/monitor.rs
Normal file
244
src/platform_impl/apple/uikit/monitor.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
#![allow(clippy::unnecessary_cast)]
|
||||
|
||||
use std::collections::{BTreeSet, VecDeque};
|
||||
use std::{fmt, hash, ptr};
|
||||
|
||||
use objc2::mutability::IsRetainable;
|
||||
use objc2::rc::Retained;
|
||||
use objc2::Message;
|
||||
use objc2_foundation::{run_on_main, MainThreadBound, MainThreadMarker, NSInteger};
|
||||
use objc2_ui_kit::{UIScreen, UIScreenMode};
|
||||
|
||||
use super::app_state;
|
||||
use crate::dpi::{PhysicalPosition, PhysicalSize};
|
||||
use crate::monitor::VideoModeHandle as RootVideoModeHandle;
|
||||
|
||||
// Workaround for `MainThreadBound` implementing almost no traits
|
||||
#[derive(Debug)]
|
||||
struct MainThreadBoundDelegateImpls<T>(MainThreadBound<Retained<T>>);
|
||||
|
||||
impl<T: IsRetainable + Message> Clone for MainThreadBoundDelegateImpls<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(run_on_main(|mtm| MainThreadBound::new(Retained::clone(self.0.get(mtm)), mtm)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IsRetainable + Message> hash::Hash for MainThreadBoundDelegateImpls<T> {
|
||||
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||
// SAFETY: Marker only used to get the pointer
|
||||
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
||||
Retained::as_ptr(self.0.get(mtm)).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IsRetainable + Message> PartialEq for MainThreadBoundDelegateImpls<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// SAFETY: Marker only used to get the pointer
|
||||
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
||||
Retained::as_ptr(self.0.get(mtm)) == Retained::as_ptr(other.0.get(mtm))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IsRetainable + Message> Eq for MainThreadBoundDelegateImpls<T> {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub struct VideoModeHandle {
|
||||
pub(crate) size: (u32, u32),
|
||||
pub(crate) bit_depth: u16,
|
||||
pub(crate) refresh_rate_millihertz: u32,
|
||||
screen_mode: MainThreadBoundDelegateImpls<UIScreenMode>,
|
||||
pub(crate) monitor: MonitorHandle,
|
||||
}
|
||||
|
||||
impl VideoModeHandle {
|
||||
fn new(
|
||||
uiscreen: Retained<UIScreen>,
|
||||
screen_mode: Retained<UIScreenMode>,
|
||||
mtm: MainThreadMarker,
|
||||
) -> VideoModeHandle {
|
||||
let refresh_rate_millihertz = refresh_rate_millihertz(&uiscreen);
|
||||
let size = screen_mode.size();
|
||||
VideoModeHandle {
|
||||
size: (size.width as u32, size.height as u32),
|
||||
bit_depth: 32,
|
||||
refresh_rate_millihertz,
|
||||
screen_mode: MainThreadBoundDelegateImpls(MainThreadBound::new(screen_mode, mtm)),
|
||||
monitor: MonitorHandle::new(uiscreen),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> PhysicalSize<u32> {
|
||||
self.size.into()
|
||||
}
|
||||
|
||||
pub fn bit_depth(&self) -> u16 {
|
||||
self.bit_depth
|
||||
}
|
||||
|
||||
pub fn refresh_rate_millihertz(&self) -> u32 {
|
||||
self.refresh_rate_millihertz
|
||||
}
|
||||
|
||||
pub fn monitor(&self) -> MonitorHandle {
|
||||
self.monitor.clone()
|
||||
}
|
||||
|
||||
pub(super) fn screen_mode(&self, mtm: MainThreadMarker) -> &Retained<UIScreenMode> {
|
||||
self.screen_mode.0.get(mtm)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MonitorHandle {
|
||||
ui_screen: MainThreadBound<Retained<UIScreen>>,
|
||||
}
|
||||
|
||||
impl Clone for MonitorHandle {
|
||||
fn clone(&self) -> Self {
|
||||
run_on_main(|mtm| Self {
|
||||
ui_screen: MainThreadBound::new(self.ui_screen.get(mtm).clone(), mtm),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl hash::Hash for MonitorHandle {
|
||||
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||
(self as *const Self).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MonitorHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
ptr::eq(self, other)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MonitorHandle {}
|
||||
|
||||
impl PartialOrd for MonitorHandle {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for MonitorHandle {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
// TODO: Make a better ordering
|
||||
(self as *const Self).cmp(&(other as *const Self))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for MonitorHandle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("MonitorHandle")
|
||||
.field("name", &self.name())
|
||||
.field("size", &self.size())
|
||||
.field("position", &self.position())
|
||||
.field("scale_factor", &self.scale_factor())
|
||||
.field("refresh_rate_millihertz", &self.refresh_rate_millihertz())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl MonitorHandle {
|
||||
pub(crate) fn new(ui_screen: Retained<UIScreen>) -> Self {
|
||||
// Holding `Retained<UIScreen>` implies we're on the main thread.
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
Self { ui_screen: MainThreadBound::new(ui_screen, mtm) }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<String> {
|
||||
run_on_main(|mtm| {
|
||||
#[allow(deprecated)]
|
||||
let main = UIScreen::mainScreen(mtm);
|
||||
if *self.ui_screen(mtm) == main {
|
||||
Some("Primary".to_string())
|
||||
} else if Some(self.ui_screen(mtm)) == main.mirroredScreen().as_ref() {
|
||||
Some("Mirrored".to_string())
|
||||
} else {
|
||||
#[allow(deprecated)]
|
||||
UIScreen::screens(mtm)
|
||||
.iter()
|
||||
.position(|rhs| rhs == &**self.ui_screen(mtm))
|
||||
.map(|idx| idx.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn size(&self) -> PhysicalSize<u32> {
|
||||
let bounds = self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeBounds());
|
||||
PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32)
|
||||
}
|
||||
|
||||
pub fn position(&self) -> PhysicalPosition<i32> {
|
||||
let bounds = self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeBounds());
|
||||
(bounds.origin.x as f64, bounds.origin.y as f64).into()
|
||||
}
|
||||
|
||||
pub fn scale_factor(&self) -> f64 {
|
||||
self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeScale()) as f64
|
||||
}
|
||||
|
||||
pub fn refresh_rate_millihertz(&self) -> Option<u32> {
|
||||
Some(self.ui_screen.get_on_main(|ui_screen| refresh_rate_millihertz(ui_screen)))
|
||||
}
|
||||
|
||||
pub fn video_modes(&self) -> impl Iterator<Item = VideoModeHandle> {
|
||||
run_on_main(|mtm| {
|
||||
let ui_screen = self.ui_screen(mtm);
|
||||
// Use Ord impl of RootVideoModeHandle
|
||||
|
||||
let modes: BTreeSet<_> = ui_screen
|
||||
.availableModes()
|
||||
.into_iter()
|
||||
.map(|mode| RootVideoModeHandle {
|
||||
video_mode: VideoModeHandle::new(ui_screen.clone(), mode, mtm),
|
||||
})
|
||||
.collect();
|
||||
|
||||
modes.into_iter().map(|mode| mode.video_mode)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ui_screen(&self, mtm: MainThreadMarker) -> &Retained<UIScreen> {
|
||||
self.ui_screen.get(mtm)
|
||||
}
|
||||
|
||||
pub fn preferred_video_mode(&self) -> VideoModeHandle {
|
||||
run_on_main(|mtm| {
|
||||
VideoModeHandle::new(
|
||||
self.ui_screen(mtm).clone(),
|
||||
self.ui_screen(mtm).preferredMode().unwrap(),
|
||||
mtm,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_rate_millihertz(uiscreen: &UIScreen) -> u32 {
|
||||
let refresh_rate_millihertz: NSInteger = {
|
||||
let os_capabilities = app_state::os_capabilities();
|
||||
if os_capabilities.maximum_frames_per_second {
|
||||
uiscreen.maximumFramesPerSecond()
|
||||
} else {
|
||||
// https://developer.apple.com/library/archive/technotes/tn2460/_index.html
|
||||
// https://en.wikipedia.org/wiki/IPad_Pro#Model_comparison
|
||||
//
|
||||
// All iOS devices support 60 fps, and on devices where `maximumFramesPerSecond` is not
|
||||
// supported, they are all guaranteed to have 60hz refresh rates. This does not
|
||||
// correctly handle external displays. ProMotion displays support 120fps, but they were
|
||||
// introduced at the same time as the `maximumFramesPerSecond` API.
|
||||
//
|
||||
// FIXME: earlier OSs could calculate the refresh rate using
|
||||
// `-[CADisplayLink duration]`.
|
||||
os_capabilities.maximum_frames_per_second_err_msg("defaulting to 60 fps");
|
||||
60
|
||||
}
|
||||
};
|
||||
|
||||
refresh_rate_millihertz as u32 * 1000
|
||||
}
|
||||
|
||||
pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque<MonitorHandle> {
|
||||
#[allow(deprecated)]
|
||||
UIScreen::screens(mtm).into_iter().map(MonitorHandle::new).collect()
|
||||
}
|
||||
515
src/platform_impl/apple/uikit/view.rs
Normal file
515
src/platform_impl/apple/uikit/view.rs
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
#![allow(clippy::unnecessary_cast)]
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::{NSObjectProtocol, ProtocolObject};
|
||||
use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass};
|
||||
use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet};
|
||||
use objc2_ui_kit::{
|
||||
UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer,
|
||||
UIGestureRecognizerDelegate, UIGestureRecognizerState, UIPanGestureRecognizer,
|
||||
UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer,
|
||||
UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView,
|
||||
};
|
||||
|
||||
use super::app_state::{self, EventWrapper};
|
||||
use super::window::WinitUIWindow;
|
||||
use super::DEVICE_ID;
|
||||
use crate::dpi::PhysicalPosition;
|
||||
use crate::event::{Event, Force, Touch, TouchPhase, WindowEvent};
|
||||
use crate::window::{WindowAttributes, WindowId as RootWindowId};
|
||||
|
||||
pub struct WinitViewState {
|
||||
pinch_gesture_recognizer: RefCell<Option<Retained<UIPinchGestureRecognizer>>>,
|
||||
doubletap_gesture_recognizer: RefCell<Option<Retained<UITapGestureRecognizer>>>,
|
||||
rotation_gesture_recognizer: RefCell<Option<Retained<UIRotationGestureRecognizer>>>,
|
||||
pan_gesture_recognizer: RefCell<Option<Retained<UIPanGestureRecognizer>>>,
|
||||
|
||||
// for iOS delta references the start of the Gesture
|
||||
rotation_last_delta: Cell<CGFloat>,
|
||||
pinch_last_delta: Cell<CGFloat>,
|
||||
pan_last_delta: Cell<CGPoint>,
|
||||
}
|
||||
|
||||
declare_class!(
|
||||
pub(crate) struct WinitView;
|
||||
|
||||
unsafe impl ClassType for WinitView {
|
||||
#[inherits(UIResponder, NSObject)]
|
||||
type Super = UIView;
|
||||
type Mutability = mutability::MainThreadOnly;
|
||||
const NAME: &'static str = "WinitUIView";
|
||||
}
|
||||
|
||||
impl DeclaredClass for WinitView {
|
||||
type Ivars = WinitViewState;
|
||||
}
|
||||
|
||||
unsafe impl WinitView {
|
||||
#[method(drawRect:)]
|
||||
fn draw_rect(&self, rect: CGRect) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
let window = self.window().unwrap();
|
||||
app_state::handle_nonuser_event(
|
||||
mtm,
|
||||
EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::RedrawRequested,
|
||||
}),
|
||||
);
|
||||
let _: () = unsafe { msg_send![super(self), drawRect: rect] };
|
||||
}
|
||||
|
||||
#[method(layoutSubviews)]
|
||||
fn layout_subviews(&self) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
let _: () = unsafe { msg_send![super(self), layoutSubviews] };
|
||||
|
||||
let window = self.window().unwrap();
|
||||
let window_bounds = window.bounds();
|
||||
let screen = window.screen();
|
||||
let screen_space = screen.coordinateSpace();
|
||||
let screen_frame = self.convertRect_toCoordinateSpace(window_bounds, &screen_space);
|
||||
let scale_factor = screen.scale();
|
||||
let size = crate::dpi::LogicalSize {
|
||||
width: screen_frame.size.width as f64,
|
||||
height: screen_frame.size.height as f64,
|
||||
}
|
||||
.to_physical(scale_factor as f64);
|
||||
|
||||
// If the app is started in landscape, the view frame and window bounds can be mismatched.
|
||||
// The view frame will be in portrait and the window bounds in landscape. So apply the
|
||||
// window bounds to the view frame to make it consistent.
|
||||
let view_frame = self.frame();
|
||||
if view_frame != window_bounds {
|
||||
self.setFrame(window_bounds);
|
||||
}
|
||||
|
||||
app_state::handle_nonuser_event(
|
||||
mtm,
|
||||
EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::Resized(size),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[method(setContentScaleFactor:)]
|
||||
fn set_content_scale_factor(&self, untrusted_scale_factor: CGFloat) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
let _: () =
|
||||
unsafe { msg_send![super(self), setContentScaleFactor: untrusted_scale_factor] };
|
||||
|
||||
// `window` is null when `setContentScaleFactor` is invoked prior to `[UIWindow
|
||||
// makeKeyAndVisible]` at window creation time (either manually or internally by
|
||||
// UIKit when the `UIView` is first created), in which case we send no events here
|
||||
let window = match self.window() {
|
||||
Some(window) => window,
|
||||
None => return,
|
||||
};
|
||||
// `setContentScaleFactor` may be called with a value of 0, which means "reset the
|
||||
// content scale factor to a device-specific default value", so we can't use the
|
||||
// parameter here. We can query the actual factor using the getter
|
||||
let scale_factor = self.contentScaleFactor();
|
||||
assert!(
|
||||
!scale_factor.is_nan()
|
||||
&& scale_factor.is_finite()
|
||||
&& scale_factor.is_sign_positive()
|
||||
&& scale_factor > 0.0,
|
||||
"invalid scale_factor set on UIView",
|
||||
);
|
||||
let scale_factor = scale_factor as f64;
|
||||
let bounds = self.bounds();
|
||||
let screen = window.screen();
|
||||
let screen_space = screen.coordinateSpace();
|
||||
let screen_frame = self.convertRect_toCoordinateSpace(bounds, &screen_space);
|
||||
let size = crate::dpi::LogicalSize {
|
||||
width: screen_frame.size.width as f64,
|
||||
height: screen_frame.size.height as f64,
|
||||
};
|
||||
let window_id = RootWindowId(window.id());
|
||||
app_state::handle_nonuser_events(
|
||||
mtm,
|
||||
std::iter::once(EventWrapper::ScaleFactorChanged(
|
||||
app_state::ScaleFactorChanged {
|
||||
window,
|
||||
scale_factor,
|
||||
suggested_size: size.to_physical(scale_factor),
|
||||
},
|
||||
))
|
||||
.chain(std::iter::once(EventWrapper::StaticEvent(
|
||||
Event::WindowEvent {
|
||||
window_id,
|
||||
event: WindowEvent::Resized(size.to_physical(scale_factor)),
|
||||
},
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
#[method(touchesBegan:withEvent:)]
|
||||
fn touches_began(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
|
||||
self.handle_touches(touches)
|
||||
}
|
||||
|
||||
#[method(touchesMoved:withEvent:)]
|
||||
fn touches_moved(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
|
||||
self.handle_touches(touches)
|
||||
}
|
||||
|
||||
#[method(touchesEnded:withEvent:)]
|
||||
fn touches_ended(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
|
||||
self.handle_touches(touches)
|
||||
}
|
||||
|
||||
#[method(touchesCancelled:withEvent:)]
|
||||
fn touches_cancelled(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
|
||||
self.handle_touches(touches)
|
||||
}
|
||||
|
||||
#[method(pinchGesture:)]
|
||||
fn pinch_gesture(&self, recognizer: &UIPinchGestureRecognizer) {
|
||||
let window = self.window().unwrap();
|
||||
|
||||
let (phase, delta) = match recognizer.state() {
|
||||
UIGestureRecognizerState::Began => {
|
||||
self.ivars().pinch_last_delta.set(recognizer.scale());
|
||||
(TouchPhase::Started, 0.0)
|
||||
}
|
||||
UIGestureRecognizerState::Changed => {
|
||||
let last_scale: f64 = self.ivars().pinch_last_delta.replace(recognizer.scale());
|
||||
(TouchPhase::Moved, recognizer.scale() - last_scale)
|
||||
}
|
||||
UIGestureRecognizerState::Ended => {
|
||||
let last_scale: f64 = self.ivars().pinch_last_delta.replace(0.0);
|
||||
(TouchPhase::Moved, recognizer.scale() - last_scale)
|
||||
}
|
||||
UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
|
||||
self.ivars().rotation_last_delta.set(0.0);
|
||||
// Pass -delta so that action is reversed
|
||||
(TouchPhase::Cancelled, -recognizer.scale())
|
||||
}
|
||||
state => panic!("unexpected recognizer state: {:?}", state),
|
||||
};
|
||||
|
||||
let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::PinchGesture {
|
||||
device_id: DEVICE_ID,
|
||||
delta: delta as f64,
|
||||
phase,
|
||||
},
|
||||
});
|
||||
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
app_state::handle_nonuser_event(mtm, gesture_event);
|
||||
}
|
||||
|
||||
#[method(doubleTapGesture:)]
|
||||
fn double_tap_gesture(&self, recognizer: &UITapGestureRecognizer) {
|
||||
let window = self.window().unwrap();
|
||||
|
||||
if recognizer.state() == UIGestureRecognizerState::Ended {
|
||||
let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::DoubleTapGesture {
|
||||
device_id: DEVICE_ID,
|
||||
},
|
||||
});
|
||||
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
app_state::handle_nonuser_event(mtm, gesture_event);
|
||||
}
|
||||
}
|
||||
|
||||
#[method(rotationGesture:)]
|
||||
fn rotation_gesture(&self, recognizer: &UIRotationGestureRecognizer) {
|
||||
let window = self.window().unwrap();
|
||||
|
||||
let (phase, delta) = match recognizer.state() {
|
||||
UIGestureRecognizerState::Began => {
|
||||
self.ivars().rotation_last_delta.set(0.0);
|
||||
|
||||
(TouchPhase::Started, 0.0)
|
||||
}
|
||||
UIGestureRecognizerState::Changed => {
|
||||
let last_rotation = self.ivars().rotation_last_delta.replace(recognizer.rotation());
|
||||
|
||||
(TouchPhase::Moved, recognizer.rotation() - last_rotation)
|
||||
}
|
||||
UIGestureRecognizerState::Ended => {
|
||||
let last_rotation = self.ivars().rotation_last_delta.replace(0.0);
|
||||
|
||||
(TouchPhase::Ended, recognizer.rotation() - last_rotation)
|
||||
}
|
||||
UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
|
||||
self.ivars().rotation_last_delta.set(0.0);
|
||||
|
||||
// Pass -delta so that action is reversed
|
||||
(TouchPhase::Cancelled, -recognizer.rotation())
|
||||
}
|
||||
state => panic!("unexpected recognizer state: {:?}", state),
|
||||
};
|
||||
|
||||
// Make delta negative to match macos, convert to degrees
|
||||
let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::RotationGesture {
|
||||
device_id: DEVICE_ID,
|
||||
delta: -delta.to_degrees() as _,
|
||||
phase,
|
||||
},
|
||||
});
|
||||
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
app_state::handle_nonuser_event(mtm, gesture_event);
|
||||
}
|
||||
|
||||
#[method(panGesture:)]
|
||||
fn pan_gesture(&self, recognizer: &UIPanGestureRecognizer) {
|
||||
let window = self.window().unwrap();
|
||||
|
||||
let translation = recognizer.translationInView(Some(self));
|
||||
|
||||
let (phase, dx, dy) = match recognizer.state() {
|
||||
UIGestureRecognizerState::Began => {
|
||||
self.ivars().pan_last_delta.set(translation);
|
||||
|
||||
(TouchPhase::Started, 0.0, 0.0)
|
||||
}
|
||||
UIGestureRecognizerState::Changed => {
|
||||
let last_pan: CGPoint = self.ivars().pan_last_delta.replace(translation);
|
||||
|
||||
let dx = translation.x - last_pan.x;
|
||||
let dy = translation.y - last_pan.y;
|
||||
|
||||
(TouchPhase::Moved, dx, dy)
|
||||
}
|
||||
UIGestureRecognizerState::Ended => {
|
||||
let last_pan: CGPoint = self.ivars().pan_last_delta.replace(CGPoint{x:0.0, y:0.0});
|
||||
|
||||
let dx = translation.x - last_pan.x;
|
||||
let dy = translation.y - last_pan.y;
|
||||
|
||||
(TouchPhase::Ended, dx, dy)
|
||||
}
|
||||
UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
|
||||
let last_pan: CGPoint = self.ivars().pan_last_delta.replace(CGPoint{x:0.0, y:0.0});
|
||||
|
||||
// Pass -delta so that action is reversed
|
||||
(TouchPhase::Cancelled, -last_pan.x, -last_pan.y)
|
||||
}
|
||||
state => panic!("unexpected recognizer state: {:?}", state),
|
||||
};
|
||||
|
||||
|
||||
let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::PanGesture {
|
||||
device_id: DEVICE_ID,
|
||||
delta: PhysicalPosition::new(dx as _, dy as _),
|
||||
phase,
|
||||
},
|
||||
});
|
||||
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
app_state::handle_nonuser_event(mtm, gesture_event);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl NSObjectProtocol for WinitView {}
|
||||
|
||||
unsafe impl UIGestureRecognizerDelegate for WinitView {
|
||||
#[method(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)]
|
||||
fn should_recognize_simultaneously(&self, _gesture_recognizer: &UIGestureRecognizer, _other_gesture_recognizer: &UIGestureRecognizer) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl WinitView {
|
||||
pub(crate) fn new(
|
||||
mtm: MainThreadMarker,
|
||||
window_attributes: &WindowAttributes,
|
||||
frame: CGRect,
|
||||
) -> Retained<Self> {
|
||||
let this = mtm.alloc().set_ivars(WinitViewState {
|
||||
pinch_gesture_recognizer: RefCell::new(None),
|
||||
doubletap_gesture_recognizer: RefCell::new(None),
|
||||
rotation_gesture_recognizer: RefCell::new(None),
|
||||
pan_gesture_recognizer: RefCell::new(None),
|
||||
|
||||
rotation_last_delta: Cell::new(0.0),
|
||||
pinch_last_delta: Cell::new(0.0),
|
||||
pan_last_delta: Cell::new(CGPoint { x: 0.0, y: 0.0 }),
|
||||
});
|
||||
let this: Retained<Self> = unsafe { msg_send_id![super(this), initWithFrame: frame] };
|
||||
|
||||
this.setMultipleTouchEnabled(true);
|
||||
|
||||
if let Some(scale_factor) = window_attributes.platform_specific.scale_factor {
|
||||
this.setContentScaleFactor(scale_factor as _);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn window(&self) -> Option<Retained<WinitUIWindow>> {
|
||||
// SAFETY: `WinitView`s are always installed in a `WinitUIWindow`
|
||||
(**self).window().map(|window| unsafe { Retained::cast(window) })
|
||||
}
|
||||
|
||||
pub(crate) fn recognize_pinch_gesture(&self, should_recognize: bool) {
|
||||
let mtm = MainThreadMarker::from(self);
|
||||
if should_recognize {
|
||||
if self.ivars().pinch_gesture_recognizer.borrow().is_none() {
|
||||
let pinch = unsafe {
|
||||
UIPinchGestureRecognizer::initWithTarget_action(
|
||||
mtm.alloc(),
|
||||
Some(self),
|
||||
Some(sel!(pinchGesture:)),
|
||||
)
|
||||
};
|
||||
pinch.setDelegate(Some(ProtocolObject::from_ref(self)));
|
||||
self.addGestureRecognizer(&pinch);
|
||||
self.ivars().pinch_gesture_recognizer.replace(Some(pinch));
|
||||
}
|
||||
} else if let Some(recognizer) = self.ivars().pinch_gesture_recognizer.take() {
|
||||
self.removeGestureRecognizer(&recognizer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn recognize_pan_gesture(
|
||||
&self,
|
||||
should_recognize: bool,
|
||||
minimum_number_of_touches: u8,
|
||||
maximum_number_of_touches: u8,
|
||||
) {
|
||||
let mtm = MainThreadMarker::from(self);
|
||||
if should_recognize {
|
||||
if self.ivars().pan_gesture_recognizer.borrow().is_none() {
|
||||
let pan = unsafe {
|
||||
UIPanGestureRecognizer::initWithTarget_action(
|
||||
mtm.alloc(),
|
||||
Some(self),
|
||||
Some(sel!(panGesture:)),
|
||||
)
|
||||
};
|
||||
pan.setDelegate(Some(ProtocolObject::from_ref(self)));
|
||||
pan.setMinimumNumberOfTouches(minimum_number_of_touches as _);
|
||||
pan.setMaximumNumberOfTouches(maximum_number_of_touches as _);
|
||||
self.addGestureRecognizer(&pan);
|
||||
self.ivars().pan_gesture_recognizer.replace(Some(pan));
|
||||
}
|
||||
} else if let Some(recognizer) = self.ivars().pan_gesture_recognizer.take() {
|
||||
self.removeGestureRecognizer(&recognizer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn recognize_doubletap_gesture(&self, should_recognize: bool) {
|
||||
let mtm = MainThreadMarker::from(self);
|
||||
if should_recognize {
|
||||
if self.ivars().doubletap_gesture_recognizer.borrow().is_none() {
|
||||
let tap = unsafe {
|
||||
UITapGestureRecognizer::initWithTarget_action(
|
||||
mtm.alloc(),
|
||||
Some(self),
|
||||
Some(sel!(doubleTapGesture:)),
|
||||
)
|
||||
};
|
||||
tap.setDelegate(Some(ProtocolObject::from_ref(self)));
|
||||
tap.setNumberOfTapsRequired(2);
|
||||
tap.setNumberOfTouchesRequired(1);
|
||||
self.addGestureRecognizer(&tap);
|
||||
self.ivars().doubletap_gesture_recognizer.replace(Some(tap));
|
||||
}
|
||||
} else if let Some(recognizer) = self.ivars().doubletap_gesture_recognizer.take() {
|
||||
self.removeGestureRecognizer(&recognizer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn recognize_rotation_gesture(&self, should_recognize: bool) {
|
||||
let mtm = MainThreadMarker::from(self);
|
||||
if should_recognize {
|
||||
if self.ivars().rotation_gesture_recognizer.borrow().is_none() {
|
||||
let rotation = unsafe {
|
||||
UIRotationGestureRecognizer::initWithTarget_action(
|
||||
mtm.alloc(),
|
||||
Some(self),
|
||||
Some(sel!(rotationGesture:)),
|
||||
)
|
||||
};
|
||||
rotation.setDelegate(Some(ProtocolObject::from_ref(self)));
|
||||
self.addGestureRecognizer(&rotation);
|
||||
self.ivars().rotation_gesture_recognizer.replace(Some(rotation));
|
||||
}
|
||||
} else if let Some(recognizer) = self.ivars().rotation_gesture_recognizer.take() {
|
||||
self.removeGestureRecognizer(&recognizer);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_touches(&self, touches: &NSSet<UITouch>) {
|
||||
let window = self.window().unwrap();
|
||||
let mut touch_events = Vec::new();
|
||||
let os_supports_force = app_state::os_capabilities().force_touch;
|
||||
for touch in touches {
|
||||
let logical_location = touch.locationInView(None);
|
||||
let touch_type = touch.r#type();
|
||||
let force = if os_supports_force {
|
||||
let trait_collection = self.traitCollection();
|
||||
let touch_capability = trait_collection.forceTouchCapability();
|
||||
// Both the OS _and_ the device need to be checked for force touch support.
|
||||
if touch_capability == UIForceTouchCapability::Available
|
||||
|| touch_type == UITouchType::Pencil
|
||||
{
|
||||
let force = touch.force();
|
||||
let max_possible_force = touch.maximumPossibleForce();
|
||||
let altitude_angle: Option<f64> = if touch_type == UITouchType::Pencil {
|
||||
let angle = touch.altitudeAngle();
|
||||
Some(angle as _)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(Force::Calibrated {
|
||||
force: force as _,
|
||||
max_possible_force: max_possible_force as _,
|
||||
altitude_angle,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let touch_id = touch as *const UITouch as u64;
|
||||
let phase = touch.phase();
|
||||
let phase = match phase {
|
||||
UITouchPhase::Began => TouchPhase::Started,
|
||||
UITouchPhase::Moved => TouchPhase::Moved,
|
||||
// 2 is UITouchPhase::Stationary and is not expected here
|
||||
UITouchPhase::Ended => TouchPhase::Ended,
|
||||
UITouchPhase::Cancelled => TouchPhase::Cancelled,
|
||||
_ => panic!("unexpected touch phase: {phase:?}"),
|
||||
};
|
||||
|
||||
let physical_location = {
|
||||
let scale_factor = self.contentScaleFactor();
|
||||
PhysicalPosition::from_logical::<(f64, f64), f64>(
|
||||
(logical_location.x as _, logical_location.y as _),
|
||||
scale_factor as f64,
|
||||
)
|
||||
};
|
||||
touch_events.push(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(window.id()),
|
||||
event: WindowEvent::Touch(Touch {
|
||||
device_id: DEVICE_ID,
|
||||
id: touch_id,
|
||||
location: physical_location,
|
||||
force,
|
||||
phase,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
app_state::handle_nonuser_events(mtm, touch_events);
|
||||
}
|
||||
}
|
||||
176
src/platform_impl/apple/uikit/view_controller.rs
Normal file
176
src/platform_impl/apple/uikit/view_controller.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
use std::cell::Cell;
|
||||
|
||||
use objc2::rc::Retained;
|
||||
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
|
||||
use objc2_foundation::{MainThreadMarker, NSObject};
|
||||
use objc2_ui_kit::{
|
||||
UIDevice, UIInterfaceOrientationMask, UIRectEdge, UIResponder, UIStatusBarStyle,
|
||||
UIUserInterfaceIdiom, UIView, UIViewController,
|
||||
};
|
||||
|
||||
use super::app_state::{self};
|
||||
use crate::platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations};
|
||||
use crate::window::WindowAttributes;
|
||||
|
||||
pub struct ViewControllerState {
|
||||
prefers_status_bar_hidden: Cell<bool>,
|
||||
preferred_status_bar_style: Cell<UIStatusBarStyle>,
|
||||
prefers_home_indicator_auto_hidden: Cell<bool>,
|
||||
supported_orientations: Cell<UIInterfaceOrientationMask>,
|
||||
preferred_screen_edges_deferring_system_gestures: Cell<UIRectEdge>,
|
||||
}
|
||||
|
||||
declare_class!(
|
||||
pub(crate) struct WinitViewController;
|
||||
|
||||
unsafe impl ClassType for WinitViewController {
|
||||
#[inherits(UIResponder, NSObject)]
|
||||
type Super = UIViewController;
|
||||
type Mutability = mutability::MainThreadOnly;
|
||||
const NAME: &'static str = "WinitUIViewController";
|
||||
}
|
||||
|
||||
impl DeclaredClass for WinitViewController {
|
||||
type Ivars = ViewControllerState;
|
||||
}
|
||||
|
||||
unsafe impl WinitViewController {
|
||||
#[method(shouldAutorotate)]
|
||||
fn should_autorotate(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[method(prefersStatusBarHidden)]
|
||||
fn prefers_status_bar_hidden(&self) -> bool {
|
||||
self.ivars().prefers_status_bar_hidden.get()
|
||||
}
|
||||
|
||||
#[method(preferredStatusBarStyle)]
|
||||
fn preferred_status_bar_style(&self) -> UIStatusBarStyle {
|
||||
self.ivars().preferred_status_bar_style.get()
|
||||
}
|
||||
|
||||
#[method(prefersHomeIndicatorAutoHidden)]
|
||||
fn prefers_home_indicator_auto_hidden(&self) -> bool {
|
||||
self.ivars().prefers_home_indicator_auto_hidden.get()
|
||||
}
|
||||
|
||||
#[method(supportedInterfaceOrientations)]
|
||||
fn supported_orientations(&self) -> UIInterfaceOrientationMask {
|
||||
self.ivars().supported_orientations.get()
|
||||
}
|
||||
|
||||
#[method(preferredScreenEdgesDeferringSystemGestures)]
|
||||
fn preferred_screen_edges_deferring_system_gestures(&self) -> UIRectEdge {
|
||||
self.ivars()
|
||||
.preferred_screen_edges_deferring_system_gestures
|
||||
.get()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl WinitViewController {
|
||||
pub(crate) fn set_prefers_status_bar_hidden(&self, val: bool) {
|
||||
self.ivars().prefers_status_bar_hidden.set(val);
|
||||
self.setNeedsStatusBarAppearanceUpdate();
|
||||
}
|
||||
|
||||
pub(crate) fn set_preferred_status_bar_style(&self, val: StatusBarStyle) {
|
||||
let val = match val {
|
||||
StatusBarStyle::Default => UIStatusBarStyle::Default,
|
||||
StatusBarStyle::LightContent => UIStatusBarStyle::LightContent,
|
||||
StatusBarStyle::DarkContent => UIStatusBarStyle::DarkContent,
|
||||
};
|
||||
self.ivars().preferred_status_bar_style.set(val);
|
||||
self.setNeedsStatusBarAppearanceUpdate();
|
||||
}
|
||||
|
||||
pub(crate) fn set_prefers_home_indicator_auto_hidden(&self, val: bool) {
|
||||
self.ivars().prefers_home_indicator_auto_hidden.set(val);
|
||||
let os_capabilities = app_state::os_capabilities();
|
||||
if os_capabilities.home_indicator_hidden {
|
||||
self.setNeedsUpdateOfHomeIndicatorAutoHidden();
|
||||
} else {
|
||||
os_capabilities.home_indicator_hidden_err_msg("ignoring")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_preferred_screen_edges_deferring_system_gestures(&self, val: ScreenEdge) {
|
||||
let val = {
|
||||
assert_eq!(val.bits() & !ScreenEdge::ALL.bits(), 0, "invalid `ScreenEdge`");
|
||||
UIRectEdge(val.bits().into())
|
||||
};
|
||||
self.ivars().preferred_screen_edges_deferring_system_gestures.set(val);
|
||||
let os_capabilities = app_state::os_capabilities();
|
||||
if os_capabilities.defer_system_gestures {
|
||||
self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures();
|
||||
} else {
|
||||
os_capabilities.defer_system_gestures_err_msg("ignoring")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_supported_interface_orientations(
|
||||
&self,
|
||||
mtm: MainThreadMarker,
|
||||
valid_orientations: ValidOrientations,
|
||||
) {
|
||||
let mask = match (valid_orientations, UIDevice::currentDevice(mtm).userInterfaceIdiom()) {
|
||||
(ValidOrientations::LandscapeAndPortrait, UIUserInterfaceIdiom::Phone) => {
|
||||
UIInterfaceOrientationMask::AllButUpsideDown
|
||||
},
|
||||
(ValidOrientations::LandscapeAndPortrait, _) => UIInterfaceOrientationMask::All,
|
||||
(ValidOrientations::Landscape, _) => UIInterfaceOrientationMask::Landscape,
|
||||
(ValidOrientations::Portrait, UIUserInterfaceIdiom::Phone) => {
|
||||
UIInterfaceOrientationMask::Portrait
|
||||
},
|
||||
(ValidOrientations::Portrait, _) => {
|
||||
UIInterfaceOrientationMask::Portrait
|
||||
| UIInterfaceOrientationMask::PortraitUpsideDown
|
||||
},
|
||||
};
|
||||
self.ivars().supported_orientations.set(mask);
|
||||
#[allow(deprecated)]
|
||||
UIViewController::attemptRotationToDeviceOrientation(mtm);
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
mtm: MainThreadMarker,
|
||||
window_attributes: &WindowAttributes,
|
||||
view: &UIView,
|
||||
) -> Retained<Self> {
|
||||
// These are set properly below, we just to set them to something in the meantime.
|
||||
let this = mtm.alloc().set_ivars(ViewControllerState {
|
||||
prefers_status_bar_hidden: Cell::new(false),
|
||||
preferred_status_bar_style: Cell::new(UIStatusBarStyle::Default),
|
||||
prefers_home_indicator_auto_hidden: Cell::new(false),
|
||||
supported_orientations: Cell::new(UIInterfaceOrientationMask::All),
|
||||
preferred_screen_edges_deferring_system_gestures: Cell::new(UIRectEdge::empty()),
|
||||
});
|
||||
let this: Retained<Self> = unsafe { msg_send_id![super(this), init] };
|
||||
|
||||
this.set_prefers_status_bar_hidden(
|
||||
window_attributes.platform_specific.prefers_status_bar_hidden,
|
||||
);
|
||||
|
||||
this.set_preferred_status_bar_style(
|
||||
window_attributes.platform_specific.preferred_status_bar_style,
|
||||
);
|
||||
|
||||
this.set_supported_interface_orientations(
|
||||
mtm,
|
||||
window_attributes.platform_specific.valid_orientations,
|
||||
);
|
||||
|
||||
this.set_prefers_home_indicator_auto_hidden(
|
||||
window_attributes.platform_specific.prefers_home_indicator_hidden,
|
||||
);
|
||||
|
||||
this.set_preferred_screen_edges_deferring_system_gestures(
|
||||
window_attributes.platform_specific.preferred_screen_edges_deferring_system_gestures,
|
||||
);
|
||||
|
||||
this.setView(Some(view));
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
736
src/platform_impl/apple/uikit/window.rs
Normal file
736
src/platform_impl/apple/uikit/window.rs
Normal file
|
|
@ -0,0 +1,736 @@
|
|||
#![allow(clippy::unnecessary_cast)]
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::{AnyObject, NSObject};
|
||||
use objc2::{class, declare_class, msg_send, msg_send_id, mutability, ClassType, DeclaredClass};
|
||||
use objc2_foundation::{
|
||||
CGFloat, CGPoint, CGRect, CGSize, MainThreadBound, MainThreadMarker, NSObjectProtocol,
|
||||
};
|
||||
use objc2_ui_kit::{
|
||||
UIApplication, UICoordinateSpace, UIResponder, UIScreen, UIScreenOverscanCompensation,
|
||||
UIViewController, UIWindow,
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use super::app_state::EventWrapper;
|
||||
use super::view::WinitView;
|
||||
use super::view_controller::WinitViewController;
|
||||
use super::{app_state, monitor, ActiveEventLoop, Fullscreen, MonitorHandle};
|
||||
use crate::cursor::Cursor;
|
||||
use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size};
|
||||
use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError};
|
||||
use crate::event::{Event, WindowEvent};
|
||||
use crate::icon::Icon;
|
||||
use crate::platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations};
|
||||
use crate::window::{
|
||||
CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes,
|
||||
WindowButtons, WindowId as RootWindowId, WindowLevel,
|
||||
};
|
||||
|
||||
declare_class!(
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct WinitUIWindow;
|
||||
|
||||
unsafe impl ClassType for WinitUIWindow {
|
||||
#[inherits(UIResponder, NSObject)]
|
||||
type Super = UIWindow;
|
||||
type Mutability = mutability::MainThreadOnly;
|
||||
const NAME: &'static str = "WinitUIWindow";
|
||||
}
|
||||
|
||||
impl DeclaredClass for WinitUIWindow {}
|
||||
|
||||
unsafe impl WinitUIWindow {
|
||||
#[method(becomeKeyWindow)]
|
||||
fn become_key_window(&self) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
app_state::handle_nonuser_event(
|
||||
mtm,
|
||||
EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(self.id()),
|
||||
event: WindowEvent::Focused(true),
|
||||
}),
|
||||
);
|
||||
let _: () = unsafe { msg_send![super(self), becomeKeyWindow] };
|
||||
}
|
||||
|
||||
#[method(resignKeyWindow)]
|
||||
fn resign_key_window(&self) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
app_state::handle_nonuser_event(
|
||||
mtm,
|
||||
EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: RootWindowId(self.id()),
|
||||
event: WindowEvent::Focused(false),
|
||||
}),
|
||||
);
|
||||
let _: () = unsafe { msg_send![super(self), resignKeyWindow] };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl WinitUIWindow {
|
||||
pub(crate) fn new(
|
||||
mtm: MainThreadMarker,
|
||||
window_attributes: &WindowAttributes,
|
||||
frame: CGRect,
|
||||
view_controller: &UIViewController,
|
||||
) -> Retained<Self> {
|
||||
let this: Retained<Self> = unsafe { msg_send_id![mtm.alloc(), initWithFrame: frame] };
|
||||
|
||||
this.setRootViewController(Some(view_controller));
|
||||
|
||||
match window_attributes.fullscreen.clone().map(Into::into) {
|
||||
Some(Fullscreen::Exclusive(ref video_mode)) => {
|
||||
let monitor = video_mode.monitor();
|
||||
let screen = monitor.ui_screen(mtm);
|
||||
screen.setCurrentMode(Some(video_mode.screen_mode(mtm)));
|
||||
this.setScreen(screen);
|
||||
},
|
||||
Some(Fullscreen::Borderless(Some(ref monitor))) => {
|
||||
let screen = monitor.ui_screen(mtm);
|
||||
this.setScreen(screen);
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub(crate) fn id(&self) -> WindowId {
|
||||
(self as *const Self as usize as u64).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Inner {
|
||||
window: Retained<WinitUIWindow>,
|
||||
view_controller: Retained<WinitViewController>,
|
||||
view: Retained<WinitView>,
|
||||
gl_or_metal_backed: bool,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
pub fn set_title(&self, _title: &str) {
|
||||
debug!("`Window::set_title` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn set_transparent(&self, _transparent: bool) {
|
||||
debug!("`Window::set_transparent` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn set_blur(&self, _blur: bool) {
|
||||
debug!("`Window::set_blur` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn set_visible(&self, visible: bool) {
|
||||
self.window.setHidden(!visible)
|
||||
}
|
||||
|
||||
pub fn is_visible(&self) -> Option<bool> {
|
||||
warn!("`Window::is_visible` is ignored on iOS");
|
||||
None
|
||||
}
|
||||
|
||||
pub fn request_redraw(&self) {
|
||||
if self.gl_or_metal_backed {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
// `setNeedsDisplay` does nothing on UIViews which are directly backed by CAEAGLLayer or
|
||||
// CAMetalLayer. Ordinarily the OS sets up a bunch of UIKit state before
|
||||
// calling drawRect: on a UIView, but when using raw or gl/metal for drawing
|
||||
// this work is completely avoided.
|
||||
//
|
||||
// The docs for `setNeedsDisplay` don't mention `CAMetalLayer`; however, this has been
|
||||
// confirmed via testing.
|
||||
//
|
||||
// https://developer.apple.com/documentation/uikit/uiview/1622437-setneedsdisplay?language=objc
|
||||
app_state::queue_gl_or_metal_redraw(mtm, self.window.clone());
|
||||
} else {
|
||||
self.view.setNeedsDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pre_present_notify(&self) {}
|
||||
|
||||
pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
||||
let safe_area = self.safe_area_screen_space();
|
||||
let position =
|
||||
LogicalPosition { x: safe_area.origin.x as f64, y: safe_area.origin.y as f64 };
|
||||
let scale_factor = self.scale_factor();
|
||||
Ok(position.to_physical(scale_factor))
|
||||
}
|
||||
|
||||
pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
||||
let screen_frame = self.screen_frame();
|
||||
let position =
|
||||
LogicalPosition { x: screen_frame.origin.x as f64, y: screen_frame.origin.y as f64 };
|
||||
let scale_factor = self.scale_factor();
|
||||
Ok(position.to_physical(scale_factor))
|
||||
}
|
||||
|
||||
pub fn set_outer_position(&self, physical_position: Position) {
|
||||
let scale_factor = self.scale_factor();
|
||||
let position = physical_position.to_logical::<f64>(scale_factor);
|
||||
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.rect_from_screen_space(new_screen_frame);
|
||||
self.window.setBounds(bounds);
|
||||
}
|
||||
|
||||
pub fn inner_size(&self) -> PhysicalSize<u32> {
|
||||
let scale_factor = self.scale_factor();
|
||||
let safe_area = self.safe_area_screen_space();
|
||||
let size = LogicalSize {
|
||||
width: safe_area.size.width as f64,
|
||||
height: safe_area.size.height as f64,
|
||||
};
|
||||
size.to_physical(scale_factor)
|
||||
}
|
||||
|
||||
pub fn outer_size(&self) -> PhysicalSize<u32> {
|
||||
let scale_factor = self.scale_factor();
|
||||
let screen_frame = self.screen_frame();
|
||||
let size = LogicalSize {
|
||||
width: screen_frame.size.width as f64,
|
||||
height: screen_frame.size.height as f64,
|
||||
};
|
||||
size.to_physical(scale_factor)
|
||||
}
|
||||
|
||||
pub fn request_inner_size(&self, _size: Size) -> Option<PhysicalSize<u32>> {
|
||||
Some(self.inner_size())
|
||||
}
|
||||
|
||||
pub fn set_min_inner_size(&self, _dimensions: Option<Size>) {
|
||||
warn!("`Window::set_min_inner_size` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn set_max_inner_size(&self, _dimensions: Option<Size>) {
|
||||
warn!("`Window::set_max_inner_size` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn resize_increments(&self) -> Option<PhysicalSize<u32>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_resize_increments(&self, _increments: Option<Size>) {
|
||||
warn!("`Window::set_resize_increments` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn set_resizable(&self, _resizable: bool) {
|
||||
warn!("`Window::set_resizable` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn is_resizable(&self) -> bool {
|
||||
warn!("`Window::is_resizable` is ignored on iOS");
|
||||
false
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {
|
||||
warn!("`Window::set_enabled_buttons` is ignored on iOS");
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn enabled_buttons(&self) -> WindowButtons {
|
||||
warn!("`Window::enabled_buttons` is ignored on iOS");
|
||||
WindowButtons::all()
|
||||
}
|
||||
|
||||
pub fn scale_factor(&self) -> f64 {
|
||||
self.view.contentScaleFactor() as _
|
||||
}
|
||||
|
||||
pub fn set_cursor(&self, _cursor: Cursor) {
|
||||
debug!("`Window::set_cursor` ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> {
|
||||
Err(ExternalError::NotSupported(NotSupportedError::new()))
|
||||
}
|
||||
|
||||
pub fn set_cursor_grab(&self, _: CursorGrabMode) -> Result<(), ExternalError> {
|
||||
Err(ExternalError::NotSupported(NotSupportedError::new()))
|
||||
}
|
||||
|
||||
pub fn set_cursor_visible(&self, _visible: bool) {
|
||||
debug!("`Window::set_cursor_visible` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn drag_window(&self) -> Result<(), ExternalError> {
|
||||
Err(ExternalError::NotSupported(NotSupportedError::new()))
|
||||
}
|
||||
|
||||
pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> {
|
||||
Err(ExternalError::NotSupported(NotSupportedError::new()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn show_window_menu(&self, _position: Position) {}
|
||||
|
||||
pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> {
|
||||
Err(ExternalError::NotSupported(NotSupportedError::new()))
|
||||
}
|
||||
|
||||
pub fn set_minimized(&self, _minimized: bool) {
|
||||
warn!("`Window::set_minimized` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn is_minimized(&self) -> Option<bool> {
|
||||
warn!("`Window::is_minimized` is ignored on iOS");
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_maximized(&self, _maximized: bool) {
|
||||
warn!("`Window::set_maximized` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn is_maximized(&self) -> bool {
|
||||
warn!("`Window::is_maximized` is ignored on iOS");
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn set_fullscreen(&self, monitor: Option<Fullscreen>) {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
let uiscreen = match &monitor {
|
||||
Some(Fullscreen::Exclusive(video_mode)) => {
|
||||
let uiscreen = video_mode.monitor.ui_screen(mtm);
|
||||
uiscreen.setCurrentMode(Some(video_mode.screen_mode(mtm)));
|
||||
uiscreen.clone()
|
||||
},
|
||||
Some(Fullscreen::Borderless(Some(monitor))) => monitor.ui_screen(mtm).clone(),
|
||||
Some(Fullscreen::Borderless(None)) => {
|
||||
self.current_monitor_inner().ui_screen(mtm).clone()
|
||||
},
|
||||
None => {
|
||||
warn!("`Window::set_fullscreen(None)` ignored on iOS");
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
// this is pretty slow on iOS, so avoid doing it if we can
|
||||
let current = self.window.screen();
|
||||
if uiscreen != current {
|
||||
self.window.setScreen(&uiscreen);
|
||||
}
|
||||
|
||||
let bounds = uiscreen.bounds();
|
||||
self.window.setFrame(bounds);
|
||||
|
||||
// For external displays, we must disable overscan compensation or
|
||||
// the displayed image will have giant black bars surrounding it on
|
||||
// each side
|
||||
uiscreen.setOverscanCompensation(UIScreenOverscanCompensation::None);
|
||||
}
|
||||
|
||||
pub(crate) fn fullscreen(&self) -> Option<Fullscreen> {
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
let monitor = self.current_monitor_inner();
|
||||
let uiscreen = monitor.ui_screen(mtm);
|
||||
let screen_space_bounds = self.screen_frame();
|
||||
let screen_bounds = uiscreen.bounds();
|
||||
|
||||
// TODO: track fullscreen instead of relying on brittle float comparisons
|
||||
if screen_space_bounds.origin.x == screen_bounds.origin.x
|
||||
&& 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(Fullscreen::Borderless(Some(monitor)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_decorations(&self, _decorations: bool) {}
|
||||
|
||||
pub fn is_decorated(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_window_level(&self, _level: WindowLevel) {
|
||||
warn!("`Window::set_window_level` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn set_window_icon(&self, _icon: Option<Icon>) {
|
||||
warn!("`Window::set_window_icon` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) {
|
||||
warn!("`Window::set_ime_cursor_area` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn set_ime_allowed(&self, _allowed: bool) {
|
||||
warn!("`Window::set_ime_allowed` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn set_ime_purpose(&self, _purpose: ImePurpose) {
|
||||
warn!("`Window::set_ime_allowed` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn focus_window(&self) {
|
||||
warn!("`Window::set_focus` is ignored on iOS")
|
||||
}
|
||||
|
||||
pub fn request_user_attention(&self, _request_type: Option<UserAttentionType>) {
|
||||
warn!("`Window::request_user_attention` is ignored on iOS")
|
||||
}
|
||||
|
||||
// Allow directly accessing the current monitor internally without unwrapping.
|
||||
fn current_monitor_inner(&self) -> MonitorHandle {
|
||||
MonitorHandle::new(self.window.screen())
|
||||
}
|
||||
|
||||
pub fn current_monitor(&self) -> Option<MonitorHandle> {
|
||||
Some(self.current_monitor_inner())
|
||||
}
|
||||
|
||||
pub fn available_monitors(&self) -> VecDeque<MonitorHandle> {
|
||||
monitor::uiscreens(MainThreadMarker::new().unwrap())
|
||||
}
|
||||
|
||||
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
|
||||
#[allow(deprecated)]
|
||||
Some(MonitorHandle::new(UIScreen::mainScreen(MainThreadMarker::new().unwrap())))
|
||||
}
|
||||
|
||||
pub fn id(&self) -> WindowId {
|
||||
self.window.id()
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_04")]
|
||||
pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle {
|
||||
let mut window_handle = rwh_04::UiKitHandle::empty();
|
||||
window_handle.ui_window = Retained::as_ptr(&self.window) as _;
|
||||
window_handle.ui_view = Retained::as_ptr(&self.view) as _;
|
||||
window_handle.ui_view_controller = Retained::as_ptr(&self.view_controller) as _;
|
||||
rwh_04::RawWindowHandle::UiKit(window_handle)
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_05")]
|
||||
pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle {
|
||||
let mut window_handle = rwh_05::UiKitWindowHandle::empty();
|
||||
window_handle.ui_window = Retained::as_ptr(&self.window) as _;
|
||||
window_handle.ui_view = Retained::as_ptr(&self.view) as _;
|
||||
window_handle.ui_view_controller = Retained::as_ptr(&self.view_controller) as _;
|
||||
rwh_05::RawWindowHandle::UiKit(window_handle)
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_05")]
|
||||
pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle {
|
||||
rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty())
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_06")]
|
||||
pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle {
|
||||
let mut window_handle = rwh_06::UiKitWindowHandle::new({
|
||||
let ui_view = Retained::as_ptr(&self.view) as _;
|
||||
std::ptr::NonNull::new(ui_view).expect("Retained<T> should never be null")
|
||||
});
|
||||
window_handle.ui_view_controller =
|
||||
std::ptr::NonNull::new(Retained::as_ptr(&self.view_controller) as _);
|
||||
rwh_06::RawWindowHandle::UiKit(window_handle)
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> Option<Theme> {
|
||||
warn!("`Window::theme` is ignored on iOS");
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_content_protected(&self, _protected: bool) {}
|
||||
|
||||
pub fn has_focus(&self) -> bool {
|
||||
self.window.isKeyWindow()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_theme(&self, _theme: Option<Theme>) {
|
||||
warn!("`Window::set_theme` is ignored on iOS");
|
||||
}
|
||||
|
||||
pub fn title(&self) -> String {
|
||||
warn!("`Window::title` is ignored on iOS");
|
||||
String::new()
|
||||
}
|
||||
|
||||
pub fn reset_dead_keys(&self) {
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Window {
|
||||
inner: MainThreadBound<Inner>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new(
|
||||
event_loop: &ActiveEventLoop,
|
||||
window_attributes: WindowAttributes,
|
||||
) -> Result<Window, RootOsError> {
|
||||
let mtm = event_loop.mtm;
|
||||
|
||||
if window_attributes.min_inner_size.is_some() {
|
||||
warn!("`WindowAttributes::min_inner_size` is ignored on iOS");
|
||||
}
|
||||
if window_attributes.max_inner_size.is_some() {
|
||||
warn!("`WindowAttributes::max_inner_size` is ignored on iOS");
|
||||
}
|
||||
|
||||
// TODO: transparency, visible
|
||||
|
||||
#[allow(deprecated)]
|
||||
let main_screen = UIScreen::mainScreen(mtm);
|
||||
let fullscreen = window_attributes.fullscreen.clone().map(Into::into);
|
||||
let screen = match fullscreen {
|
||||
Some(Fullscreen::Exclusive(ref video_mode)) => video_mode.monitor.ui_screen(mtm),
|
||||
Some(Fullscreen::Borderless(Some(ref monitor))) => monitor.ui_screen(mtm),
|
||||
Some(Fullscreen::Borderless(None)) | None => &main_screen,
|
||||
};
|
||||
|
||||
let screen_bounds = screen.bounds();
|
||||
|
||||
let frame = match window_attributes.inner_size {
|
||||
Some(dim) => {
|
||||
let scale_factor = screen.scale();
|
||||
let size = dim.to_logical::<f64>(scale_factor as f64);
|
||||
CGRect {
|
||||
origin: screen_bounds.origin,
|
||||
size: CGSize { width: size.width as _, height: size.height as _ },
|
||||
}
|
||||
},
|
||||
None => screen_bounds,
|
||||
};
|
||||
|
||||
let view = WinitView::new(mtm, &window_attributes, frame);
|
||||
|
||||
let gl_or_metal_backed =
|
||||
view.isKindOfClass(class!(CAMetalLayer)) || view.isKindOfClass(class!(CAEAGLLayer));
|
||||
|
||||
let view_controller = WinitViewController::new(mtm, &window_attributes, &view);
|
||||
let window = WinitUIWindow::new(mtm, &window_attributes, frame, &view_controller);
|
||||
|
||||
app_state::set_key_window(mtm, &window);
|
||||
|
||||
// Like the Windows and macOS backends, we send a `ScaleFactorChanged` and `Resized`
|
||||
// event on window creation if the DPI factor != 1.0
|
||||
let scale_factor = view.contentScaleFactor();
|
||||
let scale_factor = scale_factor as f64;
|
||||
if scale_factor != 1.0 {
|
||||
let bounds = view.bounds();
|
||||
let screen = window.screen();
|
||||
let screen_space = screen.coordinateSpace();
|
||||
let screen_frame = view.convertRect_toCoordinateSpace(bounds, &screen_space);
|
||||
let size = LogicalSize {
|
||||
width: screen_frame.size.width as f64,
|
||||
height: screen_frame.size.height as f64,
|
||||
};
|
||||
let window_id = RootWindowId(window.id());
|
||||
app_state::handle_nonuser_events(
|
||||
mtm,
|
||||
std::iter::once(EventWrapper::ScaleFactorChanged(app_state::ScaleFactorChanged {
|
||||
window: window.clone(),
|
||||
scale_factor,
|
||||
suggested_size: size.to_physical(scale_factor),
|
||||
}))
|
||||
.chain(std::iter::once(EventWrapper::StaticEvent(
|
||||
Event::WindowEvent {
|
||||
window_id,
|
||||
event: WindowEvent::Resized(size.to_physical(scale_factor)),
|
||||
},
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
let inner = Inner { window, view_controller, view, gl_or_metal_backed };
|
||||
Ok(Window { inner: MainThreadBound::new(inner, mtm) })
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) {
|
||||
// For now, don't actually do queuing, since it may be less predictable
|
||||
self.maybe_wait_on_main(f)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_wait_on_main<R: Send>(&self, f: impl FnOnce(&Inner) -> R + Send) -> R {
|
||||
self.inner.get_on_main(|inner| f(inner))
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_06")]
|
||||
#[inline]
|
||||
pub(crate) fn raw_window_handle_rwh_06(
|
||||
&self,
|
||||
) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
|
||||
if let Some(mtm) = MainThreadMarker::new() {
|
||||
Ok(self.inner.get(mtm).raw_window_handle_rwh_06())
|
||||
} else {
|
||||
Err(rwh_06::HandleError::Unavailable)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_06")]
|
||||
#[inline]
|
||||
pub(crate) fn raw_display_handle_rwh_06(
|
||||
&self,
|
||||
) -> Result<rwh_06::RawDisplayHandle, rwh_06::HandleError> {
|
||||
Ok(rwh_06::RawDisplayHandle::UiKit(rwh_06::UiKitDisplayHandle::new()))
|
||||
}
|
||||
}
|
||||
|
||||
// WindowExtIOS
|
||||
impl Inner {
|
||||
pub fn set_scale_factor(&self, scale_factor: f64) {
|
||||
assert!(
|
||||
dpi::validate_scale_factor(scale_factor),
|
||||
"`WindowExtIOS::set_scale_factor` received an invalid hidpi factor"
|
||||
);
|
||||
let scale_factor = scale_factor as CGFloat;
|
||||
self.view.setContentScaleFactor(scale_factor);
|
||||
}
|
||||
|
||||
pub fn set_valid_orientations(&self, valid_orientations: ValidOrientations) {
|
||||
self.view_controller.set_supported_interface_orientations(
|
||||
MainThreadMarker::new().unwrap(),
|
||||
valid_orientations,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_prefers_home_indicator_hidden(&self, hidden: bool) {
|
||||
self.view_controller.set_prefers_home_indicator_auto_hidden(hidden);
|
||||
}
|
||||
|
||||
pub fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) {
|
||||
self.view_controller.set_preferred_screen_edges_deferring_system_gestures(edges);
|
||||
}
|
||||
|
||||
pub fn set_prefers_status_bar_hidden(&self, hidden: bool) {
|
||||
self.view_controller.set_prefers_status_bar_hidden(hidden);
|
||||
}
|
||||
|
||||
pub fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle) {
|
||||
self.view_controller.set_preferred_status_bar_style(status_bar_style);
|
||||
}
|
||||
|
||||
pub fn recognize_pinch_gesture(&self, should_recognize: bool) {
|
||||
self.view.recognize_pinch_gesture(should_recognize);
|
||||
}
|
||||
|
||||
pub fn recognize_pan_gesture(
|
||||
&self,
|
||||
should_recognize: bool,
|
||||
minimum_number_of_touches: u8,
|
||||
maximum_number_of_touches: u8,
|
||||
) {
|
||||
self.view.recognize_pan_gesture(
|
||||
should_recognize,
|
||||
minimum_number_of_touches,
|
||||
maximum_number_of_touches,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn recognize_doubletap_gesture(&self, should_recognize: bool) {
|
||||
self.view.recognize_doubletap_gesture(should_recognize);
|
||||
}
|
||||
|
||||
pub fn recognize_rotation_gesture(&self, should_recognize: bool) {
|
||||
self.view.recognize_rotation_gesture(should_recognize);
|
||||
}
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn screen_frame(&self) -> CGRect {
|
||||
self.rect_to_screen_space(self.window.bounds())
|
||||
}
|
||||
|
||||
fn rect_to_screen_space(&self, rect: CGRect) -> CGRect {
|
||||
let screen_space = self.window.screen().coordinateSpace();
|
||||
self.window.convertRect_toCoordinateSpace(rect, &screen_space)
|
||||
}
|
||||
|
||||
fn rect_from_screen_space(&self, rect: CGRect) -> CGRect {
|
||||
let screen_space = self.window.screen().coordinateSpace();
|
||||
self.window.convertRect_fromCoordinateSpace(rect, &screen_space)
|
||||
}
|
||||
|
||||
fn safe_area_screen_space(&self) -> CGRect {
|
||||
let bounds = self.window.bounds();
|
||||
if app_state::os_capabilities().safe_area {
|
||||
let safe_area = 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.rect_to_screen_space(safe_bounds)
|
||||
} else {
|
||||
let screen_frame = self.rect_to_screen_space(bounds);
|
||||
let status_bar_frame = {
|
||||
let app = UIApplication::sharedApplication(MainThreadMarker::new().unwrap());
|
||||
#[allow(deprecated)]
|
||||
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: *mut WinitUIWindow,
|
||||
}
|
||||
|
||||
impl WindowId {
|
||||
pub const unsafe fn dummy() -> Self {
|
||||
WindowId { window: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WindowId> for u64 {
|
||||
fn from(window_id: WindowId) -> Self {
|
||||
window_id.window as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for WindowId {
|
||||
fn from(raw_id: u64) -> Self {
|
||||
Self { window: raw_id as _ }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for WindowId {}
|
||||
unsafe impl Sync for WindowId {}
|
||||
|
||||
impl From<&AnyObject> for WindowId {
|
||||
fn from(window: &AnyObject) -> WindowId {
|
||||
WindowId { window: window as *const _ as _ }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PlatformSpecificWindowAttributes {
|
||||
pub scale_factor: Option<f64>,
|
||||
pub valid_orientations: ValidOrientations,
|
||||
pub prefers_home_indicator_hidden: bool,
|
||||
pub prefers_status_bar_hidden: bool,
|
||||
pub preferred_status_bar_style: StatusBarStyle,
|
||||
pub preferred_screen_edges_deferring_system_gestures: ScreenEdge,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue