mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-26 11:36:58 -06:00
Thumbnails: only render the slides in view (#969)
The slide thumbnail pane and the Slide Sorter asked the texture cache for a thumbnail on every row, in view or not, three or four new ones a frame. The cache keeps at most 600 thumbnails and drops the 200 oldest beyond that, so on a deck of more than 600 slides the pane evicted its own thumbnails before it reached the end: it kept rendering, the rows in view went white again and again, and it never finished. Now only rows within the scroll area's clip (plus two rows of margin) ask for a thumbnail and keep the pane repainting; the others are still laid out, so scroll height, section headers, clicks and drag-to-reorder are unchanged. Small decks behave as before. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
|||||||
|
|
||||||
| Date | What landed |
|
| Date | What landed |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| 2026-10-18 | Slide thumbnails and the Slide Sorter render only the slides in view: on a deck of more than 600 slides they used to evict their own thumbnails, so the pane flashed white and never finished (#969) |
|
||||||
| 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort |
|
| 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort |
|
||||||
| 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape |
|
| 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape |
|
||||||
| 2026-10-17 | Edit Points: turning a shape into a freeform is one undo step, so Undo brings the original shape back (it used to stay a freeform) |
|
| 2026-10-17 | Edit Points: turning a shape into a freeform is one undo step, so Undo brings the original shape back (it used to stay a freeform) |
|
||||||
@@ -237,6 +238,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-18 | trivial | Progress log: thumbnails of huge decks render only the slides in view (#969) |
|
||||||
| 2026-10-15 | trivial | Progress log: PPTX trigger click-step round trip. Feature scores and milestone names unchanged |
|
| 2026-10-15 | trivial | Progress log: PPTX trigger click-step round trip. Feature scores and milestone names unchanged |
|
||||||
| 2026-10-16 | trivial | Progress log: PNG export with a transparent background (#784) |
|
| 2026-10-16 | trivial | Progress log: PNG export with a transparent background (#784) |
|
||||||
| 2026-10-16 | trivial | Progress log: a `.ppsx` from the system starts the slide show (#862) |
|
| 2026-10-16 | trivial | Progress log: a `.ppsx` from the system starts the slide show (#862) |
|
||||||
|
|||||||
@@ -37,3 +37,40 @@ fn the_outline_of_a_huge_deck_lays_out_only_what_is_in_view() {
|
|||||||
assert!(h.query_all_by_role(Role::TextInput).count() < 200);
|
assert!(h.query_all_by_role(Role::TextInput).count() < 200);
|
||||||
assert_eq!(h.state().session.doc().unwrap().selection.slide, 1500);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -203,13 +203,17 @@ fn place_slides(app: &mut SlideApp, ui: &mut Ui, g: &Grid<'_>, budget: &mut u32,
|
|||||||
let img = crate::textures::device_pixel_rect(Rect::from_min_size(cell.min + vec2(18.0, 8.0), vec2(g.tw, g.th)), ppp);
|
let img = crate::textures::device_pixel_rect(Rect::from_min_size(cell.min + vec2(18.0, 8.0), vec2(g.tw, g.th)), ppp);
|
||||||
rects.push((i, cell));
|
rects.push((i, cell));
|
||||||
let p = ui.painter();
|
let p = ui.painter();
|
||||||
match app.textures.thumb(ui.ctx(), g.doc, i, (img.width() * ppp).round() as u32, budget) {
|
// Only cells in view (plus a little margin) ask for a thumbnail, so a deck bigger
|
||||||
|
// than the thumbnail cache does not evict its own thumbnails forever (#969).
|
||||||
|
let near = ui.clip_rect().expand2(vec2(0.0, 2.0 * cell.height())).intersects(cell);
|
||||||
|
let tex = if near { app.textures.thumb(ui.ctx(), g.doc, i, (img.width() * ppp).round() as u32, budget) } else { None };
|
||||||
|
match tex {
|
||||||
Some(tex) => {
|
Some(tex) => {
|
||||||
p.image(tex.id(), img, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
p.image(tex.id(), img, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
p.rect_filled(img, CornerRadius::ZERO, Color32::WHITE);
|
p.rect_filled(img, CornerRadius::ZERO, Color32::WHITE);
|
||||||
need_more = true;
|
need_more |= near;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let id = g.doc.slides.get(i).map(|s| s.id);
|
let id = g.doc.slides.get(i).map(|s| s.id);
|
||||||
|
|||||||
@@ -115,7 +115,11 @@ fn list(app: &mut SlideApp, ui: &mut Ui) {
|
|||||||
p.text(pos2(row.min.x + 14.0, img.min.y + 20.0), Align2::CENTER_TOP, "★", theme::font(10.0), t.text_faint);
|
p.text(pos2(row.min.x + 14.0, img.min.y + 20.0), Align2::CENTER_TOP, "★", theme::font(10.0), t.text_faint);
|
||||||
}
|
}
|
||||||
let wpx = (img.width() * ppp).round() as u32;
|
let wpx = (img.width() * ppp).round() as u32;
|
||||||
match app.textures.thumb(ui.ctx(), &doc, i, wpx, &mut budget) {
|
// Only rows in view (plus a little margin) ask for a thumbnail: with every row asking, a
|
||||||
|
// deck bigger than the thumbnail cache evicts its own thumbnails and never settles (#969).
|
||||||
|
let near = ui.clip_rect().expand2(vec2(0.0, 2.0 * row.height())).intersects(row);
|
||||||
|
let tex = if near { app.textures.thumb(ui.ctx(), &doc, i, wpx, &mut budget) } else { None };
|
||||||
|
match tex {
|
||||||
Some(tex) => {
|
Some(tex) => {
|
||||||
p.image(
|
p.image(
|
||||||
tex.id(),
|
tex.id(),
|
||||||
@@ -126,7 +130,7 @@ fn list(app: &mut SlideApp, ui: &mut Ui) {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
p.rect_filled(img, CornerRadius::ZERO, Color32::WHITE);
|
p.rect_filled(img, CornerRadius::ZERO, Color32::WHITE);
|
||||||
need_more = true;
|
need_more |= near;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if selected {
|
if selected {
|
||||||
|
|||||||
Reference in New Issue
Block a user