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
359#[derive(Clone, Debug)]
362pub enum ContextMenuItem {
363 BuiltIn(MenuAction),
366 Action { id: String, label: String },
368 Separator,
370}
371
372impl ContextMenuItem {
373 #[must_use]
375 pub fn action(id: impl Into<String>, label: impl Into<String>) -> Self {
376 Self::Action {
377 id: id.into(),
378 label: label.into(),
379 }
380 }
381
382 #[must_use]
384 pub fn separator() -> Self {
385 Self::Separator
386 }
387
388 #[must_use]
392 pub fn standard_column_header_items() -> Vec<Self> {
393 vec![
394 Self::BuiltIn(MenuAction::SelectColumn),
395 Self::BuiltIn(MenuAction::CopyColumn),
396 Self::BuiltIn(MenuAction::CopyColumnWithHeaders),
397 Self::Separator,
398 Self::BuiltIn(MenuAction::SortAscending),
399 Self::BuiltIn(MenuAction::SortDescending),
400 Self::BuiltIn(MenuAction::ClearSort),
401 Self::Separator,
402 Self::BuiltIn(MenuAction::FilterPrompt),
403 Self::BuiltIn(MenuAction::ClearFilter),
404 ]
405 }
406}
407
408pub trait ContextMenuProvider: 'static {
422 fn menu_items(&self, request: &ContextMenuRequest) -> Vec<ContextMenuItem>;
424
425 #[allow(unused_variables)]
430 fn on_action(
431 &self,
432 action_id: &str,
433 request: &ContextMenuRequest,
434 state: &mut GridState,
435 cx: &mut gpui::App,
436 ) {
437 }
438}
439
440#[derive(Clone)]
443pub(crate) struct ContextMenuProviderHandle(Arc<dyn ContextMenuProvider>);
444
445impl ContextMenuProviderHandle {
446 pub(crate) fn new(provider: impl ContextMenuProvider + 'static) -> Self {
447 Self(Arc::new(provider))
448 }
449}
450
451impl fmt::Debug for ContextMenuProviderHandle {
452 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453 f.debug_struct("ContextMenuProviderHandle")
454 .finish_non_exhaustive()
455 }
456}
457
458impl std::ops::Deref for ContextMenuProviderHandle {
459 type Target = dyn ContextMenuProvider;
460
461 fn deref(&self) -> &Self::Target {
462 &*self.0
463 }
464}
465
466#[derive(Clone, Debug)]
468pub(crate) struct PendingCustomContextMenuAction {
469 pub id: String,
470 pub request: ContextMenuRequest,
471}
472
473#[cfg(test)]
474#[allow(clippy::unwrap_used, clippy::expect_used)]
475mod tests {
476 use super::*;
477
478 fn row(name: &str, values: &[CellValue]) -> SelectedRowContext {
479 let columns = vec![
480 ColumnContext {
481 index: 0,
482 name: "id".into(),
483 kind: ColumnKind::Integer,
484 },
485 ColumnContext {
486 index: 1,
487 name: name.into(),
488 kind: ColumnKind::Text,
489 },
490 ];
491 SelectedRowContext {
492 display_row_index: 0,
493 source_row_index: 0,
494 values: values.to_vec(),
495 columns,
496 }
497 }
498
499 #[test]
500 fn value_at_returns_by_ordinal() {
501 let r = row(
502 "name",
503 &[CellValue::Integer(7), CellValue::Text("hi".into())],
504 );
505 assert_eq!(r.value_at(0), Some(&CellValue::Integer(7)));
506 assert_eq!(r.value_at(1), Some(&CellValue::Text("hi".into())));
507 assert_eq!(r.value_at(2), None);
508 }
509
510 #[test]
511 fn value_by_name_exact_case_sensitive() {
512 let r = row(
513 "Name",
514 &[CellValue::Integer(7), CellValue::Text("hi".into())],
515 );
516 assert_eq!(r.value_by_name("Name"), Some(&CellValue::Text("hi".into())));
517 assert_eq!(r.value_by_name("name"), None);
518 assert_eq!(r.value_by_name("NAME"), None);
519 }
520
521 #[test]
522 fn value_by_name_first_duplicate_wins() {
523 let columns = vec![
524 ColumnContext {
525 index: 0,
526 name: "dup".into(),
527 kind: ColumnKind::Integer,
528 },
529 ColumnContext {
530 index: 1,
531 name: "dup".into(),
532 kind: ColumnKind::Integer,
533 },
534 ];
535 let r = SelectedRowContext {
536 display_row_index: 0,
537 source_row_index: 0,
538 values: vec![CellValue::Integer(1), CellValue::Integer(2)],
539 columns,
540 };
541 assert_eq!(r.value_by_name("dup"), Some(&CellValue::Integer(1)));
542 assert_eq!(r.column_index("dup"), Some(0));
543 }
544
545 #[test]
546 fn named_values_iterates_all_columns() {
547 let r = row(
548 "name",
549 &[CellValue::Integer(7), CellValue::Text("hi".into())],
550 );
551 let pairs: Vec<_> = r.named_values().collect();
552 assert_eq!(pairs.len(), 2);
553 assert_eq!(pairs[0].0, "id");
554 assert_eq!(pairs[0].1, &CellValue::Integer(7));
555 assert_eq!(pairs[1].0, "name");
556 assert_eq!(pairs[1].1, &CellValue::Text("hi".into()));
557 }
558
559 #[test]
560 fn context_menu_target_column_index() {
561 assert_eq!(
562 ContextMenuTarget::Cell {
563 display_row_index: 0,
564 source_row_index: 0,
565 column_index: 3
566 }
567 .column_index(),
568 Some(3)
569 );
570 assert_eq!(
571 ContextMenuTarget::RowHeader {
572 display_row_index: 0,
573 source_row_index: 0
574 }
575 .column_index(),
576 None
577 );
578 }
579
580 #[test]
581 fn context_menu_target_display_row_index() {
582 assert_eq!(
583 ContextMenuTarget::Cell {
584 display_row_index: 5,
585 source_row_index: 2,
586 column_index: 0
587 }
588 .display_row_index(),
589 Some(5)
590 );
591 assert_eq!(
592 ContextMenuTarget::ColumnHeader { column_index: 1 }.display_row_index(),
593 None
594 );
595 }
596
597 #[test]
598 fn standard_column_header_items_match_builtin_order() {
599 let items = ContextMenuItem::standard_column_header_items();
600 assert_eq!(items.len(), 10);
601 assert!(matches!(
602 items[0],
603 ContextMenuItem::BuiltIn(MenuAction::SelectColumn)
604 ));
605 assert!(matches!(items[3], ContextMenuItem::Separator));
606 assert!(matches!(
607 items[9],
608 ContextMenuItem::BuiltIn(MenuAction::ClearFilter)
609 ));
610 }
611
612 fn cols() -> Arc<[ColumnContext]> {
613 Arc::from(vec![
614 ColumnContext {
615 index: 0,
616 name: "a".into(),
617 kind: ColumnKind::Integer,
618 },
619 ColumnContext {
620 index: 1,
621 name: "b".into(),
622 kind: ColumnKind::Text,
623 },
624 ])
625 }
626
627 fn sel(r1: usize, c1: usize, r2: usize, c2: usize) -> ContextMenuSelection {
628 ContextMenuSelection {
629 row_start: r1,
630 row_end: r2,
631 column_start: c1,
632 column_end: c2,
633 }
634 }
635
636 #[test]
637 fn clicked_cell_finds_target_cell() {
638 let rows = Arc::new(vec![
639 vec![CellValue::Integer(1), CellValue::Text("x".into())],
640 vec![CellValue::Integer(2), CellValue::Text("y".into())],
641 vec![CellValue::Integer(3), CellValue::Text("z".into())],
642 ]);
643 let display = Arc::new(vec![0usize, 2usize]);
645 let request = ContextMenuRequest::new(
646 ContextMenuTarget::Cell {
647 display_row_index: 1,
648 source_row_index: 2,
649 column_index: 0,
650 },
651 Some(sel(0, 0, 1, 1)),
652 rows,
653 display,
654 cols(),
655 false,
656 );
657 let clicked = request.clicked_cell().unwrap();
658 assert_eq!(clicked.source_row_index, 2);
659 assert_eq!(clicked.value, CellValue::Integer(3));
660 }
661
662 #[test]
663 fn clicked_cell_none_for_column_header_target() {
664 let request = ContextMenuRequest::new(
665 ContextMenuTarget::ColumnHeader { column_index: 0 },
666 None,
667 Arc::new(vec![]),
668 Arc::new(vec![]),
669 cols(),
670 true,
671 );
672 assert!(request.clicked_cell().is_none());
673 }
674
675 #[test]
676 fn clicked_row_finds_target_for_row_header() {
677 let rows = Arc::new(vec![
678 vec![CellValue::Integer(1), CellValue::Text("x".into())],
679 vec![CellValue::Integer(2), CellValue::Text("y".into())],
680 vec![CellValue::Integer(3), CellValue::Text("z".into())],
681 ]);
682 let display = Arc::new(vec![0usize, 2usize]);
683 let request = ContextMenuRequest::new(
684 ContextMenuTarget::RowHeader {
685 display_row_index: 1,
686 source_row_index: 2,
687 },
688 Some(sel(0, 0, 1, 1)),
689 rows,
690 display,
691 cols(),
692 false,
693 );
694 let clicked = request.clicked_row().unwrap();
695 assert_eq!(clicked.source_row_index, 2);
696 assert_eq!(
697 clicked.values,
698 vec![CellValue::Integer(3), CellValue::Text("z".into())]
699 );
700 }
701
702 #[test]
703 fn clicked_row_none_for_column_header() {
704 let request = ContextMenuRequest::new(
705 ContextMenuTarget::ColumnHeader { column_index: 0 },
706 None,
707 Arc::new(vec![]),
708 Arc::new(vec![]),
709 cols(),
710 true,
711 );
712 assert!(request.clicked_row().is_none());
713 }
714
715 #[test]
716 fn counts_are_computed_from_bounds() {
717 let rows = Arc::new(vec![
718 vec![CellValue::Integer(1), CellValue::Text("x".into())],
719 vec![CellValue::Integer(2), CellValue::Text("y".into())],
720 ]);
721 let display = Arc::new(vec![0usize, 1usize]);
722 let request = ContextMenuRequest::new(
723 ContextMenuTarget::Cell {
724 display_row_index: 0,
725 source_row_index: 0,
726 column_index: 0,
727 },
728 Some(sel(0, 0, 1, 1)),
729 rows,
730 display,
731 cols(),
732 false,
733 );
734 assert_eq!(request.selected_cell_count(), 4);
735 assert_eq!(request.selected_row_count(), 2);
736 assert_eq!(request.selected_cells().len(), 4);
737 assert_eq!(request.selected_rows().len(), 2);
738 }
739
740 #[test]
741 fn column_oriented_has_no_rows() {
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::ColumnHeader { column_index: 0 },
749 Some(sel(0, 0, 1, 0)),
750 rows,
751 display,
752 cols(),
753 true,
754 );
755 assert_eq!(request.selected_row_count(), 0);
756 assert!(request.selected_rows().is_empty());
757 assert_eq!(request.selected_cell_count(), 2);
759 assert_eq!(request.selected_cells().len(), 2);
760 }
761}