1use std::fmt;
16use std::sync::Arc;
17
18use crate::data::{CellValue, ColumnKind};
19use crate::grid::menu::MenuAction;
20use crate::grid::state::GridState;
21
22#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum ContextMenuTarget {
25 Cell {
27 display_row_index: usize,
28 source_row_index: usize,
29 column_index: usize,
30 },
31 RowHeader {
33 display_row_index: usize,
34 source_row_index: usize,
35 },
36 ColumnHeader { column_index: usize },
38 SortButton { column_index: usize },
40}
41
42impl ContextMenuTarget {
43 #[must_use]
46 pub fn column_index(&self) -> Option<usize> {
47 match self {
48 Self::Cell { column_index, .. } => Some(*column_index),
49 Self::ColumnHeader { column_index } => Some(*column_index),
50 Self::SortButton { column_index } => Some(*column_index),
51 Self::RowHeader { .. } => None,
52 }
53 }
54
55 #[must_use]
58 pub fn display_row_index(&self) -> Option<usize> {
59 match self {
60 Self::Cell {
61 display_row_index, ..
62 } => Some(*display_row_index),
63 Self::RowHeader {
64 display_row_index, ..
65 } => Some(*display_row_index),
66 Self::ColumnHeader { .. } | Self::SortButton { .. } => None,
67 }
68 }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct ContextMenuSelection {
74 pub row_start: usize,
75 pub row_end: usize,
76 pub column_start: usize,
77 pub column_end: usize,
78}
79
80#[derive(Clone, Debug)]
82pub struct SelectedCellContext {
83 pub display_row_index: usize,
84 pub source_row_index: usize,
85 pub column_index: usize,
86 pub column_name: String,
87 pub value: CellValue,
88}
89
90#[derive(Clone, Debug)]
92pub struct ColumnContext {
93 pub index: usize,
94 pub name: String,
95 pub kind: ColumnKind,
96}
97
98#[derive(Clone, Debug)]
101pub struct SelectedRowContext {
102 pub display_row_index: usize,
103 pub source_row_index: usize,
104 pub values: Vec<CellValue>,
105 pub columns: Vec<ColumnContext>,
106}
107
108impl SelectedRowContext {
109 #[must_use]
111 pub fn value_at(&self, column_index: usize) -> Option<&CellValue> {
112 self.values.get(column_index)
113 }
114
115 #[must_use]
118 pub fn value_by_name(&self, column_name: &str) -> Option<&CellValue> {
119 self.column_index(column_name)
120 .and_then(|i| self.values.get(i))
121 }
122
123 pub fn named_values(&self) -> impl Iterator<Item = (&str, &CellValue)> {
126 self.columns
127 .iter()
128 .filter_map(move |col| self.values.get(col.index).map(|v| (col.name.as_str(), v)))
129 }
130
131 #[must_use]
134 pub fn column_index(&self, column_name: &str) -> Option<usize> {
135 self.columns
136 .iter()
137 .find(|c| c.name == column_name)
138 .map(|c| c.index)
139 }
140}
141
142#[derive(Clone)]
173pub struct ContextMenuRequest {
174 pub target: ContextMenuTarget,
175 pub selection: Option<ContextMenuSelection>,
176 rows: Arc<Vec<Vec<CellValue>>>,
177 display_indices: Arc<Vec<usize>>,
178 columns: Arc<[ColumnContext]>,
179 column_oriented: bool,
180}
181
182impl fmt::Debug for ContextMenuRequest {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 f.debug_struct("ContextMenuRequest")
185 .field("target", &self.target)
186 .field("selection", &self.selection)
187 .field("column_oriented", &self.column_oriented)
188 .field("selected_cell_count", &self.selected_cell_count())
189 .field("selected_row_count", &self.selected_row_count())
190 .finish_non_exhaustive()
191 }
192}
193
194impl ContextMenuRequest {
195 pub(crate) fn new(
198 target: ContextMenuTarget,
199 selection: Option<ContextMenuSelection>,
200 rows: Arc<Vec<Vec<CellValue>>>,
201 display_indices: Arc<Vec<usize>>,
202 columns: Arc<[ColumnContext]>,
203 column_oriented: bool,
204 ) -> Self {
205 Self {
206 target,
207 selection,
208 rows,
209 display_indices,
210 columns,
211 column_oriented,
212 }
213 }
214
215 fn bounds(&self) -> Option<(usize, usize, usize, usize)> {
219 self.selection.as_ref().map(|s| {
220 (
221 s.row_start,
222 s.column_start,
223 s.row_end.min(self.display_indices.len().saturating_sub(1)),
224 s.column_end.min(self.columns.len().saturating_sub(1)),
225 )
226 })
227 }
228
229 fn cell_at(&self, display_row: usize, column: usize) -> Option<SelectedCellContext> {
233 let &source_row_index = self.display_indices.get(display_row)?;
234 let value = self.rows.get(source_row_index)?.get(column)?.clone();
235 let col = self.columns.get(column)?;
236 Some(SelectedCellContext {
237 display_row_index: display_row,
238 source_row_index,
239 column_index: column,
240 column_name: col.name.clone(),
241 value,
242 })
243 }
244
245 fn row_at(&self, display_row: usize) -> Option<SelectedRowContext> {
248 let &source_row_index = self.display_indices.get(display_row)?;
249 let values = self.rows.get(source_row_index)?.clone();
250 Some(SelectedRowContext {
251 display_row_index: display_row,
252 source_row_index,
253 values,
254 columns: self.columns.to_vec(),
255 })
256 }
257
258 #[must_use]
261 pub fn clicked_cell(&self) -> Option<SelectedCellContext> {
262 match self.target {
263 ContextMenuTarget::Cell {
264 display_row_index,
265 column_index,
266 ..
267 } => self.cell_at(display_row_index, column_index),
268 _ => None,
269 }
270 }
271
272 #[must_use]
275 pub fn clicked_row(&self) -> Option<SelectedRowContext> {
276 let row = self.target.display_row_index()?;
277 self.row_at(row)
278 }
279
280 #[must_use]
283 pub fn selected_cell_count(&self) -> usize {
284 self.bounds()
285 .map_or(0, |(r1, c1, r2, c2)| (r2 - r1 + 1) * (c2 - c1 + 1))
286 }
287
288 #[must_use]
291 pub fn selected_row_count(&self) -> usize {
292 if self.column_oriented {
293 return 0;
294 }
295 self.bounds().map_or(0, |(r1, _, r2, _)| r2 - r1 + 1)
296 }
297
298 #[must_use]
302 pub fn is_column_oriented(&self) -> bool {
303 self.column_oriented
304 }
305
306 pub fn for_each_selected_cell(&self, mut f: impl FnMut(SelectedCellContext)) {
309 let Some((r1, c1, r2, c2)) = self.bounds() else {
310 return;
311 };
312 for dr in r1..=r2 {
313 for c in c1..=c2 {
314 if let Some(cell) = self.cell_at(dr, c) {
315 f(cell);
316 }
317 }
318 }
319 }
320
321 pub fn for_each_selected_row(&self, mut f: impl FnMut(SelectedRowContext)) {
325 if self.column_oriented {
326 return;
327 }
328 let Some((r1, _, r2, _)) = self.bounds() else {
329 return;
330 };
331 for dr in r1..=r2 {
332 if let Some(r) = self.row_at(dr) {
333 f(r);
334 }
335 }
336 }
337
338 #[must_use]
342 pub fn selected_cells(&self) -> Vec<SelectedCellContext> {
343 let mut out = Vec::with_capacity(self.selected_cell_count());
344 self.for_each_selected_cell(|c| out.push(c));
345 out
346 }
347
348 #[must_use]
352 pub fn selected_rows(&self) -> Vec<SelectedRowContext> {
353 let mut out = Vec::with_capacity(self.selected_row_count());
354 self.for_each_selected_row(|r| out.push(r));
355 out
356 }
357
358 #[must_use]
362 pub fn for_test(
363 target: ContextMenuTarget,
364 selection: Option<ContextMenuSelection>,
365 rows: Vec<Vec<CellValue>>,
366 columns: Vec<ColumnContext>,
367 ) -> Self {
368 let display_indices: Vec<usize> = (0..rows.len()).collect();
369 Self {
370 target,
371 selection,
372 rows: Arc::new(rows),
373 display_indices: Arc::new(display_indices),
374 columns: columns.into(),
375 column_oriented: false,
376 }
377 }
378}
379
380#[derive(Clone, Debug)]
383pub enum ContextMenuItem {
384 BuiltIn(MenuAction),
387 Action { id: String, label: String },
389 Separator,
391}
392
393impl ContextMenuItem {
394 #[must_use]
396 pub fn action(id: impl Into<String>, label: impl Into<String>) -> Self {
397 Self::Action {
398 id: id.into(),
399 label: label.into(),
400 }
401 }
402
403 #[must_use]
405 pub fn separator() -> Self {
406 Self::Separator
407 }
408
409 #[must_use]
413 pub fn standard_column_header_items() -> Vec<Self> {
414 vec![
415 Self::BuiltIn(MenuAction::SelectColumn),
416 Self::BuiltIn(MenuAction::CopyColumn),
417 Self::BuiltIn(MenuAction::CopyColumnWithHeaders),
418 Self::Separator,
419 Self::BuiltIn(MenuAction::SortAscending),
420 Self::BuiltIn(MenuAction::SortDescending),
421 Self::BuiltIn(MenuAction::ClearSort),
422 Self::Separator,
423 Self::BuiltIn(MenuAction::FilterPrompt),
424 Self::BuiltIn(MenuAction::ClearFilter),
425 ]
426 }
427}
428
429pub trait ContextMenuProvider: 'static {
443 fn menu_items(&self, request: &ContextMenuRequest) -> Vec<ContextMenuItem>;
445
446 #[allow(unused_variables)]
451 fn on_action(
452 &self,
453 action_id: &str,
454 request: &ContextMenuRequest,
455 state: &mut GridState,
456 cx: &mut gpui::App,
457 ) {
458 }
459}
460
461#[derive(Clone)]
464pub(crate) struct ContextMenuProviderHandle(Arc<dyn ContextMenuProvider>);
465
466impl ContextMenuProviderHandle {
467 pub(crate) fn new(provider: impl ContextMenuProvider + 'static) -> Self {
468 Self(Arc::new(provider))
469 }
470}
471
472impl fmt::Debug for ContextMenuProviderHandle {
473 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474 f.debug_struct("ContextMenuProviderHandle")
475 .finish_non_exhaustive()
476 }
477}
478
479impl std::ops::Deref for ContextMenuProviderHandle {
480 type Target = dyn ContextMenuProvider;
481
482 fn deref(&self) -> &Self::Target {
483 &*self.0
484 }
485}
486
487#[derive(Clone, Debug)]
489pub(crate) struct PendingCustomContextMenuAction {
490 pub id: String,
491 pub request: ContextMenuRequest,
492}
493
494#[cfg(test)]
495#[allow(clippy::unwrap_used, clippy::expect_used)]
496mod tests {
497 use super::*;
498
499 fn row(name: &str, values: &[CellValue]) -> SelectedRowContext {
500 let columns = vec![
501 ColumnContext {
502 index: 0,
503 name: "id".into(),
504 kind: ColumnKind::Integer,
505 },
506 ColumnContext {
507 index: 1,
508 name: name.into(),
509 kind: ColumnKind::Text,
510 },
511 ];
512 SelectedRowContext {
513 display_row_index: 0,
514 source_row_index: 0,
515 values: values.to_vec(),
516 columns,
517 }
518 }
519
520 #[test]
521 fn value_at_returns_by_ordinal() {
522 let r = row(
523 "name",
524 &[CellValue::Integer(7), CellValue::Text("hi".into())],
525 );
526 assert_eq!(r.value_at(0), Some(&CellValue::Integer(7)));
527 assert_eq!(r.value_at(1), Some(&CellValue::Text("hi".into())));
528 assert_eq!(r.value_at(2), None);
529 }
530
531 #[test]
532 fn value_by_name_exact_case_sensitive() {
533 let r = row(
534 "Name",
535 &[CellValue::Integer(7), CellValue::Text("hi".into())],
536 );
537 assert_eq!(r.value_by_name("Name"), Some(&CellValue::Text("hi".into())));
538 assert_eq!(r.value_by_name("name"), None);
539 assert_eq!(r.value_by_name("NAME"), None);
540 }
541
542 #[test]
543 fn value_by_name_first_duplicate_wins() {
544 let columns = vec![
545 ColumnContext {
546 index: 0,
547 name: "dup".into(),
548 kind: ColumnKind::Integer,
549 },
550 ColumnContext {
551 index: 1,
552 name: "dup".into(),
553 kind: ColumnKind::Integer,
554 },
555 ];
556 let r = SelectedRowContext {
557 display_row_index: 0,
558 source_row_index: 0,
559 values: vec![CellValue::Integer(1), CellValue::Integer(2)],
560 columns,
561 };
562 assert_eq!(r.value_by_name("dup"), Some(&CellValue::Integer(1)));
563 assert_eq!(r.column_index("dup"), Some(0));
564 }
565
566 #[test]
567 fn named_values_iterates_all_columns() {
568 let r = row(
569 "name",
570 &[CellValue::Integer(7), CellValue::Text("hi".into())],
571 );
572 let pairs: Vec<_> = r.named_values().collect();
573 assert_eq!(pairs.len(), 2);
574 assert_eq!(pairs[0].0, "id");
575 assert_eq!(pairs[0].1, &CellValue::Integer(7));
576 assert_eq!(pairs[1].0, "name");
577 assert_eq!(pairs[1].1, &CellValue::Text("hi".into()));
578 }
579
580 #[test]
581 fn context_menu_target_column_index() {
582 assert_eq!(
583 ContextMenuTarget::Cell {
584 display_row_index: 0,
585 source_row_index: 0,
586 column_index: 3
587 }
588 .column_index(),
589 Some(3)
590 );
591 assert_eq!(
592 ContextMenuTarget::RowHeader {
593 display_row_index: 0,
594 source_row_index: 0
595 }
596 .column_index(),
597 None
598 );
599 }
600
601 #[test]
602 fn context_menu_target_display_row_index() {
603 assert_eq!(
604 ContextMenuTarget::Cell {
605 display_row_index: 5,
606 source_row_index: 2,
607 column_index: 0
608 }
609 .display_row_index(),
610 Some(5)
611 );
612 assert_eq!(
613 ContextMenuTarget::ColumnHeader { column_index: 1 }.display_row_index(),
614 None
615 );
616 }
617
618 #[test]
619 fn standard_column_header_items_match_builtin_order() {
620 let items = ContextMenuItem::standard_column_header_items();
621 assert_eq!(items.len(), 10);
622 assert!(matches!(
623 items[0],
624 ContextMenuItem::BuiltIn(MenuAction::SelectColumn)
625 ));
626 assert!(matches!(items[3], ContextMenuItem::Separator));
627 assert!(matches!(
628 items[9],
629 ContextMenuItem::BuiltIn(MenuAction::ClearFilter)
630 ));
631 }
632
633 fn cols() -> Arc<[ColumnContext]> {
634 Arc::from(vec![
635 ColumnContext {
636 index: 0,
637 name: "a".into(),
638 kind: ColumnKind::Integer,
639 },
640 ColumnContext {
641 index: 1,
642 name: "b".into(),
643 kind: ColumnKind::Text,
644 },
645 ])
646 }
647
648 fn sel(r1: usize, c1: usize, r2: usize, c2: usize) -> ContextMenuSelection {
649 ContextMenuSelection {
650 row_start: r1,
651 row_end: r2,
652 column_start: c1,
653 column_end: c2,
654 }
655 }
656
657 #[test]
658 fn clicked_cell_finds_target_cell() {
659 let rows = Arc::new(vec![
660 vec![CellValue::Integer(1), CellValue::Text("x".into())],
661 vec![CellValue::Integer(2), CellValue::Text("y".into())],
662 vec![CellValue::Integer(3), CellValue::Text("z".into())],
663 ]);
664 let display = Arc::new(vec![0usize, 2usize]);
666 let request = ContextMenuRequest::new(
667 ContextMenuTarget::Cell {
668 display_row_index: 1,
669 source_row_index: 2,
670 column_index: 0,
671 },
672 Some(sel(0, 0, 1, 1)),
673 rows,
674 display,
675 cols(),
676 false,
677 );
678 let clicked = request.clicked_cell().unwrap();
679 assert_eq!(clicked.source_row_index, 2);
680 assert_eq!(clicked.value, CellValue::Integer(3));
681 }
682
683 #[test]
684 fn clicked_cell_none_for_column_header_target() {
685 let request = ContextMenuRequest::new(
686 ContextMenuTarget::ColumnHeader { column_index: 0 },
687 None,
688 Arc::new(vec![]),
689 Arc::new(vec![]),
690 cols(),
691 true,
692 );
693 assert!(request.clicked_cell().is_none());
694 }
695
696 #[test]
697 fn clicked_row_finds_target_for_row_header() {
698 let rows = Arc::new(vec![
699 vec![CellValue::Integer(1), CellValue::Text("x".into())],
700 vec![CellValue::Integer(2), CellValue::Text("y".into())],
701 vec![CellValue::Integer(3), CellValue::Text("z".into())],
702 ]);
703 let display = Arc::new(vec![0usize, 2usize]);
704 let request = ContextMenuRequest::new(
705 ContextMenuTarget::RowHeader {
706 display_row_index: 1,
707 source_row_index: 2,
708 },
709 Some(sel(0, 0, 1, 1)),
710 rows,
711 display,
712 cols(),
713 false,
714 );
715 let clicked = request.clicked_row().unwrap();
716 assert_eq!(clicked.source_row_index, 2);
717 assert_eq!(
718 clicked.values,
719 vec![CellValue::Integer(3), CellValue::Text("z".into())]
720 );
721 }
722
723 #[test]
724 fn clicked_row_none_for_column_header() {
725 let request = ContextMenuRequest::new(
726 ContextMenuTarget::ColumnHeader { column_index: 0 },
727 None,
728 Arc::new(vec![]),
729 Arc::new(vec![]),
730 cols(),
731 true,
732 );
733 assert!(request.clicked_row().is_none());
734 }
735
736 #[test]
737 fn counts_are_computed_from_bounds() {
738 let rows = Arc::new(vec![
739 vec![CellValue::Integer(1), CellValue::Text("x".into())],
740 vec![CellValue::Integer(2), CellValue::Text("y".into())],
741 ]);
742 let display = Arc::new(vec![0usize, 1usize]);
743 let request = ContextMenuRequest::new(
744 ContextMenuTarget::Cell {
745 display_row_index: 0,
746 source_row_index: 0,
747 column_index: 0,
748 },
749 Some(sel(0, 0, 1, 1)),
750 rows,
751 display,
752 cols(),
753 false,
754 );
755 assert_eq!(request.selected_cell_count(), 4);
756 assert_eq!(request.selected_row_count(), 2);
757 assert_eq!(request.selected_cells().len(), 4);
758 assert_eq!(request.selected_rows().len(), 2);
759 }
760
761 #[test]
762 fn column_oriented_has_no_rows() {
763 let rows = Arc::new(vec![
764 vec![CellValue::Integer(1), CellValue::Text("x".into())],
765 vec![CellValue::Integer(2), CellValue::Text("y".into())],
766 ]);
767 let display = Arc::new(vec![0usize, 1usize]);
768 let request = ContextMenuRequest::new(
769 ContextMenuTarget::ColumnHeader { column_index: 0 },
770 Some(sel(0, 0, 1, 0)),
771 rows,
772 display,
773 cols(),
774 true,
775 );
776 assert_eq!(request.selected_row_count(), 0);
777 assert!(request.selected_rows().is_empty());
778 assert_eq!(request.selected_cell_count(), 2);
780 assert_eq!(request.selected_cells().len(), 2);
781 }
782}