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)]
174pub struct ContextMenuRequest {
175 pub target: ContextMenuTarget,
176 pub selection: Option<ContextMenuSelection>,
177 rows: Arc<Vec<Vec<CellValue>>>,
178 display_indices: Arc<Vec<usize>>,
179 columns: Arc<[ColumnContext]>,
180 column_oriented: bool,
181}
182
183impl fmt::Debug for ContextMenuRequest {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 f.debug_struct("ContextMenuRequest")
186 .field("target", &self.target)
187 .field("selection", &self.selection)
188 .field("column_oriented", &self.column_oriented)
189 .field("selected_cell_count", &self.selected_cell_count())
190 .field("selected_row_count", &self.selected_row_count())
191 .finish_non_exhaustive()
192 }
193}
194
195impl ContextMenuRequest {
196 pub(crate) fn new(
199 target: ContextMenuTarget,
200 selection: Option<ContextMenuSelection>,
201 rows: Arc<Vec<Vec<CellValue>>>,
202 display_indices: Arc<Vec<usize>>,
203 columns: Arc<[ColumnContext]>,
204 column_oriented: bool,
205 ) -> Self {
206 Self {
207 target,
208 selection,
209 rows,
210 display_indices,
211 columns,
212 column_oriented,
213 }
214 }
215
216 fn bounds(&self) -> Option<(usize, usize, usize, usize)> {
220 self.selection.as_ref().map(|s| {
221 (
222 s.row_start,
223 s.column_start,
224 s.row_end.min(self.display_indices.len().saturating_sub(1)),
225 s.column_end.min(self.columns.len().saturating_sub(1)),
226 )
227 })
228 }
229
230 fn cell_at(&self, display_row: usize, column: usize) -> Option<SelectedCellContext> {
234 let &source_row_index = self.display_indices.get(display_row)?;
235 let value = self.rows.get(source_row_index)?.get(column)?.clone();
236 let col = self.columns.get(column)?;
237 Some(SelectedCellContext {
238 display_row_index: display_row,
239 source_row_index,
240 column_index: column,
241 column_name: col.name.clone(),
242 value,
243 })
244 }
245
246 fn row_at(&self, display_row: usize) -> Option<SelectedRowContext> {
249 let &source_row_index = self.display_indices.get(display_row)?;
250 let values = self.rows.get(source_row_index)?.clone();
251 Some(SelectedRowContext {
252 display_row_index: display_row,
253 source_row_index,
254 values,
255 columns: self.columns.to_vec(),
256 })
257 }
258
259 #[must_use]
262 pub fn clicked_cell(&self) -> Option<SelectedCellContext> {
263 match self.target {
264 ContextMenuTarget::Cell {
265 display_row_index,
266 column_index,
267 ..
268 } => self.cell_at(display_row_index, column_index),
269 _ => None,
270 }
271 }
272
273 #[must_use]
276 pub fn clicked_row(&self) -> Option<SelectedRowContext> {
277 let row = self.target.display_row_index()?;
278 self.row_at(row)
279 }
280
281 #[must_use]
284 pub fn selected_cell_count(&self) -> usize {
285 self.bounds()
286 .map_or(0, |(r1, c1, r2, c2)| (r2 - r1 + 1) * (c2 - c1 + 1))
287 }
288
289 #[must_use]
292 pub fn selected_row_count(&self) -> usize {
293 if self.column_oriented {
294 return 0;
295 }
296 self.bounds().map_or(0, |(r1, _, r2, _)| r2 - r1 + 1)
297 }
298
299 #[must_use]
303 pub fn is_column_oriented(&self) -> bool {
304 self.column_oriented
305 }
306
307 pub fn for_each_selected_cell(&self, mut f: impl FnMut(SelectedCellContext)) {
310 let Some((r1, c1, r2, c2)) = self.bounds() else {
311 return;
312 };
313 for dr in r1..=r2 {
314 for c in c1..=c2 {
315 if let Some(cell) = self.cell_at(dr, c) {
316 f(cell);
317 }
318 }
319 }
320 }
321
322 pub fn for_each_selected_row(&self, mut f: impl FnMut(SelectedRowContext)) {
326 if self.column_oriented {
327 return;
328 }
329 let Some((r1, _, r2, _)) = self.bounds() else {
330 return;
331 };
332 for dr in r1..=r2 {
333 if let Some(r) = self.row_at(dr) {
334 f(r);
335 }
336 }
337 }
338
339 #[must_use]
343 pub fn selected_cells(&self) -> Vec<SelectedCellContext> {
344 let mut out = Vec::with_capacity(self.selected_cell_count());
345 self.for_each_selected_cell(|c| out.push(c));
346 out
347 }
348
349 #[must_use]
353 pub fn selected_rows(&self) -> Vec<SelectedRowContext> {
354 let mut out = Vec::with_capacity(self.selected_row_count());
355 self.for_each_selected_row(|r| out.push(r));
356 out
357 }
358
359 #[must_use]
363 pub fn for_test(
364 target: ContextMenuTarget,
365 selection: Option<ContextMenuSelection>,
366 rows: Vec<Vec<CellValue>>,
367 columns: Vec<ColumnContext>,
368 ) -> Self {
369 let display_indices: Vec<usize> = (0..rows.len()).collect();
370 Self {
371 target,
372 selection,
373 rows: Arc::new(rows),
374 display_indices: Arc::new(display_indices),
375 columns: columns.into(),
376 column_oriented: false,
377 }
378 }
379}
380
381#[derive(Clone, Debug)]
384pub enum ContextMenuItem {
385 BuiltIn(MenuAction),
388 Action { id: String, label: String },
390 Separator,
392}
393
394impl ContextMenuItem {
395 #[must_use]
397 pub fn action(id: impl Into<String>, label: impl Into<String>) -> Self {
398 Self::Action {
399 id: id.into(),
400 label: label.into(),
401 }
402 }
403
404 #[must_use]
406 pub fn separator() -> Self {
407 Self::Separator
408 }
409
410 #[must_use]
414 pub fn standard_column_header_items() -> Vec<Self> {
415 vec![
416 Self::BuiltIn(MenuAction::SelectColumn),
417 Self::BuiltIn(MenuAction::CopyColumn),
418 Self::BuiltIn(MenuAction::CopyColumnWithHeaders),
419 Self::Separator,
420 Self::BuiltIn(MenuAction::SortAscending),
421 Self::BuiltIn(MenuAction::SortDescending),
422 Self::BuiltIn(MenuAction::ClearSort),
423 Self::Separator,
424 Self::BuiltIn(MenuAction::GroupBy),
425 Self::BuiltIn(MenuAction::ClearGrouping),
426 Self::Separator,
427 Self::BuiltIn(MenuAction::FilterPrompt),
428 Self::BuiltIn(MenuAction::ClearFilter),
429 ]
430 }
431}
432
433pub trait ContextMenuProvider: 'static {
447 fn menu_items(&self, request: &ContextMenuRequest) -> Vec<ContextMenuItem>;
449
450 #[allow(unused_variables)]
455 fn on_action(
456 &self,
457 action_id: &str,
458 request: &ContextMenuRequest,
459 state: &mut GridState,
460 cx: &mut gpui::App,
461 ) {
462 }
463}
464
465#[derive(Clone)]
468pub(crate) struct ContextMenuProviderHandle(Arc<dyn ContextMenuProvider>);
469
470impl ContextMenuProviderHandle {
471 pub(crate) fn new(provider: impl ContextMenuProvider + 'static) -> Self {
472 Self(Arc::new(provider))
473 }
474}
475
476impl fmt::Debug for ContextMenuProviderHandle {
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 f.debug_struct("ContextMenuProviderHandle")
479 .finish_non_exhaustive()
480 }
481}
482
483impl std::ops::Deref for ContextMenuProviderHandle {
484 type Target = dyn ContextMenuProvider;
485
486 fn deref(&self) -> &Self::Target {
487 &*self.0
488 }
489}
490
491#[derive(Clone, Debug)]
493pub(crate) struct PendingCustomContextMenuAction {
494 pub id: String,
495 pub request: ContextMenuRequest,
496}
497
498#[cfg(test)]
499#[allow(clippy::unwrap_used, clippy::expect_used)]
500mod tests {
501 use super::*;
502
503 fn row(name: &str, values: &[CellValue]) -> SelectedRowContext {
504 let columns = vec![
505 ColumnContext {
506 index: 0,
507 name: "id".into(),
508 kind: ColumnKind::Integer,
509 },
510 ColumnContext {
511 index: 1,
512 name: name.into(),
513 kind: ColumnKind::Text,
514 },
515 ];
516 SelectedRowContext {
517 display_row_index: 0,
518 source_row_index: 0,
519 values: values.to_vec(),
520 columns,
521 }
522 }
523
524 #[test]
525 fn value_at_returns_by_ordinal() {
526 let r = row(
527 "name",
528 &[CellValue::Integer(7), CellValue::Text("hi".into())],
529 );
530 assert_eq!(r.value_at(0), Some(&CellValue::Integer(7)));
531 assert_eq!(r.value_at(1), Some(&CellValue::Text("hi".into())));
532 assert_eq!(r.value_at(2), None);
533 }
534
535 #[test]
536 fn value_by_name_exact_case_sensitive() {
537 let r = row(
538 "Name",
539 &[CellValue::Integer(7), CellValue::Text("hi".into())],
540 );
541 assert_eq!(r.value_by_name("Name"), Some(&CellValue::Text("hi".into())));
542 assert_eq!(r.value_by_name("name"), None);
543 assert_eq!(r.value_by_name("NAME"), None);
544 }
545
546 #[test]
547 fn value_by_name_first_duplicate_wins() {
548 let columns = vec![
549 ColumnContext {
550 index: 0,
551 name: "dup".into(),
552 kind: ColumnKind::Integer,
553 },
554 ColumnContext {
555 index: 1,
556 name: "dup".into(),
557 kind: ColumnKind::Integer,
558 },
559 ];
560 let r = SelectedRowContext {
561 display_row_index: 0,
562 source_row_index: 0,
563 values: vec![CellValue::Integer(1), CellValue::Integer(2)],
564 columns,
565 };
566 assert_eq!(r.value_by_name("dup"), Some(&CellValue::Integer(1)));
567 assert_eq!(r.column_index("dup"), Some(0));
568 }
569
570 #[test]
571 fn named_values_iterates_all_columns() {
572 let r = row(
573 "name",
574 &[CellValue::Integer(7), CellValue::Text("hi".into())],
575 );
576 let pairs: Vec<_> = r.named_values().collect();
577 assert_eq!(pairs.len(), 2);
578 assert_eq!(pairs[0].0, "id");
579 assert_eq!(pairs[0].1, &CellValue::Integer(7));
580 assert_eq!(pairs[1].0, "name");
581 assert_eq!(pairs[1].1, &CellValue::Text("hi".into()));
582 }
583
584 #[test]
585 fn context_menu_target_column_index() {
586 assert_eq!(
587 ContextMenuTarget::Cell {
588 display_row_index: 0,
589 source_row_index: 0,
590 column_index: 3
591 }
592 .column_index(),
593 Some(3)
594 );
595 assert_eq!(
596 ContextMenuTarget::RowHeader {
597 display_row_index: 0,
598 source_row_index: 0
599 }
600 .column_index(),
601 None
602 );
603 }
604
605 #[test]
606 fn context_menu_target_display_row_index() {
607 assert_eq!(
608 ContextMenuTarget::Cell {
609 display_row_index: 5,
610 source_row_index: 2,
611 column_index: 0
612 }
613 .display_row_index(),
614 Some(5)
615 );
616 assert_eq!(
617 ContextMenuTarget::ColumnHeader { column_index: 1 }.display_row_index(),
618 None
619 );
620 }
621
622 #[test]
623 fn standard_column_header_items_match_builtin_order() {
624 let items = ContextMenuItem::standard_column_header_items();
625 assert_eq!(items.len(), 13);
626 assert!(matches!(
627 items[0],
628 ContextMenuItem::BuiltIn(MenuAction::SelectColumn)
629 ));
630 assert!(matches!(items[3], ContextMenuItem::Separator));
631 assert!(matches!(
632 items[12],
633 ContextMenuItem::BuiltIn(MenuAction::ClearFilter)
634 ));
635 }
636
637 fn cols() -> Arc<[ColumnContext]> {
638 Arc::from(vec![
639 ColumnContext {
640 index: 0,
641 name: "a".into(),
642 kind: ColumnKind::Integer,
643 },
644 ColumnContext {
645 index: 1,
646 name: "b".into(),
647 kind: ColumnKind::Text,
648 },
649 ])
650 }
651
652 fn sel(r1: usize, c1: usize, r2: usize, c2: usize) -> ContextMenuSelection {
653 ContextMenuSelection {
654 row_start: r1,
655 row_end: r2,
656 column_start: c1,
657 column_end: c2,
658 }
659 }
660
661 #[test]
662 fn clicked_cell_finds_target_cell() {
663 let rows = Arc::new(vec![
664 vec![CellValue::Integer(1), CellValue::Text("x".into())],
665 vec![CellValue::Integer(2), CellValue::Text("y".into())],
666 vec![CellValue::Integer(3), CellValue::Text("z".into())],
667 ]);
668 let display = Arc::new(vec![0usize, 2usize]);
670 let request = ContextMenuRequest::new(
671 ContextMenuTarget::Cell {
672 display_row_index: 1,
673 source_row_index: 2,
674 column_index: 0,
675 },
676 Some(sel(0, 0, 1, 1)),
677 rows,
678 display,
679 cols(),
680 false,
681 );
682 let clicked = request.clicked_cell().unwrap();
683 assert_eq!(clicked.source_row_index, 2);
684 assert_eq!(clicked.value, CellValue::Integer(3));
685 }
686
687 #[test]
688 fn clicked_cell_none_for_column_header_target() {
689 let request = ContextMenuRequest::new(
690 ContextMenuTarget::ColumnHeader { column_index: 0 },
691 None,
692 Arc::new(vec![]),
693 Arc::new(vec![]),
694 cols(),
695 true,
696 );
697 assert!(request.clicked_cell().is_none());
698 }
699
700 #[test]
701 fn clicked_row_finds_target_for_row_header() {
702 let rows = Arc::new(vec![
703 vec![CellValue::Integer(1), CellValue::Text("x".into())],
704 vec![CellValue::Integer(2), CellValue::Text("y".into())],
705 vec![CellValue::Integer(3), CellValue::Text("z".into())],
706 ]);
707 let display = Arc::new(vec![0usize, 2usize]);
708 let request = ContextMenuRequest::new(
709 ContextMenuTarget::RowHeader {
710 display_row_index: 1,
711 source_row_index: 2,
712 },
713 Some(sel(0, 0, 1, 1)),
714 rows,
715 display,
716 cols(),
717 false,
718 );
719 let clicked = request.clicked_row().unwrap();
720 assert_eq!(clicked.source_row_index, 2);
721 assert_eq!(
722 clicked.values,
723 vec![CellValue::Integer(3), CellValue::Text("z".into())]
724 );
725 }
726
727 #[test]
728 fn clicked_row_none_for_column_header() {
729 let request = ContextMenuRequest::new(
730 ContextMenuTarget::ColumnHeader { column_index: 0 },
731 None,
732 Arc::new(vec![]),
733 Arc::new(vec![]),
734 cols(),
735 true,
736 );
737 assert!(request.clicked_row().is_none());
738 }
739
740 #[test]
741 fn counts_are_computed_from_bounds() {
742 let rows = Arc::new(vec![
743 vec![CellValue::Integer(1), CellValue::Text("x".into())],
744 vec![CellValue::Integer(2), CellValue::Text("y".into())],
745 ]);
746 let display = Arc::new(vec![0usize, 1usize]);
747 let request = ContextMenuRequest::new(
748 ContextMenuTarget::Cell {
749 display_row_index: 0,
750 source_row_index: 0,
751 column_index: 0,
752 },
753 Some(sel(0, 0, 1, 1)),
754 rows,
755 display,
756 cols(),
757 false,
758 );
759 assert_eq!(request.selected_cell_count(), 4);
760 assert_eq!(request.selected_row_count(), 2);
761 assert_eq!(request.selected_cells().len(), 4);
762 assert_eq!(request.selected_rows().len(), 2);
763 }
764
765 #[test]
766 fn column_oriented_has_no_rows() {
767 let rows = Arc::new(vec![
768 vec![CellValue::Integer(1), CellValue::Text("x".into())],
769 vec![CellValue::Integer(2), CellValue::Text("y".into())],
770 ]);
771 let display = Arc::new(vec![0usize, 1usize]);
772 let request = ContextMenuRequest::new(
773 ContextMenuTarget::ColumnHeader { column_index: 0 },
774 Some(sel(0, 0, 1, 0)),
775 rows,
776 display,
777 cols(),
778 true,
779 );
780 assert_eq!(request.selected_row_count(), 0);
781 assert!(request.selected_rows().is_empty());
782 assert_eq!(request.selected_cell_count(), 2);
784 assert_eq!(request.selected_cells().len(), 2);
785 }
786}