Transitions: play transition preview from previous slide when Preview is clicked (#1654)

- When Preview is clicked on the Transitions ribbon, initialize Show with
  start_transition_preview from the prior slide into the selected slide
  (or from black for slide 0).
- If tr.old is unset for a non-part transition with tr.from, render the
  previous slide texture on the first frame so the transition blends cleanly.
- Indicate active playback on the Preview button while previewing.
- Regression tests for transitions from previous slide and from black on slide 0.
This commit is contained in:
Naseq
2026-11-02 15:04:05 +04:30
parent e690829543
commit ea9dad8ef9
4 changed files with 73 additions and 5 deletions
+8 -5
View File
@@ -12,11 +12,14 @@ pub(super) fn transitions(app: &mut SlideApp, ui: &mut Ui) {
let cur = app.session.active().and_then(|d| d.current_slide().and_then(|s| s.transition.clone()));
let kind = cur.as_ref().map(|t| t.kind.clone()).unwrap_or_else(|| "none".into());
group(ui, |ui| {
if big_button(ui, Icon::Preview, "Preview", true).clicked() {
let from = app.session.active().map(|d| d.selection.slide).unwrap_or(0);
app.start_show(from, true);
if let Some(s) = app.show.as_mut() {
s.preview_only = true;
let playing = app.show.as_ref().is_some_and(|s| s.preview_only);
let (resp, _) = widgets::big_button_ex(ui, Icon::Preview, "Preview", true, false, playing);
if resp.on_hover_text("Play this slide's transition").clicked() {
let to = app.session.active().map(|d| d.selection.slide).unwrap_or(0);
app.start_show(to, true);
let now = ui.input(|i| i.time);
if let (Some(s), Some(d)) = (app.show.as_mut(), app.session.active()) {
s.start_transition_preview(&d.doc, now);
}
}
});
+21
View File
@@ -261,6 +261,27 @@ impl Show {
}
}
/// Play this slide's transition from the previous slide (or black for slide 0) as a preview (#1654).
pub fn start_transition_preview(&mut self, doc: &Presentation, now: f64) {
let i = self.state.slide;
let from = i.checked_sub(1);
let (kind, option, dur) = doc
.slides
.get(i)
.and_then(|s| s.transition.as_ref())
.map(|t| (t.kind.clone(), t.option.clone(), t.duration_ms as f64 / 1000.0))
.unwrap_or(("none".into(), String::new(), 0.0));
self.trans = if kind != "none" && dur > 0.0 && !doc.show.without_animation {
Some(Transition { old: None, from, morph: None, parts: None, start: now, kind, option, dur, reverse: false })
} else {
None
};
self.preview_only = true;
self.slide_start = now;
self.step_start = now;
self.step_end = now;
}
/// Identity of the show state a queued window-point event belongs to.
pub(crate) fn input_state(&self) -> ShowInputState {
let bits = |v: f32| if v.is_finite() { v.to_bits() } else { 0 };
+34
View File
@@ -1238,3 +1238,37 @@ fn a_preview_of_an_edited_presentation_is_rendered_again() {
let tex = harness.state().1.next_tex.as_ref().map(|(d, k, _)| (std::sync::Arc::ptr_eq(d, &edited), *k));
assert_eq!(tex, Some((true, 1)), "the preview comes from the edited presentation");
}
/// #1654: Preview on the Transitions ribbon plays the current slide's transition from the
/// previous slide into the current slide, and exits when finished.
#[test]
fn transition_preview_plays_from_the_previous_slide() {
let mut s = deckcraft_engine::Session::with_new();
s.execute("slide.new", &json!({})).unwrap();
s.execute("transition.set", &json!({"kind": "push", "index": 1, "duration": 1.0})).unwrap();
let doc = s.active().unwrap().doc.clone();
let mut show = Show::new(&doc, 1, true, false);
show.start_transition_preview(&doc, 10.0);
assert!(show.preview_only);
let tr = show.trans.as_ref().expect("transition preview is playing");
assert_eq!(tr.kind, "push");
assert_eq!(tr.from, Some(0), "enters from slide 0 into slide 1");
assert_eq!(tr.dur, 1.0);
assert!(!tr.reverse);
assert_eq!(tr.start, 10.0);
}
/// #1654: Transition preview for slide 0 plays with from = None (transitioning from black).
#[test]
fn transition_preview_for_first_slide_transitions_from_black() {
let mut s = deckcraft_engine::Session::with_new();
s.execute("transition.set", &json!({"kind": "fade", "index": 0, "duration": 500})).unwrap();
let doc = s.active().unwrap().doc.clone();
let mut show = Show::new(&doc, 0, true, false);
show.start_transition_preview(&doc, 5.0);
assert!(show.preview_only);
let tr = show.trans.as_ref().expect("fade transition preview is playing");
assert_eq!(tr.kind, "fade");
assert_eq!(tr.from, None, "slide 0 transitions from black (None)");
assert_eq!(tr.dur, 0.5);
}
+10
View File
@@ -388,6 +388,16 @@ fn draw_transition(show: &mut Show, f: &Fr, sl: &Sl, painter: &egui::Painter, te
if tr.parts.is_none() && deckcraft_anim::uses_slide_parts(&tr.kind, &tr.option) {
tr.parts = Some(part_textures(ctx, doc, tr.from, idx, size));
}
if tr.old.is_none()
&& let Some(prev) = tr.from
&& !deckcraft_anim::uses_slide_parts(&tr.kind, &tr.option)
{
let threads =
if cfg!(target_arch = "wasm32") { 0 } else { std::thread::available_parallelism().map(|n| n.get().min(8) as u16).unwrap_or(0) };
let opts = RenderOpts { scale: size.0 as f64 / doc.slide_size.width.max(1.0), size: Some(size), threads, ..Default::default() };
let img = deckcraft_render::render_slide(doc, prev, &opts);
tr.old = Some(crate::textures::upload(ctx, "trans-old", crate::textures::to_color_image(&img, false)));
}
let aspect = doc.slide_size.width / doc.slide_size.height.max(1.0);
let part = |k: usize| tr.parts.as_ref().and_then(|p| p.get(k).cloned().flatten());
let parts = [part(0), part(1), part(2), part(3)];