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::widget::canvas::{self, event, Frame, Geometry, Path, Program, Stroke};
8use iced::{Color, Point, Rectangle};
9use iced::mouse;
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()
93                .with_color(Color::WHITE)
94                .with_width(2.0),
95        );
96        frame.stroke(
97            &y_axis,
98            Stroke::default()
99                .with_color(Color::WHITE)
100                .with_width(2.0),
101        );
102
103        // Generate curve points (51 points for smooth curve: 0.0, 0.02, 0.04, ..., 1.0)
104        let num_points = 51;
105        let points: Vec<Point> = (0..num_points)
106            .map(|i| {
107                let input = i as f32 / (num_points - 1) as f32; // 0.0 to 1.0
108                let output = Self::apply_curve(input, &self.curve);
109
110                // Convert to screen coordinates
111                Point::new(
112                    origin.x + input * graph_width,
113                    origin.y - output * graph_height,
114                )
115            })
116            .collect();
117
118        // Draw curve as connected line segments in green
119        for window in points.windows(2) {
120            let segment = Path::line(window[0], window[1]);
121            frame.stroke(
122                &segment,
123                Stroke::default()
124                    .with_color(Color::from_rgb(0.3, 0.8, 0.3))
125                    .with_width(2.0),
126            );
127        }
128
129        // Add optional "clamped" indicator if multiplier > 1.0
130        if self.multiplier > 1.0 {
131            // Draw a subtle warning indicator at the top of the graph
132            let clamped_y = origin.y - graph_height;
133            let indicator = Path::line(
134                Point::new(bounds.width - margin - 30.0, clamped_y + 5.0),
135                Point::new(bounds.width - margin, clamped_y + 5.0),
136            );
137            frame.stroke(
138                &indicator,
139                Stroke::default()
140                    .with_color(Color::from_rgb(0.8, 0.5, 0.0))
141                    .with_width(3.0),
142            );
143        }
144
145        vec![frame.into_geometry()]
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_apply_curve_linear() {
155        // Linear: output = input
156        assert!((CurveGraph::apply_curve(0.0, &SensitivityCurve::Linear) - 0.0).abs() < f32::EPSILON);
157        assert!((CurveGraph::apply_curve(0.5, &SensitivityCurve::Linear) - 0.5).abs() < f32::EPSILON);
158        assert!((CurveGraph::apply_curve(1.0, &SensitivityCurve::Linear) - 1.0).abs() < f32::EPSILON);
159    }
160
161    #[test]
162    fn test_apply_curve_quadratic() {
163        // Quadratic: output = input^2
164        let result = CurveGraph::apply_curve(0.5, &SensitivityCurve::Quadratic);
165        assert!((result - 0.25).abs() < 0.001);
166    }
167
168    #[test]
169    fn test_apply_curve_exponential() {
170        // Exponential: output = input^2 (GUI uses fixed exponent 2.0)
171        let result = CurveGraph::apply_curve(0.5, &SensitivityCurve::Exponential);
172        assert!((result - 0.25).abs() < 0.001); // 0.5^2 = 0.25
173    }
174
175    #[test]
176    fn test_apply_curve_with_multiplier() {
177        // Multiplier is separate from curve calculation
178        // apply_curve doesn't use multiplier, but we verify it exists on the struct
179        let graph = CurveGraph::new(SensitivityCurve::Linear, 2.0);
180        assert_eq!(graph.multiplier, 2.0);
181    }
182
183    #[test]
184    fn test_apply_curve_negative_input_quadratic() {
185        // Quadratic doesn't preserve sign: input * input is always positive
186        // This is the actual behavior - the GUI's quadratic curve is non-negative
187        let result = CurveGraph::apply_curve(-0.5, &SensitivityCurve::Quadratic);
188        assert!((result - 0.25).abs() < 0.001); // (-0.5) * (-0.5) = 0.25
189    }
190
191    #[test]
192    fn test_apply_curve_negative_input_exponential() {
193        // Negative inputs for exponential (with sign preservation)
194        let result = CurveGraph::apply_curve(-0.5, &SensitivityCurve::Exponential);
195        assert!((result - (-0.25)).abs() < 0.001); // -0.5^2 = -0.25
196    }
197
198    #[test]
199    fn test_curve_graph_new() {
200        let graph = CurveGraph::new(SensitivityCurve::Quadratic, 1.5);
201        assert_eq!(graph.multiplier, 1.5);
202        // Curve is Copy, so we can compare directly
203    }
204
205    #[test]
206    fn test_apply_curve_zero() {
207        // Zero input should always produce zero output
208        assert!((CurveGraph::apply_curve(0.0, &SensitivityCurve::Linear) - 0.0).abs() < f32::EPSILON);
209        assert!((CurveGraph::apply_curve(0.0, &SensitivityCurve::Quadratic) - 0.0).abs() < f32::EPSILON);
210        assert!((CurveGraph::apply_curve(0.0, &SensitivityCurve::Exponential) - 0.0).abs() < f32::EPSILON);
211    }
212
213    #[test]
214    fn test_apply_curve_full_deflection() {
215        // Full deflection (1.0) tests
216        assert_eq!(CurveGraph::apply_curve(1.0, &SensitivityCurve::Linear), 1.0);
217        assert_eq!(CurveGraph::apply_curve(1.0, &SensitivityCurve::Quadratic), 1.0);
218        assert_eq!(CurveGraph::apply_curve(1.0, &SensitivityCurve::Exponential), 1.0);
219    }
220}