use std::{ cell::{Cell as Count, RefCell}, collections::HashMap, rc::Rc, }; use gpui::{ Context, FocusHandle, IntoElement, KeyBinding, Modifiers, ParentElement, Render, SharedString, Styled, TestAppContext, VisualTestContext, Window, div, point, px, }; use super::{Column, DataTable, FilterBuilder, FilterRule, Row}; use crate::{ primitives::{FocusNext, FocusPrev, FocusScope}, theme::Theme, }; /// Five selectable rows, 36 tall under a 36 tall header; it keeps what the table reports. struct Picks(Rc>>); impl Render for Picks { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { let (now, store) = (self.0.borrow().clone(), self.0.clone()); DataTable::new("picks", [Column::new("name", "Name")]) .rows( (0..5) .map(|ix| Row::new(format!("r{ix}"), [format!("Row {ix}").into()])) .collect::>(), ) .selected(now) .on_select(move |keys, _, _| *store.borrow_mut() = keys.to_vec()) .w(px(400.0)) } } fn settle(cx: &mut VisualTestContext) { cx.run_until_parked(); cx.update(|window, _| window.refresh()); cx.run_until_parked(); } fn picks(seen: &Rc>>) -> Vec { seen.borrow().iter().map(|key| key.to_string()).collect() } #[gpui::test] fn boxes_toggle_shift_takes_a_range_and_the_header_takes_all(cx: &mut TestAppContext) { cx.update(Theme::init); let seen = Rc::new(RefCell::new(Vec::new())); let store = seen.clone(); let (_, cx) = cx.add_window_view(|_, _| Picks(store)); settle(cx); let row = |ix: usize, x: f32| point(px(x), px(54.0 + 36.0 * ix as f32)); cx.simulate_click(row(0, 14.0), Modifiers::none()); settle(cx); assert_eq!(picks(&seen), ["r0"]); cx.simulate_click( row(3, 200.0), Modifiers { shift: true, ..Modifiers::none() }, ); settle(cx); assert_eq!(picks(&seen), ["r0", "r1", "r2", "r3"]); cx.simulate_click(point(px(14.0), px(18.0)), Modifiers::none()); settle(cx); assert_eq!( picks(&seen), ["r0", "r1", "r2", "r3", "r4"], "a mixed header box takes all" ); cx.simulate_click(point(px(14.0), px(18.0)), Modifiers::none()); settle(cx); assert!(picks(&seen).is_empty(), "a full one clears"); } /// Six rows in two groups, measured as they draw. struct Grouped(Rc>); impl Render for Grouped { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { let tall = self.0.clone(); let rows: Vec = (0..6) .map(|ix| { Row::new( format!("r{ix}"), [ if ix < 4 { "Europe" } else { "Asia" }.into(), format!("Row {ix}").into(), ], ) }) .collect(); crate::primitives::Measure::new("measure", move |bounds, _, _| tall.set(bounds.size.height)) .w(px(400.0)) .child( DataTable::new( "grouped", [Column::new("region", "Region"), Column::new("name", "Name")], ) .rows(rows) .group_by("region"), ) } } #[gpui::test] fn a_press_on_a_group_folds_its_rows(cx: &mut TestAppContext) { cx.update(Theme::init); let tall = Rc::new(std::cell::Cell::new(gpui::Pixels::ZERO)); let seen = tall.clone(); let (_, cx) = cx.add_window_view(|_, _| Grouped(seen)); settle(cx); let open = tall.get(); cx.simulate_click(point(px(60.0), px(54.0)), Modifiers::none()); settle(cx); assert_eq!( open - tall.get(), px(36.0 * 4.0), "Europe's four rows fold away" ); } /// Two rows whose names edit in place and whose detail opens; it keeps what it hears. struct Editable(Rc>>); impl Render for Editable { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { let (heard, opened) = (self.0.clone(), self.0.clone()); DataTable::new("editable", [Column::new("name", "Name").editable()]) .rows(vec![ Row::new("a", ["Ada".into()]), Row::new("b", ["Alan".into()]), ]) .detail(move |key, _, _| { opened.borrow_mut().push(format!("detail {key}")); gpui::div().h(px(40.0)).into_any_element() }) .on_edit(move |row, column, text, _, _| { heard .borrow_mut() .push(format!("edit {row} {column} {text}")) }) .w(px(400.0)) } } #[gpui::test] fn a_row_opens_its_detail_and_a_double_press_edits_a_cell(cx: &mut TestAppContext) { cx.update(|cx| { Theme::init(cx); crate::forms::bind_keys(cx); }); let heard = Rc::new(RefCell::new(Vec::new())); let seen = heard.clone(); let (_, cx) = cx.add_window_view(|_, _| Editable(seen)); settle(cx); cx.simulate_click(point(px(14.0), px(54.0)), Modifiers::none()); settle(cx); assert_eq!(heard.borrow().last().map(String::as_str), Some("detail a")); let name = point(px(120.0), px(54.0 + 36.0 + 40.0 + 24.0)); for count in [1, 2] { cx.simulate_event(gpui::MouseDownEvent { button: gpui::MouseButton::Left, position: name, modifiers: Modifiers::none(), click_count: count, first_mouse: false, }); cx.simulate_event(gpui::MouseUpEvent { button: gpui::MouseButton::Left, position: name, modifiers: Modifiers::none(), click_count: count, }); } settle(cx); cx.simulate_input("Alan Kay"); cx.simulate_keystrokes("enter"); settle(cx); assert_eq!( heard .borrow() .iter() .filter(|line| line.starts_with("edit")) .cloned() .collect::>(), ["edit b name Alan Kay"] ); } /// A two-column grid of three rows that keeps every edit it hears. struct Grid(Rc>>); impl Render for Grid { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { self.render_grid() } } impl Grid { fn render_grid(&self) -> impl IntoElement + use<> { let heard = self.0.clone(); let rows: Vec> = (0..3) .map(|row| vec![format!("a{row}").into(), format!("b{row}").into()]) .collect(); super::DataGrid::new("grid", ["A", "B"], rows) .on_change(move |edits, _, _| { heard.borrow_mut().extend( edits .iter() .map(|(row, col, text)| (*row, *col, text.to_string())), ) }) .w(px(300.0)) .h(px(200.0)) } } #[gpui::test] fn typing_edits_enter_keeps_and_backspace_clears_a_range(cx: &mut TestAppContext) { cx.update(|cx| { Theme::init(cx); crate::forms::bind_keys(cx); }); let heard = Rc::new(RefCell::new(Vec::new())); let seen = heard.clone(); let (_, cx) = cx.add_window_view(|_, _| Grid(seen)); settle(cx); cx.simulate_click(point(px(48.0), px(42.0)), Modifiers::none()); settle(cx); cx.simulate_keystrokes("7"); settle(cx); cx.simulate_keystrokes("enter"); settle(cx); assert_eq!(*heard.borrow(), [(0, 0, "7".to_string())]); cx.simulate_keystrokes("shift-down backspace"); settle(cx); assert_eq!( heard.borrow()[1..], [(1, 0, String::new()), (2, 0, String::new())], "enter moved down; the range clears" ); } /// Filter rules the owner keeps, 600 across, rebuilt from what the builder reports. struct Filters(Rc>>); impl Render for Filters { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let (rules, store, view) = (self.0.borrow().clone(), self.0.clone(), cx.entity()); div().w(px(600.0)).child( FilterBuilder::new("filters", [("name", "Name"), ("region", "Region")]) .rules(rules, false) .on_change(move |rules, _, _, cx| { *store.borrow_mut() = rules.to_vec(); view.update(cx, |_, cx| cx.notify()); }), ) } } #[gpui::test] fn removing_the_rule_being_typed_leaves_the_next_rule_its_own_value(cx: &mut TestAppContext) { cx.update(|cx| { Theme::init(cx); crate::forms::bind_keys(cx); }); let rule = |column: &'static str, value: &'static str| FilterRule { column: column.into(), value: value.into(), ..FilterRule::default() }; let rules = Rc::new(RefCell::new(vec![ rule("name", "one"), rule("region", "keep"), ])); let seen = rules.clone(); let (_, cx) = cx.add_window_view(|_, _| Filters(seen)); settle(cx); cx.simulate_click(point(px(330.0), px(12.0)), Modifiers::none()); settle(cx); cx.simulate_input("typed"); settle(cx); assert_eq!( rules.borrow()[0].value.as_ref(), "onetyped", "the first field took the typing" ); cx.simulate_click(point(px(588.0), px(12.0)), Modifiers::none()); settle(cx); cx.simulate_input("X"); settle(cx); assert_eq!( *rules.borrow(), [rule("region", "keep")], "the removed rule's draft went with it" ); } /// A two-column grid whose row count the test sets. struct Shrinking(Rc>); impl Render for Shrinking { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { let rows: Vec> = (0..self.0.get()) .map(|row| vec![format!("a{row}").into(), format!("b{row}").into()]) .collect(); super::DataGrid::new("shrink", ["A", "B"], rows) .w(px(300.0)) .h(px(200.0)) } } fn grid_keys(cx: &mut TestAppContext) { cx.update(|cx| { Theme::init(cx); crate::forms::bind_keys(cx); cx.bind_keys([ KeyBinding::new("tab", FocusNext, None), KeyBinding::new("shift-tab", FocusPrev, None), ]); }); } #[gpui::test] fn a_grid_that_shrinks_keeps_its_cursor_inside(cx: &mut TestAppContext) { grid_keys(cx); let count = Rc::new(Count::new(3)); let (view, cx) = cx.add_window_view(|_, _| Shrinking(count.clone())); settle(cx); cx.simulate_click(point(px(146.0), px(98.0)), Modifiers::none()); settle(cx); count.set(1); view.update(cx, |_, cx| cx.notify()); settle(cx); cx.simulate_keystrokes("cmd-c"); settle(cx); let copied = cx.update(|_, cx| cx.read_from_clipboard().and_then(|item| item.text())); assert_eq!( copied.as_deref(), Some("b0"), "the cursor moved to the last row left" ); } /// A sheet with A1:C1 merged, 4 across, that keeps every edit it hears. struct Merged(Rc>>); impl Render for Merged { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { let heard = self.0.clone(); let cells: HashMap<(usize, usize), SharedString> = [((0, 0), "Title".into())].into_iter().collect(); super::Spreadsheet::new("merged", 3, 4) .cells(cells) .merge((0, 0), (0, 2)) .on_change(move |edits, _, _| { heard.borrow_mut().extend( edits .iter() .map(|(row, col, text)| (*row, *col, text.to_string())), ) }) .w(px(500.0)) .h(px(200.0)) } } #[gpui::test] fn a_merge_answers_for_every_cell_it_covers(cx: &mut TestAppContext) { grid_keys(cx); let heard = Rc::new(RefCell::new(Vec::new())); let seen = heard.clone(); let (_, cx) = cx.add_window_view(|_, _| Merged(seen)); settle(cx); cx.simulate_click(point(px(146.0), px(42.0)), Modifiers::none()); settle(cx); cx.simulate_keystrokes("7 enter"); settle(cx); cx.simulate_keystrokes("up right 8 enter"); settle(cx); assert_eq!( *heard.borrow(), [(0, 0, "7".to_string()), (0, 3, "8".to_string())], "a press inside lands on the merge; right steps past it" ); } /// A grid inside a focus scope, as apps hold it, that keeps every edit it hears. struct Scoped(FocusHandle, Rc>>); impl Render for Scoped { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { FocusScope::new(&self.0) .size_full() .child(Grid(self.1.clone()).render_grid()) } } #[gpui::test] fn tab_steps_across_the_grid_and_keeps_an_edit(cx: &mut TestAppContext) { grid_keys(cx); let heard = Rc::new(RefCell::new(Vec::new())); let seen = heard.clone(); let (_, cx) = cx.add_window_view(|_, cx| Scoped(cx.focus_handle(), seen)); settle(cx); cx.simulate_click(point(px(48.0), px(42.0)), Modifiers::none()); settle(cx); cx.simulate_keystrokes("tab 7 enter"); settle(cx); cx.simulate_click(point(px(48.0), px(70.0)), Modifiers::none()); settle(cx); cx.simulate_keystrokes("5 tab"); settle(cx); cx.simulate_keystrokes("6 enter shift-tab"); settle(cx); assert_eq!( *heard.borrow(), [ (0, 1, "7".to_string()), (1, 0, "5".to_string()), (1, 1, "6".to_string()) ], "tab moves right, and keeps an open edit first" ); } #[gpui::test] fn tab_steps_past_a_merge_like_the_arrows(cx: &mut TestAppContext) { grid_keys(cx); let heard = Rc::new(RefCell::new(Vec::new())); let seen = heard.clone(); let (_, cx) = cx.add_window_view(|_, _| Merged(seen)); settle(cx); cx.simulate_click(point(px(146.0), px(42.0)), Modifiers::none()); settle(cx); cx.simulate_keystrokes("tab 9 enter"); settle(cx); assert_eq!( *heard.borrow(), [(0, 3, "9".to_string())], "tab leaves the merge by its far side" ); }