pub const CSS_DPI: f64 = 96.0;
const MIN_PX: u32 = 32;
const MAX_PX: u32 = 16_384;
const MIN_RATIO: f64 = 0.5;
const MAX_RATIO: f64 = 4.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Canvas {
pub width: u32,
pub height: u32,
pub dpi: f64,
}
impl Canvas {
pub fn from_logical(width: f64, height: f64, pixel_ratio: f64) -> Self {
let ratio = if pixel_ratio.is_finite() {
pixel_ratio.clamp(MIN_RATIO, MAX_RATIO)
} else {
1.0
};
Self {
width: clamp_px(width * ratio),
height: clamp_px(height * ratio),
dpi: CSS_DPI * ratio,
}
}
pub fn from_inches(width: f64, height: f64, dpi: f64) -> Self {
let dpi = if dpi.is_finite() && dpi > 0.0 {
dpi
} else {
CSS_DPI
};
Self {
width: clamp_px(width * dpi),
height: clamp_px(height * dpi),
dpi,
}
}
pub fn default_at(dpi: f64) -> Self {
Self::from_inches(DEFAULT_WIDTH_IN, DEFAULT_HEIGHT_IN, dpi)
}
pub fn css_size(&self) -> (u32, u32) {
let scale = CSS_DPI / self.dpi;
(
((self.width as f64) * scale).round().max(1.0) as u32,
((self.height as f64) * scale).round().max(1.0) as u32,
)
}
}
const DEFAULT_WIDTH_IN: f64 = 1000.0 / CSS_DPI;
const DEFAULT_HEIGHT_IN: f64 = 618.0 / CSS_DPI;
impl Default for Canvas {
fn default() -> Self {
Self::default_at(CSS_DPI)
}
}
fn clamp_px(value: f64) -> u32 {
if !value.is_finite() {
return MIN_PX;
}
(value.round().max(0.0) as u32).clamp(MIN_PX, MAX_PX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_ratio_of_one_is_the_logical_size_at_css_dpi() {
let canvas = Canvas::from_logical(800.0, 600.0, 1.0);
assert_eq!((canvas.width, canvas.height), (800, 600));
assert_eq!(canvas.dpi, 96.0);
assert_eq!(canvas.css_size(), (800, 600));
}
#[test]
fn a_retina_ratio_scales_pixels_and_dpi_together() {
let canvas = Canvas::from_logical(800.0, 600.0, 2.0);
assert_eq!((canvas.width, canvas.height), (1600, 1200));
assert_eq!(canvas.dpi, 192.0);
assert_eq!(canvas.css_size(), (800, 600));
}
#[test]
fn a_zero_size_is_clamped_rather_than_rendered() {
let canvas = Canvas::from_logical(0.0, 0.0, 1.0);
assert_eq!((canvas.width, canvas.height), (MIN_PX, MIN_PX));
}
#[test]
fn an_absurd_size_is_clamped_rather_than_allocated() {
let canvas = Canvas::from_logical(99_999.0, 99_999.0, 1.0);
assert_eq!((canvas.width, canvas.height), (MAX_PX, MAX_PX));
}
#[test]
fn a_large_pane_at_two_x_renders_at_its_full_size() {
let canvas = Canvas::from_logical(2400.0, 1400.0, 2.0);
assert_eq!((canvas.width, canvas.height), (4800, 2800));
assert_eq!(canvas.css_size(), (2400, 1400));
}
#[test]
fn a_nonsense_ratio_falls_back_rather_than_propagating() {
for ratio in [f64::NAN, f64::INFINITY] {
let canvas = Canvas::from_logical(800.0, 600.0, ratio);
assert_eq!((canvas.width, canvas.height), (800, 600), "{ratio}");
assert_eq!(canvas.dpi, 96.0);
}
assert_eq!(Canvas::from_logical(100.0, 100.0, 99.0).dpi, CSS_DPI * 4.0);
assert_eq!(Canvas::from_logical(100.0, 100.0, 0.01).dpi, CSS_DPI * 0.5);
}
#[test]
fn inches_go_through_the_requested_resolution() {
let canvas = Canvas::from_inches(6.0, 4.0, 150.0);
assert_eq!((canvas.width, canvas.height), (900, 600));
assert_eq!(canvas.dpi, 150.0);
assert_eq!(canvas.css_size(), (576, 384));
}
#[test]
fn the_default_figure_holds_its_size_across_resolutions() {
assert_eq!(Canvas::default(), Canvas::default_at(CSS_DPI));
let dense = Canvas::default_at(300.0);
assert_eq!(dense.dpi, 300.0);
assert_eq!(dense.css_size(), Canvas::default().css_size());
}
#[test]
fn a_bad_dpi_falls_back_to_css_dpi() {
for dpi in [0.0, -100.0, f64::NAN] {
let canvas = Canvas::from_inches(6.0, 4.0, dpi);
assert_eq!(canvas.dpi, CSS_DPI, "{dpi}");
}
}
}