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