#![allow(clippy::expect_used)]
use gpui::{
div, point, px, AppContext, Context, Entity, IntoElement, Modifiers, MouseButton,
ParentElement, Render, Styled, TestAppContext, Window,
};
use sqlly_datatable::{
CellValue, Column, ColumnKind, ContextMenuItem, ContextMenuProvider, ContextMenuRequest,
ContextMenuTarget, GridConfig, GridData, Selection, SqllyDataTable,
};
struct Harness {
grid: Entity<SqllyDataTable>,
}
const PAD_LEFT: f32 = 120.0;
const PAD_TOP: f32 = 200.0;
impl Render for Harness {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.pl(px(PAD_LEFT))
.pt(px(PAD_TOP))
.child(self.grid.clone())
}
}
struct TestProvider;
impl ContextMenuProvider for TestProvider {
fn menu_items(&self, request: &ContextMenuRequest) -> Vec<ContextMenuItem> {
match request.target {
ContextMenuTarget::ColumnHeader { .. } | ContextMenuTarget::SortButton { .. } => {
ContextMenuItem::standard_column_header_items()
}
ContextMenuTarget::Cell { .. } | ContextMenuTarget::RowHeader { .. } => {
vec![ContextMenuItem::action("copy", "Copy")]
}
}
}
}
fn sample() -> GridData {
GridData::new(
vec![
Column {
name: "id".into(),
kind: ColumnKind::Integer,
width: 100.0,
},
Column {
name: "name".into(),
kind: ColumnKind::Text,
width: 200.0,
},
],
(0..30)
.map(|i| vec![CellValue::Integer(i), CellValue::Text(format!("row{i}"))])
.collect(),
)
.expect("rectangular data")
}
#[gpui::test]
fn left_click_selects_single_cell(cx: &mut TestAppContext) {
let (view, cx) = cx.add_window_view(|_window, cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.build(cx)
});
cx.run_until_parked();
let (origin, header_h, row_h) = view.read_with(cx, |v, cx| {
let s = v.state.read(cx);
(s.bounds.origin, s.header_height, s.row_height)
});
let x = f32::from(origin.x) + 90.0;
let y = f32::from(origin.y) + header_h + row_h * 0.5;
cx.simulate_mouse_down(point(px(x), px(y)), MouseButton::Left, Modifiers::none());
cx.simulate_mouse_up(point(px(x), px(y)), MouseButton::Left, Modifiers::none());
cx.run_until_parked();
let selection = view.read_with(cx, |v, cx| v.state.read(cx).selection.clone());
assert_eq!(
selection,
Selection::Cell(0, 0),
"left click inside first data cell should select Cell(0,0), got {selection:?}"
);
}
#[gpui::test]
fn right_click_on_column_header_opens_menu(cx: &mut TestAppContext) {
let (view, cx) = cx.add_window_view(|_window, cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.build(cx)
});
cx.run_until_parked();
let (origin, header_h) = view.read_with(cx, |v, cx| {
let s = v.state.read(cx);
(s.bounds.origin, s.header_height)
});
let x = f32::from(origin.x) + 90.0;
let y = f32::from(origin.y) + header_h * 0.5;
cx.simulate_mouse_down(point(px(x), px(y)), MouseButton::Right, Modifiers::none());
cx.run_until_parked();
let has_menu = view.read_with(cx, |v, cx| v.state.read(cx).context_menu.is_some());
assert!(
has_menu,
"right click on a column header should open the built-in context menu"
);
}
#[gpui::test]
fn right_click_column_header_with_provider_opens_menu(cx: &mut TestAppContext) {
let (view, cx) = cx.add_window_view(|_window, cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.context_menu_provider(TestProvider)
.build(cx)
});
cx.run_until_parked();
let (origin, header_h) = view.read_with(cx, |v, cx| {
let s = v.state.read(cx);
(s.bounds.origin, s.header_height)
});
let x = f32::from(origin.x) + 90.0;
let y = f32::from(origin.y) + header_h * 0.5;
cx.simulate_mouse_down(point(px(x), px(y)), MouseButton::Right, Modifiers::none());
cx.run_until_parked();
let has_menu = view.read_with(cx, |v, cx| v.state.read(cx).context_menu.is_some());
assert!(
has_menu,
"right click on a column header WITH a provider should open the provider menu"
);
}
#[gpui::test]
fn right_click_cell_with_provider_opens_menu(cx: &mut TestAppContext) {
let (view, cx) = cx.add_window_view(|_window, cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.context_menu_provider(TestProvider)
.build(cx)
});
cx.run_until_parked();
let (origin, header_h, row_h) = view.read_with(cx, |v, cx| {
let s = v.state.read(cx);
(s.bounds.origin, s.header_height, s.row_height)
});
let x = f32::from(origin.x) + 90.0;
let y = f32::from(origin.y) + header_h + row_h * 0.5;
cx.simulate_mouse_down(point(px(x), px(y)), MouseButton::Right, Modifiers::none());
cx.run_until_parked();
let has_menu = view.read_with(cx, |v, cx| v.state.read(cx).context_menu.is_some());
assert!(
has_menu,
"right click on a cell WITH a provider should open the provider menu"
);
}
#[gpui::test]
fn keyboard_select_all_after_click(cx: &mut TestAppContext) {
let (view, cx) = cx.add_window_view(|_window, cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.build(cx)
});
cx.run_until_parked();
let (origin, header_h, row_h) = view.read_with(cx, |v, cx| {
let s = v.state.read(cx);
(s.bounds.origin, s.header_height, s.row_height)
});
let x = f32::from(origin.x) + 90.0;
let y = f32::from(origin.y) + header_h + row_h * 0.5;
cx.simulate_click(point(px(x), px(y)), Modifiers::none());
cx.run_until_parked();
cx.simulate_keystrokes("cmd-a");
cx.run_until_parked();
let selection = view.read_with(cx, |v, cx| v.state.read(cx).selection.clone());
assert!(
matches!(selection, Selection::CellRange(0, 0, r, c) if r >= 29 && c >= 1),
"cmd-a should select the whole grid, got {selection:?}"
);
}
#[gpui::test]
fn keyboard_shift_arrow_extends_range(cx: &mut TestAppContext) {
let (view, cx) = cx.add_window_view(|_window, cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.build(cx)
});
cx.run_until_parked();
let (origin, header_h, row_h) = view.read_with(cx, |v, cx| {
let s = v.state.read(cx);
(s.bounds.origin, s.header_height, s.row_height)
});
let x = f32::from(origin.x) + 90.0;
let y = f32::from(origin.y) + header_h + row_h * 0.5;
cx.simulate_click(point(px(x), px(y)), Modifiers::none());
cx.run_until_parked();
cx.simulate_keystrokes("shift-down shift-down");
cx.run_until_parked();
let selection = view.read_with(cx, |v, cx| v.state.read(cx).selection.clone());
assert!(
matches!(selection, Selection::CellRange(0, 0, 2, 0)),
"shift-down twice should extend to CellRange(0,0,2,0), got {selection:?}"
);
}
#[gpui::test]
fn shift_click_selects_cell_range(cx: &mut TestAppContext) {
let (view, cx) = cx.add_window_view(|_window, cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.build(cx)
});
cx.run_until_parked();
let (origin, header_h, row_h) = view.read_with(cx, |v, cx| {
let s = v.state.read(cx);
(s.bounds.origin, s.header_height, s.row_height)
});
let x0 = f32::from(origin.x) + 90.0;
let y0 = f32::from(origin.y) + header_h + row_h * 0.5;
cx.simulate_click(point(px(x0), px(y0)), Modifiers::none());
cx.run_until_parked();
let x1 = f32::from(origin.x) + 50.0 + 100.0 + 90.0;
let y1 = f32::from(origin.y) + header_h + row_h * 2.5;
cx.simulate_mouse_down(point(px(x1), px(y1)), MouseButton::Left, Modifiers::shift());
cx.simulate_mouse_up(point(px(x1), px(y1)), MouseButton::Left, Modifiers::shift());
cx.run_until_parked();
let selection = view.read_with(cx, |v, cx| v.state.read(cx).selection.clone());
assert_eq!(
selection,
Selection::CellRange(0, 0, 2, 1),
"shift-click should extend to CellRange(0,0,2,1), got {selection:?}"
);
}
#[gpui::test]
fn click_column_header_selects_column(cx: &mut TestAppContext) {
let (view, cx) = cx.add_window_view(|_window, cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.build(cx)
});
cx.run_until_parked();
let (origin, header_h) = view.read_with(cx, |v, cx| {
let s = v.state.read(cx);
(s.bounds.origin, s.header_height)
});
let x = f32::from(origin.x) + 50.0 + 100.0 + 90.0;
let y = f32::from(origin.y) + header_h * 0.5;
cx.simulate_click(point(px(x), px(y)), Modifiers::none());
cx.run_until_parked();
let selection = view.read_with(cx, |v, cx| v.state.read(cx).selection.clone());
assert_eq!(
selection,
Selection::Column(1),
"clicking a column header should select that column, got {selection:?}"
);
}
#[gpui::test]
fn click_row_header_selects_row(cx: &mut TestAppContext) {
let (view, cx) = cx.add_window_view(|_window, cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.build(cx)
});
cx.run_until_parked();
let (origin, header_h, row_h) = view.read_with(cx, |v, cx| {
let s = v.state.read(cx);
(s.bounds.origin, s.header_height, s.row_height)
});
let x = f32::from(origin.x) + 25.0;
let y = f32::from(origin.y) + header_h + row_h * 2.5;
cx.simulate_click(point(px(x), px(y)), Modifiers::none());
cx.run_until_parked();
let selection = view.read_with(cx, |v, cx| v.state.read(cx).selection.clone());
assert_eq!(
selection,
Selection::Row(2),
"clicking the row-header gutter should select that row, got {selection:?}"
);
}
#[gpui::test]
fn click_position_is_relative_to_grid_container(cx: &mut TestAppContext) {
let (harness, cx) = cx.add_window_view(|_window, cx| {
let grid = cx.new(|cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.build(cx)
});
Harness { grid }
});
cx.run_until_parked();
let grid = harness.read_with(cx, |h, _cx| h.grid.clone());
let origin = grid.read_with(cx, |g, cx| g.state.read(cx).bounds.origin);
assert!(
f32::from(origin.x) >= PAD_LEFT && f32::from(origin.y) >= PAD_TOP,
"grid should be inset; origin was {origin:?}"
);
let x = f32::from(origin.x) + 8.0;
let y = f32::from(origin.y) + 8.0;
cx.simulate_click(point(px(x), px(y)), Modifiers::none());
cx.run_until_parked();
let click_pos = grid.read_with(cx, |g, cx| g.state.read(cx).click_pos);
assert_eq!(
click_pos,
Some(point(px(8.0), px(8.0))),
"click 8px from the grid's top-left should record grid-relative (8,8), got {click_pos:?}"
);
}
#[gpui::test]
fn cell_selection_correct_with_nonzero_origin(cx: &mut TestAppContext) {
let (harness, cx) = cx.add_window_view(|_window, cx| {
let grid = cx.new(|cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.build(cx)
});
Harness { grid }
});
cx.run_until_parked();
let grid = harness.read_with(cx, |h, _cx| h.grid.clone());
let (origin, header_h, row_h) = grid.read_with(cx, |g, cx| {
let s = g.state.read(cx);
(s.bounds.origin, s.header_height, s.row_height)
});
let x = f32::from(origin.x) + 90.0;
let y = f32::from(origin.y) + header_h + row_h * 0.5;
cx.simulate_click(point(px(x), px(y)), Modifiers::none());
cx.run_until_parked();
let selection = grid.read_with(cx, |g, cx| g.state.read(cx).selection.clone());
assert_eq!(
selection,
Selection::Cell(0, 0),
"cell hit-test must map correctly at a non-zero grid origin, got {selection:?}"
);
}
#[gpui::test]
fn drag_start_is_relative_to_grid_container(cx: &mut TestAppContext) {
let (harness, cx) = cx.add_window_view(|_window, cx| {
let grid = cx.new(|cx| {
SqllyDataTable::builder(sample())
.config(GridConfig::default())
.build(cx)
});
Harness { grid }
});
cx.run_until_parked();
let grid = harness.read_with(cx, |h, _cx| h.grid.clone());
let (origin, header_h, row_h) = grid.read_with(cx, |g, cx| {
let s = g.state.read(cx);
(s.bounds.origin, s.header_height, s.row_height)
});
let rel_x = 90.0;
let rel_y = header_h + row_h * 0.5;
let x = f32::from(origin.x) + rel_x;
let y = f32::from(origin.y) + rel_y;
cx.simulate_mouse_down(point(px(x), px(y)), MouseButton::Left, Modifiers::none());
cx.run_until_parked();
let drag_start = grid.read_with(cx, |g, cx| g.state.read(cx).drag_start);
assert_eq!(
drag_start,
Some(point(px(rel_x), px(rel_y))),
"drag_start should be grid-relative ({rel_x},{rel_y}), got {drag_start:?}"
);
}