1use std::cell::RefCell;
22use std::time::{Duration, Instant};
23
24use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
25use ratatui::layout::{Position, Rect};
26
27use crate::form::{FieldKind, Form};
28use crate::widgets::datatable::CellHit;
29use crate::{App, InputMode};
30
31pub const WHEEL_ROWS: usize = 3;
33
34pub const DOUBLE_CLICK: Duration = Duration::from_millis(500);
36
37pub fn wanted(mouse: &MouseEvent) -> bool {
41 matches!(
42 mouse.kind,
43 MouseEventKind::Down(MouseButton::Left | MouseButton::Right)
44 | MouseEventKind::Drag(MouseButton::Left)
45 | MouseEventKind::Up(MouseButton::Left)
46 | MouseEventKind::ScrollDown
47 | MouseEventKind::ScrollUp
48 | MouseEventKind::ScrollLeft
49 | MouseEventKind::ScrollRight
50 )
51}
52
53pub struct EnableMouse;
59
60impl crossterm::Command for EnableMouse {
61 fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
62 f.write_str("\x1b[?1000h\x1b[?1002h\x1b[?1006h")
63 }
64
65 #[cfg(windows)]
66 fn execute_winapi(&self) -> std::io::Result<()> {
67 crossterm::Command::execute_winapi(&crossterm::event::EnableMouseCapture)
68 }
69
70 #[cfg(windows)]
71 fn is_ansi_code_supported(&self) -> bool {
72 crossterm::Command::is_ansi_code_supported(&crossterm::event::EnableMouseCapture)
73 }
74}
75
76pub fn capture(on: bool, out: &mut impl std::io::Write) {
79 if on {
80 let _ = crossterm::execute!(out, EnableMouse);
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct FieldId {
88 form: &'static str,
89 field: String,
90}
91
92impl FieldId {
93 pub fn of<T: Form>(field: T::Field) -> Self {
94 Self {
95 form: std::any::type_name::<T>(),
96 field: format!("{field:?}"),
97 }
98 }
99
100 pub fn focus_in<T: Form>(&self, form: &mut T) -> Option<Clicked> {
103 if self.form != std::any::type_name::<T>() {
104 return None;
105 }
106 let (field, kind) = form
107 .fields()
108 .into_iter()
109 .find(|(f, _)| format!("{f:?}") == self.field)?;
110 let focused_already = form.focused() == field;
111 let acts = !form.list_row(field) || focused_already;
112 form.focus(field).then_some(Clicked { kind, acts })
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct Clicked {
120 pub kind: FieldKind,
121 pub acts: bool,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum Hit {
127 Field(FieldId),
129 Option {
132 field: Option<FieldId>,
133 index: usize,
134 current: usize,
135 },
136 PickerItem {
139 visible: usize,
140 selected: usize,
141 multi: bool,
142 },
143 Picker,
145 Editor,
148 Tool(usize),
150 Key(KeyEvent),
152 Chip(KeyEvent),
155 MenuItem(usize),
157 Menu,
159 Modal,
162}
163
164thread_local! {
165 static DRAWING: RefCell<Option<Vec<(Rect, Hit)>>> = const { RefCell::new(None) };
168}
169
170pub fn record(rect: Rect, hit: Hit) {
172 if rect.width == 0 || rect.height == 0 {
173 return;
174 }
175 DRAWING.with_borrow_mut(|drawing| {
176 if let Some(regions) = drawing {
177 regions.push((rect, hit));
178 }
179 });
180}
181
182pub fn record_field<T: Form>(rect: Rect, field: T::Field) {
184 record(rect, Hit::Field(FieldId::of::<T>(field)));
185}
186
187pub fn record_spans(area: Rect, line: &ratatui::text::Line, spans: Vec<(usize, Hit)>) {
191 let mut x = area.x;
192 let mut at = Vec::with_capacity(line.spans.len());
193 for span in &line.spans {
194 let w = span.width() as u16;
195 at.push((x, w));
196 x = x.saturating_add(w);
197 }
198 for (i, hit) in spans {
199 let Some(&(x, w)) = at.get(i) else { continue };
200 if x >= area.right() {
201 continue;
202 }
203 record(Rect::new(x, area.y, w.min(area.right() - x), 1), hit);
204 }
205}
206
207#[cfg(test)]
209pub(crate) fn recording(draw: impl FnOnce()) -> Vec<(Rect, Hit)> {
210 begin_recording();
211 draw();
212 end_recording()
213}
214
215fn begin_recording() {
216 DRAWING.with_borrow_mut(|drawing| *drawing = Some(Vec::new()));
217}
218
219fn end_recording() -> Vec<(Rect, Hit)> {
220 DRAWING.with_borrow_mut(|drawing| drawing.take().unwrap_or_default())
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
225pub enum Pointer {
226 Nothing,
227 Keys(Vec<KeyEvent>),
229 Point(Target, Option<KeyEvent>),
232 Form {
235 field: Option<FieldId>,
236 act: Option<bool>,
237 keys: Vec<KeyEvent>,
238 },
239 Tool(usize),
241 Resize {
243 column: String,
244 x: u16,
245 },
246 Width {
248 column: String,
249 width: u16,
250 },
251 Drop {
253 column: String,
254 onto: String,
255 },
256 Menu(CellHit, Position),
258 MenuChoose(usize),
260 CloseMenu,
262 Redraw,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
269pub enum Target {
270 Table(CellHit),
272 HomeRow(usize),
274 HomeStep(isize),
277 ChartColumn(u16),
280}
281
282#[derive(Debug, Clone, PartialEq, Eq)]
284pub enum Drag {
285 Move { column: String, over: String },
287 Resize { column: String, x: u16, width: u16 },
289}
290
291#[derive(Debug, Default)]
293pub struct Pointing {
294 chips: Vec<(Rect, KeyEvent)>,
296 home_list: Option<(Rect, Vec<Option<usize>>)>,
298 hits: Vec<(Rect, Hit)>,
300 last_click: Option<(u16, u16, Instant)>,
301 drag: Option<Drag>,
302 changed: bool,
305}
306
307impl Pointing {
308 pub fn changed(&mut self) {
310 self.changed = true;
311 }
312
313 pub fn painted(&mut self) {
315 self.changed = false;
316 }
317
318 pub fn on_screen(&self) -> bool {
321 !self.changed
322 }
323
324 pub fn forget_drawn(&mut self) {
326 self.chips.clear();
327 self.home_list = None;
328 self.hits.clear();
329 begin_recording();
330 }
331
332 pub fn drawn(&mut self) {
334 self.hits = end_recording();
335 }
336
337 pub fn chips_drawn(&mut self, chips: Vec<(Rect, &str)>) {
339 self.chips = chips
340 .into_iter()
341 .filter_map(|(rect, key)| chip_key(key).map(|key| (rect, key)))
342 .collect();
343 }
344
345 pub fn home_list_drawn(&mut self, area: Rect, lines: Vec<Option<usize>>) {
347 self.home_list = Some((area, lines));
348 }
349
350 pub fn drag(&self) -> Option<&Drag> {
352 self.drag.as_ref()
353 }
354
355 fn chip_at(&self, at: Position) -> Option<KeyEvent> {
356 self.chips
357 .iter()
358 .find(|(rect, _)| rect.contains(at))
359 .map(|(_, key)| *key)
360 }
361
362 fn home_row_at(&self, at: Position) -> Option<usize> {
363 let (area, lines) = self.home_list.as_ref()?;
364 if !area.contains(at) {
365 return None;
366 }
367 lines.get(usize::from(at.y - area.y)).copied().flatten()
368 }
369
370 fn hit_at(&self, at: Position) -> Option<&Hit> {
373 let hits = match self.hits.iter().rposition(|(_, hit)| *hit == Hit::Modal) {
374 Some(i) if !self.hits[i].0.contains(at) => return None,
375 Some(i) => &self.hits[i + 1..],
376 None => &self.hits[..],
377 };
378 let open = hits
379 .iter()
380 .any(|(_, hit)| matches!(hit, Hit::Picker | Hit::Editor));
381 hits.iter()
382 .rev()
383 .filter(|(_, hit)| {
384 !open || matches!(hit, Hit::Picker | Hit::PickerItem { .. } | Hit::Chip(_))
385 })
386 .find(|(rect, _)| rect.contains(at))
387 .map(|(_, hit)| hit)
388 }
389
390 fn over_picker(&self, at: Position) -> bool {
392 self.hits
393 .iter()
394 .any(|(rect, hit)| *hit == Hit::Picker && rect.contains(at))
395 }
396
397 fn second_click(&mut self, at: Position, now: Instant) -> bool {
400 let double = self.last_click.is_some_and(|(x, y, then)| {
401 (x, y) == (at.x, at.y) && now.saturating_duration_since(then) <= DOUBLE_CLICK
402 });
403 self.last_click = if double {
404 None
405 } else {
406 Some((at.x, at.y, now))
407 };
408 double
409 }
410}
411
412fn header_sort_key(state: &crate::widgets::datatable::DataTableState, column: &str) -> char {
415 if state.view_sort_columns() == [column] {
416 ']'
417 } else {
418 '['
419 }
420}
421
422pub fn chip_key(label: &str) -> Option<KeyEvent> {
424 let named = |code| Some(KeyEvent::new(code, KeyModifiers::NONE));
425 match label {
426 "Enter" => return named(KeyCode::Enter),
427 "Esc" => return named(KeyCode::Esc),
428 "Tab" => return named(KeyCode::Tab),
429 "Space" => return named(KeyCode::Char(' ')),
430 "F1" => return named(KeyCode::F(1)),
431 _ => {}
432 }
433 let mut chars = label.chars();
434 match (chars.next(), chars.next(), chars.next()) {
435 (Some('^'), Some(c), None) if c.is_ascii_alphabetic() => Some(KeyEvent::new(
436 KeyCode::Char(c.to_ascii_lowercase()),
437 KeyModifiers::CONTROL,
438 )),
439 (Some(c), None, None) if c.is_ascii_uppercase() => {
441 Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::SHIFT))
442 }
443 (Some(c), None, None) if c.is_ascii_graphic() => named(KeyCode::Char(c)),
444 _ => None,
445 }
446}
447
448fn press(code: KeyCode) -> KeyEvent {
449 KeyEvent::new(code, KeyModifiers::NONE)
450}
451
452fn steps(delta: isize, back: KeyCode, forward: KeyCode) -> Vec<KeyEvent> {
454 let code = if delta < 0 { back } else { forward };
455 vec![press(code); delta.unsigned_abs()]
456}
457
458pub fn act_key(kind: FieldKind, back: bool) -> Option<KeyEvent> {
462 match kind {
463 FieldKind::Text | FieldKind::MultilineText => None,
464 FieldKind::Choice | FieldKind::Picker { multi: false } if back => {
465 Some(press(KeyCode::Left))
466 }
467 _ if back => None,
468 _ => Some(press(KeyCode::Char(' '))),
469 }
470}
471
472fn click_hit(hit: &Hit, back: bool) -> Pointer {
474 match hit {
475 Hit::Field(id) => Pointer::Form {
476 field: Some(id.clone()),
477 act: Some(back),
478 keys: Vec::new(),
479 },
480 Hit::Option {
481 field,
482 index,
483 current,
484 } => Pointer::Form {
485 field: field.clone(),
486 act: None,
487 keys: steps(
488 *index as isize - *current as isize,
489 KeyCode::Left,
490 KeyCode::Right,
491 ),
492 },
493 Hit::PickerItem {
494 visible,
495 selected,
496 multi,
497 } => {
498 let mut keys = steps(
499 *visible as isize - *selected as isize,
500 KeyCode::Up,
501 KeyCode::Down,
502 );
503 keys.push(press(if *multi {
504 KeyCode::Char(' ')
505 } else {
506 KeyCode::Enter
507 }));
508 Pointer::Keys(keys)
509 }
510 Hit::Tool(i) => Pointer::Tool(*i),
511 Hit::Key(key) | Hit::Chip(key) => Pointer::Keys(vec![*key]),
512 Hit::MenuItem(i) => Pointer::MenuChoose(*i),
513 Hit::Picker | Hit::Editor | Hit::Menu | Hit::Modal => Pointer::Nothing,
514 }
515}
516
517impl App {
518 pub fn pointer(&mut self, mouse: &MouseEvent, now: Instant) -> Pointer {
520 let shift = mouse.modifiers.contains(KeyModifiers::SHIFT);
521 if self.context_menu.is_some() && !self.menu_showing() {
523 self.close_context_menu();
524 }
525 let at = Position {
526 x: mouse.column,
527 y: mouse.row,
528 };
529 match mouse.kind {
530 MouseEventKind::ScrollDown if shift => self.wheel_across(true),
531 MouseEventKind::ScrollUp if shift => self.wheel_across(false),
532 MouseEventKind::ScrollRight => self.wheel_across(true),
533 MouseEventKind::ScrollLeft => self.wheel_across(false),
534 MouseEventKind::ScrollDown => self.wheel(true, at),
535 MouseEventKind::ScrollUp => self.wheel(false, at),
536 MouseEventKind::Down(MouseButton::Left) => {
537 self.pointer.drag = None;
538 self.click(at, now)
539 }
540 MouseEventKind::Down(MouseButton::Right) => {
541 self.pointer.drag = None;
542 self.right_click(at)
543 }
544 MouseEventKind::Drag(MouseButton::Left) => self.drag_to(at),
545 MouseEventKind::Up(MouseButton::Left) => self.release(),
546 _ => Pointer::Nothing,
547 }
548 }
549
550 pub fn cancel_drag(&mut self) {
552 self.pointer.drag = None;
553 }
554
555 pub fn forget_click(&mut self) {
558 self.pointer.last_click = None;
559 self.pointer.drag = None;
560 }
561
562 pub fn point(&mut self, target: &Target) {
565 self.flash = None;
567 match target {
568 Target::Table(hit) => {
569 if let Some(state) = self.data_table_state.as_mut() {
570 state.point_at(hit);
571 }
572 if let (None, Some(column)) = (hit.row, &hit.column) {
573 self.pointer.drag = Some(Drag::Move {
574 column: column.clone(),
575 over: column.clone(),
576 });
577 }
578 }
579 Target::HomeRow(row) => {
580 self.home.status = None;
581 self.home.select(*row);
582 }
583 Target::HomeStep(rows) => {
584 self.home.status = None;
585 self.home.page_selection(*rows);
586 }
587 Target::ChartColumn(column) => {
588 self.chart_modal.plot_focus = true;
589 self.move_crosshair_to(Some(*column));
590 }
591 }
592 }
593
594 pub fn point_for_menu(&mut self, hit: &CellHit) -> bool {
598 self.flash = None;
599 self.data_table_state
600 .as_mut()
601 .is_some_and(|state| state.point_at(hit))
602 }
603
604 pub fn focus_field(&mut self, id: &FieldId) -> Option<Clicked> {
606 self.flash = None;
607 if let Some(kind) = id.focus_in(&mut self.chart_modal) {
608 self.chart_modal.plot_focus = false;
610 return Some(kind);
611 }
612 let shown = id
613 .focus_in(&mut self.export_modal)
614 .or_else(|| id.focus_in(&mut self.copy_modal))
615 .or_else(|| id.focus_in(&mut self.chart_export_modal))
616 .or_else(|| id.focus_in(&mut self.pivot_melt_modal))
617 .or_else(|| id.focus_in(&mut self.sort_filter_modal))
618 .or_else(|| id.focus_in(&mut self.view_modal))
619 .or_else(|| self.combine.as_mut().and_then(|c| id.focus_in(c)))
620 .or_else(|| self.sample_form.as_mut().and_then(|f| id.focus_in(f)));
621 if shown.is_some() {
622 return shown;
623 }
624 let analysis = &mut self.analysis_modal;
625 let kind = analysis
626 .sample_form
627 .as_mut()
628 .and_then(|f| id.focus_in(f))
629 .or_else(|| {
630 analysis
631 .data_quality_expected_form
632 .as_mut()
633 .and_then(|f| id.focus_in(f))
634 })
635 .or_else(|| {
636 analysis
637 .data_quality_intent_form
638 .as_mut()
639 .and_then(|f| id.focus_in(f))
640 })?;
641 analysis.focus = crate::analysis_modal::AnalysisFocus::Main;
643 Some(kind)
644 }
645
646 pub fn point_at_tool(&mut self, tool: usize) {
649 self.flash = None;
650 self.analysis_modal.focus = crate::analysis_modal::AnalysisFocus::Sidebar;
651 self.analysis_modal.sidebar_state.select(Some(tool));
652 }
653
654 pub fn start_resize(&mut self, column: String, x: u16) {
656 use crate::widgets::column_widths::{UNSEEN_WIDTH, WidthChoice};
657 let width = self
658 .data_table_state
659 .as_ref()
660 .and_then(|state| match state.width_choice(&column) {
661 WidthChoice::Manual(w) => Some(w),
662 _ => state.on_screen_width(&column),
663 })
664 .unwrap_or(UNSEEN_WIDTH);
665 self.pointer.drag = Some(Drag::Resize { column, x, width });
666 }
667
668 pub fn set_dragged_width(&mut self, column: String, width: u16) {
670 use crate::widgets::column_widths::{MAX_WIDTH, MIN_WIDTH, WidthChoice};
671 self.flash = None;
672 if let Some(state) = self.data_table_state.as_mut() {
673 let width = width.clamp(MIN_WIDTH, MAX_WIDTH);
674 state.set_width_choices([(column, WidthChoice::Manual(width))]);
675 }
676 }
677
678 fn home_has_the_keys(&self) -> bool {
680 self.input_mode == InputMode::Home
681 && !self.help_visible()
682 && !self.error_modal.active
683 && !self.confirmation_modal.active
684 }
685
686 fn chart_has_the_keys(&self) -> bool {
689 self.input_mode == InputMode::Chart
690 && self.chart_modal.active
691 && self.chart_modal.picker.is_none()
692 && !self.chart_export_modal.active
693 && !self.help_visible()
694 && !self.error_modal.active
695 && !self.confirmation_modal.active
696 }
697
698 fn dialog_over_all(&self) -> bool {
701 self.help_visible() || self.error_modal.active || self.confirmation_modal.active
702 }
703
704 fn wheel(&self, down: bool, at: Position) -> Pointer {
708 let code = if down { KeyCode::Down } else { KeyCode::Up };
709 let arrows = Pointer::Keys(vec![press(code); WHEEL_ROWS]);
710 if self.context_menu.is_some() {
711 return Pointer::Nothing;
712 }
713 if self.home_has_the_keys() {
714 if self.home.path_input_active {
715 return Pointer::Nothing;
716 }
717 let rows = WHEEL_ROWS as isize;
718 return Pointer::Point(Target::HomeStep(if down { rows } else { -rows }), None);
719 }
720 if !self.dialog_over_all() && self.pointer.over_picker(at) {
721 return arrows;
722 }
723 if self.text_field_focused() {
724 return Pointer::Nothing;
725 }
726 arrows
727 }
728
729 fn wheel_across(&self, right: bool) -> Pointer {
732 if !self.in_normal_table_view() || self.data_table_state.is_none() {
733 return Pointer::Nothing;
734 }
735 let code = if right { KeyCode::Right } else { KeyCode::Left };
736 Pointer::Keys(vec![press(code)])
737 }
738
739 fn click(&mut self, at: Position, now: Instant) -> Pointer {
740 if self.context_menu.is_some() {
741 self.pointer.last_click = None;
742 return match self.pointer.hit_at(at) {
743 Some(Hit::MenuItem(i)) => Pointer::MenuChoose(*i),
744 Some(Hit::Menu) => Pointer::Nothing,
745 _ => Pointer::CloseMenu,
746 };
747 }
748 if let Some(key) = self.pointer.chip_at(at) {
749 self.pointer.last_click = None;
750 return Pointer::Keys(vec![key]);
751 }
752 if self.dialog_over_all() {
754 self.pointer.last_click = None;
755 return match self.pointer.hit_at(at) {
756 Some(Hit::Chip(key)) => Pointer::Keys(vec![*key]),
757 _ => Pointer::Nothing,
758 };
759 }
760 if let Some(hit) = self.pointer.hit_at(at).cloned() {
761 self.pointer.last_click = None;
762 return click_hit(&hit, false);
763 }
764 let double = self.pointer.second_click(at, now);
765 let enter = double.then(|| press(KeyCode::Enter));
766 if self.home_has_the_keys() {
767 return match self.pointer.home_row_at(at) {
768 Some(row) if !self.home.path_input_active => {
769 Pointer::Point(Target::HomeRow(row), enter)
770 }
771 _ => Pointer::Nothing,
772 };
773 }
774 if self.chart_has_the_keys()
775 && self.chart_modal.has_crosshair()
776 && self
777 .chart_modal
778 .plot
779 .is_some_and(|plot| plot.graph.contains(at))
780 {
781 return Pointer::Point(Target::ChartColumn(at.x), None);
782 }
783 if self.in_normal_table_view()
784 && let Some(state) = self.data_table_state.as_ref()
785 {
786 if let Some(column) = state.drawn_edge(at.x, at.y) {
787 if double {
791 let hit = CellHit {
792 row: None,
793 column: Some(column),
794 };
795 return Pointer::Point(Target::Table(hit), Some(press(KeyCode::Char('='))));
796 }
797 return Pointer::Resize { column, x: at.x };
798 }
799 if let Some(hit) = state.drawn_cell(at.x, at.y) {
800 let key = match (&hit.row, &hit.column) {
803 (Some(_), _) => enter,
804 (None, Some(column)) if double => {
805 Some(press(KeyCode::Char(header_sort_key(state, column))))
806 }
807 (None, _) => None,
808 };
809 return Pointer::Point(Target::Table(hit), key);
810 }
811 }
812 Pointer::Nothing
813 }
814
815 fn right_click(&mut self, at: Position) -> Pointer {
818 self.pointer.last_click = None;
819 if self.context_menu.is_some() {
820 return Pointer::CloseMenu;
821 }
822 if self.dialog_over_all() {
823 return Pointer::Nothing;
824 }
825 if let Some(hit) = self.pointer.hit_at(at).cloned() {
826 return match hit {
827 Hit::Field(_) => click_hit(&hit, true),
828 _ => Pointer::Nothing,
829 };
830 }
831 if self.in_normal_table_view()
832 && let Some(hit) = self
833 .data_table_state
834 .as_ref()
835 .and_then(|state| state.drawn_cell(at.x, at.y))
836 && hit.row.is_some()
837 && hit.column.is_some()
838 {
839 return Pointer::Menu(hit, at);
840 }
841 Pointer::Nothing
842 }
843
844 fn drag_to(&mut self, at: Position) -> Pointer {
847 match self.pointer.drag.clone() {
848 Some(Drag::Move { column, .. }) => {
849 let over = self
852 .data_table_state
853 .as_ref()
854 .and_then(|state| state.drawn_cell(at.x, at.y))
855 .and_then(|hit| hit.column)
856 .unwrap_or_else(|| column.clone());
857 if !matches!(&self.pointer.drag, Some(Drag::Move { over: was, .. }) if *was == over)
858 {
859 self.pointer.drag = Some(Drag::Move { column, over });
861 return Pointer::Redraw;
862 }
863 Pointer::Nothing
864 }
865 Some(Drag::Resize { column, x, width }) => {
866 let width = (i32::from(width) + i32::from(at.x) - i32::from(x)).max(0);
867 Pointer::Width {
868 column,
869 width: u16::try_from(width).unwrap_or(u16::MAX),
870 }
871 }
872 None => Pointer::Nothing,
873 }
874 }
875
876 fn release(&mut self) -> Pointer {
878 match self.pointer.drag.take() {
879 Some(Drag::Move { column, over }) if column != over => {
880 Pointer::Drop { column, onto: over }
881 }
882 Some(Drag::Move { .. }) => Pointer::Redraw,
884 _ => Pointer::Nothing,
885 }
886 }
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892
893 #[test]
894 fn a_chip_presses_the_key_it_names() {
895 let key = |code, modifiers| Some(KeyEvent::new(code, modifiers));
896 assert_eq!(chip_key("Enter"), key(KeyCode::Enter, KeyModifiers::NONE));
897 assert_eq!(
898 chip_key("^O"),
899 key(KeyCode::Char('o'), KeyModifiers::CONTROL)
900 );
901 assert_eq!(chip_key("q"), key(KeyCode::Char('q'), KeyModifiers::NONE));
902 assert_eq!(chip_key("Q"), key(KeyCode::Char('Q'), KeyModifiers::SHIFT));
903 assert_eq!(chip_key("/"), key(KeyCode::Char('/'), KeyModifiers::NONE));
904 assert_eq!(chip_key("F1"), key(KeyCode::F(1), KeyModifiers::NONE));
905 assert_eq!(chip_key("↑↓"), None);
907 assert_eq!(chip_key("h/l"), None);
908 assert_eq!(chip_key("PgUp"), None);
909 }
910
911 #[test]
912 fn a_second_click_on_the_same_cell_soon_after_is_a_double_click() {
913 let mut p = Pointing::default();
914 let at = Position { x: 4, y: 7 };
915 let t = Instant::now();
916 assert!(!p.second_click(at, t));
917 assert!(p.second_click(at, t + Duration::from_millis(200)));
918 assert!(!p.second_click(at, t + Duration::from_millis(300)));
920 assert!(!p.second_click(at, t + Duration::from_secs(2)));
922 assert!(!p.second_click(Position { x: 5, y: 7 }, t + Duration::from_secs(2)));
923 }
924
925 #[test]
926 fn presses_drags_releases_a_right_press_and_the_wheel_are_wanted() {
927 let mouse = |kind| MouseEvent {
928 kind,
929 column: 0,
930 row: 0,
931 modifiers: KeyModifiers::NONE,
932 };
933 assert!(wanted(&mouse(MouseEventKind::Down(MouseButton::Left))));
934 assert!(wanted(&mouse(MouseEventKind::ScrollUp)));
935 assert!(wanted(&mouse(MouseEventKind::ScrollRight)));
936 assert!(wanted(&mouse(MouseEventKind::Down(MouseButton::Right))));
937 assert!(wanted(&mouse(MouseEventKind::Up(MouseButton::Left))));
938 assert!(wanted(&mouse(MouseEventKind::Drag(MouseButton::Left))));
939 assert!(!wanted(&mouse(MouseEventKind::Moved)));
940 assert!(!wanted(&mouse(MouseEventKind::Down(MouseButton::Middle))));
941 assert!(!wanted(&mouse(MouseEventKind::Drag(MouseButton::Right))));
942 }
943
944 #[test]
949 fn the_mouse_is_taken_only_when_asked_for() {
950 use crossterm::Command;
951 let mut out = Vec::new();
952 capture(false, &mut out);
953 assert!(out.is_empty(), "off: nothing is asked for");
954 let mut ansi = String::new();
955 EnableMouse.write_ansi(&mut ansi).unwrap();
956 assert_eq!(ansi, "\x1b[?1000h\x1b[?1002h\x1b[?1006h");
957 assert!(!ansi.contains("?1003h"), "no motion without a button");
958 #[cfg(not(windows))]
959 {
960 capture(true, &mut out);
961 assert_eq!(String::from_utf8(out).unwrap(), ansi, "on: it is asked for");
962 }
963 }
964
965 #[test]
966 fn regions_are_recorded_only_while_a_frame_is_drawn() {
967 let rect = Rect::new(1, 1, 4, 1);
968 record(rect, Hit::Tool(0));
969 let mut p = Pointing::default();
970 p.forget_drawn();
971 record(rect, Hit::Tool(1));
972 record(Rect::new(0, 0, 0, 1), Hit::Tool(2));
973 p.drawn();
974 assert_eq!(p.hits, vec![(rect, Hit::Tool(1))]);
975 record(rect, Hit::Tool(3));
976 p.forget_drawn();
977 p.drawn();
978 assert!(p.hits.is_empty(), "a frame keeps only what it drew");
979 }
980
981 #[test]
982 fn the_topmost_region_takes_the_click_and_an_open_picker_takes_them_all() {
983 let field = FieldId {
984 form: "f",
985 field: "Name".into(),
986 };
987 let mut p = Pointing {
988 hits: vec![
989 (Rect::new(0, 0, 10, 1), Hit::Field(field.clone())),
990 (Rect::new(0, 0, 3, 1), Hit::Tool(2)),
991 ],
992 ..Pointing::default()
993 };
994 assert_eq!(p.hit_at(Position { x: 1, y: 0 }), Some(&Hit::Tool(2)));
995 assert_eq!(
996 p.hit_at(Position { x: 5, y: 0 }),
997 Some(&Hit::Field(field.clone()))
998 );
999 assert_eq!(p.hit_at(Position { x: 5, y: 1 }), None);
1000 p.hits.push((Rect::new(0, 3, 10, 2), Hit::Picker));
1001 let item = Hit::PickerItem {
1002 visible: 1,
1003 selected: 0,
1004 multi: false,
1005 };
1006 p.hits.push((Rect::new(0, 4, 10, 1), item.clone()));
1007 assert_eq!(p.hit_at(Position { x: 5, y: 0 }), None, "the form waits");
1008 assert_eq!(p.hit_at(Position { x: 5, y: 4 }), Some(&item));
1009 assert!(p.over_picker(Position { x: 5, y: 3 }));
1010 }
1011
1012 #[test]
1013 fn under_a_dialog_only_what_it_drew_takes_clicks() {
1014 let p = Pointing {
1015 hits: vec![
1016 (Rect::new(0, 0, 40, 1), Hit::Tool(0)),
1017 (Rect::new(0, 5, 40, 1), Hit::Tool(1)),
1018 (Rect::new(10, 4, 20, 4), Hit::Modal),
1019 (Rect::new(10, 5, 20, 1), Hit::Tool(2)),
1020 ],
1021 ..Pointing::default()
1022 };
1023 assert_eq!(p.hit_at(Position { x: 2, y: 0 }), None, "outside: nothing");
1024 assert_eq!(p.hit_at(Position { x: 2, y: 5 }), None);
1025 assert_eq!(p.hit_at(Position { x: 12, y: 5 }), Some(&Hit::Tool(2)));
1026 assert_eq!(
1027 p.hit_at(Position { x: 12, y: 6 }),
1028 None,
1029 "inside, on nothing"
1030 );
1031 }
1032
1033 #[test]
1034 fn a_line_records_where_its_spans_landed() {
1035 use ratatui::text::{Line, Span};
1036 let line = Line::from(vec![
1037 Span::raw(" "),
1038 Span::raw("Pivot"),
1039 Span::raw(" | "),
1040 Span::raw("Melt"),
1041 ]);
1042 let mut p = Pointing::default();
1043 p.forget_drawn();
1044 record_spans(
1045 Rect::new(10, 2, 12, 1),
1046 &line,
1047 vec![(1, Hit::Tool(0)), (3, Hit::Tool(1))],
1048 );
1049 p.drawn();
1050 assert_eq!(
1051 p.hits,
1052 vec![
1053 (Rect::new(11, 2, 5, 1), Hit::Tool(0)),
1054 (Rect::new(19, 2, 3, 1), Hit::Tool(1)),
1056 ]
1057 );
1058 }
1059
1060 #[test]
1061 fn a_click_on_a_value_steps_to_it_and_on_a_picker_line_chooses_it() {
1062 let right = press(KeyCode::Right);
1063 let left = press(KeyCode::Left);
1064 let option = |index, current| Hit::Option {
1065 field: None,
1066 index,
1067 current,
1068 };
1069 assert_eq!(
1070 click_hit(&option(2, 0), false),
1071 Pointer::Form {
1072 field: None,
1073 act: None,
1074 keys: vec![right, right]
1075 }
1076 );
1077 assert_eq!(
1078 click_hit(&option(0, 1), false),
1079 Pointer::Form {
1080 field: None,
1081 act: None,
1082 keys: vec![left]
1083 }
1084 );
1085 let item = |visible, selected, multi| Hit::PickerItem {
1086 visible,
1087 selected,
1088 multi,
1089 };
1090 assert_eq!(
1091 click_hit(&item(0, 2, false), false),
1092 Pointer::Keys(vec![
1093 press(KeyCode::Up),
1094 press(KeyCode::Up),
1095 press(KeyCode::Enter)
1096 ])
1097 );
1098 assert_eq!(
1099 click_hit(&item(1, 1, true), false),
1100 Pointer::Keys(vec![press(KeyCode::Char(' '))])
1101 );
1102 }
1103
1104 #[test]
1105 fn a_click_acts_on_a_field_as_space_and_a_right_click_steps_back() {
1106 let space = Some(press(KeyCode::Char(' ')));
1107 assert_eq!(act_key(FieldKind::Text, false), None);
1108 assert_eq!(act_key(FieldKind::MultilineText, true), None);
1109 assert_eq!(act_key(FieldKind::Checkbox, false), space);
1110 assert_eq!(
1111 act_key(FieldKind::Checkbox, true),
1112 None,
1113 "a right click focuses"
1114 );
1115 assert_eq!(act_key(FieldKind::Choice, false), space);
1116 assert_eq!(act_key(FieldKind::Choice, true), Some(press(KeyCode::Left)));
1117 assert_eq!(act_key(FieldKind::Picker { multi: true }, true), None);
1118 assert_eq!(act_key(FieldKind::Button, true), None);
1119 assert_eq!(act_key(FieldKind::Button, false), space);
1120 }
1121
1122 #[test]
1123 fn a_field_id_focuses_only_its_own_form() {
1124 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1125 enum F {
1126 A,
1127 B,
1128 }
1129 struct One(F, bool);
1130 struct Two(F);
1131 impl Form for One {
1132 type Field = F;
1133 fn fields(&self) -> Vec<(F, FieldKind)> {
1134 let mut f = vec![(F::A, FieldKind::Text)];
1135 if self.1 {
1136 f.push((F::B, FieldKind::Checkbox));
1137 }
1138 f
1139 }
1140 fn focused(&self) -> F {
1141 self.0
1142 }
1143 fn set_focused(&mut self, field: F) {
1144 self.0 = field;
1145 }
1146 }
1147 impl Form for Two {
1148 type Field = F;
1149 fn fields(&self) -> Vec<(F, FieldKind)> {
1150 vec![(F::A, FieldKind::Text), (F::B, FieldKind::Choice)]
1151 }
1152 fn focused(&self) -> F {
1153 self.0
1154 }
1155 fn set_focused(&mut self, field: F) {
1156 self.0 = field;
1157 }
1158 }
1159 let id = FieldId::of::<One>(F::B);
1160 let mut two = Two(F::A);
1161 assert_eq!(id.focus_in(&mut two), None, "another form's field");
1162 assert_eq!(two.0, F::A);
1163 let mut hidden = One(F::A, false);
1164 assert_eq!(id.focus_in(&mut hidden), None, "a field not shown");
1165 let mut one = One(F::A, true);
1166 let clicked = |kind| Some(Clicked { kind, acts: true });
1167 assert_eq!(id.focus_in(&mut one), clicked(FieldKind::Checkbox));
1168 assert_eq!(one.0, F::B);
1169 }
1170}