Skip to main content

use_spacing_scale/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive spacing scale helpers.
3//!
4//! These helpers generate predictable spacing values from a base size and
5//! ratio.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_spacing_scale::{SpacingScale, spacing_step, spacing_steps};
11//!
12//! let scale = SpacingScale::new(8.0, 2.0).unwrap();
13//!
14//! assert_eq!(scale.step(-1), 4.0);
15//! assert_eq!(scale.step(2), 32.0);
16//! assert_eq!(spacing_step(8.0, 2.0, 1).unwrap(), 16.0);
17//! assert_eq!(spacing_steps(8.0, 2.0, -1, 2).unwrap(), vec![4.0, 8.0, 16.0, 32.0]);
18//! ```
19
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub struct SpacingScale {
22    base_px: f64,
23    ratio: f64,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum SpacingScaleError {
28    InvalidBaseSize,
29    InvalidRatio,
30    InvalidStepRange,
31}
32
33fn validate_base(base_px: f64) -> Result<f64, SpacingScaleError> {
34    if !base_px.is_finite() || base_px <= 0.0 {
35        Err(SpacingScaleError::InvalidBaseSize)
36    } else {
37        Ok(base_px)
38    }
39}
40
41fn validate_ratio(ratio: f64) -> Result<f64, SpacingScaleError> {
42    if !ratio.is_finite() || ratio <= 1.0 {
43        Err(SpacingScaleError::InvalidRatio)
44    } else {
45        Ok(ratio)
46    }
47}
48
49fn spacing_value(base_px: f64, ratio: f64, step: isize) -> f64 {
50    base_px * ratio.powf(step as f64)
51}
52
53impl SpacingScale {
54    pub fn new(base_px: f64, ratio: f64) -> Result<Self, SpacingScaleError> {
55        Ok(Self {
56            base_px: validate_base(base_px)?,
57            ratio: validate_ratio(ratio)?,
58        })
59    }
60
61    #[must_use]
62    pub fn step(&self, step: isize) -> f64 {
63        spacing_value(self.base_px, self.ratio, step)
64    }
65
66    #[must_use]
67    pub fn steps(&self, min_step: isize, max_step: isize) -> Vec<f64> {
68        if max_step < min_step {
69            return Vec::new();
70        }
71
72        (min_step..=max_step).map(|step| self.step(step)).collect()
73    }
74}
75
76pub fn spacing_step(base_px: f64, ratio: f64, step: isize) -> Result<f64, SpacingScaleError> {
77    let scale = SpacingScale::new(base_px, ratio)?;
78    Ok(scale.step(step))
79}
80
81pub fn spacing_steps(
82    base_px: f64,
83    ratio: f64,
84    min_step: isize,
85    max_step: isize,
86) -> Result<Vec<f64>, SpacingScaleError> {
87    if max_step < min_step {
88        return Err(SpacingScaleError::InvalidStepRange);
89    }
90
91    let scale = SpacingScale::new(base_px, ratio)?;
92    Ok(scale.steps(min_step, max_step))
93}
94
95#[cfg(test)]
96mod tests {
97    use super::{spacing_step, spacing_steps, SpacingScale, SpacingScaleError};
98
99    #[test]
100    fn generates_spacing_scale_values() {
101        let scale = SpacingScale::new(8.0, 2.0).unwrap();
102
103        assert_eq!(scale.step(-1), 4.0);
104        assert_eq!(scale.step(0), 8.0);
105        assert_eq!(scale.step(2), 32.0);
106        assert_eq!(scale.steps(-1, 2), vec![4.0, 8.0, 16.0, 32.0]);
107        assert_eq!(spacing_step(8.0, 2.0, 1).unwrap(), 16.0);
108        assert_eq!(
109            spacing_steps(8.0, 2.0, -1, 2).unwrap(),
110            vec![4.0, 8.0, 16.0, 32.0]
111        );
112    }
113
114    #[test]
115    fn rejects_invalid_spacing_scale_inputs() {
116        assert_eq!(
117            SpacingScale::new(0.0, 2.0),
118            Err(SpacingScaleError::InvalidBaseSize)
119        );
120        assert_eq!(
121            SpacingScale::new(8.0, 1.0),
122            Err(SpacingScaleError::InvalidRatio)
123        );
124        assert_eq!(
125            spacing_steps(8.0, 2.0, 2, -1),
126            Err(SpacingScaleError::InvalidStepRange)
127        );
128    }
129}