scene_management/
display_metrics.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use fuchsia_scenic::DisplayRotation;
use input_pipeline::Size;
use num_traits::float::FloatConst;

/// Predefined viewing distances with values in millimeters.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum ViewingDistance {
    Handheld = 360,
    Close = 500,
    Near = 720,
    Midrange = 1200,
    Far = 3000,
    Unknown = 600, // Should not be used, but offers a reasonable, non-zero (and unique) default
}

/// [`DisplayMetrics`] encapsulate data associated with a display device.
///
/// [`DisplayMetrics`] are created from a display's width and height in pixels.
/// Pixel density and expected viewing distance can be supplied for more accurate
/// metrics (e.g., [`width_in_mm`] uses the display's pixel density to give the correct width).
///
/// If density or viewing distance is not supplied, default values are calculated based on the
/// display dimensions.
#[derive(Clone, Copy, Debug)]
pub struct DisplayMetrics {
    /// The size of the display in pixels.
    size_in_pixels: Size,

    /// The pixel density of the display. This is either supplied by the client constructing
    /// the display metrics, or a hard-coded default is used based on the display dimensions.
    // TODO(https://fxbug.dev/42165549)
    #[allow(unused)]
    density_in_pixels_per_mm: f32,

    /// The expected viewing distance for the display, in millimeters. For example, a desktop
    /// monitor may have an expected viewing distance of around 500 mm.
    viewing_distance: ViewingDistance,

    /// The screen rotation: 0 (none), 90, 180, or 270.
    display_rotation: DisplayRotation,

    /// The pip scale factor in pixels per pip in either X or Y dimension.
    /// (Assumes square pixels.)
    scale_in_pixels_per_pip: f32,

    /// The pip density in pips per millimeter.
    density_in_pips_per_mm: f32,
}

/// Quantizes the specified floating point number to 8 significant bits of
/// precision in its mantissa (including the implicit leading 1 bit).
///
/// We quantize scale factors to reduce the likelihood of round-off errors in
/// subsequent calculations due to excess precision.  Since IEEE 754 float
/// has 24 significant bits, by using only 8 significant bits for the scaling
/// factor we're guaranteed that we can multiply the factor by any integer
/// between -65793 and 65793 without any loss of precision.  The scaled integers
/// can likewise be added or subtracted without any loss of precision.
fn quantize(f: f32) -> f32 {
    let (frac, exp) = libm::frexpf(f);
    libm::ldexpf((frac as f64 * 256.0).round() as f32, exp - 8)
}

impl DisplayMetrics {
    /// The ideal visual angle of a pip unit in degrees, assuming default settings.
    /// The value has been empirically determined.
    const IDEAL_PIP_VISUAL_ANGLE_DEGREES: f32 = 0.0255;

    /// Creates a new [`DisplayMetrics`] struct.
    ///
    /// The width and height of the display in pixels are required to construct sensible display
    /// metrics. Defaults can be computed for the other metrics, but they may not match expectations.
    ///
    /// For example, a default display pixel density can be determined based on width and height in
    /// pixels, but it's unlikely to match the actual density of the display.
    ///
    /// # Parameters
    /// - `size_in_pixels`: The size of the display, in pixels.
    /// - `density_in_pixels_per_mm`: The density of the display, in pixels per mm. If no density is
    /// provided, a best guess is made based on the width and height of the display.
    /// - `viewing_distance`: The expected viewing distance for the display (i.e., how far away the
    /// user is expected to be from the display) in mm. Defaults to [`DisplayMetrics::DEFAULT_VIEWING_DISTANCE`].
    /// This is used to compute the ratio of pixels per pip.
    /// - `display_rotation`: The rotation of the display, counter-clockwise, in 90-degree increments.
    pub fn new(
        size_in_pixels: Size,
        density_in_pixels_per_mm: Option<f32>,
        viewing_distance: Option<ViewingDistance>,
        display_rotation: Option<DisplayRotation>,
    ) -> DisplayMetrics {
        let mut density_in_pixels_per_mm = density_in_pixels_per_mm
            .unwrap_or_else(|| Self::default_density_in_pixels_per_mm(size_in_pixels));

        if density_in_pixels_per_mm == 0.0 {
            density_in_pixels_per_mm = Self::default_density_in_pixels_per_mm(size_in_pixels);
        }

        let mut viewing_distance =
            viewing_distance.unwrap_or_else(|| Self::default_viewing_distance(size_in_pixels));
        if viewing_distance == ViewingDistance::Unknown {
            viewing_distance = Self::default_viewing_distance(size_in_pixels);
        }
        let viewing_distance_in_mm = viewing_distance as u32 as f32;

        let display_rotation = match display_rotation {
            Some(rotation) => rotation,
            None => DisplayRotation::None,
        };

        assert!(density_in_pixels_per_mm != 0.0);
        assert!(viewing_distance_in_mm != 0.0);

        let scale_in_pixels_per_pip =
            Self::compute_scale(density_in_pixels_per_mm, viewing_distance_in_mm);
        let density_in_pips_per_mm = density_in_pixels_per_mm / scale_in_pixels_per_pip;
        DisplayMetrics {
            size_in_pixels,
            density_in_pixels_per_mm,
            viewing_distance,
            display_rotation,
            scale_in_pixels_per_pip,
            density_in_pips_per_mm,
        }
    }

