blob: a17d1c1fd313e804c752776757b5ca35ad487d84 [file] [log] [blame]
// 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);
}
}