274 lines
8.2 KiB
Rust
274 lines
8.2 KiB
Rust
use gpui::{
|
|
Bounds, Context, IntoElement, ListAlignment, ListState, MouseButton, MouseDownEvent,
|
|
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, Styled as _,
|
|
TestAppContext, UniformListScrollHandle, VisualTestContext, Window, div, list, point, px,
|
|
uniform_list,
|
|
};
|
|
use gpui_native_core::init;
|
|
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle};
|
|
use gpui_native_theme::ActiveTheme as _;
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
const ROWS: usize = 50;
|
|
const ROW_HEIGHT: f32 = 24.;
|
|
const VIEWPORT_HEIGHT: f32 = 300.;
|
|
|
|
struct UniformShell {
|
|
rows: usize,
|
|
handle: UniformListScrollHandle,
|
|
renders: Rc<RefCell<usize>>,
|
|
}
|
|
|
|
impl Render for UniformShell {
|
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
*self.renders.borrow_mut() += 1;
|
|
div().size_full().child(
|
|
div()
|
|
.relative()
|
|
.h(px(VIEWPORT_HEIGHT))
|
|
.w(px(400.))
|
|
.child(
|
|
uniform_list("rows", self.rows, |range, _, _| {
|
|
range
|
|
.map(|ix| div().h(px(ROW_HEIGHT)).child(format!("row {ix}")))
|
|
.collect()
|
|
})
|
|
.track_scroll(&self.handle)
|
|
.h_full()
|
|
.w_full(),
|
|
)
|
|
.child(Scrollbar::vertical(self.handle.clone())),
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ListShell {
|
|
state: ListState,
|
|
}
|
|
|
|
impl Render for ListShell {
|
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
div().size_full().child(
|
|
div()
|
|
.relative()
|
|
.h(px(VIEWPORT_HEIGHT))
|
|
.w(px(400.))
|
|
.child(
|
|
list(self.state.clone(), |ix, _, _| {
|
|
div()
|
|
.h(px(ROW_HEIGHT))
|
|
.child(format!("row {ix}"))
|
|
.into_any_element()
|
|
})
|
|
.h_full()
|
|
.w_full(),
|
|
)
|
|
.child(Scrollbar::vertical(self.state.clone())),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn uniform_shell(
|
|
rows: usize,
|
|
) -> (
|
|
TestAppContext,
|
|
VisualTestContext,
|
|
UniformListScrollHandle,
|
|
Rc<RefCell<usize>>,
|
|
) {
|
|
let mut cx = TestAppContext::single();
|
|
cx.update(init);
|
|
let handle = UniformListScrollHandle::new();
|
|
let handle_for_view = handle.clone();
|
|
let renders = Rc::new(RefCell::new(0));
|
|
let renders_for_view = renders.clone();
|
|
let window = cx.add_window(move |_, _| UniformShell {
|
|
rows,
|
|
handle: handle_for_view,
|
|
renders: renders_for_view,
|
|
});
|
|
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
|
visual.update(|window, cx| {
|
|
window.draw(cx).clear(cx);
|
|
});
|
|
(cx, visual, handle, renders)
|
|
}
|
|
|
|
fn list_shell() -> (TestAppContext, VisualTestContext, ListState) {
|
|
let mut cx = TestAppContext::single();
|
|
cx.update(init);
|
|
let state =
|
|
ListState::new(ROWS, ListAlignment::Top, px(400.)).with_uniform_item_height(px(ROW_HEIGHT));
|
|
let state_for_view = state.clone();
|
|
let window = cx.add_window(move |_, _| ListShell {
|
|
state: state_for_view,
|
|
});
|
|
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
|
visual.update(|window, cx| {
|
|
window.draw(cx).clear(cx);
|
|
});
|
|
(cx, visual, state)
|
|
}
|
|
|
|
/// The scrollbar geometry the tests need, straight from the handle and the
|
|
/// theme the component reads.
|
|
struct Metrics {
|
|
viewport: Bounds<Pixels>,
|
|
max_offset: Pixels,
|
|
width: Pixels,
|
|
inset: Pixels,
|
|
min_thumb: Pixels,
|
|
}
|
|
|
|
impl Metrics {
|
|
fn measure(cx: &TestAppContext, handle: &impl ScrollbarHandle) -> Self {
|
|
cx.update(|cx| {
|
|
let theme = cx.theme();
|
|
Self {
|
|
viewport: handle.viewport(),
|
|
max_offset: handle.max_offset().y,
|
|
width: theme.scrollbar.width,
|
|
inset: theme.scrollbar.inset,
|
|
min_thumb: theme.scrollbar.min_thumb,
|
|
}
|
|
})
|
|
}
|
|
|
|
fn track_length(&self) -> Pixels {
|
|
self.viewport.size.height - self.inset * 2.0
|
|
}
|
|
|
|
fn thumb_length(&self) -> Pixels {
|
|
let content = self.viewport.size.height + self.max_offset;
|
|
(self.viewport.size.height / content * self.track_length())
|
|
.max(self.min_thumb)
|
|
.min(self.track_length())
|
|
}
|
|
|
|
fn thumb_center(&self) -> Point<Pixels> {
|
|
point(
|
|
self.viewport.right() - self.inset - self.width / 2.0,
|
|
self.viewport.top() + self.inset + self.thumb_length() / 2.0,
|
|
)
|
|
}
|
|
|
|
fn track_near_bottom(&self) -> Point<Pixels> {
|
|
point(
|
|
self.viewport.right() - self.inset - self.width / 2.0,
|
|
self.viewport.top() + self.inset + self.track_length() - px(1.),
|
|
)
|
|
}
|
|
|
|
fn full_drag_target(&self) -> Point<Pixels> {
|
|
point(
|
|
self.thumb_center().x,
|
|
self.thumb_center().y + (self.track_length() - self.thumb_length()),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn press(visual: &mut VisualTestContext, position: Point<Pixels>) {
|
|
visual.simulate_event(MouseDownEvent {
|
|
button: MouseButton::Left,
|
|
position,
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
fn drag_to(visual: &mut VisualTestContext, position: Point<Pixels>) {
|
|
visual.simulate_event(MouseMoveEvent {
|
|
position,
|
|
pressed_button: Some(MouseButton::Left),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
fn release(visual: &mut VisualTestContext, position: Point<Pixels>) {
|
|
visual.simulate_event(MouseUpEvent {
|
|
button: MouseButton::Left,
|
|
position,
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn dragging_the_thumb_scrolls_the_uniform_list() {
|
|
let (cx, mut visual, handle, renders) = uniform_shell(ROWS);
|
|
let metrics = Metrics::measure(&cx, &handle);
|
|
assert!(
|
|
metrics.max_offset > px(0.),
|
|
"50 24px rows overflow a 300px viewport"
|
|
);
|
|
|
|
press(&mut visual, metrics.thumb_center());
|
|
let renders_after_press = *renders.borrow();
|
|
drag_to(&mut visual, metrics.full_drag_target());
|
|
let renders_after_move = *renders.borrow();
|
|
release(&mut visual, metrics.full_drag_target());
|
|
|
|
let offset = handle.offset().y;
|
|
assert!(
|
|
(offset.abs() - metrics.max_offset).abs() < px(0.5),
|
|
"a full drag reaches the end, got {offset}"
|
|
);
|
|
assert!(
|
|
renders_after_move > renders_after_press,
|
|
"dragging must repaint the owning view, or the scroll never shows"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn clicking_the_track_jumps_to_the_position() {
|
|
let (cx, mut visual, handle, _) = uniform_shell(ROWS);
|
|
let metrics = Metrics::measure(&cx, &handle);
|
|
|
|
press(&mut visual, metrics.track_near_bottom());
|
|
release(&mut visual, metrics.track_near_bottom());
|
|
|
|
let offset = handle.offset().y;
|
|
assert!(
|
|
(offset.abs() - metrics.max_offset).abs() < px(0.5),
|
|
"a track click near the bottom jumps to the end, got {offset}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_fitted_list_ignores_the_scrollbar() {
|
|
let (cx, mut visual, handle, _) = uniform_shell(5);
|
|
let metrics = Metrics::measure(&cx, &handle);
|
|
assert_eq!(metrics.max_offset, px(0.), "5 rows fit the viewport");
|
|
|
|
press(&mut visual, metrics.track_near_bottom());
|
|
drag_to(&mut visual, metrics.full_drag_target());
|
|
release(&mut visual, metrics.full_drag_target());
|
|
|
|
assert_eq!(handle.offset().y, px(0.), "nothing scrolled");
|
|
}
|
|
|
|
#[test]
|
|
fn dragging_the_thumb_drives_a_list_state_and_reports_the_drag() {
|
|
let (cx, mut visual, state) = list_shell();
|
|
let metrics = Metrics::measure(&cx, &state);
|
|
assert!(metrics.max_offset > px(0.));
|
|
|
|
press(&mut visual, metrics.thumb_center());
|
|
assert!(
|
|
state.is_scrollbar_dragging(),
|
|
"the drag freezes the list's measured height"
|
|
);
|
|
|
|
drag_to(&mut visual, metrics.full_drag_target());
|
|
let offset = state.offset().y;
|
|
assert!(
|
|
(offset.abs() - metrics.max_offset).abs() < px(0.5),
|
|
"a full drag reaches the end, got {offset}"
|
|
);
|
|
|
|
release(&mut visual, metrics.full_drag_target());
|
|
assert!(
|
|
!state.is_scrollbar_dragging(),
|
|
"the drag ends with the mouse"
|
|
);
|
|
}
|