1#![forbid(unsafe_code)]
2#[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}