250 lines
7.6 KiB
Rust
250 lines
7.6 KiB
Rust
use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
|
|
use iris::harness::Harness;
|
|
use iris::prelude::*;
|
|
|
|
const HEADER_H: f32 = 300.0;
|
|
const HEADER: Srgba8 = Srgba8::new(28, 28, 34, 255);
|
|
|
|
fn opened() -> (Harness, ai_app::ui::TranscriptScreen) {
|
|
let mut h = Harness::new(phone_size(), PHONE_SCALE);
|
|
let (opened, tree) = ai_app::ui::fixture::build_screen(&mut h.rsc).expect("the fixture folds");
|
|
let content = WidgetPtr::new().add(&mut h.rsc);
|
|
content(&mut h.rsc).set(tree);
|
|
let root = (rect(HEADER).height(abs(HEADER_H)), content.height(rest(1)))
|
|
.span(Dir::DOWN)
|
|
.add_strong(&mut h.rsc)
|
|
.any();
|
|
h.state.set_root(&mut h.rsc, root);
|
|
h.frame(0);
|
|
h.frame(PHONE_FRAME_MS);
|
|
(h, opened.screen)
|
|
}
|
|
|
|
fn list_box(h: &Harness, screen: &ai_app::ui::TranscriptScreen) -> PixelRegion {
|
|
h.render
|
|
.window_region(&screen.list.id(), &h.rsc)
|
|
.expect("the list is on screen")
|
|
}
|
|
|
|
fn drawn_rows(h: &Harness, screen: &ai_app::ui::TranscriptScreen) -> Vec<(f32, f32)> {
|
|
let mut rows: Vec<(f32, f32)> = h
|
|
.render
|
|
.active
|
|
.get(&screen.list.id())
|
|
.expect("the list is drawn")
|
|
.children
|
|
.iter()
|
|
.filter_map(|id| h.render.window_region(id, &h.rsc))
|
|
.map(|px| (px.top_left.y, px.bot_right.y))
|
|
.collect();
|
|
rows.sort_by(|a, b| a.0.total_cmp(&b.0));
|
|
rows
|
|
}
|
|
|
|
fn scrolled(h: &mut Harness, screen: &ai_app::ui::TranscriptScreen, amount: f32, t: u64) -> u64 {
|
|
(screen.list)(&mut h.rsc).scroll(amount);
|
|
h.frame(t);
|
|
t + PHONE_FRAME_MS
|
|
}
|
|
|
|
#[test]
|
|
fn the_row_across_the_top_edge_is_drawn() {
|
|
let (mut h, screen) = opened();
|
|
let top = list_box(&h, &screen).top_left.y;
|
|
let mut t = PHONE_FRAME_MS * 2;
|
|
|
|
for _ in 0..60 {
|
|
t = scrolled(&mut h, &screen, 40.0, t);
|
|
let rows = drawn_rows(&h, &screen);
|
|
let first = *rows.first().expect("something is on screen");
|
|
assert!(
|
|
first.0 <= top + 0.5,
|
|
"a band of {:.1}px under the header belongs to no row: rows start at {:.1}, the list \
|
|
at {top:.1}",
|
|
first.0 - top,
|
|
first.0,
|
|
);
|
|
assert!(
|
|
first.1 > top,
|
|
"the row across the top edge was culled: it ends at {:.1}, above the list's own \
|
|
{top:.1}",
|
|
first.1,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_list_is_clipped_to_its_own_box() {
|
|
let (h, screen) = opened();
|
|
let active = h.render.active.get(&screen.list.id()).expect("drawn");
|
|
assert!(
|
|
active.mask != MaskIdx::NONE,
|
|
"the transcript's list is drawn with nothing clipping it",
|
|
);
|
|
let clip = h.render.mask_region(active.mask, &h.rsc);
|
|
let list = list_box(&h, &screen);
|
|
assert!(
|
|
clip.top_left.y >= list.top_left.y - 0.5 && clip.bot_right.y <= list.bot_right.y + 0.5,
|
|
"the clip {clip:?} reaches outside the list's own box {list:?}, so a row straddling an \
|
|
edge still draws past it",
|
|
);
|
|
|
|
let rows = h
|
|
.render
|
|
.active
|
|
.get(&screen.list.id())
|
|
.expect("the list is drawn")
|
|
.children
|
|
.clone();
|
|
let mut checked = 0;
|
|
for row in rows {
|
|
for prim in primitives_under(&h, row) {
|
|
assert!(
|
|
mask_chain(&h, prim).contains(&active.mask),
|
|
"a primitive of row {row:?} clips to {:?}, a chain that never reaches the list's \
|
|
own mask {:?}",
|
|
mask_chain(&h, prim),
|
|
active.mask,
|
|
);
|
|
checked += 1;
|
|
}
|
|
}
|
|
assert!(
|
|
checked > 0,
|
|
"no row primitive was checked, so this test asserted nothing",
|
|
);
|
|
}
|
|
|
|
fn primitives_under(h: &Harness, id: WidgetId) -> Vec<MaskIdx> {
|
|
let Some(active) = h.render.active.get(&id) else {
|
|
return Vec::new();
|
|
};
|
|
let mut out: Vec<MaskIdx> = active
|
|
.primitives
|
|
.iter()
|
|
.filter(|p| p.binding != IMAGE_BINDING)
|
|
.map(|p| h.render.primitives.instance(p.slot).mask_idx)
|
|
.collect();
|
|
for child in &active.children {
|
|
out.extend(primitives_under(h, *child));
|
|
}
|
|
out
|
|
}
|
|
|
|
fn mask_chain(h: &Harness, mask: MaskIdx) -> Vec<MaskIdx> {
|
|
let mut chain = Vec::new();
|
|
let mut at = mask;
|
|
while at != MaskIdx::NONE {
|
|
assert!(
|
|
!chain.contains(&at),
|
|
"the mask chain from {mask:?} loops back to {at:?}",
|
|
);
|
|
chain.push(at);
|
|
at = h.rsc.ui.masks[at.idx()].parent;
|
|
}
|
|
chain
|
|
}
|
|
|
|
#[test]
|
|
fn rows_that_have_left_the_viewport_are_not_drawn() {
|
|
let (mut h, screen) = opened();
|
|
let list = list_box(&h, &screen);
|
|
let mut t = PHONE_FRAME_MS * 2;
|
|
let bounded = |rows: &[(f32, f32)], leg: &str, step: usize| {
|
|
assert!(
|
|
rows.len() <= 24,
|
|
"{leg} {step}: {} rows drawn for one 2012px viewport",
|
|
rows.len(),
|
|
);
|
|
};
|
|
let inside = |rows: &[(f32, f32)], leg: &str, step: usize| {
|
|
for &(top, bottom) in rows {
|
|
assert!(
|
|
bottom > list.top_left.y - 0.5 && top < list.bot_right.y + 0.5,
|
|
"{leg} {step}: a row at ({top:.1}, {bottom:.1}) is outside the list's box \
|
|
{list:?} and was drawn anyway",
|
|
);
|
|
}
|
|
};
|
|
|
|
for step in 0..40 {
|
|
t = scrolled(&mut h, &screen, 400.0, t);
|
|
bounded(&drawn_rows(&h, &screen), "measuring", step);
|
|
}
|
|
for step in 0..40 {
|
|
t = scrolled(&mut h, &screen, -400.0, t);
|
|
let rows = drawn_rows(&h, &screen);
|
|
bounded(&rows, "forward", step);
|
|
inside(&rows, "forward", step);
|
|
}
|
|
for step in 0..40 {
|
|
t = scrolled(&mut h, &screen, 400.0, t);
|
|
let rows = drawn_rows(&h, &screen);
|
|
bounded(&rows, "back", step);
|
|
inside(&rows, "back", step);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_row_across_the_bottom_edge_is_drawn() {
|
|
let (mut h, screen) = opened();
|
|
let list = list_box(&h, &screen);
|
|
let mut t = PHONE_FRAME_MS * 2;
|
|
|
|
for _ in 0..40 {
|
|
t = scrolled(&mut h, &screen, 37.0, t);
|
|
let rows = drawn_rows(&h, &screen);
|
|
let last = *rows.last().expect("something is on screen");
|
|
assert!(
|
|
last.1 >= list.bot_right.y - 0.5,
|
|
"a band of {:.1}px above the composer belongs to no row",
|
|
list.bot_right.y - last.1,
|
|
);
|
|
assert!(
|
|
last.0 < list.bot_right.y,
|
|
"the row across the bottom edge was culled: it starts at {:.1}, below the list's own \
|
|
{:.1}",
|
|
last.0,
|
|
list.bot_right.y,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn scrolling_past_the_first_row_settles_on_it() {
|
|
let (mut h, screen) = opened();
|
|
let list = list_box(&h, &screen);
|
|
let mut t = PHONE_FRAME_MS * 2;
|
|
|
|
for _ in 0..60 {
|
|
t = scrolled(&mut h, &screen, 100_000.0, t);
|
|
}
|
|
let rows = drawn_rows(&h, &screen);
|
|
let first = *rows.first().expect("the first row is on screen");
|
|
assert!(
|
|
(first.0 - list.top_left.y).abs() < 0.5,
|
|
"the transcript is parked {:.1}px past its own first row, so the top of the list is blank",
|
|
first.0 - list.top_left.y,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scrolling_past_the_last_row_settles_on_it() {
|
|
let (mut h, screen) = opened();
|
|
let list = list_box(&h, &screen);
|
|
let mut t = PHONE_FRAME_MS * 2;
|
|
|
|
for _ in 0..20 {
|
|
t = scrolled(&mut h, &screen, -100_000.0, t);
|
|
}
|
|
|
|
let rows = drawn_rows(&h, &screen);
|
|
let last = *rows.last().expect("the last row is on screen");
|
|
assert!(
|
|
(last.1 - list.bot_right.y).abs() < 0.5,
|
|
"the transcript is parked {:.1}px past its own last row, so the bottom of the list is \
|
|
blank",
|
|
list.bot_right.y - last.1,
|
|
);
|
|
}
|