use std::fmt;
use std::sync::Arc;
use crate::data::{CellValue, ColumnKind};
use crate::grid::menu::MenuAction;
use crate::grid::state::GridState;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ContextMenuTarget {
Cell {
display_row_index: usize,
source_row_index: usize,
column_index: usize,
},
RowHeader {
display_row_index: usize,
source_row_index: usize,
},
ColumnHeader { column_index: usize },
SortButton { column_index: usize },
}
impl ContextMenuTarget {
#[must_use]
pub fn column_index(&self) -> Option<usize> {
match self {
Self::Cell { column_index, .. } => Some(*column_index),
Self::ColumnHeader { column_index } => Some(*column_index),
Self::SortButton { column_index } => Some(*column_index),
Self::RowHeader { .. } => None,
}
}
#[must_use]
pub fn display_row_index(&self) -> Option<usize> {
match self {
Self::Cell {
display_row_index, ..
} => Some(*display_row_index),
Self::RowHeader {
display_row_index, ..
} => Some(*display_row_index),
Self::ColumnHeader { .. } | Self::SortButton { .. } => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContextMenuSelection {
pub row_start: usize,
pub row_end: usize,
pub column_start: usize,
pub column_end: usize,
}
#[derive(Clone, Debug)]
pub struct SelectedCellContext {
pub display_row_index: usize,
pub source_row_index: usize,
pub column_index: usize,
pub column_name: String,
pub value: CellValue,
}
#[derive(Clone, Debug)]
pub struct ColumnContext {
pub index: usize,
pub name: String,
pub kind: ColumnKind,
}
#[derive(Clone, Debug)]
pub struct SelectedRowContext {
pub display_row_index: usize,
pub source_row_index: usize,
pub values: Vec<CellValue>,
pub columns: Vec<ColumnContext>,
}
impl SelectedRowContext {
#[must_use]
pub fn value_at(&self, column_index: usize) -> Option<&CellValue> {
self.values.get(column_index)
}
#[must_use]
pub fn value_by_name(&self, column_name: &str) -> Option<&CellValue> {
self.column_index(column_name)
.and_then(|i| self.values.get(i))
}
pub fn named_values(&self) -> impl Iterator<Item = (&str, &CellValue)> {
self.columns
.iter()
.filter_map(move |col| self.values.get(col.index).map(|v| (col.name.as_str(), v)))
}
#[must_use]
pub fn column_index(&self, column_name: &str) -> Option<usize> {
self.columns
.iter()
.find(|c| c.name == column_name)
.map(|c| c.index)
}
}
#[derive(Clone)]
pub struct ContextMenuRequest {
pub target: ContextMenuTarget,
pub selection: Option<ContextMenuSelection>,
rows: Arc<Vec<Vec<CellValue>>>,
display_indices: Arc<Vec<usize>>,
columns: Arc<[ColumnContext]>,
column_oriented: bool,
}
impl fmt::Debug for ContextMenuRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ContextMenuRequest")
.field("target", &self.target)
.field("selection", &self.selection)
.field("column_oriented", &self.column_oriented)
.field("selected_cell_count", &self.selected_cell_count())
.field("selected_row_count", &self.selected_row_count())
.finish_non_exhaustive()
}
}
impl ContextMenuRequest {
pub(crate) fn new(
target: ContextMenuTarget,
selection: Option<ContextMenuSelection>,
rows: Arc<Vec<Vec<CellValue>>>,
display_indices: Arc<Vec<usize>>,
columns: Arc<[ColumnContext]>,
column_oriented: bool,
) -> Self {
Self {
target,
selection,
rows,
display_indices,
columns,
column_oriented,
}
}
fn bounds(&self) -> Option<(usize, usize, usize, usize)> {
self.selection.as_ref().map(|s| {
(
s.row_start,
s.column_start,
s.row_end.min(self.display_indices.len().saturating_sub(1)),
s.column_end.min(self.columns.len().saturating_sub(1)),
)
})
}
fn cell_at(&self, display_row: usize, column: usize) -> Option<SelectedCellContext> {
let &source_row_index = self.display_indices.get(display_row)?;
let value = self.rows.get(source_row_index)?.get(column)?.clone();
let col = self.columns.get(column)?;
Some(SelectedCellContext {
display_row_index: display_row,
source_row_index,
column_index: column,
column_name: col.name.clone(),
value,
})
}
fn row_at(&self, display_row: usize) -> Option<SelectedRowContext> {
let &source_row_index = self.display_indices.get(display_row)?;
let values = self.rows.get(source_row_index)?.clone();
Some(SelectedRowContext {
display_row_index: display_row,
source_row_index,
values,
columns: self.columns.to_vec(),
})
}
#[must_use]
pub fn clicked_cell(&self) -> Option<SelectedCellContext> {
match self.target {
ContextMenuTarget::Cell {
display_row_index,
column_index,
..
} => self.cell_at(display_row_index, column_index),
_ => None,
}
}
#[must_use]
pub fn clicked_row(&self) -> Option<SelectedRowContext> {
let row = self.target.display_row_index()?;
self.row_at(row)
}
#[must_use]
pub fn selected_cell_count(&self) -> usize {
self.bounds()
.map_or(0, |(r1, c1, r2, c2)| (r2 - r1 + 1) * (c2 - c1 + 1))
}
#[must_use]
pub fn selected_row_count(&self) -> usize {
if self.column_oriented {
return 0;
}
self.bounds().map_or(0, |(r1, _, r2, _)| r2 - r1 + 1)
}
#[must_use]
pub fn is_column_oriented(&self) -> bool {
self.column_oriented
}
pub fn for_each_selected_cell(&self, mut f: impl FnMut(SelectedCellContext)) {
let Some((r1, c1, r2, c2)) = self.bounds() else {
return;
};
for dr in r1..=r2 {
for c in c1..=c2 {
if let Some(cell) = self.cell_at(dr, c) {
f(cell);
}
}
}
}
pub fn for_each_selected_row(&self, mut f: impl FnMut(SelectedRowContext)) {
if self.column_oriented {
return;
}
let Some((r1, _, r2, _)) = self.bounds() else {
return;
};
for dr in r1..=r2 {
if let Some(r) = self.row_at(dr) {
f(r);
}
}
}
#[must_use]
pub fn selected_cells(&self) -> Vec<SelectedCellContext> {
let mut out = Vec::with_capacity(self.selected_cell_count());
self.for_each_selected_cell(|c| out.push(c));
out
}
#[must_use]
pub fn selected_rows(&self) -> Vec<SelectedRowContext> {
let mut out = Vec::with_capacity(self.selected_row_count());
self.for_each_selected_row(|r| out.push(r));
out
}
}
#[derive(Clone, Debug)]
pub enum ContextMenuItem {
BuiltIn(MenuAction),
Action { id: String, label: String },
Separator,
}
impl ContextMenuItem {
#[must_use]
pub fn action(id: impl Into<String>, label: impl Into<String>) -> Self {
Self::Action {
id: id.into(),
label: label.into(),
}
}
#[must_use]
pub fn separator() -> Self {
Self::Separator
}
#[must_use]
pub fn standard_column_header_items() -> Vec<Self> {
vec![
Self::BuiltIn(MenuAction::SelectColumn),
Self::BuiltIn(MenuAction::CopyColumn),
Self::BuiltIn(MenuAction::CopyColumnWithHeaders),
Self::Separator,
Self::BuiltIn(MenuAction::SortAscending),
Self::BuiltIn(MenuAction::SortDescending),
Self::BuiltIn(MenuAction::ClearSort),
Self::Separator,
Self::BuiltIn(MenuAction::FilterPrompt),
Self::BuiltIn(MenuAction::ClearFilter),
]
}
}
pub trait ContextMenuProvider: 'static {
fn menu_items(&self, request: &ContextMenuRequest) -> Vec<ContextMenuItem>;
#[allow(unused_variables)]
fn on_action(
&self,
action_id: &str,
request: &ContextMenuRequest,
state: &mut GridState,
cx: &mut gpui::App,
) {
}
}
#[derive(Clone)]
pub(crate) struct ContextMenuProviderHandle(Arc<dyn ContextMenuProvider>);
impl ContextMenuProviderHandle {
pub(crate) fn new(provider: impl ContextMenuProvider + 'static) -> Self {
Self(Arc::new(provider))
}
}
impl fmt::Debug for ContextMenuProviderHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ContextMenuProviderHandle")
.finish_non_exhaustive()
}
}
impl std::ops::Deref for ContextMenuProviderHandle {
type Target = dyn ContextMenuProvider;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
#[derive(Clone, Debug)]
pub(crate) struct PendingCustomContextMenuAction {
pub id: String,
pub request: ContextMenuRequest,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn row(name: &str, values: &[CellValue]) -> SelectedRowContext {
let columns = vec![
ColumnContext {
index: 0,
name: "id".into(),
kind: ColumnKind::Integer,
},
ColumnContext {
index: 1,
name: name.into(),
kind: ColumnKind::Text,
},
];
SelectedRowContext {
display_row_index: 0,
source_row_index: 0,
values: values.to_vec(),
columns,
}
}
#[test]
fn value_at_returns_by_ordinal() {
let r = row(
"name",
&[CellValue::Integer(7), CellValue::Text("hi".into())],
);
assert_eq!(r.value_at(0), Some(&CellValue::Integer(7)));
assert_eq!(r.value_at(1), Some(&CellValue::Text("hi".into())));
assert_eq!(r.value_at(2), None);
}
#[test]
fn value_by_name_exact_case_sensitive() {
let r = row(
"Name",
&[CellValue::Integer(7), CellValue::Text("hi".into())],
);
assert_eq!(r.value_by_name("Name"), Some(&CellValue::Text("hi".into())));
assert_eq!(r.value_by_name("name"), None);
assert_eq!(r.value_by_name("NAME"), None);
}
#[test]
fn value_by_name_first_duplicate_wins() {
let columns = vec![
ColumnContext {
index: 0,
name: "dup".into(),
kind: ColumnKind::Integer,
},
ColumnContext {
index: 1,
name: "dup".into(),
kind: ColumnKind::Integer,
},
];
let r = SelectedRowContext {
display_row_index: 0,
source_row_index: 0,
values: vec![CellValue::Integer(1), CellValue::Integer(2)],
columns,
};
assert_eq!(r.value_by_name("dup"), Some(&CellValue::Integer(1)));
assert_eq!(r.column_index("dup"), Some(0));
}
#[test]
fn named_values_iterates_all_columns() {
let r = row(
"name",
&[CellValue::Integer(7), CellValue::Text("hi".into())],
);
let pairs: Vec<_> = r.named_values().collect();
assert_eq!(pairs.len(), 2);
assert_eq!(pairs[0].0, "id");
assert_eq!(pairs[0].1, &CellValue::Integer(7));
assert_eq!(pairs[1].0, "name");
assert_eq!(pairs[1].1, &CellValue::Text("hi".into()));
}
#[test]
fn context_menu_target_column_index() {
assert_eq!(
ContextMenuTarget::Cell {
display_row_index: 0,
source_row_index: 0,
column_index: 3
}
.column_index(),
Some(3)
);
assert_eq!(
ContextMenuTarget::RowHeader {
display_row_index: 0,
source_row_index: 0
}
.column_index(),
None
);
}
#[test]
fn context_menu_target_display_row_index() {
assert_eq!(
ContextMenuTarget::Cell {
display_row_index: 5,
source_row_index: 2,
column_index: 0
}
.display_row_index(),
Some(5)
);
assert_eq!(
ContextMenuTarget::ColumnHeader { column_index: 1 }.display_row_index(),
None
);
}
#[test]
fn standard_column_header_items_match_builtin_order() {
let items = ContextMenuItem::standard_column_header_items();
assert_eq!(items.len(), 10);
assert!(matches!(
items[0],
ContextMenuItem::BuiltIn(MenuAction::SelectColumn)
));
assert!(matches!(items[3], ContextMenuItem::Separator));
assert!(matches!(
items[9],
ContextMenuItem::BuiltIn(MenuAction::ClearFilter)
));
}
fn cols() -> Arc<[ColumnContext]> {
Arc::from(vec![
ColumnContext {
index: 0,
name: "a".into(),
kind: ColumnKind::Integer,
},
ColumnContext {
index: 1,
name: "b".into(),
kind: ColumnKind::Text,
},
])
}
fn sel(r1: usize, c1: usize, r2: usize, c2: usize) -> ContextMenuSelection {
ContextMenuSelection {
row_start: r1,
row_end: r2,
column_start: c1,
column_end: c2,
}
}
#[test]
fn clicked_cell_finds_target_cell() {
let rows = Arc::new(vec![
vec![CellValue::Integer(1), CellValue::Text("x".into())],
vec![CellValue::Integer(2), CellValue::Text("y".into())],
vec![CellValue::Integer(3), CellValue::Text("z".into())],
]);
let display = Arc::new(vec![0usize, 2usize]);
let request = ContextMenuRequest::new(
ContextMenuTarget::Cell {
display_row_index: 1,
source_row_index: 2,
column_index: 0,
},
Some(sel(0, 0, 1, 1)),
rows,
display,
cols(),
false,
);
let clicked = request.clicked_cell().unwrap();
assert_eq!(clicked.source_row_index, 2);
assert_eq!(clicked.value, CellValue::Integer(3));
}
#[test]
fn clicked_cell_none_for_column_header_target() {
let request = ContextMenuRequest::new(
ContextMenuTarget::ColumnHeader { column_index: 0 },
None,
Arc::new(vec![]),
Arc::new(vec![]),
cols(),
true,
);
assert!(request.clicked_cell().is_none());
}
#[test]
fn clicked_row_finds_target_for_row_header() {
let rows = Arc::new(vec![
vec![CellValue::Integer(1), CellValue::Text("x".into())],
vec![CellValue::Integer(2), CellValue::Text("y".into())],
vec![CellValue::Integer(3), CellValue::Text("z".into())],
]);
let display = Arc::new(vec![0usize, 2usize]);
let request = ContextMenuRequest::new(
ContextMenuTarget::RowHeader {
display_row_index: 1,
source_row_index: 2,
},
Some(sel(0, 0, 1, 1)),
rows,
display,
cols(),
false,
);
let clicked = request.clicked_row().unwrap();
assert_eq!(clicked.source_row_index, 2);
assert_eq!(
clicked.values,
vec![CellValue::Integer(3), CellValue::Text("z".into())]
);
}
#[test]
fn clicked_row_none_for_column_header() {
let request = ContextMenuRequest::new(
ContextMenuTarget::ColumnHeader { column_index: 0 },
None,
Arc::new(vec![]),
Arc::new(vec![]),
cols(),
true,
);
assert!(request.clicked_row().is_none());
}
#[test]
fn counts_are_computed_from_bounds() {
let rows = Arc::new(vec![
vec![CellValue::Integer(1), CellValue::Text("x".into())],
vec![CellValue::Integer(2), CellValue::Text("y".into())],
]);
let display = Arc::new(vec![0usize, 1usize]);
let request = ContextMenuRequest::new(
ContextMenuTarget::Cell {
display_row_index: 0,
source_row_index: 0,
column_index: 0,
},
Some(sel(0, 0, 1, 1)),
rows,
display,
cols(),
false,
);
assert_eq!(request.selected_cell_count(), 4);
assert_eq!(request.selected_row_count(), 2);
assert_eq!(request.selected_cells().len(), 4);
assert_eq!(request.selected_rows().len(), 2);
}
#[test]
fn column_oriented_has_no_rows() {
let rows = Arc::new(vec![
vec![CellValue::Integer(1), CellValue::Text("x".into())],
vec![CellValue::Integer(2), CellValue::Text("y".into())],
]);
let display = Arc::new(vec![0usize, 1usize]);
let request = ContextMenuRequest::new(
ContextMenuTarget::ColumnHeader { column_index: 0 },
Some(sel(0, 0, 1, 0)),
rows,
display,
cols(),
true,
);
assert_eq!(request.selected_row_count(), 0);
assert!(request.selected_rows().is_empty());
assert_eq!(request.selected_cell_count(), 2);
assert_eq!(request.selected_cells().len(), 2);
}
}