use crate::harness;
use std::{cell::Cell, rc::Rc};
use gpui::{
hsla, point, prelude::*, px, AnyElement, Hsla, Modifiers, Quad, ScrollHandle, TestAppContext,
VisualTestContext,
};
use herogpui_components::{Orientation, Scrollbar};
use herogpui_theme::ActiveTheme;
use harness::open_host;
const TRACK_W: f32 = 300.;
const VIEWPORT: f32 = 200.;
const CONTENT: f32 = 1000.;
const H_TRACK_W: f32 = 160.;
const H_VIEWPORT: f32 = 40.;
const H_CONTENT: f32 = 400.;
fn vertical_bar(handle: ScrollHandle, bar: Scrollbar) -> AnyElement {
gpui::div()
.relative()
.w(px(TRACK_W))
.h(px(VIEWPORT))
.child(
gpui::div()
.id("api-scroller")
.track_scroll(&handle)
.size_full()
.overflow_y_scroll()
.child(gpui::div().h(px(CONTENT)).w_full()),
)
.child(bar)
.into_any_element()
}
fn horizontal_bar(handle: ScrollHandle, bar: Scrollbar) -> AnyElement {
gpui::div()
.relative()
.w(px(H_TRACK_W))
.h(px(H_VIEWPORT))
.child(
gpui::div()
.id("api-h-scroller")
.track_scroll(&handle)
.size_full()
.overflow_x_scroll()
.child(gpui::div().w(px(H_CONTENT)).h_full()),
)
.child(bar)
.into_any_element()
}
fn solid_quads(cx: &mut VisualTestContext) -> Vec<Quad> {
cx.update(|window, _| {
window
.painted_quads()
.into_iter()
.filter(|quad| quad.background.as_solid().is_some())
.collect()
})
}
fn only_quad(cx: &mut VisualTestContext, label: &str) -> Quad {
let quads = solid_quads(cx);
assert_eq!(quads.len(), 1, "{label}: expected exactly one painted quad");
quads[0]
}
fn scale_of(cx: &mut VisualTestContext) -> f32 {
cx.update(|window, _| window.scale_factor())
}
#[allow(clippy::too_many_arguments)] fn assert_thumb(label: &str, quad: &Quad, scale: f32, x: f32, y: f32, w: f32, h: f32, radius: f32) {
let close = |what: &str, actual: f32, expected: f32| {
assert!(
(actual - expected).abs() < 0.5,
"{label} {what}: expected {expected}, got {actual}"
);
};
close("origin.x", quad.bounds.origin.x.0, x * scale);
close("origin.y", quad.bounds.origin.y.0, y * scale);
close("size.width", quad.bounds.size.width.0, w * scale);
close("size.height", quad.bounds.size.height.0, h * scale);
close(
"radius top_left",
quad.corner_radii.top_left.0,
radius * scale,
);
close(
"radius top_right",
quad.corner_radii.top_right.0,
radius * scale,
);
close(
"radius bottom_right",
quad.corner_radii.bottom_right.0,
radius * scale,
);
close(
"radius bottom_left",
quad.corner_radii.bottom_left.0,
radius * scale,
);
}
fn assert_fill(label: &str, quad: &Quad, color: Hsla) {
let actual = quad.background.as_solid().expect("a solid thumb fill");
assert_eq!(actual, color, "{label}: unexpected thumb fill");
}
const INSIDE_TRACK: (f32, f32) = (TRACK_W - 4., 100.);
#[gpui::test]
fn default_geometry_and_token_fill_are_unchanged(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let scrolled = handle.clone();
let cx = open_host(cx, move || {
let scroller = handle.clone();
vertical_bar(scroller, Scrollbar::new("bar", handle.clone()))
});
cx.update(|window, _| window.refresh());
let scale = scale_of(cx);
let thumb_len = VIEWPORT * (VIEWPORT / CONTENT);
let quad = only_quad(cx, "default vertical");
assert_thumb(
"default vertical",
&quad,
scale,
TRACK_W - 8.,
0.,
8.,
thumb_len,
4.,
);
let token = cx.update(|_, app| app.colors().scrollbar);
assert_fill("default vertical", &quad, token);
scrolled.set_offset(point(px(0.), px(-200.)));
cx.update(|window, _| window.refresh());
let quad = only_quad(cx, "default vertical, scrolled");
assert_thumb(
"default vertical, scrolled",
&quad,
scale,
TRACK_W - 8.,
40.,
8.,
thumb_len,
4.,
);
}
#[gpui::test]
fn horizontal_default_matches_the_vertical_geometry_rules(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let cx = open_host(cx, move || {
let scroller = handle.clone();
horizontal_bar(
scroller,
Scrollbar::new("h-bar", handle.clone()).orientation(Orientation::Horizontal),
)
});
cx.update(|window, _| window.refresh());
let scale = scale_of(cx);
let thumb_len = H_TRACK_W * (H_TRACK_W / H_CONTENT);
let quad = only_quad(cx, "default horizontal");
assert_thumb(
"default horizontal",
&quad,
scale,
0.,
H_VIEWPORT - 8.,
thumb_len,
8.,
4.,
);
}
#[gpui::test]
fn track_override_resizes_the_track_and_rederives_the_radius(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let cx = open_host(cx, move || {
let scroller = handle.clone();
vertical_bar(
scroller,
Scrollbar::new("bar", handle.clone()).track(px(12.)),
)
});
cx.update(|window, _| window.refresh());
let scale = scale_of(cx);
let thumb_len = VIEWPORT * (VIEWPORT / CONTENT);
let quad = only_quad(cx, "track 12");
assert_thumb(
"track 12",
&quad,
scale,
TRACK_W - 12.,
0.,
12.,
thumb_len,
6.,
);
}
#[gpui::test]
fn horizontal_track_override_rederives_its_radius(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let cx = open_host(cx, move || {
let scroller = handle.clone();
horizontal_bar(
scroller,
Scrollbar::new("h-bar", handle.clone())
.orientation(Orientation::Horizontal)
.track(px(12.)),
)
});
cx.update(|window, _| window.refresh());
let scale = scale_of(cx);
let thumb_len = H_TRACK_W * (H_TRACK_W / H_CONTENT);
let quad = only_quad(cx, "horizontal track 12");
assert_thumb(
"horizontal track 12",
&quad,
scale,
0.,
H_VIEWPORT - 12.,
thumb_len,
12.,
6.,
);
}
#[gpui::test]
fn radius_override_shapes_the_thumb(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let cx = open_host(cx, move || {
let scroller = handle.clone();
vertical_bar(
scroller,
Scrollbar::new("bar", handle.clone())
.track(px(12.))
.radius(px(2.)),
)
});
cx.update(|window, _| window.refresh());
let scale = scale_of(cx);
let thumb_len = VIEWPORT * (VIEWPORT / CONTENT);
let quad = only_quad(cx, "radius 2");
assert_thumb(
"radius 2",
&quad,
scale,
TRACK_W - 12.,
0.,
12.,
thumb_len,
2.,
);
}
#[gpui::test]
fn inset_shrinks_the_painted_thumb(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let cx = open_host(cx, move || {
let scroller = handle.clone();
vertical_bar(
scroller,
Scrollbar::new("bar", handle.clone()).inset(px(2.)),
)
});
cx.update(|window, _| window.refresh());
let thumb_len = VIEWPORT * (VIEWPORT / CONTENT);
let scale = scale_of(cx);
let quad = only_quad(cx, "inset 2");
assert_thumb(
"inset 2",
&quad,
scale,
TRACK_W - 6.,
2.,
4.,
thumb_len - 4.,
4.,
);
}
#[gpui::test]
fn an_sx_pixel_size_sizes_the_track(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let cx = open_host(cx, move || {
let scroller = handle.clone();
vertical_bar(
scroller,
Scrollbar::new("bar", handle.clone()).sx(move |s| s.w(px(16.))),
)
});
cx.update(|window, _| window.refresh());
let thumb_len = VIEWPORT * (VIEWPORT / CONTENT);
let scale = scale_of(cx);
let quad = only_quad(cx, "sx w 16");
assert_thumb(
"sx w 16",
&quad,
scale,
TRACK_W - 16.,
0.,
16.,
thumb_len,
8.,
);
}
#[gpui::test]
fn sx_radii_shape_the_thumb(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let cx = open_host(cx, move || {
let scroller = handle.clone();
vertical_bar(
scroller,
Scrollbar::new("bar", handle.clone())
.track(px(12.))
.sx(move |s| s.rounded(px(3.))),
)
});
cx.update(|window, _| window.refresh());
let thumb_len = VIEWPORT * (VIEWPORT / CONTENT);
let scale = scale_of(cx);
let quad = only_quad(cx, "sx radius 3");
assert_thumb(
"sx radius 3",
&quad,
scale,
TRACK_W - 12.,
0.,
12.,
thumb_len,
3.,
);
}
#[gpui::test]
fn an_sx_background_is_the_thumb_resting_fill_and_outranks_thumb_color(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let sx_bg = hsla(0.35, 0.6, 0.5, 1.0);
let thumb_color = hsla(0.05, 0.9, 0.6, 1.0);
let cx = open_host(cx, move || {
let scroller = handle.clone();
vertical_bar(
scroller,
Scrollbar::new("bar", handle.clone())
.thumb_color(thumb_color)
.sx(move |s| s.bg(sx_bg)),
)
});
cx.update(|window, _| window.refresh());
let quads = solid_quads(cx);
assert_eq!(
quads.len(),
2,
"an sx background paints the gutter and the thumb"
);
for quad in &quads {
assert_fill("sx background", quad, sx_bg);
}
}
#[gpui::test]
fn the_thumb_repaints_when_hovered_and_takes_the_hover_fill(cx: &mut TestAppContext) {
let resting = hsla(0.3, 0.5, 0.5, 1.0);
let hover = hsla(0.7, 0.5, 0.5, 1.0);
let handle = ScrollHandle::new();
let renders = Rc::new(Cell::new(0usize));
let counter = renders.clone();
let cx = open_host(cx, move || {
counter.set(counter.get() + 1);
let scroller = handle.clone();
vertical_bar(
scroller,
Scrollbar::new("bar", handle.clone())
.thumb_color(resting)
.thumb_hover_color(hover),
)
});
cx.update(|window, _| window.refresh());
assert_fill("at rest", &only_quad(cx, "at rest"), resting);
cx.update(|window, _| window.refresh());
let before = renders.get();
cx.simulate_mouse_move(
point(px(INSIDE_TRACK.0), px(INSIDE_TRACK.1)),
None,
Modifiers::none(),
);
let after = renders.get();
assert!(
after > before,
"crossing onto the track must repaint the view ({before} renders \
before, {after} after)"
);
cx.update(|window, _| window.refresh());
assert_fill("hovered", &only_quad(cx, "hovered"), hover);
cx.simulate_mouse_move(point(px(-100.), px(-100.)), None, Modifiers::none());
cx.update(|window, _| window.refresh());
assert_fill("left again", &only_quad(cx, "left again"), resting);
}
#[gpui::test]
fn without_a_hover_color_hovering_changes_nothing(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let cx = open_host(cx, move || {
let scroller = handle.clone();
vertical_bar(scroller, Scrollbar::new("bar", handle.clone()))
});
cx.update(|window, _| window.refresh());
let token = cx.update(|_, app| app.colors().scrollbar);
cx.simulate_mouse_move(
point(px(INSIDE_TRACK.0), px(INSIDE_TRACK.1)),
None,
Modifiers::none(),
);
cx.update(|window, _| window.refresh());
assert_fill(
"hovered without a hover color",
&only_quad(cx, "hovered without a hover color"),
token,
);
}
#[gpui::test]
fn auto_hide_false_keeps_the_thumb_painted(cx: &mut TestAppContext) {
let handle = ScrollHandle::new();
let cx = open_host(cx, move || {
let scroller = handle.clone();
vertical_bar(
scroller,
Scrollbar::new("bar", handle.clone()).auto_hide(false),
)
});
cx.update(|window, _| window.refresh());
let thumb_len = VIEWPORT * (VIEWPORT / CONTENT);
let scale = scale_of(cx);
let quad = only_quad(cx, "auto_hide(false)");
assert_thumb(
"auto_hide(false)",
&quad,
scale,
TRACK_W - 8.,
0.,
8.,
thumb_len,
4.,
);
}