From 2303efe7d5e696703eade575e6c36e61606a7377 Mon Sep 17 00:00:00 2001 From: Faabyy <68284158+Faabyy@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:05:19 +0000 Subject: [PATCH] Media: stop a clip across slides when the next slide reuses its id (#994) A clip set to play across slides kept its voice even when the next slide holds a different clip under the same shape id, so that slide's own clip never started. Keep the voice only when the next slide has no clip under that id or the same media. Co-Authored-By: Claude --- crates/ui-egui/src/media.rs | 52 +++++++++++++++++++++++++++++++------ crates/ui-egui/src/show.rs | 4 +-- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/media.rs b/crates/ui-egui/src/media.rs index 344689b2..e26b475c 100644 --- a/crates/ui-egui/src/media.rs +++ b/crates/ui-egui/src/media.rs @@ -337,14 +337,27 @@ impl MediaHost { /// The clip of media shape `id`. Ids are only unique per slide (the PPTX import numbers each slide /// afresh), so the `near` slide is searched first, then the rest of the deck. pub fn find_clip(doc: &Presentation, id: ShapeId, near: Option) -> Option { - fn walk(shapes: &[deckcraft_model::Shape], id: ShapeId) -> Option { - shapes.iter().find_map(|s| match &s.kind { - ShapeKind::Media(m) if s.id == id => Some(m.clone()), - ShapeKind::Group { children, .. } => walk(children, id), - _ => None, - }) - } - near.and_then(|i| doc.slides.get(i)).and_then(|s| walk(&s.shapes, id)).or_else(|| doc.slides.iter().find_map(|s| walk(&s.shapes, id))) + near.and_then(|i| clip_on(doc, i, id)).or_else(|| doc.slides.iter().find_map(|s| walk_clip(&s.shapes, id))) +} + +/// The clip `id` on slide `index` only (inside groups too). +pub fn clip_on(doc: &Presentation, index: usize, id: ShapeId) -> Option { + doc.slides.get(index).and_then(|s| walk_clip(&s.shapes, id)) +} + +fn walk_clip(shapes: &[deckcraft_model::Shape], id: ShapeId) -> Option { + shapes.iter().find_map(|s| match &s.kind { + ShapeKind::Media(m) if s.id == id => Some(m.clone()), + ShapeKind::Group { children, .. } => walk_clip(children, id), + _ => None, + }) +} + +/// Whether the voice `id`, playing on slide `from`, keeps playing on slide `to`: its clip plays +/// across slides and `to` doesn't hold another clip under the same id. +pub fn carries_over(doc: &Presentation, id: ShapeId, from: Option, to: usize) -> bool { + let Some(clip) = find_clip(doc, id, from).filter(|c| c.play_across_slides) else { return false }; + clip_on(doc, to, id).is_none_or(|c| c.media == clip.media) } /// Media shapes directly on slide `index`: (id, box in slide points, clip). @@ -633,4 +646,27 @@ mod tests { assert_eq!(find_clip(&doc, id, None).map(|c| c.media), Some(first.media), "without a hint: first match"); assert_eq!(find_clip(&doc, id, Some(9)).map(|c| c.media), Some(first.media), "a slide that doesn't exist"); } + + #[test] + fn a_clip_across_slides_stops_where_the_next_slide_reuses_its_id() { + let (mut doc, id, first) = deck(4000, wav_level(0.05, 0.5)); + if let ShapeKind::Media(m) = &mut Arc::make_mut(&mut doc.slides[0]).shapes.last_mut().expect("shape").kind { + m.play_across_slides = true; + } + let mut plain = (*doc.slides[0]).clone(); + plain.shapes.clear(); + doc.slides.push(Arc::new(plain)); + assert!(carries_over(&doc, id, Some(0), 1), "a slide without that id"); + let mut same = (*doc.slides[0]).clone(); + if let ShapeKind::Media(m) = &mut same.shapes.last_mut().expect("shape").kind { + m.play_across_slides = false; + } + doc.slides.push(Arc::new(same)); + assert!(carries_over(&doc, id, Some(0), 2), "the same media under that id"); + let other = doc.add_media("two.wav", "audio/wav", wav_level(0.05, 0.25)).expect("media"); + let mut reused = (*doc.slides[0]).clone(); + reused.shapes.last_mut().expect("shape").kind = ShapeKind::Media(MediaClip { media: other, volume: 1.0, autoplay: true, ..first.clone() }); + doc.slides.push(Arc::new(reused)); + assert!(!carries_over(&doc, id, Some(0), 3), "another clip under that id"); + } } diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 599e366a..b959d33f 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -1077,11 +1077,11 @@ fn show_media(app: &mut SlideApp, show: &mut Show, doc_uid: u64, doc: &Presentat // [Stop Previous Sound] ends a transition sound still playing. let stop_previous = trans.is_some_and(|t| t.stop_sound); let keep: Vec = match cur { - Some(_) => app + Some(i) => app .media .ids() .into_iter() - .filter(|id| crate::media::find_clip(doc, *id, previous).is_some_and(|c| c.play_across_slides)) + .filter(|id| crate::media::carries_over(doc, *id, previous, i)) // A transition sound plays to its end, as in PowerPoint, unless the new slide has its own. .chain((sound.is_none() && !stop_previous).then_some(TRANSITION_SOUND).filter(|id| app.media.is_playing(*id))) .collect(),