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}