Skip to main content

sqlly_datatable/grid/
context_menu.rs

1//! Public context-menu extensibility types.
2//!
3//! Consumers implement [`ContextMenuProvider`] and register it on
4//! [`crate::grid::SqllyDataTableBuilder`] to fully control the right-click
5//! menu. When a provider is registered the built-in column-header menu is
6//! suppressed; consumers can compose built-in items via
7//! [`ContextMenuItem::standard_column_header_items`].
8//!
9//! The provider receives an owned [`ContextMenuRequest`] snapshot captured
10//! at menu-open time. The snapshot survives until the user clicks a menu
11//! item, so the provider's [`ContextMenuProvider::on_action`] sees exactly
12//! what was selected/right-clicked when the menu opened — even if grid state
13//! (sort, filter, selection) changed in the interim.
14
15use std::fmt;
16use std::sync::Arc;
17
18use crate::data::{CellValue, ColumnKind};
19use crate::grid::menu::MenuAction;
20use crate::grid::state::GridState;
21
22/// What was right-clicked. Maps directly from the grid's hit-test result.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum ContextMenuTarget {
25    /// A data cell.
26    Cell {
27        display_row_index: usize,
28        source_row_index: usize,
29        column_index: usize,
30    },
31    /// The row-number gutter on the left edge.
32    RowHeader {
33        display_row_index: usize,
34        source_row_index: usize,
35    },
36    /// A column header cell (excluding the sort button area).
37    ColumnHeader { column_index: usize },
38    /// The sort/indicator button inside a column header.
39    SortButton { column_index: usize },
40}
41
42impl ContextMenuTarget {
43    /// Returns the column index for targets that carry one, or `None` for
44    /// row-header targets.
45    #[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    /// Returns the display row index for targets that carry one, or `None`
56    /// for column-header/sort-button targets.
57    #[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/// Normalized inclusive selection range captured at menu-open time.
72#[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/// Metadata and value for a single selected cell.
81#[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/// Metadata for a column, included in [`SelectedRowContext`].
91#[derive(Clone, Debug)]
92pub struct ColumnContext {
93    pub index: usize,
94    pub name: String,
95    pub kind: ColumnKind,
96}
97
98/// Full selected row: all cell values plus column metadata for name-based
99/// lookup helpers.
100#[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    /// Value at the given ordinal column index.
110    #[must_use]
111    pub fn value_at(&self, column_index: usize) -> Option<&CellValue> {
112        self.values.get(column_index)
113    }
114
115    /// Value for the first column whose name matches `column_name` exactly
116    /// (case-sensitive). If duplicate names exist, the first match wins.
117    #[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    /// Iterator over `(column_name, value)` pairs for every column in this
124    /// row.
125    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    /// Ordinal column index for the first column whose name matches
132    /// `column_name` exactly (case-sensitive). First duplicate wins.
133    #[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/// Lazy snapshot of the right-click context, captured at menu-open time.
143///
144/// Construction is O(1): the request holds shared ([`Arc`]) handles to the
145/// grid's row data, display order, and column metadata plus the normalized
146/// selection bounds. **No per-cell or per-row data is cloned when the menu
147/// opens**, so right-clicking a huge selection is instant.
148///
149/// The owned per-cell / per-row snapshots are materialized on demand:
150/// - [`clicked_cell`](Self::clicked_cell) / [`clicked_row`](Self::clicked_row)
151///   are cheap (a single cell / row).
152/// - [`for_each_selected_cell`](Self::for_each_selected_cell) /
153///   [`for_each_selected_row`](Self::for_each_selected_row) stream the
154///   selection without allocating a big intermediate `Vec` — prefer these in
155///   background work (e.g. building an export).
156/// - [`selected_cells`](Self::selected_cells) /
157///   [`selected_rows`](Self::selected_rows) collect into a `Vec` for
158///   convenience; these clone O(cells)/O(rows x cols) owned data and should
159///   be called off the UI thread for large selections (see
160///   [`GridState::spawn_background`](crate::grid::GridState::spawn_background)).
161///
162/// All indices are display indices (post sort/filter) unless prefixed with
163/// `source_`.
164///
165/// For column-oriented targets (`ColumnHeader`, `SortButton`, or a
166/// `Selection::Column`), the row accessors are empty — a column right-click is
167/// column-oriented (`clicked_row()` is `None`), so the column's values are
168/// exposed through the cell accessors and full per-row snapshots are skipped.
169///
170/// The type is `Send + Sync + 'static` (it holds only `Arc`s and `Copy`
171/// bounds), so a clone can be moved into a background task.
172#[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    /// Construct a lazy request. Internal: the widget builds this at
196    /// menu-open time from the grid's shared state.
197    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    /// The inclusive selection bounds as `(row_start, col_start, row_end,
216    /// col_end)` in display/column space, or `None` when there is no
217    /// selection.
218    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    /// Build the [`SelectedCellContext`] at a given display row / column,
230    /// resolving the source row through the display order. `None` if out of
231    /// bounds.
232    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    /// Build the [`SelectedRowContext`] for a given display row. `None` if out
246    /// of bounds.
247    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    /// The specific cell under the cursor when the menu opened, if the
259    /// right-click landed on a data cell. Cheap (clones one cell).
260    #[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    /// The row under the cursor when the menu opened, if the right-click
273    /// landed on a cell or row header. Cheap (clones one row).
274    #[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    /// Number of cells in the effective selection. O(1) — computed from the
281    /// selection bounds without materializing anything.
282    #[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    /// Number of rows in the effective selection. `0` for column-oriented
289    /// targets. O(1).
290    #[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    /// Whether this request is column-oriented (a column-header/sort-button
299    /// right-click or a `Selection::Column`), in which case the row accessors
300    /// are empty.
301    #[must_use]
302    pub fn is_column_oriented(&self) -> bool {
303        self.column_oriented
304    }
305
306    /// Stream every selected cell without allocating an intermediate `Vec`.
307    /// Prefer this in background work.
308    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    /// Stream every selected row without allocating an intermediate `Vec`.
322    /// Yields nothing for column-oriented targets. Prefer this in background
323    /// work.
324    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    /// All selected cells in the effective selection, materialized into a
339    /// `Vec`. Clones O(cells) owned data — call off the UI thread for large
340    /// selections.
341    #[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    /// All selected rows in the effective selection, materialized into a
349    /// `Vec` (empty for column-oriented targets). Clones O(rows x cols) owned
350    /// data — call off the UI thread for large selections.
351    #[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/// Public menu item returned by a [`ContextMenuProvider`]. Distinct from the
360/// internal `MenuItem` used by the rendering pipeline.
361#[derive(Clone, Debug)]
362pub enum ContextMenuItem {
363    /// A built-in action (sort, copy, filter, etc.). Allows providers to
364    /// compose standard column-header actions alongside custom ones.
365    BuiltIn(MenuAction),
366    /// A custom action with a consumer-defined `id` and display label.
367    Action { id: String, label: String },
368    /// A visual separator.
369    Separator,
370}
371
372impl ContextMenuItem {
373    /// Convenience constructor for a custom action.
374    #[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    /// Convenience constructor for a separator.
383    #[must_use]
384    pub fn separator() -> Self {
385        Self::Separator
386    }
387
388    /// The standard built-in column-header menu items, in the same order
389    /// the grid uses when no provider is registered. Providers can prepend
390    /// or append custom items around this list.
391    #[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
408/// Trait implemented by consumers to supply custom right-click menu items
409/// and handle clicks on those items.
410///
411/// The provider is registered on
412/// [`crate::grid::SqllyDataTableBuilder::context_menu_provider`]. When
413/// registered, the provider fully controls the right-click menu for all
414/// targets (cells, row headers, column headers). When no provider is
415/// registered, the built-in column-header menu is used unchanged.
416///
417/// `menu_items` is called only on right-click, so normal render/scroll
418/// performance is unaffected. `on_action` is called when the user clicks a
419/// custom menu item, with `&mut GridState` and `&mut gpui::App` available
420/// for clipboard, selection, or application-level side effects.
421pub trait ContextMenuProvider: 'static {
422    /// Build the menu items for the given right-click context.
423    fn menu_items(&self, request: &ContextMenuRequest) -> Vec<ContextMenuItem>;
424
425    /// Handle a click on a custom action item. `action_id` matches the `id`
426    /// supplied in [`ContextMenuItem::Action`]. Built-in items
427    /// ([`ContextMenuItem::BuiltIn`]) are handled by the grid and do not
428    /// reach this method.
429    #[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/// Type-erased handle wrapping an `Arc<dyn ContextMenuProvider>`. Stored on
441/// `GridState` and cloned into event closures.
442#[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/// Deferred custom context-menu action, flushed at the top of `render`.
467#[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        // display order maps display row 1 -> source row 2
644        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        // cells for the column are still available
758        assert_eq!(request.selected_cell_count(), 2);
759        assert_eq!(request.selected_cells().len(), 2);
760    }
761}