rat_event/
util.rs

1//!
2//! Some utility functions that pop up all the time.
3//!
4
5use crate::_private::NonExhaustive;
6use crate::Outcome;
7use ratatui_core::layout::{Position, Rect};
8use ratatui_crossterm::crossterm::event::{
9    Event, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
10};
11use std::cell::Cell;
12use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
13use std::time::SystemTime;
14
15/// Which of the given rects is at the position.
16pub fn item_at(areas: &[Rect], x_pos: u16, y_pos: u16) -> Option<usize> {
17    for (i, r) in areas.iter().enumerate() {
18        if y_pos >= r.top() && y_pos < r.bottom() && x_pos >= r.left() && x_pos < r.right() {
19            return Some(i);
20        }
21    }
22    None
23}
24
25/// Which row of the given area contains the position.
26/// This uses only the vertical components of the given areas.
27///
28/// You might want to limit calling this functions when the full
29/// position is inside your target rect.
30pub fn row_at(areas: &[Rect], y_pos: u16) -> Option<usize> {
31    for (i, r) in areas.iter().enumerate() {
32        if y_pos >= r.top() && y_pos < r.bottom() {
33            return Some(i);
34        }
35    }
36    None
37}
38
39/// Column at given position.
40/// This uses only the horizontal components of the given areas.
41///
42/// You might want to limit calling this functions when the full
43/// position is inside your target rect.
44pub fn column_at(areas: &[Rect], x_pos: u16) -> Option<usize> {
45    for (i, r) in areas.iter().enumerate() {
46        if x_pos >= r.left() && x_pos < r.right() {
47            return Some(i);
48        }
49    }
50    None
51}
52
53/// Find a row position when dragging with the mouse. This uses positions
54/// outside the given areas to estimate an invisible row that could be meant
55/// by the mouse position. It uses the heuristic `1 row == 1 item` for simplicity’s
56/// sake.
57///
58/// Rows outside the bounds are returned as Err(isize), rows inside as Ok(usize).
59pub fn row_at_drag(encompassing: Rect, areas: &[Rect], y_pos: u16) -> Result<usize, isize> {
60    if let Some(row) = row_at(areas, y_pos) {
61        return Ok(row);
62    }
63
64    // assume row-height=1 for outside the box.
65    #[allow(clippy::collapsible_else_if)]
66    if y_pos < encompassing.top() {
67        Err(y_pos as isize - encompassing.top() as isize)
68    } else {
69        if let Some(last) = areas.last() {
70            Err(y_pos as isize - last.bottom() as isize + 1)
71        } else {
72            Err(y_pos as isize - encompassing.top() as isize)
73        }
74    }
75}
76
77/// Find a column position when dragging with the mouse. This uses positions
78/// outside the given areas to estimate an invisible column that could be meant
79/// by the mouse position. It uses the heuristic `1 column == 1 item` for simplicity’s
80/// sake.
81///
82/// Columns outside the bounds are returned as Err(isize), rows inside as Ok(usize).
83pub fn column_at_drag(encompassing: Rect, areas: &[Rect], x_pos: u16) -> Result<usize, isize> {
84    if let Some(column) = column_at(areas, x_pos) {
85        return Ok(column);
86    }
87
88    // change by 1 column if outside the box
89    #[allow(clippy::collapsible_else_if)]
90    if x_pos < encompassing.left() {
91        Err(x_pos as isize - encompassing.left() as isize)
92    } else {
93        if let Some(last) = areas.last() {
94            Err(x_pos as isize - last.right() as isize + 1)
95        } else {
96            Err(x_pos as isize - encompassing.left() as isize)
97        }
98    }
99}
100
101/// This function consumes all mouse-events in the given area,
102/// except Drag events.
103///
104/// This should catch all events when using a popup area.
105pub fn mouse_trap(event: &Event, area: Rect) -> Outcome {
106    match event {
107        Event::Mouse(MouseEvent {
108            kind:
109                MouseEventKind::ScrollLeft
110                | MouseEventKind::ScrollRight
111                | MouseEventKind::ScrollUp
112                | MouseEventKind::ScrollDown
113                | MouseEventKind::Down(_)
114                | MouseEventKind::Up(_)
115                | MouseEventKind::Moved,
116            column,
117            row,
118            ..
119        }) if area.contains(Position::new(*column, *row)) => Outcome::Unchanged,
120        _ => Outcome::Continue,
121    }
122}
123
124/// Click states for double click.
125#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
126pub enum Clicks {
127    #[default]
128    None,
129    Down1(usize),
130    Up1(usize),
131    Down2(usize),
132}
133
134/// Some state for mouse interactions.
135///
136/// This helps with double-click and mouse drag recognition.
137/// Add this to your widget state.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct MouseFlags {
140    /// Timestamp for double click
141    pub time: Cell<Option<SystemTime>>,
142    /// State for double click.
143    pub click: Cell<Clicks>,
144    /// Drag enabled.
145    pub drag: Cell<bool>,
146    /// Hover detect.
147    pub hover: Cell<bool>,
148    pub non_exhaustive: NonExhaustive,
149}
150
151impl Default for MouseFlags {
152    fn default() -> Self {
153        Self {
154            time: Default::default(),
155            click: Default::default(),
156            drag: Default::default(),
157            hover: Default::default(),
158            non_exhaustive: NonExhaustive,
159        }
160    }
161}
162
163impl MouseFlags {
164    /// Returns column/row extracted from the Mouse-Event.
165    pub fn pos_of(&self, event: &MouseEvent) -> (u16, u16) {
166        (event.column, event.row)
167    }
168
169    /// Which of the given rects is at the position.
170    pub fn item_at(&self, areas: &[Rect], x_pos: u16, y_pos: u16) -> Option<usize> {
171        item_at(areas, x_pos, y_pos)
172    }
173
174    /// Which row of the given contains the position.
175    /// This uses only the vertical components of the given areas.
176    ///
177    /// You might want to limit calling this functions when the full
178    /// position is inside your target rect.
179    pub fn row_at(&self, areas: &[Rect], y_pos: u16) -> Option<usize> {
180        row_at(areas, y_pos)
181    }
182
183    /// Column at given position.
184    /// This uses only the horizontal components of the given areas.
185    ///
186    /// You might want to limit calling this functions when the full
187    /// position is inside your target rect.
188    pub fn column_at(&self, areas: &[Rect], x_pos: u16) -> Option<usize> {
189        column_at(areas, x_pos)
190    }
191
192    /// Find a row position when dragging with the mouse. This uses positions
193    /// outside the given areas to estimate an invisible row that could be meant
194    /// by the mouse position. It uses the heuristic `1 row == 1 item` for simplicity’s
195    /// sake.
196    ///
197    /// Rows outside the bounds are returned as Err(isize), rows inside as Ok(usize).
198    pub fn row_at_drag(
199        &self,
200        encompassing: Rect,
201        areas: &[Rect],
202        y_pos: u16,
203    ) -> Result<usize, isize> {
204        row_at_drag(encompassing, areas, y_pos)
205    }
206
207    /// Find a column position when dragging with the mouse. This uses positions
208    /// outside the given areas to estimate an invisible column that could be meant
209    /// by the mouse position. It uses the heuristic `1 column == 1 item` for simplicity’s
210    /// sake.
211    ///
212    /// Columns outside the bounds are returned as Err(isize), rows inside as Ok(usize).
213    pub fn column_at_drag(
214        &self,
215        encompassing: Rect,
216        areas: &[Rect],
217        x_pos: u16,
218    ) -> Result<usize, isize> {
219        column_at_drag(encompassing, areas, x_pos)
220    }
221
222    /// Checks if this is a hover event for the widget.
223    pub fn hover(&self, area: Rect, event: &MouseEvent) -> bool {
224        match event {
225            MouseEvent {
226                kind: MouseEventKind::Moved,
227                column,
228                row,
229                modifiers: KeyModifiers::NONE,
230            } => {
231                let old_hover = self.hover.get();
232                if area.contains((*column, *row).into()) {
233                    self.hover.set(true);
234                } else {
235                    self.hover.set(false);
236                }
237                old_hover != self.hover.get()
238            }
239            _ => false,
240        }
241    }
242
243    /// Checks if this is a drag event for the widget.
244    ///
245    /// It makes sense to allow drag events outside the given area, if the
246    /// drag has been started with a click to the given area.
247    ///
248    /// This can be integrated in the event-match with a guard:
249    ///
250    /// ```rust ignore
251    /// match event {
252    ///         Event::Mouse(m) if state.mouse.drag(state.area, m) => {
253    ///             // ...
254    ///             Outcome::Changed
255    ///         }
256    /// }
257    /// ```
258    pub fn drag(&self, area: Rect, event: &MouseEvent) -> bool {
259        self.drag2(area, event, KeyModifiers::NONE)
260    }
261
262    /// Checks if this is a drag event for the widget.
263    ///
264    /// It makes sense to allow drag events outside the given area, if the
265    /// drag has been started with a click to the given area.
266    ///
267    /// This function handles that case.
268    pub fn drag2(&self, area: Rect, event: &MouseEvent, filter: KeyModifiers) -> bool {
269        match event {
270            MouseEvent {
271                kind: MouseEventKind::Down(MouseButton::Left),
272                column,
273                row,
274                modifiers,
275            } if *modifiers == filter => {
276                if area.contains((*column, *row).into()) {
277                    self.drag.set(true);
278                } else {
279                    self.drag.set(false);
280                }
281            }
282            MouseEvent {
283                kind: MouseEventKind::Drag(MouseButton::Left),
284                modifiers,
285                ..
286            } if *modifiers == filter => {
287                if self.drag.get() {
288                    return true;
289                }
290            }
291            MouseEvent {
292                kind: MouseEventKind::Up(MouseButton::Left) | MouseEventKind::Moved,
293                ..
294            } => {
295                self.drag.set(false);
296            }
297
298            _ => {}
299        }
300
301        false
302    }
303
304    /// Checks for double-click events.
305    ///
306    /// This can be integrated in the event-match with a guard:
307    ///
308    /// ```rust ignore
309    /// match event {
310    ///         Event::Mouse(m) if state.mouse.doubleclick(state.area, m) => {
311    ///             state.flip = !state.flip;
312    ///             Outcome::Changed
313    ///         }
314    /// }
315    /// ```
316    ///
317    pub fn doubleclick(&self, area: Rect, event: &MouseEvent) -> bool {
318        self.doubleclick2(area, event, KeyModifiers::NONE)
319    }
320
321    /// Checks for double-click events.
322    /// This one can have an extra KeyModifiers.
323    ///
324    /// This can be integrated in the event-match with a guard:
325    ///
326    /// ```rust ignore
327    /// match event {
328    ///         Event::Mouse(m) if state.mouse.doubleclick(state.area, m) => {
329    ///             state.flip = !state.flip;
330    ///             Outcome::Changed
331    ///         }
332    /// }
333    /// ```
334    ///
335    #[allow(clippy::collapsible_if)]
336    pub fn doubleclick2(&self, area: Rect, event: &MouseEvent, filter: KeyModifiers) -> bool {
337        match event {
338            MouseEvent {
339                kind: MouseEventKind::Down(MouseButton::Left),
340                column,
341                row,
342                modifiers,
343            } if *modifiers == filter => 'f: {
344                if area.contains((*column, *row).into()) {
345                    match self.click.get() {
346                        Clicks::Up1(_) => {
347                            if let Some(time) = self.time.get() {
348                                if time.elapsed().unwrap_or_default().as_millis() as u32
349                                    > double_click_timeout()
350                                {
351                                    self.time.set(Some(SystemTime::now()));
352                                    self.click.set(Clicks::Down1(0));
353                                    break 'f false;
354                                }
355                            }
356                        }
357                        _ => {
358                            self.time.set(Some(SystemTime::now()));
359                            self.click.set(Clicks::Down1(0));
360                        }
361                    }
362                    break 'f false;
363                } else {
364                    self.time.set(None);
365                    self.click.set(Clicks::None);
366                    break 'f false;
367                }
368            }
369            MouseEvent {
370                kind: MouseEventKind::Up(MouseButton::Left),
371                column,
372                row,
373                modifiers,
374            } if *modifiers == filter => 'f: {
375                if area.contains((*column, *row).into()) {
376                    match self.click.get() {
377                        Clicks::Down1(_) => {
378                            self.click.set(Clicks::Up1(0));
379                            break 'f false;
380                        }
381                        Clicks::Up1(_) => {
382                            self.click.set(Clicks::None);
383                            break 'f true;
384                        }
385                        Clicks::Down2(_) => {
386                            self.click.set(Clicks::None);
387                            break 'f true;
388                        }
389                        _ => {
390                            self.click.set(Clicks::None);
391                            break 'f false;
392                        }
393                    }
394                } else {
395                    self.click.set(Clicks::None);
396                    break 'f false;
397                }
398            }
399            _ => false,
400        }
401    }
402}
403
404/// Some state for mouse interactions with multiple areas.
405///
406/// This helps with double-click and mouse drag recognition.
407/// Add this to your widget state.
408#[derive(Debug, Clone, PartialEq, Eq)]
409pub struct MouseFlagsN {
410    /// Timestamp for double click
411    pub time: Cell<Option<SystemTime>>,
412    /// Flag for the first down.
413    pub click: Cell<Clicks>,
414    /// Drag enabled.
415    pub drag: Cell<Option<usize>>,
416    /// Hover detect.
417    pub hover: Cell<Option<usize>>,
418    pub non_exhaustive: NonExhaustive,
419}
420
421impl Default for MouseFlagsN {
422    fn default() -> Self {
423        Self {
424            time: Default::default(),
425            click: Default::default(),
426            drag: Default::default(),
427            hover: Default::default(),
428            non_exhaustive: NonExhaustive,
429        }
430    }
431}
432
433impl MouseFlagsN {
434    /// Returns column/row extracted from the Mouse-Event.
435    pub fn pos_of(&self, event: &MouseEvent) -> (u16, u16) {
436        (event.column, event.row)
437    }
438
439    /// Which of the given rects is at the position.
440    pub fn item_at(&self, areas: &[Rect], x_pos: u16, y_pos: u16) -> Option<usize> {
441        item_at(areas, x_pos, y_pos)
442    }
443
444    /// Which row of the given contains the position.
445    /// This uses only the vertical components of the given areas.
446    ///
447    /// You might want to limit calling this functions when the full
448    /// position is inside your target rect.
449    pub fn row_at(&self, areas: &[Rect], y_pos: u16) -> Option<usize> {
450        row_at(areas, y_pos)
451    }
452
453    /// Column at given position.
454    /// This uses only the horizontal components of the given areas.
455    ///
456    /// You might want to limit calling this functions when the full
457    /// position is inside your target rect.
458    pub fn column_at(&self, areas: &[Rect], x_pos: u16) -> Option<usize> {
459        column_at(areas, x_pos)
460    }
461
462    /// Find a row position when dragging with the mouse. This uses positions
463    /// outside the given areas to estimate an invisible row that could be meant
464    /// by the mouse position. It uses the heuristic `1 row == 1 item` for simplicity’s
465    /// sake.
466    ///
467    /// Rows outside the bounds are returned as Err(isize), rows inside as Ok(usize).
468    pub fn row_at_drag(
469        &self,
470        encompassing: Rect,
471        areas: &[Rect],
472        y_pos: u16,
473    ) -> Result<usize, isize> {
474        row_at_drag(encompassing, areas, y_pos)
475    }
476
477    /// Find a column position when dragging with the mouse. This uses positions
478    /// outside the given areas to estimate an invisible column that could be meant
479    /// by the mouse position. It uses the heuristic `1 column == 1 item` for simplicity’s
480    /// sake.
481    ///
482    /// Columns outside the bounds are returned as Err(isize), rows inside as Ok(usize).
483    pub fn column_at_drag(
484        &self,
485        encompassing: Rect,
486        areas: &[Rect],
487        x_pos: u16,
488    ) -> Result<usize, isize> {
489        column_at_drag(encompassing, areas, x_pos)
490    }
491
492    /// Checks if this is a hover event for the widget.
493    pub fn hover(&self, areas: &[Rect], event: &MouseEvent) -> bool {
494        match event {
495            MouseEvent {
496                kind: MouseEventKind::Moved,
497                column,
498                row,
499                modifiers: KeyModifiers::NONE,
500            } => {
501                let old_hover = self.hover.get();
502                if let Some(n) = self.item_at(areas, *column, *row) {
503                    self.hover.set(Some(n));
504                } else {
505                    self.hover.set(None);
506                }
507                old_hover != self.hover.get()
508            }
509            _ => false,
510        }
511    }
512
513    /// Checks if this is a drag event for the widget.
514    ///
515    /// It makes sense to allow drag events outside the given area, if the
516    /// drag has been started with a click to the given area.
517    ///
518    /// This function handles that case.
519    pub fn drag(&self, areas: &[Rect], event: &MouseEvent) -> bool {
520        self.drag2(areas, event, KeyModifiers::NONE)
521    }
522
523    /// Checks if this is a drag event for the widget.
524    ///
525    /// It makes sense to allow drag events outside the given area, if the
526    /// drag has been started with a click to the given area.
527    ///
528    /// This function handles that case.
529    pub fn drag2(&self, areas: &[Rect], event: &MouseEvent, filter: KeyModifiers) -> bool {
530        match event {
531            MouseEvent {
532                kind: MouseEventKind::Down(MouseButton::Left),
533                column,
534                row,
535                modifiers,
536            } if *modifiers == filter => {
537                self.drag.set(None);
538                for (n, area) in areas.iter().enumerate() {
539                    if area.contains((*column, *row).into()) {
540                        self.drag.set(Some(n));
541                    }
542                }
543            }
544            MouseEvent {
545                kind: MouseEventKind::Drag(MouseButton::Left),
546                modifiers,
547                ..
548            } if *modifiers == filter => {
549                if self.drag.get().is_some() {
550                    return true;
551                }
552            }
553            MouseEvent {
554                kind: MouseEventKind::Up(MouseButton::Left) | MouseEventKind::Moved,
555                ..
556            } => {
557                self.drag.set(None);
558            }
559
560            _ => {}
561        }
562
563        false
564    }
565
566    /// Checks for double-click events.
567    ///
568    /// This can be integrated in the event-match with a guard:
569    ///
570    /// ```rust ignore
571    /// match event {
572    ///         Event::Mouse(m) if state.mouse.doubleclick(state.area, m) => {
573    ///             state.flip = !state.flip;
574    ///             Outcome::Changed
575    ///         }
576    /// }
577    /// ```
578    ///
579    pub fn doubleclick(&self, areas: &[Rect], event: &MouseEvent) -> bool {
580        self.doubleclick2(areas, event, KeyModifiers::NONE)
581    }
582
583    /// Checks for double-click events.
584    /// This one can have an extra KeyModifiers.
585    ///
586    /// This can be integrated in the event-match with a guard:
587    ///
588    /// ```rust ignore
589    /// match event {
590    ///         Event::Mouse(m) if state.mouse.doubleclick(state.area, m) => {
591    ///             state.flip = !state.flip;
592    ///             Outcome::Changed
593    ///         }
594    /// }
595    /// ```
596    ///
597    #[allow(clippy::collapsible_if)]
598    pub fn doubleclick2(&self, areas: &[Rect], event: &MouseEvent, filter: KeyModifiers) -> bool {
599        match event {
600            MouseEvent {
601                kind: MouseEventKind::Down(MouseButton::Left),
602                column,
603                row,
604                modifiers,
605            } if *modifiers == filter => 'f: {
606                for (n, area) in areas.iter().enumerate() {
607                    if area.contains((*column, *row).into()) {
608                        match self.click.get() {
609                            Clicks::Up1(v) => {
610                                if let Some(time) = self.time.get() {
611                                    if time.elapsed().unwrap_or_default().as_millis() as u32
612                                        > double_click_timeout()
613                                    {
614                                        self.time.set(Some(SystemTime::now()));
615                                        self.click.set(Clicks::Down1(n));
616                                        break 'f false;
617                                    }
618                                }
619                                if n == v {
620                                    self.click.set(Clicks::Down2(n));
621                                } else {
622                                    self.click.set(Clicks::None);
623                                }
624                            }
625                            _ => {
626                                self.time.set(Some(SystemTime::now()));
627                                self.click.set(Clicks::Down1(n));
628                            }
629                        }
630                        break 'f false;
631                    }
632                }
633                self.time.set(None);
634                self.click.set(Clicks::None);
635                false
636            }
637            MouseEvent {
638                kind: MouseEventKind::Up(MouseButton::Left),
639                column,
640                row,
641                modifiers,
642            } if *modifiers == filter => 'f: {
643                for (n, area) in areas.iter().enumerate() {
644                    if area.contains((*column, *row).into()) {
645                        match self.click.get() {
646                            Clicks::Down1(v) => {
647                                if n == v {
648                                    self.click.set(Clicks::Up1(v));
649                                } else {
650                                    self.click.set(Clicks::None);
651                                }
652                            }
653                            Clicks::Up1(v) => {
654                                if n == v {
655                                    self.click.set(Clicks::None);
656                                    break 'f true;
657                                } else {
658                                    self.click.set(Clicks::None);
659                                }
660                            }
661                            Clicks::Down2(v) => {
662                                if n == v {
663                                    self.click.set(Clicks::None);
664                                    break 'f true;
665                                } else {
666                                    self.click.set(Clicks::None);
667                                }
668                            }
669                            _ => {
670                                self.click.set(Clicks::None);
671                            }
672                        }
673                        break 'f false;
674                    }
675                }
676                self.click.set(Clicks::None);
677                false
678            }
679            _ => false,
680        }
681    }
682}
683
684static DOUBLE_CLICK: AtomicU32 = AtomicU32::new(250);
685
686/// Sets the global double click time-out between consecutive clicks.
687/// In milliseconds.
688///
689/// Default is 250ms.
690pub fn set_double_click_timeout(timeout: u32) {
691    DOUBLE_CLICK.store(timeout, Ordering::Release);
692}
693
694/// The global double click time-out between consecutive clicks.
695/// In milliseconds.
696///
697/// Default is 250ms.
698pub fn double_click_timeout() -> u32 {
699    DOUBLE_CLICK.load(Ordering::Acquire)
700}
701
702static ENHANCED_KEYS: AtomicBool = AtomicBool::new(false);
703
704/// Are enhanced keys available?
705/// Only if true `Release` and `Repeat` keys are available.
706///
707/// This flag is set during startup of the application when
708/// configuring the terminal.
709pub fn have_keyboard_enhancement() -> bool {
710    ENHANCED_KEYS.load(Ordering::Acquire)
711}
712
713/// Set the flag for enhanced keys.
714///
715/// For windows + crossterm this can always be set to true.
716///
717/// For unix this needs to activate the enhancements with PushKeyboardEnhancementFlags,
718/// and it still needs to query supports_keyboard_enhancement().
719/// If you enable REPORT_ALL_KEYS_AS_ESCAPE_CODES you need REPORT_ALTERNATE_KEYS to,
720/// otherwise shift+key will not return something useful.
721///
722pub fn set_have_keyboard_enhancement(have: bool) {
723    ENHANCED_KEYS.store(have, Ordering::Release);
724}