Files
deckcraft/crates/ui-egui/src/huge_deck_tests.rs
T
Hamza 42e89b347d Thumbnails: count the renders, not only the thumbnails held (#969)
Follow-up to #1011, as asked on #1038: a counter of thumbnails actually
rendered, and tests that use it.

`Textures::thumb_count` is the number of thumbnails *held*. It settles at the
rows in view whether the pane rendered them once or a thousand times, so it
cannot see a pane that renders the same rows over and over -- which is what
#969 was: the flashing white was the rendering, not the holding.

`Textures::thumb_renders` counts the renders, and a settled pane is one that
has stopped asking for them.

The two new tests assert what the held count cannot:

- every thumbnail held was rendered, and not many more than that were rendered
  and thrown away (measured: 8 held, 8 rendered at rest);
- ten more frames of the same view render nothing at all;
- after a 40,000 pt scroll of an 800-slide deck, 24 renders in total -- the
  rows it landed on and the ones it passed over, not the deck -- and then it
  settles again.

With the cache made to thrash (its 600-entry limit cut to 4, so the rows in
view are evicted and re-rendered every frame), the merged test for the Slide
Sorter still passes -- the held count settles at 4 -- while this one fails with
"255 renders for 4 thumbnails". That is the hole the counter closes.
2026-10-22 19:20:07 +05:00

134 lines
6.1 KiB
Rust

//! Large presentations in the editor: views that list every slide lay out only what is in view.
#![allow(clippy::unwrap_used, clippy::expect_used)]
use egui::accesskit::Role;
use egui_kittest::kittest::Queryable;
use serde_json::json;
use crate::{Services, SlideApp};
use deckcraft_engine::Session;
fn harness(n: usize) -> egui_kittest::Harness<'static, SlideApp> {
let mut s = Session::with_new();
for _ in 1..n {
s.execute("slide.new", &json!({})).unwrap();
}
egui_kittest::Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
SlideApp::new(s, Services::default()),
)
}
/// Outline view of 2000 slides lays out the slides in view (and the current one), not all 2000
/// title editors every frame (40 ms a frame in a release build, growing with the text).
#[test]
fn the_outline_of_a_huge_deck_lays_out_only_what_is_in_view() {
let mut h = harness(2000);
h.state_mut().run("view.outline", json!({})).unwrap();
h.run_steps(3);
let editors = h.query_all_by_role(Role::TextInput).count();
assert!(editors > 5 && editors < 200, "{editors} text editors laid out");
// The current slide is always laid out, wherever it is.
h.state_mut().run("slide.go", json!({"index": 1500})).unwrap();
h.run_steps(3);
assert!(h.query_all_by_role(Role::TextInput).count() < 200);
assert_eq!(h.state().session.doc().unwrap().selection.slide, 1500);
}
/// The thumbnail pane of an 800-slide deck renders only the thumbnails in view. When every row
/// asked for one, a deck bigger than the 600-entry thumbnail cache evicted its own thumbnails before
/// the pane was done, so the pane kept rendering and flashing white forever (#969).
#[test]
fn the_thumbnail_pane_of_a_huge_deck_renders_only_the_slides_in_view() {
let mut h = harness(800);
h.run_steps(60);
let settled = h.state().textures.thumb_count();
assert!(settled > 3 && settled < 60, "{settled} thumbnails rendered");
// The visible thumbnails are done: more frames render nothing new.
h.run_steps(10);
assert_eq!(h.state().textures.thumb_count(), settled);
// Scrolled far down, the slides now in view get their thumbnails, and only those.
h.hover_at(egui::pos2(100.0, 450.0));
h.event(egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, -40_000.0),
modifiers: egui::Modifiers::NONE,
phase: egui::TouchPhase::Move,
});
h.run_steps(60);
let scrolled = h.state().textures.thumb_count();
assert!(scrolled > settled && scrolled < 2 * settled + 10, "{scrolled} thumbnails after scrolling (was {settled})");
}
/// The same in the Slide Sorter: only the cells in view render a thumbnail (#969).
#[test]
fn the_sorter_of_a_huge_deck_renders_only_the_slides_in_view() {
let mut h = harness(800);
h.state_mut().ui.view = crate::ViewMode::Sorter;
h.run_steps(60);
let settled = h.state().textures.thumb_count();
assert!(settled > 3 && settled < 80, "{settled} thumbnails rendered");
h.run_steps(10);
assert_eq!(h.state().textures.thumb_count(), settled);
}
/// What the count of thumbnails *held* cannot see: a pane that renders the same rows over and
/// over. The cache settles at the rows in view either way, so `thumb_count` is the same whether
/// the pane rendered them once or a thousand times — and the flashing white of #969 was the
/// rendering, not the holding. `thumb_renders` counts the renders, so a settled pane is one that
/// has stopped asking for them.
///
/// A follow-up to #1011, as skylartaylor asked on #1038.
#[test]
fn a_settled_thumbnail_pane_renders_nothing_more() {
let mut h = harness(800);
h.run_steps(60);
let (held, rendered) = (h.state().textures.thumb_count(), h.state().textures.thumb_renders());
// Every thumbnail held was rendered, and the pane did not render many it then threw away:
// this is the assertion a count of held thumbnails cannot make, and it comes first so that a
// thrashing pane is reported as thrashing rather than as holding too few.
assert!(rendered >= held as u64, "{rendered} renders for {held} thumbnails");
assert!(rendered < held as u64 * 2, "{rendered} renders for {held} thumbnails: the pane is re-rendering");
assert!(held > 3 && held < 60, "{held} thumbnails held");
// Ten more frames of the same view: not one more render.
h.run_steps(10);
assert_eq!(h.state().textures.thumb_renders(), rendered, "a settled pane renders nothing more");
assert_eq!(h.state().textures.thumb_count(), held);
// Scrolled far down: the slides now in view are rendered, and only those -- the renders go up
// by about a screen's worth, not by the whole deck.
h.hover_at(egui::pos2(100.0, 450.0));
h.event(egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, -40_000.0),
modifiers: egui::Modifiers::NONE,
phase: egui::TouchPhase::Move,
});
h.run_steps(60);
let after = h.state().textures.thumb_renders();
// Measured: 8 held and 8 rendered at rest, 24 rendered after the scroll -- the rows it
// landed on plus the ones it passed over, not the 800 slides of the deck.
assert!(after > rendered, "the slides now in view are rendered: {after} vs {rendered}");
assert!(after < 100, "{after} renders after one scroll of an 800-slide deck (was {rendered})");
// And it settles again.
h.run_steps(10);
assert_eq!(h.state().textures.thumb_renders(), after, "settled again");
}
/// The same for the Slide Sorter: it settles, and stays settled.
#[test]
fn a_settled_sorter_renders_nothing_more() {
let mut h = harness(800);
h.state_mut().ui.view = crate::ViewMode::Sorter;
h.run_steps(60);
let (held, rendered) = (h.state().textures.thumb_count(), h.state().textures.thumb_renders());
assert!(rendered >= held as u64 && rendered < held as u64 * 2, "{rendered} renders for {held} thumbnails");
assert!(held > 3 && held < 80, "{held} thumbnails held");
h.run_steps(10);
assert_eq!(h.state().textures.thumb_renders(), rendered, "a settled sorter renders nothing more");
}