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