Skip to main content

rustmotion_components/
legacy_dispatch.rs

1use rustmotion_core::css::CssStyle;
2use rustmotion_core::engine::animator::{resolve_props_for_effects, AnimatedProperties};
3use rustmotion_core::engine::box_tree::NodeId;
4use rustmotion_core::engine::layout_pass::BoxLayout;
5use rustmotion_core::engine::paint_pass::{PaintDispatcher, PaintFrame};
6use rustmotion_core::traits::PaintCtx;
7use skia_safe::Canvas;
8
9use crate::{ChildComponent, Component};
10
11pub struct LegacyPaintDispatcher<'a> {
12    components: &'a [Option<&'a ChildComponent>],
13    stagger_delays: &'a [f64],
14    time_params: &'a [(f64, f64)],
15}
16
17impl<'a> LegacyPaintDispatcher<'a> {
18    pub fn new(components: &'a [Option<&'a ChildComponent>]) -> Self {
19        Self {
20            components,
21            stagger_delays: &[],
22            time_params: &[],
23        }
24    }
25
26    pub fn for_scene(built: &'a crate::box_builder::BuiltScene<'a>) -> Self {
27        Self {
28            components: &built.components,
29            stagger_delays: &built.stagger_delays,
30            time_params: &built.time_params,
31        }
32    }
33
34    fn lookup(&self, id: NodeId) -> Option<&'a ChildComponent> {
35        let idx = id as usize;
36        self.components.get(idx).copied().flatten()
37    }
38}
39
40impl<'a> PaintDispatcher for LegacyPaintDispatcher<'a> {
41    fn dispatch(
42        &self,
43        canvas: &Canvas,
44        payload: &(dyn std::any::Any + Send + Sync),
45        css: &CssStyle,
46        layout: &BoxLayout,
47        frame: &PaintFrame,
48    ) {
49        let Some(node_id) = payload.downcast_ref::<NodeId>() else {
50            return;
51        };
52        let Some(child) = self.lookup(*node_id) else {
53            return;
54        };
55
56        if is_container(&child.component) {
57            return;
58        }
59
60        let stagger_delay = self
61            .stagger_delays
62            .get(*node_id as usize)
63            .copied()
64            .unwrap_or(0.0);
65        let (t_scale, t_shift) = self
66            .time_params
67            .get(*node_id as usize)
68            .copied()
69            .unwrap_or((1.0, 0.0));
70        let local_time = frame.time * t_scale + t_shift;
71        let props = match crate::box_builder::effective_effects(
72            &child.component,
73            stagger_delay,
74            local_time,
75        ) {
76            Some(effects) => resolve_props_for_effects(&effects, local_time, frame.scene_duration),
77            None => AnimatedProperties::default(),
78        };
79        if props.opacity <= 0.0 {
80            return;
81        }
82
83        let cascaded_component = child.component.with_cascaded_style(css);
84        let cascaded = cascaded_component.as_ref().unwrap_or(&child.component);
85        let repainted = cascaded.with_resolved_background(css);
86        let effective_component = repainted.as_ref().unwrap_or(cascaded);
87        let Some(painter) = effective_component.as_painter() else {
88            return;
89        };
90
91        canvas.save();
92        let (cx, cy, cw, ch) = layout.content_box();
93        canvas.translate((cx, cy));
94        let local = BoxLayout {
95            x: 0.0,
96            y: 0.0,
97            width: cw,
98            height: ch,
99            ..Default::default()
100        };
101
102        let paint_ctx = PaintCtx {
103            time: local_time,
104            scenario_time: frame.scenario_time,
105            scene_duration: frame.scene_duration,
106            frame_index: frame.frame_index,
107            fps: frame.fps,
108            video_width: frame.video_width,
109            video_height: frame.video_height,
110            stagger_offset: stagger_delay,
111        };
112        painter.paint_content(canvas, &local, &props, &paint_ctx);
113
114        canvas.restore();
115    }
116}
117
118fn is_container(c: &Component) -> bool {
119    matches!(c, Component::Container(_))
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::box_builder::build_scene;
126    use crate::shape::Shape;
127    use crate::PositionMode;
128    use rustmotion_core::css::style::{CssStyle, Size as CSize};
129    use rustmotion_core::css::taffy_bridge::ConversionContext;
130    use rustmotion_core::css::units::LengthPercentage as CLP;
131    use rustmotion_core::engine::box_tree::BoxKind;
132    use rustmotion_core::engine::layout_pass::run_layout;
133    use rustmotion_core::schema::ShapeType;
134    use std::sync::Arc;
135
136    fn shape_child(w: f32, h: f32, x: f32, y: f32) -> ChildComponent {
137        ChildComponent {
138            id: None,
139            component: Component::Shape(Shape {
140                shape: ShapeType::Rect,
141                text: None,
142                timing: Default::default(),
143                style: CssStyle {
144                    width: Some(CSize::Length(CLP::Px(w))),
145                    height: Some(CSize::Length(CLP::Px(h))),
146                    ..Default::default()
147                },
148                timeline: Vec::new(),
149                stagger: None,
150                fill: Some(rustmotion_core::schema::Fill::Solid("#ff0000".into())),
151                stroke: None,
152                draw_start: None,
153                path_morph: None,
154            }),
155            position: Some(PositionMode::Absolute { x, y }),
156            x: None,
157            y: None,
158            z_index: None,
159            bleed: false,
160        }
161    }
162
163    #[test]
164    fn leaf_painter_content_is_inset_by_padding() {
165        use rustmotion_core::css::style::Edges;
166
167        let mut scene = vec![shape_child(100.0, 80.0, 0.0, 0.0)];
168        if let Component::Shape(s) = &mut scene[0].component {
169            s.style.padding = Some(Edges::Uniform(CLP::Px(20.0)));
170        }
171        let built = build_scene(&scene, (200.0, 200.0));
172        let layout = run_layout(&built.root, (200.0, 200.0), &ConversionContext::default());
173
174        let mut surface =
175            skia_safe::surfaces::raster_n32_premul((200, 200)).expect("raster surface");
176        let canvas = surface.canvas();
177        let dispatcher = LegacyPaintDispatcher::new(&built.components);
178        let frame = PaintFrame {
179            light: Default::default(),
180            time: 0.0,
181            scenario_time: 0.0,
182            frame_index: 0,
183            fps: 30,
184            video_width: 200,
185            video_height: 200,
186            scene_duration: 1.0,
187            camera: None,
188        };
189        rustmotion_core::engine::paint_pass::paint_tree(
190            canvas,
191            &built.root,
192            &layout,
193            &frame,
194            &dispatcher,
195        );
196
197        let snapshot = surface.image_snapshot();
198        let info = skia_safe::ImageInfo::new(
199            (1, 1),
200            skia_safe::ColorType::RGBA8888,
201            skia_safe::AlphaType::Premul,
202            None,
203        );
204        let read = |x: i32, y: i32| -> [u8; 4] {
205            let mut buf = [0u8; 4];
206            assert!(snapshot.read_pixels(
207                &info,
208                &mut buf,
209                4,
210                skia_safe::IPoint::new(x, y),
211                skia_safe::image::CachingHint::Disallow,
212            ));
213            buf
214        };
215
216        let padding_zone = read(5, 5);
217        assert!(
218            !(padding_zone[0] > 200 && padding_zone[1] < 50 && padding_zone[2] < 50),
219            "padding ring must not be painted by the leaf's own fill, got {:?}",
220            padding_zone
221        );
222        let content_zone = read(50, 40);
223        assert!(
224            content_zone[0] > 200 && content_zone[1] < 50 && content_zone[2] < 50,
225            "content box must still be painted red, got {:?}",
226            content_zone
227        );
228    }
229
230    #[test]
231    fn dispatch_runs_paint_content_on_leaf() {
232        let scene = vec![shape_child(50.0, 30.0, 10.0, 20.0)];
233        let built = build_scene(&scene, (200.0, 200.0));
234        let layout = run_layout(&built.root, (200.0, 200.0), &ConversionContext::default());
235
236        assert!(built.components[0].is_none());
237        assert!(built.components[1].is_some());
238
239        let mut surface =
240            skia_safe::surfaces::raster_n32_premul((200, 200)).expect("raster surface");
241        let canvas = surface.canvas();
242        let dispatcher = LegacyPaintDispatcher::new(&built.components);
243        let frame = PaintFrame {
244            light: Default::default(),
245            time: 0.0,
246            scenario_time: 0.0,
247            frame_index: 0,
248            fps: 30,
249            video_width: 200,
250            video_height: 200,
251            scene_duration: 1.0,
252            camera: None,
253        };
254        rustmotion_core::engine::paint_pass::paint_tree(
255            canvas,
256            &built.root,
257            &layout,
258            &frame,
259            &dispatcher,
260        );
261
262        let snapshot = surface.image_snapshot();
263        let mut buf = [0u8; 4];
264        let info = skia_safe::ImageInfo::new(
265            (1, 1),
266            skia_safe::ColorType::RGBA8888,
267            skia_safe::AlphaType::Premul,
268            None,
269        );
270        let read_ok = snapshot.read_pixels(
271            &info,
272            &mut buf,
273            4,
274            skia_safe::IPoint::new(35, 35),
275            skia_safe::image::CachingHint::Disallow,
276        );
277        assert!(read_ok, "pixel read should succeed");
278        assert!(buf[0] > 200, "expected red, got rgba {:?}", buf);
279        assert!(buf[1] < 50, "green should be low, got rgba {:?}", buf);
280        assert!(buf[2] < 50, "blue should be low, got rgba {:?}", buf);
281    }
282
283    #[test]
284    fn card_background_painted_with_red_shape_inside() {
285        use crate::container::ContainerComponent;
286
287        use rustmotion_core::css::style::{Background, Color};
288
289        let red_shape = ChildComponent {
290            id: None,
291            component: Component::Shape(Shape {
292                shape: ShapeType::Rect,
293                text: None,
294                timing: Default::default(),
295                style: CssStyle {
296                    width: Some(CSize::Length(CLP::Px(30.0))),
297                    height: Some(CSize::Length(CLP::Px(20.0))),
298                    ..Default::default()
299                },
300                timeline: Vec::new(),
301                stagger: None,
302                fill: Some(rustmotion_core::schema::Fill::Solid("#ff0000".into())),
303                stroke: None,
304                draw_start: None,
305                path_morph: None,
306            }),
307            position: Some(PositionMode::Absolute { x: 10.0, y: 10.0 }),
308            x: None,
309            y: None,
310            z_index: None,
311            bleed: false,
312        };
313
314        let card = ChildComponent {
315            id: None,
316            component: Component::Container(ContainerComponent {
317                children: vec![red_shape],
318                timing: Default::default(),
319                style: CssStyle {
320                    width: Some(CSize::Length(CLP::Px(100.0))),
321                    height: Some(CSize::Length(CLP::Px(80.0))),
322                    background: Some(Background::Color(Color::String("#00ff00".into()))),
323                    ..Default::default()
324                },
325                timeline: Vec::new(),
326                stagger: None,
327                time_scale: None,
328                time_offset: None,
329            }),
330            position: Some(PositionMode::Absolute { x: 40.0, y: 30.0 }),
331            x: None,
332            y: None,
333            z_index: None,
334            bleed: false,
335        };
336
337        let scene = vec![card];
338        let built = build_scene(&scene, (200.0, 200.0));
339        let layout = run_layout(&built.root, (200.0, 200.0), &ConversionContext::default());
340
341        let mut surface =
342            skia_safe::surfaces::raster_n32_premul((200, 200)).expect("raster surface");
343        let canvas = surface.canvas();
344        let dispatcher = LegacyPaintDispatcher::new(&built.components);
345        let frame = PaintFrame {
346            light: Default::default(),
347            time: 0.0,
348            scenario_time: 0.0,
349            frame_index: 0,
350            fps: 30,
351            video_width: 200,
352            video_height: 200,
353            scene_duration: 1.0,
354            camera: None,
355        };
356        rustmotion_core::engine::paint_pass::paint_tree(
357            canvas,
358            &built.root,
359            &layout,
360            &frame,
361            &dispatcher,
362        );
363
364        let snapshot = surface.image_snapshot();
365        let info = skia_safe::ImageInfo::new(
366            (1, 1),
367            skia_safe::ColorType::RGBA8888,
368            skia_safe::AlphaType::Premul,
369            None,
370        );
371        let read = |x: i32, y: i32| -> [u8; 4] {
372            let mut buf = [0u8; 4];
373            assert!(snapshot.read_pixels(
374                &info,
375                &mut buf,
376                4,
377                skia_safe::IPoint::new(x, y),
378                skia_safe::image::CachingHint::Disallow,
379            ));
380            buf
381        };
382
383        let bg = read(130, 100);
384        assert!(bg[1] > 200, "expected green card bg, got {:?}", bg);
385        assert!(bg[0] < 50, "red should be low at bg, got {:?}", bg);
386
387        let fg = read(65, 50);
388        assert!(fg[0] > 200, "expected red shape, got {:?}", fg);
389        assert!(fg[1] < 50, "green should be low at shape, got {:?}", fg);
390    }
391
392    #[test]
393    fn fade_in_preset_drives_alpha_through_dispatcher() {
394        use crate::shape::Shape;
395        use rustmotion_core::schema::{AnimationEffect, AnimationTiming, ShapeType};
396
397        let make_scene = || {
398            let shape = ChildComponent {
399                id: None,
400                component: Component::Shape(Shape {
401                    shape: ShapeType::Rect,
402                    text: None,
403                    timing: Default::default(),
404                    style: CssStyle {
405                        width: Some(CSize::Length(CLP::Px(100.0))),
406                        height: Some(CSize::Length(CLP::Px(100.0))),
407                        animation: vec![AnimationEffect::FadeIn(AnimationTiming {
408                            duration: 0.5,
409                            ..Default::default()
410                        })],
411                        ..Default::default()
412                    },
413                    timeline: Vec::new(),
414                    stagger: None,
415                    fill: Some(rustmotion_core::schema::Fill::Solid("#ff0000".into())),
416                    stroke: None,
417                    draw_start: None,
418                    path_morph: None,
419                }),
420                position: Some(PositionMode::Absolute { x: 0.0, y: 0.0 }),
421                x: None,
422                y: None,
423                z_index: None,
424                bleed: false,
425            };
426            vec![shape]
427        };
428
429        let sample_red_at = |time: f64| -> u8 {
430            let scene = make_scene();
431            let built = crate::box_builder::build_scene_with_anim(
432                &scene,
433                (200.0, 200.0),
434                crate::box_builder::BuildAnimationCtx {
435                    time,
436                    scenario_time: time,
437                    scene_duration: 1.0,
438                    fps: 30,
439                },
440            );
441            let layout = run_layout(&built.root, (200.0, 200.0), &ConversionContext::default());
442            let mut surface =
443                skia_safe::surfaces::raster_n32_premul((200, 200)).expect("raster surface");
444            let canvas = surface.canvas();
445            canvas.clear(skia_safe::Color::BLACK);
446            let dispatcher = LegacyPaintDispatcher::new(&built.components);
447            let frame = PaintFrame {
448                light: Default::default(),
449                time,
450                scenario_time: time,
451                frame_index: 0,
452                fps: 30,
453                video_width: 200,
454                video_height: 200,
455                scene_duration: 1.0,
456                camera: None,
457            };
458            rustmotion_core::engine::paint_pass::paint_tree(
459                canvas,
460                &built.root,
461                &layout,
462                &frame,
463                &dispatcher,
464            );
465            let snap = surface.image_snapshot();
466            let info = skia_safe::ImageInfo::new(
467                (1, 1),
468                skia_safe::ColorType::RGBA8888,
469                skia_safe::AlphaType::Premul,
470                None,
471            );
472            let mut buf = [0u8; 4];
473            assert!(snap.read_pixels(
474                &info,
475                &mut buf,
476                4,
477                skia_safe::IPoint::new(50, 50),
478                skia_safe::image::CachingHint::Disallow,
479            ));
480            buf[0]
481        };
482
483        let early = sample_red_at(0.05);
484        let late = sample_red_at(0.5);
485        assert!(
486            late > early + 50,
487            "FadeIn should produce a clearly higher red at t=0.5 than at t=0.05 \
488             (early={}, late={})",
489            early,
490            late,
491        );
492        assert!(
493            late > 200,
494            "at t=duration the shape should be ~fully opaque red, got {}",
495            late
496        );
497        assert!(
498            early < 150,
499            "at t=0.05 the shape should be mostly transparent, got {}",
500            early
501        );
502    }
503
504    #[test]
505    fn dispatch_skips_unknown_payloads() {
506        let dispatcher = LegacyPaintDispatcher::new(&[]);
507        let mut surface = skia_safe::surfaces::raster_n32_premul((10, 10)).unwrap();
508        let canvas = surface.canvas();
509        let css = rustmotion_core::css::CssStyle::default();
510        let layout = BoxLayout {
511            x: 0.0,
512            y: 0.0,
513            width: 10.0,
514            height: 10.0,
515            ..Default::default()
516        };
517        let frame = PaintFrame {
518            light: Default::default(),
519            time: 0.0,
520            scenario_time: 0.0,
521            frame_index: 0,
522            fps: 30,
523            video_width: 10,
524            video_height: 10,
525            scene_duration: 1.0,
526            camera: None,
527        };
528        let bogus: Arc<dyn std::any::Any + Send + Sync> = Arc::new(42i64);
529        dispatcher.dispatch(canvas, bogus.as_ref(), &css, &layout, &frame);
530        let _ = BoxKind::Container;
531    }
532}