    /// Computes and returns `scale_in_pixels_per_pip`.
    ///
    /// # Parameters
    /// - `density_in_pixels_per_mm`: The density of the display as given, or the default (see
    /// `new()`).
    /// - `viewing_distance_in_mm`: The expected viewing distance for the display (i.e., how far
    /// away the user is expected to be from the display) as given, or the default (see `new()`).
    ///
    /// Returns the computed scale ratio in pixels per pip.
    fn compute_scale(density_in_pixels_per_mm: f32, viewing_distance_in_mm: f32) -> f32 {
        // Compute the pixel visual size as a function of viewing distance in
        // millimeters per millimeter.
        let pvsize_in_mm_per_mm = 1.0 / (density_in_pixels_per_mm * viewing_distance_in_mm);

        // The adaption factor is an empirically determined fudge factor to take into account
        // human perceptual differences for objects at varying distances, even if those objects
        // are adjusted to be the same size to the eye.
        let adaptation_factor = (viewing_distance_in_mm * 0.5 + 180.0) / viewing_distance_in_mm;

        // Compute the pip visual size as a function of viewing distance in
        // millimeters per millimeter.
        let pip_visual_size_in_mm_per_mm =
            (Self::IDEAL_PIP_VISUAL_ANGLE_DEGREES * f32::PI() / 180.0).tan() * adaptation_factor;

        quantize(pip_visual_size_in_mm_per_mm / pvsize_in_mm_per_mm)
    }

    /// Returns the number of pixels per pip.
    #[inline]
    pub fn pixels_per_pip(&self) -> f32 {
        self.scale_in_pixels_per_pip
    }

    /// Returns the number of pips per millimeter.
    #[inline]
    pub fn pips_per_mm(&self) -> f32 {
        self.density_in_pips_per_mm
    }

    /// Returns the number of millimeters per pip.
    #[inline]
    pub fn mm_per_pip(&self) -> f32 {
        1.0 / self.pips_per_mm()
    }

    /// Returns the width of the display in pixels.
    #[inline]
    pub fn width_in_pixels(&self) -> u32 {
        self.size_in_pixels.width as u32
    }

    /// Returns the height of the display in pixels.
    #[inline]
    pub fn height_in_pixels(&self) -> u32 {
        self.size_in_pixels.height as u32
    }

    /// Returns the size of the display in pixels.
    #[inline]
    pub fn size_in_pixels(&self) -> Size {
        self.size_in_pixels
    }

    /// Returns the width of the display in pips.
    #[inline]
    pub fn width_in_pips(&self) -> f32 {
        self.size_in_pixels.width / self.pixels_per_pip()
    }

    /// Returns the height of the display in pips.
    #[inline]
    pub fn height_in_pips(&self) -> f32 {
        self.size_in_pixels.height / self.pixels_per_pip()
    }

    /// Returns the size of the display in pips.
    #[inline]
    pub fn size_in_pips(&self) -> Size {
        self.size_in_pixels / self.pixels_per_pip()
    }

    /// Returns the width of the display in millimeters.
    #[inline]
    pub fn width_in_mm(&self) -> f32 {
        self.width_in_pips() * self.mm_per_pip()
    }

    /// Returns the height of the display in millimeters.
    #[inline]
    pub fn height_in_mm(&self) -> f32 {
        self.height_in_pips() * self.mm_per_pip()
    }

