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