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}