Skip to main content

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    #[allow(clippy::collapsible_match)]
599    pub fn doubleclick2(&self, areas: &[Rect], event: &MouseEvent, filter: KeyModifiers) -> bool {
600        match event {
601            MouseEvent {
602                kind: MouseEventKind::Down(MouseButton::Left),
603                column,
604                row,
605                modifiers,
606            } if *modifiers == filter => 'f: {
607                for (n, area) in areas.iter().enumerate() {
608                    if area.contains((*column, *row).into()) {
609                        match self.click.get() {
610                            Clicks::Up1(v) => {
611                                if let Some(time) = self.time.get() {
612                                    if time.elapsed().unwrap_or_default().as_millis() as u32
613                                        > double_click_timeout()
614                                    {
615                                        self.time.set(Some(SystemTime::now()));
616                                        self.click.set(Clicks::Down1(n));
617                                        break 'f false;
618                                    }
619                                }
620                                if n == v {
621                                    self.click.set(Clicks::Down2(n));
622                                } else {
623                                    self.click.set(Clicks::None);
624                                }
625                            }
626                            _ => {
627                                self.time.set(Some(SystemTime::now()));
628                                self.click.set(Clicks::Down1(n));
629                            }
630                        }
631                        break 'f false;
632                    }
633                }
634                self.time.set(None);
635                self.click.set(Clicks::None);
636                false
637            }
638            MouseEvent {
639                kind: MouseEventKind::Up(MouseButton::Left),
640                column,
641                row,
642                modifiers,
643            } if *modifiers == filter => 'f: {
644                for (n, area) in areas.iter().enumerate() {
645                    if area.contains((*column, *row).into()) {
646                        match self.click.get() {
647                            Clicks::Down1(v) => {
648                                if n == v {
649                                    self.click.set(Clicks::Up1(v));
650                                } else {
651                                    self.click.set(Clicks::None);
652                                }
653                            }
654                            Clicks::Up1(v) => {
655                                if n == v {
656                                    self.click.set(Clicks::None);
657                                    break 'f true;
658                                } else {
659                                    self.click.set(Clicks::None);
660                                }
661                            }
662                            Clicks::Down2(v) => {
663                                if n == v {
664                                    self.click.set(Clicks::None);
665                                    break 'f true;
666                                } else {
667                                    self.click.set(Clicks::None);
668                                }
669                            }
670                            _ => {
671                                self.click.set(Clicks::None);
672                            }
673                        }
674                        break 'f false;
675                    }
676                }
677                self.click.set(Clicks::None);
678                false
679            }
680            _ => false,
681        }
682    }
683}
684
685static DOUBLE_CLICK: AtomicU32 = AtomicU32::new(250);
686
687/// Sets the global double click time-out between consecutive clicks.
688/// In milliseconds.
689///
690/// Default is 250ms.
691pub fn set_double_click_timeout(timeout: u32) {
692    DOUBLE_CLICK.store(timeout, Ordering::Release);
693}
694
695/// The global double click time-out between consecutive clicks.
696/// In milliseconds.
697///
698/// Default is 250ms.
699pub fn double_click_timeout() -> u32 {
700    DOUBLE_CLICK.load(Ordering::Acquire)
701}
702
703static ENHANCED_KEYS: AtomicBool = AtomicBool::new(false);
704
705/// Are enhanced keys available?
706/// Only if true `Release` and `Repeat` keys are available.
707///
708/// This flag is set during startup of the application when
709/// configuring the terminal.
710pub fn have_keyboard_enhancement() -> bool {
711    ENHANCED_KEYS.load(Ordering::Acquire)
712}
713
714/// Set the flag for enhanced keys.
715///
716/// For windows + crossterm this can always be set to true.
717///
718/// For unix this needs to activate the enhancements with PushKeyboardEnhancementFlags,
719/// and it still needs to query supports_keyboard_enhancement().
720/// If you enable REPORT_ALL_KEYS_AS_ESCAPE_CODES you need REPORT_ALTERNATE_KEYS to,
721/// otherwise shift+key will not return something useful.
722///
723pub fn set_have_keyboard_enhancement(have: bool) {
724    ENHANCED_KEYS.store(have, Ordering::Release);
725}