use std::{cell::RefCell, rc::Rc}; use gpui::{ Context, IntoElement, Modifiers, MouseButton, ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext, Window, WindowHandle, div, point, px, }; use gpui_native_data::tree::{Tree, TreeItem}; use gpui_native_theme::Theme; type Changes = Rc>>>; type Disclosures = Rc>>; struct Harness { selected: Option, changes: Changes, disclosures: Disclosures, } impl Render for Harness { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let changes = self.changes.clone(); let disclosures = self.disclosures.clone(); let view = cx.entity(); let change_view = view.clone(); div().size_full().child( Tree::new("test-tree") .selected(self.selected) .on_change(move |next, _, cx| { changes.borrow_mut().push(next); change_view.update(cx, |harness, cx| { harness.selected = next; cx.notify(); }); }) .on_disclosure(move |index, _, cx| { disclosures.borrow_mut().push(index); view.update(cx, |_, cx| cx.notify()); }) .items(|_, _| { vec![ TreeItem::new("Root") .depth(0) .expandable(true) .expanded(true), TreeItem::new("Child").depth(1), TreeItem::new("Collapsed").depth(0).expandable(true), TreeItem::new("Untouchable").depth(1).disabled(true), TreeItem::new("Last").depth(0), ] }), ) } } /// Rows paint at the top-left, one per ~34 px band (same rhythm as the /// list), so clicks at `(50, 16/48/80/112/144)` land on Root / Child / /// Collapsed / the disabled Untouchable / Last. Each expandable row leads /// with a 16 px disclosure slot at its indent — a click at `(24, y)` hits /// row 0's chevron. fn tree_window() -> ( TestAppContext, VisualTestContext, WindowHandle, Changes, Disclosures, ) { let mut cx = TestAppContext::single(); cx.update(Theme::init); let changes = Rc::new(RefCell::new(Vec::new())); let disclosures = Rc::new(RefCell::new(Vec::new())); let harness_changes = changes.clone(); let harness_disclosures = disclosures.clone(); let window = cx.add_window(move |_, _| Harness { selected: None, changes: harness_changes, disclosures: harness_disclosures, }); let mut visual = VisualTestContext::from_window(window.into(), &cx); visual.update(|window, cx| { window.draw(cx).clear(cx); }); (cx, visual, window, changes, disclosures) } fn draw(visual: &mut VisualTestContext) { visual.update(|window, cx| { window.draw(cx).clear(cx); }); } fn click(visual: &mut VisualTestContext, position: gpui::Point) { visual.simulate_mouse_down(position, MouseButton::Left, Modifiers::default()); visual.simulate_mouse_up(position, MouseButton::Left, Modifiers::default()); } #[test] fn clicking_a_row_selects_it_and_hands_the_tree_the_keyboard() { let (_cx, mut visual, _window, changes, disclosures) = tree_window(); click(&mut visual, point(px(50.), px(16.))); assert_eq!(*changes.borrow(), vec![Some(0)]); assert!(disclosures.borrow().is_empty()); // The click focused the tree; arrows move within the visible rows. visual.simulate_keystrokes("down"); draw(&mut visual); assert_eq!(*changes.borrow(), vec![Some(0), Some(1)]); } #[test] fn clicking_the_disclosure_toggles_without_selecting() { let (_cx, mut visual, _window, changes, disclosures) = tree_window(); // Row 0's chevron, not its label. click(&mut visual, point(px(24.), px(16.))); draw(&mut visual); assert_eq!(*disclosures.borrow(), vec![0]); assert!( changes.borrow().is_empty(), "a disclosure is not a selection" ); } #[test] fn arrow_right_expands_and_left_collapses_the_selected_row() { let (_cx, mut visual, _window, changes, disclosures) = tree_window(); click(&mut visual, point(px(50.), px(16.))); // Root: expanded expandable visual.simulate_keystrokes("left"); // collapse → reports visual.simulate_keystrokes("right"); // still collapsed per the caller → nothing yet visual.simulate_keystrokes("down"); // Child: a leaf → arrows are inert visual.simulate_keystrokes("right"); visual.simulate_keystrokes("left"); draw(&mut visual); assert_eq!(*disclosures.borrow(), vec![0]); assert_eq!( *changes.borrow(), vec![Some(0), Some(1)], "disclosure keys never change the selection" ); // Select the collapsed expandable row and expand it. click(&mut visual, point(px(50.), px(80.))); visual.simulate_keystrokes("right"); draw(&mut visual); assert_eq!(*disclosures.borrow(), vec![0, 2]); } #[test] fn keyboard_navigation_skips_disabled_rows_and_never_wraps() { let (_cx, mut visual, _window, changes, _disclosures) = tree_window(); click(&mut visual, point(px(50.), px(16.))); visual.simulate_keystrokes("down"); // Two visual.simulate_keystrokes("down"); // Collapsed — selectable, not disabled visual.simulate_keystrokes("down"); // over the disabled row → Last visual.simulate_keystrokes("down"); // last selectable; no wrap draw(&mut visual); assert_eq!(*changes.borrow(), vec![Some(0), Some(1), Some(2), Some(4)]); visual.simulate_keystrokes("home"); visual.simulate_keystrokes("end"); visual.simulate_keystrokes("up"); // over the disabled row draw(&mut visual); assert_eq!( *changes.borrow(), vec![ Some(0), Some(1), Some(2), Some(4), Some(0), Some(4), Some(2) ] ); } #[test] fn disclosure_keys_skip_disabled_and_unselected_rows() { let (_cx, mut visual, _window, changes, disclosures) = tree_window(); // Focus through the first row, then park the selection on the leaf. click(&mut visual, point(px(50.), px(16.))); visual.simulate_keystrokes("down"); // Child (leaf), focused tree visual.simulate_keystrokes("left"); // leaf: no disclosure visual.simulate_keystrokes("right"); // leaf: no disclosure draw(&mut visual); assert!(disclosures.borrow().is_empty()); assert_eq!(*changes.borrow(), vec![Some(0), Some(1)]); }