Skip to main content

aethermap_gui/
gui.rs

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
12// Import custom widgets
13use 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// Import focus_tracker types - need to use path from lib.rs root
23// Since we're in gui.rs (a module of aethermap_gui library),
24// we access sibling modules via super:: or direct path when in closures
25
26// Razer brand colors (for future custom theming)
27// const RAZER_GREEN: Color = Color::from_rgb(0.267, 0.839, 0.173); // #44D62C
28// const RAZER_GREEN_DIM: Color = Color::from_rgb(0.176, 0.561, 0.118); // #2D8F1E
29// const BG_DEEP: Color = Color::from_rgb(0.051, 0.051, 0.051); // #0D0D0D
30// const BG_SURFACE: Color = Color::from_rgb(0.102, 0.102, 0.102); // #1A1A1A
31// const BG_ELEVATED: Color = Color::from_rgb(0.141, 0.141, 0.141); // #242424
32// const TEXT_PRIMARY: Color = Color::WHITE;
33// const TEXT_SECONDARY: Color = Color::from_rgb(0.702, 0.702, 0.702); // #B3B3B3
34// const TEXT_MUTED: Color = Color::from_rgb(0.400, 0.400, 0.400); // #666666
35// const DANGER_RED: Color = Color::from_rgb(1.0, 0.231, 0.188); // #FF3B30
36// const WARNING_YELLOW: Color = Color::from_rgb(1.0, 0.722, 0.0); // #FFB800
37
38#[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/// A button in the visual keypad layout
53///
54/// Represents the physical layout of a button on devices like the Azeron Cyborg.
55/// Coordinates are in a 10x10 grid for layout positioning.
56#[derive(Debug, Clone)]
57pub struct KeypadButton {
58    /// Button identifier (e.g., "JOY_BTN_0" through "JOY_BTN_25")
59    pub id: String,
60    /// Display label for the button
61    pub label: String,
62    /// Grid row (0-9) for layout positioning
63    pub row: usize,
64    /// Grid column (0-9) for layout positioning - reserved for future 2D layout use
65    #[allow(dead_code)]
66    pub col: usize,
67    /// Current remapping target (if any)
68    pub current_remap: Option<String>,
69}
70
71/// Azeron Cyborg keypad layout definition
72///
73/// Returns the button layout for the Azeron Cyborg keypad with all 26 joystick buttons.
74/// The layout positions buttons in a grid approximating the physical device.
75pub fn azeron_keypad_layout() -> Vec<KeypadButton> {
76    vec![
77        // Top function row (5 buttons)
78        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        // Main keyboard area (QWERTY-style layout, 12 buttons)
85        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        // Number/extra keys (5 buttons)
99        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        // Thumb cluster (5 buttons)
106        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/// Auto-switch rule for profile switching based on focused window
115///
116/// GUI representation of AutoSwitchRule from the daemon config.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct AutoSwitchRule {
119    /// Application identifier to match (e.g., "org.alacritty", "firefox", "*")
120    pub app_id: String,
121    /// Profile name to activate when this app has focus
122    pub profile_name: String,
123    /// Device ID to apply profile to (None = all devices)
124    pub device_id: Option<String>,
125    /// Layer ID to activate (None = profile default)
126    pub layer_id: Option<usize>,
127}
128
129/// Auto-switch rules view state
130///
131/// Manages the UI for configuring auto-profile switching rules.
132#[derive(Debug, Clone, Default)]
133pub struct AutoSwitchRulesView {
134    /// Device ID being configured
135    pub device_id: String,
136    /// List of configured rules
137    pub rules: Vec<AutoSwitchRule>,
138    /// Currently editing rule index (None = adding new)
139    pub editing_rule: Option<usize>,
140    /// New rule app_id input
141    pub new_app_id: String,
142    /// New rule profile_name input
143    pub new_profile_name: String,
144    /// New rule layer_id input
145    pub new_layer_id: String,
146}
147
148/// Hotkey binding for manual profile switching
149///
150/// GUI representation of HotkeyBinding from the daemon config.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct HotkeyBinding {
153    /// Modifier keys (Ctrl, Alt, Shift, Super)
154    pub modifiers: Vec<String>,
155    /// Trigger key (number 1-9 for profile switching)
156    pub key: String,
157    /// Profile to activate when hotkey pressed
158    pub profile_name: String,
159    /// Device to apply to (None = all devices)
160    pub device_id: Option<String>,
161    /// Layer to activate (None = profile default)
162    pub layer_id: Option<usize>,
163}
164
165/// Hotkey bindings view state
166///
167/// Manages the UI for configuring global hotkey bindings.
168#[derive(Debug, Clone, Default)]
169pub struct HotkeyBindingsView {
170    /// Device ID being configured
171    pub device_id: String,
172    /// List of configured bindings
173    pub bindings: Vec<HotkeyBinding>,
174    /// Currently editing binding index (None = adding new)
175    pub editing_binding: Option<usize>,
176    /// New binding modifiers (checkboxes)
177    pub new_modifiers: Vec<String>,
178    /// New binding key input
179    pub new_key: String,
180    /// New binding profile_name input
181    pub new_profile_name: String,
182    /// New binding layer_id input
183    pub new_layer_id: String,
184}
185
186/// Deadzone shape for analog calibration
187#[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/// Sensitivity curve for analog calibration
213#[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/// Analog calibration configuration state (GUI version)
245///
246/// Tracks the calibration settings for analog stick processing.
247/// This wraps the common type with Display conversion helpers.
248#[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/// Analog calibration view state
278///
279/// Manages the UI for configuring analog stick calibration settings.
280#[derive(Debug)]
281pub struct AnalogCalibrationView {
282    /// Device ID being configured
283    pub device_id: String,
284    /// Layer ID being configured
285    pub layer_id: usize,
286    /// Current calibration settings
287    pub calibration: CalibrationConfig,
288
289    /// Deadzone shape selection
290    pub deadzone_shape_selected: DeadzoneShape,
291    /// Sensitivity curve selection
292    pub sensitivity_curve_selected: SensitivityCurve,
293
294    /// Analog mode selection
295    pub analog_mode_selected: AnalogMode,
296    /// Camera output mode selection (when analog_mode is Camera)
297    pub camera_mode_selected: CameraOutputMode,
298
299    /// Inversion checkboxes
300    pub invert_x_checked: bool,
301    pub invert_y_checked: bool,
302
303    /// Current stick position for visualization (-1.0 to 1.0)
304    pub stick_x: f32,
305    /// Current stick position for visualization (-1.0 to 1.0)
306    pub stick_y: f32,
307
308    /// Loading state
309    pub loading: bool,
310    /// Error message if any
311    pub error: Option<String>,
312
313    /// Last time visualizer was updated (for throttling to ~30 FPS)
314    /// Not cloned - reset to Instant::now() on clone
315    pub last_visualizer_update: Instant,
316
317    /// Canvas cache for visualizer static elements (deadzone, axes)
318    /// Cleared when deadzone or shape changes.
319    /// Wrapped in Arc for sharing across widget instances.
320    pub visualizer_cache: Arc<iced::widget::canvas::Cache>,
321}
322
323// Manual Clone implementation since Instant doesn't implement Clone
324// Cache is wrapped in Arc so it can be cloned (shared)
325impl 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            // Reset to now for cloned instances - throttling will work correctly
342            last_visualizer_update: Instant::now(),
343            // Arc allows cloning the cache reference
344            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/// LED configuration state for a device
372///
373/// Tracks current LED settings including per-zone colors,
374/// brightness levels, and active pattern.
375#[derive(Debug, Clone)]
376pub struct LedState {
377    /// Per-zone RGB colors (Logo, Keys, Thumbstick, etc.)
378    pub zone_colors: HashMap<LedZone, (u8, u8, u8)>,
379    /// Global brightness (0-100)
380    pub global_brightness: u8,
381    /// Per-zone brightness (0-100)
382    pub zone_brightness: HashMap<LedZone, u8>,
383    /// Active LED pattern
384    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    /// Available profiles per device (device_id -> profile names)
417    pub device_profiles: HashMap<String, Vec<String>>,
418    /// Active profile per device (device_id -> profile name)
419    pub active_profiles: HashMap<String, String>,
420    /// Available remap profiles per device (device_path -> profile info)
421    pub remap_profiles: HashMap<String, Vec<RemapProfileInfo>>,
422    /// Active remap profile per device (device_path -> profile name)
423    pub active_remap_profiles: HashMap<String, String>,
424    /// Active remaps per device (device_path -> remap entries)
425    pub active_remaps: HashMap<String, (String, Vec<RemapEntry>)>,
426    /// Azeron keypad layout for selected device
427    pub keypad_layout: Vec<KeypadButton>,
428    /// Current device path being viewed in keypad layout
429    pub keypad_view_device: Option<String>,
430    /// Selected button for remapping (index into keypad_layout)
431    pub selected_button: Option<usize>,
432    /// Device capabilities for current selection
433    pub device_capabilities: Option<DeviceCapabilities>,
434    /// Active layer per device (device_id -> active_layer_id)
435    pub active_layers: HashMap<String, usize>,
436    /// Layer configurations per device (device_id -> layers)
437    pub layer_configs: HashMap<String, Vec<LayerConfigInfo>>,
438    /// Layer configuration dialog state (device_id, layer_id, name, mode)
439    pub layer_config_dialog: Option<(String, usize, String, LayerMode)>,
440    /// D-pad mode per device (device_id -> mode)
441    pub analog_dpad_modes: HashMap<String, String>,
442    /// Per-axis deadzone values (device_id -> (x_percentage, y_percentage))
443    pub analog_deadzones_xy: HashMap<String, (u8, u8)>,
444    /// Per-axis outer deadzone values (device_id -> (x_percentage, y_percentage))
445    pub analog_outer_deadzones_xy: HashMap<String, (u8, u8)>,
446    /// LED configuration state per device (device_id -> LedState)
447    pub led_states: HashMap<String, LedState>,
448    /// LED configuration dialog open for device
449    pub led_config_device: Option<String>,
450    /// Currently selected LED zone for color editing
451    pub selected_led_zone: Option<LedZone>,
452    /// Pending color picker values (r, g, b) before application
453    pub pending_led_color: Option<(u8, u8, u8)>,
454    /// Current focused application ID (for auto-switch rule creation)
455    pub current_focus: Option<String>,
456    /// Focus tracking is active
457    pub focus_tracking_active: bool,
458    /// Auto-switch rules view (open when configuring auto-profile switching)
459    pub auto_switch_view: Option<AutoSwitchRulesView>,
460    /// Hotkey bindings view (open when configuring hotkeys)
461    pub hotkey_view: Option<HotkeyBindingsView>,
462    /// Analog calibration view (open when configuring analog stick)
463    pub analog_calibration_view: Option<AnalogCalibrationView>,
464    /// Global macro timing and jitter settings
465    pub macro_settings: MacroSettings,
466    /// Current UI theme (Adaptive COSMIC)
467    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    // Navigation
534    SwitchTab(Tab),
535    ThemeChanged(iced::Theme),
536
537    // Device Management
538    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    // Macro Recording
547    UpdateMacroName(String),
548    StartRecording,
549    StopRecording,
550    RecordingStarted(Result<String, String>),
551    RecordingStopped(Result<MacroEntry, String>),
552
553    // Macro Management
554    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    // Profile Management
568    UpdateProfileName(String),
569    SaveProfile,
570    ProfileSaved(Result<(String, usize), String>),
571    LoadProfile,
572    ProfileLoaded(Result<(String, usize), String>),
573
574    // Device Profile Management
575    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    // Remap Profile Management
584    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    // Status
594    CheckDaemonConnection,
595    DaemonStatusChanged(bool),
596
597    // UI
598    TickAnimations,
599    ShowNotification(String, bool), // (message, is_error)
600
601    // Mouse Event Recording
602    RecordMouseEvent {
603        event_type: String,
604        button: Option<u16>,
605        x: i32,
606        y: i32,
607        delta: i32,
608    },
609
610    // Keypad Remapping
611    /// Show keypad remapping view for a device
612    ShowKeypadView(String),
613    /// Select a keypad button for remapping
614    SelectKeypadButton(String),
615    /// Load device capabilities for keypad view
616    DeviceCapabilitiesLoaded(String, Result<DeviceCapabilities, String>),
617
618    // Layer Management
619    /// Layer state changed (device_id, layer_id)
620    LayerStateChanged(String, usize),
621    /// Request layer configuration for a device
622    LayerConfigRequested(String),
623    /// Request activation of a layer (device_id, layer_id, mode)
624    LayerActivateRequested(String, usize, LayerMode),
625    /// Layer configuration updated (device_id, config)
626    LayerConfigUpdated(String, LayerConfigInfo),
627    /// Open layer config dialog for editing
628    OpenLayerConfigDialog(String, usize),
629    /// Update layer name in dialog
630    LayerConfigNameChanged(String),
631    /// Update layer mode in dialog
632    LayerConfigModeChanged(LayerMode),
633    /// Save layer config from dialog
634    SaveLayerConfig,
635    /// Cancel layer config dialog
636    CancelLayerConfig,
637    /// Periodic refresh of layer states
638    RefreshLayers,
639    /// Layer list loaded from daemon (device_id, layers)
640    LayerListLoaded(String, Vec<LayerConfigInfo>),
641
642    // D-pad Mode Management
643    /// Request D-pad mode for a device
644    AnalogDpadModeRequested(String),
645    /// D-pad mode loaded (device_id, mode)
646    AnalogDpadModeLoaded(String, String),
647    /// Set D-pad mode (device_id, mode)
648    SetAnalogDpadMode(String, String),
649    /// D-pad mode set result
650    AnalogDpadModeSet(Result<(), String>),
651
652    // Per-Axis Deadzone Management
653    /// Request per-axis deadzone for a device
654    AnalogDeadzoneXYRequested(String),
655    /// Per-axis deadzone loaded (device_id, (x_pct, y_pct))
656    AnalogDeadzoneXYLoaded(String, (u8, u8)),
657    /// Set per-axis deadzone (device_id, x_pct, y_pct)
658    SetAnalogDeadzoneXY(String, u8, u8),
659    /// Per-axis deadzone set result
660    AnalogDeadzoneXYSet(Result<(), String>),
661    /// Request per-axis outer deadzone for a device
662    AnalogOuterDeadzoneXYRequested(String),
663    /// Per-axis outer deadzone loaded (device_id, (x_pct, y_pct))
664    AnalogOuterDeadzoneXYLoaded(String, (u8, u8)),
665    /// Set per-axis outer deadzone (device_id, x_pct, y_pct)
666    SetAnalogOuterDeadzoneXY(String, u8, u8),
667    /// Per-axis outer deadzone set result
668    AnalogOuterDeadzoneXYSet(Result<(), String>),
669
670    // LED Configuration Management
671    /// Open LED configuration dialog for device
672    OpenLedConfig(String),
673    /// Close LED configuration dialog
674    CloseLedConfig,
675    /// Select LED zone for color editing
676    SelectLedZone(LedZone),
677    /// Set LED color (device_id, zone, red, green, blue)
678    SetLedColor(String, LedZone, u8, u8, u8),
679    /// LED color set result
680    LedColorSet(Result<(), String>),
681    /// Set LED brightness (device_id, zone_opt, brightness)
682    SetLedBrightness(String, Option<LedZone>, u8),
683    /// LED brightness set result
684    LedBrightnessSet(Result<(), String>),
685    /// Set LED pattern (device_id, pattern)
686    SetLedPattern(String, LedPattern),
687    /// LED pattern set result
688    LedPatternSet(Result<(), String>),
689    /// Request LED state refresh for device
690    RefreshLedState(String),
691    /// LED state loaded (device_id, colors)
692    LedStateLoaded(String, Result<HashMap<LedZone, (u8, u8, u8)>, String>),
693    /// RGB slider changed (red, green, blue)
694    LedSliderChanged(u8, u8, u8),
695
696    // Focus Tracking
697    /// Start focus tracking after daemon connection confirmed
698    StartFocusTracking,
699    /// Focus tracking started successfully
700    FocusTrackingStarted(Result<bool, String>),
701    /// Focus change event received from tracker
702    FocusChanged(String, Option<String>), // (app_id, window_title)
703
704    // Auto-Switch Rules Management
705    /// Open auto-switch rules view for a device
706    ShowAutoSwitchRules(String),
707    /// Close auto-switch rules view
708    CloseAutoSwitchRules,
709    /// Load auto-switch rules for a device
710    LoadAutoSwitchRules(String),
711    /// Auto-switch rules loaded (device_id, rules)
712    AutoSwitchRulesLoaded(Result<Vec<AutoSwitchRule>, String>),
713    /// Start editing a rule (index in list)
714    EditAutoSwitchRule(usize),
715    /// Update new rule app_id input
716    AutoSwitchAppIdChanged(String),
717    /// Update new rule profile_name input
718    AutoSwitchProfileNameChanged(String),
719    /// Update new rule layer_id input
720    AutoSwitchLayerIdChanged(String),
721    /// Use current focused app as app_id
722    AutoSwitchUseCurrentApp,
723    /// Save the current rule (add or update)
724    SaveAutoSwitchRule,
725    /// Delete a rule
726    DeleteAutoSwitchRule(usize),
727
728    // Hotkey Bindings Management
729    /// Open hotkey bindings view for a device
730    ShowHotkeyBindings(String),
731    /// Close hotkey bindings view
732    CloseHotkeyBindings,
733    /// Load hotkey bindings for a device
734    LoadHotkeyBindings(String),
735    /// Hotkey bindings loaded result
736    HotkeyBindingsLoaded(Result<Vec<HotkeyBinding>, String>),
737    /// Start editing a binding (index in list)
738    EditHotkeyBinding(usize),
739    /// Toggle modifier checkbox (modifier_name)
740    ToggleHotkeyModifier(String),
741    /// Update new binding key input
742    HotkeyKeyChanged(String),
743    /// Update new binding profile_name input
744    HotkeyProfileNameChanged(String),
745    /// Update new binding layer_id input
746    HotkeyLayerIdChanged(String),
747    /// Save the current binding (add or update)
748    SaveHotkeyBinding,
749    /// Delete a binding
750    DeleteHotkeyBinding(usize),
751    /// Hotkey bindings updated after delete
752    HotkeyBindingsUpdated(Vec<HotkeyBinding>),
753
754    // Analog Calibration Management
755    /// Open analog calibration view for a device and layer
756    OpenAnalogCalibration {
757        device_id: String,
758        layer_id: usize,
759    },
760    /// Analog calibration field changed
761    AnalogDeadzoneChanged(f32),
762    AnalogDeadzoneShapeChanged(DeadzoneShape),
763    AnalogSensitivityChanged(f32),
764    AnalogSensitivityCurveChanged(SensitivityCurve),
765    AnalogRangeMinChanged(i32),
766    AnalogRangeMaxChanged(i32),
767    AnalogInvertXToggled(bool),
768    AnalogInvertYToggled(bool),
769    /// Analog mode changed
770    AnalogModeChanged(AnalogMode),
771    /// Camera output mode changed
772    CameraModeChanged(CameraOutputMode),
773    /// Apply calibration changes
774    ApplyAnalogCalibration,
775    /// Analog calibration loaded
776    AnalogCalibrationLoaded(Result<aethermap_common::AnalogCalibrationConfig, String>),
777    /// Analog calibration applied
778    AnalogCalibrationApplied(Result<(), String>),
779    /// Close analog calibration view
780    CloseAnalogCalibration,
781    /// Analog input updated (streaming from daemon)
782    AnalogInputUpdated(f32, f32), // (x, y)
783}
784
785// Reserved for future use
786#[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                // Load analog settings for the selected device if it has analog stick
828                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                    // Start focus tracking after successful daemon connection
859                    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                // Spawn async task to initialize and start focus tracking
867                // We create a simple check for portal availability
868                Command::perform(
869                    async move {
870                        // Check if WAYLAND_DISPLAY is set (basic portal check)
871                        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                // Update current focus for auto-switch rule creation UI
898                self.current_focus = Some(app_id.clone());
899                // Send focus change to daemon for auto-profile switching
900                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, // Silent success
908                        Err(e) => Message::ProfileError(format!("Focus change failed: {}", e)),
909                    },
910                )
911            }
912
913            // Auto-Switch Rules Management
914            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                // Load rules from daemon
924                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                                // Convert common::AutoSwitchRule to gui::AutoSwitchRule
943                                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                    // Update local state immediately
1035                    self.auto_switch_view = Some(view);
1036
1037                    // Sync to daemon
1038                    Command::perform(
1039                        async move {
1040                            // Convert GUI AutoSwitchRule to common AutoSwitchRule
1041                            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                        // Update local state immediately
1076                        self.auto_switch_view.as_mut().map(|v| v.rules = rules.clone());
1077
1078                        // Sync to daemon
1079                        return Command::perform(
1080                            async move {
1081                                // Convert GUI AutoSwitchRule to common AutoSwitchRule
1082                                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            // Hotkey Bindings Management
1111            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                // Load bindings from daemon
1122                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                                // Convert common::HotkeyBinding to gui::HotkeyBinding
1141                                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                    // Update local state immediately
1224                    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                        // Update local state immediately
1275                        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            // Analog Calibration Management
1318            Message::OpenAnalogCalibration { device_id, layer_id } => {
1319                // Create the view with loading state
1320                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                // Load calibration from daemon
1339                let device_id_clone = device_id.clone();
1340                let socket_path = self.socket_path.clone();
1341
1342                // Subscribe to analog input updates
1343                let device_id_subscribe = device_id.clone();
1344                let socket_path_subscribe = self.socket_path.clone();
1345
1346                Command::batch(vec![
1347                    // Subscribe to analog input updates
1348                    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                    // Load calibration data
1359                    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                    // Convert common config to local CalibrationConfig
1371                    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                    // Update selections from loaded calibration
1385                    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                    // Clear cache so deadzone redraws with new size
1412                    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                    // Clear cache so deadzone redraws with new shape
1421                    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                // Unsubscribe from analog input updates
1525                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                // Unsubscribe is fire-and-forget - we don't need to wait for result
1533                // Spawn a background task to handle it
1534                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                // Update analog calibration view stick position with throttling
1548                // Throttle to ~30 FPS (33ms between updates) to prevent overwhelming the GUI
1549                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() // Triggers redraw
1555                    } else {
1556                        Command::none() // Skip redraw, no state change
1557                    }
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, // Silent success
1633                        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, &macro_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                // Auto-dismiss old notifications
1816                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                // Refresh active remaps after activation
1980                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                // Mouse events are captured by daemon during recording via device grab
2033                // This handler is for GUI-side mouse event logging
2034                if self.recording {
2035                    // Log the mouse event for debugging/confirmation
2036                    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                    // Update status to show mouse event was captured
2044                    self.status = event_desc;
2045                }
2046                Command::none()
2047            }
2048            Message::ShowKeypadView(device_path) => {
2049                // Empty string means back button was pressed - clear keypad view
2050                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                // Store the device path for keypad view
2058                self.keypad_view_device = Some(device_path.clone());
2059                // Query device capabilities and load keypad layout
2060                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                // Load current remappings and update button.current_remap
2077                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                // Switch to Devices tab to show keypad view
2086                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                            // Store layers and trigger UI refresh
2113                            // We'll emit LayerStateChanged for the active layer
2114                            if let Some(active_layer) = layers.first() {
2115                                Message::LayerStateChanged(device_id, active_layer.layer_id)
2116                            } else {
2117                                Message::TickAnimations // No-op refresh
2118                            }
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                            // Refresh layer list after config update
2152                            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                // Get current layer config if available
2160                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), // Default blue - TODO: allow GUI configuration
2195                        led_zone: None, // Default zone - TODO: allow GUI configuration
2196                    };
2197                    // Return LayerConfigUpdated message to handle the async save
2198                    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                // Periodic refresh of layer states for all devices
2212                let mut commands = Vec::new();
2213
2214                // Request layer configuration refresh for devices that have profiles loaded
2215                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                                // Store layers and update active layer
2226                                Message::LayerListLoaded(device_id, layers)
2227                            }
2228                            Err(_) => Message::TickAnimations, // Silent fail on refresh
2229                        }
2230                    ));
2231                }
2232
2233                // Also refresh active layer states
2234                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 // Silent fail
2271                        }
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                        // Success - D-pad mode updated
2300                        Command::none()
2301                    }
2302                    Err(e) => {
2303                        eprintln!("Failed to set D-pad mode: {}", e);
2304                        // Could show a toast notification here
2305                        Command::none()
2306                    }
2307                }
2308            }
2309
2310            // Per-Axis Deadzone handlers
2311            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 // Silent fail
2324                        }
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                        // Success - per-axis deadzone updated
2352                        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            // Per-Axis Outer Deadzone handlers
2363            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 // Silent fail
2376                        }
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                        // Success - per-axis outer deadzone updated
2404                        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            // LED Configuration handlers
2415            Message::OpenLedConfig(device_id) => {
2416                self.led_config_device = Some(device_id.clone());
2417                self.selected_led_zone = Some(LedZone::Logo); // Default to Logo zone
2418                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                        // Initialize LED state for device if not exists
2458                        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                        // Silent fail - LED may not be supported
2465                        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                        // Success - color updated
2489                        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                        // Success - brightness updated
2517                        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                        // Success - pattern updated
2545                        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 a device and zone are selected, apply the color immediately
2558                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        // Show layer config dialog overlay if active
2594        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            // Show LED config dialog overlay if active
2607            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            // Show analog calibration dialog overlay if active
2619            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        // Periodic layer state refresh (every 2 seconds)
2638        let layer_refresh = iced::time::every(Duration::from_secs(2))
2639            .map(|_| Message::RefreshLayers);
2640
2641        // Subscribe to mouse events only when recording
2642        // Note: In iced 0.12, mouse events are handled via the runtime event stream
2643        // The actual mouse event capture for macros happens at the daemon level via evdev
2644        // This subscription tracks recording state for UI updates only
2645        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), // BTN_LEFT in evdev
2651                        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), // BTN_RIGHT in evdev
2660                        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), // BTN_MIDDLE in evdev
2669                        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                    // Note: Cursor movement is tracked but may be sampled at reduced rate
2698                    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        // Only enable mouse event subscription during recording
2711        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        // Show auto-switch rules view when open
2878        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        // Show hotkey bindings view when open
2898        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        // Show keypad view when capabilities are loaded
2918        if self.device_capabilities.is_some() && !self.keypad_layout.is_empty() {
2919            // Build keypad view content
2920            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            // Add profile quick toggles at the bottom if device path is available
2936            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        // Use device_type from capability detection (not name heuristics)
2991        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        // Get device_id for layer operations
3022        let device_id = format!("{:04x}:{:04x}", device.vendor_id, device.product_id);
3023
3024        // Add "Configure Keypad" button for keypad devices
3025        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        // Add "Configure LEDs" button for LED-capable devices (keypad/gamepad)
3036        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        // Add "Auto-Switch Rules" button for all devices
3047        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        // Add "Hotkey Bindings" button for all devices
3054        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        // Add "Analog Calibration" button for devices with analog support
3061        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            // Profile quick toggles - horizontal row of profile buttons
3113            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        // Build card content with optional D-pad mode selector
3137        let mut card_elements: Vec<Element<'_, Message>> = vec![card_content.into()];
3138
3139        // Add D-pad mode selector for devices with analog sticks
3140        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            // Add per-axis deadzone controls
3176            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            // Inner deadzone controls
3182            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            // Outer deadzone controls
3209            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        // Add keypad button if applicable
3238        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        // Add LED configuration button if applicable
3248        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        // Add auto-switch rules button
3258        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        // Add hotkey bindings button
3268        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        // Add analog calibration button
3278        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    /// View a single macro action with icon formatting
3448    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(&macro_entry.name);
3473            let name_prefix = if is_recent { "★ " } else { "⚡ " };
3474
3475            // Create action preview list (show first 3 actions)
3476            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                        // Show action previews
3499                        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    /// Render profile selection dropdown for a device
3594    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                // Add deactivate button if profile is active
3626                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    /// Render remap profile switcher for a device
3656    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                // Add deactivate button if profile is active
3688                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                // Add refresh button
3698                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    /// Render active remaps display for a device
3732    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    /// Format an action with an appropriate icon for display
3765    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                // Convert axis code to human-readable name
3778                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    /// View for auto-switch rules configuration
3793    ///
3794    /// Displays the current focus, list of rules, and controls for adding/editing rules.
3795    fn view_auto_switch_rules(&self) -> Element<'_, Message> {
3796        let view = self.auto_switch_view.as_ref().unwrap();
3797
3798        // Current focus display
3799        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        // Rules list header
3815        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        // Rules list
3831        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        // Edit form (shown when editing or adding)
3872        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    /// View for hotkey bindings configuration
3952    ///
3953    /// Displays list of hotkey bindings and controls for adding/editing bindings.
3954    fn view_hotkey_bindings(&self) -> Element<'_, Message> {
3955        let view = self.hotkey_view.as_ref().unwrap();
3956
3957        // Bindings list header
3958        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        // Bindings list
3974        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        // Edit form (shown when editing or adding)
4016        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    /// Helper function to create a modifier checkbox
4099    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    /// Format a remap target key name for display
4112    ///
4113    /// Converts internal key names like "KEY_A", "BTN_LEFT", etc.
4114    /// into user-friendly display names like "A", "LMB", etc.
4115    fn format_remap_target(target: &str) -> String {
4116        // Handle common key prefixes
4117        if let Some(rest) = target.strip_prefix("KEY_") {
4118            // Convert KEY_A -> A, KEY_LEFTCTRL -> LCtrl, etc.
4119            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                // Single character keys
4144                s if s.len() == 1 => s.to_uppercase(),
4145                // F-keys
4146                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            // Mouse buttons
4151            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            // Relative axes (wheel)
4163            match rest {
4164                "WHEEL" => "Wheel".to_string(),
4165                "HWHEEL" => "HWheel".to_string(),
4166                _ => rest.to_string(),
4167            }
4168        } else {
4169            // Return as-is for unknown formats (truncate if too long)
4170            if target.len() > 6 {
4171                format!("{}...", &target[..6])
4172            } else {
4173                target.to_string()
4174            }
4175        }
4176    }
4177
4178    /// View for Azeron keypad remapping interface
4179    ///
4180    /// Displays a visual representation of the Azeron Cyborg keypad with
4181    /// clickable buttons for remapping configuration.
4182    /// Shows the current mapping for each button in a clean, readable format.
4183    ///
4184    /// Format: Unmapped buttons show the button label (1, 2, Q, W, etc.).
4185    /// Mapped buttons show the original label small at top, mapped key below.
4186    fn view_azeron_keypad(&self) -> Element<'_, Message> {
4187        let layout = azeron_keypad_layout();
4188
4189        // Create grid of buttons organized by row
4190        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            // Button styling based on remap state and selection
4204            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            // Format the button content to show mapping clearly
4213            // If remapped, show the mapped key name prominently
4214            let button_content: Element<'_, Message> = if let Some(ref target) = remap {
4215                // Parse the target to get a readable key name
4216                let display_name = Self::format_remap_target(target);
4217                // Create a column with original label small on top, mapped key below
4218                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                // Unmapped button - show the label centered
4231                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        // Add hat switch indicator in center
4251        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        // Insert hat switch at center position (row 5, col 2)
4262        if rows.get_mut(5).is_some() {
4263            rows[5].push(hat_switch);
4264        }
4265
4266        // Build the keypad layout
4267        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(&notif.message).size(12)
4300            } else {
4301                text(&notif.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    /// View layer indicator for a device
4324    ///
4325    /// Displays the active layer name/ID for the given device.
4326    /// Shows "Layer N: {name}" format with Primary style for visibility.
4327    fn layer_indicator(&self, device_id: &str) -> Element<'_, Message> {
4328        if let Some(&layer_id) = self.active_layers.get(device_id) {
4329            // Get layer name from configs if available
4330            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            // No active layer - show default base layer
4345            container(
4346                text("Layer 0: Base").size(12)
4347            )
4348            .padding([4, 8])
4349            .style(container_styles::card)
4350            .into()
4351        }
4352    }
4353
4354    /// View profile quick toggle buttons for a device
4355    ///
4356    /// Shows horizontal row of toggle buttons for each available remap profile.
4357    /// Highlights the active profile with Primary style.
4358    /// Similar to the official Azeron software's profile toggle interface.
4359    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(); // Empty row when no profiles
4366            }
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            // If there's an active profile, add a deactivate button at the end
4389            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() // Empty row when profiles not loaded
4405        }
4406    }
4407
4408    /// View layer activation buttons for a device
4409    ///
4410    /// Shows buttons for each toggle layer available for the device.
4411    /// Highlights active toggle layers with Secondary style.
4412    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            // Filter for toggle layers only
4417            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    /// Deadzone quick-select buttons
4459    ///
4460    /// Provides buttons for common deadzone percentages.
4461    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    /// Outer deadzone quick-select buttons
4487    ///
4488    /// Provides buttons for common outer deadzone percentages.
4489    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    /// View layer settings for a device
4515    ///
4516    /// Displays a table/list of all layers for the device with edit buttons.
4517    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            // Add "Add Layer" button if less than 8 layers
4556            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    /// View layer configuration dialog
4595    ///
4596    /// Modal dialog for editing layer name and mode.
4597    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            // Overlay dialog on semi-transparent background
4647            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    /// Get current color for a zone, with default fallback
4665    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        // Default to white if not set
4674        (255, 255, 255)
4675    }
4676
4677    /// View LED RGB sliders for color adjustment
4678    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    /// Get color style for LED preview container
4727    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    /// View LED configuration dialog
4754    ///
4755    /// Displays modal dialog for LED configuration with zone selection,
4756    /// RGB sliders, brightness control, and pattern selection.
4757    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            // Zone buttons
4765            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            // Color preview
4788            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            // Pattern buttons
4808            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                    // Header
4837                    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                    // Device ID
4851                    text(device_id).size(11).width(Length::Fill),
4852
4853                    // Zone selection
4854                    text("Zone:").size(13),
4855                    row(zone_buttons).spacing(8),
4856
4857                    horizontal_rule(1),
4858
4859                    // Color preview
4860                    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                    // RGB sliders
4873                    self.view_led_rgb_sliders(),
4874
4875                    horizontal_rule(1),
4876
4877                    // Brightness control
4878                    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                    // Pattern selection
4887                    text("Pattern:").size(13),
4888                    row(pattern_buttons).spacing(8),
4889
4890                    horizontal_rule(1),
4891
4892                    // Close button
4893                    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            // Modal overlay
4909            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    /// View analog calibration dialog
4925    ///
4926    /// Displays modal dialog for analog stick calibration with deadzone,
4927    /// sensitivity, range, and inversion controls.
4928    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            // Modal overlay
4936            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        // Device and layer info
4970        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        // Visualizer section
4976        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        // Mode section
5003        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        // Add camera sub-mode selector if camera mode is selected
5018        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        // Deadzone section
5034        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        // Sensitivity section
5056        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        // Range section
5091        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        // Inversion section
5110        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        // Apply and Close buttons
5121        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        // Error display
5134        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        // Clone resets last_visualizer_update to Instant::now()
5281        assert!(cloned.last_visualizer_update.elapsed() < Duration::from_secs(1));
5282    }
5283
5284    #[test]
5285    fn test_throttling_threshold() {
5286        // Verify the 30 FPS throttling threshold (33ms)
5287        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        // Immediately after update, elapsed time should be small
5306        assert!(view.last_visualizer_update.elapsed() < Duration::from_millis(33));
5307
5308        // After 40ms, should definitely exceed the threshold
5309        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        // Verify that Arc<Cache> can be cloned and shared
5316        let cache = Arc::new(iced::widget::canvas::Cache::default());
5317        let cache_clone = Arc::clone(&cache);
5318
5319        // Both Arcs point to the same Cache
5320        assert!(Arc::ptr_eq(&cache, &cache_clone));
5321    }
5322
5323    #[test]
5324    fn test_analog_mode_selection_states() {
5325        // Test that all analog modes can be selected
5326        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        // Test that all camera output modes can be selected
5347        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}