rat_event/
util.rs

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