#![allow(clippy::unnecessary_cast)] use std::{ collections::{BTreeSet, VecDeque}, fmt, ops::{Deref, DerefMut}, }; use objc2::foundation::{MainThreadMarker, NSInteger}; use objc2::rc::{Id, Shared}; use super::uikit::{UIScreen, UIScreenMode}; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, monitor::VideoMode as RootVideoMode, platform_impl::platform::app_state, }; // TODO(madsmtm): Remove or refactor this #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub(crate) struct ScreenModeSendSync(pub(crate) Id); unsafe impl Send for ScreenModeSendSync {} unsafe impl Sync for ScreenModeSendSync {} #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct VideoMode { pub(crate) size: (u32, u32), pub(crate) bit_depth: u16, pub(crate) refresh_rate_millihertz: u32, pub(crate) screen_mode: ScreenModeSendSync, pub(crate) monitor: MonitorHandle, } impl VideoMode { fn new(uiscreen: Id, screen_mode: Id) -> VideoMode { assert_main_thread!("`VideoMode` can only be created on the main thread on iOS"); let refresh_rate_millihertz = refresh_rate_millihertz(&uiscreen); let size = screen_mode.size(); VideoMode { size: (size.width as u32, size.height as u32), bit_depth: 32, refresh_rate_millihertz, screen_mode: ScreenModeSendSync(screen_mode), monitor: MonitorHandle::new(uiscreen), } } pub fn size(&self) -> PhysicalSize { 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() } } #[derive(Clone, PartialEq, Eq, Hash)] pub struct Inner { uiscreen: Id, } #[derive(Clone, PartialEq, Eq, Hash)] pub struct MonitorHandle { inner: Inner, } impl PartialOrd for MonitorHandle { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for MonitorHandle { fn cmp(&self, other: &Self) -> std::cmp::Ordering { // TODO: Make a better ordering (self as *const Self).cmp(&(other as *const Self)) } } impl Deref for MonitorHandle { type Target = Inner; fn deref(&self) -> &Inner { assert_main_thread!("`MonitorHandle` methods can only be run on the main thread on iOS"); &self.inner } } impl DerefMut for MonitorHandle { fn deref_mut(&mut self) -> &mut Inner { assert_main_thread!("`MonitorHandle` methods can only be run on the main thread on iOS"); &mut self.inner } } unsafe impl Send for MonitorHandle {} unsafe impl Sync for MonitorHandle {} impl Drop for MonitorHandle { fn drop(&mut self) { assert_main_thread!("`MonitorHandle` can only be dropped on the main thread on iOS"); } } impl fmt::Debug for MonitorHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // TODO: Do this using the proper fmt API #[derive(Debug)] #[allow(dead_code)] struct MonitorHandle { name: Option, size: PhysicalSize, position: PhysicalPosition, scale_factor: f64, } let monitor_id_proxy = MonitorHandle { name: self.name(), size: self.size(), position: self.position(), scale_factor: self.scale_factor(), }; monitor_id_proxy.fmt(f) } } impl MonitorHandle { pub(crate) fn new(uiscreen: Id) -> Self { assert_main_thread!("`MonitorHandle` can only be created on the main thread on iOS"); Self { inner: Inner { uiscreen }, } } } impl Inner { pub fn name(&self) -> Option { let main = UIScreen::main(MainThreadMarker::new().unwrap()); if self.uiscreen == main { Some("Primary".to_string()) } else if self.uiscreen == main.mirroredScreen() { Some("Mirrored".to_string()) } else { UIScreen::screens(MainThreadMarker::new().unwrap()) .iter() .position(|rhs| rhs == &*self.uiscreen) .map(|idx| idx.to_string()) } } pub fn size(&self) -> PhysicalSize { let bounds = self.uiscreen.nativeBounds(); PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32) } pub fn position(&self) -> PhysicalPosition { let bounds = self.uiscreen.nativeBounds(); (bounds.origin.x as f64, bounds.origin.y as f64).into() } pub fn scale_factor(&self) -> f64 { self.uiscreen.nativeScale() as f64 } pub fn refresh_rate_millihertz(&self) -> Option { Some(refresh_rate_millihertz(&self.uiscreen)) } pub fn video_modes(&self) -> impl Iterator { // Use Ord impl of RootVideoMode let modes: BTreeSet<_> = self .uiscreen .availableModes() .into_iter() .map(|mode| { let mode: *const UIScreenMode = mode; let mode = unsafe { Id::retain(mode as *mut UIScreenMode).unwrap() }; RootVideoMode { video_mode: VideoMode::new(self.uiscreen.clone(), mode), } }) .collect(); modes.into_iter().map(|mode| mode.video_mode) } } 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 } // MonitorHandleExtIOS impl Inner { pub(crate) fn ui_screen(&self) -> &Id { &self.uiscreen } pub fn preferred_video_mode(&self) -> VideoMode { VideoMode::new( self.uiscreen.clone(), self.uiscreen.preferredMode().unwrap(), ) } } pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque { UIScreen::screens(mtm) .into_iter() .map(|screen| { let screen: *const UIScreen = screen; let screen = unsafe { Id::retain(screen as *mut UIScreen).unwrap() }; MonitorHandle::new(screen) }) .collect() }