Skip to main content

aethermap_gui/widgets/
curve_graph.rs

1//! Canvas-based sensitivity curve graph widget
2//!
3//! CurveGraph visualizes how the sensitivity curve transforms analog input.
4//! Shows input (0-1) on X-axis and output (0-1) on Y-axis with the selected
5//! curve shape plotted.
6
7use iced::mouse;
8use iced::widget::canvas::{self, event, Frame, Geometry, Path, Program, Stroke};
9use iced::{Color, Point, Rectangle};
10
11use crate::gui::SensitivityCurve;
12
13/// Canvas widget that plots the sensitivity curve
14///
15/// Displays a graph showing how input values are transformed by the selected
16/// sensitivity curve. X-axis represents input (0-1), Y-axis represents output (0-1).
17pub struct CurveGraph {
18    /// The sensitivity curve to plot
19    pub curve: SensitivityCurve,
20    /// Sensitivity multiplier (for display reference only)
21    pub multiplier: f32,
22}
23
24impl CurveGraph {
25    /// Create a new curve graph with the specified curve and multiplier
26    pub fn new(curve: SensitivityCurve, multiplier: f32) -> Self {
27        Self { curve, multiplier }
28    }
29
30    /// Apply the sensitivity curve to an input value
31    ///
32    /// This matches the daemon's curve application logic for visualization.
33    /// The graph shows the normalized curve (0-1 range), not scaled by multiplier.
34    pub fn apply_curve(input: f32, curve: &SensitivityCurve) -> f32 {
35        match curve {
36            SensitivityCurve::Linear => input,
37            SensitivityCurve::Quadratic => input * input,
38            // GUI's Exponential has no exponent field, use default 2.0
39            SensitivityCurve::Exponential => {
40                if input >= 0.0 {
41                    input.powf(2.0)
42                } else {
43                    -(-input).powf(2.0)
44                }
45            }
46        }
47    }
48}
49
50impl<Message> Program<Message> for CurveGraph {
51    type State = ();
52
53    fn update(
54        &self,
55        _state: &mut Self::State,
56        _event: canvas::Event,
57        _bounds: Rectangle,
58        _cursor: mouse::Cursor,
59    ) -> (event::Status, Option<Message>) {
60        (event::Status::Ignored, None)
61    }
62
63    fn draw(
64        &self,
65        _state: &Self::State,
66        renderer: &iced::Renderer,
67        _theme: &iced::Theme,
68        bounds: Rectangle,
69        _cursor: mouse::Cursor,
70    ) -> Vec<Geometry> {
71        let mut frame = Frame::new(renderer, bounds.size());
72
73        // Graph margins for axes labels
74        let margin = 20.0;
75        let graph_width = bounds.width - 2.0 * margin;
76        let graph_height = bounds.height - 2.0 * margin;
77
78        // Origin point (bottom-left of graph area)
79        let origin = Point::new(margin, bounds.height - margin);
80
81        // Draw X and Y axes as white lines
82        let x_axis = Path::line(
83            Point::new(margin, bounds.height - margin),
84            Point::new(bounds.width - margin, bounds.height - margin),
85        );
86        let y_axis = Path::line(
87            Point::new(margin, margin),
88            Point::new(margin, bounds.height - margin),
89        );
90        frame.stroke(
91            &x_axis,
92            Stroke::default().with_color(Color::WHITE).with_width(2.0),
93        );
94        frame.stroke(
95            &y_axis,
96            Stroke::default().with_color(Color::WHITE).with_width(2.0),
97        );
98
99        // Generate curve points (51 points for smooth curve: 0.0, 0.02, 0.04, ..., 1.0)
100        let num_points = 51;
101        let points: Vec<Point> = (0..num_points)
102            .map(|i| {
103                let input = i as f32 / (num_points - 1) as f32; // 0.0 to 1.0
104                let output = Self::apply_curve(input, &self.curve);
105
106                // Convert to screen coordinates
107                Point::new(
108                    origin.x + input * graph_width,
109                    origin.y - output * graph_height,
110                )
111            })
112            .collect();
113
114        // Draw curve as connected line segments in green
115        for window in points.windows(2) {
116            let segment = Path::line(window[0], window[1]);
117            frame.stroke(
118                &segment,
119                Stroke::default()
120                    .with_color(Color::from_rgb(0.3, 0.8, 0.3))
121                    .with_width(2.0),
122            );
123        }
124
125        // Add optional "clamped" indicator if multiplier > 1.0
126        if self.multiplier > 1.0 {
127            // Draw a subtle warning indicator at the top of the graph
128            let clamped_y = origin.y - graph_height;
129            let indicator = Path::line(
130                Point::new(bounds.width - margin - 30.0, clamped_y + 5.0),
131                Point::new(bounds.width - margin, clamped_y + 5.0),
132            );
133            frame.stroke(
134                &indicator,
135                Stroke::default()
136                    .with_color(Color::from_rgb(0.8, 0.5, 0.0))
137                    .with_width(3.0),
138            );
139        }
140
141        vec![frame.into_geometry()]
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_apply_curve_linear() {
151        // Linear: output = input
152        assert!(
153            (CurveGraph::apply_curve(0.0, &SensitivityCurve::Linear) - 0.0).abs() < f32::EPSILON
154        );
155        assert!(
156            (CurveGraph::apply_curve(0.5, &SensitivityCurve::Linear) - 0.5).abs() < f32::EPSILON
157        );
158        assert!(
159            (CurveGraph::apply_curve(1.0, &SensitivityCurve::Linear) - 1.0).abs() < f32::EPSILON
160        );
161    }
162
163    #[test]
164    fn test_apply_curve_quadratic() {
165        // Quadratic: output = input^2
166        let result = CurveGraph::apply_curve(0.5, &SensitivityCurve::Quadratic);
167        assert!((result - 0.25).abs() < 0.001);
168    }
169
170    #[test]
171    fn test_apply_curve_exponential() {
172        // Exponential: output = input^2 (GUI uses fixed exponent 2.0)
173        let result = CurveGraph::apply_curve(0.5, &SensitivityCurve::Exponential);
174        assert!((result - 0.25).abs() < 0.001); // 0.5^2 = 0.25
175    }
176
177    #[test]
178    fn test_apply_curve_with_multiplier() {
179        // Multiplier is separate from curve calculation
180        // apply_curve doesn't use multiplier, but we verify it exists on the struct
181        let graph = CurveGraph::new(SensitivityCurve::Linear, 2.0);
182        assert_eq!(graph.multiplier, 2.0);
183    }
184
185    #[test]
186    fn test_apply_curve_negative_input_quadratic() {
187        // Quadratic doesn't preserve sign: input * input is always positive
188        // This is the actual behavior - the GUI's quadratic curve is non-negative
189        let result = CurveGraph::apply_curve(-0.5, &SensitivityCurve::Quadratic);
190        assert!((result - 0.25).abs() < 0.001); // (-0.5) * (-0.5) = 0.25
191    }
192
193    #[test]
194    fn test_apply_curve_negative_input_exponential() {
195        // Negative inputs for exponential (with sign preservation)
196        let result = CurveGraph::apply_curve(-0.5, &SensitivityCurve::Exponential);
197        assert!((result - (-0.25)).abs() < 0.001); // -0.5^2 = -0.25
198    }
199
200    #[test]
201    fn test_curve_graph_new() {
202        let graph = CurveGraph::new(SensitivityCurve::Quadratic, 1.5);
203        assert_eq!(graph.multiplier, 1.5);
204        // Curve is Copy, so we can compare directly
205    }
206
207    #[test]
208    fn test_apply_curve_zero() {
209        // Zero input should always produce zero output
210        assert!(
211            (CurveGraph::apply_curve(0.0, &SensitivityCurve::Linear) - 0.0).abs() < f32::EPSILON
212        );
213        assert!(
214            (CurveGraph::apply_curve(0.0, &SensitivityCurve::Quadratic) - 0.0).abs() < f32::EPSILON
215        );
216        assert!(
217            (CurveGraph::apply_curve(0.0, &SensitivityCurve::Exponential) - 0.0).abs()
218                < f32::EPSILON
219        );
220    }
221
222    #[test]
223    fn test_apply_curve_full_deflection() {
224        // Full deflection (1.0) tests
225        assert_eq!(CurveGraph::apply_curve(1.0, &SensitivityCurve::Linear), 1.0);
226        assert_eq!(
227            CurveGraph::apply_curve(1.0, &SensitivityCurve::Quadratic),
228            1.0
229        );
230        assert_eq!(
231            CurveGraph::apply_curve(1.0, &SensitivityCurve::Exponential),
232            1.0
233        );
234    }
235}