scene_management/display_metrics.rs
1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fuchsia_scenic::DisplayRotation;
6use input_pipeline::Size;
7use num_traits::float::FloatConst;
8
9/// Predefined viewing distances with values in millimeters.
10#[derive(Copy, Clone, PartialEq, Debug)]
11pub enum ViewingDistance {
12 Handheld = 360,
13 Close = 500,
14 Near = 720,
15 Midrange = 1200,
16 Far = 3000,
17 Unknown = 600, // Should not be used, but offers a reasonable, non-zero (and unique) default
18}
19
20/// [`DisplayMetrics`] encapsulate data associated with a display device.
21///
22/// [`DisplayMetrics`] are created from a display's width and height in pixels.
23/// Pixel density and expected viewing distance can be supplied for more accurate
24/// metrics (e.g., [`width_in_mm`] uses the display's pixel density to give the correct width).
25///
26/// If density or viewing distance is not supplied, default values are calculated based on the
27/// display dimensions.
28#[derive(Clone, Copy, Debug)]
29pub struct DisplayMetrics {
30 /// The size of the display in pixels.
31 size_in_pixels: Size,
32
33 /// The pixel density of the display. This is either supplied by the client constructing
34 /// the display metrics, or a hard-coded default is used based on the display dimensions.
35 // TODO(https://fxbug.dev/42165549)
36 #[allow(unused)]
37 density_in_pixels_per_mm: f32,
38
39 /// The expected viewing distance for the display, in millimeters. For example, a desktop
40 /// monitor may have an expected viewing distance of around 500 mm.
41 viewing_distance: ViewingDistance,
42
43 /// The screen rotation: 0 (none), 90, 180, or 270.
44 display_rotation: DisplayRotation,
45
46 /// The pip scale factor in pixels per pip in either X or Y dimension.
47 /// (Assumes square pixels.)
48 scale_in_pixels_per_pip: f32,
49
50 /// The pip density in pips per millimeter.
51 density_in_pips_per_mm: f32,
52}
53
54/// Quantizes the specified floating point number to 8 significant bits of
55/// precision in its mantissa (including the implicit leading 1 bit).
56///
57/// We quantize scale factors to reduce the likelihood of round-off errors in
58/// subsequent calculations due to excess precision. Since IEEE 754 float
59/// has 24 significant bits, by using only 8 significant bits for the scaling
60/// factor we're guaranteed that we can multiply the factor by any integer
61/// between -65793 and 65793 without any loss of precision. The scaled integers
62/// can likewise be added or subtracted without any loss of precision.
63fn quantize(f: f32) -> f32 {
64 let (frac, exp) = libm::frexpf(f);
65 libm::ldexpf((frac as f64 * 256.0).round() as f32, exp - 8)
66}
67
68impl DisplayMetrics {
69 /// The ideal visual angle of a pip unit in degrees, assuming default settings.
70 /// The value has been empirically determined.
71 const IDEAL_PIP_VISUAL_ANGLE_DEGREES: f32 = 0.0255;
72
73 /// Creates a new [`DisplayMetrics`] struct.
74 ///
75 /// The width and height of the display in pixels are required to construct sensible display
76 /// metrics. Defaults can be computed for the other metrics, but they may not match expectations.
77 ///
78 /// For example, a default display pixel density can be determined based on width and height in
79 /// pixels, but it's unlikely to match the actual density of the display.
80 ///
81 /// # Parameters
82 /// - `size_in_pixels`: The size of the display, in pixels.
83 /// - `density_in_pixels_per_mm`: The density of the display, in pixels per mm. If no density is
84 /// provided, a best guess is made based on the width and height of the display.
85 /// - `viewing_distance`: The expected viewing distance for the display (i.e., how far away the
86 /// user is expected to be from the display) in mm. Defaults to [`DisplayMetrics::DEFAULT_VIEWING_DISTANCE`].
87 /// This is used to compute the ratio of pixels per pip.
88 /// - `display_rotation`: The rotation of the display, counter-clockwise, in 90-degree increments.
89 pub fn new(
90 size_in_pixels: Size,
91 density_in_pixels_per_mm: Option<f32>,
92 viewing_distance: Option<ViewingDistance>,
93 display_rotation: Option<DisplayRotation>,
94 ) -> DisplayMetrics {
95 let mut density_in_pixels_per_mm = density_in_pixels_per_mm
96 .unwrap_or_else(|| Self::default_density_in_pixels_per_mm(size_in_pixels));
97
98 if density_in_pixels_per_mm == 0.0 {
99 density_in_pixels_per_mm = Self::default_density_in_pixels_per_mm(size_in_pixels);
100 }
101
102 let mut viewing_distance =
103 viewing_distance.unwrap_or_else(|| Self::default_viewing_distance(size_in_pixels));
104 if viewing_distance == ViewingDistance::Unknown {
105 viewing_distance = Self::default_viewing_distance(size_in_pixels);
106 }
107 let viewing_distance_in_mm = viewing_distance as u32 as f32;
108
109 let display_rotation = match display_rotation {
110 Some(rotation) => rotation,
111 None => DisplayRotation::None,
112 };
113
114 assert!(density_in_pixels_per_mm != 0.0);
115 assert!(viewing_distance_in_mm != 0.0);
116
117 let scale_in_pixels_per_pip =
118 Self::compute_scale(density_in_pixels_per_mm, viewing_distance_in_mm);
119 let density_in_pips_per_mm = density_in_pixels_per_mm / scale_in_pixels_per_pip;
120 DisplayMetrics {
121 size_in_pixels,
122 density_in_pixels_per_mm,
123 viewing_distance,
124 display_rotation,
125 scale_in_pixels_per_pip,
126 density_in_pips_per_mm,
127 }
128 }
129
130 /// Computes and returns `scale_in_pixels_per_pip`.
131 ///
132 /// # Parameters
133 /// - `density_in_pixels_per_mm`: The density of the display as given, or the default (see
134 /// `new()`).
135 /// - `viewing_distance_in_mm`: The expected viewing distance for the display (i.e., how far
136 /// away the user is expected to be from the display) as given, or the default (see `new()`).
137 ///
138 /// Returns the computed scale ratio in pixels per pip.
139 fn compute_scale(density_in_pixels_per_mm: f32, viewing_distance_in_mm: f32) -> f32 {
140 // Compute the pixel visual size as a function of viewing distance in
141 // millimeters per millimeter.
142 let pvsize_in_mm_per_mm = 1.0 / (density_in_pixels_per_mm * viewing_distance_in_mm);
143
144 // The adaption factor is an empirically determined fudge factor to take into account
145 // human perceptual differences for objects at varying distances, even if those objects
146 // are adjusted to be the same size to the eye.
147 let adaptation_factor = (viewing_distance_in_mm * 0.5 + 180.0) / viewing_distance_in_mm;
148
149 // Compute the pip visual size as a function of viewing distance in
150 // millimeters per millimeter.
151 let pip_visual_size_in_mm_per_mm =
152 (Self::IDEAL_PIP_VISUAL_ANGLE_DEGREES * f32::PI() / 180.0).tan() * adaptation_factor;
153
154 quantize(pip_visual_size_in_mm_per_mm / pvsize_in_mm_per_mm)
155 }
156
157 /// Returns the number of pixels per pip.
158 #[inline]
159 pub fn pixels_per_pip(&self) -> f32 {
160 self.scale_in_pixels_per_pip
161 }
162
163 /// Returns the number of pips per millimeter.
164 #[inline]
165 pub fn pips_per_mm(&self) -> f32 {
166 self.density_in_pips_per_mm
167 }
168
169 /// Returns the number of millimeters per pip.
170 #[inline]
171 pub fn mm_per_pip(&self) -> f32 {
172 1.0 / self.pips_per_mm()
173 }
174
175 /// Returns the width of the display in pixels.
176 #[inline]
177 pub fn width_in_pixels(&self) -> u32 {
178 self.size_in_pixels.width as u32
179 }
180
181 /// Returns the height of the display in pixels.
182 #[inline]
183 pub fn height_in_pixels(&self) -> u32 {
184 self.size_in_pixels.height as u32
185 }
186
187 /// Returns the size of the display in pixels.
188 #[inline]
189 pub fn size_in_pixels(&self) -> Size {
190 self.size_in_pixels
191 }
192
193 /// Returns the width of the display in pips.
194 #[inline]
195 pub fn width_in_pips(&self) -> f32 {
196 self.size_in_pixels.width / self.pixels_per_pip()
197 }
198
199 /// Returns the height of the display in pips.
200 #[inline]
201 pub fn height_in_pips(&self) -> f32 {
202 self.size_in_pixels.height / self.pixels_per_pip()
203 }
204
205 /// Returns the size of the display in pips.
206 #[inline]
207 pub fn size_in_pips(&self) -> Size {
208 self.size_in_pixels / self.pixels_per_pip()
209 }
210
211 /// Returns the width of the display in millimeters.
212 #[inline]
213 pub fn width_in_mm(&self) -> f32 {
214 self.width_in_pips() * self.mm_per_pip()
215 }
216
217 /// Returns the height of the display in millimeters.
218 #[inline]
219 pub fn height_in_mm(&self) -> f32 {
220 self.height_in_pips() * self.mm_per_pip()
221 }
222
223 /// Returns the size of the display in millimeters.
224 #[inline]
225 pub fn size_in_mm(&self) -> Size {
226 self.size_in_pips() * self.mm_per_pip()
227 }
228
229 #[inline]
230 pub fn rotation(&self) -> DisplayRotation {
231 self.display_rotation
232 }
233
234 #[inline]
235 pub fn rotation_in_degrees(&self) -> u32 {
236 self.display_rotation as u32
237 }
238
239 #[inline]
240 pub fn viewing_distance(&self) -> ViewingDistance {
241 self.viewing_distance
242 }
243
244 #[inline]
245 pub fn viewing_distance_in_mm(&self) -> f32 {
246 self.viewing_distance as u32 as f32
247 }
248
249 #[inline]
250 pub fn physical_pixel_ratio(&self) -> f32 {
251 self.density_in_pixels_per_mm / Self::DEFAULT_DENSITY
252 }
253
254 /// The dimensions used to determine whether or not the device dimensions correspond to
255 /// an Acer Switch 12 Alpha. Used to set a default display pixel density.
256 const ACER_SWITCH_12_ALPHA_DIMENSIONS: (u32, u32) = (2160, 1440);
257
258 /// The dimensions used to determine whether or not the device dimensions correspond to
259 /// a Google Pixelbook. Used to set a default display pixel density.
260 const GOOGLE_PIXELBOOK_DIMENSIONS: (u32, u32) = (2400, 1600);
261
262 /// The dimensions used to determine whether or not the device dimensions correspond to
263 /// a Google Pixelbook Go with a 2K display. Used to set a default display pixel density.
264 const GOOGLE_PIXELBOOK_GO_2K_DIMENSIONS: (u32, u32) = (1920, 1080);
265
266 /// The dimensions used to determine whether or not the device dimensions correspond to
267 /// a Google Pixelbook Go with a 4K display. Used to set a default display pixel density.
268 const GOOGLE_PIXELBOOK_GO_4K_DIMENSIONS: (u32, u32) = (3840, 2160);
269
270 /// The dimensions used to determine whether or not the device dimensions correspond to
271 /// a 24 inch monitor. Used to set a default display pixel density.
272 const MONITOR_24_IN_DIMENSIONS: (u32, u32) = (1920, 1200);
273
274 /// The dimensions used to determine whether or not the device dimensions correspond to
275 /// a 27 inch, 2K monitor. Used to set a default display pixel density.
276 const MONITOR_27_IN_2K_DIMENSIONS: (u32, u32) = (2560, 1440);
277
278 /// Display densities are calculated by taking the pixels per inch and dividing that by 25.4
279 /// in order to convert that to pixels per millimeter. For example the Google Pixelbook Go is
280 /// 166 ppi. The result of converting that to millimeters is 6.53543307087. Rounding that to 4
281 /// decimal places is how the value of 6.5354 is calculated.
282
283 /// The display pixel density used for an Acer Switch 12 Alpha.
284 const ACER_SWITCH_12_ALPHA_DENSITY: f32 = 8.5;
285
286 /// The display pixel density used for a Google Pixelbook.
287 const GOOGLE_PIXELBOOK_DENSITY: f32 = 9.252;
288
289 /// The display pixel density used for a Google Pixelbook Go with a 2K display.
290 const GOOGLE_PIXELBOOK_GO_2K_DENSITY: f32 = 4.1725;
291
292 /// The display pixel density used for a Google Pixelbook Go with a 4K display.
293 const GOOGLE_PIXELBOOK_GO_4K_DENSITY: f32 = 8.345;
294
295 /// The display pixel density used for a 24 inch monitor.
296 const MONITOR_24_IN_DENSITY: f32 = 4.16;
297
298 // TODO(https://fxbug.dev/42119026): Allow Root Presenter clients to specify exact pixel ratio
299 /// The display pixel density used for a 27 inch monitor.
300 const MONITOR_27_IN_2K_DENSITY: f32 = 5.22;
301
302 // TODO(https://fxbug.dev/42097727): Don't lie.
303 /// The display pixel density used as default when no other default device matches.
304 /// This results in a logical to physical pixel ratio of 1.0.
305 const DEFAULT_DENSITY: f32 = 5.24;
306
307 /// Returns a default display pixel density based on the provided display dimensions.
308 ///
309 /// The pixel density is defined as pixels per millimeters.
310 ///
311 /// Clients using a `SceneManager` are expected to provide the pixel density for the display,
312 /// but this provides reasonable defaults for a few commonly used devices.
313 ///
314 /// # Parameters
315 /// - `size_in_pixels`: The size of the display in pixels.
316 fn default_density_in_pixels_per_mm(size_in_pixels: Size) -> f32 {
317 match (size_in_pixels.width as u32, size_in_pixels.height as u32) {
318 DisplayMetrics::ACER_SWITCH_12_ALPHA_DIMENSIONS => {
319 DisplayMetrics::ACER_SWITCH_12_ALPHA_DENSITY
320 }
321 DisplayMetrics::GOOGLE_PIXELBOOK_DIMENSIONS => DisplayMetrics::GOOGLE_PIXELBOOK_DENSITY,
322 DisplayMetrics::GOOGLE_PIXELBOOK_GO_2K_DIMENSIONS => {
323 DisplayMetrics::GOOGLE_PIXELBOOK_GO_2K_DENSITY
324 }
325 DisplayMetrics::GOOGLE_PIXELBOOK_GO_4K_DIMENSIONS => {
326 DisplayMetrics::GOOGLE_PIXELBOOK_GO_4K_DENSITY
327 }
328 DisplayMetrics::MONITOR_24_IN_DIMENSIONS => DisplayMetrics::MONITOR_24_IN_DENSITY,
329 DisplayMetrics::MONITOR_27_IN_2K_DIMENSIONS => DisplayMetrics::MONITOR_27_IN_2K_DENSITY,
330 _ => DisplayMetrics::DEFAULT_DENSITY,
331 }
332 }
333
334 fn default_viewing_distance(size_in_pixels: Size) -> ViewingDistance {
335 match (size_in_pixels.width as u32, size_in_pixels.height as u32) {
336 DisplayMetrics::ACER_SWITCH_12_ALPHA_DIMENSIONS => ViewingDistance::Close,
337 DisplayMetrics::GOOGLE_PIXELBOOK_DIMENSIONS => ViewingDistance::Close,
338 DisplayMetrics::GOOGLE_PIXELBOOK_GO_2K_DIMENSIONS => ViewingDistance::Near,
339 DisplayMetrics::GOOGLE_PIXELBOOK_GO_4K_DIMENSIONS => ViewingDistance::Near,
340 DisplayMetrics::MONITOR_24_IN_DIMENSIONS => ViewingDistance::Near,
341 DisplayMetrics::MONITOR_27_IN_2K_DIMENSIONS => ViewingDistance::Near,
342 _ => ViewingDistance::Close,
343 }
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 // Density is used as the denominator in pip calculation, so must be handled explicitly.
352 #[test]
353 fn test_zero_density() {
354 let metrics =
355 DisplayMetrics::new(Size { width: 100.0, height: 100.0 }, Some(0.0), None, None);
356 let second_metrics =
357 DisplayMetrics::new(Size { width: 100.0, height: 100.0 }, None, None, None);
358 assert_eq!(metrics.width_in_pips(), second_metrics.width_in_pips());
359 assert_eq!(metrics.height_in_pips(), second_metrics.height_in_pips());
360 }
361
362 // Viewing distance is used as the denominator in pip calculation, so must be handled explicitly.
363 #[test]
364 fn test_zero_distance() {
365 let metrics = DisplayMetrics::new(
366 Size { width: 100.0, height: 100.0 },
367 None,
368 Some(ViewingDistance::Unknown),
369 None,
370 );
371 let second_metrics =
372 DisplayMetrics::new(Size { width: 100.0, height: 100.0 }, None, None, None);
373 assert_eq!(metrics.width_in_pips(), second_metrics.width_in_pips());
374 assert_eq!(metrics.height_in_pips(), second_metrics.height_in_pips());
375 }
376
377 // Tests that a known default density produces the same metrics as explicitly specified.
378 #[test]
379 fn test_pixels_per_pip_default() {
380 let dimensions = DisplayMetrics::ACER_SWITCH_12_ALPHA_DIMENSIONS;
381 let metrics = DisplayMetrics::new(
382 Size { width: dimensions.0 as f32, height: dimensions.1 as f32 },
383 None,
384 None,
385 None,
386 );
387 let second_metrics = DisplayMetrics::new(
388 Size { width: dimensions.0 as f32, height: dimensions.1 as f32 },
389 Some(DisplayMetrics::ACER_SWITCH_12_ALPHA_DENSITY),
390 Some(ViewingDistance::Close),
391 None,
392 );
393 assert_eq!(metrics.width_in_pips(), second_metrics.width_in_pips());
394 assert_eq!(metrics.height_in_pips(), second_metrics.height_in_pips());
395
396 // The expected values here were generated and tested manually to be the expected
397 // values for the Acer Switch 12 Alpha.
398 assert_eq!(metrics.width_in_pips(), 1329.2307);
399 assert_eq!(metrics.height_in_pips(), 886.1539);
400 }
401}