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, Debug)]
155pub struct ContextMenuRequest {
156 pub target: ContextMenuTarget,
157 pub selection: Option<ContextMenuSelection>,
158 pub selected_cells: Vec<SelectedCellContext>,
159 pub selected_rows: Vec<SelectedRowContext>,
160}
161
162impl ContextMenuRequest {
163 #[must_use]
166 pub fn clicked_cell(&self) -> Option<&SelectedCellContext> {
167 match &self.target {
168 ContextMenuTarget::Cell {
169 display_row_index,
170 column_index,
171 ..
172 } => self.selected_cells.iter().find(|c| {
173 c.display_row_index == *display_row_index && c.column_index == *column_index
174 }),
175 _ => None,
176 }
177 }
178
179 #[must_use]
182 pub fn clicked_row(&self) -> Option<&SelectedRowContext> {
183 let row = self.target.display_row_index()?;
184 self.selected_rows
185 .iter()
186 .find(|r| r.display_row_index == row)
187 }
188
189 #[must_use]
191 pub fn selected_cells(&self) -> &[SelectedCellContext] {
192 &self.selected_cells
193 }
194
195 #[must_use]
197 pub fn selected_rows(&self) -> &[SelectedRowContext] {
198 &self.selected_rows
199 }
200}
201
202#[derive(Clone, Debug)]
205pub enum ContextMenuItem {
206 BuiltIn(MenuAction),
209 Action { id: String, label: String },
211 Separator,
213}
214
215impl ContextMenuItem {
216 #[must_use]
218 pub fn action(id: impl Into<String>, label: impl Into<String>) -> Self {
219 Self::Action {
220 id: id.into(),
221 label: label.into(),
222 }
223 }
224
225 #[must_use]
227 pub fn separator() -> Self {
228 Self::Separator
229 }
230
231 #[must_use]
235 pub fn standard_column_header_items() -> Vec<Self> {
236 vec![
237 Self::BuiltIn(MenuAction::SelectColumn),
238 Self::BuiltIn(MenuAction::CopyColumn),
239 Self::BuiltIn(MenuAction::CopyColumnWithHeaders),
240 Self::Separator,
241 Self::BuiltIn(MenuAction::SortAscending),
242 Self::BuiltIn(MenuAction::SortDescending),
243 Self::BuiltIn(MenuAction::ClearSort),
244 Self::Separator,
245 Self::BuiltIn(MenuAction::FilterPrompt),
246 Self::BuiltIn(MenuAction::ClearFilter),
247 ]
248 }
249}
250
251pub trait ContextMenuProvider: 'static {
265 fn menu_items(&self, request: &ContextMenuRequest) -> Vec<ContextMenuItem>;
267
268 #[allow(unused_variables)]
273 fn on_action(
274 &self,
275 action_id: &str,
276 request: &ContextMenuRequest,
277 state: &mut GridState,
278 cx: &mut gpui::App,
279 ) {
280 }
281}
282
283#[derive(Clone)]
286pub(crate) struct ContextMenuProviderHandle(Arc<dyn ContextMenuProvider>);
287
288impl ContextMenuProviderHandle {
289 pub(crate) fn new(provider: impl ContextMenuProvider + 'static) -> Self {
290 Self(Arc::new(provider))
291 }
292}
293
294impl fmt::Debug for ContextMenuProviderHandle {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 f.debug_struct("ContextMenuProviderHandle")
297 .finish_non_exhaustive()
298 }
299}
300
301impl std::ops::Deref for ContextMenuProviderHandle {
302 type Target = dyn ContextMenuProvider;
303
304 fn deref(&self) -> &Self::Target {
305 &*self.0
306 }
307}
308
309#[derive(Clone, Debug)]
311pub(crate) struct PendingCustomContextMenuAction {
312 pub id: String,
313 pub request: ContextMenuRequest,
314}
315
316#[cfg(test)]
317#[allow(clippy::unwrap_used, clippy::expect_used)]
318mod tests {
319 use super::*;
320
321 fn row(name: &str, values: &[CellValue]) -> SelectedRowContext {
322 let columns = vec![
323 ColumnContext {
324 index: 0,
325 name: "id".into(),
326 kind: ColumnKind::Integer,
327 },
328 ColumnContext {
329 index: 1,
330 name: name.into(),
331 kind: ColumnKind::Text,
332 },
333 ];
334 SelectedRowContext {
335 display_row_index: 0,
336 source_row_index: 0,
337 values: values.to_vec(),
338 columns,
339 }
340 }
341
342 #[test]
343 fn value_at_returns_by_ordinal() {
344 let r = row(
345 "name",
346 &[CellValue::Integer(7), CellValue::Text("hi".into())],
347 );
348 assert_eq!(r.value_at(0), Some(&CellValue::Integer(7)));
349 assert_eq!(r.value_at(1), Some(&CellValue::Text("hi".into())));
350 assert_eq!(r.value_at(2), None);
351 }
352
353 #[test]
354 fn value_by_name_exact_case_sensitive() {
355 let r = row(
356 "Name",
357 &[CellValue::Integer(7), CellValue::Text("hi".into())],
358 );
359 assert_eq!(r.value_by_name("Name"), Some(&CellValue::Text("hi".into())));
360 assert_eq!(r.value_by_name("name"), None);
361 assert_eq!(r.value_by_name("NAME"), None);
362 }
363
364 #[test]
365 fn value_by_name_first_duplicate_wins() {
366 let columns = vec![
367 ColumnContext {
368 index: 0,
369 name: "dup".into(),
370 kind: ColumnKind::Integer,
371 },
372 ColumnContext {
373 index: 1,
374 name: "dup".into(),
375 kind: ColumnKind::Integer,
376 },
377 ];
378 let r = SelectedRowContext {
379 display_row_index: 0,
380 source_row_index: 0,
381 values: vec![CellValue::Integer(1), CellValue::Integer(2)],
382 columns,
383 };
384 assert_eq!(r.value_by_name("dup"), Some(&CellValue::Integer(1)));
385 assert_eq!(r.column_index("dup"), Some(0));
386 }
387
388 #[test]
389 fn named_values_iterates_all_columns() {
390 let r = row(
391 "name",
392 &[CellValue::Integer(7), CellValue::Text("hi".into())],
393 );
394 let pairs: Vec<_> = r.named_values().collect();
395 assert_eq!(pairs.len(), 2);
396 assert_eq!(pairs[0].0, "id");
397 assert_eq!(pairs[0].1, &CellValue::Integer(7));
398 assert_eq!(pairs[1].0, "name");
399 assert_eq!(pairs[1].1, &CellValue::Text("hi".into()));
400 }
401
402 #[test]
403 fn context_menu_target_column_index() {
404 assert_eq!(
405 ContextMenuTarget::Cell {
406 display_row_index: 0,
407 source_row_index: 0,
408 column_index: 3
409 }
410 .column_index(),
411 Some(3)
412 );
413 assert_eq!(
414 ContextMenuTarget::RowHeader {
415 display_row_index: 0,
416 source_row_index: 0
417 }
418 .column_index(),
419 None
420 );
421 }
422
423 #[test]
424 fn context_menu_target_display_row_index() {
425 assert_eq!(
426 ContextMenuTarget::Cell {
427 display_row_index: 5,
428 source_row_index: 2,
429 column_index: 0
430 }
431 .display_row_index(),
432 Some(5)
433 );
434 assert_eq!(
435 ContextMenuTarget::ColumnHeader { column_index: 1 }.display_row_index(),
436 None
437 );
438 }
439
440 #[test]
441 fn standard_column_header_items_match_builtin_order() {
442 let items = ContextMenuItem::standard_column_header_items();
443 assert_eq!(items.len(), 10);
444 assert!(matches!(
445 items[0],
446 ContextMenuItem::BuiltIn(MenuAction::SelectColumn)
447 ));
448 assert!(matches!(items[3], ContextMenuItem::Separator));
449 assert!(matches!(
450 items[9],
451 ContextMenuItem::BuiltIn(MenuAction::ClearFilter)
452 ));
453 }
454
455 #[test]
456 fn clicked_cell_finds_target_cell() {
457 let request = ContextMenuRequest {
458 target: ContextMenuTarget::Cell {
459 display_row_index: 1,
460 source_row_index: 2,
461 column_index: 0,
462 },
463 selection: None,
464 selected_cells: vec![
465 SelectedCellContext {
466 display_row_index: 0,
467 source_row_index: 0,
468 column_index: 0,
469 column_name: "a".into(),
470 value: CellValue::Integer(1),
471 },
472 SelectedCellContext {
473 display_row_index: 1,
474 source_row_index: 2,
475 column_index: 0,
476 column_name: "a".into(),
477 value: CellValue::Integer(3),
478 },
479 ],
480 selected_rows: vec![],
481 };
482 let clicked = request.clicked_cell().unwrap();
483 assert_eq!(clicked.source_row_index, 2);
484 assert_eq!(clicked.value, CellValue::Integer(3));
485 }
486
487 #[test]
488 fn clicked_cell_none_for_column_header_target() {
489 let request = ContextMenuRequest {
490 target: ContextMenuTarget::ColumnHeader { column_index: 0 },
491 selection: None,
492 selected_cells: vec![],
493 selected_rows: vec![],
494 };
495 assert!(request.clicked_cell().is_none());
496 }
497
498 #[test]
499 fn clicked_row_finds_target_for_row_header() {
500 let request = ContextMenuRequest {
501 target: ContextMenuTarget::RowHeader {
502 display_row_index: 1,
503 source_row_index: 2,
504 },
505 selection: None,
506 selected_cells: vec![],
507 selected_rows: vec![
508 SelectedRowContext {
509 display_row_index: 0,
510 source_row_index: 0,
511 values: vec![],
512 columns: vec![],
513 },
514 SelectedRowContext {
515 display_row_index: 1,
516 source_row_index: 2,
517 values: vec![],
518 columns: vec![],
519 },
520 ],
521 };
522 assert_eq!(request.clicked_row().unwrap().source_row_index, 2);
523 }
524
525 #[test]
526 fn clicked_row_none_for_column_header() {
527 let request = ContextMenuRequest {
528 target: ContextMenuTarget::ColumnHeader { column_index: 0 },
529 selection: None,
530 selected_cells: vec![],
531 selected_rows: vec![],
532 };
533 assert!(request.clicked_row().is_none());
534 }
535}