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}