Skip to main content

minui/input/
mouse.rs

1//! Mouse input handling implementation.
2//!
3//! This module provides comprehensive mouse input functionality using crossterm
4//! for cross-platform terminal mouse input handling. It supports mouse movement,
5//! clicks, drags, and scroll events.
6
7use crate::{Event, MouseButton, Result};
8use crossterm::event::{
9    self, Event as CrosstermEvent, MouseButton as CrosstermMouseButton, MouseEvent, MouseEventKind,
10};
11use std::time::{Duration, Instant};
12
13/// Handles mouse input with configurable polling rates and event tracking.
14///
15/// `MouseHandler` provides a flexible interface for receiving mouse input in terminal
16/// applications. It supports mouse movement tracking, click detection, drag operations,
17/// and scroll wheel events.
18///
19/// # Key Features
20///
21/// - **Movement Tracking**: Track cursor position within the terminal
22/// - **Click Detection**: Handle left, right, and middle mouse button clicks
23/// - **Drag Operations**: Support for drag-and-drop interactions
24/// - **Scroll Events**: Mouse wheel scrolling support
25/// - **Configurable Polling**: Adjustable polling rates for different performance needs
26/// - **Cross-platform**: Works consistently across Windows, macOS, and Linux
27///
28/// # Examples
29///
30/// ## Basic Mouse Polling
31///
32/// ```rust
33/// use minui::input::MouseHandler;
34/// use minui::{Event, MouseButton};
35///
36/// let mouse = MouseHandler::new();
37///
38/// // Non-blocking check for mouse input
39/// if let Some(event) = mouse.poll()? {
40///     match event {
41///         Event::MouseClick { x, y, button } => {
42///             match button {
43///                 MouseButton::Left => println!("Left click at ({}, {})", x, y),
44///                 MouseButton::Right => println!("Right click at ({}, {})", x, y),
45///                 MouseButton::Middle => println!("Middle click at ({}, {})", x, y),
46///                 MouseButton::Other(code) => println!("Button {} click at ({}, {})", code, x, y),
47///             }
48///         },
49///         Event::MouseMove { x, y } => println!("Mouse moved to ({}, {})", x, y),
50///         Event::MouseScroll { delta } => {
51///             if delta > 0 {
52///                 println!("Scrolled up");
53///             } else {
54///                 println!("Scrolled down");
55///             }
56///         },
57///         _ => {}
58///     }
59/// }
60/// # Ok::<(), minui::Error>(())
61/// ```
62///
63/// ## Drag Detection
64///
65/// ```rust
66/// use minui::input::MouseHandler;
67/// use minui::{Event, MouseButton};
68///
69/// let mut mouse = MouseHandler::new();
70/// mouse.enable_drag_detection(true);
71///
72/// if let Some(event) = mouse.poll()? {
73///     match event {
74///         Event::MouseClick { x, y, button: MouseButton::Left } => {
75///             println!("Click at ({}, {})", x, y);
76///         },
77///         Event::MouseDrag { x, y, button } => {
78///             println!("Dragging with {:?} to ({}, {})", button, x, y);
79///         },
80///         _ => {}
81///     }
82/// }
83/// # Ok::<(), minui::Error>(())
84/// ```
85pub struct MouseHandler {
86    poll_rate: Duration,
87    track_movement: bool,
88    drag_detection: bool,
89    last_click_pos: Option<(u16, u16)>,
90    is_dragging: bool,
91    last_scroll_direction: Option<ScrollDirection>,
92    scroll_buffer_count: u8,
93    invert_scroll_vertical: bool,
94    invert_scroll_horizontal: bool,
95    click_tracker: ClickTracker,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
99enum ScrollDirection {
100    Vertical,
101    Horizontal,
102}
103
104/// Tracks click timing and position for double-click detection.
105///
106/// `ClickTracker` monitors mouse clicks and determines if a click is a double-click
107/// based on time interval and position proximity.
108///
109/// # Examples
110///
111/// ```rust
112/// use minui::input::ClickTracker;
113///
114/// let mut tracker = ClickTracker::new();
115///
116/// // Check if a click is a double-click
117/// if tracker.is_double_click(10, 5) {
118///     println!("Double-click detected at (10, 5)");
119/// }
120/// ```
121pub struct ClickTracker {
122    /// Timestamp of the last click
123    last_click: Instant,
124    /// Position of the last click
125    last_pos: (u16, u16),
126    /// Maximum time between clicks to be considered a double-click (default: 500ms)
127    double_click_threshold: Duration,
128    /// Maximum distance between clicks to be considered a double-click (default: 3 pixels)
129    double_click_distance: u16,
130}
131
132impl ClickTracker {
133    /// Creates a new click tracker with default thresholds.
134    ///
135    /// Defaults:
136    /// - 500ms double-click threshold
137    /// - 3 cell maximum distance
138    pub fn new() -> Self {
139        Self {
140            last_click: Instant::now() - Duration::from_secs(1), // Initialize in the past
141            last_pos: (0, 0),
142            double_click_threshold: Duration::from_millis(500),
143            double_click_distance: 3,
144        }
145    }
146
147    /// Sets the maximum time between clicks for double-click detection.
148    pub fn with_threshold(mut self, threshold: Duration) -> Self {
149        self.double_click_threshold = threshold;
150        self
151    }
152
153    /// Sets the maximum distance between clicks for double-click detection.
154    pub fn with_distance(mut self, distance: u16) -> Self {
155        self.double_click_distance = distance;
156        self
157    }
158
159    /// Checks if a click at the given position is a double-click.
160    ///
161    /// A double-click is detected if:
162    /// - The time since the last click is less than `double_click_threshold`
163    /// - The click position is within `double_click_distance` of the last click
164    ///
165    /// # Returns
166    ///
167    /// - `true` if this is a double-click
168    /// - `false` otherwise
169    ///
170    /// # Examples
171    ///
172    /// ```rust
173    /// use minui::input::ClickTracker;
174    ///
175    /// let mut tracker = ClickTracker::new();
176    ///
177    /// // First click
178    /// assert!(!tracker.is_double_click(10, 5)); // First click is never a double-click
179    ///
180    /// // Simulate a quick second click
181    /// // (in real code, you'd call this from the mouse event handler)
182    /// ```
183    pub fn is_double_click(&mut self, x: u16, y: u16) -> bool {
184        let now = Instant::now();
185        let time_diff = now.duration_since(self.last_click);
186        let pos_diff_x = if x > self.last_pos.0 {
187            x - self.last_pos.0
188        } else {
189            self.last_pos.0 - x
190        };
191        let pos_diff_y = if y > self.last_pos.1 {
192            y - self.last_pos.1
193        } else {
194            self.last_pos.1 - y
195        };
196
197        let is_double = time_diff < self.double_click_threshold
198            && pos_diff_x <= self.double_click_distance
199            && pos_diff_y <= self.double_click_distance;
200
201        self.last_click = now;
202        self.last_pos = (x, y);
203
204        is_double
205    }
206
207    /// Returns the position of the last click.
208    pub fn last_position(&self) -> (u16, u16) {
209        self.last_pos
210    }
211
212    /// Returns the time elapsed since the last click.
213    pub fn time_since_last_click(&self) -> Duration {
214        self.last_click.elapsed()
215    }
216}
217
218impl Default for ClickTracker {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224impl MouseHandler {
225    /// Creates a new mouse handler with default settings.
226    ///
227    /// The handler is initialized with:
228    /// - 1ms poll rate for responsive input
229    /// - Movement tracking enabled
230    /// - Drag detection disabled
231    ///
232    /// # Returns
233    ///
234    /// A new `MouseHandler` with default configuration.
235    ///
236    /// # Examples
237    ///
238    /// ```rust
239    /// use minui::input::MouseHandler;
240    ///
241    /// let mouse = MouseHandler::new();
242    /// # Ok::<(), minui::Error>(())
243    /// ```
244    pub fn new() -> Self {
245        Self {
246            poll_rate: Duration::from_millis(1),
247            track_movement: true,
248            drag_detection: false,
249            last_click_pos: None,
250            is_dragging: false,
251            last_scroll_direction: None,
252            scroll_buffer_count: 0,
253            invert_scroll_vertical: false,
254            invert_scroll_horizontal: false,
255            click_tracker: ClickTracker::new(),
256        }
257    }
258
259    /// Sets the polling rate for mouse input detection.
260    ///
261    /// The poll rate determines how frequently the handler checks for available input
262    /// when using the `poll()` method. Lower values provide more responsive input
263    /// at the cost of higher CPU usage.
264    ///
265    /// # Arguments
266    ///
267    /// * `milliseconds` - The polling interval in milliseconds
268    ///
269    /// # Examples
270    ///
271    /// ```rust
272    /// use minui::input::MouseHandler;
273    ///
274    /// let mut mouse = MouseHandler::new();
275    /// mouse.set_poll_rate(16); // 60 FPS
276    /// ```
277    pub fn set_poll_rate(&mut self, milliseconds: u64) {
278        self.poll_rate = Duration::from_millis(milliseconds);
279    }
280
281    /// Returns the current polling rate.
282    ///
283    /// # Returns
284    ///
285    /// The current poll rate as a `Duration`.
286    ///
287    /// # Examples
288    ///
289    /// ```rust
290    /// use minui::input::MouseHandler;
291    /// use std::time::Duration;
292    ///
293    /// let mouse = MouseHandler::new();
294    /// assert_eq!(mouse.poll_rate(), Duration::from_millis(1));
295    /// ```
296    pub fn poll_rate(&self) -> Duration {
297        self.poll_rate
298    }
299
300    /// Enables or disables mouse movement tracking.
301    ///
302    /// When enabled, the handler will generate `MouseMove` events whenever
303    /// the cursor position changes. When disabled, only clicks and scrolls
304    /// are tracked, which can reduce event volume.
305    ///
306    /// # Arguments
307    ///
308    /// * `enabled` - Whether to track mouse movement
309    ///
310    /// # Examples
311    ///
312    /// ```rust
313    /// use minui::input::MouseHandler;
314    ///
315    /// let mut mouse = MouseHandler::new();
316    /// mouse.set_movement_tracking(false); // Only track clicks and scrolls
317    /// ```
318    pub fn set_movement_tracking(&mut self, enabled: bool) {
319        self.track_movement = enabled;
320    }
321
322    /// Returns whether movement tracking is enabled.
323    ///
324    /// # Returns
325    ///
326    /// `true` if movement tracking is enabled, `false` otherwise.
327    pub fn is_movement_tracking_enabled(&self) -> bool {
328        self.track_movement
329    }
330
331    /// Enables or disables drag detection.
332    ///
333    /// When enabled, the handler tracks when a mouse button is pressed and
334    /// the mouse is subsequently moved, allowing for drag-and-drop operations.
335    ///
336    /// # Arguments
337    ///
338    /// * `enabled` - Whether to detect drag operations
339    ///
340    /// # Examples
341    ///
342    /// ```rust
343    /// use minui::input::MouseHandler;
344    ///
345    /// let mut mouse = MouseHandler::new();
346    /// mouse.enable_drag_detection(true);
347    /// ```
348    pub fn enable_drag_detection(&mut self, enabled: bool) {
349        self.drag_detection = enabled;
350        if !enabled {
351            self.is_dragging = false;
352            self.last_click_pos = None;
353        }
354    }
355
356    /// Returns whether drag detection is enabled.
357    ///
358    /// # Returns
359    ///
360    /// `true` if drag detection is enabled, `false` otherwise.
361    pub fn is_drag_detection_enabled(&self) -> bool {
362        self.drag_detection
363    }
364
365    /// Returns whether a drag operation is currently in progress.
366    ///
367    /// This is only meaningful when drag detection is enabled.
368    ///
369    /// # Returns
370    ///
371    /// `true` if currently dragging, `false` otherwise.
372    pub fn is_dragging(&self) -> bool {
373        self.is_dragging
374    }
375
376    /// Returns the position where the current drag started (if any).
377    ///
378    /// # Returns
379    ///
380    /// - `Some((x, y))` - The starting position of the current drag
381    /// - `None` - No drag is in progress
382    pub fn drag_start_position(&self) -> Option<(u16, u16)> {
383        if self.is_dragging {
384            self.last_click_pos
385        } else {
386            None
387        }
388    }
389
390    /// Returns a reference to the click tracker for double-click detection.
391    ///
392    /// This allows you to manually check if a click is a double-click or customize
393    /// the double-click thresholds.
394    ///
395    /// # Examples
396    ///
397    /// ```rust
398    /// use minui::input::MouseHandler;
399    ///
400    /// let mut mouse = MouseHandler::new();
401    ///
402    /// // Check for double-click
403    /// if mouse.click_tracker().is_double_click(10, 5) {
404    ///     println!("Double-click detected!");
405    /// }
406    /// ```
407    pub fn click_tracker(&self) -> &ClickTracker {
408        &self.click_tracker
409    }
410
411    /// Returns a mutable reference to the click tracker for configuration.
412    ///
413    /// This allows you to customize double-click thresholds.
414    pub fn click_tracker_mut(&mut self) -> &mut ClickTracker {
415        &mut self.click_tracker
416    }
417
418    /// Sets whether to invert vertical scrolling (natural scrolling).
419    ///
420    /// When enabled, positive deltas scroll down and negative deltas scroll up,
421    /// matching the "natural" scrolling behavior common on trackpads.
422    ///
423    /// # Arguments
424    ///
425    /// * `invert` - `true` to enable natural scrolling, `false` for traditional
426    ///
427    /// # Examples
428    ///
429    /// ```rust
430    /// use minui::input::MouseHandler;
431    ///
432    /// let mut mouse = MouseHandler::new();
433    /// mouse.set_invert_scroll_vertical(true); // Enable natural scrolling
434    /// ```
435    pub fn set_invert_scroll_vertical(&mut self, invert: bool) {
436        self.invert_scroll_vertical = invert;
437    }
438
439    /// Returns whether vertical scrolling is inverted.
440    pub fn is_scroll_vertical_inverted(&self) -> bool {
441        self.invert_scroll_vertical
442    }
443
444    /// Sets whether to invert horizontal scrolling.
445    ///
446    /// When enabled, positive deltas scroll left and negative deltas scroll right.
447    ///
448    /// # Arguments
449    ///
450    /// * `invert` - `true` to invert horizontal scrolling, `false` for normal
451    ///
452    /// # Examples
453    ///
454    /// ```rust
455    /// use minui::input::MouseHandler;
456    ///
457    /// let mut mouse = MouseHandler::new();
458    /// mouse.set_invert_scroll_horizontal(true);
459    /// ```
460    pub fn set_invert_scroll_horizontal(&mut self, invert: bool) {
461        self.invert_scroll_horizontal = invert;
462    }
463
464    /// Returns whether horizontal scrolling is inverted.
465    pub fn is_scroll_horizontal_inverted(&self) -> bool {
466        self.invert_scroll_horizontal
467    }
468
469    /// Polls for mouse input without blocking.
470    ///
471    /// This method immediately checks if mouse input is available and returns
472    /// the result. It never blocks execution, making it perfect for game loops
473    /// and real-time applications.
474    ///
475    /// # Returns
476    ///
477    /// - `Ok(Some(Event))` - Mouse input was available and has been converted to an event
478    /// - `Ok(None)` - No mouse input is currently available
479    /// - `Err(...)` - An error occurred while checking for input
480    ///
481    /// # Examples
482    ///
483    /// ```rust
484    /// use minui::input::MouseHandler;
485    /// use minui::{Event, MouseButton};
486    ///
487    /// let mouse = MouseHandler::new();
488    ///
489    /// match mouse.poll()? {
490    ///     Some(Event::MouseClick { x, y, button }) => {
491    ///         println!("Click at ({}, {}) with {:?}", x, y, button);
492    ///     },
493    ///     Some(Event::MouseMove { x, y }) => {
494    ///         println!("Mouse at ({}, {})", x, y);
495    ///     },
496    ///     Some(event) => println!("Other event: {:?}", event),
497    ///     None => {}, // No input available
498    /// }
499    /// # Ok::<(), minui::Error>(())
500    /// ```
501    pub fn poll(&mut self) -> Result<Option<Event>> {
502        if event::poll(self.poll_rate)? {
503            if let CrosstermEvent::Mouse(mouse_event) = event::read()? {
504                return Ok(Some(self.convert_mouse_event(mouse_event)));
505            }
506        }
507        Ok(None)
508    }
509
510    /// Waits for mouse input with a timeout.
511    ///
512    /// This method blocks execution for up to the specified timeout duration,
513    /// waiting for mouse input. If input is received within the timeout,
514    /// it's converted to an event and returned. If the timeout expires without
515    /// input, `None` is returned.
516    ///
517    /// # Arguments
518    ///
519    /// * `timeout` - Maximum duration to wait for input
520    ///
521    /// # Returns
522    ///
523    /// - `Ok(Some(Event))` - Mouse input was received within the timeout
524    /// - `Ok(None)` - Timeout expired without input
525    /// - `Err(...)` - An error occurred while waiting for input
526    ///
527    /// # Examples
528    ///
529    /// ```rust
530    /// use minui::input::MouseHandler;
531    /// use minui::Event;
532    /// use std::time::Duration;
533    ///
534    /// let mut mouse = MouseHandler::new();
535    ///
536    /// // Wait up to 1 second for mouse input
537    /// match mouse.get_input(Duration::from_secs(1))? {
538    ///     Some(Event::MouseClick { x, y, .. }) => {
539    ///         println!("Got click at ({}, {})", x, y);
540    ///     },
541    ///     Some(event) => println!("Got event: {:?}", event),
542    ///     None => println!("Timeout - no mouse input"),
543    /// }
544    /// # Ok::<(), minui::Error>(())
545    /// ```
546    pub fn get_input(&mut self, timeout: Duration) -> Result<Option<Event>> {
547        if event::poll(timeout)? {
548            if let CrosstermEvent::Mouse(mouse_event) = event::read()? {
549                return Ok(Some(self.convert_mouse_event(mouse_event)));
550            }
551        }
552        Ok(None)
553    }
554
555    /// Waits indefinitely for mouse input.
556    ///
557    /// This method blocks execution until mouse input is available.
558    /// It will wait forever if necessary.
559    ///
560    /// # Returns
561    ///
562    /// - `Ok(Event)` - Mouse input was received and converted to an event
563    /// - `Err(...)` - An error occurred while waiting for input
564    ///
565    /// # Examples
566    ///
567    /// ```rust
568    /// use minui::input::MouseHandler;
569    /// use minui::Event;
570    ///
571    /// let mut mouse = MouseHandler::new();
572    ///
573    /// println!("Click anywhere to continue...");
574    /// let event = mouse.wait_for_input()?;
575    /// println!("Got input: {:?}", event);
576    /// # Ok::<(), minui::Error>(())
577    /// ```
578    pub fn wait_for_input(&mut self) -> Result<Event> {
579        loop {
580            if let CrosstermEvent::Mouse(mouse_event) = event::read()? {
581                return Ok(self.convert_mouse_event(mouse_event));
582            }
583        }
584    }
585
586    /// Converts a crossterm mouse event to a MinUI event.
587    ///
588    /// This internal method handles the conversion from crossterm's mouse event
589    /// format to MinUI's event types, including drag detection logic.
590    ///
591    /// # Arguments
592    ///
593    /// * `mouse_event` - The crossterm mouse event to convert
594    ///
595    /// # Returns
596    ///
597    /// The corresponding MinUI Event.
598    fn convert_mouse_event(&mut self, mouse_event: MouseEvent) -> Event {
599        let x = mouse_event.column;
600        let y = mouse_event.row;
601
602        match mouse_event.kind {
603            MouseEventKind::Down(button) => {
604                let minui_button = self.convert_mouse_button(button);
605
606                // Track click position for drag detection
607                if self.drag_detection {
608                    self.last_click_pos = Some((x, y));
609                    self.is_dragging = false;
610                }
611
612                Event::MouseClick {
613                    x,
614                    y,
615                    button: minui_button,
616                }
617            }
618            MouseEventKind::Up(button) => {
619                let minui_button = self.convert_mouse_button(button);
620
621                // End drag operation
622                if self.drag_detection {
623                    self.is_dragging = false;
624                    self.last_click_pos = None;
625                }
626
627                Event::MouseRelease {
628                    x,
629                    y,
630                    button: minui_button,
631                }
632            }
633            MouseEventKind::Drag(button) => {
634                let minui_button = self.convert_mouse_button(button);
635
636                // Mark as dragging if drag detection is enabled
637                if self.drag_detection && self.last_click_pos.is_some() {
638                    self.is_dragging = true;
639                }
640
641                Event::MouseDrag {
642                    x,
643                    y,
644                    button: minui_button,
645                }
646            }
647            MouseEventKind::Moved => {
648                // Only generate move events if movement tracking is enabled
649                if self.track_movement {
650                    // Update drag state if drag detection is enabled
651                    if self.drag_detection && self.last_click_pos.is_some() {
652                        self.is_dragging = true;
653                    }
654
655                    Event::MouseMove { x, y }
656                } else {
657                    Event::Unknown
658                }
659            }
660            MouseEventKind::ScrollDown => self.handle_scroll(ScrollDirection::Vertical, 1),
661            MouseEventKind::ScrollUp => self.handle_scroll(ScrollDirection::Vertical, -1),
662            MouseEventKind::ScrollLeft => self.handle_scroll(ScrollDirection::Horizontal, 1),
663            MouseEventKind::ScrollRight => self.handle_scroll(ScrollDirection::Horizontal, -1),
664        }
665    }
666
667    /// Converts a crossterm mouse button to a MinUI mouse button.
668    ///
669    /// # Arguments
670    ///
671    /// * `button` - The crossterm mouse button to convert
672    ///
673    /// # Returns
674    ///
675    /// The corresponding MinUI MouseButton.
676    fn convert_mouse_button(&self, button: CrosstermMouseButton) -> MouseButton {
677        match button {
678            CrosstermMouseButton::Left => MouseButton::Left,
679            CrosstermMouseButton::Right => MouseButton::Right,
680            CrosstermMouseButton::Middle => MouseButton::Middle,
681        }
682    }
683
684    /// Handles scroll events with direction buffering to prevent cross-axis noise.
685    ///
686    /// This maintains a buffer that requires 2 consecutive scroll events in the
687    /// opposite direction before switching scroll axes, preventing accidental
688    /// cross-axis scrolling.
689    fn handle_scroll(&mut self, direction: ScrollDirection, delta: i8) -> Event {
690        const BUFFER_THRESHOLD: u8 = 2;
691
692        match self.last_scroll_direction {
693            None => {
694                // First scroll event, set the direction
695                self.last_scroll_direction = Some(direction);
696                self.scroll_buffer_count = 0;
697                self.emit_scroll_event(direction, delta)
698            }
699            Some(last_dir) if last_dir == direction => {
700                // Same direction, reset buffer and emit
701                self.scroll_buffer_count = 0;
702                self.emit_scroll_event(direction, delta)
703            }
704            Some(_) => {
705                // Different direction, increment buffer
706                self.scroll_buffer_count += 1;
707
708                if self.scroll_buffer_count >= BUFFER_THRESHOLD {
709                    // Buffer threshold reached, switch direction
710                    self.last_scroll_direction = Some(direction);
711                    self.scroll_buffer_count = 0;
712                    self.emit_scroll_event(direction, delta)
713                } else {
714                    // Still in buffer, emit in the previous direction
715                    self.emit_scroll_event(self.last_scroll_direction.unwrap(), delta)
716                }
717            }
718        }
719    }
720
721    /// Emits the appropriate scroll event for the given direction and delta.
722    fn emit_scroll_event(&self, direction: ScrollDirection, delta: i8) -> Event {
723        match direction {
724            ScrollDirection::Vertical => {
725                let final_delta = if self.invert_scroll_vertical {
726                    -delta
727                } else {
728                    delta
729                };
730                Event::MouseScroll { delta: final_delta }
731            }
732            ScrollDirection::Horizontal => {
733                let final_delta = if self.invert_scroll_horizontal {
734                    -delta
735                } else {
736                    delta
737                };
738                Event::MouseScrollHorizontal { delta: final_delta }
739            }
740        }
741    }
742
743    /// Converts a crossterm MouseEvent to a MinUI Event.
744    ///
745    /// This public method allows external code to process mouse events through
746    /// the mouse handler, applying drag detection logic if configured.
747    ///
748    /// # Arguments
749    ///
750    /// * `mouse_event` - The crossterm mouse event to convert
751    ///
752    /// # Returns
753    ///
754    /// The corresponding MinUI Event.
755    pub fn process_mouse_event(&mut self, mouse_event: MouseEvent) -> Event {
756        self.convert_mouse_event(mouse_event)
757    }
758}
759
760impl Default for MouseHandler {
761    fn default() -> Self {
762        Self::new()
763    }
764}
765
766/// Combined input handler for both keyboard and mouse input.
767///
768/// This convenience struct allows handling both keyboard and mouse input
769/// from a single interface, which is useful for applications that need
770/// comprehensive input handling.
771///
772/// # Examples
773///
774/// ```rust
775/// use minui::input::CombinedInputHandler;
776/// use minui::Event;
777///
778/// let mut input = CombinedInputHandler::new();
779///
780/// if let Some(event) = input.poll()? {
781///     match event {
782///         Event::Character(c) => println!("Typed: {}", c),
783///         Event::MouseClick { x, y, .. } => println!("Clicked at ({}, {})", x, y),
784///         Event::KeyUp => println!("Up arrow pressed"),
785///         _ => println!("Other input: {:?}", event),
786///     }
787/// }
788/// # Ok::<(), minui::Error>(())
789/// ```
790pub struct CombinedInputHandler {
791    keyboard: crate::input::KeyboardHandler,
792    mouse: MouseHandler,
793}
794
795impl CombinedInputHandler {
796    fn process_input_event(&mut self, input: CrosstermEvent) -> Option<Event> {
797        match input {
798            CrosstermEvent::Key(key_event) => Some(self.keyboard.process_key_event(key_event)),
799            CrosstermEvent::Mouse(mouse_event) => Some(self.mouse.process_mouse_event(mouse_event)),
800            CrosstermEvent::Paste(text) => Some(Event::Paste(text)),
801            CrosstermEvent::Resize(width, height) => Some(Event::Resize { width, height }),
802            _ => None,
803        }
804    }
805
806    /// Creates a new combined input handler.
807    ///
808    /// Both keyboard and mouse handlers are initialized with their default settings.
809    pub fn new() -> Self {
810        Self {
811            keyboard: crate::input::KeyboardHandler::new(),
812            mouse: MouseHandler::new(),
813        }
814    }
815
816    /// Creates a new combined input handler with common keybinds.
817    ///
818    /// The keyboard handler is initialized with common keybinds,
819    /// and the mouse handler uses default settings.
820    pub fn with_common_keybinds() -> Self {
821        Self {
822            keyboard: crate::input::KeyboardHandler::with_common_keybinds(),
823            mouse: MouseHandler::new(),
824        }
825    }
826
827    /// Returns a mutable reference to the keyboard handler.
828    ///
829    /// This allows configuration of keyboard-specific settings.
830    pub fn keyboard_mut(&mut self) -> &mut crate::input::KeyboardHandler {
831        &mut self.keyboard
832    }
833
834    /// Returns a mutable reference to the mouse handler.
835    ///
836    /// This allows configuration of mouse-specific settings.
837    pub fn mouse_mut(&mut self) -> &mut MouseHandler {
838        &mut self.mouse
839    }
840
841    /// Polls for any input (keyboard or mouse) without blocking.
842    ///
843    /// This method reads the shared terminal event queue once and dispatches the event to the
844    /// appropriate handler.
845    ///
846    /// # Returns
847    ///
848    /// - `Ok(Some(Event))` - Input was available from either source
849    /// - `Ok(None)` - No input is currently available
850    /// - `Err(...)` - An error occurred while checking for input
851    pub fn poll(&mut self) -> Result<Option<Event>> {
852        if !event::poll(Duration::ZERO)? {
853            return Ok(None);
854        }
855
856        Ok(self.process_input_event(event::read()?))
857    }
858
859    /// Waits for any input (keyboard or mouse) with a timeout.
860    ///
861    /// This method waits for input from either keyboard or mouse sources
862    /// up to the specified timeout.
863    ///
864    /// # Arguments
865    ///
866    /// * `timeout` - Maximum duration to wait for input
867    ///
868    /// # Returns
869    ///
870    /// - `Ok(Some(Event))` - Input was received within the timeout
871    /// - `Ok(None)` - Timeout expired without input
872    /// - `Err(...)` - An error occurred while waiting for input
873    pub fn get_input(&mut self, timeout: Duration) -> Result<Option<Event>> {
874        if !event::poll(timeout)? {
875            return Ok(None);
876        }
877
878        Ok(self.process_input_event(event::read()?))
879    }
880}
881
882impl Default for CombinedInputHandler {
883    fn default() -> Self {
884        Self::new()
885    }
886}