1use iced::{
2 widget::{
3 button, checkbox, column, container, row, text, text_input, scrollable,
4 horizontal_rule, vertical_rule, pick_list, slider, Column, Space,
5 },
6 Element, Length, Subscription, Theme, Application, Command, Color,
7 Alignment,
8};
9use std::sync::Arc;
10use crate::theme::{aether_dark, aether_light, container_styles};
11
12use crate::widgets::{AnalogVisualizer, CurveGraph, analog_visualizer::DeadzoneShape as WidgetDeadzoneShape};
14use aethermap_common::{DeviceInfo, DeviceCapabilities, DeviceType, LayerConfigInfo, LayerMode, LedPattern, LedZone, MacroEntry, MacroSettings, RemapProfileInfo, RemapEntry, Action, AnalogMode, CameraOutputMode, Request, Response, AutoSwitchRule as CommonAutoSwitchRule};
15use aethermap_common::HotkeyBinding as CommonHotkeyBinding;
16use aethermap_common::ipc_client::IpcClient;
17use serde::{Deserialize, Serialize};
18use std::path::PathBuf;
19use std::collections::{VecDeque, HashMap, HashSet};
20use std::time::{Duration, Instant};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Tab {
40 Devices,
41 Macros,
42 Profiles,
43}
44
45#[derive(Debug, Clone)]
46pub struct Notification {
47 pub message: String,
48 pub is_error: bool,
49 pub timestamp: Instant,
50}
51
52#[derive(Debug, Clone)]
57pub struct KeypadButton {
58 pub id: String,
60 pub label: String,
62 pub row: usize,
64 #[allow(dead_code)]
66 pub col: usize,
67 pub current_remap: Option<String>,
69}
70
71pub fn azeron_keypad_layout() -> Vec<KeypadButton> {
76 vec![
77 KeypadButton { id: "JOY_BTN_0".to_string(), label: "1".to_string(), row: 0, col: 0, current_remap: None },
79 KeypadButton { id: "JOY_BTN_1".to_string(), label: "2".to_string(), row: 0, col: 1, current_remap: None },
80 KeypadButton { id: "JOY_BTN_2".to_string(), label: "3".to_string(), row: 0, col: 2, current_remap: None },
81 KeypadButton { id: "JOY_BTN_3".to_string(), label: "4".to_string(), row: 0, col: 3, current_remap: None },
82 KeypadButton { id: "JOY_BTN_4".to_string(), label: "5".to_string(), row: 0, col: 4, current_remap: None },
83
84 KeypadButton { id: "JOY_BTN_5".to_string(), label: "Q".to_string(), row: 2, col: 0, current_remap: None },
86 KeypadButton { id: "JOY_BTN_6".to_string(), label: "W".to_string(), row: 2, col: 1, current_remap: None },
87 KeypadButton { id: "JOY_BTN_7".to_string(), label: "E".to_string(), row: 2, col: 2, current_remap: None },
88 KeypadButton { id: "JOY_BTN_8".to_string(), label: "R".to_string(), row: 2, col: 3, current_remap: None },
89 KeypadButton { id: "JOY_BTN_9".to_string(), label: "A".to_string(), row: 3, col: 0, current_remap: None },
90 KeypadButton { id: "JOY_BTN_10".to_string(), label: "S".to_string(), row: 3, col: 1, current_remap: None },
91 KeypadButton { id: "JOY_BTN_11".to_string(), label: "D".to_string(), row: 3, col: 2, current_remap: None },
92 KeypadButton { id: "JOY_BTN_12".to_string(), label: "F".to_string(), row: 3, col: 3, current_remap: None },
93 KeypadButton { id: "JOY_BTN_13".to_string(), label: "Z".to_string(), row: 4, col: 0, current_remap: None },
94 KeypadButton { id: "JOY_BTN_14".to_string(), label: "X".to_string(), row: 4, col: 1, current_remap: None },
95 KeypadButton { id: "JOY_BTN_15".to_string(), label: "C".to_string(), row: 4, col: 2, current_remap: None },
96 KeypadButton { id: "JOY_BTN_16".to_string(), label: "V".to_string(), row: 4, col: 3, current_remap: None },
97
98 KeypadButton { id: "JOY_BTN_17".to_string(), label: "6".to_string(), row: 0, col: 5, current_remap: None },
100 KeypadButton { id: "JOY_BTN_18".to_string(), label: "7".to_string(), row: 1, col: 5, current_remap: None },
101 KeypadButton { id: "JOY_BTN_19".to_string(), label: "8".to_string(), row: 2, col: 5, current_remap: None },
102 KeypadButton { id: "JOY_BTN_20".to_string(), label: "9".to_string(), row: 3, col: 5, current_remap: None },
103 KeypadButton { id: "JOY_BTN_21".to_string(), label: "0".to_string(), row: 4, col: 5, current_remap: None },
104
105 KeypadButton { id: "JOY_BTN_22".to_string(), label: "TL".to_string(), row: 6, col: 0, current_remap: None },
107 KeypadButton { id: "JOY_BTN_23".to_string(), label: "TM".to_string(), row: 6, col: 1, current_remap: None },
108 KeypadButton { id: "JOY_BTN_24".to_string(), label: "TR".to_string(), row: 6, col: 2, current_remap: None },
109 KeypadButton { id: "JOY_BTN_25".to_string(), label: "BL".to_string(), row: 7, col: 0, current_remap: None },
110 KeypadButton { id: "JOY_BTN_26".to_string(), label: "BR".to_string(), row: 7, col: 1, current_remap: None },
111 ]
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct AutoSwitchRule {
119 pub app_id: String,
121 pub profile_name: String,
123 pub device_id: Option<String>,
125 pub layer_id: Option<usize>,
127}
128
129#[derive(Debug, Clone, Default)]
133pub struct AutoSwitchRulesView {
134 pub device_id: String,
136 pub rules: Vec<AutoSwitchRule>,
138 pub editing_rule: Option<usize>,
140 pub new_app_id: String,
142 pub new_profile_name: String,
144 pub new_layer_id: String,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct HotkeyBinding {
153 pub modifiers: Vec<String>,
155 pub key: String,
157 pub profile_name: String,
159 pub device_id: Option<String>,
161 pub layer_id: Option<usize>,
163}
164
165#[derive(Debug, Clone, Default)]
169pub struct HotkeyBindingsView {
170 pub device_id: String,
172 pub bindings: Vec<HotkeyBinding>,
174 pub editing_binding: Option<usize>,
176 pub new_modifiers: Vec<String>,
178 pub new_key: String,
180 pub new_profile_name: String,
182 pub new_layer_id: String,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum DeadzoneShape {
189 Circular,
190 Square,
191}
192
193impl std::fmt::Display for DeadzoneShape {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 match self {
196 DeadzoneShape::Circular => write!(f, "Circular"),
197 DeadzoneShape::Square => write!(f, "Square"),
198 }
199 }
200}
201
202impl DeadzoneShape {
203 pub const ALL: [DeadzoneShape; 2] = [DeadzoneShape::Circular, DeadzoneShape::Square];
204}
205
206impl Default for DeadzoneShape {
207 fn default() -> Self {
208 Self::Circular
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum SensitivityCurve {
215 Linear,
216 Quadratic,
217 Exponential,
218}
219
220impl std::fmt::Display for SensitivityCurve {
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222 match self {
223 SensitivityCurve::Linear => write!(f, "Linear"),
224 SensitivityCurve::Quadratic => write!(f, "Quadratic"),
225 SensitivityCurve::Exponential => write!(f, "Exponential"),
226 }
227 }
228}
229
230impl SensitivityCurve {
231 pub const ALL: [SensitivityCurve; 3] = [
232 SensitivityCurve::Linear,
233 SensitivityCurve::Quadratic,
234 SensitivityCurve::Exponential,
235 ];
236}
237
238impl Default for SensitivityCurve {
239 fn default() -> Self {
240 Self::Linear
241 }
242}
243
244#[derive(Debug, Clone)]
249pub struct CalibrationConfig {
250 pub deadzone: f32,
251 pub deadzone_shape: String,
252 pub sensitivity: String,
253 pub sensitivity_multiplier: f32,
254 pub range_min: i32,
255 pub range_max: i32,
256 pub invert_x: bool,
257 pub invert_y: bool,
258 pub exponent: f32,
259}
260
261impl Default for CalibrationConfig {
262 fn default() -> Self {
263 Self {
264 deadzone: 0.15,
265 deadzone_shape: "circular".to_string(),
266 sensitivity: "linear".to_string(),
267 sensitivity_multiplier: 1.0,
268 range_min: -32768,
269 range_max: 32767,
270 invert_x: false,
271 invert_y: false,
272 exponent: 2.0,
273 }
274 }
275}
276
277#[derive(Debug)]
281pub struct AnalogCalibrationView {
282 pub device_id: String,
284 pub layer_id: usize,
286 pub calibration: CalibrationConfig,
288
289 pub deadzone_shape_selected: DeadzoneShape,
291 pub sensitivity_curve_selected: SensitivityCurve,
293
294 pub analog_mode_selected: AnalogMode,
296 pub camera_mode_selected: CameraOutputMode,
298
299 pub invert_x_checked: bool,
301 pub invert_y_checked: bool,
302
303 pub stick_x: f32,
305 pub stick_y: f32,
307
308 pub loading: bool,
310 pub error: Option<String>,
312
313 pub last_visualizer_update: Instant,
316
317 pub visualizer_cache: Arc<iced::widget::canvas::Cache>,
321}
322
323impl Clone for AnalogCalibrationView {
326 fn clone(&self) -> Self {
327 Self {
328 device_id: self.device_id.clone(),
329 layer_id: self.layer_id,
330 calibration: self.calibration.clone(),
331 deadzone_shape_selected: self.deadzone_shape_selected,
332 sensitivity_curve_selected: self.sensitivity_curve_selected,
333 analog_mode_selected: self.analog_mode_selected,
334 camera_mode_selected: self.camera_mode_selected,
335 invert_x_checked: self.invert_x_checked,
336 invert_y_checked: self.invert_y_checked,
337 stick_x: self.stick_x,
338 stick_y: self.stick_y,
339 loading: self.loading,
340 error: self.error.clone(),
341 last_visualizer_update: Instant::now(),
343 visualizer_cache: Arc::clone(&self.visualizer_cache),
345 }
346 }
347}
348
349impl Default for AnalogCalibrationView {
350 fn default() -> Self {
351 Self {
352 device_id: String::new(),
353 layer_id: 0,
354 calibration: CalibrationConfig::default(),
355 deadzone_shape_selected: DeadzoneShape::Circular,
356 sensitivity_curve_selected: SensitivityCurve::Linear,
357 analog_mode_selected: AnalogMode::Disabled,
358 camera_mode_selected: CameraOutputMode::Scroll,
359 invert_x_checked: false,
360 invert_y_checked: false,
361 stick_x: 0.0,
362 stick_y: 0.0,
363 loading: false,
364 error: None,
365 last_visualizer_update: Instant::now(),
366 visualizer_cache: Arc::new(iced::widget::canvas::Cache::default()),
367 }
368 }
369}
370
371#[derive(Debug, Clone)]
376pub struct LedState {
377 pub zone_colors: HashMap<LedZone, (u8, u8, u8)>,
379 pub global_brightness: u8,
381 pub zone_brightness: HashMap<LedZone, u8>,
383 pub active_pattern: LedPattern,
385}
386
387impl Default for LedState {
388 fn default() -> Self {
389 Self {
390 zone_colors: HashMap::new(),
391 global_brightness: 100,
392 zone_brightness: HashMap::new(),
393 active_pattern: LedPattern::Static,
394 }
395 }
396}
397
398pub struct State {
399 pub devices: Vec<DeviceInfo>,
400 pub macros: Vec<MacroEntry>,
401 pub selected_device: Option<usize>,
402 pub status: String,
403 pub status_history: VecDeque<String>,
404 pub loading: bool,
405 pub recording: bool,
406 pub recording_macro_name: Option<String>,
407 pub daemon_connected: bool,
408 pub new_macro_name: String,
409 pub socket_path: PathBuf,
410 pub recently_updated_macros: HashMap<String, Instant>,
411 pub grabbed_devices: HashSet<String>,
412 pub profile_name: String,
413 pub active_tab: Tab,
414 pub notifications: VecDeque<Notification>,
415 pub recording_pulse: bool,
416 pub device_profiles: HashMap<String, Vec<String>>,
418 pub active_profiles: HashMap<String, String>,
420 pub remap_profiles: HashMap<String, Vec<RemapProfileInfo>>,
422 pub active_remap_profiles: HashMap<String, String>,
424 pub active_remaps: HashMap<String, (String, Vec<RemapEntry>)>,
426 pub keypad_layout: Vec<KeypadButton>,
428 pub keypad_view_device: Option<String>,
430 pub selected_button: Option<usize>,
432 pub device_capabilities: Option<DeviceCapabilities>,
434 pub active_layers: HashMap<String, usize>,
436 pub layer_configs: HashMap<String, Vec<LayerConfigInfo>>,
438 pub layer_config_dialog: Option<(String, usize, String, LayerMode)>,
440 pub analog_dpad_modes: HashMap<String, String>,
442 pub analog_deadzones_xy: HashMap<String, (u8, u8)>,
444 pub analog_outer_deadzones_xy: HashMap<String, (u8, u8)>,
446 pub led_states: HashMap<String, LedState>,
448 pub led_config_device: Option<String>,
450 pub selected_led_zone: Option<LedZone>,
452 pub pending_led_color: Option<(u8, u8, u8)>,
454 pub current_focus: Option<String>,
456 pub focus_tracking_active: bool,
458 pub auto_switch_view: Option<AutoSwitchRulesView>,
460 pub hotkey_view: Option<HotkeyBindingsView>,
462 pub analog_calibration_view: Option<AnalogCalibrationView>,
464 pub macro_settings: MacroSettings,
466 pub current_theme: Theme,
468}
469
470impl Default for State {
471 fn default() -> Self {
472 let socket_path = if cfg!(target_os = "linux") {
473 PathBuf::from("/run/aethermap/aethermap.sock")
474 } else if cfg!(target_os = "macos") {
475 PathBuf::from("/tmp/aethermap.sock")
476 } else {
477 std::env::temp_dir().join("aethermap.sock")
478 };
479 State {
480 devices: Vec::new(),
481 macros: Vec::new(),
482 selected_device: None,
483 status: "Initializing...".to_string(),
484 status_history: VecDeque::with_capacity(10),
485 loading: false,
486 recording: false,
487 recording_macro_name: None,
488 daemon_connected: false,
489 new_macro_name: String::new(),
490 socket_path,
491 recently_updated_macros: HashMap::new(),
492 grabbed_devices: HashSet::new(),
493 profile_name: "default".to_string(),
494 active_tab: Tab::Devices,
495 notifications: VecDeque::with_capacity(5),
496 recording_pulse: false,
497 device_profiles: HashMap::new(),
498 active_profiles: HashMap::new(),
499 remap_profiles: HashMap::new(),
500 active_remap_profiles: HashMap::new(),
501 active_remaps: HashMap::new(),
502 keypad_layout: Vec::new(),
503 keypad_view_device: None,
504 selected_button: None,
505 device_capabilities: None,
506 active_layers: HashMap::new(),
507 layer_configs: HashMap::new(),
508 layer_config_dialog: None,
509 analog_dpad_modes: HashMap::new(),
510 analog_deadzones_xy: HashMap::new(),
511 analog_outer_deadzones_xy: HashMap::new(),
512 led_states: HashMap::new(),
513 led_config_device: None,
514 selected_led_zone: None,
515 pending_led_color: None,
516 current_focus: None,
517 focus_tracking_active: false,
518 auto_switch_view: None,
519 hotkey_view: None,
520 analog_calibration_view: None,
521 macro_settings: MacroSettings {
522 latency_offset_ms: 0,
523 jitter_pct: 0.0,
524 capture_mouse: false,
525 },
526 current_theme: aether_dark(),
527 }
528 }
529}
530
531#[derive(Debug, Clone)]
532pub enum Message {
533 SwitchTab(Tab),
535 ThemeChanged(iced::Theme),
536
537 LoadDevices,
539 DevicesLoaded(Result<Vec<DeviceInfo>, String>),
540 GrabDevice(String),
541 UngrabDevice(String),
542 DeviceGrabbed(Result<String, String>),
543 DeviceUngrabbed(Result<String, String>),
544 SelectDevice(usize),
545
546 UpdateMacroName(String),
548 StartRecording,
549 StopRecording,
550 RecordingStarted(Result<String, String>),
551 RecordingStopped(Result<MacroEntry, String>),
552
553 LoadMacros,
555 MacrosLoaded(Result<Vec<MacroEntry>, String>),
556 LoadMacroSettings,
557 MacroSettingsLoaded(Result<MacroSettings, String>),
558 SetMacroSettings(MacroSettings),
559 LatencyChanged(u32),
560 JitterChanged(f32),
561 CaptureMouseToggled(bool),
562 PlayMacro(String),
563 MacroPlayed(Result<String, String>),
564 DeleteMacro(String),
565 MacroDeleted(Result<String, String>),
566
567 UpdateProfileName(String),
569 SaveProfile,
570 ProfileSaved(Result<(String, usize), String>),
571 LoadProfile,
572 ProfileLoaded(Result<(String, usize), String>),
573
574 LoadDeviceProfiles(String),
576 DeviceProfilesLoaded(String, Result<Vec<String>, String>),
577 ActivateProfile(String, String),
578 ProfileActivated(String, String),
579 DeactivateProfile(String),
580 ProfileDeactivated(String),
581 ProfileError(String),
582
583 LoadRemapProfiles(String),
585 RemapProfilesLoaded(String, Result<Vec<RemapProfileInfo>, String>),
586 ActivateRemapProfile(String, String),
587 RemapProfileActivated(String, String),
588 DeactivateRemapProfile(String),
589 RemapProfileDeactivated(String),
590 LoadActiveRemaps(String),
591 ActiveRemapsLoaded(String, Result<Option<(String, Vec<RemapEntry>)>, String>),
592
593 CheckDaemonConnection,
595 DaemonStatusChanged(bool),
596
597 TickAnimations,
599 ShowNotification(String, bool), RecordMouseEvent {
603 event_type: String,
604 button: Option<u16>,
605 x: i32,
606 y: i32,
607 delta: i32,
608 },
609
610 ShowKeypadView(String),
613 SelectKeypadButton(String),
615 DeviceCapabilitiesLoaded(String, Result<DeviceCapabilities, String>),
617
618 LayerStateChanged(String, usize),
621 LayerConfigRequested(String),
623 LayerActivateRequested(String, usize, LayerMode),
625 LayerConfigUpdated(String, LayerConfigInfo),
627 OpenLayerConfigDialog(String, usize),
629 LayerConfigNameChanged(String),
631 LayerConfigModeChanged(LayerMode),
633 SaveLayerConfig,
635 CancelLayerConfig,
637 RefreshLayers,
639 LayerListLoaded(String, Vec<LayerConfigInfo>),
641
642 AnalogDpadModeRequested(String),
645 AnalogDpadModeLoaded(String, String),
647 SetAnalogDpadMode(String, String),
649 AnalogDpadModeSet(Result<(), String>),
651
652 AnalogDeadzoneXYRequested(String),
655 AnalogDeadzoneXYLoaded(String, (u8, u8)),
657 SetAnalogDeadzoneXY(String, u8, u8),
659 AnalogDeadzoneXYSet(Result<(), String>),
661 AnalogOuterDeadzoneXYRequested(String),
663 AnalogOuterDeadzoneXYLoaded(String, (u8, u8)),
665 SetAnalogOuterDeadzoneXY(String, u8, u8),
667 AnalogOuterDeadzoneXYSet(Result<(), String>),
669
670 OpenLedConfig(String),
673 CloseLedConfig,
675 SelectLedZone(LedZone),
677 SetLedColor(String, LedZone, u8, u8, u8),
679 LedColorSet(Result<(), String>),
681 SetLedBrightness(String, Option<LedZone>, u8),
683 LedBrightnessSet(Result<(), String>),
685 SetLedPattern(String, LedPattern),
687 LedPatternSet(Result<(), String>),
689 RefreshLedState(String),
691 LedStateLoaded(String, Result<HashMap<LedZone, (u8, u8, u8)>, String>),
693 LedSliderChanged(u8, u8, u8),
695
696 StartFocusTracking,
699 FocusTrackingStarted(Result<bool, String>),
701 FocusChanged(String, Option<String>), ShowAutoSwitchRules(String),
707 CloseAutoSwitchRules,
709 LoadAutoSwitchRules(String),
711 AutoSwitchRulesLoaded(Result<Vec<AutoSwitchRule>, String>),
713 EditAutoSwitchRule(usize),
715 AutoSwitchAppIdChanged(String),
717 AutoSwitchProfileNameChanged(String),
719 AutoSwitchLayerIdChanged(String),
721 AutoSwitchUseCurrentApp,
723 SaveAutoSwitchRule,
725 DeleteAutoSwitchRule(usize),
727
728 ShowHotkeyBindings(String),
731 CloseHotkeyBindings,
733 LoadHotkeyBindings(String),
735 HotkeyBindingsLoaded(Result<Vec<HotkeyBinding>, String>),
737 EditHotkeyBinding(usize),
739 ToggleHotkeyModifier(String),
741 HotkeyKeyChanged(String),
743 HotkeyProfileNameChanged(String),
745 HotkeyLayerIdChanged(String),
747 SaveHotkeyBinding,
749 DeleteHotkeyBinding(usize),
751 HotkeyBindingsUpdated(Vec<HotkeyBinding>),
753
754 OpenAnalogCalibration {
757 device_id: String,
758 layer_id: usize,
759 },
760 AnalogDeadzoneChanged(f32),
762 AnalogDeadzoneShapeChanged(DeadzoneShape),
763 AnalogSensitivityChanged(f32),
764 AnalogSensitivityCurveChanged(SensitivityCurve),
765 AnalogRangeMinChanged(i32),
766 AnalogRangeMaxChanged(i32),
767 AnalogInvertXToggled(bool),
768 AnalogInvertYToggled(bool),
769 AnalogModeChanged(AnalogMode),
771 CameraModeChanged(CameraOutputMode),
773 ApplyAnalogCalibration,
775 AnalogCalibrationLoaded(Result<aethermap_common::AnalogCalibrationConfig, String>),
777 AnalogCalibrationApplied(Result<(), String>),
779 CloseAnalogCalibration,
781 AnalogInputUpdated(f32, f32), }
784
785#[allow(dead_code)]
787pub enum _FutureMessage {
788 DismissNotification,
789}
790
791impl Application for State {
792 type Message = Message;
793 type Theme = Theme;
794 type Executor = iced::executor::Default;
795 type Flags = ();
796
797 fn new(_flags: ()) -> (Self, Command<Message>) {
798 let initial_state = State::default();
799 let initial_commands = Command::batch([
800 Command::perform(async { Message::CheckDaemonConnection }, |msg| msg),
801 Command::perform(async { Message::LoadDevices }, |msg| msg),
802 Command::perform(async { Message::LoadMacroSettings }, |msg| msg),
803 ]);
804 (initial_state, initial_commands)
805 }
806
807 fn title(&self) -> String {
808 String::from("Aethermap")
809 }
810
811 fn theme(&self) -> Theme {
812 self.current_theme.clone()
813 }
814
815 fn update(&mut self, message: Message) -> Command<Message> {
816 match message {
817 Message::ThemeChanged(theme) => {
818 self.current_theme = theme;
819 Command::none()
820 }
821 Message::SwitchTab(tab) => {
822 self.active_tab = tab;
823 Command::none()
824 }
825 Message::SelectDevice(idx) => {
826 self.selected_device = Some(idx);
827 if let Some(device) = self.devices.get(idx) {
829 let device_id = format!("{:04x}:{:04x}", device.vendor_id, device.product_id);
830 if device.device_type == DeviceType::Gamepad || device.device_type == DeviceType::Keypad {
831 let device_id_clone1 = device_id.clone();
832 let device_id_clone2 = device_id.clone();
833 let device_id_clone3 = device_id.clone();
834 return Command::batch(vec![
835 Command::none(),
836 Command::perform(async move { device_id_clone1 }, |id| Message::AnalogDpadModeRequested(id)),
837 Command::perform(async move { device_id_clone2 }, |id| Message::AnalogDeadzoneXYRequested(id)),
838 Command::perform(async move { device_id_clone3 }, |id| Message::AnalogOuterDeadzoneXYRequested(id)),
839 ]);
840 }
841 }
842 Command::none()
843 }
844 Message::CheckDaemonConnection => {
845 let socket_path = self.socket_path.clone();
846 Command::perform(
847 async move {
848 let client = crate::ipc::IpcClient::new(socket_path);
849 client.connect().await.is_ok()
850 },
851 Message::DaemonStatusChanged,
852 )
853 }
854 Message::DaemonStatusChanged(connected) => {
855 self.daemon_connected = connected;
856 if connected {
857 self.add_notification("Connected to daemon", false);
858 Command::perform(async { Message::StartFocusTracking }, |msg| msg)
860 } else {
861 self.add_notification("Daemon not running - start aethermapd", true);
862 Command::none()
863 }
864 }
865 Message::StartFocusTracking => {
866 Command::perform(
869 async move {
870 let wayland_available = std::env::var("WAYLAND_DISPLAY").is_ok();
872 if wayland_available {
873 tracing::info!("Focus tracking available (Wayland detected)");
874 } else {
875 tracing::warn!("Focus tracking unavailable (not on Wayland)");
876 }
877 wayland_available
878 },
879 |available| Message::FocusTrackingStarted(Ok(available)),
880 )
881 }
882 Message::FocusTrackingStarted(Ok(available)) => {
883 self.focus_tracking_active = available;
884 if available {
885 self.add_notification("Focus tracking enabled", false);
886 } else {
887 self.add_notification("Focus tracking unavailable (portal not connected)", true);
888 }
889 Command::none()
890 }
891 Message::FocusTrackingStarted(Err(e)) => {
892 self.add_notification(&format!("Focus tracking error: {}", e), true);
893 self.focus_tracking_active = false;
894 Command::none()
895 }
896 Message::FocusChanged(app_id, window_title) => {
897 self.current_focus = Some(app_id.clone());
899 let socket_path = self.socket_path.clone();
901 Command::perform(
902 async move {
903 let client = crate::ipc::IpcClient::new(socket_path);
904 client.send_focus_change(app_id, window_title).await
905 },
906 |result| match result {
907 Ok(()) => Message::TickAnimations, Err(e) => Message::ProfileError(format!("Focus change failed: {}", e)),
909 },
910 )
911 }
912
913 Message::ShowAutoSwitchRules(device_id) => {
915 self.auto_switch_view = Some(AutoSwitchRulesView {
916 device_id: device_id.clone(),
917 rules: Vec::new(),
918 editing_rule: None,
919 new_app_id: String::new(),
920 new_profile_name: String::new(),
921 new_layer_id: String::new(),
922 });
923 let device_id_clone = device_id.clone();
925 Command::perform(
926 async move { device_id_clone },
927 |id| Message::LoadAutoSwitchRules(id)
928 )
929 }
930 Message::CloseAutoSwitchRules => {
931 self.auto_switch_view = None;
932 Command::none()
933 }
934 Message::LoadAutoSwitchRules(_device_id) => {
935 let socket_path = self.socket_path.clone();
936 Command::perform(
937 async move {
938 let client = IpcClient::with_socket_path(&socket_path);
939 let request = Request::GetAutoSwitchRules;
940 match client.send(&request).await {
941 Ok(Response::AutoSwitchRules { rules }) => {
942 Ok(rules.into_iter().map(|r| AutoSwitchRule {
944 app_id: r.app_id,
945 profile_name: r.profile_name,
946 device_id: r.device_id,
947 layer_id: r.layer_id,
948 }).collect())
949 }
950 Ok(Response::Error(msg)) => Err(msg),
951 Err(e) => Err(format!("IPC error: {}", e)),
952 _ => Err("Unexpected response".to_string()),
953 }
954 },
955 Message::AutoSwitchRulesLoaded,
956 )
957 }
958 Message::AutoSwitchRulesLoaded(Ok(rules)) => {
959 self.auto_switch_view.as_mut().map(|view| {
960 view.rules = rules;
961 });
962 Command::none()
963 }
964 Message::AutoSwitchRulesLoaded(Err(error)) => {
965 self.add_notification(&format!("Failed to load auto-switch rules: {}", error), true);
966 Command::none()
967 }
968 Message::EditAutoSwitchRule(index) => {
969 if let Some(view) = &self.auto_switch_view {
970 if let Some(rule) = view.rules.get(index) {
971 self.auto_switch_view = Some(AutoSwitchRulesView {
972 device_id: view.device_id.clone(),
973 rules: view.rules.clone(),
974 editing_rule: Some(index),
975 new_app_id: rule.app_id.clone(),
976 new_profile_name: rule.profile_name.clone(),
977 new_layer_id: rule.layer_id.map(|id| id.to_string()).unwrap_or_default(),
978 });
979 }
980 }
981 Command::none()
982 }
983 Message::AutoSwitchAppIdChanged(value) => {
984 self.auto_switch_view.as_mut().map(|view| {
985 view.new_app_id = value;
986 });
987 Command::none()
988 }
989 Message::AutoSwitchProfileNameChanged(value) => {
990 self.auto_switch_view.as_mut().map(|view| {
991 view.new_profile_name = value;
992 });
993 Command::none()
994 }
995 Message::AutoSwitchLayerIdChanged(value) => {
996 self.auto_switch_view.as_mut().map(|view| {
997 view.new_layer_id = value;
998 });
999 Command::none()
1000 }
1001 Message::AutoSwitchUseCurrentApp => {
1002 if let Some(ref focus) = self.current_focus {
1003 self.auto_switch_view.as_mut().map(|view| {
1004 view.new_app_id = focus.clone();
1005 });
1006 }
1007 Command::none()
1008 }
1009 Message::SaveAutoSwitchRule => {
1010 if let Some(mut view) = self.auto_switch_view.clone() {
1011 let rule = AutoSwitchRule {
1012 app_id: view.new_app_id.clone(),
1013 profile_name: view.new_profile_name.clone(),
1014 device_id: Some(view.device_id.clone()),
1015 layer_id: view.new_layer_id.parse().ok(),
1016 };
1017
1018 if let Some(editing) = view.editing_rule {
1019 if editing < view.rules.len() {
1020 view.rules[editing] = rule.clone();
1021 }
1022 } else {
1023 view.rules.push(rule.clone());
1024 }
1025
1026 view.editing_rule = None;
1027 view.new_app_id = String::new();
1028 view.new_profile_name = String::new();
1029 view.new_layer_id = String::new();
1030
1031 let rules = view.rules.clone();
1032 let socket_path = self.socket_path.clone();
1033
1034 self.auto_switch_view = Some(view);
1036
1037 Command::perform(
1039 async move {
1040 let common_rules: Vec<CommonAutoSwitchRule> = rules.into_iter()
1042 .map(|r| CommonAutoSwitchRule {
1043 app_id: r.app_id,
1044 profile_name: r.profile_name,
1045 device_id: r.device_id,
1046 layer_id: r.layer_id,
1047 })
1048 .collect();
1049
1050 let client = IpcClient::with_socket_path(socket_path);
1051 let request = Request::SetAutoSwitchRules { rules: common_rules };
1052 match client.send(&request).await {
1053 Ok(Response::AutoSwitchRulesAck) => Ok(()),
1054 Ok(Response::Error(msg)) => Err(msg),
1055 Err(e) => Err(format!("IPC error: {}", e)),
1056 _ => Err("Unexpected response".to_string()),
1057 }
1058 },
1059 |result| match result {
1060 Ok(()) => Message::ShowNotification("Auto-switch rules saved".to_string(), false),
1061 Err(e) => Message::ShowNotification(format!("Failed to save rules: {}", e), true),
1062 }
1063 )
1064 } else {
1065 Command::none()
1066 }
1067 }
1068 Message::DeleteAutoSwitchRule(index) => {
1069 if let Some(view) = self.auto_switch_view.clone() {
1070 if index < view.rules.len() {
1071 let mut rules = view.rules.clone();
1072 rules.remove(index);
1073 let socket_path = self.socket_path.clone();
1074
1075 self.auto_switch_view.as_mut().map(|v| v.rules = rules.clone());
1077
1078 return Command::perform(
1080 async move {
1081 let common_rules: Vec<CommonAutoSwitchRule> = rules.into_iter()
1083 .map(|r| CommonAutoSwitchRule {
1084 app_id: r.app_id,
1085 profile_name: r.profile_name,
1086 device_id: r.device_id,
1087 layer_id: r.layer_id,
1088 })
1089 .collect();
1090
1091 let client = IpcClient::with_socket_path(&socket_path);
1092 let request = Request::SetAutoSwitchRules { rules: common_rules };
1093 match client.send(&request).await {
1094 Ok(Response::AutoSwitchRulesAck) => Ok(()),
1095 Ok(Response::Error(msg)) => Err(msg),
1096 Err(e) => Err(format!("IPC error: {}", e)),
1097 _ => Err("Unexpected response".to_string()),
1098 }
1099 },
1100 |result| match result {
1101 Ok(()) => Message::ShowNotification("Rule deleted".to_string(), false),
1102 Err(e) => Message::ShowNotification(format!("Failed to delete rule: {}", e), true),
1103 }
1104 );
1105 }
1106 }
1107 Command::none()
1108 }
1109
1110 Message::ShowHotkeyBindings(device_id) => {
1112 self.hotkey_view = Some(HotkeyBindingsView {
1113 device_id: device_id.clone(),
1114 bindings: Vec::new(),
1115 editing_binding: None,
1116 new_modifiers: Vec::new(),
1117 new_key: String::new(),
1118 new_profile_name: String::new(),
1119 new_layer_id: String::new(),
1120 });
1121 let device_id_clone = device_id.clone();
1123 Command::perform(
1124 async move { device_id_clone },
1125 |id| Message::LoadHotkeyBindings(id)
1126 )
1127 }
1128 Message::CloseHotkeyBindings => {
1129 self.hotkey_view = None;
1130 Command::none()
1131 }
1132 Message::LoadHotkeyBindings(device_id) => {
1133 let socket_path = self.socket_path.clone();
1134 Command::perform(
1135 async move {
1136 let client = IpcClient::with_socket_path(&socket_path);
1137 let request = Request::ListHotkeys { device_id };
1138 match client.send(&request).await {
1139 Ok(Response::HotkeyList { bindings, .. }) => {
1140 Ok(bindings.into_iter().map(|b| HotkeyBinding {
1142 modifiers: b.modifiers,
1143 key: b.key,
1144 profile_name: b.profile_name,
1145 device_id: b.device_id,
1146 layer_id: b.layer_id,
1147 }).collect())
1148 }
1149 Ok(Response::Error(msg)) => Err(msg),
1150 Err(e) => Err(format!("IPC error: {}", e)),
1151 _ => Err("Unexpected response".to_string()),
1152 }
1153 },
1154 Message::HotkeyBindingsLoaded,
1155 )
1156 }
1157 Message::HotkeyBindingsLoaded(Ok(bindings)) => {
1158 if let Some(view) = &mut self.hotkey_view {
1159 view.bindings = bindings;
1160 }
1161 Command::none()
1162 }
1163 Message::HotkeyBindingsLoaded(Err(error)) => {
1164 self.add_notification(&format!("Failed to load hotkey bindings: {}", error), true);
1165 Command::none()
1166 }
1167 Message::EditHotkeyBinding(index) => {
1168 if let Some(view) = &self.hotkey_view {
1169 if let Some(binding) = view.bindings.get(index) {
1170 self.hotkey_view = Some(HotkeyBindingsView {
1171 device_id: view.device_id.clone(),
1172 bindings: view.bindings.clone(),
1173 editing_binding: Some(index),
1174 new_modifiers: binding.modifiers.clone(),
1175 new_key: binding.key.clone(),
1176 new_profile_name: binding.profile_name.clone(),
1177 new_layer_id: binding.layer_id.map(|id| id.to_string()).unwrap_or_default(),
1178 });
1179 }
1180 }
1181 Command::none()
1182 }
1183 Message::ToggleHotkeyModifier(modifier) => {
1184 self.hotkey_view.as_mut().map(|view| {
1185 if view.new_modifiers.contains(&modifier) {
1186 view.new_modifiers.retain(|m| m != &modifier);
1187 } else {
1188 view.new_modifiers.push(modifier);
1189 }
1190 });
1191 Command::none()
1192 }
1193 Message::HotkeyKeyChanged(value) => {
1194 self.hotkey_view.as_mut().map(|view| {
1195 view.new_key = value;
1196 });
1197 Command::none()
1198 }
1199 Message::HotkeyProfileNameChanged(value) => {
1200 self.hotkey_view.as_mut().map(|view| {
1201 view.new_profile_name = value;
1202 });
1203 Command::none()
1204 }
1205 Message::HotkeyLayerIdChanged(value) => {
1206 self.hotkey_view.as_mut().map(|view| {
1207 view.new_layer_id = value;
1208 });
1209 Command::none()
1210 }
1211 Message::SaveHotkeyBinding => {
1212 if let Some(view) = &self.hotkey_view {
1213 let device_id = view.device_id.clone();
1214 let binding = CommonHotkeyBinding {
1215 modifiers: view.new_modifiers.clone(),
1216 key: view.new_key.clone(),
1217 profile_name: view.new_profile_name.clone(),
1218 device_id: Some(view.device_id.clone()),
1219 layer_id: if view.new_layer_id.is_empty() { None } else { view.new_layer_id.parse().ok() },
1220 };
1221 let socket_path = self.socket_path.clone();
1222
1223 if let Some(local_view) = &self.hotkey_view {
1225 let gui_binding = HotkeyBinding {
1226 modifiers: binding.modifiers.clone(),
1227 key: binding.key.clone(),
1228 profile_name: binding.profile_name.clone(),
1229 device_id: binding.device_id.clone(),
1230 layer_id: binding.layer_id,
1231 };
1232 let mut updated_view = local_view.clone();
1233 if let Some(editing) = local_view.editing_binding {
1234 if editing < local_view.bindings.len() {
1235 updated_view.bindings[editing] = gui_binding;
1236 }
1237 } else {
1238 updated_view.bindings.push(gui_binding);
1239 }
1240 updated_view.editing_binding = None;
1241 updated_view.new_modifiers = Vec::new();
1242 updated_view.new_key = String::new();
1243 updated_view.new_profile_name = String::new();
1244 updated_view.new_layer_id = String::new();
1245 self.hotkey_view = Some(updated_view);
1246 }
1247
1248 return Command::perform(
1249 async move {
1250 let client = IpcClient::with_socket_path(&socket_path);
1251 let request = Request::RegisterHotkey { device_id, binding };
1252 match client.send(&request).await {
1253 Ok(Response::HotkeyRegistered { .. }) => Ok(()),
1254 Ok(Response::Error(msg)) => Err(msg),
1255 Err(e) => Err(format!("IPC error: {}", e)),
1256 _ => Err("Unexpected response".to_string()),
1257 }
1258 },
1259 |result| match result {
1260 Ok(()) => Message::ShowNotification("Hotkey saved".to_string(), false),
1261 Err(e) => Message::ShowNotification(format!("Failed to save hotkey: {}", e), true),
1262 }
1263 );
1264 }
1265 Command::none()
1266 }
1267 Message::DeleteHotkeyBinding(index) => {
1268 if let Some(view) = &self.hotkey_view {
1269 if index < view.bindings.len() {
1270 let device_id = view.device_id.clone();
1271 let binding = view.bindings[index].clone();
1272 let socket_path = self.socket_path.clone();
1273
1274 let updated_bindings = view.bindings.iter()
1276 .enumerate()
1277 .filter(|(i, _)| *i != index)
1278 .map(|(_, b)| b.clone())
1279 .collect();
1280
1281 return Command::perform(
1282 async move {
1283 let client = IpcClient::with_socket_path(&socket_path);
1284 let request = Request::RemoveHotkey {
1285 device_id,
1286 key: binding.key.clone(),
1287 modifiers: binding.modifiers.clone(),
1288 };
1289 match client.send(&request).await {
1290 Ok(Response::HotkeyRemoved { .. }) => Ok(()),
1291 Ok(Response::Error(msg)) => Err(msg),
1292 Err(e) => Err(format!("IPC error: {}", e)),
1293 _ => Err("Unexpected response".to_string()),
1294 }
1295 },
1296 move |result| {
1297 if result.is_ok() {
1298 Message::HotkeyBindingsUpdated(updated_bindings)
1299 } else {
1300 let err_msg = result.unwrap_err();
1301 Message::ShowNotification(format!("Failed to delete hotkey: {}", err_msg), true)
1302 }
1303 }
1304 );
1305 }
1306 }
1307 Command::none()
1308 }
1309 Message::HotkeyBindingsUpdated(bindings) => {
1310 if let Some(view) = &mut self.hotkey_view {
1311 view.bindings = bindings;
1312 }
1313 self.add_notification("Hotkey deleted", false);
1314 Command::none()
1315 }
1316
1317 Message::OpenAnalogCalibration { device_id, layer_id } => {
1319 self.analog_calibration_view = Some(AnalogCalibrationView {
1321 device_id: device_id.clone(),
1322 layer_id,
1323 calibration: CalibrationConfig::default(),
1324 deadzone_shape_selected: DeadzoneShape::Circular,
1325 sensitivity_curve_selected: SensitivityCurve::Linear,
1326 analog_mode_selected: AnalogMode::Disabled,
1327 camera_mode_selected: CameraOutputMode::Scroll,
1328 invert_x_checked: false,
1329 invert_y_checked: false,
1330 stick_x: 0.0,
1331 stick_y: 0.0,
1332 loading: true,
1333 error: None,
1334 last_visualizer_update: Instant::now(),
1335 visualizer_cache: Arc::new(iced::widget::canvas::Cache::default()),
1336 });
1337
1338 let device_id_clone = device_id.clone();
1340 let socket_path = self.socket_path.clone();
1341
1342 let device_id_subscribe = device_id.clone();
1344 let socket_path_subscribe = self.socket_path.clone();
1345
1346 Command::batch(vec![
1347 Command::perform(
1349 async move {
1350 let client = crate::ipc::IpcClient::new(socket_path_subscribe);
1351 client.subscribe_analog_input(&device_id_subscribe).await
1352 },
1353 |result| match result {
1354 Ok(_) => Message::ShowNotification("Subscribed to analog input".to_string(), false),
1355 Err(e) => Message::ShowNotification(format!("Subscription failed: {}", e), true),
1356 },
1357 ),
1358 Command::perform(
1360 async move {
1361 let client = crate::ipc::IpcClient::new(socket_path);
1362 client.get_analog_calibration(&device_id_clone, layer_id).await
1363 },
1364 Message::AnalogCalibrationLoaded,
1365 ),
1366 ])
1367 }
1368 Message::AnalogCalibrationLoaded(Ok(calibration)) => {
1369 if let Some(view) = &mut self.analog_calibration_view {
1370 view.calibration = CalibrationConfig {
1372 deadzone: calibration.deadzone,
1373 deadzone_shape: calibration.deadzone_shape.clone(),
1374 sensitivity: calibration.sensitivity.clone(),
1375 sensitivity_multiplier: calibration.sensitivity_multiplier,
1376 range_min: calibration.range_min,
1377 range_max: calibration.range_max,
1378 invert_x: calibration.invert_x,
1379 invert_y: calibration.invert_y,
1380 exponent: calibration.exponent,
1381 };
1382 view.loading = false;
1383
1384 view.deadzone_shape_selected = match calibration.deadzone_shape.as_str() {
1386 "circular" => DeadzoneShape::Circular,
1387 "square" => DeadzoneShape::Square,
1388 _ => DeadzoneShape::Circular,
1389 };
1390 view.sensitivity_curve_selected = match calibration.sensitivity.as_str() {
1391 "linear" => SensitivityCurve::Linear,
1392 "quadratic" => SensitivityCurve::Quadratic,
1393 "exponential" => SensitivityCurve::Exponential,
1394 _ => SensitivityCurve::Linear,
1395 };
1396 view.invert_x_checked = calibration.invert_x;
1397 view.invert_y_checked = calibration.invert_y;
1398 }
1399 Command::none()
1400 }
1401 Message::AnalogCalibrationLoaded(Err(error)) => {
1402 if let Some(view) = &mut self.analog_calibration_view {
1403 view.error = Some(error);
1404 view.loading = false;
1405 }
1406 Command::none()
1407 }
1408 Message::AnalogDeadzoneChanged(value) => {
1409 if let Some(view) = &mut self.analog_calibration_view {
1410 view.calibration.deadzone = value;
1411 view.visualizer_cache.clear();
1413 }
1414 Command::none()
1415 }
1416 Message::AnalogDeadzoneShapeChanged(shape) => {
1417 if let Some(view) = &mut self.analog_calibration_view {
1418 view.deadzone_shape_selected = shape;
1419 view.calibration.deadzone_shape = shape.to_string().to_lowercase();
1420 view.visualizer_cache.clear();
1422 }
1423 Command::none()
1424 }
1425 Message::AnalogSensitivityChanged(value) => {
1426 if let Some(view) = &mut self.analog_calibration_view {
1427 view.calibration.sensitivity_multiplier = value;
1428 }
1429 Command::none()
1430 }
1431 Message::AnalogSensitivityCurveChanged(curve) => {
1432 if let Some(view) = &mut self.analog_calibration_view {
1433 view.sensitivity_curve_selected = curve;
1434 view.calibration.sensitivity = curve.to_string().to_lowercase();
1435 }
1436 Command::none()
1437 }
1438 Message::AnalogRangeMinChanged(value) => {
1439 if let Some(view) = &mut self.analog_calibration_view {
1440 view.calibration.range_min = value;
1441 }
1442 Command::none()
1443 }
1444 Message::AnalogRangeMaxChanged(value) => {
1445 if let Some(view) = &mut self.analog_calibration_view {
1446 view.calibration.range_max = value;
1447 }
1448 Command::none()
1449 }
1450 Message::AnalogInvertXToggled(checked) => {
1451 if let Some(view) = &mut self.analog_calibration_view {
1452 view.invert_x_checked = checked;
1453 view.calibration.invert_x = checked;
1454 }
1455 Command::none()
1456 }
1457 Message::AnalogInvertYToggled(checked) => {
1458 if let Some(view) = &mut self.analog_calibration_view {
1459 view.invert_y_checked = checked;
1460 view.calibration.invert_y = checked;
1461 }
1462 Command::none()
1463 }
1464 Message::AnalogModeChanged(mode) => {
1465 if let Some(view) = &mut self.analog_calibration_view {
1466 view.analog_mode_selected = mode;
1467 }
1468 Command::none()
1469 }
1470 Message::CameraModeChanged(mode) => {
1471 if let Some(view) = &mut self.analog_calibration_view {
1472 view.camera_mode_selected = mode;
1473 }
1474 Command::none()
1475 }
1476 Message::ApplyAnalogCalibration => {
1477 if let Some(view) = self.analog_calibration_view.clone() {
1478 let device_id = view.device_id.clone();
1479 let layer_id = view.layer_id;
1480 let calibration = aethermap_common::AnalogCalibrationConfig {
1481 deadzone: view.calibration.deadzone,
1482 deadzone_shape: view.calibration.deadzone_shape.clone(),
1483 sensitivity: view.calibration.sensitivity.clone(),
1484 sensitivity_multiplier: view.calibration.sensitivity_multiplier,
1485 range_min: view.calibration.range_min,
1486 range_max: view.calibration.range_max,
1487 invert_x: view.calibration.invert_x,
1488 invert_y: view.calibration.invert_y,
1489 exponent: view.calibration.exponent,
1490 analog_mode: view.analog_mode_selected,
1491 camera_output_mode: if view.analog_mode_selected == aethermap_common::AnalogMode::Camera {
1492 Some(view.camera_mode_selected)
1493 } else {
1494 None
1495 },
1496 };
1497 let socket_path = self.socket_path.clone();
1498
1499 return Command::perform(
1500 async move {
1501 let client = crate::ipc::IpcClient::new(socket_path);
1502 client.set_analog_calibration(&device_id, layer_id, calibration).await
1503 .map_err(|e| e.to_string())
1504 },
1505 Message::AnalogCalibrationApplied,
1506 );
1507 }
1508 Command::none()
1509 }
1510 Message::AnalogCalibrationApplied(Ok(())) => {
1511 self.add_notification("Calibration saved successfully", false);
1512 Command::none()
1513 }
1514 Message::AnalogCalibrationApplied(Err(error)) => {
1515 self.add_notification(&format!("Failed to save calibration: {}", error), true);
1516 if let Some(view) = &mut self.analog_calibration_view {
1517 let mut view = view.clone();
1518 view.error = Some(error);
1519 self.analog_calibration_view = Some(view);
1520 }
1521 Command::none()
1522 }
1523 Message::CloseAnalogCalibration => {
1524 let device_id = self.analog_calibration_view.as_ref()
1526 .map(|v| v.device_id.clone())
1527 .unwrap_or_default();
1528 let socket_path = self.socket_path.clone();
1529
1530 self.analog_calibration_view = None;
1531
1532 let _ = std::thread::spawn(move || {
1535 let rt = tokio::runtime::Runtime::new().unwrap();
1536 rt.block_on(async move {
1537 let client = crate::ipc::IpcClient::new(socket_path);
1538 if let Err(e) = client.unsubscribe_analog_input(&device_id).await {
1539 eprintln!("Failed to unsubscribe: {}", e);
1540 }
1541 });
1542 });
1543
1544 Command::none()
1545 }
1546 Message::AnalogInputUpdated(x, y) => {
1547 if let Some(view) = &mut self.analog_calibration_view {
1550 if view.last_visualizer_update.elapsed() >= Duration::from_millis(33) {
1551 view.stick_x = x;
1552 view.stick_y = y;
1553 view.last_visualizer_update = Instant::now();
1554 Command::none() } else {
1556 Command::none() }
1558 } else {
1559 Command::none()
1560 }
1561 }
1562
1563 Message::LoadDevices => {
1564 let socket_path = self.socket_path.clone();
1565 self.loading = true;
1566 Command::perform(
1567 async move {
1568 let client = crate::ipc::IpcClient::new(socket_path);
1569 client.get_devices().await.map_err(|e| e.to_string())
1570 },
1571 Message::DevicesLoaded,
1572 )
1573 }
1574 Message::DevicesLoaded(Ok(devices)) => {
1575 let count = devices.len();
1576 self.devices = devices;
1577 self.loading = false;
1578 self.add_notification(&format!("Found {} devices", count), false);
1579 Command::perform(async { Message::LoadMacros }, |msg| msg)
1580 }
1581 Message::DevicesLoaded(Err(e)) => {
1582 self.loading = false;
1583 self.add_notification(&format!("Error: {}", e), true);
1584 Command::none()
1585 }
1586 Message::LoadMacros => {
1587 let socket_path = self.socket_path.clone();
1588 Command::perform(
1589 async move {
1590 let client = crate::ipc::IpcClient::new(socket_path);
1591 client.list_macros().await.map_err(|e| e.to_string())
1592 },
1593 Message::MacrosLoaded,
1594 )
1595 }
1596 Message::MacrosLoaded(Ok(macros)) => {
1597 let count = macros.len();
1598 self.macros = macros;
1599 self.add_notification(&format!("Loaded {} macros", count), false);
1600 Command::none()
1601 }
1602 Message::MacrosLoaded(Err(e)) => {
1603 self.add_notification(&format!("Error loading macros: {}", e), true);
1604 Command::none()
1605 }
1606 Message::LoadMacroSettings => {
1607 let socket_path = self.socket_path.clone();
1608 Command::perform(
1609 async move {
1610 let client = crate::ipc::IpcClient::new(socket_path);
1611 client.get_macro_settings().await.map_err(|e| e.to_string())
1612 },
1613 Message::MacroSettingsLoaded,
1614 )
1615 }
1616 Message::MacroSettingsLoaded(Ok(settings)) => {
1617 self.macro_settings = settings;
1618 Command::none()
1619 }
1620 Message::MacroSettingsLoaded(Err(e)) => {
1621 self.add_notification(&format!("Error loading macro settings: {}", e), true);
1622 Command::none()
1623 }
1624 Message::SetMacroSettings(settings) => {
1625 let socket_path = self.socket_path.clone();
1626 Command::perform(
1627 async move {
1628 let client = crate::ipc::IpcClient::new(socket_path);
1629 client.set_macro_settings(settings).await.map_err(|e| e.to_string())
1630 },
1631 |result| match result {
1632 Ok(_) => Message::TickAnimations, Err(e) => Message::ShowNotification(format!("Failed to save settings: {}", e), true),
1634 }
1635 )
1636 }
1637 Message::LatencyChanged(ms) => {
1638 self.macro_settings.latency_offset_ms = ms;
1639 let settings = self.macro_settings.clone();
1640 Command::perform(async move { Message::SetMacroSettings(settings) }, |msg| msg)
1641 }
1642 Message::JitterChanged(pct) => {
1643 self.macro_settings.jitter_pct = pct;
1644 let settings = self.macro_settings.clone();
1645 Command::perform(async move { Message::SetMacroSettings(settings) }, |msg| msg)
1646 }
1647 Message::CaptureMouseToggled(enabled) => {
1648 self.macro_settings.capture_mouse = enabled;
1649 let settings = self.macro_settings.clone();
1650 Command::perform(async move { Message::SetMacroSettings(settings) }, |msg| msg)
1651 }
1652 Message::PlayMacro(macro_name) => {
1653 let socket_path = self.socket_path.clone();
1654 let name = macro_name.clone();
1655 Command::perform(
1656 async move {
1657 let client = crate::ipc::IpcClient::new(socket_path);
1658 client.test_macro(&name).await.map(|_| name).map_err(|e| e.to_string())
1659 },
1660 Message::MacroPlayed,
1661 )
1662 }
1663 Message::MacroPlayed(Ok(name)) => {
1664 self.add_notification(&format!("Played macro: {}", name), false);
1665 Command::none()
1666 }
1667 Message::MacroPlayed(Err(e)) => {
1668 self.add_notification(&format!("Failed to play: {}", e), true);
1669 Command::none()
1670 }
1671 Message::UpdateMacroName(name) => {
1672 self.new_macro_name = name;
1673 Command::none()
1674 }
1675 Message::UpdateProfileName(name) => {
1676 self.profile_name = name;
1677 Command::none()
1678 }
1679 Message::StartRecording => {
1680 if self.new_macro_name.trim().is_empty() {
1681 self.add_notification("Enter a macro name first", true);
1682 return Command::none();
1683 }
1684 if self.grabbed_devices.is_empty() {
1685 self.add_notification("Grab a device first", true);
1686 return Command::none();
1687 }
1688
1689 let device_path = self.grabbed_devices.iter().next().unwrap().clone();
1690 let socket_path = self.socket_path.clone();
1691 let macro_name = self.new_macro_name.clone();
1692 let capture_mouse = self.macro_settings.capture_mouse;
1693 self.recording = true;
1694 self.recording_macro_name = Some(macro_name.clone());
1695
1696 Command::perform(
1697 async move {
1698 let client = crate::ipc::IpcClient::new(socket_path);
1699 client.start_recording_macro(&device_path, ¯o_name, capture_mouse)
1700 .await
1701 .map(|_| macro_name)
1702 .map_err(|e| e.to_string())
1703 },
1704 Message::RecordingStarted,
1705 )
1706 }
1707 Message::RecordingStarted(Ok(name)) => {
1708 self.add_notification(&format!("Recording '{}' - Press keys now!", name), false);
1709 Command::none()
1710 }
1711 Message::RecordingStarted(Err(e)) => {
1712 self.recording = false;
1713 self.recording_macro_name = None;
1714 self.add_notification(&format!("Failed to start recording: {}", e), true);
1715 Command::none()
1716 }
1717 Message::StopRecording => {
1718 let socket_path = self.socket_path.clone();
1719 Command::perform(
1720 async move {
1721 let client = crate::ipc::IpcClient::new(socket_path);
1722 client.stop_recording_macro().await.map_err(|e| e.to_string())
1723 },
1724 Message::RecordingStopped,
1725 )
1726 }
1727 Message::RecordingStopped(Ok(macro_entry)) => {
1728 let name = macro_entry.name.clone();
1729 self.macros.push(macro_entry);
1730 self.recording = false;
1731 self.recording_macro_name = None;
1732 self.recently_updated_macros.insert(name.clone(), Instant::now());
1733 self.new_macro_name.clear();
1734 self.add_notification(&format!("Recorded macro: {}", name), false);
1735 Command::none()
1736 }
1737 Message::RecordingStopped(Err(e)) => {
1738 self.recording = false;
1739 self.recording_macro_name = None;
1740 self.add_notification(&format!("Recording failed: {}", e), true);
1741 Command::none()
1742 }
1743 Message::DeleteMacro(macro_name) => {
1744 let socket_path = self.socket_path.clone();
1745 let name = macro_name.clone();
1746 Command::perform(
1747 async move {
1748 let client = crate::ipc::IpcClient::new(socket_path);
1749 client.delete_macro(&name).await.map(|_| name).map_err(|e| e.to_string())
1750 },
1751 Message::MacroDeleted,
1752 )
1753 }
1754 Message::MacroDeleted(Ok(name)) => {
1755 self.macros.retain(|m| m.name != name);
1756 self.add_notification(&format!("Deleted: {}", name), false);
1757 Command::none()
1758 }
1759 Message::MacroDeleted(Err(e)) => {
1760 self.add_notification(&format!("Delete failed: {}", e), true);
1761 Command::none()
1762 }
1763 Message::SaveProfile => {
1764 if self.profile_name.trim().is_empty() {
1765 self.add_notification("Enter a profile name", true);
1766 return Command::none();
1767 }
1768 let socket_path = self.socket_path.clone();
1769 let name = self.profile_name.clone();
1770 Command::perform(
1771 async move {
1772 let client = crate::ipc::IpcClient::new(socket_path);
1773 client.save_profile(&name).await.map_err(|e| e.to_string())
1774 },
1775 Message::ProfileSaved,
1776 )
1777 }
1778 Message::ProfileSaved(Ok((name, count))) => {
1779 self.add_notification(&format!("Saved '{}' ({} macros)", name, count), false);
1780 Command::none()
1781 }
1782 Message::ProfileSaved(Err(e)) => {
1783 self.add_notification(&format!("Save failed: {}", e), true);
1784 Command::none()
1785 }
1786 Message::LoadProfile => {
1787 if self.profile_name.trim().is_empty() {
1788 self.add_notification("Enter a profile name to load", true);
1789 return Command::none();
1790 }
1791 let socket_path = self.socket_path.clone();
1792 let name = self.profile_name.clone();
1793 Command::perform(
1794 async move {
1795 let client = crate::ipc::IpcClient::new(socket_path);
1796 client.load_profile(&name).await.map_err(|e| e.to_string())
1797 },
1798 Message::ProfileLoaded,
1799 )
1800 }
1801 Message::ProfileLoaded(Ok((name, count))) => {
1802 self.add_notification(&format!("Loaded '{}' ({} macros)", name, count), false);
1803 Command::perform(async { Message::LoadMacros }, |msg| msg)
1804 }
1805 Message::ProfileLoaded(Err(e)) => {
1806 self.add_notification(&format!("Load failed: {}", e), true);
1807 Command::none()
1808 }
1809 Message::TickAnimations => {
1810 let now = Instant::now();
1811 self.recently_updated_macros.retain(|_, timestamp| {
1812 now.duration_since(*timestamp) < Duration::from_secs(3)
1813 });
1814 self.recording_pulse = !self.recording_pulse;
1815 while let Some(notif) = self.notifications.front() {
1817 if now.duration_since(notif.timestamp) > Duration::from_secs(5) {
1818 self.notifications.pop_front();
1819 } else {
1820 break;
1821 }
1822 }
1823 Command::none()
1824 }
1825 Message::ShowNotification(message, is_error) => {
1826 self.add_notification(&message, is_error);
1827 Command::none()
1828 }
1829 Message::GrabDevice(device_path) => {
1830 let socket_path = self.socket_path.clone();
1831 let path_clone = device_path.clone();
1832 Command::perform(
1833 async move {
1834 let client = crate::ipc::IpcClient::new(socket_path);
1835 client.grab_device(&path_clone).await.map(|_| path_clone).map_err(|e| e.to_string())
1836 },
1837 Message::DeviceGrabbed,
1838 )
1839 }
1840 Message::UngrabDevice(device_path) => {
1841 let socket_path = self.socket_path.clone();
1842 let path_clone = device_path.clone();
1843 Command::perform(
1844 async move {
1845 let client = crate::ipc::IpcClient::new(socket_path);
1846 client.ungrab_device(&path_clone).await.map(|_| path_clone).map_err(|e| e.to_string())
1847 },
1848 Message::DeviceUngrabbed,
1849 )
1850 }
1851 Message::DeviceGrabbed(Ok(device_path)) => {
1852 self.grabbed_devices.insert(device_path.clone());
1853 if let Some(idx) = self.devices.iter().position(|d| d.path.to_string_lossy() == device_path) {
1854 self.selected_device = Some(idx);
1855 }
1856 self.add_notification("Device grabbed - ready for recording", false);
1857 Command::none()
1858 }
1859 Message::DeviceGrabbed(Err(e)) => {
1860 self.add_notification(&format!("Grab failed: {}", e), true);
1861 Command::none()
1862 }
1863 Message::DeviceUngrabbed(Ok(device_path)) => {
1864 self.grabbed_devices.remove(&device_path);
1865 self.add_notification("Device released", false);
1866 Command::none()
1867 }
1868 Message::DeviceUngrabbed(Err(e)) => {
1869 self.add_notification(&format!("Release failed: {}", e), true);
1870 Command::none()
1871 }
1872 Message::LoadDeviceProfiles(device_id) => {
1873 let socket_path = self.socket_path.clone();
1874 let id = device_id.clone();
1875 Command::perform(
1876 async move {
1877 let client = crate::ipc::IpcClient::new(socket_path);
1878 (id.clone(), client.get_device_profiles(id).await)
1879 },
1880 |(device_id, result)| Message::DeviceProfilesLoaded(
1881 device_id,
1882 result.map_err(|e| e.to_string())
1883 )
1884 )
1885 }
1886 Message::DeviceProfilesLoaded(device_id, Ok(profiles)) => {
1887 self.device_profiles.insert(device_id.clone(), profiles);
1888 self.add_notification(&format!("Loaded {} profiles for {}", self.device_profiles.get(&device_id).map(|p| p.len()).unwrap_or(0), device_id), false);
1889 Command::none()
1890 }
1891 Message::DeviceProfilesLoaded(_device_id, Err(e)) => {
1892 self.add_notification(&format!("Failed to load device profiles: {}", e), true);
1893 Command::none()
1894 }
1895 Message::ActivateProfile(device_id, profile_name) => {
1896 let socket_path = self.socket_path.clone();
1897 let id = device_id.clone();
1898 let name = profile_name.clone();
1899 Command::perform(
1900 async move {
1901 let client = crate::ipc::IpcClient::new(socket_path);
1902 client.activate_profile(id.clone(), name.clone()).await
1903 },
1904 move |result| match result {
1905 Ok(()) => Message::ProfileActivated(device_id, profile_name),
1906 Err(e) => Message::ProfileError(format!("Failed to activate profile: {}", e)),
1907 }
1908 )
1909 }
1910 Message::ProfileActivated(device_id, profile_name) => {
1911 self.active_profiles.insert(device_id.clone(), profile_name.clone());
1912 self.add_notification(&format!("Activated profile '{}' on {}", profile_name, device_id), false);
1913 Command::none()
1914 }
1915 Message::DeactivateProfile(device_id) => {
1916 let socket_path = self.socket_path.clone();
1917 let id = device_id.clone();
1918 Command::perform(
1919 async move {
1920 let client = crate::ipc::IpcClient::new(socket_path);
1921 client.deactivate_profile(id.clone()).await
1922 },
1923 move |result| match result {
1924 Ok(()) => Message::ProfileDeactivated(device_id),
1925 Err(e) => Message::ProfileError(format!("Failed to deactivate profile: {}", e)),
1926 }
1927 )
1928 }
1929 Message::ProfileDeactivated(device_id) => {
1930 self.active_profiles.remove(&device_id);
1931 self.add_notification(&format!("Deactivated profile on {}", device_id), false);
1932 Command::none()
1933 }
1934 Message::ProfileError(msg) => {
1935 self.add_notification(&msg, true);
1936 Command::none()
1937 }
1938 Message::LoadRemapProfiles(device_path) => {
1939 let socket_path = self.socket_path.clone();
1940 let path = device_path.clone();
1941 Command::perform(
1942 async move {
1943 let client = crate::ipc::IpcClient::new(socket_path);
1944 (path.clone(), client.list_remap_profiles(&path).await)
1945 },
1946 |(device_path, result)| Message::RemapProfilesLoaded(
1947 device_path,
1948 result.map_err(|e| e.to_string())
1949 )
1950 )
1951 }
1952 Message::RemapProfilesLoaded(device_path, Ok(profiles)) => {
1953 self.remap_profiles.insert(device_path.clone(), profiles);
1954 self.add_notification(&format!("Loaded {} remap profiles for {}", self.remap_profiles.get(&device_path).map(|p| p.len()).unwrap_or(0), device_path), false);
1955 Command::none()
1956 }
1957 Message::RemapProfilesLoaded(_device_path, Err(e)) => {
1958 self.add_notification(&format!("Failed to load remap profiles: {}", e), true);
1959 Command::none()
1960 }
1961 Message::ActivateRemapProfile(device_path, profile_name) => {
1962 let socket_path = self.socket_path.clone();
1963 let path = device_path.clone();
1964 let name = profile_name.clone();
1965 Command::perform(
1966 async move {
1967 let client = crate::ipc::IpcClient::new(socket_path);
1968 client.activate_remap_profile(&path, &name).await
1969 },
1970 move |result| match result {
1971 Ok(()) => Message::RemapProfileActivated(device_path, profile_name),
1972 Err(e) => Message::ProfileError(format!("Failed to activate remap profile: {}", e)),
1973 }
1974 )
1975 }
1976 Message::RemapProfileActivated(device_path, profile_name) => {
1977 self.active_remap_profiles.insert(device_path.clone(), profile_name.clone());
1978 self.add_notification(&format!("Activated remap profile '{}' on {}", profile_name, device_path), false);
1979 Command::perform(
1981 async move { device_path.clone() },
1982 |path| Message::LoadActiveRemaps(path)
1983 )
1984 }
1985 Message::DeactivateRemapProfile(device_path) => {
1986 let socket_path = self.socket_path.clone();
1987 let path = device_path.clone();
1988 Command::perform(
1989 async move {
1990 let client = crate::ipc::IpcClient::new(socket_path);
1991 client.deactivate_remap_profile(&path).await
1992 },
1993 move |result| match result {
1994 Ok(()) => Message::RemapProfileDeactivated(device_path),
1995 Err(e) => Message::ProfileError(format!("Failed to deactivate remap profile: {}", e)),
1996 }
1997 )
1998 }
1999 Message::RemapProfileDeactivated(device_path) => {
2000 self.active_remap_profiles.remove(&device_path);
2001 self.active_remaps.remove(&device_path);
2002 self.add_notification(&format!("Deactivated remap profile on {}", device_path), false);
2003 Command::none()
2004 }
2005 Message::LoadActiveRemaps(device_path) => {
2006 let socket_path = self.socket_path.clone();
2007 let path = device_path.clone();
2008 Command::perform(
2009 async move {
2010 let client = crate::ipc::IpcClient::new(socket_path);
2011 (path.clone(), client.get_active_remaps(&path).await)
2012 },
2013 |(device_path, result)| Message::ActiveRemapsLoaded(
2014 device_path,
2015 result.map_err(|e| e.to_string())
2016 )
2017 )
2018 }
2019 Message::ActiveRemapsLoaded(device_path, Ok(Some((profile_name, remaps)))) => {
2020 self.active_remaps.insert(device_path.clone(), (profile_name, remaps));
2021 Command::none()
2022 }
2023 Message::ActiveRemapsLoaded(device_path, Ok(None)) => {
2024 self.active_remaps.remove(&device_path);
2025 Command::none()
2026 }
2027 Message::ActiveRemapsLoaded(_device_path, Err(e)) => {
2028 self.add_notification(&format!("Failed to load active remaps: {}", e), true);
2029 Command::none()
2030 }
2031 Message::RecordMouseEvent { event_type, button, x, y, delta } => {
2032 if self.recording {
2035 let event_desc = match event_type.as_str() {
2037 "button_press" => format!("Mouse button {} pressed", button.unwrap_or(0)),
2038 "button_release" => format!("Mouse button {} released", button.unwrap_or(0)),
2039 "movement" => format!("Mouse moved to ({}, {})", x, y),
2040 "scroll" => format!("Mouse scrolled {}", delta),
2041 _ => format!("Unknown mouse event: {}", event_type),
2042 };
2043 self.status = event_desc;
2045 }
2046 Command::none()
2047 }
2048 Message::ShowKeypadView(device_path) => {
2049 if device_path.is_empty() {
2051 self.device_capabilities = None;
2052 self.keypad_layout.clear();
2053 self.keypad_view_device = None;
2054 self.selected_button = None;
2055 return Command::none();
2056 }
2057 self.keypad_view_device = Some(device_path.clone());
2059 let socket_path = self.socket_path.clone();
2061 let path_clone = device_path.clone();
2062 Command::perform(
2063 async move {
2064 let client = crate::ipc::IpcClient::new(socket_path);
2065 (path_clone.clone(), client.get_device_capabilities(&path_clone).await)
2066 },
2067 |(device_path, result)| Message::DeviceCapabilitiesLoaded(
2068 device_path,
2069 result.map_err(|e| e.to_string())
2070 )
2071 )
2072 }
2073 Message::DeviceCapabilitiesLoaded(device_path, Ok(capabilities)) => {
2074 self.device_capabilities = Some(capabilities);
2075 self.keypad_layout = azeron_keypad_layout();
2076 if let Some((profile_name, remaps)) = self.active_remaps.get(&device_path) {
2078 for remap in remaps {
2079 if let Some(button) = self.keypad_layout.iter_mut().find(|b| b.id == remap.from_key) {
2080 button.current_remap = Some(remap.to_key.clone());
2081 }
2082 }
2083 self.add_notification(&format!("Loaded remaps from profile '{}'", profile_name), false);
2084 }
2085 self.active_tab = Tab::Devices;
2087 Command::none()
2088 }
2089 Message::DeviceCapabilitiesLoaded(_device_path, Err(e)) => {
2090 self.add_notification(&format!("Failed to load device capabilities: {}", e), true);
2091 Command::none()
2092 }
2093 Message::SelectKeypadButton(button_id) => {
2094 self.selected_button = self.keypad_layout.iter().position(|b| b.id == button_id);
2095 self.status = format!("Selected button: {} - Configure remapping in device profile", button_id);
2096 Command::none()
2097 }
2098 Message::LayerStateChanged(device_id, layer_id) => {
2099 self.active_layers.insert(device_id, layer_id);
2100 Command::none()
2101 }
2102 Message::LayerConfigRequested(device_id) => {
2103 let socket_path = self.socket_path.clone();
2104 let id = device_id.clone();
2105 Command::perform(
2106 async move {
2107 let client = crate::ipc::IpcClient::new(socket_path);
2108 (id.clone(), client.list_layers(&id).await)
2109 },
2110 |(device_id, result)| match result {
2111 Ok(layers) => {
2112 if let Some(active_layer) = layers.first() {
2115 Message::LayerStateChanged(device_id, active_layer.layer_id)
2116 } else {
2117 Message::TickAnimations }
2119 }
2120 Err(e) => Message::ProfileError(format!("Failed to load layers: {}", e)),
2121 }
2122 )
2123 }
2124 Message::LayerActivateRequested(device_id, layer_id, mode) => {
2125 let socket_path = self.socket_path.clone();
2126 let id = device_id.clone();
2127 Command::perform(
2128 async move {
2129 let client = crate::ipc::IpcClient::new(socket_path);
2130 client.activate_layer(&id, layer_id, mode).await
2131 },
2132 move |result| match result {
2133 Ok(()) => Message::LayerStateChanged(device_id, layer_id),
2134 Err(e) => Message::ProfileError(format!("Failed to activate layer: {}", e)),
2135 }
2136 )
2137 }
2138 Message::LayerConfigUpdated(device_id, config) => {
2139 let socket_path = self.socket_path.clone();
2140 let id = device_id.clone();
2141 let layer_id = config.layer_id;
2142 let name = config.name.clone();
2143 let mode = config.mode;
2144 Command::perform(
2145 async move {
2146 let client = crate::ipc::IpcClient::new(socket_path);
2147 client.set_layer_config(&id, layer_id, name, mode).await
2148 },
2149 move |result| match result {
2150 Ok(()) => {
2151 Message::LayerConfigRequested(device_id)
2153 }
2154 Err(e) => Message::ProfileError(format!("Failed to update layer config: {}", e)),
2155 }
2156 )
2157 }
2158 Message::OpenLayerConfigDialog(device_id, layer_id) => {
2159 let current_name = self.layer_configs
2161 .get(&device_id)
2162 .and_then(|layers| layers.iter().find(|l| l.layer_id == layer_id))
2163 .map(|l| l.name.clone())
2164 .unwrap_or_else(|| format!("Layer {}", layer_id));
2165
2166 let current_mode = self.layer_configs
2167 .get(&device_id)
2168 .and_then(|layers| layers.iter().find(|l| l.layer_id == layer_id))
2169 .map(|l| l.mode)
2170 .unwrap_or(LayerMode::Hold);
2171
2172 self.layer_config_dialog = Some((device_id, layer_id, current_name, current_mode));
2173 Command::none()
2174 }
2175 Message::LayerConfigNameChanged(name) => {
2176 if let Some((device_id, layer_id, _, mode)) = self.layer_config_dialog.take() {
2177 self.layer_config_dialog = Some((device_id, layer_id, name, mode));
2178 }
2179 Command::none()
2180 }
2181 Message::LayerConfigModeChanged(mode) => {
2182 if let Some((device_id, layer_id, name, _)) = self.layer_config_dialog.take() {
2183 self.layer_config_dialog = Some((device_id, layer_id, name, mode));
2184 }
2185 Command::none()
2186 }
2187 Message::SaveLayerConfig => {
2188 if let Some((device_id, layer_id, name, mode)) = self.layer_config_dialog.take() {
2189 let config = LayerConfigInfo {
2190 layer_id,
2191 name: name.clone(),
2192 mode,
2193 remap_count: 0,
2194 led_color: (0, 0, 255), led_zone: None, };
2197 Command::perform(
2199 async move { (device_id, config) },
2200 |(device_id, config)| Message::LayerConfigUpdated(device_id, config)
2201 )
2202 } else {
2203 Command::none()
2204 }
2205 }
2206 Message::CancelLayerConfig => {
2207 self.layer_config_dialog = None;
2208 Command::none()
2209 }
2210 Message::RefreshLayers => {
2211 let mut commands = Vec::new();
2213
2214 for device_id in self.device_profiles.keys() {
2216 let device_id = device_id.clone();
2217 let socket_path = self.socket_path.clone();
2218 commands.push(Command::perform(
2219 async move {
2220 let client = crate::ipc::IpcClient::new(socket_path);
2221 (device_id.clone(), client.list_layers(&device_id).await)
2222 },
2223 |(device_id, result)| match result {
2224 Ok(layers) => {
2225 Message::LayerListLoaded(device_id, layers)
2227 }
2228 Err(_) => Message::TickAnimations, }
2230 ));
2231 }
2232
2233 for device_id in self.active_layers.keys().cloned().collect::<Vec<_>>() {
2235 let device_id = device_id.clone();
2236 let socket_path = self.socket_path.clone();
2237 commands.push(Command::perform(
2238 async move {
2239 let client = crate::ipc::IpcClient::new(socket_path);
2240 (device_id.clone(), client.get_active_layer(&device_id).await)
2241 },
2242 |(device_id, result)| match result {
2243 Ok(Some(layer_id)) => {
2244 Message::LayerStateChanged(device_id, layer_id)
2245 }
2246 _ => Message::TickAnimations,
2247 }
2248 ));
2249 }
2250
2251 Command::batch(commands)
2252 }
2253 Message::LayerListLoaded(device_id, layers) => {
2254 self.layer_configs.insert(device_id.clone(), layers);
2255 Command::none()
2256 }
2257
2258 Message::AnalogDpadModeRequested(device_id) => {
2259 let socket_path = self.socket_path.clone();
2260 let device_id_clone = device_id.clone();
2261 Command::perform(
2262 async move {
2263 let client = crate::ipc::IpcClient::new(socket_path);
2264 client.get_analog_dpad_mode(&device_id_clone).await
2265 },
2266 move |result| match result {
2267 Ok(mode) => Message::AnalogDpadModeLoaded(device_id, mode),
2268 Err(e) => {
2269 eprintln!("Failed to get D-pad mode: {}", e);
2270 Message::TickAnimations }
2272 },
2273 )
2274 }
2275
2276 Message::AnalogDpadModeLoaded(device_id, mode) => {
2277 self.analog_dpad_modes.insert(device_id, mode);
2278 Command::none()
2279 }
2280
2281 Message::SetAnalogDpadMode(device_id, mode) => {
2282 let socket_path = self.socket_path.clone();
2283 let device_id_clone = device_id.clone();
2284 Command::perform(
2285 async move {
2286 let client = crate::ipc::IpcClient::new(socket_path);
2287 client.set_analog_dpad_mode(&device_id_clone, &mode).await
2288 },
2289 |result| match result {
2290 Ok(_) => Message::AnalogDpadModeSet(Ok(())),
2291 Err(e) => Message::AnalogDpadModeSet(Err(e)),
2292 },
2293 )
2294 }
2295
2296 Message::AnalogDpadModeSet(result) => {
2297 match result {
2298 Ok(_) => {
2299 Command::none()
2301 }
2302 Err(e) => {
2303 eprintln!("Failed to set D-pad mode: {}", e);
2304 Command::none()
2306 }
2307 }
2308 }
2309
2310 Message::AnalogDeadzoneXYRequested(device_id) => {
2312 let socket_path = self.socket_path.clone();
2313 let device_id_clone = device_id.clone();
2314 Command::perform(
2315 async move {
2316 let client = crate::ipc::IpcClient::new(socket_path);
2317 client.get_analog_deadzone_xy(&device_id_clone).await
2318 },
2319 move |result| match result {
2320 Ok((x_pct, y_pct)) => Message::AnalogDeadzoneXYLoaded(device_id, (x_pct, y_pct)),
2321 Err(e) => {
2322 eprintln!("Failed to get per-axis deadzone: {}", e);
2323 Message::TickAnimations }
2325 },
2326 )
2327 }
2328
2329 Message::AnalogDeadzoneXYLoaded(device_id, (x_pct, y_pct)) => {
2330 self.analog_deadzones_xy.insert(device_id, (x_pct, y_pct));
2331 Command::none()
2332 }
2333
2334 Message::SetAnalogDeadzoneXY(device_id, x_pct, y_pct) => {
2335 let socket_path = self.socket_path.clone();
2336 Command::perform(
2337 async move {
2338 let client = crate::ipc::IpcClient::new(socket_path);
2339 client.set_analog_deadzone_xy(&device_id, x_pct, y_pct).await
2340 },
2341 |result| match result {
2342 Ok(_) => Message::AnalogDeadzoneXYSet(Ok(())),
2343 Err(e) => Message::AnalogDeadzoneXYSet(Err(e)),
2344 },
2345 )
2346 }
2347
2348 Message::AnalogDeadzoneXYSet(result) => {
2349 match result {
2350 Ok(_) => {
2351 Command::none()
2353 }
2354 Err(e) => {
2355 eprintln!("Failed to set per-axis deadzone: {}", e);
2356 self.add_notification(&format!("Failed to set deadzone: {}", e), true);
2357 Command::none()
2358 }
2359 }
2360 }
2361
2362 Message::AnalogOuterDeadzoneXYRequested(device_id) => {
2364 let socket_path = self.socket_path.clone();
2365 let device_id_clone = device_id.clone();
2366 Command::perform(
2367 async move {
2368 let client = crate::ipc::IpcClient::new(socket_path);
2369 client.get_analog_outer_deadzone_xy(&device_id_clone).await
2370 },
2371 move |result| match result {
2372 Ok((x_pct, y_pct)) => Message::AnalogOuterDeadzoneXYLoaded(device_id, (x_pct, y_pct)),
2373 Err(e) => {
2374 eprintln!("Failed to get per-axis outer deadzone: {}", e);
2375 Message::TickAnimations }
2377 },
2378 )
2379 }
2380
2381 Message::AnalogOuterDeadzoneXYLoaded(device_id, (x_pct, y_pct)) => {
2382 self.analog_outer_deadzones_xy.insert(device_id, (x_pct, y_pct));
2383 Command::none()
2384 }
2385
2386 Message::SetAnalogOuterDeadzoneXY(device_id, x_pct, y_pct) => {
2387 let socket_path = self.socket_path.clone();
2388 Command::perform(
2389 async move {
2390 let client = crate::ipc::IpcClient::new(socket_path);
2391 client.set_analog_outer_deadzone_xy(&device_id, x_pct, y_pct).await
2392 },
2393 |result| match result {
2394 Ok(_) => Message::AnalogOuterDeadzoneXYSet(Ok(())),
2395 Err(e) => Message::AnalogOuterDeadzoneXYSet(Err(e)),
2396 },
2397 )
2398 }
2399
2400 Message::AnalogOuterDeadzoneXYSet(result) => {
2401 match result {
2402 Ok(_) => {
2403 Command::none()
2405 }
2406 Err(e) => {
2407 eprintln!("Failed to set per-axis outer deadzone: {}", e);
2408 self.add_notification(&format!("Failed to set outer deadzone: {}", e), true);
2409 Command::none()
2410 }
2411 }
2412 }
2413
2414 Message::OpenLedConfig(device_id) => {
2416 self.led_config_device = Some(device_id.clone());
2417 self.selected_led_zone = Some(LedZone::Logo); return Command::batch([
2419 Command::none(),
2420 Command::perform(
2421 async move { device_id },
2422 |device_id| Message::RefreshLedState(device_id)
2423 ),
2424 ]);
2425 }
2426
2427 Message::CloseLedConfig => {
2428 self.led_config_device = None;
2429 self.selected_led_zone = None;
2430 self.pending_led_color = None;
2431 Command::none()
2432 }
2433
2434 Message::SelectLedZone(zone) => {
2435 self.selected_led_zone = Some(zone);
2436 Command::none()
2437 }
2438
2439 Message::RefreshLedState(device_id) => {
2440 let socket_path = self.socket_path.clone();
2441 let device_id_clone = device_id.clone();
2442 Command::perform(
2443 async move {
2444 let client = crate::ipc::IpcClient::new(socket_path);
2445 client.get_all_led_colors(&device_id_clone).await
2446 },
2447 move |result| match result {
2448 Ok(colors) => Message::LedStateLoaded(device_id, Ok(colors)),
2449 Err(e) => Message::LedStateLoaded(device_id, Err(e)),
2450 },
2451 )
2452 }
2453
2454 Message::LedStateLoaded(device_id, result) => {
2455 match result {
2456 Ok(colors) => {
2457 let led_state = self.led_states.entry(device_id.clone()).or_default();
2459 led_state.zone_colors = colors;
2460 Command::none()
2461 }
2462 Err(e) => {
2463 eprintln!("Failed to load LED state: {}", e);
2464 Command::none()
2466 }
2467 }
2468 }
2469
2470 Message::SetLedColor(device_id, zone, red, green, blue) => {
2471 let socket_path = self.socket_path.clone();
2472 let device_id_clone = device_id.clone();
2473 Command::perform(
2474 async move {
2475 let client = crate::ipc::IpcClient::new(socket_path);
2476 client.set_led_color(&device_id_clone, zone, red, green, blue).await
2477 },
2478 move |result| match result {
2479 Ok(_) => Message::LedColorSet(Ok(())),
2480 Err(e) => Message::LedColorSet(Err(e)),
2481 },
2482 )
2483 }
2484
2485 Message::LedColorSet(result) => {
2486 match result {
2487 Ok(_) => {
2488 Command::none()
2490 }
2491 Err(e) => {
2492 eprintln!("Failed to set LED color: {}", e);
2493 self.add_notification(&format!("Failed to set LED color: {}", e), true);
2494 Command::none()
2495 }
2496 }
2497 }
2498
2499 Message::SetLedBrightness(device_id, zone, brightness) => {
2500 let socket_path = self.socket_path.clone();
2501 Command::perform(
2502 async move {
2503 let client = crate::ipc::IpcClient::new(socket_path);
2504 client.set_led_brightness(&device_id, zone, brightness).await
2505 },
2506 |result| match result {
2507 Ok(_) => Message::LedBrightnessSet(Ok(())),
2508 Err(e) => Message::LedBrightnessSet(Err(e)),
2509 },
2510 )
2511 }
2512
2513 Message::LedBrightnessSet(result) => {
2514 match result {
2515 Ok(_) => {
2516 Command::none()
2518 }
2519 Err(e) => {
2520 eprintln!("Failed to set LED brightness: {}", e);
2521 self.add_notification(&format!("Failed to set LED brightness: {}", e), true);
2522 Command::none()
2523 }
2524 }
2525 }
2526
2527 Message::SetLedPattern(device_id, pattern) => {
2528 let socket_path = self.socket_path.clone();
2529 Command::perform(
2530 async move {
2531 let client = crate::ipc::IpcClient::new(socket_path);
2532 client.set_led_pattern(&device_id, pattern).await
2533 },
2534 |result| match result {
2535 Ok(_) => Message::LedPatternSet(Ok(())),
2536 Err(e) => Message::LedPatternSet(Err(e)),
2537 },
2538 )
2539 }
2540
2541 Message::LedPatternSet(result) => {
2542 match result {
2543 Ok(_) => {
2544 Command::none()
2546 }
2547 Err(e) => {
2548 eprintln!("Failed to set LED pattern: {}", e);
2549 self.add_notification(&format!("Failed to set LED pattern: {}", e), true);
2550 Command::none()
2551 }
2552 }
2553 }
2554
2555 Message::LedSliderChanged(red, green, blue) => {
2556 self.pending_led_color = Some((red, green, blue));
2557 if let (Some(ref device_id), Some(zone)) = (&self.led_config_device, self.selected_led_zone) {
2559 let device_id = device_id.clone();
2560 return Command::perform(
2561 async move { (device_id, zone, red, green, blue) },
2562 |(device_id, zone, red, green, blue)| {
2563 Message::SetLedColor(device_id, zone, red, green, blue)
2564 },
2565 );
2566 }
2567 Command::none()
2568 }
2569 }
2570 }
2571
2572 fn view(&self) -> Element<'_, Message> {
2573 let sidebar = self.view_sidebar();
2574 let main_content = self.view_main_content();
2575 let status_bar = self.view_status_bar();
2576
2577 let main_layout = row![
2578 sidebar,
2579 vertical_rule(1),
2580 column![
2581 main_content,
2582 horizontal_rule(1),
2583 status_bar,
2584 ]
2585 .height(Length::Fill)
2586 ];
2587
2588 let base: Element<'_, Message> = container(main_layout)
2589 .width(Length::Fill)
2590 .height(Length::Fill)
2591 .into();
2592
2593 if let Some(dialog) = self.layer_config_dialog() {
2595 container(
2596 column![
2597 base,
2598 dialog,
2599 ]
2600 .height(Length::Fill)
2601 )
2602 .width(Length::Fill)
2603 .height(Length::Fill)
2604 .into()
2605 } else if let Some(led_dialog) = self.view_led_config() {
2606 container(
2608 column![
2609 base,
2610 led_dialog,
2611 ]
2612 .height(Length::Fill)
2613 )
2614 .width(Length::Fill)
2615 .height(Length::Fill)
2616 .into()
2617 } else if let Some(calib_dialog) = self.view_analog_calibration() {
2618 container(
2620 column![
2621 base,
2622 calib_dialog,
2623 ]
2624 .height(Length::Fill)
2625 )
2626 .width(Length::Fill)
2627 .height(Length::Fill)
2628 .into()
2629 } else {
2630 base
2631 }
2632 }
2633
2634 fn subscription(&self) -> Subscription<Message> {
2635 let timer = iced::time::every(Duration::from_millis(500)).map(|_| Message::TickAnimations);
2636
2637 let layer_refresh = iced::time::every(Duration::from_secs(2))
2639 .map(|_| Message::RefreshLayers);
2640
2641 let mouse_events = iced::event::listen_with(|event, _status| {
2646 match event {
2647 iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)) => {
2648 Some(Message::RecordMouseEvent {
2649 event_type: "button_press".to_string(),
2650 button: Some(0x110), x: 0,
2652 y: 0,
2653 delta: 0,
2654 })
2655 }
2656 iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Right)) => {
2657 Some(Message::RecordMouseEvent {
2658 event_type: "button_press".to_string(),
2659 button: Some(0x111), x: 0,
2661 y: 0,
2662 delta: 0,
2663 })
2664 }
2665 iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Middle)) => {
2666 Some(Message::RecordMouseEvent {
2667 event_type: "button_press".to_string(),
2668 button: Some(0x112), x: 0,
2670 y: 0,
2671 delta: 0,
2672 })
2673 }
2674 iced::Event::Mouse(iced::mouse::Event::ButtonReleased(_)) => {
2675 Some(Message::RecordMouseEvent {
2676 event_type: "button_release".to_string(),
2677 button: Some(0),
2678 x: 0,
2679 y: 0,
2680 delta: 0,
2681 })
2682 }
2683 iced::Event::Mouse(iced::mouse::Event::WheelScrolled { delta }) => {
2684 let scroll_delta = match delta {
2685 iced::mouse::ScrollDelta::Lines { y, .. } => y as i32,
2686 iced::mouse::ScrollDelta::Pixels { y, .. } => y as i32,
2687 };
2688 Some(Message::RecordMouseEvent {
2689 event_type: "scroll".to_string(),
2690 button: None,
2691 x: 0,
2692 y: 0,
2693 delta: scroll_delta,
2694 })
2695 }
2696 iced::Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => {
2697 Some(Message::RecordMouseEvent {
2699 event_type: "movement".to_string(),
2700 button: None,
2701 x: 0,
2702 y: 0,
2703 delta: 0,
2704 })
2705 }
2706 _ => None,
2707 }
2708 });
2709
2710 let mouse_subscription = if self.recording {
2712 mouse_events
2713 } else {
2714 Subscription::none()
2715 };
2716
2717 let theme_subscription = iced::subscription::unfold(
2718 "ashpd-theme",
2719 None,
2720 |state: Option<iced::futures::stream::BoxStream<'static, ashpd::desktop::settings::ColorScheme>>| async move {
2721 use ashpd::desktop::settings::{ColorScheme, Settings};
2722 use iced::futures::StreamExt;
2723
2724 let mut stream = match state {
2725 Some(s) => s,
2726 None => {
2727 let settings = match Settings::new().await {
2728 Ok(s) => s,
2729 Err(_) => return iced::futures::future::pending().await,
2730 };
2731 let initial = settings.color_scheme().await.unwrap_or(ColorScheme::NoPreference);
2732 let theme = match initial {
2733 ColorScheme::PreferDark => aether_dark(),
2734 ColorScheme::PreferLight => aether_light(),
2735 ColorScheme::NoPreference => aether_dark(),
2736 };
2737
2738 let s = match settings.receive_color_scheme_changed().await {
2739 Ok(s) => s,
2740 Err(_) => return (Message::ThemeChanged(theme), None),
2741 };
2742 return (Message::ThemeChanged(theme), Some(s.boxed()));
2743 }
2744 };
2745
2746 if let Some(scheme) = stream.next().await {
2747 let theme = match scheme {
2748 ColorScheme::PreferDark => aether_dark(),
2749 ColorScheme::PreferLight => aether_light(),
2750 ColorScheme::NoPreference => aether_dark(),
2751 };
2752 (Message::ThemeChanged(theme), Some(stream))
2753 } else {
2754 iced::futures::future::pending().await
2755 }
2756 }
2757 );
2758
2759 Subscription::batch(vec![timer, layer_refresh, mouse_subscription, theme_subscription])
2760 }
2761}
2762
2763impl State {
2764 fn add_notification(&mut self, message: &str, is_error: bool) {
2765 self.notifications.push_back(Notification {
2766 message: message.to_string(),
2767 is_error,
2768 timestamp: Instant::now(),
2769 });
2770 self.status = message.to_string();
2771 self.status_history.push_back(message.to_string());
2772 if self.status_history.len() > 10 {
2773 self.status_history.pop_front();
2774 }
2775 if self.notifications.len() > 5 {
2776 self.notifications.pop_front();
2777 }
2778 }
2779
2780 fn view_sidebar(&self) -> Element<'_, Message> {
2781 let logo = column![
2782 text("◢").size(40),
2783 text("AETHERMAP").size(16),
2784 text("v1.4.1").size(10),
2785 ]
2786 .spacing(2)
2787 .align_items(Alignment::Center)
2788 .width(Length::Fill);
2789
2790 let nav_button = |label: &str, icon: &str, tab: Tab| {
2791 let is_active = self.active_tab == tab;
2792 let btn_style = if is_active {
2793 iced::theme::Button::Primary
2794 } else {
2795 iced::theme::Button::Text
2796 };
2797
2798 button(
2799 row![
2800 text(icon).size(18),
2801 Space::with_width(10),
2802 text(label).size(14),
2803 ]
2804 .align_items(Alignment::Center)
2805 )
2806 .on_press(Message::SwitchTab(tab))
2807 .style(btn_style)
2808 .padding([12, 20])
2809 .width(Length::Fill)
2810 };
2811
2812 let connection_status = if self.daemon_connected {
2813 row![
2814 text("●").size(12),
2815 Space::with_width(8),
2816 text("Connected").size(11),
2817 ]
2818 } else {
2819 row![
2820 text("○").size(12),
2821 Space::with_width(8),
2822 text("Disconnected").size(11),
2823 ]
2824 }
2825 .align_items(Alignment::Center);
2826
2827 let sidebar_content = column![
2828 logo,
2829 Space::with_height(30),
2830 nav_button("Devices", "🎮", Tab::Devices),
2831 nav_button("Macros", "⚡", Tab::Macros),
2832 nav_button("Profiles", "📁", Tab::Profiles),
2833 Space::with_height(Length::Fill),
2834 horizontal_rule(1),
2835 Space::with_height(10),
2836 connection_status,
2837 Space::with_height(5),
2838 button("Refresh")
2839 .on_press(Message::CheckDaemonConnection)
2840 .style(iced::theme::Button::Text)
2841 .width(Length::Fill),
2842 ]
2843 .spacing(4)
2844 .padding(16)
2845 .align_items(Alignment::Center);
2846
2847 container(sidebar_content)
2848 .width(180)
2849 .height(Length::Fill)
2850 .into()
2851 }
2852
2853 fn view_main_content(&self) -> Element<'_, Message> {
2854 let content = match self.active_tab {
2855 Tab::Devices => self.view_devices_tab(),
2856 Tab::Macros => self.view_macros_tab(),
2857 Tab::Profiles => self.view_profiles_tab(),
2858 };
2859
2860 container(scrollable(content))
2861 .width(Length::Fill)
2862 .height(Length::Fill)
2863 .padding(24)
2864 .into()
2865 }
2866
2867 fn view_devices_tab(&self) -> Element<'_, Message> {
2868 let header = row![
2869 text("DEVICES").size(24),
2870 Space::with_width(Length::Fill),
2871 button("Reload")
2872 .on_press(Message::LoadDevices)
2873 .style(iced::theme::Button::Secondary),
2874 ]
2875 .align_items(Alignment::Center);
2876
2877 if let Some(ref view) = self.auto_switch_view {
2879 return column![
2880 header,
2881 Space::with_height(20),
2882 row![
2883 button("← Back to Devices")
2884 .on_press(Message::CloseAutoSwitchRules)
2885 .style(iced::theme::Button::Text),
2886 Space::with_width(Length::Fill),
2887 text(format!("Auto-Switch Rules: {}", view.device_id)).size(18),
2888 ]
2889 .align_items(Alignment::Center),
2890 Space::with_height(20),
2891 self.view_auto_switch_rules(),
2892 ]
2893 .spacing(10)
2894 .into();
2895 }
2896
2897 if let Some(ref view) = self.hotkey_view {
2899 return column![
2900 header,
2901 Space::with_height(20),
2902 row![
2903 button("← Back to Devices")
2904 .on_press(Message::CloseHotkeyBindings)
2905 .style(iced::theme::Button::Text),
2906 Space::with_width(Length::Fill),
2907 text(format!("Hotkey Bindings: {}", view.device_id)).size(18),
2908 ]
2909 .align_items(Alignment::Center),
2910 Space::with_height(20),
2911 self.view_hotkey_bindings(),
2912 ]
2913 .spacing(10)
2914 .into();
2915 }
2916
2917 if self.device_capabilities.is_some() && !self.keypad_layout.is_empty() {
2919 let mut keypad_content = vec![
2921 header.into(),
2922 Space::with_height(20).into(),
2923 row![
2924 button("← Back to Devices")
2925 .on_press(Message::ShowKeypadView("".to_string()))
2926 .style(iced::theme::Button::Text),
2927 Space::with_width(Length::Fill),
2928 ]
2929 .align_items(Alignment::Center)
2930 .into(),
2931 Space::with_height(20).into(),
2932 self.view_azeron_keypad().into(),
2933 ];
2934
2935 if let Some(ref device_path) = self.keypad_view_device {
2937 keypad_content.push(Space::with_height(20).into());
2938 keypad_content.push(
2939 container(
2940 column![
2941 text("Quick Profile Switch").size(14),
2942 Space::with_height(8),
2943 self.profile_quick_toggles(device_path),
2944 ]
2945 .spacing(4)
2946 )
2947 .padding(16)
2948 .width(Length::Fill)
2949 .style(container_styles::card)
2950 .into()
2951 );
2952 }
2953
2954 return column(keypad_content)
2955 .spacing(10)
2956 .into();
2957 }
2958
2959 let device_list = if self.devices.is_empty() {
2960 column![
2961 Space::with_height(40),
2962 text("No devices found").size(16),
2963 Space::with_height(10),
2964 text("Click 'Reload' to scan for input devices").size(12),
2965 ]
2966 .align_items(Alignment::Center)
2967 .width(Length::Fill)
2968 } else {
2969 let mut list: Column<Message> = column![].spacing(12);
2970 for (idx, device) in self.devices.iter().enumerate() {
2971 list = list.push(self.view_device_card(device, idx));
2972 }
2973 list
2974 };
2975
2976 column![
2977 header,
2978 Space::with_height(20),
2979 device_list,
2980 ]
2981 .spacing(10)
2982 .into()
2983 }
2984
2985 fn view_device_card(&self, device: &DeviceInfo, idx: usize) -> Element<'_, Message> {
2986 let device_path = device.path.to_string_lossy().to_string();
2987 let is_grabbed = self.grabbed_devices.contains(&device_path);
2988 let is_selected = self.selected_device == Some(idx);
2989
2990 let icon = match device.device_type {
2992 DeviceType::Keyboard => "⌨️",
2993 DeviceType::Mouse => "🖱️",
2994 DeviceType::Gamepad => "🎮",
2995 DeviceType::Keypad => "🎹",
2996 DeviceType::Other => "📱",
2997 };
2998
2999 let status_badge = if is_grabbed {
3000 container(
3001 text("GRABBED").size(10)
3002 )
3003 .padding([4, 8])
3004 .style(container_styles::card)
3005 } else {
3006 container(text("").size(10))
3007 };
3008
3009 let action_button = if is_grabbed {
3010 button("Release")
3011 .on_press(Message::UngrabDevice(device_path.clone()))
3012 .style(iced::theme::Button::Destructive)
3013 } else {
3014 button("Grab Device")
3015 .on_press(Message::GrabDevice(device_path.clone()))
3016 .style(iced::theme::Button::Primary)
3017 };
3018
3019 let select_indicator = if is_selected { "▶ " } else { "" };
3020
3021 let device_id = format!("{:04x}:{:04x}", device.vendor_id, device.product_id);
3023
3024 let keypad_button = if device.device_type == DeviceType::Keypad {
3026 Some(
3027 button("Configure Keypad")
3028 .on_press(Message::ShowKeypadView(device_path.clone()))
3029 .style(iced::theme::Button::Secondary)
3030 )
3031 } else {
3032 None
3033 };
3034
3035 let led_button = if device.device_type == DeviceType::Keypad || device.device_type == DeviceType::Gamepad {
3037 Some(
3038 button("Configure LEDs")
3039 .on_press(Message::OpenLedConfig(device_id.clone()))
3040 .style(iced::theme::Button::Secondary)
3041 )
3042 } else {
3043 None
3044 };
3045
3046 let auto_switch_button = Some(
3048 button("Auto-Switch Rules")
3049 .on_press(Message::ShowAutoSwitchRules(device_id.clone()))
3050 .style(iced::theme::Button::Secondary)
3051 );
3052
3053 let hotkey_button = Some(
3055 button("Hotkey Bindings")
3056 .on_press(Message::ShowHotkeyBindings(device_id.clone()))
3057 .style(iced::theme::Button::Secondary)
3058 );
3059
3060 let analog_button = if device.device_type == DeviceType::Keypad ||
3062 device.device_type == DeviceType::Gamepad {
3063 Some(
3064 button("Analog Calibration")
3065 .on_press(Message::OpenAnalogCalibration {
3066 device_id: device_id.clone(),
3067 layer_id: self.active_layers.get(&device_id).copied().unwrap_or(0),
3068 })
3069 .style(iced::theme::Button::Secondary)
3070 )
3071 } else {
3072 None
3073 };
3074
3075 let card_content = column![
3076 row![
3077 text(icon).size(28),
3078 Space::with_width(12),
3079 column![
3080 row![
3081 text(format!("{}{}", select_indicator, device.name)).size(16),
3082 Space::with_width(8),
3083 text(match device.device_type {
3084 DeviceType::Keyboard => "Keyboard",
3085 DeviceType::Mouse => "Mouse",
3086 DeviceType::Gamepad => "Gamepad",
3087 DeviceType::Keypad => "Keypad",
3088 DeviceType::Other => "Other",
3089 }).size(12).style(iced::theme::Text::Color(iced::Color::from_rgb(0.6, 0.6, 0.6))),
3090 ],
3091 text(format!(
3092 "VID:{:04X} PID:{:04X} | {}",
3093 device.vendor_id, device.product_id, device_path
3094 )).size(11),
3095 ],
3096 Space::with_width(Length::Fill),
3097 status_badge,
3098 ]
3099 .align_items(Alignment::Center),
3100 Space::with_height(12),
3101 row![
3102 button("Select")
3103 .on_press(Message::SelectDevice(idx))
3104 .style(iced::theme::Button::Text),
3105 Space::with_width(Length::Fill),
3106 action_button,
3107 ],
3108 Space::with_height(8),
3109 self.view_profile_selector(device),
3110 self.view_remap_profile_switcher(&device_path),
3111 Space::with_height(4),
3112 container(
3114 column![
3115 text("Profiles").size(11).style(iced::theme::Text::Color(iced::Color::from_rgb(0.5, 0.5, 0.5))),
3116 Space::with_height(4),
3117 self.profile_quick_toggles(&device_path),
3118 ]
3119 .spacing(4)
3120 )
3121 .padding([8, 0])
3122 .width(Length::Fill),
3123 Space::with_height(8),
3124 row![
3125 text("Layer:").size(12),
3126 Space::with_width(8),
3127 self.layer_indicator(&device_id),
3128 Space::with_width(Length::Fill),
3129 self.layer_activation_buttons(&device_id),
3130 ]
3131 .spacing(4)
3132 .align_items(Alignment::Center),
3133 ]
3134 .spacing(8);
3135
3136 let mut card_elements: Vec<Element<'_, Message>> = vec![card_content.into()];
3138
3139 if device.device_type == DeviceType::Gamepad || device.device_type == DeviceType::Keypad {
3141 let current_mode = self.analog_dpad_modes.get(&device_id).cloned().unwrap_or_else(|| "disabled".to_string());
3142
3143 card_elements.push(Space::with_height(4).into());
3144 card_elements.push(
3145 row![
3146 text("D-pad:").size(12),
3147 Space::with_width(4),
3148 button("Off")
3149 .on_press(Message::SetAnalogDpadMode(device_id.clone(), "disabled".to_string()))
3150 .style(if current_mode == "disabled" {
3151 iced::theme::Button::Primary
3152 } else {
3153 iced::theme::Button::Text
3154 }),
3155 button("8-Way")
3156 .on_press(Message::SetAnalogDpadMode(device_id.clone(), "eight_way".to_string()))
3157 .style(if current_mode == "eight_way" {
3158 iced::theme::Button::Primary
3159 } else {
3160 iced::theme::Button::Text
3161 }),
3162 button("4-Way")
3163 .on_press(Message::SetAnalogDpadMode(device_id.clone(), "four_way".to_string()))
3164 .style(if current_mode == "four_way" {
3165 iced::theme::Button::Primary
3166 } else {
3167 iced::theme::Button::Text
3168 }),
3169 ]
3170 .spacing(4)
3171 .align_items(Alignment::Center)
3172 .into()
3173 );
3174
3175 let (deadzone_x, deadzone_y) = self.analog_deadzones_xy.get(&device_id).cloned().unwrap_or((43, 43));
3177 let (outer_deadzone_x, outer_deadzone_y) = self.analog_outer_deadzones_xy.get(&device_id).cloned().unwrap_or((100, 100));
3178
3179 card_elements.push(Space::with_height(8).into());
3180
3181 card_elements.push(
3183 column![
3184 text("Deadzone (noise filter)").size(11),
3185 row![
3186 text("X:").size(11),
3187 Space::with_width(4),
3188 self.deadzone_buttons(&device_id, false, deadzone_x),
3189 Space::with_width(8),
3190 text(format!("{}%", deadzone_x)).size(11),
3191 ]
3192 .spacing(2)
3193 .align_items(Alignment::Center),
3194 row![
3195 text("Y:").size(11),
3196 Space::with_width(4),
3197 self.deadzone_buttons(&device_id, true, deadzone_y),
3198 Space::with_width(8),
3199 text(format!("{}%", deadzone_y)).size(11),
3200 ]
3201 .spacing(2)
3202 .align_items(Alignment::Center),
3203 ]
3204 .spacing(4)
3205 .into()
3206 );
3207
3208 card_elements.push(Space::with_height(4).into());
3210 card_elements.push(
3211 column![
3212 text("Max Range (input clamp)").size(11),
3213 row![
3214 text("X:").size(11),
3215 Space::with_width(4),
3216 self.outer_deadzone_buttons(&device_id, false, outer_deadzone_x),
3217 Space::with_width(8),
3218 text(format!("{}%", outer_deadzone_x)).size(11),
3219 ]
3220 .spacing(2)
3221 .align_items(Alignment::Center),
3222 row![
3223 text("Y:").size(11),
3224 Space::with_width(4),
3225 self.outer_deadzone_buttons(&device_id, true, outer_deadzone_y),
3226 Space::with_width(8),
3227 text(format!("{}%", outer_deadzone_y)).size(11),
3228 ]
3229 .spacing(2)
3230 .align_items(Alignment::Center),
3231 ]
3232 .spacing(4)
3233 .into()
3234 );
3235 }
3236
3237 if let Some(keypad_btn) = keypad_button {
3239 card_elements.push(Space::with_height(4).into());
3240 card_elements.push(
3241 row![Space::with_width(Length::Fill), keypad_btn,]
3242 .spacing(4)
3243 .into()
3244 );
3245 }
3246
3247 if let Some(led_btn) = led_button {
3249 card_elements.push(Space::with_height(4).into());
3250 card_elements.push(
3251 row![Space::with_width(Length::Fill), led_btn,]
3252 .spacing(4)
3253 .into()
3254 );
3255 }
3256
3257 if let Some(auto_btn) = auto_switch_button {
3259 card_elements.push(Space::with_height(4).into());
3260 card_elements.push(
3261 row![Space::with_width(Length::Fill), auto_btn,]
3262 .spacing(4)
3263 .into()
3264 );
3265 }
3266
3267 if let Some(hotkey_btn) = hotkey_button {
3269 card_elements.push(Space::with_height(4).into());
3270 card_elements.push(
3271 row![Space::with_width(Length::Fill), hotkey_btn,]
3272 .spacing(4)
3273 .into()
3274 );
3275 }
3276
3277 if let Some(analog_btn) = analog_button {
3279 card_elements.push(Space::with_height(4).into());
3280 card_elements.push(
3281 row![Space::with_width(Length::Fill), analog_btn,]
3282 .spacing(4)
3283 .into()
3284 );
3285 }
3286
3287 let card_content = column(card_elements).spacing(4);
3288
3289 container(card_content)
3290 .padding(16)
3291 .width(Length::Fill)
3292 .style(container_styles::card)
3293 .into()
3294 }
3295
3296 fn view_macros_tab(&self) -> Element<'_, Message> {
3297 let header = row![
3298 text("MACROS").size(24),
3299 Space::with_width(Length::Fill),
3300 text(format!("{} total", self.macros.len())).size(14),
3301 ]
3302 .align_items(Alignment::Center);
3303
3304 let recording_section = self.view_recording_panel();
3305 let settings_section = self.view_macro_settings_panel();
3306 let macro_list = self.view_macro_list();
3307
3308 column![
3309 header,
3310 Space::with_height(20),
3311 row![
3312 recording_section,
3313 settings_section,
3314 ].spacing(20),
3315 Space::with_height(20),
3316 text("MACRO LIBRARY").size(18),
3317 Space::with_height(10),
3318 macro_list,
3319 ]
3320 .spacing(10)
3321 .into()
3322 }
3323
3324 fn view_recording_panel(&self) -> Element<'_, Message> {
3325 let name_input = text_input("Enter macro name (e.g., 'Quick Reload')", &self.new_macro_name)
3326 .on_input(Message::UpdateMacroName)
3327 .padding(12)
3328 .size(14);
3329
3330 let record_button = if self.recording {
3331 let indicator = if self.recording_pulse { "●" } else { "○" };
3332 button(
3333 row![
3334 text(indicator).size(18),
3335 Space::with_width(8),
3336 text("STOP RECORDING").size(14),
3337 ]
3338 .align_items(Alignment::Center)
3339 )
3340 .on_press(Message::StopRecording)
3341 .style(iced::theme::Button::Destructive)
3342 .padding([14, 24])
3343 } else {
3344 button(
3345 row![
3346 text("⏺").size(18),
3347 Space::with_width(8),
3348 text("START RECORDING").size(14),
3349 ]
3350 .align_items(Alignment::Center)
3351 )
3352 .on_press(Message::StartRecording)
3353 .style(iced::theme::Button::Primary)
3354 .padding([14, 24])
3355 };
3356
3357 let instructions = column![
3358 text("Recording Instructions").size(14),
3359 Space::with_height(8),
3360 text("1. Go to Devices tab and grab a device").size(12),
3361 text("2. Enter a descriptive macro name above").size(12),
3362 text("3. Click 'Start Recording' and press keys").size(12),
3363 text("4. Click 'Stop Recording' when finished").size(12),
3364 ]
3365 .spacing(4);
3366
3367 let recording_status = if self.recording {
3368 container(
3369 row![
3370 text("●").size(14),
3371 Space::with_width(8),
3372 text(format!(
3373 "Recording '{}' - Press keys on grabbed device...",
3374 self.recording_macro_name.as_deref().unwrap_or("")
3375 )).size(13),
3376 ]
3377 .align_items(Alignment::Center)
3378 )
3379 .padding(12)
3380 .width(Length::Fill)
3381 .style(container_styles::card)
3382 } else {
3383 container(text(""))
3384 };
3385
3386 let panel_content = column![
3387 text("MACRO RECORDING").size(16),
3388 Space::with_height(16),
3389 name_input,
3390 Space::with_height(16),
3391 instructions,
3392 Space::with_height(16),
3393 recording_status,
3394 Space::with_height(16),
3395 container(record_button).center_x(),
3396 ];
3397
3398 container(panel_content)
3399 .padding(20)
3400 .width(Length::Fill)
3401 .style(container_styles::card)
3402 .into()
3403 }
3404
3405 fn view_macro_settings_panel(&self) -> Element<'_, Message> {
3406 let latency_label = text(format!("Latency Offset: {}ms", self.macro_settings.latency_offset_ms)).size(14);
3407 let latency_slider = slider(
3408 0..=200,
3409 self.macro_settings.latency_offset_ms,
3410 Message::LatencyChanged,
3411 );
3412
3413 let jitter_label = text(format!("Jitter: {:.0}%", self.macro_settings.jitter_pct * 100.0)).size(14);
3414 let jitter_slider = slider(
3415 0.0..=0.5,
3416 self.macro_settings.jitter_pct,
3417 Message::JitterChanged,
3418 ).step(0.01);
3419
3420 let capture_mouse_checkbox = checkbox(
3421 "Capture Mouse (Macro playback moves mouse)",
3422 self.macro_settings.capture_mouse,
3423 )
3424 .on_toggle(Message::CaptureMouseToggled)
3425 .size(14);
3426
3427 let content = column![
3428 text("GLOBAL MACRO SETTINGS").size(16),
3429 Space::with_height(16),
3430 latency_label,
3431 latency_slider,
3432 Space::with_height(12),
3433 jitter_label,
3434 jitter_slider,
3435 Space::with_height(16),
3436 capture_mouse_checkbox,
3437 ]
3438 .spacing(4);
3439
3440 container(content)
3441 .padding(20)
3442 .width(Length::Fill)
3443 .style(container_styles::card)
3444 .into()
3445 }
3446
3447 fn view_macro_action(&self, action: &Action) -> Element<'_, Message> {
3449 let action_text = Self::format_action_with_icon(action);
3450 text(action_text).size(11).into()
3451 }
3452
3453 fn view_macro_list(&self) -> Element<'_, Message> {
3454 if self.macros.is_empty() {
3455 return container(
3456 column![
3457 text("No macros yet").size(14),
3458 text("Record your first macro above").size(12),
3459 ]
3460 .spacing(8)
3461 .align_items(Alignment::Center)
3462 )
3463 .padding(20)
3464 .width(Length::Fill)
3465 .center_x()
3466 .into();
3467 }
3468
3469 let mut list: Column<Message> = column![].spacing(8);
3470
3471 for macro_entry in &self.macros {
3472 let is_recent = self.recently_updated_macros.contains_key(¯o_entry.name);
3473 let name_prefix = if is_recent { "★ " } else { "⚡ " };
3474
3475 let action_preview: Vec<Element<'_, Message>> = macro_entry.actions
3477 .iter()
3478 .take(3)
3479 .map(|action| self.view_macro_action(action))
3480 .collect();
3481
3482 let more_indicator = if macro_entry.actions.len() > 3 {
3483 Some(text(format!("+ {} more actions...", macro_entry.actions.len() - 3)).size(10))
3484 } else {
3485 None
3486 };
3487
3488 let macro_card = container(
3489 row![
3490 column![
3491 text(format!("{}{}", name_prefix, macro_entry.name)).size(15),
3492 text(format!(
3493 "{} actions | {} trigger keys | {}",
3494 macro_entry.actions.len(),
3495 macro_entry.trigger.keys.len(),
3496 if macro_entry.enabled { "enabled" } else { "disabled" }
3497 )).size(11),
3498 column(action_preview)
3500 .spacing(2)
3501 .padding([4, 0]),
3502 more_indicator.unwrap_or_else(|| text("").size(10)),
3503 ]
3504 .spacing(4),
3505 Space::with_width(Length::Fill),
3506 button("▶ Test")
3507 .on_press(Message::PlayMacro(macro_entry.name.clone()))
3508 .style(iced::theme::Button::Secondary),
3509 button("🗑")
3510 .on_press(Message::DeleteMacro(macro_entry.name.clone()))
3511 .style(iced::theme::Button::Destructive),
3512 ]
3513 .spacing(8)
3514 .align_items(Alignment::Center)
3515 )
3516 .padding(12)
3517 .width(Length::Fill)
3518 .style(container_styles::card);
3519
3520 list = list.push(macro_card);
3521 }
3522
3523 scrollable(list).height(300).into()
3524 }
3525
3526 fn view_profiles_tab(&self) -> Element<'_, Message> {
3527 let header = text("PROFILES").size(24);
3528
3529 let profile_input = text_input("Profile name...", &self.profile_name)
3530 .on_input(Message::UpdateProfileName)
3531 .padding(12)
3532 .size(14);
3533
3534 let save_button = button(
3535 row![
3536 text("💾").size(16),
3537 Space::with_width(8),
3538 text("Save Profile").size(14),
3539 ]
3540 .align_items(Alignment::Center)
3541 )
3542 .on_press(Message::SaveProfile)
3543 .style(iced::theme::Button::Primary)
3544 .padding([12, 20]);
3545
3546 let load_button = button(
3547 row![
3548 text("📂").size(16),
3549 Space::with_width(8),
3550 text("Load Profile").size(14),
3551 ]
3552 .align_items(Alignment::Center)
3553 )
3554 .on_press(Message::LoadProfile)
3555 .style(iced::theme::Button::Secondary)
3556 .padding([12, 20]);
3557
3558 let profile_info = column![
3559 text("Current Configuration").size(16),
3560 Space::with_height(10),
3561 text(format!("• {} devices detected", self.devices.len())).size(12),
3562 text(format!("• {} devices grabbed", self.grabbed_devices.len())).size(12),
3563 text(format!("• {} macros configured", self.macros.len())).size(12),
3564 ]
3565 .spacing(4);
3566
3567 let panel_content = column![
3568 text("SAVE / LOAD CONFIGURATION").size(16),
3569 Space::with_height(16),
3570 profile_input,
3571 Space::with_height(16),
3572 row![
3573 save_button,
3574 Space::with_width(10),
3575 load_button,
3576 ],
3577 Space::with_height(20),
3578 profile_info,
3579 ];
3580
3581 column![
3582 header,
3583 Space::with_height(20),
3584 container(panel_content)
3585 .padding(20)
3586 .width(Length::Fill)
3587 .style(container_styles::card),
3588 ]
3589 .spacing(10)
3590 .into()
3591 }
3592
3593 fn view_profile_selector(&self, device: &DeviceInfo) -> Element<'_, Message> {
3595 let device_id = format!("{:04x}:{:04x}", device.vendor_id, device.product_id);
3596 let profiles = self.device_profiles.get(&device_id);
3597 let active_profile = self.active_profiles.get(&device_id);
3598
3599 let profile_row: Element<'_, Message> = if let Some(profiles) = profiles {
3600 if profiles.is_empty() {
3601 row![
3602 text("Profile: ").size(12),
3603 text("No profiles configured").size(12),
3604 ]
3605 .spacing(10)
3606 .align_items(Alignment::Center)
3607 .into()
3608 } else {
3609 let device_id_for_closure = device_id.clone();
3610 let picker = pick_list(
3611 profiles.clone(),
3612 active_profile.cloned(),
3613 move |profile_name| Message::ActivateProfile(device_id_for_closure.clone(), profile_name),
3614 )
3615 .placeholder("Select profile")
3616 .width(Length::Fixed(150.0));
3617
3618 let mut row_content = row![
3619 text("Profile: ").size(12),
3620 picker,
3621 ]
3622 .spacing(10)
3623 .align_items(Alignment::Center);
3624
3625 if let Some(_active) = active_profile {
3627 row_content = row_content.push(
3628 button(text("Deactivate").size(11))
3629 .on_press(Message::DeactivateProfile(device_id.clone()))
3630 .padding(5)
3631 .style(iced::theme::Button::Text)
3632 );
3633 }
3634
3635 row_content.into()
3636 }
3637 } else {
3638 row![
3639 text("Profile: ").size(12),
3640 button(text("Load Profiles").size(11))
3641 .on_press(Message::LoadDeviceProfiles(device_id.clone()))
3642 .padding([4, 8])
3643 .style(iced::theme::Button::Text),
3644 ]
3645 .spacing(10)
3646 .align_items(Alignment::Center)
3647 .into()
3648 };
3649
3650 container(profile_row)
3651 .padding([4, 0])
3652 .into()
3653 }
3654
3655 fn view_remap_profile_switcher(&self, device_path: &str) -> Element<'_, Message> {
3657 let profiles = self.remap_profiles.get(device_path);
3658 let active_profile = self.active_remap_profiles.get(device_path);
3659
3660 let profile_row: Element<'_, Message> = if let Some(profiles) = profiles {
3661 if profiles.is_empty() {
3662 row![
3663 text("Remap: ").size(12),
3664 text("No remap profiles").size(12),
3665 ]
3666 .spacing(10)
3667 .align_items(Alignment::Center)
3668 .into()
3669 } else {
3670 let profile_names: Vec<String> = profiles.iter().map(|p| p.name.clone()).collect();
3671 let device_path_for_closure = device_path.to_string();
3672 let picker = pick_list(
3673 profile_names,
3674 active_profile.cloned(),
3675 move |profile_name| Message::ActivateRemapProfile(device_path_for_closure.clone(), profile_name),
3676 )
3677 .placeholder("Select remap profile")
3678 .width(Length::Fixed(150.0));
3679
3680 let mut row_content = row![
3681 text("Remap: ").size(12),
3682 picker,
3683 ]
3684 .spacing(10)
3685 .align_items(Alignment::Center);
3686
3687 if let Some(_active) = active_profile {
3689 row_content = row_content.push(
3690 button(text("Off").size(11))
3691 .on_press(Message::DeactivateRemapProfile(device_path.to_string()))
3692 .padding(5)
3693 .style(iced::theme::Button::Text)
3694 );
3695 }
3696
3697 row_content = row_content.push(
3699 button(text("↻").size(11))
3700 .on_press(Message::LoadRemapProfiles(device_path.to_string()))
3701 .padding(5)
3702 .style(iced::theme::Button::Text)
3703 );
3704
3705 row_content.into()
3706 }
3707 } else {
3708 row![
3709 text("Remap: ").size(12),
3710 button(text("Load Remaps").size(11))
3711 .on_press(Message::LoadRemapProfiles(device_path.to_string()))
3712 .padding([4, 8])
3713 .style(iced::theme::Button::Text),
3714 ]
3715 .spacing(10)
3716 .align_items(Alignment::Center)
3717 .into()
3718 };
3719
3720 let remap_content = column![
3721 profile_row,
3722 self.view_active_remaps_display(device_path),
3723 ]
3724 .spacing(4);
3725
3726 container(remap_content)
3727 .padding([4, 0])
3728 .into()
3729 }
3730
3731 fn view_active_remaps_display(&self, device_path: &str) -> Element<'_, Message> {
3733 if let Some((profile_name, remaps)) = self.active_remaps.get(device_path) {
3734 if remaps.is_empty() {
3735 return text(format!("Profile: {} (no remaps)", profile_name))
3736 .size(10)
3737 .into();
3738 }
3739
3740 let remap_rows: Vec<Element<'_, Message>> = remaps.iter().map(|remap| {
3741 row![
3742 text(format!("{} → {}", remap.from_key, remap.to_key))
3743 .size(10)
3744 ]
3745 .into()
3746 }).collect();
3747
3748 let remap_list = scrollable(
3749 column(remap_rows).spacing(2)
3750 )
3751 .height(Length::Fixed(60.0));
3752
3753 column![
3754 text(format!("Active: {} ({} remaps)", profile_name, remaps.len())).size(10),
3755 remap_list,
3756 ]
3757 .spacing(2)
3758 .into()
3759 } else {
3760 text("").size(10).into()
3761 }
3762 }
3763
3764 fn format_action_with_icon(action: &Action) -> String {
3766 match action {
3767 Action::KeyPress(key) => format!("⌨️ Press Key {}", key),
3768 Action::KeyRelease(key) => format!("⌨️ Release Key {}", key),
3769 Action::Delay(ms) => format!("⏱️ Wait {}ms", ms),
3770 Action::MousePress(btn) => format!("🖱️ Click Button {}", btn),
3771 Action::MouseRelease(btn) => format!("🖱️ Release Button {}", btn),
3772 Action::MouseMove(x, y) => format!("↕️ Move X={} Y={}", x, y),
3773 Action::MouseScroll(amount) => format!("🔄 Scroll {}", amount),
3774 Action::Execute(cmd) => format!("▶️ Execute {}", cmd),
3775 Action::Type(text) => format!("⌨️ Type {}", text),
3776 Action::AnalogMove { axis_code, normalized } => {
3777 let axis_name = match axis_code {
3779 61000 => "X",
3780 61001 => "Y",
3781 61002 => "Z",
3782 61003 => "RX",
3783 61004 => "RY",
3784 61005 => "RZ",
3785 _ => "UNKNOWN",
3786 };
3787 format!("🕹️ Analog({}, {:.2})", axis_name, normalized)
3788 }
3789 }
3790 }
3791
3792 fn view_auto_switch_rules(&self) -> Element<'_, Message> {
3796 let view = self.auto_switch_view.as_ref().unwrap();
3797
3798 let focus_display = row![
3800 text("Current Focus:").size(14),
3801 Space::with_width(8),
3802 if let Some(ref focus) = self.current_focus {
3803 container(text(focus).size(14))
3804 .padding([4, 12])
3805 .style(container_styles::card)
3806 } else {
3807 container(text("Unknown").size(14).style(iced::theme::Text::Color(iced::Color::from_rgb(0.6, 0.6, 0.6))))
3808 .padding([4, 12])
3809 },
3810 ]
3811 .spacing(4)
3812 .align_items(Alignment::Center);
3813
3814 let rules_header = row![
3816 text("Auto-Switch Rules").size(18),
3817 Space::with_width(Length::Fill),
3818 if view.editing_rule.is_some() {
3819 button("Cancel")
3820 .on_press(Message::EditAutoSwitchRule(usize::MAX))
3821 .style(iced::theme::Button::Text)
3822 } else {
3823 button("Add Rule")
3824 .on_press(Message::EditAutoSwitchRule(usize::MAX))
3825 .style(iced::theme::Button::Primary)
3826 },
3827 ]
3828 .align_items(Alignment::Center);
3829
3830 let rules_list = if view.rules.is_empty() {
3832 column![
3833 Space::with_height(20),
3834 text("No rules configured").size(14).style(iced::theme::Text::Color(iced::Color::from_rgb(0.6, 0.6, 0.6))),
3835 Space::with_height(8),
3836 text("Add a rule to automatically switch profiles when windows gain focus").size(12).style(iced::theme::Text::Color(iced::Color::from_rgb(0.5, 0.5, 0.5))),
3837 ]
3838 .align_items(Alignment::Center)
3839 } else {
3840 let mut list = column![].spacing(8);
3841 for (idx, rule) in view.rules.iter().enumerate() {
3842 let is_editing = view.editing_rule == Some(idx);
3843 let indicator: Element<'_, Message> = if is_editing {
3844 container(text("▶")).padding([0, 8]).into()
3845 } else {
3846 Space::with_width(20).into()
3847 };
3848 let row = row![
3849 indicator,
3850 column![
3851 text(format!("App: {}", rule.app_id)).size(14),
3852 text(format!("Profile: {}{}", rule.profile_name,
3853 rule.layer_id.map(|l| format!(" + Layer {}", l)).unwrap_or_default())).size(12),
3854 ]
3855 .spacing(2),
3856 Space::with_width(Length::Fill),
3857 button("Edit")
3858 .on_press(Message::EditAutoSwitchRule(idx))
3859 .style(iced::theme::Button::Text),
3860 button("Delete")
3861 .on_press(Message::DeleteAutoSwitchRule(idx))
3862 .style(iced::theme::Button::Destructive),
3863 ]
3864 .spacing(8)
3865 .align_items(Alignment::Center);
3866 list = list.push(row);
3867 }
3868 list
3869 };
3870
3871 let edit_form = if view.editing_rule.is_some() {
3873 Some(column![
3874 Space::with_height(20),
3875 text(if view.editing_rule.unwrap_or(0) < view.rules.len() {
3876 "Edit Rule"
3877 } else {
3878 "Add New Rule"
3879 }).size(16),
3880 Space::with_height(12),
3881 row![
3882 text("App ID:").size(14),
3883 Space::with_width(8),
3884 text_input("org.alacritty", &view.new_app_id)
3885 .on_input(Message::AutoSwitchAppIdChanged)
3886 .padding(8)
3887 .size(14),
3888 Space::with_width(8),
3889 button("Use Current")
3890 .on_press(Message::AutoSwitchUseCurrentApp)
3891 .style(iced::theme::Button::Secondary),
3892 ]
3893 .spacing(4)
3894 .align_items(Alignment::Center),
3895 Space::with_height(8),
3896 row![
3897 text("Profile:").size(14),
3898 Space::with_width(8),
3899 text_input("default", &view.new_profile_name)
3900 .on_input(Message::AutoSwitchProfileNameChanged)
3901 .padding(8)
3902 .size(14),
3903 ]
3904 .spacing(4)
3905 .align_items(Alignment::Center),
3906 Space::with_height(8),
3907 row![
3908 text("Layer (optional):").size(14),
3909 Space::with_width(8),
3910 text_input("0", &view.new_layer_id)
3911 .on_input(Message::AutoSwitchLayerIdChanged)
3912 .padding(8)
3913 .size(14),
3914 ]
3915 .spacing(4)
3916 .align_items(Alignment::Center),
3917 Space::with_height(12),
3918 row![
3919 Space::with_width(Length::Fill),
3920 button("Save Rule")
3921 .on_press(Message::SaveAutoSwitchRule)
3922 .style(iced::theme::Button::Primary),
3923 ]
3924 .align_items(Alignment::Center),
3925 ]
3926 .spacing(4))
3927 } else {
3928 None
3929 };
3930
3931 let mut content = column![
3932 focus_display,
3933 Space::with_height(20),
3934 rules_header,
3935 Space::with_height(12),
3936 scrollable(rules_list).height(Length::Fixed(200.0)),
3937 ]
3938 .spacing(4);
3939
3940 if let Some(form) = edit_form {
3941 content = content.push(form);
3942 }
3943
3944 container(content)
3945 .padding(20)
3946 .width(Length::Fill)
3947 .style(container_styles::card)
3948 .into()
3949 }
3950
3951 fn view_hotkey_bindings(&self) -> Element<'_, Message> {
3955 let view = self.hotkey_view.as_ref().unwrap();
3956
3957 let bindings_header = row![
3959 text("Hotkey Bindings").size(18),
3960 Space::with_width(Length::Fill),
3961 if view.editing_binding.is_some() {
3962 button("Cancel")
3963 .on_press(Message::EditHotkeyBinding(usize::MAX))
3964 .style(iced::theme::Button::Text)
3965 } else {
3966 button("Add Binding")
3967 .on_press(Message::EditHotkeyBinding(usize::MAX))
3968 .style(iced::theme::Button::Primary)
3969 },
3970 ]
3971 .align_items(Alignment::Center);
3972
3973 let bindings_list = if view.bindings.is_empty() {
3975 column![
3976 Space::with_height(20),
3977 text("No bindings configured").size(14).style(iced::theme::Text::Color(iced::Color::from_rgb(0.6, 0.6, 0.6))),
3978 Space::with_height(8),
3979 text("Add a binding to switch profiles using keyboard shortcuts").size(12).style(iced::theme::Text::Color(iced::Color::from_rgb(0.5, 0.5, 0.5))),
3980 ]
3981 .align_items(Alignment::Center)
3982 } else {
3983 let mut list = column![].spacing(8);
3984 for (idx, binding) in view.bindings.iter().enumerate() {
3985 let is_editing = view.editing_binding == Some(idx);
3986 let modifiers_str = binding.modifiers.join("+");
3987 let indicator: Element<'_, Message> = if is_editing {
3988 container(text("▶")).padding([0, 8]).into()
3989 } else {
3990 Space::with_width(20).into()
3991 };
3992 let row = row![
3993 indicator,
3994 column![
3995 text(format!("{}+{} → {}", modifiers_str, binding.key, binding.profile_name)).size(14),
3996 text(format!("Layer: {}",
3997 binding.layer_id.map(|l| l.to_string()).unwrap_or_else(|| "default".to_string()))).size(12),
3998 ]
3999 .spacing(2),
4000 Space::with_width(Length::Fill),
4001 button("Edit")
4002 .on_press(Message::EditHotkeyBinding(idx))
4003 .style(iced::theme::Button::Text),
4004 button("Delete")
4005 .on_press(Message::DeleteHotkeyBinding(idx))
4006 .style(iced::theme::Button::Destructive),
4007 ]
4008 .spacing(8)
4009 .align_items(Alignment::Center);
4010 list = list.push(row);
4011 }
4012 list
4013 };
4014
4015 let edit_form = if view.editing_binding.is_some() {
4017 Some(column![
4018 Space::with_height(20),
4019 text(if view.editing_binding.unwrap_or(0) < view.bindings.len() {
4020 "Edit Binding"
4021 } else {
4022 "Add New Binding"
4023 }).size(16),
4024 Space::with_height(12),
4025 text("Modifiers:").size(14),
4026 row![
4027 self.modifier_checkbox("Ctrl", "ctrl", &view.new_modifiers),
4028 self.modifier_checkbox("Alt", "alt", &view.new_modifiers),
4029 self.modifier_checkbox("Shift", "shift", &view.new_modifiers),
4030 self.modifier_checkbox("Super", "super", &view.new_modifiers),
4031 ]
4032 .spacing(8),
4033 Space::with_height(8),
4034 row![
4035 text("Key:").size(14),
4036 Space::with_width(8),
4037 text_input("1", &view.new_key)
4038 .on_input(Message::HotkeyKeyChanged)
4039 .padding(8)
4040 .size(14),
4041 ]
4042 .spacing(4)
4043 .align_items(Alignment::Center),
4044 Space::with_height(8),
4045 row![
4046 text("Profile:").size(14),
4047 Space::with_width(8),
4048 text_input("default", &view.new_profile_name)
4049 .on_input(Message::HotkeyProfileNameChanged)
4050 .padding(8)
4051 .size(14),
4052 ]
4053 .spacing(4)
4054 .align_items(Alignment::Center),
4055 Space::with_height(8),
4056 row![
4057 text("Layer (optional):").size(14),
4058 Space::with_width(8),
4059 text_input("0", &view.new_layer_id)
4060 .on_input(Message::HotkeyLayerIdChanged)
4061 .padding(8)
4062 .size(14),
4063 ]
4064 .spacing(4)
4065 .align_items(Alignment::Center),
4066 Space::with_height(12),
4067 row![
4068 Space::with_width(Length::Fill),
4069 button("Save Binding")
4070 .on_press(Message::SaveHotkeyBinding)
4071 .style(iced::theme::Button::Primary),
4072 ]
4073 .align_items(Alignment::Center),
4074 ]
4075 .spacing(4))
4076 } else {
4077 None
4078 };
4079
4080 let mut content = column![
4081 bindings_header,
4082 Space::with_height(12),
4083 scrollable(bindings_list).height(Length::Fixed(200.0)),
4084 ]
4085 .spacing(4);
4086
4087 if let Some(form) = edit_form {
4088 content = content.push(form);
4089 }
4090
4091 container(content)
4092 .padding(20)
4093 .width(Length::Fill)
4094 .style(container_styles::card)
4095 .into()
4096 }
4097
4098 fn modifier_checkbox<'a>(&'a self, label: &str, modifier: &str, selected: &[String]) -> Element<'a, Message> {
4100 let is_checked = selected.iter().any(|m| m.to_lowercase() == modifier);
4101 let btn = if is_checked {
4102 button(text(format!("[{}] ", label)).size(12))
4103 } else {
4104 button(text(format!("[ ] {}", label)).size(12))
4105 };
4106 btn.on_press(Message::ToggleHotkeyModifier(modifier.to_string()))
4107 .style(iced::theme::Button::Text)
4108 .into()
4109 }
4110
4111 fn format_remap_target(target: &str) -> String {
4116 if let Some(rest) = target.strip_prefix("KEY_") {
4118 match rest {
4120 "LEFTCTRL" => "LCtrl".to_string(),
4121 "RIGHTCTRL" => "RCtrl".to_string(),
4122 "LEFTSHIFT" => "LShft".to_string(),
4123 "RIGHTSHIFT" => "RShft".to_string(),
4124 "LEFTALT" => "LAlt".to_string(),
4125 "RIGHTALT" => "RAlt".to_string(),
4126 "LEFTMETA" => "LMeta".to_string(),
4127 "RIGHTMETA" => "RMeta".to_string(),
4128 "SPACE" => "Space".to_string(),
4129 "TAB" => "Tab".to_string(),
4130 "ENTER" => "Enter".to_string(),
4131 "ESC" => "Esc".to_string(),
4132 "BACKSPACE" => "Bksp".to_string(),
4133 "DELETE" => "Del".to_string(),
4134 "INSERT" => "Ins".to_string(),
4135 "HOME" => "Home".to_string(),
4136 "END" => "End".to_string(),
4137 "PAGEUP" => "PgUp".to_string(),
4138 "PAGEDOWN" => "PgDn".to_string(),
4139 "UP" => "↑".to_string(),
4140 "DOWN" => "↓".to_string(),
4141 "LEFT" => "←".to_string(),
4142 "RIGHT" => "→".to_string(),
4143 s if s.len() == 1 => s.to_uppercase(),
4145 s if s.starts_with('F') => format!("F{}", &s[1..]),
4147 _ => rest.to_string(),
4148 }
4149 } else if let Some(rest) = target.strip_prefix("BTN_") {
4150 match rest {
4152 "LEFT" => "LMB".to_string(),
4153 "RIGHT" => "RMB".to_string(),
4154 "MIDDLE" => "Mid".to_string(),
4155 "SIDE" => "Side".to_string(),
4156 "EXTRA" => "Extra".to_string(),
4157 "FORWARD" => "Fwd".to_string(),
4158 "BACK" => "Back".to_string(),
4159 _ => rest.to_string(),
4160 }
4161 } else if let Some(rest) = target.strip_prefix("REL_") {
4162 match rest {
4164 "WHEEL" => "Wheel".to_string(),
4165 "HWHEEL" => "HWheel".to_string(),
4166 _ => rest.to_string(),
4167 }
4168 } else {
4169 if target.len() > 6 {
4171 format!("{}...", &target[..6])
4172 } else {
4173 target.to_string()
4174 }
4175 }
4176 }
4177
4178 fn view_azeron_keypad(&self) -> Element<'_, Message> {
4187 let layout = azeron_keypad_layout();
4188
4189 let mut rows: Vec<Vec<Element<'_, Message>>> = Vec::with_capacity(10);
4191 for _ in 0..10 {
4192 rows.push(Vec::new());
4193 }
4194
4195 for keypad_button in &layout {
4196 let button_id = keypad_button.id.clone();
4197 let label = keypad_button.label.clone();
4198 let remap = keypad_button.current_remap.clone();
4199 let is_selected = self.selected_button == Some(
4200 layout.iter().position(|b| b.id == keypad_button.id).unwrap_or(usize::MAX)
4201 );
4202
4203 let button_style = if is_selected {
4205 iced::theme::Button::Primary
4206 } else if remap.is_some() {
4207 iced::theme::Button::Secondary
4208 } else {
4209 iced::theme::Button::Text
4210 };
4211
4212 let button_content: Element<'_, Message> = if let Some(ref target) = remap {
4215 let display_name = Self::format_remap_target(target);
4217 container(
4219 column![
4220 text(label).size(8).style(iced::theme::Text::Color(iced::Color::from_rgb(0.5, 0.5, 0.5))),
4221 text(display_name).size(11).width(Length::Fixed(45.0)),
4222 ]
4223 .spacing(2)
4224 .align_items(Alignment::Center)
4225 )
4226 .center_x()
4227 .center_y()
4228 .into()
4229 } else {
4230 container(text(label).size(12))
4232 .center_x()
4233 .center_y()
4234 .into()
4235 };
4236
4237 let btn = button(button_content)
4238 .on_press(Message::SelectKeypadButton(button_id.clone()))
4239 .style(button_style)
4240 .padding([6, 8])
4241 .width(iced::Length::Fixed(54.0))
4242 .height(iced::Length::Fixed(54.0))
4243 .into();
4244
4245 if rows.get_mut(keypad_button.row).is_some() {
4246 rows[keypad_button.row].push(btn);
4247 }
4248 }
4249
4250 let hat_switch = container(
4252 text("HAT\n↕").size(10)
4253 )
4254 .width(iced::Length::Fixed(54.0))
4255 .height(iced::Length::Fixed(54.0))
4256 .center_x()
4257 .center_y()
4258 .style(container_styles::card)
4259 .into();
4260
4261 if rows.get_mut(5).is_some() {
4263 rows[5].push(hat_switch);
4264 }
4265
4266 let keypad_rows: Vec<Element<'_, Message>> = rows
4268 .into_iter()
4269 .filter(|r| !r.is_empty())
4270 .map(|row_elements| row(row_elements).spacing(4).align_items(Alignment::Center).into())
4271 .collect();
4272
4273 let keypad_content = column![
4274 text("Azeron Keypad Layout").size(20),
4275 Space::with_height(10),
4276 text("Click a button to configure remapping").size(12),
4277 Space::with_height(20),
4278 ]
4279 .spacing(10)
4280 .align_items(Alignment::Center)
4281 .push(column(keypad_rows).spacing(4).align_items(Alignment::Center));
4282
4283 container(keypad_content)
4284 .padding(24)
4285 .width(Length::Fill)
4286 .center_x()
4287 .into()
4288 }
4289
4290 fn view_status_bar(&self) -> Element<'_, Message> {
4291 let connection_indicator = if self.daemon_connected {
4292 text("● Connected").size(12)
4293 } else {
4294 text("○ Disconnected").size(12)
4295 };
4296
4297 let latest_notification = if let Some(notif) = self.notifications.back() {
4298 if notif.is_error {
4299 text(¬if.message).size(12)
4300 } else {
4301 text(¬if.message).size(12)
4302 }
4303 } else {
4304 text("Ready").size(12)
4305 };
4306
4307 container(
4308 row![
4309 connection_indicator,
4310 text(" | ").size(12),
4311 latest_notification,
4312 Space::with_width(Length::Fill),
4313 text(format!("{} macros", self.macros.len())).size(12),
4314 ]
4315 .spacing(5)
4316 .align_items(Alignment::Center)
4317 )
4318 .padding([8, 16])
4319 .width(Length::Fill)
4320 .into()
4321 }
4322
4323 fn layer_indicator(&self, device_id: &str) -> Element<'_, Message> {
4328 if let Some(&layer_id) = self.active_layers.get(device_id) {
4329 let layer_name = self.layer_configs
4331 .get(device_id)
4332 .and_then(|layers| layers.iter().find(|l| l.layer_id == layer_id))
4333 .map(|l| l.name.as_str())
4334 .unwrap_or("Unknown");
4335
4336 container(
4337 text(format!("Layer {}: {}", layer_id, layer_name))
4338 .size(12)
4339 )
4340 .padding([4, 8])
4341 .style(container_styles::card)
4342 .into()
4343 } else {
4344 container(
4346 text("Layer 0: Base").size(12)
4347 )
4348 .padding([4, 8])
4349 .style(container_styles::card)
4350 .into()
4351 }
4352 }
4353
4354 fn profile_quick_toggles(&self, device_path: &str) -> Element<'_, Message> {
4360 let profiles = self.remap_profiles.get(device_path);
4361 let active_profile = self.active_remap_profiles.get(device_path);
4362
4363 if let Some(profile_list) = profiles {
4364 if profile_list.is_empty() {
4365 return row![].into(); }
4367
4368 let buttons: Vec<Element<'_, Message>> = profile_list
4369 .iter()
4370 .map(|profile| {
4371 let is_active = active_profile.as_ref().map(|s| s.as_str()) == Some(profile.name.as_str());
4372 let button_style = if is_active {
4373 iced::theme::Button::Primary
4374 } else {
4375 iced::theme::Button::Secondary
4376 };
4377
4378 button(
4379 text(&profile.name).size(11)
4380 )
4381 .on_press(Message::ActivateRemapProfile(device_path.to_string(), profile.name.clone()))
4382 .style(button_style)
4383 .padding([6, 10])
4384 .into()
4385 })
4386 .collect();
4387
4388 let mut final_buttons = buttons;
4390 if active_profile.is_some() {
4391 final_buttons.push(
4392 button(
4393 text("Off").size(11)
4394 )
4395 .on_press(Message::DeactivateRemapProfile(device_path.to_string()))
4396 .style(iced::theme::Button::Text)
4397 .padding([6, 10])
4398 .into()
4399 );
4400 }
4401
4402 row(final_buttons).spacing(6).into()
4403 } else {
4404 row![].into() }
4406 }
4407
4408 fn layer_activation_buttons(&self, device_id: &str) -> Element<'_, Message> {
4413 let layers = self.layer_configs.get(device_id);
4414
4415 if let Some(layer_list) = layers {
4416 let toggle_layers: Vec<_> = layer_list
4418 .iter()
4419 .filter(|l| l.mode == LayerMode::Toggle && l.layer_id > 0)
4420 .collect();
4421
4422 if toggle_layers.is_empty() {
4423 return text("No toggle layers configured").size(11).into();
4424 }
4425
4426 let active_layer_id = self.active_layers.get(device_id).copied().unwrap_or(0);
4427
4428 let buttons: Vec<Element<'_, Message>> = toggle_layers
4429 .iter()
4430 .map(|layer| {
4431 let is_active = active_layer_id == layer.layer_id;
4432 let button_style = if is_active {
4433 iced::theme::Button::Secondary
4434 } else {
4435 iced::theme::Button::Text
4436 };
4437
4438 button(
4439 text(format!("L{}", layer.layer_id)).size(11)
4440 )
4441 .on_press(Message::LayerActivateRequested(
4442 device_id.to_string(),
4443 layer.layer_id,
4444 LayerMode::Toggle,
4445 ))
4446 .style(button_style)
4447 .padding([4, 8])
4448 .into()
4449 })
4450 .collect();
4451
4452 row(buttons).spacing(4).into()
4453 } else {
4454 text("Load layers to see toggle buttons").size(11).into()
4455 }
4456 }
4457
4458 fn deadzone_buttons(&self, device_id: &str, is_y_axis: bool, current: u8) -> Element<'_, Message> {
4462 let percentages = [0, 10, 20, 30, 40, 50];
4463 let buttons: Vec<Element<'_, Message>> = percentages
4464 .iter()
4465 .map(|&pct| {
4466 let is_current = current == pct;
4467 button(text(format!("{}%", pct)).size(10))
4468 .on_press(if is_y_axis {
4469 Message::SetAnalogDeadzoneXY(device_id.to_string(), current, pct)
4470 } else {
4471 Message::SetAnalogDeadzoneXY(device_id.to_string(), pct, current)
4472 })
4473 .style(if is_current {
4474 iced::theme::Button::Primary
4475 } else {
4476 iced::theme::Button::Text
4477 })
4478 .padding([2, 6])
4479 .into()
4480 })
4481 .collect();
4482
4483 row(buttons).spacing(2).into()
4484 }
4485
4486 fn outer_deadzone_buttons(&self, device_id: &str, is_y_axis: bool, current: u8) -> Element<'_, Message> {
4490 let percentages = [80, 85, 90, 95, 100];
4491 let buttons: Vec<Element<'_, Message>> = percentages
4492 .iter()
4493 .map(|&pct| {
4494 let is_current = current == pct;
4495 button(text(format!("{}%", pct)).size(10))
4496 .on_press(if is_y_axis {
4497 Message::SetAnalogOuterDeadzoneXY(device_id.to_string(), current, pct)
4498 } else {
4499 Message::SetAnalogOuterDeadzoneXY(device_id.to_string(), pct, current)
4500 })
4501 .style(if is_current {
4502 iced::theme::Button::Primary
4503 } else {
4504 iced::theme::Button::Text
4505 })
4506 .padding([2, 6])
4507 .into()
4508 })
4509 .collect();
4510
4511 row(buttons).spacing(2).into()
4512 }
4513
4514 fn layer_settings_view(&self, device_id: &str) -> Element<'_, Message> {
4518 let layers = self.layer_configs.get(device_id);
4519
4520 if let Some(layer_list) = layers {
4521 if layer_list.is_empty() {
4522 return column![
4523 text("No layers configured").size(14),
4524 text("Default base layer will be created automatically").size(11),
4525 ]
4526 .spacing(4)
4527 .into();
4528 }
4529
4530 let mut rows: Vec<Element<'_, Message>> = layer_list
4531 .iter()
4532 .map(|layer| {
4533 let mode_text = match layer.mode {
4534 LayerMode::Hold => "Hold",
4535 LayerMode::Toggle => "Toggle",
4536 };
4537
4538 row![
4539 text(format!("L{}", layer.layer_id)).size(12).width(Length::Fixed(30.0)),
4540 text(&layer.name).size(12).width(Length::Fixed(100.0)),
4541 text(mode_text).size(12).width(Length::Fixed(60.0)),
4542 text(format!("{} remaps", layer.remap_count)).size(11),
4543 Space::with_width(Length::Fill),
4544 button(text("Edit").size(11))
4545 .on_press(Message::OpenLayerConfigDialog(device_id.to_string(), layer.layer_id))
4546 .style(iced::theme::Button::Text)
4547 .padding([4, 8]),
4548 ]
4549 .spacing(8)
4550 .align_items(Alignment::Center)
4551 .into()
4552 })
4553 .collect();
4554
4555 let add_button = if layer_list.len() < 8 {
4557 Some(
4558 button(
4559 row![
4560 text("+").size(14),
4561 text("Add Layer").size(12),
4562 ]
4563 .spacing(4)
4564 )
4565 .on_press(Message::OpenLayerConfigDialog(
4566 device_id.to_string(),
4567 layer_list.len(),
4568 ))
4569 .style(iced::theme::Button::Secondary)
4570 .padding([6, 12])
4571 .into()
4572 )
4573 } else {
4574 None
4575 };
4576
4577 if let Some(btn) = add_button {
4578 rows.push(btn);
4579 }
4580
4581 column(rows).spacing(8).into()
4582 } else {
4583 column![
4584 text("Load layers to see settings").size(12),
4585 button("Load Layers")
4586 .on_press(Message::LayerConfigRequested(device_id.to_string()))
4587 .style(iced::theme::Button::Secondary),
4588 ]
4589 .spacing(8)
4590 .into()
4591 }
4592 }
4593
4594 fn layer_config_dialog(&self) -> Option<Element<'_, Message>> {
4598 if let Some((_device_id, layer_id, name, mode)) = &self.layer_config_dialog {
4599 let mode_options = vec!["Hold".to_string(), "Toggle".to_string()];
4600 let current_mode_str = match mode {
4601 LayerMode::Hold => "Hold",
4602 LayerMode::Toggle => "Toggle",
4603 };
4604
4605 let dialog = container(
4606 column![
4607 text(format!("Configure Layer {}", layer_id)).size(18),
4608 Space::with_height(20),
4609 text("Layer Name:").size(12),
4610 text_input("Enter layer name...", name)
4611 .on_input(Message::LayerConfigNameChanged)
4612 .padding(8)
4613 .size(14)
4614 .width(Length::Fixed(250.0)),
4615 Space::with_height(12),
4616 text("Activation Mode:").size(12),
4617 pick_list(mode_options, Some(current_mode_str.to_string()), |selected| {
4618 let new_mode = match selected.as_str() {
4619 "Toggle" => LayerMode::Toggle,
4620 _ => LayerMode::Hold,
4621 };
4622 Message::LayerConfigModeChanged(new_mode)
4623 })
4624 .width(Length::Fixed(250.0))
4625 .padding(8),
4626 Space::with_height(20),
4627 row![
4628 button("Cancel")
4629 .on_press(Message::CancelLayerConfig)
4630 .style(iced::theme::Button::Text)
4631 .padding([8, 16]),
4632 Space::with_width(Length::Fill),
4633 button("Save")
4634 .on_press(Message::SaveLayerConfig)
4635 .style(iced::theme::Button::Primary)
4636 .padding([8, 16]),
4637 ]
4638 .spacing(8),
4639 ]
4640 .spacing(4)
4641 )
4642 .padding(24)
4643 .width(Length::Fixed(300.0))
4644 .style(container_styles::card);
4645
4646 Some(
4648 container(
4649 container(dialog)
4650 .width(Length::Fill)
4651 .center_x()
4652 .center_y()
4653 )
4654 .width(Length::Fill)
4655 .height(Length::Fill)
4656 .style(iced::theme::Container::Transparent)
4657 .into()
4658 )
4659 } else {
4660 None
4661 }
4662 }
4663
4664 fn get_zone_color(&self, zone: LedZone) -> (u8, u8, u8) {
4666 if let Some(device_id) = &self.led_config_device {
4667 if let Some(led_state) = self.led_states.get(device_id) {
4668 if let Some(&color) = led_state.zone_colors.get(&zone) {
4669 return color;
4670 }
4671 }
4672 }
4673 (255, 255, 255)
4675 }
4676
4677 fn view_led_rgb_sliders(&self) -> Element<'_, Message> {
4679 let zone = self.selected_led_zone.unwrap_or(LedZone::Logo);
4680 let (r, g, b) = self.pending_led_color.unwrap_or_else(|| self.get_zone_color(zone));
4681
4682 Column::new()
4683 .spacing(8)
4684 .push(
4685 row![
4686 text("Red:").size(12).width(Length::Fixed(40.0)),
4687 text(format!("{}", r)).size(12).width(Length::Fixed(30.0)),
4688 slider(0..=255, r, move |v| {
4689 let (_, g, b) = (v as u8, g, b);
4690 Message::LedSliderChanged(v as u8, g, b)
4691 })
4692 .width(Length::Fill)
4693 ]
4694 .spacing(8)
4695 .align_items(Alignment::Center)
4696 )
4697 .push(
4698 row![
4699 text("Green:").size(12).width(Length::Fixed(40.0)),
4700 text(format!("{}", g)).size(12).width(Length::Fixed(30.0)),
4701 slider(0..=255, g, move |v| {
4702 let (r, _, b) = (r, v as u8, b);
4703 Message::LedSliderChanged(r, v as u8, b)
4704 })
4705 .width(Length::Fill)
4706 ]
4707 .spacing(8)
4708 .align_items(Alignment::Center)
4709 )
4710 .push(
4711 row![
4712 text("Blue:").size(12).width(Length::Fixed(40.0)),
4713 text(format!("{}", b)).size(12).width(Length::Fixed(30.0)),
4714 slider(0..=255, b, move |v| {
4715 let (r, g, _) = (r, g, v as u8);
4716 Message::LedSliderChanged(r, g, v as u8)
4717 })
4718 .width(Length::Fill)
4719 ]
4720 .spacing(8)
4721 .align_items(Alignment::Center)
4722 )
4723 .into()
4724 }
4725
4726 fn led_color_style(zone: Option<LedZone>, zone_colors: &std::collections::HashMap<LedZone, (u8, u8, u8)>) -> iced::theme::Container {
4728 let (r, g, b) = zone
4729 .and_then(|z| zone_colors.get(&z))
4730 .copied()
4731 .unwrap_or((255, 255, 255));
4732
4733 struct LedColorStyle {
4734 r: u8,
4735 g: u8,
4736 b: u8,
4737 }
4738
4739 impl iced::widget::container::StyleSheet for LedColorStyle {
4740 type Style = Theme;
4741
4742 fn appearance(&self, _style: &Self::Style) -> iced::widget::container::Appearance {
4743 iced::widget::container::Appearance {
4744 background: Some(Color::from_rgb8(self.r, self.g, self.b).into()),
4745 ..Default::default()
4746 }
4747 }
4748 }
4749
4750 iced::theme::Container::Custom(Box::new(LedColorStyle { r, g, b }))
4751 }
4752
4753 pub fn view_led_config(&self) -> Option<Element<'_, Message>> {
4758 if let Some(ref device_id) = self.led_config_device {
4759 let selected_zone = self.selected_led_zone.unwrap_or(LedZone::Logo);
4760 let led_state = self.led_states.get(device_id);
4761 let zone_colors = led_state.map(|s| &s.zone_colors);
4762 let current_color = self.get_zone_color(selected_zone);
4763
4764 let zones = vec![
4766 (LedZone::Logo, "Logo"),
4767 (LedZone::Keys, "Keys"),
4768 (LedZone::Thumbstick, "Thumbstick"),
4769 ];
4770
4771 let zone_buttons: Vec<Element<'_, Message>> = zones
4772 .into_iter()
4773 .map(|(zone, label)| {
4774 let is_selected = self.selected_led_zone == Some(zone);
4775 button(text(label).size(12))
4776 .on_press(Message::SelectLedZone(zone))
4777 .style(if is_selected {
4778 iced::theme::Button::Primary
4779 } else {
4780 iced::theme::Button::Secondary
4781 })
4782 .padding([6, 12])
4783 .into()
4784 })
4785 .collect();
4786
4787 let preview = container(
4789 container(
4790 text(format!("RGB({}, {}, {})", current_color.0, current_color.1, current_color.2))
4791 .size(11)
4792 .horizontal_alignment(iced::alignment::Horizontal::Center)
4793 )
4794 .width(Length::Fill)
4795 .height(Length::Fill)
4796 .align_x(iced::alignment::Horizontal::Center)
4797 .align_y(iced::alignment::Vertical::Center)
4798 )
4799 .width(Length::Fixed(120.0))
4800 .height(Length::Fixed(60.0))
4801 .style(if let Some(colors) = zone_colors {
4802 Self::led_color_style(self.selected_led_zone, colors)
4803 } else {
4804 iced::theme::Container::Transparent
4805 });
4806
4807 let patterns = vec![
4809 (LedPattern::Static, "Static"),
4810 (LedPattern::Breathing, "Breathing"),
4811 (LedPattern::Rainbow, "Rainbow"),
4812 ];
4813
4814 let current_pattern = led_state.map(|s| s.active_pattern).unwrap_or(LedPattern::Static);
4815
4816 let pattern_buttons: Vec<Element<'_, Message>> = patterns
4817 .into_iter()
4818 .map(|(pattern, label)| {
4819 let is_active = current_pattern == pattern;
4820 button(text(label).size(11))
4821 .on_press(Message::SetLedPattern(device_id.clone(), pattern))
4822 .style(if is_active {
4823 iced::theme::Button::Primary
4824 } else {
4825 iced::theme::Button::Secondary
4826 })
4827 .padding([4, 10])
4828 .into()
4829 })
4830 .collect();
4831
4832 let brightness = led_state.map(|s| s.global_brightness as f32).unwrap_or(100.0);
4833
4834 let dialog = container(
4835 column![
4836 row![
4838 text("LED Configuration").size(18),
4839 Space::with_width(Length::Fill),
4840 button(text("×").size(20))
4841 .on_press(Message::CloseLedConfig)
4842 .style(iced::theme::Button::Text)
4843 .padding([0, 8])
4844 ]
4845 .spacing(8)
4846 .align_items(Alignment::Center),
4847
4848 horizontal_rule(1),
4849
4850 text(device_id).size(11).width(Length::Fill),
4852
4853 text("Zone:").size(13),
4855 row(zone_buttons).spacing(8),
4856
4857 horizontal_rule(1),
4858
4859 text("Color:").size(13),
4861 row![
4862 preview,
4863 column![
4864 text("Adjust RGB sliders below").size(11),
4865 text("to change color").size(11),
4866 ]
4867 .spacing(4)
4868 ]
4869 .spacing(12)
4870 .align_items(Alignment::Center),
4871
4872 self.view_led_rgb_sliders(),
4874
4875 horizontal_rule(1),
4876
4877 text(format!("Brightness: {}%", brightness as u8)).size(13),
4879 slider(0.0..=100.0, brightness, move |v| {
4880 Message::SetLedBrightness(device_id.clone(), None, v as u8)
4881 })
4882 .width(Length::Fill),
4883
4884 horizontal_rule(1),
4885
4886 text("Pattern:").size(13),
4888 row(pattern_buttons).spacing(8),
4889
4890 horizontal_rule(1),
4891
4892 row![
4894 Space::with_width(Length::Fill),
4895 button(text("Close").size(13))
4896 .on_press(Message::CloseLedConfig)
4897 .style(iced::theme::Button::Secondary)
4898 .padding([6, 16])
4899 ]
4900 .spacing(8)
4901 ]
4902 .spacing(12)
4903 .padding(20)
4904 )
4905 .max_width(500)
4906 .style(container_styles::card);
4907
4908 Some(
4910 container(dialog)
4911 .width(Length::Fill)
4912 .height(Length::Fill)
4913 .align_x(iced::alignment::Horizontal::Center)
4914 .align_y(iced::alignment::Vertical::Center)
4915 .padding(40)
4916 .style(iced::theme::Container::Transparent)
4917 .into(),
4918 )
4919 } else {
4920 None
4921 }
4922 }
4923
4924 pub fn view_analog_calibration(&self) -> Option<Element<'_, Message>> {
4929 if let Some(ref view) = self.analog_calibration_view {
4930 let dialog = container(view.view())
4931 .max_width(600)
4932 .max_height(800)
4933 .style(container_styles::card);
4934
4935 Some(
4937 container(dialog)
4938 .width(Length::Fill)
4939 .height(Length::Fill)
4940 .align_x(iced::alignment::Horizontal::Center)
4941 .align_y(iced::alignment::Vertical::Center)
4942 .padding(40)
4943 .style(iced::theme::Container::Transparent)
4944 .into(),
4945 )
4946 } else {
4947 None
4948 }
4949 }
4950}
4951
4952impl AnalogCalibrationView {
4953 fn checkbox_button<'a>(&'a self, label: &str, is_checked: bool, msg: fn(bool) -> Message) -> Element<'a, Message> {
4954 let btn = if is_checked {
4955 button(text(format!("[X] {}", label)).size(14))
4956 } else {
4957 button(text(format!("[ ] {}", label)).size(14))
4958 };
4959 btn.on_press(msg(is_checked))
4960 .style(iced::theme::Button::Text)
4961 .into()
4962 }
4963
4964 pub fn view(&self) -> Element<Message> {
4965 use iced::widget::{horizontal_rule as rule, Row, Column, container, Canvas};
4966
4967 let title = text("Analog Calibration").size(24);
4968
4969 let info = Column::new()
4971 .spacing(5)
4972 .push(text(format!("Device: {}", self.device_id)).size(14))
4973 .push(text(format!("Layer: {}", self.layer_id)).size(14));
4974
4975 let visualizer_section = Column::new()
4977 .spacing(10)
4978 .push(text("Stick Position").size(18))
4979 .push(
4980 container(
4981 Canvas::new(AnalogVisualizer {
4982 stick_x: self.stick_x,
4983 stick_y: self.stick_y,
4984 deadzone: self.calibration.deadzone,
4985 deadzone_shape: match self.deadzone_shape_selected {
4986 DeadzoneShape::Circular => WidgetDeadzoneShape::Circular,
4987 DeadzoneShape::Square => WidgetDeadzoneShape::Square,
4988 },
4989 range_min: self.calibration.range_min,
4990 range_max: self.calibration.range_max,
4991 cache: Arc::clone(&self.visualizer_cache),
4992 })
4993 .width(Length::Fixed(250.0))
4994 .height(Length::Fixed(250.0))
4995 )
4996 .width(Length::Fixed(270.0))
4997 .height(Length::Fixed(270.0))
4998 .center_x()
4999 .center_y()
5000 );
5001
5002 let mode_section = Column::new()
5004 .spacing(10)
5005 .push(text("Output Mode").size(18))
5006 .push(
5007 Row::new()
5008 .spacing(10)
5009 .push(text("Mode:"))
5010 .push(pick_list(
5011 &AnalogMode::ALL[..],
5012 Some(self.analog_mode_selected),
5013 Message::AnalogModeChanged,
5014 ))
5015 );
5016
5017 let mode_section = if self.analog_mode_selected == AnalogMode::Camera {
5019 mode_section.push(
5020 Row::new()
5021 .spacing(10)
5022 .push(text("Camera:"))
5023 .push(pick_list(
5024 &CameraOutputMode::ALL[..],
5025 Some(self.camera_mode_selected),
5026 Message::CameraModeChanged,
5027 ))
5028 )
5029 } else {
5030 mode_section
5031 };
5032
5033 let deadzone_section = Column::new()
5035 .spacing(10)
5036 .push(text("Deadzone").size(18))
5037 .push(
5038 Row::new()
5039 .spacing(10)
5040 .push(text("Size:"))
5041 .push(text(format!("{:.0}%", self.calibration.deadzone * 100.0)))
5042 .push(slider(0.0..=1.0, self.calibration.deadzone, Message::AnalogDeadzoneChanged).step(0.01))
5043 )
5044 .push(
5045 Row::new()
5046 .spacing(10)
5047 .push(text("Shape:"))
5048 .push(pick_list(
5049 &DeadzoneShape::ALL[..],
5050 Some(self.deadzone_shape_selected),
5051 Message::AnalogDeadzoneShapeChanged,
5052 ))
5053 );
5054
5055 let sensitivity_section = Column::new()
5057 .spacing(10)
5058 .push(text("Sensitivity").size(18))
5059 .push(
5060 Row::new()
5061 .spacing(10)
5062 .push(text("Multiplier:"))
5063 .push(text(format!("{:.1}", self.calibration.sensitivity_multiplier)))
5064 .push(slider(0.1..=5.0, self.calibration.sensitivity_multiplier, Message::AnalogSensitivityChanged).step(0.1))
5065 )
5066 .push(
5067 Row::new()
5068 .spacing(10)
5069 .push(text("Curve:"))
5070 .push(pick_list(
5071 &SensitivityCurve::ALL[..],
5072 Some(self.sensitivity_curve_selected),
5073 Message::AnalogSensitivityCurveChanged,
5074 ))
5075 )
5076 .push(text(format!("Curve: {}", self.sensitivity_curve_selected)).size(14))
5077 .push(
5078 container(
5079 Canvas::new(CurveGraph {
5080 curve: self.sensitivity_curve_selected,
5081 multiplier: self.calibration.sensitivity_multiplier,
5082 })
5083 .width(Length::Fixed(300.0))
5084 .height(Length::Fixed(200.0))
5085 )
5086 .width(Length::Fixed(320.0))
5087 .center_x()
5088 );
5089
5090 let range_section = Column::new()
5092 .spacing(10)
5093 .push(text("Output Range").size(18))
5094 .push(
5095 Row::new()
5096 .spacing(10)
5097 .push(text("Min:"))
5098 .push(text(self.calibration.range_min.to_string()))
5099 .push(slider(-32768..=0, self.calibration.range_min, Message::AnalogRangeMinChanged))
5100 )
5101 .push(
5102 Row::new()
5103 .spacing(10)
5104 .push(text("Max:"))
5105 .push(text(self.calibration.range_max.to_string()))
5106 .push(slider(0..=32767, self.calibration.range_max, Message::AnalogRangeMaxChanged))
5107 );
5108
5109 let inversion_section = Column::new()
5111 .spacing(10)
5112 .push(text("Axis Inversion").size(18))
5113 .push(
5114 Row::new()
5115 .spacing(20)
5116 .push(self.checkbox_button("Invert X", self.invert_x_checked, Message::AnalogInvertXToggled))
5117 .push(self.checkbox_button("Invert Y", self.invert_y_checked, Message::AnalogInvertYToggled))
5118 );
5119
5120 let buttons = Row::new()
5122 .spacing(10)
5123 .push(
5124 button("Apply")
5125 .on_press(Message::ApplyAnalogCalibration)
5126 )
5127 .push(
5128 button("Close")
5129 .on_press(Message::CloseAnalogCalibration)
5130 .style(iced::theme::Button::Secondary)
5131 );
5132
5133 let content = if let Some(error) = &self.error {
5135 Column::new()
5136 .spacing(20)
5137 .push(title)
5138 .push(info)
5139 .push(rule(1))
5140 .push(text(format!("Error: {}", error)).style(Color::from_rgb(1.0, 0.4, 0.4)))
5141 .push(buttons)
5142 } else {
5143 Column::new()
5144 .spacing(20)
5145 .push(title)
5146 .push(info)
5147 .push(rule(1))
5148 .push(visualizer_section)
5149 .push(rule(1))
5150 .push(mode_section)
5151 .push(rule(1))
5152 .push(deadzone_section)
5153 .push(rule(1))
5154 .push(sensitivity_section)
5155 .push(rule(1))
5156 .push(range_section)
5157 .push(rule(1))
5158 .push(inversion_section)
5159 .push(rule(1))
5160 .push(buttons)
5161 };
5162
5163 scrollable(content).height(Length::Fill).into()
5164 }
5165}
5166
5167#[cfg(test)]
5168mod calibration_tests {
5169 use super::*;
5170 use aethermap_common::{AnalogMode, CameraOutputMode};
5171
5172 #[test]
5173 fn test_analog_calibration_view_default() {
5174 let view = AnalogCalibrationView::default();
5175
5176 assert_eq!(view.device_id, "");
5177 assert_eq!(view.layer_id, 0);
5178 assert_eq!(view.calibration.deadzone, 0.15);
5179 assert_eq!(view.stick_x, 0.0);
5180 assert_eq!(view.stick_y, 0.0);
5181 assert_eq!(view.loading, false);
5182 assert!(view.error.is_none());
5183 }
5184
5185 #[test]
5186 fn test_analog_calibration_view_with_values() {
5187 let view = AnalogCalibrationView {
5188 device_id: "test_device".to_string(),
5189 layer_id: 1,
5190 calibration: CalibrationConfig {
5191 deadzone: 0.2,
5192 deadzone_shape: "circular".to_string(),
5193 sensitivity: "quadratic".to_string(),
5194 sensitivity_multiplier: 1.5,
5195 range_min: -16384,
5196 range_max: 16383,
5197 invert_x: true,
5198 invert_y: false,
5199 exponent: 2.0,
5200 },
5201 deadzone_shape_selected: DeadzoneShape::Square,
5202 sensitivity_curve_selected: SensitivityCurve::Quadratic,
5203 analog_mode_selected: AnalogMode::Mouse,
5204 camera_mode_selected: CameraOutputMode::Keys,
5205 invert_x_checked: true,
5206 invert_y_checked: false,
5207 stick_x: 0.5,
5208 stick_y: -0.3,
5209 loading: false,
5210 error: None,
5211 last_visualizer_update: Instant::now(),
5212 visualizer_cache: Arc::new(iced::widget::canvas::Cache::default()),
5213 };
5214
5215 assert_eq!(view.device_id, "test_device");
5216 assert_eq!(view.layer_id, 1);
5217 assert_eq!(view.calibration.deadzone, 0.2);
5218 assert_eq!(view.stick_x, 0.5);
5219 assert_eq!(view.stick_y, -0.3);
5220 assert_eq!(view.analog_mode_selected, AnalogMode::Mouse);
5221 assert_eq!(view.camera_mode_selected, CameraOutputMode::Keys);
5222 assert_eq!(view.invert_x_checked, true);
5223 assert_eq!(view.invert_y_checked, false);
5224 }
5225
5226 #[test]
5227 fn test_calibration_config_default() {
5228 let config = CalibrationConfig::default();
5229
5230 assert_eq!(config.deadzone, 0.15);
5231 assert_eq!(config.deadzone_shape, "circular");
5232 assert_eq!(config.sensitivity, "linear");
5233 assert_eq!(config.sensitivity_multiplier, 1.0);
5234 assert_eq!(config.range_min, -32768);
5235 assert_eq!(config.range_max, 32767);
5236 assert_eq!(config.invert_x, false);
5237 assert_eq!(config.invert_y, false);
5238 assert_eq!(config.exponent, 2.0);
5239 }
5240
5241 #[test]
5242 fn test_deadzone_shape_display() {
5243 assert_eq!(DeadzoneShape::Circular.to_string(), "Circular");
5244 assert_eq!(DeadzoneShape::Square.to_string(), "Square");
5245 }
5246
5247 #[test]
5248 fn test_sensitivity_curve_display() {
5249 assert_eq!(SensitivityCurve::Linear.to_string(), "Linear");
5250 assert_eq!(SensitivityCurve::Quadratic.to_string(), "Quadratic");
5251 assert_eq!(SensitivityCurve::Exponential.to_string(), "Exponential");
5252 }
5253
5254 #[test]
5255 fn test_deadzone_shape_default() {
5256 assert_eq!(DeadzoneShape::default(), DeadzoneShape::Circular);
5257 }
5258
5259 #[test]
5260 fn test_sensitivity_curve_default() {
5261 assert_eq!(SensitivityCurve::default(), SensitivityCurve::Linear);
5262 }
5263
5264 #[test]
5265 fn test_analog_calibration_view_clone() {
5266 let view = AnalogCalibrationView {
5267 device_id: "test_device".to_string(),
5268 layer_id: 1,
5269 calibration: CalibrationConfig {
5270 deadzone: 0.2,
5271 ..Default::default()
5272 },
5273 ..Default::default()
5274 };
5275
5276 let cloned = view.clone();
5277 assert_eq!(cloned.device_id, "test_device");
5278 assert_eq!(cloned.layer_id, 1);
5279 assert_eq!(cloned.calibration.deadzone, 0.2);
5280 assert!(cloned.last_visualizer_update.elapsed() < Duration::from_secs(1));
5282 }
5283
5284 #[test]
5285 fn test_throttling_threshold() {
5286 let view = AnalogCalibrationView {
5288 device_id: "test".to_string(),
5289 layer_id: 0,
5290 calibration: CalibrationConfig::default(),
5291 deadzone_shape_selected: DeadzoneShape::Circular,
5292 sensitivity_curve_selected: SensitivityCurve::Linear,
5293 analog_mode_selected: AnalogMode::Disabled,
5294 camera_mode_selected: CameraOutputMode::Scroll,
5295 invert_x_checked: false,
5296 invert_y_checked: false,
5297 stick_x: 0.0,
5298 stick_y: 0.0,
5299 loading: false,
5300 error: None,
5301 last_visualizer_update: Instant::now(),
5302 visualizer_cache: Arc::new(iced::widget::canvas::Cache::default()),
5303 };
5304
5305 assert!(view.last_visualizer_update.elapsed() < Duration::from_millis(33));
5307
5308 std::thread::sleep(Duration::from_millis(40));
5310 assert!(view.last_visualizer_update.elapsed() >= Duration::from_millis(33));
5311 }
5312
5313 #[test]
5314 fn test_visualizer_cache_arc_sharing() {
5315 let cache = Arc::new(iced::widget::canvas::Cache::default());
5317 let cache_clone = Arc::clone(&cache);
5318
5319 assert!(Arc::ptr_eq(&cache, &cache_clone));
5321 }
5322
5323 #[test]
5324 fn test_analog_mode_selection_states() {
5325 let modes = [
5327 AnalogMode::Disabled,
5328 AnalogMode::Dpad,
5329 AnalogMode::Gamepad,
5330 AnalogMode::Camera,
5331 AnalogMode::Mouse,
5332 AnalogMode::Wasd,
5333 ];
5334
5335 for mode in modes {
5336 let view = AnalogCalibrationView {
5337 analog_mode_selected: mode,
5338 ..Default::default()
5339 };
5340 assert_eq!(view.analog_mode_selected, mode);
5341 }
5342 }
5343
5344 #[test]
5345 fn test_camera_mode_selection_states() {
5346 let modes = [CameraOutputMode::Scroll, CameraOutputMode::Keys];
5348
5349 for mode in modes {
5350 let view = AnalogCalibrationView {
5351 camera_mode_selected: mode,
5352 ..Default::default()
5353 };
5354 assert_eq!(view.camera_mode_selected, mode);
5355 }
5356 }
5357}