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 data-row display indices (post sort/filter) unless prefixed
163/// with `source_`. When flat-grid grouping is active, visual section headers
164/// are omitted and the visible data rows are compacted for this snapshot.
165///
166/// For column-oriented targets (`ColumnHeader`, `SortButton`, or a
167/// `Selection::Column`), the row accessors are empty — a column right-click is
168/// column-oriented (`clicked_row()` is `None`), so the column's values are
169/// exposed through the cell accessors and full per-row snapshots are skipped.
170///
171/// The type is `Send + Sync + 'static` (it holds only `Arc`s and `Copy`
172/// bounds), so a clone can be moved into a background task.
173#[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    /// Construct a lazy request. Internal: the widget builds this at
197    /// menu-open time from the grid's shared state.
198    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    /// The inclusive selection bounds as `(row_start, col_start, row_end,
217    /// col_end)` in display/column space, or `None` when there is no
218    /// selection.
219    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    /// Build the [`SelectedCellContext`] at a given display row / column,
231    /// resolving the source row through the display order. `None` if out of
232    /// bounds.
233    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    /// Build the [`SelectedRowContext`] for a given display row. `None` if out
247    /// of bounds.
248    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    /// The specific cell under the cursor when the menu opened, if the
260    /// right-click landed on a data cell. Cheap (clones one cell).
261    #[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    /// The row under the cursor when the menu opened, if the right-click
274    /// landed on a cell or row header. Cheap (clones one row).
275    #[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    /// Number of cells in the effective selection. O(1) — computed from the
282    /// selection bounds without materializing anything.
283    #[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    /// Number of rows in the effective selection. `0` for column-oriented
290    /// targets. O(1).
291    #[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    /// Whether this request is column-oriented (a column-header/sort-button
300    /// right-click or a `Selection::Column`), in which case the row accessors
301    /// are empty.
302    #[must_use]
303    pub fn is_column_oriented(&self) -> bool {
304        self.column_oriented
305    }
306
307    /// Stream every selected cell without allocating an intermediate `Vec`.
308    /// Prefer this in background work.
309    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    /// Stream every selected row without allocating an intermediate `Vec`.
323    /// Yields nothing for column-oriented targets. Prefer this in background
324    /// work.
325    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    /// All selected cells in the effective selection, materialized into a
340    /// `Vec`. Clones O(cells) owned data — call off the UI thread for large
341    /// selections.
342    #[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    /// All selected rows in the effective selection, materialized into a
350    /// `Vec` (empty for column-oriented targets). Clones O(rows x cols) owned
351    /// data — call off the UI thread for large selections.
352    #[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    /// Construct a request for testing. Builds the internal `Arc` shared
360    /// state from plain owned vectors so downstream test suites can create
361    /// `ContextMenuRequest` instances without the widget.
362    #[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/// Public menu item returned by a [`ContextMenuProvider`]. Distinct from the
382/// internal `MenuItem` used by the rendering pipeline.
383#[derive(Clone, Debug)]
384pub enum ContextMenuItem {
385    /// A built-in action (sort, copy, filter, etc.). Allows providers to
386    /// compose standard column-header actions alongside custom ones.
387    BuiltIn(MenuAction),
388    /// A custom action with a consumer-defined `id` and display label.
389    Action { id: String, label: String },
390    /// A visual separator.
391    Separator,
392}
393
394impl ContextMenuItem {
395    /// Convenience constructor for a custom action.
396    #[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    /// Convenience constructor for a separator.
405    #[must_use]
406    pub fn separator() -> Self {
407        Self::Separator
408    }
409
410    /// The standard built-in column-header menu items, in the same order
411    /// the grid uses when no provider is registered. Providers can prepend
412    /// or append custom items around this list.
413    #[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
433/// Trait implemented by consumers to supply custom right-click menu items
434/// and handle clicks on those items.
435///
436/// The provider is registered on
437/// [`crate::grid::SqllyDataTableBuilder::context_menu_provider`]. When
438/// registered, the provider fully controls the right-click menu for all
439/// targets (cells, row headers, column headers). When no provider is
440/// registered, the built-in column-header menu is used unchanged.
441///
442/// `menu_items` is called only on right-click, so normal render/scroll
443/// performance is unaffected. `on_action` is called when the user clicks a
444/// custom menu item, with `&mut GridState` and `&mut gpui::App` available
445/// for clipboard, selection, or application-level side effects.
446pub trait ContextMenuProvider: 'static {
447    /// Build the menu items for the given right-click context.
448    fn menu_items(&self, request: &ContextMenuRequest) -> Vec<ContextMenuItem>;
449
450    /// Handle a click on a custom action item. `action_id` matches the `id`
451    /// supplied in [`ContextMenuItem::Action`]. Built-in items
452    /// ([`ContextMenuItem::BuiltIn`]) are handled by the grid and do not
453    /// reach this method.
454    #[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/// Type-erased handle wrapping an `Arc<dyn ContextMenuProvider>`. Stored on
466/// `GridState` and cloned into event closures.
467#[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/// Deferred custom context-menu action, flushed at the top of `render`.
492#[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        // display order maps display row 1 -> source row 2
669        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        // cells for the column are still available
783        assert_eq!(request.selected_cell_count(), 2);
784        assert_eq!(request.selected_cells().len(), 2);
785    }
786}