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(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 { let Some(active) = h.render.active.get(&id) else { return Vec::new(); }; let mut out: Vec = 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 { 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, ); }