From 8c8ab9561b0eae945fdeaaa949e3b2602b2073fb Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:20:19 -0500 Subject: [PATCH] fix(render): animation state never applies to layout and master shapes sharing a slide shape id (fixes #1362) --- crates/render/src/lib.rs | 11 +++++++---- crates/render/src/tests.rs | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 1e7c3dca..214af83f 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -267,6 +267,7 @@ impl Default for RenderOpts<'_> { } } +#[derive(Clone)] struct SlideFields { num: u32, /// Text for every date field instead of the date ([`RenderOpts::date_text`]). @@ -667,6 +668,10 @@ impl Renderer { /// Background plus the master and layout graphics the slide shows. fn backdrop(&mut self, ctx: &mut RenderContext, f: &Frame, rctx: &Ctx, slide: &Slide, view: Affine) { + // Backdrop graphics never take shape states: their ids may clash with the slide's shapes. + let bopts = RenderOpts { state: None, date_text: f.opts.date_text.clone(), ..*f.opts }; + let bf = Frame { pres: f.pres, opts: &bopts, fields: f.fields.clone() }; + let f = &bf; self.background(ctx, f, rctx, Some(slide), view); let (show_layout, show_master) = resolve::show_master_shapes(slide, rctx.layout); if show_master { @@ -742,14 +747,12 @@ impl Renderer { self.in_shadow_pass = false; let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, old.0) || needs_filters(pres, new.0)); let (Some(co), Some(cn)) = (Ctx::for_slide(pres, old.0), Ctx::for_slide(pres, new.0)) else { return self.finish(ctx, w, h) }; - // Backdrop graphics never take shape states: their ids may clash with the frame's shapes. - let bopts = RenderOpts { state: None, date_text: opts.date_text.clone(), ..*opts }; if mix < 1.0 { - let f = Frame { pres, opts: &bopts, fields: Self::fields(pres, old.1, opts) }; + let f = Frame { pres, opts, fields: Self::fields(pres, old.1, opts) }; self.backdrop(&mut ctx, &f, &co, old.0, view); } if mix > 0.0 { - let f = Frame { pres, opts: &bopts, fields: Self::fields(pres, new.1, opts) }; + let f = Frame { pres, opts, fields: Self::fields(pres, new.1, opts) }; let layered = mix < 1.0; if layered { ctx.push_layer(None, None, Some(mix as f32), None, None); diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index bcddf4c9..2f77e74d 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1776,6 +1776,26 @@ fn the_editor_pasteboard_draws_shapes_off_the_slide() { assert!(drawn.x0 <= -100.0 && drawn.y1 >= 660.0 && drawn.x1 >= 960.0, "{drawn:?}"); } +#[test] +fn animation_state_skips_layout_and_master_shapes_sharing_a_slide_shape_id() { + let mut p = Presentation::default(); + let bar = |x| Shape { + id: ShapeId(77), + xfrm: Some(Xfrm::new(x, 400.0, 100.0, 60.0)), + fill: Some(Fill::solid(ColorRef::rgb(Rgba::rgb(200, 0, 0)))), + ..Default::default() + }; + let lid = p.slides[0].layout; + let m = std::sync::Arc::make_mut(&mut p.masters[0]); + m.shapes.push(bar(100.0)); + m.layouts.iter_mut().find(|l| l.id == lid).expect("layout").shapes.push(bar(400.0)); + let hide = |_: ShapeId| ShapeState { visible: false, ..Default::default() }; + let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, state: Some(&hide), ..Default::default() }); + for x in [150, 450] { + assert_eq!(img.pixel(x, 430)[..3], [200, 0, 0], "the backdrop bar at x={x} stays"); + } +} + #[test] fn master_graphics_off_the_slide_show_on_the_pasteboard() { let mut p = Presentation::default();