1use iced::widget::canvas::{self, event, Cache, Frame, Geometry, Path, Program, Stroke};
7use iced::{Color, Point, Rectangle};
8use iced::mouse;
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum DeadzoneShape {
14 Circular,
15 Square,
16}
17
18pub struct AnalogVisualizer {
32 pub stick_x: f32,
34 pub stick_y: f32,
36 pub deadzone: f32,
38 pub deadzone_shape: DeadzoneShape,
40 pub range_min: i32,
42 pub range_max: i32,
44 pub cache: Arc<Cache>,
47}
48
49impl Default for AnalogVisualizer {
50 fn default() -> Self {
51 Self {
52 stick_x: 0.0,
53 stick_y: 0.0,
54 deadzone: 0.15,
55 deadzone_shape: DeadzoneShape::Circular,
56 range_min: -32768,
57 range_max: 32767,
58 cache: Arc::new(Cache::default()),
59 }
60 }
61}
62
63impl<Message> Program<Message> for AnalogVisualizer {
64 type State = ();
65
66 fn update(
67 &self,
68 _state: &mut Self::State,
69 _event: canvas::Event,
70 _bounds: Rectangle,
71 _cursor: mouse::Cursor,
72 ) -> (event::Status, Option<Message>) {
73 (event::Status::Ignored, None)
74 }
75
76 fn draw(
77 &self,
78 _state: &Self::State,
79 renderer: &iced::Renderer,
80 _theme: &iced::Theme,
81 bounds: Rectangle,
82 _cursor: mouse::Cursor,
83 ) -> Vec<Geometry> {
84 let center = Point::new(bounds.width / 2.0, bounds.height / 2.0);
85 let size = bounds.width.min(bounds.height);
86 let outer_radius = size * 0.45;
87
88 let background = self.cache.draw(renderer, bounds.size(), |frame| {
90 let outer_circle = Path::circle(center, outer_radius);
92 frame.fill(&outer_circle, Color::from_rgb(0.15, 0.15, 0.15));
93 frame.stroke(
94 &outer_circle,
95 Stroke::default()
96 .with_color(Color::from_rgb(0.4, 0.4, 0.4))
97 .with_width(2.0),
98 );
99
100 let deadzone_radius = (outer_radius * self.deadzone.clamp(0.0, 1.0)).max(0.0);
102 let deadzone_color = Color::from_rgba(0.2, 0.5, 0.2, 0.4);
103
104 if self.deadzone_shape == DeadzoneShape::Circular && deadzone_radius > 0.5 {
105 let deadzone_circle = Path::circle(center, deadzone_radius);
106 frame.fill(&deadzone_circle, deadzone_color);
107 frame.stroke(
108 &deadzone_circle,
109 Stroke::default()
110 .with_color(Color::from_rgb(0.3, 0.7, 0.3))
111 .with_width(1.0),
112 );
113 } else if deadzone_radius > 0.5 {
114 let dz_size = deadzone_radius * 2.0;
116 let deadzone_rect = Path::rectangle(
117 Point::new(center.x - deadzone_radius, center.y - deadzone_radius),
118 iced::Size::new(dz_size, dz_size),
119 );
120 frame.fill(&deadzone_rect, deadzone_color);
121 frame.stroke(
122 &deadzone_rect,
123 Stroke::default()
124 .with_color(Color::from_rgb(0.3, 0.7, 0.3))
125 .with_width(1.0),
126 );
127 }
128
129 let h_line = Path::line(
131 Point::new(center.x - outer_radius, center.y),
132 Point::new(center.x + outer_radius, center.y),
133 );
134 let v_line = Path::line(
135 Point::new(center.x, center.y - outer_radius),
136 Point::new(center.x, center.y + outer_radius),
137 );
138 frame.stroke(
139 &h_line,
140 Stroke::default()
141 .with_color(Color::from_rgba(0.5, 0.5, 0.5, 0.3))
142 .with_width(1.0),
143 );
144 frame.stroke(
145 &v_line,
146 Stroke::default()
147 .with_color(Color::from_rgba(0.5, 0.5, 0.5, 0.3))
148 .with_width(1.0),
149 );
150
151 let center_dot = Path::circle(center, 3.0);
153 frame.fill(¢er_dot, Color::from_rgb(0.6, 0.6, 0.6));
154 });
155
156 let mut frame = Frame::new(renderer, bounds.size());
158
159 let stick_x_clamped = self.stick_x.clamp(-1.0, 1.0);
161 let stick_y_clamped = self.stick_y.clamp(-1.0, 1.0);
162
163 let stick_offset_x = stick_x_clamped * outer_radius;
164 let stick_offset_y = -stick_y_clamped * outer_radius;
166 let stick_pos = Point::new(center.x + stick_offset_x, center.y + stick_offset_y);
167
168 let stick_dot = Path::circle(stick_pos, 6.0);
169 frame.fill(&stick_dot, Color::from_rgb(0.9, 0.3, 0.3));
170 frame.stroke(
171 &stick_dot,
172 Stroke::default()
173 .with_color(Color::from_rgb(1.0, 1.0, 1.0))
174 .with_width(1.0),
175 );
176
177 vec![background, frame.into_geometry()]
178 }
179}
180
181impl AnalogVisualizer {
182 pub fn clear_cache(&self) {
187 self.cache.clear();
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_analog_visualizer_default() {
197 let viz = AnalogVisualizer::default();
198 assert_eq!(viz.stick_x, 0.0);
199 assert_eq!(viz.stick_y, 0.0);
200 assert_eq!(viz.deadzone, 0.15);
201 assert_eq!(viz.deadzone_shape, DeadzoneShape::Circular);
202 }
203
204 #[test]
205 fn test_analog_visualizer_with_values() {
206 let viz = AnalogVisualizer {
207 stick_x: 0.5,
208 stick_y: -0.3,
209 deadzone: 0.2,
210 deadzone_shape: DeadzoneShape::Square,
211 range_min: -32768,
212 range_max: 32767,
213 cache: Arc::new(Cache::default()),
214 };
215 assert_eq!(viz.stick_x, 0.5);
216 assert_eq!(viz.stick_y, -0.3);
217 assert_eq!(viz.deadzone, 0.2);
218 assert_eq!(viz.deadzone_shape, DeadzoneShape::Square);
219 }
220
221 #[test]
222 fn test_deadzone_shapes() {
223 let circular = AnalogVisualizer {
224 deadzone_shape: DeadzoneShape::Circular,
225 ..Default::default()
226 };
227 assert_eq!(circular.deadzone_shape, DeadzoneShape::Circular);
228
229 let square = AnalogVisualizer {
230 deadzone_shape: DeadzoneShape::Square,
231 ..Default::default()
232 };
233 assert_eq!(square.deadzone_shape, DeadzoneShape::Square);
234 }
235
236 #[test]
237 fn test_range_values() {
238 let viz = AnalogVisualizer {
239 range_min: -16384,
240 range_max: 16383,
241 ..Default::default()
242 };
243 assert_eq!(viz.range_min, -16384);
244 assert_eq!(viz.range_max, 16383);
245 }
246
247 #[test]
248 fn test_stick_position_clamping_bounds() {
249 let viz = AnalogVisualizer {
251 stick_x: 1.0,
252 stick_y: 1.0,
253 ..Default::default()
254 };
255 assert_eq!(viz.stick_x, 1.0);
256 assert_eq!(viz.stick_y, 1.0);
257
258 let viz_negative = AnalogVisualizer {
259 stick_x: -1.0,
260 stick_y: -1.0,
261 ..Default::default()
262 };
263 assert_eq!(viz_negative.stick_x, -1.0);
264 assert_eq!(viz_negative.stick_y, -1.0);
265 }
266
267 #[test]
268 fn test_clear_cache_exists() {
269 let viz = AnalogVisualizer::default();
270 viz.clear_cache();
272 }
275}