    /// Returns the size of the display in millimeters.
    #[inline]
    pub fn size_in_mm(&self) -> Size {
        self.size_in_pips() * self.mm_per_pip()
    }

    #[inline]
    pub fn rotation(&self) -> DisplayRotation {
        self.display_rotation
    }

    #[inline]
    pub fn rotation_in_degrees(&self) -> u32 {
        self.display_rotation as u32
    }

    #[inline]
    pub fn viewing_distance(&self) -> ViewingDistance {
        self.viewing_distance
    }

    #[inline]
    pub fn viewing_distance_in_mm(&self) -> f32 {
        self.viewing_distance as u32 as f32
    }

    #[inline]
    pub fn physical_pixel_ratio(&self) -> f32 {
        self.density_in_pixels_per_mm / Self::DEFAULT_DENSITY
    }

    /// The dimensions used to determine whether or not the device dimensions correspond to
    /// an Acer Switch 12 Alpha. Used to set a default display pixel density.
    const ACER_SWITCH_12_ALPHA_DIMENSIONS: (u32, u32) = (2160, 1440);

    /// The dimensions used to determine whether or not the device dimensions correspond to
    /// a Google Pixelbook. Used to set a default display pixel density.
    const GOOGLE_PIXELBOOK_DIMENSIONS: (u32, u32) = (2400, 1600);

    /// The dimensions used to determine whether or not the device dimensions correspond to
    /// a Google Pixelbook Go with a 2K display. Used to set a default display pixel density.
    const GOOGLE_PIXELBOOK_GO_2K_DIMENSIONS: (u32, u32) = (1920, 1080);

    /// The dimensions used to determine whether or not the device dimensions correspond to
    /// a Google Pixelbook Go with a 4K display. Used to set a default display pixel density.
    const GOOGLE_PIXELBOOK_GO_4K_DIMENSIONS: (u32, u32) = (3840, 2160);

    /// The dimensions used to determine whether or not the device dimensions correspond to
    /// a 24 inch monitor. Used to set a default display pixel density.
    const MONITOR_24_IN_DIMENSIONS: (u32, u32) = (1920, 1200);

    /// The dimensions used to determine whether or not the device dimensions correspond to
    /// a 27 inch, 2K monitor. Used to set a default display pixel density.
    const MONITOR_27_IN_2K_DIMENSIONS: (u32, u32) = (2560, 1440);

    /// Display densities are calculated by taking the pixels per inch and dividing that by 25.4
    /// in order to convert that to pixels per millimeter. For example the Google Pixelbook Go is
    /// 166 ppi. The result of converting that to millimeters is 6.53543307087. Rounding that to 4
    /// decimal places is how the value of 6.5354 is calculated.

    /// The display pixel density used for an Acer Switch 12 Alpha.
    const ACER_SWITCH_12_ALPHA_DENSITY: f32 = 8.5;

    /// The display pixel density used for a Google Pixelbook.
    const GOOGLE_PIXELBOOK_DENSITY: f32 = 9.252;

    /// The display pixel density used for a Google Pixelbook Go with a 2K display.
    const GOOGLE_PIXELBOOK_GO_2K_DENSITY: f32 = 4.1725;

    /// The display pixel density used for a Google Pixelbook Go with a 4K display.
    const GOOGLE_PIXELBOOK_GO_4K_DENSITY: f32 = 8.345;

    /// The display pixel density used for a 24 inch monitor.
    const MONITOR_24_IN_DENSITY: f32 = 4.16;

    // TODO(https://fxbug.dev/42119026): Allow Root Presenter clients to specify exact pixel ratio
    /// The display pixel density used for a 27 inch monitor.
    const MONITOR_27_IN_2K_DENSITY: f32 = 5.22;

    // TODO(https://fxbug.dev/42097727): Don't lie.
    /// The display pixel density used as default when no other default device matches.
    /// This results in a logical to physical pixel ratio of 1.0.
    const DEFAULT_DENSITY: f32 = 5.24;

