1use crate::gui::Message;
2use crate::theme;
3use crate::widgets::{
4 analog_visualizer::DeadzoneShape as WidgetDeadzoneShape, AnalogVisualizer, CurveGraph,
5};
6use aethermap_common::{AnalogMode, CameraOutputMode};
7use iced::{
8 widget::{button, container, pick_list, scrollable, slider, text},
9 Color, Element, Length,
10};
11use std::sync::Arc;
12use std::time::Instant;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum DeadzoneShape {
16 #[default]
17 Circular,
18 Square,
19}
20
21impl std::fmt::Display for DeadzoneShape {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 DeadzoneShape::Circular => write!(f, "Circular"),
25 DeadzoneShape::Square => write!(f, "Square"),
26 }
27 }
28}
29
30impl DeadzoneShape {
31 pub const ALL: [DeadzoneShape; 2] = [DeadzoneShape::Circular, DeadzoneShape::Square];
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum SensitivityCurve {
36 #[default]
37 Linear,
38 Quadratic,
39 Exponential,
40}
41
42impl std::fmt::Display for SensitivityCurve {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 SensitivityCurve::Linear => write!(f, "Linear"),
46 SensitivityCurve::Quadratic => write!(f, "Quadratic"),
47 SensitivityCurve::Exponential => write!(f, "Exponential"),
48 }
49 }
50}
51
52impl SensitivityCurve {
53 pub const ALL: [SensitivityCurve; 3] = [
54 SensitivityCurve::Linear,
55 SensitivityCurve::Quadratic,
56 SensitivityCurve::Exponential,
57 ];
58}
59
60#[derive(Debug, Clone)]
61pub struct CalibrationConfig {
62 pub deadzone: f32,
63 pub deadzone_shape: String,
64 pub sensitivity: String,
65 pub sensitivity_multiplier: f32,
66 pub range_min: i32,
67 pub range_max: i32,
68 pub invert_x: bool,
69 pub invert_y: bool,
70 pub exponent: f32,
71}
72
73impl Default for CalibrationConfig {
74 fn default() -> Self {
75 Self {
76 deadzone: 0.15,
77 deadzone_shape: "circular".to_string(),
78 sensitivity: "linear".to_string(),
79 sensitivity_multiplier: 1.0,
80 range_min: -32768,
81 range_max: 32767,
82 invert_x: false,
83 invert_y: false,
84 exponent: 2.0,
85 }
86 }
87}
88
89#[derive(Debug)]
90pub struct AnalogCalibrationView {
91 pub device_id: String,
92 pub layer_id: usize,
93 pub calibration: CalibrationConfig,
94 pub deadzone_shape_selected: DeadzoneShape,
95 pub sensitivity_curve_selected: SensitivityCurve,
96 pub analog_mode_selected: AnalogMode,
97 pub camera_mode_selected: CameraOutputMode,
98 pub invert_x_checked: bool,
99 pub invert_y_checked: bool,
100 pub stick_x: f32,
101 pub stick_y: f32,
102 pub loading: bool,
103 pub error: Option<String>,
104 pub last_visualizer_update: Instant,
105 pub visualizer_cache: Arc<iced::widget::canvas::Cache>,
106}
107
108impl Clone for AnalogCalibrationView {
109 fn clone(&self) -> Self {
110 Self {
111 device_id: self.device_id.clone(),
112 layer_id: self.layer_id,
113 calibration: self.calibration.clone(),
114 deadzone_shape_selected: self.deadzone_shape_selected,
115 sensitivity_curve_selected: self.sensitivity_curve_selected,
116 analog_mode_selected: self.analog_mode_selected,
117 camera_mode_selected: self.camera_mode_selected,
118 invert_x_checked: self.invert_x_checked,
119 invert_y_checked: self.invert_y_checked,
120 stick_x: self.stick_x,
121 stick_y: self.stick_y,
122 loading: self.loading,
123 error: self.error.clone(),
124 last_visualizer_update: Instant::now(),
125 visualizer_cache: Arc::clone(&self.visualizer_cache),
126 }
127 }
128}
129
130impl Default for AnalogCalibrationView {
131 fn default() -> Self {
132 Self {
133 device_id: String::new(),
134 layer_id: 0,
135 calibration: CalibrationConfig::default(),
136 deadzone_shape_selected: DeadzoneShape::Circular,
137 sensitivity_curve_selected: SensitivityCurve::Linear,
138 analog_mode_selected: AnalogMode::Disabled,
139 camera_mode_selected: CameraOutputMode::Scroll,
140 invert_x_checked: false,
141 invert_y_checked: false,
142 stick_x: 0.0,
143 stick_y: 0.0,
144 loading: false,
145 error: None,
146 last_visualizer_update: Instant::now(),
147 visualizer_cache: Arc::new(iced::widget::canvas::Cache::default()),
148 }
149 }
150}
151
152impl AnalogCalibrationView {
153 fn checkbox_button<'a>(
154 &'a self,
155 label: &str,
156 is_checked: bool,
157 msg: fn(bool) -> Message,
158 ) -> Element<'a, Message> {
159 let btn = if is_checked {
160 button(text(format!("[X] {}", label)).size(14))
161 } else {
162 button(text(format!("[ ] {}", label)).size(14))
163 };
164 btn.on_press(msg(is_checked))
165 .style(iced::theme::Button::Text)
166 .into()
167 }
168
169 pub fn view(&self) -> Element<'_, Message> {
170 use iced::widget::{container, horizontal_rule as rule, Canvas, Column, Row};
171
172 let title = text("Analog Calibration").size(24);
173 let info = Column::new()
174 .spacing(5)
175 .push(text(format!("Device: {}", self.device_id)).size(14))
176 .push(text(format!("Layer: {}", self.layer_id)).size(14));
177
178 let visualizer_section = Column::new()
179 .spacing(10)
180 .push(text("Stick Position").size(18))
181 .push(
182 container(
183 Canvas::new(AnalogVisualizer {
184 stick_x: self.stick_x,
185 stick_y: self.stick_y,
186 deadzone: self.calibration.deadzone,
187 deadzone_shape: match self.deadzone_shape_selected {
188 DeadzoneShape::Circular => WidgetDeadzoneShape::Circular,
189 DeadzoneShape::Square => WidgetDeadzoneShape::Square,
190 },
191 range_min: self.calibration.range_min,
192 range_max: self.calibration.range_max,
193 cache: Arc::clone(&self.visualizer_cache),
194 })
195 .width(Length::Fixed(250.0))
196 .height(Length::Fixed(250.0)),
197 )
198 .width(Length::Fixed(270.0))
199 .height(Length::Fixed(270.0))
200 .center_x()
201 .center_y(),
202 );
203
204 let mode_section = Column::new()
205 .spacing(10)
206 .push(text("Output Mode").size(18))
207 .push(Row::new().spacing(10).push(text("Mode:")).push(pick_list(
208 &AnalogMode::ALL[..],
209 Some(self.analog_mode_selected),
210 Message::AnalogModeChanged,
211 )));
212
213 let mode_section = if self.analog_mode_selected == AnalogMode::Camera {
214 mode_section.push(Row::new().spacing(10).push(text("Camera:")).push(pick_list(
215 &CameraOutputMode::ALL[..],
216 Some(self.camera_mode_selected),
217 Message::CameraModeChanged,
218 )))
219 } else {
220 mode_section
221 };
222
223 let deadzone_section = Column::new()
224 .spacing(10)
225 .push(text("Deadzone").size(18))
226 .push(
227 Row::new()
228 .spacing(10)
229 .push(text("Size:"))
230 .push(text(format!("{:.0}%", self.calibration.deadzone * 100.0)))
231 .push(
232 slider(
233 0.0..=1.0,
234 self.calibration.deadzone,
235 Message::AnalogDeadzoneChanged,
236 )
237 .step(0.01),
238 ),
239 )
240 .push(Row::new().spacing(10).push(text("Shape:")).push(pick_list(
241 &DeadzoneShape::ALL[..],
242 Some(self.deadzone_shape_selected),
243 Message::AnalogDeadzoneShapeChanged,
244 )));
245
246 let sensitivity_section = Column::new()
247 .spacing(10)
248 .push(text("Sensitivity").size(18))
249 .push(
250 Row::new()
251 .spacing(10)
252 .push(text("Multiplier:"))
253 .push(text(format!(
254 "{:.1}",
255 self.calibration.sensitivity_multiplier
256 )))
257 .push(
258 slider(
259 0.1..=5.0,
260 self.calibration.sensitivity_multiplier,
261 Message::AnalogSensitivityChanged,
262 )
263 .step(0.1),
264 ),
265 )
266 .push(Row::new().spacing(10).push(text("Curve:")).push(pick_list(
267 &SensitivityCurve::ALL[..],
268 Some(self.sensitivity_curve_selected),
269 Message::AnalogSensitivityCurveChanged,
270 )))
271 .push(text(format!("Curve: {}", self.sensitivity_curve_selected)).size(14))
272 .push(
273 container(
274 Canvas::new(CurveGraph {
275 curve: self.sensitivity_curve_selected,
276 multiplier: self.calibration.sensitivity_multiplier,
277 })
278 .width(Length::Fixed(300.0))
279 .height(Length::Fixed(200.0)),
280 )
281 .width(Length::Fixed(320.0))
282 .center_x(),
283 );
284
285 let range_section = Column::new()
286 .spacing(10)
287 .push(text("Output Range").size(18))
288 .push(
289 Row::new()
290 .spacing(10)
291 .push(text("Min:"))
292 .push(text(self.calibration.range_min.to_string()))
293 .push(slider(
294 -32768..=0,
295 self.calibration.range_min,
296 Message::AnalogRangeMinChanged,
297 )),
298 )
299 .push(
300 Row::new()
301 .spacing(10)
302 .push(text("Max:"))
303 .push(text(self.calibration.range_max.to_string()))
304 .push(slider(
305 0..=32767,
306 self.calibration.range_max,
307 Message::AnalogRangeMaxChanged,
308 )),
309 );
310
311 let inversion_section = Column::new()
312 .spacing(10)
313 .push(text("Axis Inversion").size(18))
314 .push(
315 Row::new()
316 .spacing(20)
317 .push(self.checkbox_button(
318 "Invert X",
319 self.invert_x_checked,
320 Message::AnalogInvertXToggled,
321 ))
322 .push(self.checkbox_button(
323 "Invert Y",
324 self.invert_y_checked,
325 Message::AnalogInvertYToggled,
326 )),
327 );
328
329 let buttons = Row::new()
330 .spacing(10)
331 .push(button("Apply").on_press(Message::ApplyAnalogCalibration))
332 .push(
333 button("Close")
334 .on_press(Message::CloseAnalogCalibration)
335 .style(iced::theme::Button::Secondary),
336 );
337
338 let content = if let Some(error) = &self.error {
339 Column::new()
340 .spacing(20)
341 .push(title)
342 .push(info)
343 .push(rule(1))
344 .push(text(format!("Error: {}", error)).style(Color::from_rgb(1.0, 0.4, 0.4)))
345 .push(buttons)
346 } else {
347 Column::new()
348 .spacing(20)
349 .push(title)
350 .push(info)
351 .push(rule(1))
352 .push(visualizer_section)
353 .push(rule(1))
354 .push(mode_section)
355 .push(rule(1))
356 .push(deadzone_section)
357 .push(rule(1))
358 .push(sensitivity_section)
359 .push(rule(1))
360 .push(range_section)
361 .push(rule(1))
362 .push(inversion_section)
363 .push(rule(1))
364 .push(buttons)
365 };
366
367 scrollable(content).height(Length::Fill).into()
368 }
369}
370
371pub fn overlay_view(state: &crate::gui::State) -> Option<Element<'_, Message>> {
372 if let Some(ref view) = state.analog_calibration_view {
373 let dialog = container(view.view())
374 .max_width(600)
375 .max_height(800)
376 .style(theme::styles::card);
377
378 Some(
379 container(dialog)
380 .width(Length::Fill)
381 .height(Length::Fill)
382 .align_x(iced::alignment::Horizontal::Center)
383 .align_y(iced::alignment::Vertical::Center)
384 .padding(40)
385 .style(iced::theme::Container::Transparent)
386 .into(),
387 )
388 } else {
389 None
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use std::time::Duration;
397
398 #[test]
399 fn test_analog_calibration_view_default() {
400 let view = AnalogCalibrationView::default();
401
402 assert_eq!(view.device_id, "");
403 assert_eq!(view.layer_id, 0);
404 assert_eq!(view.calibration.deadzone, 0.15);
405 assert_eq!(view.stick_x, 0.0);
406 assert_eq!(view.stick_y, 0.0);
407 assert!(!view.loading);
408 assert!(view.error.is_none());
409 }
410
411 #[test]
412 fn test_analog_calibration_view_with_values() {
413 let view = AnalogCalibrationView {
414 device_id: "test_device".to_string(),
415 layer_id: 1,
416 calibration: CalibrationConfig {
417 deadzone: 0.2,
418 deadzone_shape: "circular".to_string(),
419 sensitivity: "quadratic".to_string(),
420 sensitivity_multiplier: 1.5,
421 range_min: -16384,
422 range_max: 16383,
423 invert_x: true,
424 invert_y: false,
425 exponent: 2.0,
426 },
427 deadzone_shape_selected: DeadzoneShape::Square,
428 sensitivity_curve_selected: SensitivityCurve::Quadratic,
429 analog_mode_selected: AnalogMode::Mouse,
430 camera_mode_selected: CameraOutputMode::Keys,
431 invert_x_checked: true,
432 invert_y_checked: false,
433 stick_x: 0.5,
434 stick_y: -0.3,
435 loading: false,
436 error: None,
437 last_visualizer_update: Instant::now(),
438 visualizer_cache: Arc::new(iced::widget::canvas::Cache::default()),
439 };
440
441 assert_eq!(view.device_id, "test_device");
442 assert_eq!(view.layer_id, 1);
443 assert_eq!(view.calibration.deadzone, 0.2);
444 assert_eq!(view.stick_x, 0.5);
445 assert_eq!(view.stick_y, -0.3);
446 assert_eq!(view.analog_mode_selected, AnalogMode::Mouse);
447 assert_eq!(view.camera_mode_selected, CameraOutputMode::Keys);
448 assert!(view.invert_x_checked);
449 assert!(!view.invert_y_checked);
450 }
451
452 #[test]
453 fn test_calibration_config_default() {
454 let config = CalibrationConfig::default();
455
456 assert_eq!(config.deadzone, 0.15);
457 assert_eq!(config.deadzone_shape, "circular");
458 assert_eq!(config.sensitivity, "linear");
459 assert_eq!(config.sensitivity_multiplier, 1.0);
460 assert_eq!(config.range_min, -32768);
461 assert_eq!(config.range_max, 32767);
462 assert!(!config.invert_x);
463 assert!(!config.invert_y);
464 assert_eq!(config.exponent, 2.0);
465 }
466
467 #[test]
468 fn test_deadzone_shape_display() {
469 assert_eq!(DeadzoneShape::Circular.to_string(), "Circular");
470 assert_eq!(DeadzoneShape::Square.to_string(), "Square");
471 }
472
473 #[test]
474 fn test_sensitivity_curve_display() {
475 assert_eq!(SensitivityCurve::Linear.to_string(), "Linear");
476 assert_eq!(SensitivityCurve::Quadratic.to_string(), "Quadratic");
477 assert_eq!(SensitivityCurve::Exponential.to_string(), "Exponential");
478 }
479
480 #[test]
481 fn test_deadzone_shape_default() {
482 assert_eq!(DeadzoneShape::default(), DeadzoneShape::Circular);
483 }
484
485 #[test]
486 fn test_sensitivity_curve_default() {
487 assert_eq!(SensitivityCurve::default(), SensitivityCurve::Linear);
488 }
489
490 #[test]
491 fn test_analog_calibration_view_clone() {
492 let view = AnalogCalibrationView {
493 device_id: "test_device".to_string(),
494 layer_id: 1,
495 calibration: CalibrationConfig {
496 deadzone: 0.2,
497 ..Default::default()
498 },
499 ..Default::default()
500 };
501
502 let cloned = view.clone();
503 assert_eq!(cloned.device_id, "test_device");
504 assert_eq!(cloned.layer_id, 1);
505 assert_eq!(cloned.calibration.deadzone, 0.2);
506 assert!(cloned.last_visualizer_update.elapsed() < Duration::from_secs(1));
507 }
508
509 #[test]
510 fn test_throttling_threshold() {
511 let view = AnalogCalibrationView {
512 device_id: "test".to_string(),
513 layer_id: 0,
514 calibration: CalibrationConfig::default(),
515 deadzone_shape_selected: DeadzoneShape::Circular,
516 sensitivity_curve_selected: SensitivityCurve::Linear,
517 analog_mode_selected: AnalogMode::Disabled,
518 camera_mode_selected: CameraOutputMode::Scroll,
519 invert_x_checked: false,
520 invert_y_checked: false,
521 stick_x: 0.0,
522 stick_y: 0.0,
523 loading: false,
524 error: None,
525 last_visualizer_update: Instant::now(),
526 visualizer_cache: Arc::new(iced::widget::canvas::Cache::default()),
527 };
528
529 assert!(view.last_visualizer_update.elapsed() < Duration::from_millis(33));
530 std::thread::sleep(Duration::from_millis(40));
531 assert!(view.last_visualizer_update.elapsed() >= Duration::from_millis(33));
532 }
533
534 #[test]
535 fn test_visualizer_cache_arc_sharing() {
536 let cache = Arc::new(iced::widget::canvas::Cache::default());
537 let cache_clone = Arc::clone(&cache);
538 assert!(Arc::ptr_eq(&cache, &cache_clone));
539 }
540
541 #[test]
542 fn test_analog_mode_selection_states() {
543 let modes = [
544 AnalogMode::Disabled,
545 AnalogMode::Dpad,
546 AnalogMode::Gamepad,
547 AnalogMode::Camera,
548 AnalogMode::Mouse,
549 AnalogMode::Wasd,
550 ];
551
552 for mode in modes {
553 let view = AnalogCalibrationView {
554 analog_mode_selected: mode,
555 ..Default::default()
556 };
557 assert_eq!(view.analog_mode_selected, mode);
558 }
559 }
560
561 #[test]
562 fn test_camera_mode_selection_states() {
563 let modes = [CameraOutputMode::Scroll, CameraOutputMode::Keys];
564
565 for mode in modes {
566 let view = AnalogCalibrationView {
567 camera_mode_selected: mode,
568 ..Default::default()
569 };
570 assert_eq!(view.camera_mode_selected, mode);
571 }
572 }
573}