2022-12-22 21:35:33 +02:00
|
|
|
#![allow(clippy::unnecessary_cast)]
|
|
|
|
|
|
2025-01-02 03:29:42 +03:00
|
|
|
use std::collections::VecDeque;
|
|
|
|
|
use std::num::NonZeroU32;
|
2023-08-27 17:04:39 +02:00
|
|
|
use std::{fmt, hash, ptr};
|
2019-05-25 18:10:41 -07:00
|
|
|
|
2023-08-27 17:04:39 +02:00
|
|
|
use objc2::mutability::IsRetainable;
|
2024-05-27 14:49:22 +02:00
|
|
|
use objc2::rc::Retained;
|
2023-12-23 18:04:24 +01:00
|
|
|
use objc2::Message;
|
2024-04-18 17:34:19 +02:00
|
|
|
use objc2_foundation::{run_on_main, MainThreadBound, MainThreadMarker, NSInteger};
|
2024-05-27 14:49:22 +02:00
|
|
|
use objc2_ui_kit::{UIScreen, UIScreenMode};
|
2022-09-02 15:48:02 +02:00
|
|
|
|
2024-06-24 13:26:49 +02:00
|
|
|
use super::app_state;
|
2025-01-02 03:29:42 +03:00
|
|
|
use crate::dpi::PhysicalPosition;
|
|
|
|
|
use crate::monitor::VideoMode;
|
2019-05-25 18:10:41 -07:00
|
|
|
|
2023-08-27 17:04:39 +02:00
|
|
|
// Workaround for `MainThreadBound` implementing almost no traits
|
|
|
|
|
#[derive(Debug)]
|
2024-05-27 14:49:22 +02:00
|
|
|
struct MainThreadBoundDelegateImpls<T>(MainThreadBound<Retained<T>>);
|
2023-08-27 17:04:39 +02:00
|
|
|
|
2023-12-23 18:04:24 +01:00
|
|
|
impl<T: IsRetainable + Message> Clone for MainThreadBoundDelegateImpls<T> {
|
2023-08-27 17:04:39 +02:00
|
|
|
fn clone(&self) -> Self {
|
2024-05-27 14:49:22 +02:00
|
|
|
Self(run_on_main(|mtm| MainThreadBound::new(Retained::clone(self.0.get(mtm)), mtm)))
|
2023-08-27 17:04:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-23 18:04:24 +01:00
|
|
|
impl<T: IsRetainable + Message> hash::Hash for MainThreadBoundDelegateImpls<T> {
|
2023-08-27 17:04:39 +02:00
|
|
|
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
|
|
|
|
// SAFETY: Marker only used to get the pointer
|
|
|
|
|
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
2024-05-27 14:49:22 +02:00
|
|
|
Retained::as_ptr(self.0.get(mtm)).hash(state);
|
2023-08-27 17:04:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-23 18:04:24 +01:00
|
|
|
impl<T: IsRetainable + Message> PartialEq for MainThreadBoundDelegateImpls<T> {
|
2023-08-27 17:04:39 +02:00
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
|
// SAFETY: Marker only used to get the pointer
|
|
|
|
|
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
2024-05-27 14:49:22 +02:00
|
|
|
Retained::as_ptr(self.0.get(mtm)) == Retained::as_ptr(other.0.get(mtm))
|
2023-08-27 17:04:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
2022-12-28 18:36:32 +01:00
|
|
|
|
2023-12-23 18:04:24 +01:00
|
|
|
impl<T: IsRetainable + Message> Eq for MainThreadBoundDelegateImpls<T> {}
|
2022-12-28 18:36:32 +01:00
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
2023-12-26 22:12:33 +01:00
|
|
|
pub struct VideoModeHandle {
|
2025-01-02 03:29:42 +03:00
|
|
|
pub(crate) mode: VideoMode,
|
2023-08-27 17:04:39 +02:00
|
|
|
screen_mode: MainThreadBoundDelegateImpls<UIScreenMode>,
|
2019-07-29 21:16:14 +03:00
|
|
|
}
|
|
|
|
|
|
2023-12-26 22:12:33 +01:00
|
|
|
impl VideoModeHandle {
|
2023-08-27 17:04:39 +02:00
|
|
|
fn new(
|
2024-05-27 14:49:22 +02:00
|
|
|
uiscreen: Retained<UIScreen>,
|
|
|
|
|
screen_mode: Retained<UIScreenMode>,
|
2023-08-27 17:04:39 +02:00
|
|
|
mtm: MainThreadMarker,
|
2023-12-26 22:12:33 +01:00
|
|
|
) -> VideoModeHandle {
|
2022-12-28 18:36:32 +01:00
|
|
|
let refresh_rate_millihertz = refresh_rate_millihertz(&uiscreen);
|
|
|
|
|
let size = screen_mode.size();
|
2025-01-02 03:29:42 +03:00
|
|
|
let mode = VideoMode {
|
|
|
|
|
size: (size.width as u32, size.height as u32).into(),
|
|
|
|
|
bit_depth: None,
|
2022-07-08 13:25:56 +03:00
|
|
|
refresh_rate_millihertz,
|
2025-01-02 03:29:42 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
VideoModeHandle {
|
|
|
|
|
mode,
|
2023-08-27 17:04:39 +02:00
|
|
|
screen_mode: MainThreadBoundDelegateImpls(MainThreadBound::new(screen_mode, mtm)),
|
2019-07-30 23:57:31 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-27 14:49:22 +02:00
|
|
|
pub(super) fn screen_mode(&self, mtm: MainThreadMarker) -> &Retained<UIScreenMode> {
|
2023-08-27 17:04:39 +02:00
|
|
|
self.screen_mode.0.get(mtm)
|
|
|
|
|
}
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
|
|
|
|
|
2022-12-28 18:36:32 +01:00
|
|
|
pub struct MonitorHandle {
|
2024-05-27 14:49:22 +02:00
|
|
|
ui_screen: MainThreadBound<Retained<UIScreen>>,
|
2022-12-28 18:36:32 +01:00
|
|
|
}
|
|
|
|
|
|
2023-08-27 17:04:39 +02:00
|
|
|
impl Clone for MonitorHandle {
|
|
|
|
|
fn clone(&self) -> Self {
|
2024-04-18 17:34:19 +02:00
|
|
|
run_on_main(|mtm| Self {
|
2023-12-23 18:04:24 +01:00
|
|
|
ui_screen: MainThreadBound::new(self.ui_screen.get(mtm).clone(), mtm),
|
|
|
|
|
})
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-27 17:04:39 +02:00
|
|
|
impl hash::Hash for MonitorHandle {
|
|
|
|
|
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
2024-12-03 19:02:53 +01:00
|
|
|
// SAFETY: Only getting the pointer.
|
|
|
|
|
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
|
|
|
|
Retained::as_ptr(self.ui_screen.get(mtm)).hash(state);
|
2022-12-28 18:36:32 +01:00
|
|
|
}
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
|
|
|
|
|
2023-08-27 17:04:39 +02:00
|
|
|
impl PartialEq for MonitorHandle {
|
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
2024-12-03 19:02:53 +01:00
|
|
|
// SAFETY: Only getting the pointer.
|
|
|
|
|
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
|
|
|
|
ptr::eq(
|
|
|
|
|
Retained::as_ptr(self.ui_screen.get(mtm)),
|
|
|
|
|
Retained::as_ptr(other.ui_screen.get(mtm)),
|
|
|
|
|
)
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-27 17:04:39 +02:00
|
|
|
impl Eq for MonitorHandle {}
|
|
|
|
|
|
|
|
|
|
impl PartialOrd for MonitorHandle {
|
|
|
|
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
|
|
|
Some(self.cmp(other))
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-27 17:04:39 +02:00
|
|
|
impl Ord for MonitorHandle {
|
|
|
|
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
2024-12-03 19:02:53 +01:00
|
|
|
// SAFETY: Only getting the pointer.
|
2023-08-27 17:04:39 +02:00
|
|
|
// TODO: Make a better ordering
|
2024-12-03 19:02:53 +01:00
|
|
|
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
|
|
|
|
Retained::as_ptr(self.ui_screen.get(mtm)).cmp(&Retained::as_ptr(other.ui_screen.get(mtm)))
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl fmt::Debug for MonitorHandle {
|
2019-06-18 02:27:00 +08:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2023-12-24 10:12:09 +01:00
|
|
|
f.debug_struct("MonitorHandle")
|
|
|
|
|
.field("name", &self.name())
|
|
|
|
|
.field("position", &self.position())
|
|
|
|
|
.field("scale_factor", &self.scale_factor())
|
|
|
|
|
.finish_non_exhaustive()
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl MonitorHandle {
|
2024-05-27 14:49:22 +02:00
|
|
|
pub(crate) fn new(ui_screen: Retained<UIScreen>) -> Self {
|
|
|
|
|
// Holding `Retained<UIScreen>` implies we're on the main thread.
|
2023-08-27 17:04:39 +02:00
|
|
|
let mtm = MainThreadMarker::new().unwrap();
|
|
|
|
|
Self { ui_screen: MainThreadBound::new(ui_screen, mtm) }
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
|
|
|
|
|
2019-05-29 21:29:54 -04:00
|
|
|
pub fn name(&self) -> Option<String> {
|
2024-04-18 17:34:19 +02:00
|
|
|
run_on_main(|mtm| {
|
2024-05-27 14:49:22 +02:00
|
|
|
#[allow(deprecated)]
|
|
|
|
|
let main = UIScreen::mainScreen(mtm);
|
2023-12-23 18:04:24 +01:00
|
|
|
if *self.ui_screen(mtm) == main {
|
2023-08-27 17:04:39 +02:00
|
|
|
Some("Primary".to_string())
|
2024-05-27 14:49:22 +02:00
|
|
|
} else if Some(self.ui_screen(mtm)) == main.mirroredScreen().as_ref() {
|
2023-08-27 17:04:39 +02:00
|
|
|
Some("Mirrored".to_string())
|
|
|
|
|
} else {
|
2024-05-27 14:49:22 +02:00
|
|
|
#[allow(deprecated)]
|
2023-08-27 17:04:39 +02:00
|
|
|
UIScreen::screens(mtm)
|
|
|
|
|
.iter()
|
2023-12-23 18:04:24 +01:00
|
|
|
.position(|rhs| rhs == &**self.ui_screen(mtm))
|
2023-08-27 17:04:39 +02:00
|
|
|
.map(|idx| idx.to_string())
|
|
|
|
|
}
|
|
|
|
|
})
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
2019-05-29 21:29:54 -04:00
|
|
|
|
2024-07-23 19:59:37 +02:00
|
|
|
pub fn position(&self) -> Option<PhysicalPosition<i32>> {
|
2023-12-23 18:04:24 +01:00
|
|
|
let bounds = self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeBounds());
|
2024-07-23 19:59:37 +02:00
|
|
|
Some((bounds.origin.x as f64, bounds.origin.y as f64).into())
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
2019-05-29 21:29:54 -04:00
|
|
|
|
2020-01-03 14:52:27 -05:00
|
|
|
pub fn scale_factor(&self) -> f64 {
|
2023-12-23 18:04:24 +01:00
|
|
|
self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeScale()) as f64
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
2019-06-12 21:07:25 +03:00
|
|
|
|
2025-01-02 03:29:42 +03:00
|
|
|
pub fn current_video_mode(&self) -> Option<VideoMode> {
|
2024-07-21 00:01:43 +02:00
|
|
|
Some(run_on_main(|mtm| {
|
|
|
|
|
VideoModeHandle::new(
|
|
|
|
|
self.ui_screen(mtm).clone(),
|
|
|
|
|
self.ui_screen(mtm).currentMode().unwrap(),
|
|
|
|
|
mtm,
|
|
|
|
|
)
|
2025-01-02 03:29:42 +03:00
|
|
|
.mode
|
2024-07-21 00:01:43 +02:00
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-02 03:29:42 +03:00
|
|
|
pub fn video_modes_handles(&self) -> impl Iterator<Item = VideoModeHandle> {
|
2024-04-18 17:34:19 +02:00
|
|
|
run_on_main(|mtm| {
|
2023-12-23 18:04:24 +01:00
|
|
|
let ui_screen = self.ui_screen(mtm);
|
2023-08-27 17:04:39 +02:00
|
|
|
|
2025-01-02 03:29:42 +03:00
|
|
|
ui_screen
|
2023-08-27 17:04:39 +02:00
|
|
|
.availableModes()
|
|
|
|
|
.into_iter()
|
2025-01-02 03:29:42 +03:00
|
|
|
.map(|mode| VideoModeHandle::new(ui_screen.clone(), mode, mtm))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.into_iter()
|
2023-08-27 17:04:39 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-02 03:29:42 +03:00
|
|
|
pub fn video_modes(&self) -> impl Iterator<Item = VideoMode> {
|
|
|
|
|
self.video_modes_handles().map(|handle| handle.mode)
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-27 14:49:22 +02:00
|
|
|
pub(crate) fn ui_screen(&self, mtm: MainThreadMarker) -> &Retained<UIScreen> {
|
2023-08-27 17:04:39 +02:00
|
|
|
self.ui_screen.get(mtm)
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-02 03:29:42 +03:00
|
|
|
pub fn preferred_video_mode(&self) -> VideoMode {
|
2024-04-18 17:34:19 +02:00
|
|
|
run_on_main(|mtm| {
|
2023-12-26 22:12:33 +01:00
|
|
|
VideoModeHandle::new(
|
2023-12-23 18:04:24 +01:00
|
|
|
self.ui_screen(mtm).clone(),
|
|
|
|
|
self.ui_screen(mtm).preferredMode().unwrap(),
|
|
|
|
|
mtm,
|
|
|
|
|
)
|
2025-01-02 03:29:42 +03:00
|
|
|
.mode
|
2023-08-27 17:04:39 +02:00
|
|
|
})
|
2019-06-12 21:07:25 +03:00
|
|
|
}
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
|
|
|
|
|
2024-07-23 19:59:37 +02:00
|
|
|
fn refresh_rate_millihertz(uiscreen: &UIScreen) -> Option<NonZeroU32> {
|
2022-12-28 18:36:32 +01:00
|
|
|
let refresh_rate_millihertz: NSInteger = {
|
2022-07-08 13:25:56 +03:00
|
|
|
let os_capabilities = app_state::os_capabilities();
|
|
|
|
|
if os_capabilities.maximum_frames_per_second {
|
2022-12-28 18:36:32 +01:00
|
|
|
uiscreen.maximumFramesPerSecond()
|
2022-07-08 13:25:56 +03:00
|
|
|
} 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
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-07-23 19:59:37 +02:00
|
|
|
NonZeroU32::new(refresh_rate_millihertz as u32 * 1000)
|
2022-07-08 13:25:56 +03:00
|
|
|
}
|
|
|
|
|
|
2022-12-28 18:36:32 +01:00
|
|
|
pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque<MonitorHandle> {
|
2024-05-27 14:49:22 +02:00
|
|
|
#[allow(deprecated)]
|
2023-07-29 00:33:03 +02:00
|
|
|
UIScreen::screens(mtm).into_iter().map(MonitorHandle::new).collect()
|
2019-05-25 18:10:41 -07:00
|
|
|
}
|
2024-12-03 19:02:53 +01:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use objc2_foundation::NSSet;
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
// Test that UIScreen pointer comparisons are correct.
|
|
|
|
|
#[test]
|
|
|
|
|
#[allow(deprecated)]
|
|
|
|
|
fn screen_comparisons() {
|
|
|
|
|
// Test code, doesn't matter that it's not thread safe
|
|
|
|
|
let mtm = unsafe { MainThreadMarker::new_unchecked() };
|
|
|
|
|
|
|
|
|
|
assert!(ptr::eq(&*UIScreen::mainScreen(mtm), &*UIScreen::mainScreen(mtm)));
|
|
|
|
|
|
|
|
|
|
let main = UIScreen::mainScreen(mtm);
|
|
|
|
|
assert!(UIScreen::screens(mtm).iter().any(|screen| ptr::eq(screen, &*main)));
|
|
|
|
|
|
|
|
|
|
assert!(unsafe {
|
|
|
|
|
NSSet::setWithArray(&UIScreen::screens(mtm)).containsObject(&UIScreen::mainScreen(mtm))
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|