    /// Returns a default display pixel density based on the provided display dimensions.
    ///
    /// The pixel density is defined as pixels per millimeters.
    ///
    /// Clients using a `SceneManager` are expected to provide the pixel density for the display,
    /// but this provides reasonable defaults for a few commonly used devices.
    ///
    /// # Parameters
    /// - `size_in_pixels`: The size of the display in pixels.
    fn default_density_in_pixels_per_mm(size_in_pixels: Size) -> f32 {
        match (size_in_pixels.width as u32, size_in_pixels.height as u32) {
            DisplayMetrics::ACER_SWITCH_12_ALPHA_DIMENSIONS => {
                DisplayMetrics::ACER_SWITCH_12_ALPHA_DENSITY
            }
            DisplayMetrics::GOOGLE_PIXELBOOK_DIMENSIONS => DisplayMetrics::GOOGLE_PIXELBOOK_DENSITY,
            DisplayMetrics::GOOGLE_PIXELBOOK_GO_2K_DIMENSIONS => {
                DisplayMetrics::GOOGLE_PIXELBOOK_GO_2K_DENSITY
            }
            DisplayMetrics::GOOGLE_PIXELBOOK_GO_4K_DIMENSIONS => {
                DisplayMetrics::GOOGLE_PIXELBOOK_GO_4K_DENSITY
            }
            DisplayMetrics::MONITOR_24_IN_DIMENSIONS => DisplayMetrics::MONITOR_24_IN_DENSITY,
            DisplayMetrics::MONITOR_27_IN_2K_DIMENSIONS => DisplayMetrics::MONITOR_27_IN_2K_DENSITY,
            _ => DisplayMetrics::DEFAULT_DENSITY,
        }
    }

    fn default_viewing_distance(size_in_pixels: Size) -> ViewingDistance {
        match (size_in_pixels.width as u32, size_in_pixels.height as u32) {
            DisplayMetrics::ACER_SWITCH_12_ALPHA_DIMENSIONS => ViewingDistance::Close,
            DisplayMetrics::GOOGLE_PIXELBOOK_DIMENSIONS => ViewingDistance::Close,
            DisplayMetrics::GOOGLE_PIXELBOOK_GO_2K_DIMENSIONS => ViewingDistance::Near,
            DisplayMetrics::GOOGLE_PIXELBOOK_GO_4K_DIMENSIONS => ViewingDistance::Near,
            DisplayMetrics::MONITOR_24_IN_DIMENSIONS => ViewingDistance::Near,
            DisplayMetrics::MONITOR_27_IN_2K_DIMENSIONS => ViewingDistance::Near,
            _ => ViewingDistance::Close,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Density is used as the denominator in pip calculation, so must be handled explicitly.
    #[test]
    fn test_zero_density() {
        let metrics =
            DisplayMetrics::new(Size { width: 100.0, height: 100.0 }, Some(0.0), None, None);
        let second_metrics =
            DisplayMetrics::new(Size { width: 100.0, height: 100.0 }, None, None, None);
        assert_eq!(metrics.width_in_pips(), second_metrics.width_in_pips());
        assert_eq!(metrics.height_in_pips(), second_metrics.height_in_pips());
    }

    // Viewing distance is used as the denominator in pip calculation, so must be handled explicitly.
    #[test]
    fn test_zero_distance() {
        let metrics = DisplayMetrics::new(
            Size { width: 100.0, height: 100.0 },
            None,
            Some(ViewingDistance::Unknown),
            None,
        );
        let second_metrics =
            DisplayMetrics::new(Size { width: 100.0, height: 100.0 }, None, None, None);
        assert_eq!(metrics.width_in_pips(), second_metrics.width_in_pips());
        assert_eq!(metrics.height_in_pips(), second_metrics.height_in_pips());
    }

    // Tests that a known default density produces the same metrics as explicitly specified.
    #[test]
    fn test_pixels_per_pip_default() {
        let dimensions = DisplayMetrics::ACER_SWITCH_12_ALPHA_DIMENSIONS;
        let metrics = DisplayMetrics::new(
            Size { width: dimensions.0 as f32, height: dimensions.1 as f32 },
            None,
            None,
            None,
        );
        let second_metrics = DisplayMetrics::new(
            Size { width: dimensions.0 as f32, height: dimensions.1 as f32 },
            Some(DisplayMetrics::ACER_SWITCH_12_ALPHA_DENSITY),
            Some(ViewingDistance::Close),
            None,
        );
        assert_eq!(metrics.width_in_pips(), second_metrics.width_in_pips());
        assert_eq!(metrics.height_in_pips(), second_metrics.height_in_pips());

        // The expected values here were generated and tested manually to be the expected
        // values for the Acer Switch 12 Alpha.
        assert_eq!(metrics.width_in_pips(), 1329.2307);
        assert_eq!(metrics.height_in_pips(), 886.1539);
    }
}