diff --git a/crates/ui-egui/src/dialogs/settings/slide_layout.rs b/crates/ui-egui/src/dialogs/settings/slide_layout.rs index 2fc55abda..c78d3df4d 100644 --- a/crates/ui-egui/src/dialogs/settings/slide_layout.rs +++ b/crates/ui-egui/src/dialogs/settings/slide_layout.rs @@ -247,9 +247,14 @@ pub fn preview(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, max_h: f3 } else { ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64((DEBOUNCE - still).clamp(0.01, DEBOUNCE))); } + let say = + |ui: &mut egui::Ui| hint(ui, t, "The selected photo as a slide. Playback fills the screen; the aspect preview only changes this preview."); let Some(tex) = app.renderer.textures.get(&Slot::SlidePreview).filter(|tex| tex.photo == id) else { p.rect_filled(rect, 0.0, Color32::from_rgb(s.backdrop_color[0], s.backdrop_color[1], s.backdrop_color[2])); p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("Rendering…"), t.font(11.5), Color32::from_gray(200)); + // (the hint too: the render lands on a frame nobody can name, and a line that came with it would push + // the pinned preview's block, and the page under it, down while a control is being clicked) + say(ui); return; }; let drawn = crate::panels::detail::fit_texture_rect(rect, tex.size); @@ -269,7 +274,7 @@ pub fn preview(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, max_h: f3 p.line_segment([pos2(drawn.left(), gy), pos2(drawn.right(), gy)], stroke); } } - hint(ui, t, "The selected photo as a slide. Playback fills the screen; the aspect preview only changes this preview."); + say(ui); } /// The preview showing title screen `title` (composed off the UI thread like the photo's slide, diff --git a/crates/ui-egui/src/tests/slideshow.rs b/crates/ui-egui/src/tests/slideshow.rs index 26c72c2cd..137dd9317 100644 --- a/crates/ui-egui/src/tests/slideshow.rs +++ b/crates/ui-egui/src/tests/slideshow.rs @@ -400,6 +400,29 @@ fn playback_settings_drive_the_slideshow_and_bad_ones_are_refused() { h.settle(SETTLE); } +/// The slide preview's render lands on a frame nobody can name (a worker thread); the page under it must +/// not move when it does, or a click made in that frame lands beside the control aimed at (the gate went +/// red on it: `playback_settings_…` clicks "Random order" and the checkbox never changed). +#[test] +fn the_settings_page_does_not_move_when_the_slide_preview_finishes_rendering() { + let mut h = grid(); + let open = std::rc::Rc::new(std::cell::Cell::new(false)); + h.app.renderer.set_offload(Box::new(crate::headless::RenderGate::new(open.clone()))); + run(&mut h, "view.slideshowOptions", json!({})); + // (the held render keeps the app "busy", so settle() would wait out its timeout: a second of frames instead) + for _ in 0..60 { + h.step(); + } + let rect = |h: &Headless, w: &str| h.app.widgets.iter().find(|(id, _)| id == w).map(|(_, r)| *r).unwrap_or_else(|| panic!("{w} is on screen")); + let (room, check) = (rect(&h, "settings:pageRoom"), rect(&h, "check:settings.slideshowRandom")); + assert!(!h.app.renderer.textures.contains_key(&Slot::SlidePreview), "the render is held back"); + open.set(true); + assert!(h.step_until(SETTLE, |h| h.app.renderer.textures.contains_key(&Slot::SlidePreview)), "the render lands"); + h.settle(Duration::from_secs(5)); + assert_eq!(rect(&h, "settings:pageRoom"), room, "the page's room is the same before and after the render"); + assert_eq!(rect(&h, "check:settings.slideshowRandom"), check, "a control stays where it was drawn"); +} + #[test] fn a_click_on_a_control_scrolled_out_of_the_settings_page_is_refused() { // A compact window leaves the lower controls of the Slideshow settings page outside the diff --git a/docs/whats-new.md b/docs/whats-new.md index c6c259200..f729f0aab 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -2,6 +2,11 @@ ## 2026-10-24 +### The Slideshow settings page stays put while the preview renders +- In Settings ▸ Slideshow, the line under the slide preview ("The selected photo as a slide…") now shows from the first + frame instead of only once the preview has finished rendering. Before, it appeared with the render, pushing the page + below it down by a line, so a click made in that moment could land beside the control you aimed at. + ### Relative-date filters and smart albums follow the clock - A current view or source count using “in the last” rules now refreshes when the session clock moves into a new minute, even when no catalog edit occurred. This fixes stale photos remaining in the grid and stale counts for