Skip to main content

tuiserial_ui/
mouse.rs

1//! Mouse event handling for UI interactions
2//!
3//! This module provides mouse event handling functionality for menu bar,
4//! tabs, buttons, and other interactive UI elements.
5
6use ratatui::layout::Rect;
7use tuiserial_core::{AppState, FocusedField, Language, MenuState};
8
9use crate::areas::{get_clicked_field, get_clicked_menu, is_inside, is_shortcuts_hint_clicked};
10
11/// Mouse action result
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum MouseAction {
14    /// No action taken
15    None,
16    /// Field was focused
17    FocusField(FocusedField),
18    /// Menu was opened
19    OpenMenu(usize),
20    /// Menu item was selected
21    SelectMenuItem(usize, usize), // menu_idx, item_idx
22    /// Tab was switched
23    SwitchTab(usize),
24    /// Connect/disconnect button clicked
25    ToggleConnection,
26    /// Clear log button clicked
27    ClearLog,
28    /// Refresh ports button clicked
29    RefreshPorts,
30    /// Send button clicked
31    SendData,
32    /// Show shortcuts help
33    ShowShortcutsHelp,
34    /// Close shortcuts help
35    CloseShortcutsHelp,
36    /// Close menu/dropdown
37    CloseMenu,
38}
39
40/// Handle mouse click events
41pub fn handle_mouse_click(
42    app: &AppState,
43    x: u16,
44    y: u16,
45    menu_dropdown_area: Option<Rect>,
46) -> MouseAction {
47    // If shortcuts help is showing, check if clicked outside to close
48    if app.show_shortcuts_help {
49        // Check if clicked on shortcuts hint area to toggle
50        if is_shortcuts_hint_clicked(x, y) {
51            return MouseAction::CloseShortcutsHelp;
52        }
53        // Click anywhere else closes the help
54        return MouseAction::CloseShortcutsHelp;
55    }
56
57    // Check menu dropdown first (if open)
58    if let MenuState::Dropdown(menu_idx, _) = app.menu_state {
59        if let Some(dropdown_area) = menu_dropdown_area {
60            if is_inside(dropdown_area, x, y) {
61                // Clicked inside dropdown - determine which item
62                let item_idx = calculate_dropdown_item(dropdown_area, y);
63                return MouseAction::SelectMenuItem(menu_idx, item_idx);
64            } else {
65                // Clicked outside dropdown - close it
66                return MouseAction::CloseMenu;
67            }
68        }
69    }
70
71    // Check menu bar
72    if let Some(menu_idx) = get_clicked_menu(x, y) {
73        return MouseAction::OpenMenu(menu_idx);
74    }
75
76    // Check shortcuts hint bar
77    if is_shortcuts_hint_clicked(x, y) {
78        return MouseAction::ShowShortcutsHelp;
79    }
80
81    // Check tab bar (if visible)
82    // Note: This would be used in multi-session mode
83    // For now, returning None as tabs are not in current single-session UI
84
85    // Check configuration fields
86    if let Some(field) = get_clicked_field(x, y) {
87        return MouseAction::FocusField(field);
88    }
89
90    // Check for button clicks in status panel
91    // This is approximate and would need actual button areas
92    // For now, we return None
93
94    MouseAction::None
95}
96
97/// Handle mouse hover/move events
98pub fn handle_mouse_hover(_app: &AppState, x: u16, y: u16) -> Option<String> {
99    // Return tooltip text based on hover position
100
101    // Check menu bar
102    if let Some(menu_idx) = get_clicked_menu(x, y) {
103        let tooltip = match menu_idx {
104            0 => "File operations",
105            1 => "Session management",
106            2 => "View layouts",
107            3 => "Application settings",
108            4 => "Help and information",
109            _ => "",
110        };
111        return Some(tooltip.to_string());
112    }
113
114    // Check shortcuts hint
115    if is_shortcuts_hint_clicked(x, y) {
116        return Some("Click to show keyboard shortcuts".to_string());
117    }
118
119    // Check configuration fields
120    if let Some(field) = get_clicked_field(x, y) {
121        let tooltip = match field {
122            FocusedField::Port => "Select serial port",
123            FocusedField::BaudRate => "Select baud rate",
124            FocusedField::DataBits => "Select data bits",
125            FocusedField::Parity => "Select parity",
126            FocusedField::StopBits => "Select stop bits",
127            FocusedField::FlowControl => "Select flow control",
128            FocusedField::LogArea => "Serial communication log",
129            FocusedField::TxInput => "Enter data to send",
130        };
131        return Some(tooltip.to_string());
132    }
133
134    None
135}
136
137/// Calculate which dropdown item was clicked based on Y coordinate
138fn calculate_dropdown_item(dropdown_area: Rect, y: u16) -> usize {
139    let _ = dropdown_area;
140    if y < dropdown_area.y || y >= dropdown_area.y + dropdown_area.height {
141        return 0;
142    }
143
144    let relative_y = y.saturating_sub(dropdown_area.y);
145
146    // Account for border (1 line at top)
147    if relative_y == 0 {
148        return 0;
149    }
150
151    // 1-based index: 0 = border/outside, 1+ = menu items
152    relative_y as usize
153}
154
155/// Get the area for a dropdown menu
156pub fn calculate_dropdown_area(
157    menu_bar_area: Rect,
158    menu_idx: usize,
159    item_count: usize,
160    lang: Language,
161) -> Rect {
162    // Calculate x position based on menu index using centralized calculation
163    let x_offset = tuiserial_core::menu_def::calculate_menu_x_offset(menu_idx, lang);
164
165    // Calculate dropdown dimensions
166    let max_width = 25u16; // Reasonable default width
167    let height = item_count as u16 + 2; // +2 for borders
168
169    Rect {
170        x: menu_bar_area.x + x_offset,
171        y: menu_bar_area.y + 1, // Below menu bar
172        width: max_width,
173        height,
174    }
175}
176
177/// Check if a click is on a button area (approximate)
178#[allow(dead_code)]
179pub fn is_button_area(area: Rect, x: u16, y: u16, _button_text: &str) -> bool {
180    if !is_inside(area, x, y) {
181        return false;
182    }
183
184    // This is a simplified check - in practice you'd need actual button positions
185    // For now, just check if inside the area
186    true
187}
188
189/// Handle mouse scroll events in log area
190pub fn handle_mouse_scroll(
191    _app: &AppState,
192    x: u16,
193    y: u16,
194    direction: ScrollDirection,
195) -> Option<ScrollAction> {
196    use crate::areas::get_ui_areas;
197
198    let areas = get_ui_areas();
199
200    // Check if scrolling in log area
201    if is_inside(areas.log_area, x, y) {
202        match direction {
203            ScrollDirection::Up => Some(ScrollAction::ScrollUp(3)),
204            ScrollDirection::Down => Some(ScrollAction::ScrollDown(3)),
205        }
206    } else {
207        None
208    }
209}
210
211/// Scroll direction
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum ScrollDirection {
214    Up,
215    Down,
216}
217
218/// Scroll action
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum ScrollAction {
221    ScrollUp(u16),
222    ScrollDown(u16),
223}
224
225/// Get visual feedback for hover state
226pub fn get_hover_style(is_hovered: bool) -> ratatui::style::Style {
227    use ratatui::style::{Color, Modifier, Style};
228
229    if is_hovered {
230        Style::default()
231            .fg(Color::Yellow)
232            .add_modifier(Modifier::BOLD)
233    } else {
234        Style::default()
235    }
236}
237
238/// Check if mouse is in a clickable area
239pub fn is_clickable_area(x: u16, y: u16) -> bool {
240    use crate::areas::get_ui_areas;
241
242    let areas = get_ui_areas();
243
244    // Check all interactive areas
245    is_inside(areas.menu_bar, x, y)
246        || is_inside(areas.port, x, y)
247        || is_inside(areas.baud_rate, x, y)
248        || is_inside(areas.data_bits, x, y)
249        || is_inside(areas.parity, x, y)
250        || is_inside(areas.stop_bits, x, y)
251        || is_inside(areas.flow_control, x, y)
252        || is_inside(areas.tx_area, x, y)
253        || is_inside(areas.shortcuts_hint, x, y)
254        || is_inside(areas.tab_bar, x, y)
255}
256
257/// Mouse cursor type for different areas
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum CursorType {
260    Default,
261    Pointer, // Clickable
262    Text,    // Text input
263    Help,    // Help/info
264}
265
266/// Get appropriate cursor type for position
267pub fn get_cursor_type(_app: &AppState, x: u16, y: u16) -> CursorType {
268    use crate::areas::get_ui_areas;
269
270    let areas = get_ui_areas();
271
272    if is_inside(areas.tx_area, x, y) {
273        CursorType::Text
274    } else if is_shortcuts_hint_clicked(x, y) {
275        CursorType::Help
276    } else if is_clickable_area(x, y) {
277        CursorType::Pointer
278    } else {
279        CursorType::Default
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_calculate_dropdown_item() {
289        let area = Rect {
290            x: 0,
291            y: 1,
292            width: 20,
293            height: 5,
294        };
295
296        assert_eq!(calculate_dropdown_item(area, 0), 0); // Above area
297        assert_eq!(calculate_dropdown_item(area, 1), 0); // Border
298        assert_eq!(calculate_dropdown_item(area, 2), 1); // First item
299        assert_eq!(calculate_dropdown_item(area, 3), 2); // Second item
300    }
301
302    #[test]
303    fn test_calculate_dropdown_area() {
304        let menu_bar = Rect {
305            x: 0,
306            y: 0,
307            width: 80,
308            height: 1,
309        };
310
311        let area = calculate_dropdown_area(menu_bar, 0, 4, Language::English);
312        assert_eq!(area.y, 1); // Below menu bar
313        assert_eq!(area.height, 6); // 4 items + 2 borders
314    }
315
316    #[test]
317    fn test_is_clickable_area() {
318        // This would need actual UI areas set up
319        // For now, just test that the function exists
320        let _result = is_clickable_area(0, 0);
321    }
322}