use crate::harness;
use std::cell::RefCell;
use std::collections::BTreeSet;
use std::rc::Rc;
use gpui::{prelude::*, px, TestAppContext, VisualTestContext};
use harness::open_host;
use herogpui_components::{VirtualList, VirtualListHandle, VirtualListScroll};
const ROWS: usize = 1_000;
const VIEWPORT: f32 = 300.;
fn row_height(ix: usize) -> f32 {
if ix.is_multiple_of(5) {
60.
} else {
20.
}
}
fn host(
cx: &mut TestAppContext,
) -> (
VirtualListHandle,
Rc<RefCell<BTreeSet<usize>>>,
&mut VisualTestContext,
) {
let handle = VirtualListHandle::new(ROWS);
let built = Rc::new(RefCell::new(BTreeSet::new()));
let (h, b) = (handle.clone(), built.clone());
let cx = open_host(cx, move || {
let b = b.clone();
VirtualList::new("vl", &h, move |ix, _, _| {
b.borrow_mut().insert(ix);
gpui::div()
.h(px(row_height(ix)))
.w_full()
.into_any_element()
})
.height(px(VIEWPORT))
.into_any_element()
});
(handle, built, cx)
}
fn frame(cx: &mut VisualTestContext) {
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
#[gpui::test]
fn only_rows_near_the_viewport_are_built(cx: &mut TestAppContext) {
let (handle, built, cx) = host(cx);
frame(cx);
let built = built.borrow();
assert!(built.contains(&0));
assert!(built.len() < 60, "built {} of {ROWS} rows", built.len());
assert!(!built.contains(&(ROWS - 1)));
let first = handle.bounds_for_item(0).unwrap();
let second = handle.bounds_for_item(1).unwrap();
assert_eq!(first.size.height, px(60.));
assert_eq!(second.size.height, px(20.));
assert_eq!(second.origin.y, first.origin.y + px(60.));
}
#[gpui::test]
fn scroll_to_item_top_puts_the_row_at_the_top(cx: &mut TestAppContext) {
let (handle, _, cx) = host(cx);
frame(cx);
handle.scroll_to_item(500, VirtualListScroll::Top);
frame(cx);
assert_eq!(handle.first_visible_item(), 500);
let row = handle.bounds_for_item(500).expect("row 500 rendered");
assert_eq!(row.origin.y, px(0.));
}
#[gpui::test]
fn scroll_to_item_reveal_brings_the_row_fully_into_view(cx: &mut TestAppContext) {
let (handle, _, cx) = host(cx);
frame(cx);
let below = (0..ROWS)
.find(|&ix| {
handle
.bounds_for_item(ix)
.is_some_and(|b| b.bottom() > px(VIEWPORT))
})
.unwrap();
handle.scroll_to_item(below, VirtualListScroll::Reveal);
frame(cx);
let row = handle.bounds_for_item(below).unwrap();
assert!((f32::from(row.bottom()) - VIEWPORT).abs() < 0.5, "{row:?}");
handle.scroll_to_item(below, VirtualListScroll::Reveal);
frame(cx);
assert_eq!(
handle.bounds_for_item(below).unwrap().origin.y,
row.origin.y
);
handle.scroll_to_item(700, VirtualListScroll::Reveal);
frame(cx);
let far = handle.bounds_for_item(700).expect("row 700 rendered");
assert!(
far.origin.y >= px(0.) && far.bottom() <= px(VIEWPORT),
"{far:?}"
);
}
#[gpui::test]
fn item_count_changes_are_honoured(cx: &mut TestAppContext) {
let (handle, built, cx) = host(cx);
frame(cx);
handle.set_item_count(3);
built.borrow_mut().clear();
frame(cx);
assert_eq!(handle.item_count(), 3);
assert_eq!(*built.borrow(), BTreeSet::from([0, 1, 2]));
handle.splice(3..3, 2);
frame(cx);
assert_eq!(handle.item_count(), 5);
handle.scroll_to_item(99, VirtualListScroll::Top);
frame(cx);
assert!(handle.bounds_for_item(4).is_some());
}
const UNIFORM_ROW: f32 = 30.;
fn uniform_host(
cx: &mut TestAppContext,
) -> (
VirtualListHandle,
Rc<RefCell<BTreeSet<usize>>>,
&mut VisualTestContext,
) {
let handle = VirtualListHandle::uniform(ROWS);
let built = Rc::new(RefCell::new(BTreeSet::new()));
let (h, b) = (handle.clone(), built.clone());
let cx = open_host(cx, move || {
let b = b.clone();
VirtualList::new("uvl", &h, move |ix, _, _| {
b.borrow_mut().insert(ix);
gpui::div().h(px(UNIFORM_ROW)).w_full().into_any_element()
})
.height(px(VIEWPORT))
.into_any_element()
});
(handle, built, cx)
}
#[gpui::test]
fn uniform_list_builds_only_the_viewport_rows(cx: &mut TestAppContext) {
let (handle, built, cx) = uniform_host(cx);
frame(cx);
assert!(handle.is_uniform());
assert_eq!(*built.borrow(), (0..10).collect::<BTreeSet<_>>());
assert_eq!(handle.viewport_bounds().size.height, px(VIEWPORT));
let row = handle.bounds_for_item(3).expect("row 3 on screen");
assert_eq!(row.origin.y, px(90.));
assert_eq!(row.size.height, px(UNIFORM_ROW));
assert!(handle.bounds_for_item(10).is_none());
assert!(handle.is_scrolled_to_top());
assert!(!handle.is_scrolled_to_end());
assert_eq!(
handle.remaining_below(),
Some(px(ROWS as f32 * UNIFORM_ROW - VIEWPORT))
);
}
#[gpui::test]
fn uniform_center_centres_an_off_screen_row_and_clamps_at_the_ends(cx: &mut TestAppContext) {
let (handle, _, cx) = uniform_host(cx);
frame(cx);
handle.scroll_to_item(500, VirtualListScroll::Center);
frame(cx);
let row = handle.bounds_for_item(500).expect("row 500 on screen");
assert_eq!(row.center().y, px(VIEWPORT / 2.));
handle.scroll_to_item(499, VirtualListScroll::Center);
frame(cx);
assert_eq!(handle.bounds_for_item(500).unwrap().origin.y, row.origin.y);
handle.scroll_to_item(usize::MAX, VirtualListScroll::Center);
frame(cx);
let last = handle
.bounds_for_item(ROWS - 1)
.expect("last row on screen");
assert_eq!(last.bottom(), px(VIEWPORT));
assert!(handle.is_scrolled_to_end());
assert_eq!(handle.remaining_below(), Some(px(0.)));
handle.scroll_to_item(0, VirtualListScroll::Center);
frame(cx);
assert!(handle.is_scrolled_to_top());
assert_eq!(handle.first_visible_item(), 0);
}
#[gpui::test]
fn uniform_top_reveal_and_scroll_by(cx: &mut TestAppContext) {
let (handle, _, cx) = uniform_host(cx);
frame(cx);
handle.scroll_to_item(200, VirtualListScroll::Top);
frame(cx);
assert_eq!(handle.first_visible_item(), 200);
assert_eq!(handle.bounds_for_item(200).unwrap().origin.y, px(0.));
handle.scroll_to_item(215, VirtualListScroll::Reveal);
frame(cx);
assert_eq!(handle.bounds_for_item(215).unwrap().bottom(), px(VIEWPORT));
handle.scroll_by(px(-UNIFORM_ROW * 2.));
frame(cx);
assert_eq!(handle.bounds_for_item(213).unwrap().bottom(), px(VIEWPORT));
assert!(handle.bounds_for_item(215).is_none(), "scrolled out below");
handle.scroll_by(px(-1.0e7));
frame(cx);
assert!(handle.is_scrolled_to_top());
}
#[gpui::test]
fn uniform_splice_keeps_the_scroll_and_set_item_count_resets_it(cx: &mut TestAppContext) {
let (handle, built, cx) = uniform_host(cx);
frame(cx);
handle.scroll_to_item(100, VirtualListScroll::Top);
frame(cx);
handle.splice(ROWS..ROWS, 50);
frame(cx);
assert_eq!(handle.item_count(), ROWS + 50);
assert_eq!(handle.first_visible_item(), 100);
handle.set_item_count(3);
built.borrow_mut().clear();
frame(cx);
assert_eq!(handle.item_count(), 3);
assert_eq!(handle.first_visible_item(), 0);
assert_eq!(*built.borrow(), BTreeSet::from([0, 1, 2]));
assert!(handle.is_scrolled_to_top() && handle.is_scrolled_to_end());
}
#[gpui::test]
fn measured_center_centres_a_laid_out_row(cx: &mut TestAppContext) {
let (handle, _, cx) = host(cx);
frame(cx);
let below = (0..ROWS)
.find(|&ix| {
handle
.bounds_for_item(ix)
.is_some_and(|b| b.bottom() > px(VIEWPORT))
})
.unwrap();
handle.scroll_to_item(below, VirtualListScroll::Center);
frame(cx);
let row = handle.bounds_for_item(below).unwrap();
assert!(
(f32::from(row.center().y) - VIEWPORT / 2.).abs() < 0.5,
"{row:?}"
);
}