ferrite_config/
zoom.rs

1use crate::{
2    defaults::zoom::*,
3    error::{ConfigError, Result},
4};
5use serde::{Deserialize, Serialize};
6
7/// Validated, ordered set of zoom steps
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(transparent)]
10pub struct ZoomSteps(Vec<f64>);
11
12impl ZoomSteps {
13    pub fn new(mut steps: Vec<f64>) -> Result<Self> {
14        if steps.is_empty() {
15            return Err(ConfigError::ValidationError(
16                "Zoom steps cannot be empty".into(),
17            ));
18        }
19        steps.sort_by(|a, b| {
20            a.partial_cmp(b)
21                .unwrap_or(std::cmp::Ordering::Equal)
22        });
23        steps.dedup_by(|a, b| (*a - *b).abs() < f64::EPSILON);
24        Ok(Self(steps))
25    }
26
27    pub fn as_slice(&self) -> &[f64] {
28        &self.0
29    }
30}
31
32impl Default for ZoomSteps {
33    fn default() -> Self {
34        Self::new(DEFAULT_ZOOM_STEPS.to_vec())
35            .expect("Default zoom steps must be valid")
36    }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
40pub enum FitMode {
41    /// Display image at actual size (100% zoom)
42    OneToOne,
43    /// Fit image to window, scaling by the longer dimension
44    FitLonger,
45    /// Fit image to window, scaling by the shorter dimension
46    FitShorter,
47    /// Use custom zoom level
48    Custom,
49}
50
51impl Default for FitMode {
52    fn default() -> Self {
53        FitMode::FitLonger
54    }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ZoomConfig {
59    pub min_zoom:              f64,
60    pub max_zoom:              f64,
61    pub default_zoom:          f64,
62    pub zoom_step:             f64,
63    pub use_predefined_steps:  bool,
64    pub zoom_steps:            ZoomSteps,
65    pub focal_point_enabled:   bool,
66    pub transition_enabled:    bool,
67    pub transition_duration:   f64,
68    pub fit_to_window:         bool,
69    pub maintain_aspect_ratio: bool,
70    pub default_fit_mode:      FitMode,
71}
72
73impl Default for ZoomConfig {
74    fn default() -> Self {
75        Self {
76            min_zoom:              MIN_ZOOM,
77            max_zoom:              MAX_ZOOM,
78            default_zoom:          DEFAULT_ZOOM,
79            zoom_step:             ZOOM_STEP,
80            use_predefined_steps:  USE_PREDEFINED_STEPS,
81            zoom_steps:            ZoomSteps::default(),
82            focal_point_enabled:   FOCAL_POINT_ENABLED,
83            transition_enabled:    TRANSITION_ENABLED,
84            transition_duration:   TRANSITION_DURATION,
85            fit_to_window:         FIT_TO_WINDOW,
86            maintain_aspect_ratio: MAINTAIN_ASPECT_RATIO,
87            default_fit_mode:      FitMode::default(),
88        }
89    }
90}
91
92impl ZoomConfig {
93    pub fn validate(&self) -> Result<()> {
94        if self.min_zoom <= 0.0 {
95            return Err(ConfigError::ValidationError(
96                "min_zoom must be positive".into(),
97            ));
98        }
99
100        if self.max_zoom <= self.min_zoom {
101            return Err(ConfigError::ValidationError(format!(
102                "max_zoom ({}) must be greater than min_zoom ({})",
103                self.max_zoom, self.min_zoom
104            )));
105        }
106
107        if self.default_zoom < self.min_zoom
108            || self.default_zoom > self.max_zoom
109        {
110            return Err(ConfigError::ValidationError(format!(
111                "default_zoom must be between {} and {}",
112                self.min_zoom, self.max_zoom
113            )));
114        }
115
116        if self.zoom_step <= 0.0 {
117            return Err(ConfigError::ValidationError(
118                "zoom_step must be positive".into(),
119            ));
120        }
121
122        if self.transition_duration < 0.0 {
123            return Err(ConfigError::ValidationError(
124                "transition_duration cannot be negative".into(),
125            ));
126        }
127
128        if self.use_predefined_steps {
129            for &step in self.zoom_steps.as_slice() {
130                if step < self.min_zoom || step > self.max_zoom {
131                    return Err(ConfigError::ValidationError(format!(
132                        "zoom step {} is outside allowed range [{}, {}]",
133                        step, self.min_zoom, self.max_zoom
134                    )));
135                }
136            }
137        }
138
139        Ok(())
140    }
141}