From 9b56f8392950bfc73efc25ad71659738f2771e20 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:35:28 +0200 Subject: [PATCH 1/2] Show: draw shapes stacked above a playing video over its frame (#1005) --- crates/ui-egui/src/show.rs | 107 ++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 51f859af..3f5aba28 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -27,6 +27,8 @@ pub struct Show { step_end: f64, trans: Option, tex: Option<(Key, TextureHandle)>, + /// The shapes stacked above the playing videos, drawn over the video frames (#1005). + overlay: Option<(Key, Vec, TextureHandle)>, next_tex: Option<(usize, TextureHandle)>, /// The next slide's first frame, rendered in the background while this one is shown so /// advancing only uploads it (#240). @@ -130,6 +132,55 @@ fn render_frame( crate::textures::to_color_image(&img, false) } +/// Render only the slide's own shapes except `skip`, over transparency: what is stacked above a +/// playing video, to be drawn over the video frame. +#[allow(clippy::too_many_arguments)] +fn render_overlay( + doc: &Presentation, + idx: usize, + tl: Timeline, + step: usize, + t: f64, + size: (u32, u32), + hidden: Vec, + skip: &[ShapeId], + times: &[(ShapeId, f64)], + threads: u16, + plays: &[TriggerPlay], + stamps: &[StepStamp], +) -> egui::ColorImage { + let f = move |id: ShapeId| { + let mut st = state_for(&tl, id, step, t, plays, stamps); + if hidden.contains(&id) { + st.visible = false; + } + st + }; + let opts = RenderOpts { + scale: size.0 as f64 / doc.slide_size.width.max(1.0), + size: Some(size), + state: Some(&f), + skip, + gif_times: times, + threads, + ..Default::default() + }; + let img = deckcraft_render::render_slide_part(doc, idx, deckcraft_render::SlidePart::Content, &opts); + crate::textures::to_color_image(&img, false) +} + +/// The shapes of slide `idx` to leave out when drawing what is stacked above the lowest of the +/// playing `videos` (that video, everything below it and the other videos). Empty when nothing +/// is above a video: the video frame is then painted last. +fn below_playing_video(doc: &Presentation, idx: usize, videos: &[ShapeId]) -> Vec { + let Some(slide) = doc.slides.get(idx) else { return vec![] }; + let Some(first) = slide.shapes.iter().position(|s| videos.contains(&s.id)) else { return vec![] }; + if slide.shapes.iter().skip(first + 1).all(|s| videos.contains(&s.id)) { + return vec![]; + } + slide.shapes.iter().enumerate().filter(|(i, s)| *i <= first || videos.contains(&s.id)).map(|(_, s)| s.id).collect() +} + /// Media on slide `idx` hidden until it plays, as it is when the slide is entered. fn hidden_on_entry(doc: &Presentation, idx: usize) -> Vec { crate::media::slide_media(doc, idx).into_iter().filter(|(_, _, clip)| clip.hide_while_not_playing).map(|m| m.0).collect() @@ -183,6 +234,7 @@ impl Show { step_end: 0.0, trans: None, tex: None, + overlay: None, next_tex: None, prefetch: None, blank: None, @@ -736,7 +788,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { } None => crate::textures::upload(&ctx, "show", ci), }; - show.tex = Some((key, tex.clone())); + show.tex = Some((key.clone(), tex.clone())); Some(tex) }; // While this slide rests, render the next one's first frame (not one with GIFs, whose @@ -842,16 +894,44 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { // Playing video: the current frame in its box, or full screen. let (sw, sh) = (doc.slide_size.width as f32, doc.slide_size.height as f32); let uv = show.zoom_uv(); + let mut framed: Vec = vec![]; + let mut full_screen = false; for (id, x, clip) in &media { if !clip.video || hidden.contains(id) { continue; } let r = if clip.full_screen && app.media.is_playing(*id) { + full_screen = true; full } else { slide_box(srect, uv, sw, sh, x.x as f32, x.y as f32, x.w as f32, x.h as f32) }; - crate::media::paint_frame(&painter, &app.media, *id, r); + if crate::media::paint_frame(&painter, &app.media, *id, r) { + framed.push(*id); + } + } + // Shapes stacked above a playing video show over it, as in PowerPoint (#1005); a video + // playing full screen covers the whole slide. + let skip = if full_screen { vec![] } else { below_playing_video(&doc, idx, &framed) }; + if skip.is_empty() { + show.overlay = None; + } else { + let fresh = !animating && show.overlay.as_ref().is_some_and(|(k, s, _)| *k == key && *s == skip); + if !fresh { + let tl = show.timeline(&doc).clone(); + let ci = render_overlay(&doc, idx, tl, step, t, size, hidden.clone(), &skip, ×, render_threads(), &plays, &stamps); + let handle = match show.overlay.take() { + Some((_, _, mut h)) => { + crate::textures::replace(&ctx, &mut h, ci); + h + } + None => crate::textures::upload(&ctx, "show-overlay", ci), + }; + show.overlay = Some((key.clone(), skip, handle)); + } + if let Some((_, _, h)) = &show.overlay { + painter.image(h.id(), srect, uv, Color32::WHITE); + } } // Play/pause on the animated GIF under the pointer, and the audio/video bar. // Slide Show ▸ Show Media Controls hides both (the click still plays a clip). @@ -1883,6 +1963,29 @@ mod tests { use super::*; + /// #1005: shapes stacked above a playing video must be drawn over its frame; the video, what + /// is below it and other videos stay out of that overlay. + #[test] + fn shapes_above_a_playing_video_are_the_overlay() { + let mut session = deckcraft_engine::Session::with_new(); + for _ in 0..3 { + session.execute("shape.insert", &json!({"preset": "rect", "rect": [0.0, 0.0, 100.0, 100.0]})).unwrap(); + } + let mut doc = (*session.active().unwrap().doc).clone(); + // Keep only the three inserted rects (a new slide may start with placeholders). + let keep = doc.slides[0].shapes.len() - 3; + doc.slides[0].shapes.drain(..keep); + let ids: Vec = doc.slides[0].shapes.iter().map(|s| s.id).collect(); + doc.slides[0].shapes[1].kind = deckcraft_model::ShapeKind::Media(deckcraft_model::MediaClip { video: true, ..Default::default() }); + // Video in the middle: the shape above it is drawn over it. + assert_eq!(below_playing_video(&doc, 0, &[ids[1]]), vec![ids[0], ids[1]]); + // Video on top: nothing above it, the frame is painted last as before. + doc.slides[0].shapes.swap(1, 2); + assert!(below_playing_video(&doc, 0, &[ids[1]]).is_empty()); + // No video playing. + assert!(below_playing_video(&doc, 0, &[]).is_empty()); + } + /// #968: over a shape a click acts on (a clip with its play trigger, a link) the pointing hand /// never gave way to the idle hide, so a still pointer stayed on the playing video. #[test] From 5ac14f20150539670df70151f833752faf516d0d Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:44:03 +0200 Subject: [PATCH 2/2] Test: modify the slide through Arc::make_mut (#1005) --- crates/ui-egui/src/show.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 3f5aba28..4915c2ea 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -1973,14 +1973,15 @@ mod tests { } let mut doc = (*session.active().unwrap().doc).clone(); // Keep only the three inserted rects (a new slide may start with placeholders). - let keep = doc.slides[0].shapes.len() - 3; - doc.slides[0].shapes.drain(..keep); - let ids: Vec = doc.slides[0].shapes.iter().map(|s| s.id).collect(); - doc.slides[0].shapes[1].kind = deckcraft_model::ShapeKind::Media(deckcraft_model::MediaClip { video: true, ..Default::default() }); + let slide = std::sync::Arc::make_mut(&mut doc.slides[0]); + let keep = slide.shapes.len() - 3; + slide.shapes.drain(..keep); + let ids: Vec = slide.shapes.iter().map(|s| s.id).collect(); + slide.shapes[1].kind = deckcraft_model::ShapeKind::Media(deckcraft_model::MediaClip { video: true, ..Default::default() }); // Video in the middle: the shape above it is drawn over it. assert_eq!(below_playing_video(&doc, 0, &[ids[1]]), vec![ids[0], ids[1]]); // Video on top: nothing above it, the frame is painted last as before. - doc.slides[0].shapes.swap(1, 2); + std::sync::Arc::make_mut(&mut doc.slides[0]).shapes.swap(1, 2); assert!(below_playing_video(&doc, 0, &[ids[1]]).is_empty()); // No video playing. assert!(below_playing_video(&doc, 0, &[]).is_empty());