use gpui::{ Context, IntoElement, Modifiers, ParentElement as _, Render, ScrollDelta, ScrollEvent, ScrollHandle, SharedString, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px, }; use gpui_native_core::scroll_area::ScrollArea; use gpui_native_theme::Theme; struct Harness { scroll: ScrollHandle, rows: usize, } impl Render for Harness { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div().size_full().child( ScrollArea::new("test-scroll") .handle(self.scroll.clone()) .h(px(200.)) .w(px(300.)) .child(div().flex().flex_col().children((0..self.rows).map(|row| { div() .h(px(20.)) .child(SharedString::from(format!("row {row}"))) }))), ) } } /// A 300×200 scroll area over `rows` 20px rows, with the handle the test /// can inspect directly. fn scroll_area_window(rows: usize) -> (TestAppContext, VisualTestContext, ScrollHandle) { let mut cx = TestAppContext::single(); cx.update(Theme::init); let handle = ScrollHandle::new(); let handle_for_view = handle.clone(); let window = cx.add_window(move |_, _| Harness { scroll: handle_for_view, rows, }); let mut visual = VisualTestContext::from_window(window.into(), &cx); visual.update(|window, cx| { window.draw(cx).clear(cx); }); (cx, visual, handle) } #[test] fn wheel_scrolling_moves_the_offset_and_stays_inside_the_content() { let (_cx, mut visual, handle) = scroll_area_window(30); // 30 rows × 20px = 600px of content in a 200px viewport. visual.simulate_event(ScrollEvent { position: point(px(100.), px(100.)), delta: ScrollDelta::Pixels(point(px(0.), px(-60.))), modifiers: Modifiers::default(), ..Default::default() }); let offset = handle.offset().y; assert_eq!(offset, px(-60.)); // Wheeling far past the end clamps to the content. for _ in 0..10 { visual.simulate_event(ScrollEvent { position: point(px(100.), px(100.)), delta: ScrollDelta::Pixels(point(px(0.), px(-60.))), modifiers: Modifiers::default(), ..Default::default() }); } // max_offset = 600 - 200 = 400. assert_eq!(handle.offset().y, px(-400.)); } #[test] fn the_content_starts_unscrolled_and_horizontal_axes_scroll_x() { let (_cx, mut visual, handle) = scroll_area_window(30); assert_eq!(handle.offset(), gpui::point(px(0.), px(0.))); // A vertical-only area ignores horizontal wheel deltas. visual.simulate_event(ScrollEvent { position: point(px(100.), px(100.)), delta: ScrollDelta::Pixels(point(px(-60.), px(0.))), modifiers: Modifiers::default(), ..Default::default() }); assert_eq!(handle.offset().x, px(0.)); } #[test] fn the_thumb_paints_when_the_content_overflows() { let (_cx, _visual, handle) = scroll_area_window(30); // 30 rows × 20px in a 200px viewport: the thumb has something to report. assert!(handle.max_offset().y > px(0.)); } /// The gallery's exact shell: a window-filling root, a window-filling scroll /// area over padded flex-column content that towers past the viewport. struct GalleryShell { scroll: ScrollHandle, } impl Render for GalleryShell { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div().size_full().child( ScrollArea::new("gallery-scroll") .handle(self.scroll.clone()) .size_full() .child( div() .p_6() .flex() .flex_col() .gap_5() .children((0..12).map(|section| { div() .h(px(220.)) .child(SharedString::from(format!("section {section}"))) })), ), ) } } #[test] fn a_gallery_style_window_fill_scroll_area_scrolls_tall_flex_content() { let mut cx = TestAppContext::single(); cx.update(Theme::init); let handle = ScrollHandle::new(); let handle_for_view = handle.clone(); let window = cx.add_window(move |_, _| GalleryShell { scroll: handle_for_view, }); let mut visual = VisualTestContext::from_window(window.into(), &cx); visual.update(|window, cx| { window.draw(cx).clear(cx); }); // 12 sections × 220px + padding towers past the window; a wheel event // over the middle of the viewport must scroll the root area. visual.simulate_event(ScrollEvent { position: point(px(300.), px(300.)), delta: ScrollDelta::Pixels(point(px(0.), px(-60.))), modifiers: Modifiers::default(), ..Default::default() }); assert_eq!(handle.offset().y, px(-60.)); assert!(handle.max_offset().y > px(1000.)); }