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::list::{List, ListItem}; use gpui_native_theme::Theme; struct Harness { selected: Option, changes: Rc>>>, } impl Render for Harness { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let changes = self.changes.clone(); let view = cx.entity(); div().size_full().child( List::new("test-list") .selected(self.selected) .on_change(move |next, _, cx| { changes.borrow_mut().push(next); view.update(cx, |harness, cx| { harness.selected = next; cx.notify(); }); }) .items(|_, _| { vec![ ListItem::new("One"), ListItem::new("Two"), ListItem::new("Unavailable").disabled(true), ListItem::new("Three"), ] }), ) } } type Changes = Rc>>>; type ListWindow = ( TestAppContext, VisualTestContext, WindowHandle, Changes, ); /// Rows paint at the top-left with no outer padding: one row per ~34 px band /// (16 px text line + 2×4 px padding + 2 px gap), so clicks at `(50, 16/48/80/112)` /// land on One / Two / the disabled Unavailable / Three. fn list_window(selected: Option) -> ListWindow { let mut cx = TestAppContext::single(); cx.update(Theme::init); let changes = Rc::new(RefCell::new(Vec::new())); let harness_changes = changes.clone(); let window = cx.add_window(move |_, _| Harness { selected, changes: harness_changes, }); let mut visual = VisualTestContext::from_window(window.into(), &cx); visual.update(|window, cx| { window.draw(cx).clear(cx); }); (cx, visual, window, changes) } 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_list_the_keyboard() { let (_cx, mut visual, _window, changes) = list_window(None); click(&mut visual, point(px(50.), px(16.))); assert_eq!(*changes.borrow(), vec![Some(0)]); // The click focused the list, so the arrows navigate from here. visual.simulate_keystrokes("down"); draw(&mut visual); assert_eq!(*changes.borrow(), vec![Some(0), Some(1)]); } #[test] fn clicking_a_disabled_row_reports_nothing() { let (_cx, mut visual, _window, changes) = list_window(None); click(&mut visual, point(px(50.), px(80.))); draw(&mut visual); assert_eq!(*changes.borrow(), Vec::>::new()); } #[test] fn keyboard_navigation_skips_disabled_rows_and_never_wraps() { let (_cx, mut visual, _window, changes) = list_window(None); click(&mut visual, point(px(50.), px(16.))); visual.simulate_keystrokes("down"); // over the disabled row → Two visual.simulate_keystrokes("down"); // over the disabled row → Three visual.simulate_keystrokes("down"); // last selectable; no wrap draw(&mut visual); assert_eq!(*changes.borrow(), vec![Some(0), Some(1), Some(3)]); visual.simulate_keystrokes("home"); visual.simulate_keystrokes("end"); visual.simulate_keystrokes("up"); // over the disabled row → Two draw(&mut visual); assert_eq!( *changes.borrow(), vec![Some(0), Some(1), Some(3), Some(0), Some(3), Some(1)] ); } #[test] fn arrow_down_with_no_selection_picks_the_first_selectable_row() { let (mut cx, mut visual, window, changes) = list_window(Some(1)); // Focus the list, then clear the selection app-side. click(&mut visual, point(px(50.), px(48.))); window .update(&mut cx, |harness, _, cx| { harness.selected = None; cx.notify(); }) .unwrap(); draw(&mut visual); visual.simulate_keystrokes("down"); draw(&mut visual); assert_eq!(*changes.borrow(), vec![Some(1), Some(0)]); }