diff --git a/Cargo.toml b/Cargo.toml index d4896cec22..36885fab73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,6 +241,7 @@ objc2-ui-kit = { version = "0.3.1", default-features = false, features = [ # Windows [target.'cfg(target_os = "windows")'.dependencies] +scopeguard = "1.2.0" unicode-segmentation = "1.7.1" windows-sys = { version = "0.59.0", features = [ "Win32_Devices_HumanInterfaceDevice", @@ -254,6 +255,7 @@ windows-sys = { version = "0.59.0", features = [ "Win32_System_LibraryLoader", "Win32_System_Ole", "Win32_Security", + "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 1109dd8399..a42e2c369c 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -42,6 +42,7 @@ changelog entry. ### Added +- Add `MonitorHandle::physical_size()` that returns monitor physical size in millimeters. - Add `ActiveEventLoop::create_proxy()`. - On Web, add `ActiveEventLoopExtWeb::is_cursor_lock_raw()` to determine if `DeviceEvent::MouseMotion` is returning raw data, not OS accelerated, when using diff --git a/src/monitor.rs b/src/monitor.rs index 88dc84a6bc..80bcaab7ad 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -113,6 +113,27 @@ pub trait MonitorHandleProvider: AsAny + fmt::Debug + Send + Sync { #[cfg_attr(not(web_platform), doc = "detailed monitor permissions.")] fn position(&self) -> Option>; + /// Returns physical size of monitor in millimeters, where the + /// first field of a tuple is width and the second is height. + /// + /// Returns `None` if the size of the monitor could not be determined. + /// + /// Should be used with care, since not all corner cases may be handled. + /// + /// If monitor is rotated by 90° or 270°, width and height will be swapped. + /// + /// Every major platform gets this data from + /// [EDID](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data), + /// which contains display dimensions in centimeters, and + /// optionally higher precision in millimeters. Therefore, by + /// convention, we also return millimeters. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Has centimeter precision. + /// - **iOS / Web / Android / Redox:** Unimplemented, always returns [`None`]. + fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)>; + /// Returns the scale factor of the underlying monitor. To map logical pixels to physical /// pixels and vice versa, use [`Window::scale_factor`]. /// diff --git a/src/platform_impl/apple/appkit/monitor.rs b/src/platform_impl/apple/appkit/monitor.rs index 09c6931c40..5a408083e6 100644 --- a/src/platform_impl/apple/appkit/monitor.rs +++ b/src/platform_impl/apple/appkit/monitor.rs @@ -12,7 +12,8 @@ use objc2_app_kit::NSScreen; use objc2_core_foundation::{CFArray, CFRetained, CFUUID}; use objc2_core_graphics::{ CGDirectDisplayID, CGDisplayBounds, CGDisplayCopyAllDisplayModes, CGDisplayCopyDisplayMode, - CGDisplayMode, CGDisplayModelNumber, CGGetActiveDisplayList, CGMainDisplayID, + CGDisplayMode, CGDisplayModelNumber, CGDisplayScreenSize, CGGetActiveDisplayList, + CGMainDisplayID, }; use objc2_core_video::{kCVReturnSuccess, CVDisplayLink, CVTimeFlags}; use objc2_foundation::{ns_string, NSNumber, NSPoint, NSRect}; @@ -211,6 +212,18 @@ impl MonitorHandleProvider for MonitorHandle { Some(position.to_physical(self.scale_factor())) } + fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> { + let size_float = unsafe { CGDisplayScreenSize(self.display_id()) }; + if size_float.width > 0.0 && size_float.height > 0.0 { + Some(( + NonZeroU32::new(size_float.width.round() as u32)?, + NonZeroU32::new(size_float.height.round() as u32)?, + )) + } else { + None + } + } + fn scale_factor(&self) -> f64 { run_on_main(|mtm| { match self.ns_screen(mtm) { diff --git a/src/platform_impl/apple/uikit/monitor.rs b/src/platform_impl/apple/uikit/monitor.rs index c72b09e0ef..0f8fca7797 100644 --- a/src/platform_impl/apple/uikit/monitor.rs +++ b/src/platform_impl/apple/uikit/monitor.rs @@ -110,6 +110,13 @@ impl MonitorHandleProvider for MonitorHandle { Some((bounds.origin.x as f64, bounds.origin.y as f64).into()) } + fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> { + // NOTE: There is no way to query the PPI on iOS. + // TODO(madsmtm): Use a hardcoded mapping of device models to PPI. + // + None + } + fn scale_factor(&self) -> f64 { self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeScale()) as f64 } diff --git a/src/platform_impl/linux/wayland/output.rs b/src/platform_impl/linux/wayland/output.rs index c4dedc5bd4..70615735b6 100644 --- a/src/platform_impl/linux/wayland/output.rs +++ b/src/platform_impl/linux/wayland/output.rs @@ -51,6 +51,16 @@ impl CoreMonitorHandle for MonitorHandle { })) } + fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> { + let output_data = self.proxy.data::().unwrap(); + let (width_mm, height_mm) = output_data.with_output_info(|oi| oi.physical_size); + + Some(( + NonZeroU32::new(width_mm.try_into().ok()?)?, + NonZeroU32::new(height_mm.try_into().ok()?)?, + )) + } + fn scale_factor(&self) -> f64 { let output_data = self.proxy.data::().unwrap(); output_data.scale_factor() as f64 diff --git a/src/platform_impl/linux/x11/monitor.rs b/src/platform_impl/linux/x11/monitor.rs index 28b721f8d5..a8d3342fc6 100644 --- a/src/platform_impl/linux/x11/monitor.rs +++ b/src/platform_impl/linux/x11/monitor.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU32; use x11rb::connection::RequestConnection; -use x11rb::protocol::randr::{self, ConnectionExt as _}; +use x11rb::protocol::randr::{self, ConnectionExt as _, Rotation}; use x11rb::protocol::xproto; use super::{util, X11Error, XConnection}; @@ -41,6 +41,8 @@ pub struct MonitorHandle { pub(crate) position: (i32, i32), /// If the monitor is the primary one primary: bool, + /// The physical size of a monitor in millimeters. + pub(crate) physical_size: Option<(NonZeroU32, NonZeroU32)>, /// The DPI scale factor pub(crate) scale_factor: f64, /// Used to determine which windows are on this monitor @@ -66,6 +68,10 @@ impl MonitorHandleProvider for MonitorHandle { Some(self.position.into()) } + fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> { + self.physical_size + } + fn scale_factor(&self) -> f64 { self.scale_factor } @@ -125,19 +131,46 @@ impl MonitorHandle { crtc: &randr::GetCrtcInfoReply, primary: bool, ) -> Option { - let (name, scale_factor, video_modes) = xconn.get_output_info(resources, crtc)?; + let (name, mut physical_size, scale_factor, video_modes) = + xconn.get_output_info(resources, crtc)?; + + let rotation = xconn + .xcb_connection() + .randr_get_crtc_info(id, x11rb::CURRENT_TIME) + .ok()? + .reply() + .ok()? + .rotation; + + // By default, X11 window system is the only that does not + // swap width and height when display is rotated. To match + // behaviour with Windows and MacOS we do this manually. + if matches!(rotation, Rotation::ROTATE90 | Rotation::ROTATE270) { + physical_size = physical_size.map(|(width_mm, height_mm)| (height_mm, width_mm)); + } + let dimensions = (crtc.width as u32, crtc.height as u32); let position = (crtc.x as i32, crtc.y as i32); let rect = util::AaRect::new(position, dimensions); - Some(MonitorHandle { id, name, scale_factor, position, primary, rect, video_modes }) + Some(MonitorHandle { + id, + name, + physical_size, + scale_factor, + position, + primary, + rect, + video_modes, + }) } pub fn dummy() -> Self { MonitorHandle { id: 0, name: "".into(), + physical_size: None, scale_factor: 1.0, position: (0, 0), primary: true, diff --git a/src/platform_impl/linux/x11/util/randr.rs b/src/platform_impl/linux/x11/util/randr.rs index 5a2a1c695a..620f86f1aa 100644 --- a/src/platform_impl/linux/x11/util/randr.rs +++ b/src/platform_impl/linux/x11/util/randr.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU16; +use std::num::{NonZeroU16, NonZeroU32}; use std::str::FromStr; use std::{env, str}; @@ -10,6 +10,8 @@ use crate::dpi::validate_scale_factor; use crate::monitor::VideoMode; use crate::platform_impl::platform::x11::{monitor, VideoModeHandle}; +pub type OutputInfo = (String, Option<(NonZeroU32, NonZeroU32)>, f64, Vec); + /// Represents values of `WINIT_HIDPI_FACTOR`. pub enum EnvVarDPI { Randr, @@ -59,7 +61,7 @@ impl XConnection { &self, resources: &monitor::ScreenResources, crtc: &randr::GetCrtcInfoReply, - ) -> Option<(String, f64, Vec)> { + ) -> Option { let output_info = match self .xcb_connection() .randr_get_output_info(crtc.outputs[0], x11rb::CURRENT_TIME) @@ -127,6 +129,10 @@ impl XConnection { }, ); + let physical_size = NonZeroU32::new(output_info.mm_width).and_then(|mm_width| { + NonZeroU32::new(output_info.mm_height).map(|mm_height| (mm_width, mm_height)) + }); + let scale_factor = match dpi_env { EnvVarDPI::Randr => calc_dpi_factor( (crtc.width.into(), crtc.height.into()), @@ -153,7 +159,7 @@ impl XConnection { }, }; - Some((name, scale_factor, modes)) + Some((name, physical_size, scale_factor, modes)) } pub fn set_crtc_config( diff --git a/src/platform_impl/web/monitor.rs b/src/platform_impl/web/monitor.rs index 71f9391f13..315fc52d31 100644 --- a/src/platform_impl/web/monitor.rs +++ b/src/platform_impl/web/monitor.rs @@ -4,7 +4,7 @@ use std::fmt::{self, Debug, Formatter}; use std::future::Future; use std::hash::{Hash, Hasher}; use std::mem; -use std::num::NonZeroU16; +use std::num::{NonZeroU16, NonZeroU32}; use std::ops::{Deref, DerefMut}; use std::pin::Pin; use std::rc::{Rc, Weak}; @@ -128,6 +128,21 @@ impl MonitorHandleProvider for MonitorHandle { self.id.unwrap_or_default() } + fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> { + // NOTE: Browsers expose only CSS-pixel which is different + // from the actual pixel. CSS mm also means logical + // millimeters, and not an actual millimeter and equivalent to + // approximately 3.78px. There are couple of ways to solve + // this problem. First one is to stick with approximation, and + // other is to explicitly ask user. Both are non-ideal, so no + // implementation provided. + // References: + // + // [1] + // [2] + None + } + fn scale_factor(&self) -> f64 { self.inner.queue(|inner| inner.scale_factor()) } diff --git a/src/platform_impl/windows/monitor.rs b/src/platform_impl/windows/monitor.rs index 1e4179df7f..6137767ec4 100644 --- a/src/platform_impl/windows/monitor.rs +++ b/src/platform_impl/windows/monitor.rs @@ -3,6 +3,7 @@ use std::hash::Hash; use std::num::{NonZeroU16, NonZeroU32}; use std::{io, iter, mem, ptr}; +use scopeguard::{guard, ScopeGuard}; use windows_sys::Win32::Foundation::{BOOL, HWND, LPARAM, POINT, RECT}; use windows_sys::Win32::Graphics::Gdi::{ EnumDisplayMonitors, EnumDisplaySettingsExW, GetMonitorInfoW, MonitorFromPoint, @@ -162,6 +163,220 @@ impl MonitorHandle { } } +use std::ffi::OsStr; +use std::mem::zeroed; +use std::os::windows::ffi::OsStrExt; +use std::ptr::null_mut; + +use windows_sys::Win32::Foundation::ERROR_SUCCESS; +use windows_sys::Win32::Graphics::Gdi::{ + EnumDisplayDevicesW, DISPLAY_DEVICEW, DISPLAY_DEVICE_ACTIVE, DISPLAY_DEVICE_ATTACHED, DMDO_270, + DMDO_90, +}; +use windows_sys::Win32::System::Registry::{ + RegCloseKey, RegEnumKeyExW, RegOpenKeyExW, RegQueryValueExW, HKEY, HKEY_LOCAL_MACHINE, KEY_READ, +}; + +fn to_wide(s: &str) -> Vec { + let mut v: Vec = OsStr::new(s).encode_wide().collect(); + v.push(0); + v +} + +unsafe fn get_monitor_device(adapter_name: *const u16) -> Option { + let mut dd_mon: DISPLAY_DEVICEW = unsafe { zeroed() }; + dd_mon.cb = mem::size_of::() as u32; + + // 1. find ACTIVE + ATTACHED + let mut idx = 0; + loop { + let ok = unsafe { EnumDisplayDevicesW(adapter_name, idx, &mut dd_mon, 0) } != 0; + if !ok { + break; + } + if (dd_mon.StateFlags & DISPLAY_DEVICE_ACTIVE) != 0 + && (dd_mon.StateFlags & DISPLAY_DEVICE_ATTACHED) != 0 + { + break; + } + idx += 1; + } + + // 2. fallback to first if no DeviceString + if dd_mon.DeviceString[0] == 0 { + let ok = unsafe { EnumDisplayDevicesW(adapter_name, 0, &mut dd_mon, 0) } != 0; + if !ok || dd_mon.DeviceString[0] == 0 { + let def = to_wide("Default Monitor"); + dd_mon.DeviceString[..def.len()].copy_from_slice(&def); + } + } + + (dd_mon.DeviceID[0] != 0).then_some(dd_mon) +} + +unsafe fn read_size_from_edid(dd: &DISPLAY_DEVICEW) -> Option<(u32, u32)> { + // Parse DeviceID: "DISPLAY\\\\" + let id_buf = &dd.DeviceID; + let len = id_buf.iter().position(|&c| c == 0).unwrap_or(id_buf.len()); + let id_str = String::from_utf16_lossy(&id_buf[..len]); + let mut parts = id_str.split('\\'); + let _ = parts.next(); + let model = parts.next().unwrap_or(""); + let inst = parts.next().unwrap_or(""); + let model = if model.len() > 7 { &model[..7] } else { model }; + + // Open HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY\ + let base = format!("SYSTEM\\CurrentControlSet\\Enum\\DISPLAY\\{model}"); + let base_w = to_wide(&base); + let mut hkey: ScopeGuard = guard(std::ptr::null_mut(), |v| unsafe { + RegCloseKey(v); + }); + if unsafe { + RegOpenKeyExW(HKEY_LOCAL_MACHINE, base_w.as_ptr(), 0, KEY_READ, &mut *hkey) != ERROR_SUCCESS + } { + return None; + } + + // enumerate instances + let mut i = 0; + loop { + let mut name_buf = [0u16; 128]; + let mut name_len = name_buf.len() as u32; + let mut ft = unsafe { mem::zeroed() }; + let r = unsafe { + RegEnumKeyExW( + *hkey, + i, + name_buf.as_mut_ptr(), + &mut name_len, + null_mut(), + null_mut(), + null_mut(), + &mut ft, + ) + }; + if r != ERROR_SUCCESS { + break; + } + i += 1; + + let name = String::from_utf16_lossy(&name_buf[..name_len as usize]); + let subkey = format!("{base}\\{name}"); + let sub_w = to_wide(&subkey); + let mut hkey2: ScopeGuard = guard(std::ptr::null_mut(), |v| unsafe { + RegCloseKey(v); + }); + if unsafe { + RegOpenKeyExW(HKEY_LOCAL_MACHINE, sub_w.as_ptr(), 0, KEY_READ, &mut *hkey2) + != ERROR_SUCCESS + } { + continue; + } + + // Check Driver == inst + let drv_w = to_wide("Driver"); + let mut drv_buf = [0u16; 128]; + let mut drv_len = (drv_buf.len() * 2) as u32; + + if !unsafe { + RegQueryValueExW( + *hkey2, + drv_w.as_ptr(), + null_mut(), + null_mut(), + drv_buf.as_mut_ptr() as *mut u8, + &mut drv_len, + ) == ERROR_SUCCESS + } { + continue; + } + + let got = String::from_utf16_lossy(&drv_buf[..(drv_len as usize / 2)]); + + if !got.starts_with(inst) { + continue; + } + + // Open Device Parameters + let params = format!("{subkey}\\Device Parameters"); + let params_w = to_wide(¶ms); + let mut hkey3: ScopeGuard = guard(std::ptr::null_mut(), |v| unsafe { + RegCloseKey(v); + }); + + if unsafe { + RegOpenKeyExW(HKEY_LOCAL_MACHINE, params_w.as_ptr(), 0, KEY_READ, &mut *hkey3) + != ERROR_SUCCESS + } { + continue; + } + + let edid_w = to_wide("EDID"); + let mut edid = [0u8; 256]; + let mut edid_len = edid.len() as u32; + if unsafe { + RegQueryValueExW( + *hkey3, + edid_w.as_ptr(), + null_mut(), + null_mut(), + edid.as_mut_ptr(), + &mut edid_len, + ) != ERROR_SUCCESS + } || edid_len < 23 + { + continue; + } + let width_mm: u32; + let height_mm: u32; + + // We want to have more detailed resolution than centimeters, + // specifically millimeters. EDID provides Detailed Timing + // Descriptor (DTD) table which can contain desired + // size. There can be up to 4 DTDs in EDID, but the first one + // non-zero-clock DTD is the monitor’s preferred (native) + // mode, so we try only it. + const DTD0: usize = 54; + + let pixel_clock = (edid[DTD0] as u16) | ((edid[DTD0 + 1] as u16) << 8); + + if pixel_clock != 0 { + // For mm precision we need 12-14 bits from Detailed + // Timing Descriptor + // https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#Detailed_Timing_Descriptor. + let h_size_lsb = edid[DTD0 + 12] as u16; + let v_size_lsb = edid[DTD0 + 13] as u16; + let size_msb = edid[DTD0 + 14] as u16; + let h_msb = (size_msb >> 4) & 0x0f; + let v_msb = size_msb & 0x0f; + + width_mm = ((h_msb << 8) | h_size_lsb) as u32; + height_mm = ((v_msb << 8) | v_size_lsb) as u32; + } else { + let width_cm = edid[21] as u32; + let height_cm = edid[22] as u32; + + width_mm = width_cm * 10; + height_mm = height_cm * 10; + } + + return Some((width_mm, height_mm)); + } + + None +} + +fn monitor_physical_size(mi: &MONITORINFOEXW) -> Option<(u32, u32)> { + unsafe { + // adapter_name is the wide-string in MONITORINFOEXW.szDevice + let adapter_name = mi.szDevice.as_ptr(); + // find the matching DISPLAY_DEVICEW + let dd = get_monitor_device(adapter_name)?; + // read EDID + read_size_from_edid(&dd) + } +} + impl MonitorHandleProvider for MonitorHandle { fn id(&self) -> u128 { self.native_id() as _ @@ -185,6 +400,31 @@ impl MonitorHandleProvider for MonitorHandle { .ok() } + fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> { + let monitor_info_ex_w = get_monitor_info(self.0).ok()?; + + let mut physical_size = monitor_physical_size(&monitor_info_ex_w) + .and_then(|(width, height)| Some((NonZeroU32::new(width)?, NonZeroU32::new(height)?))); + + let sz_device = monitor_info_ex_w.szDevice.as_ptr(); + let mut dev_mode_w: DEVMODEW = unsafe { mem::zeroed() }; + dev_mode_w.dmSize = mem::size_of_val(&dev_mode_w) as u16; + + if unsafe { + EnumDisplaySettingsExW(sz_device, ENUM_CURRENT_SETTINGS, &mut dev_mode_w, 0) == 0 + } { + return None; + } + + let display_orientation = unsafe { dev_mode_w.Anonymous1.Anonymous2.dmDisplayOrientation }; + + if matches!(display_orientation, DMDO_90 | DMDO_270) { + physical_size = physical_size.map(|(width, height)| (height, width)); + } + + physical_size + } + fn scale_factor(&self) -> f64 { dpi_to_scale_factor(get_monitor_dpi(self.0).unwrap_or(96)) }