Skip to content

Commit 49c1cb9

Browse files
committed
Expose monitor physical size
Currently only X11 and Wayland are supported.
1 parent 587ade8 commit 49c1cb9

File tree

10 files changed

+356
-8
lines changed

10 files changed

+356
-8
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ objc2-ui-kit = { version = "0.3.1", default-features = false, features = [
242242
# Windows
243243
[target.'cfg(target_os = "windows")'.dependencies]
244244
unicode-segmentation = "1.7.1"
245+
scopeguard = "1.2.0"
245246
windows-sys = { version = "0.59.0", features = [
246247
"Win32_Devices_HumanInterfaceDevice",
247248
"Win32_Foundation",
@@ -254,6 +255,7 @@ windows-sys = { version = "0.59.0", features = [
254255
"Win32_System_LibraryLoader",
255256
"Win32_System_Ole",
256257
"Win32_Security",
258+
"Win32_System_Registry",
257259
"Win32_System_SystemInformation",
258260
"Win32_System_SystemServices",
259261
"Win32_System_Threading",

src/changelog/unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ changelog entry.
4242

4343
### Added
4444

45+
- Add `MonitorHandle::physical_size()` that returns monitor physical size in millimeters.
4546
- Add `ActiveEventLoop::create_proxy()`.
4647
- On Web, add `ActiveEventLoopExtWeb::is_cursor_lock_raw()` to determine if
4748
`DeviceEvent::MouseMotion` is returning raw data, not OS accelerated, when using

src/monitor.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,27 @@ pub trait MonitorHandleProvider: AsAny + fmt::Debug + Send + Sync {
113113
#[cfg_attr(not(web_platform), doc = "detailed monitor permissions.")]
114114
fn position(&self) -> Option<PhysicalPosition<i32>>;
115115

116+
/// Returns physical size of monitor in millimeters, where the
117+
/// first field of a tuple is width and the second is height.
118+
///
119+
/// Returns `None` if the size of the monitor could not be determined.
120+
///
121+
/// Should be used with care, since not all corner cases may be handled.
122+
///
123+
/// If monitor is rotated by 90° or 270°, width and height will be swapped.
124+
///
125+
/// Every major platform gets this data from
126+
/// [EDID](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data),
127+
/// which contains display dimensions in centimeters, and
128+
/// optionally higher precision in millimeters. Therefore, by
129+
/// convention, we also return millimeters.
130+
///
131+
/// ## Platform-specific
132+
///
133+
/// - **Wayland:** Has centimeter precision.
134+
/// - **iOS / Web / Android / Redox:** Unimplemented, always returns [`None`].
135+
fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)>;
136+
116137
/// Returns the scale factor of the underlying monitor. To map logical pixels to physical
117138
/// pixels and vice versa, use [`Window::scale_factor`].
118139
///

src/platform_impl/apple/appkit/monitor.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use objc2_app_kit::NSScreen;
1212
use objc2_core_foundation::{CFArray, CFRetained, CFUUID};
1313
use objc2_core_graphics::{
1414
CGDirectDisplayID, CGDisplayBounds, CGDisplayCopyAllDisplayModes, CGDisplayCopyDisplayMode,
15-
CGDisplayMode, CGDisplayModelNumber, CGGetActiveDisplayList, CGMainDisplayID,
15+
CGDisplayMode, CGDisplayModelNumber, CGDisplayScreenSize, CGGetActiveDisplayList,
16+
CGMainDisplayID,
1617
};
1718
use objc2_core_video::{kCVReturnSuccess, CVDisplayLink, CVTimeFlags};
1819
use objc2_foundation::{ns_string, NSNumber, NSPoint, NSRect};
@@ -211,6 +212,18 @@ impl MonitorHandleProvider for MonitorHandle {
211212
Some(position.to_physical(self.scale_factor()))
212213
}
213214

215+
fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> {
216+
let size_float = unsafe { CGDisplayScreenSize(self.display_id()) };
217+
if size_float.width > 0.0 && size_float.height > 0.0 {
218+
Some((
219+
NonZeroU32::new(size_float.width.round() as u32)?,
220+
NonZeroU32::new(size_float.height.round() as u32)?,
221+
))
222+
} else {
223+
None
224+
}
225+
}
226+
214227
fn scale_factor(&self) -> f64 {
215228
run_on_main(|mtm| {
216229
match self.ns_screen(mtm) {

src/platform_impl/apple/uikit/monitor.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ impl MonitorHandleProvider for MonitorHandle {
110110
Some((bounds.origin.x as f64, bounds.origin.y as f64).into())
111111
}
112112

113+
fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> {
114+
// NOTE: There is no way to query the PPI on iOS.
115+
// TODO(madsmtm): Use a hardcoded mapping of device models to PPI.
116+
// <https://stackoverflow.com/a/28573791>
117+
None
118+
}
119+
113120
fn scale_factor(&self) -> f64 {
114121
self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeScale()) as f64
115122
}

src/platform_impl/linux/wayland/output.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ impl CoreMonitorHandle for MonitorHandle {
5151
}))
5252
}
5353

54+
fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> {
55+
let output_data = self.proxy.data::<OutputData>().unwrap();
56+
let (width_mm, height_mm) = output_data.with_output_info(|oi| oi.physical_size);
57+
58+
Some((
59+
NonZeroU32::new(width_mm.try_into().ok()?)?,
60+
NonZeroU32::new(height_mm.try_into().ok()?)?,
61+
))
62+
}
63+
5464
fn scale_factor(&self) -> f64 {
5565
let output_data = self.proxy.data::<OutputData>().unwrap();
5666
output_data.scale_factor() as f64

src/platform_impl/linux/x11/monitor.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::num::NonZeroU32;
22

33
use x11rb::connection::RequestConnection;
4-
use x11rb::protocol::randr::{self, ConnectionExt as _};
4+
use x11rb::protocol::randr::{self, ConnectionExt as _, Rotation};
55
use x11rb::protocol::xproto;
66

77
use super::{util, X11Error, XConnection};
@@ -41,6 +41,8 @@ pub struct MonitorHandle {
4141
pub(crate) position: (i32, i32),
4242
/// If the monitor is the primary one
4343
primary: bool,
44+
/// The physical size of a monitor in millimeters.
45+
pub(crate) physical_size: Option<(NonZeroU32, NonZeroU32)>,
4446
/// The DPI scale factor
4547
pub(crate) scale_factor: f64,
4648
/// Used to determine which windows are on this monitor
@@ -66,6 +68,10 @@ impl MonitorHandleProvider for MonitorHandle {
6668
Some(self.position.into())
6769
}
6870

71+
fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> {
72+
self.physical_size
73+
}
74+
6975
fn scale_factor(&self) -> f64 {
7076
self.scale_factor
7177
}
@@ -125,19 +131,46 @@ impl MonitorHandle {
125131
crtc: &randr::GetCrtcInfoReply,
126132
primary: bool,
127133
) -> Option<Self> {
128-
let (name, scale_factor, video_modes) = xconn.get_output_info(resources, crtc)?;
134+
let (name, mut physical_size, scale_factor, video_modes) =
135+
xconn.get_output_info(resources, crtc)?;
136+
137+
let rotation = xconn
138+
.xcb_connection()
139+
.randr_get_crtc_info(id, x11rb::CURRENT_TIME)
140+
.ok()?
141+
.reply()
142+
.ok()?
143+
.rotation;
144+
145+
// By default, X11 window system is the only that does not
146+
// swap width and height when display is rotated. To match
147+
// behaviour with Windows and MacOS we do this manually.
148+
if matches!(rotation, Rotation::ROTATE90 | Rotation::ROTATE270) {
149+
physical_size = physical_size.map(|(width_mm, height_mm)| (height_mm, width_mm));
150+
}
151+
129152
let dimensions = (crtc.width as u32, crtc.height as u32);
130153
let position = (crtc.x as i32, crtc.y as i32);
131154

132155
let rect = util::AaRect::new(position, dimensions);
133156

134-
Some(MonitorHandle { id, name, scale_factor, position, primary, rect, video_modes })
157+
Some(MonitorHandle {
158+
id,
159+
name,
160+
physical_size,
161+
scale_factor,
162+
position,
163+
primary,
164+
rect,
165+
video_modes,
166+
})
135167
}
136168

137169
pub fn dummy() -> Self {
138170
MonitorHandle {
139171
id: 0,
140172
name: "<dummy monitor>".into(),
173+
physical_size: None,
141174
scale_factor: 1.0,
142175
position: (0, 0),
143176
primary: true,

src/platform_impl/linux/x11/util/randr.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::num::NonZeroU16;
1+
use std::num::{NonZeroU16, NonZeroU32};
22
use std::str::FromStr;
33
use std::{env, str};
44

@@ -10,6 +10,8 @@ use crate::dpi::validate_scale_factor;
1010
use crate::monitor::VideoMode;
1111
use crate::platform_impl::platform::x11::{monitor, VideoModeHandle};
1212

13+
pub type OutputInfo = (String, Option<(NonZeroU32, NonZeroU32)>, f64, Vec<VideoModeHandle>);
14+
1315
/// Represents values of `WINIT_HIDPI_FACTOR`.
1416
pub enum EnvVarDPI {
1517
Randr,
@@ -59,7 +61,7 @@ impl XConnection {
5961
&self,
6062
resources: &monitor::ScreenResources,
6163
crtc: &randr::GetCrtcInfoReply,
62-
) -> Option<(String, f64, Vec<VideoModeHandle>)> {
64+
) -> Option<OutputInfo> {
6365
let output_info = match self
6466
.xcb_connection()
6567
.randr_get_output_info(crtc.outputs[0], x11rb::CURRENT_TIME)
@@ -127,6 +129,10 @@ impl XConnection {
127129
},
128130
);
129131

132+
let physical_size = NonZeroU32::new(output_info.mm_width).and_then(|mm_width| {
133+
NonZeroU32::new(output_info.mm_height).map(|mm_height| (mm_width, mm_height))
134+
});
135+
130136
let scale_factor = match dpi_env {
131137
EnvVarDPI::Randr => calc_dpi_factor(
132138
(crtc.width.into(), crtc.height.into()),
@@ -153,7 +159,7 @@ impl XConnection {
153159
},
154160
};
155161

156-
Some((name, scale_factor, modes))
162+
Some((name, physical_size, scale_factor, modes))
157163
}
158164

159165
pub fn set_crtc_config(

src/platform_impl/web/monitor.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::fmt::{self, Debug, Formatter};
44
use std::future::Future;
55
use std::hash::{Hash, Hasher};
66
use std::mem;
7-
use std::num::NonZeroU16;
7+
use std::num::{NonZeroU16, NonZeroU32};
88
use std::ops::{Deref, DerefMut};
99
use std::pin::Pin;
1010
use std::rc::{Rc, Weak};
@@ -128,6 +128,21 @@ impl MonitorHandleProvider for MonitorHandle {
128128
self.id.unwrap_or_default()
129129
}
130130

131+
fn physical_size(&self) -> Option<(NonZeroU32, NonZeroU32)> {
132+
// NOTE: Browsers expose only CSS-pixel which is different
133+
// from the actual pixel. CSS mm also means logical
134+
// millimeters, and not an actual millimeter and equivalent to
135+
// approximately 3.78px. There are couple of ways to solve
136+
// this problem. First one is to stick with approximation, and
137+
// other is to explicitly ask user. Both are non-ideal, so no
138+
// implementation provided.
139+
// References:
140+
//
141+
// [1] <https://developer.mozilla.org/en-US/docs/Web/CSS/length#px>
142+
// [2] <https://stackoverflow.com/questions/75533347/how-can-i-get-the-physical-size-of-a-pixel-in-millimeters-at-a-specific-window>
143+
None
144+
}
145+
131146
fn scale_factor(&self) -> f64 {
132147
self.inner.queue(|inner| inner.scale_factor())
133148
}

0 commit comments

Comments
 (0)