diff --git a/crates/ui-egui/src/ribbon/transitions.rs b/crates/ui-egui/src/ribbon/transitions.rs index 3f43ebd9..6986d828 100644 --- a/crates/ui-egui/src/ribbon/transitions.rs +++ b/crates/ui-egui/src/ribbon/transitions.rs @@ -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); } } }); diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 1896e3be..7bd2edf7 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -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 }; diff --git a/crates/ui-egui/src/show/tests.rs b/crates/ui-egui/src/show/tests.rs index a1069a5b..5e39bd18 100644 --- a/crates/ui-egui/src/show/tests.rs +++ b/crates/ui-egui/src/show/tests.rs @@ -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); +} diff --git a/crates/ui-egui/src/show/ui.rs b/crates/ui-egui/src/show/ui.rs index 71107f94..321f8116 100644 --- a/crates/ui-egui/src/show/ui.rs +++ b/crates/ui-egui/src/show/ui.rs @@ -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)];