freya_core/render/
compositor.rs

1use std::{
2    collections::HashSet,
3    ops::{
4        Deref,
5        DerefMut,
6    },
7};
8
9use freya_native_core::{
10    exports::shipyard::{
11        IntoIter,
12        View,
13    },
14    prelude::NodeImmutable,
15    NodeId,
16};
17use rustc_hash::FxHashMap;
18use torin::prelude::{
19    Area,
20    LayoutNode,
21    Torin,
22};
23
24use crate::{
25    dom::{
26        CompositorDirtyNodes,
27        DioxusDOM,
28        DioxusNode,
29    },
30    elements::{
31        ElementUtils,
32        ElementUtilsResolver,
33        ElementWithUtils,
34    },
35    layers::Layers,
36    states::{
37        LayerState,
38        StyleState,
39        TransformState,
40        ViewportState,
41    },
42};
43
44/// Text-like elements with shadows are the only type of elements
45/// whose drawing area
46///     1. Can affect other nodes
47///     2. Are not part of their layout
48///
49/// Therefore a special cache is needed to be able to mark as dirty the previous area
50/// where the shadow of the text was.
51#[derive(Clone, Default, Debug)]
52pub struct CompositorCache(FxHashMap<NodeId, Area>);
53
54impl Deref for CompositorCache {
55    type Target = FxHashMap<NodeId, Area>;
56
57    fn deref(&self) -> &Self::Target {
58        &self.0
59    }
60}
61
62impl DerefMut for CompositorCache {
63    fn deref_mut(&mut self) -> &mut Self::Target {
64        &mut self.0
65    }
66}
67
68#[derive(Clone, Default, Debug)]
69pub struct CompositorDirtyArea(Option<Area>);
70
71impl CompositorDirtyArea {
72    /// Take the area, leaving nothing behind.
73    pub fn take(&mut self) -> Option<Area> {
74        self.0.take()
75    }
76
77    /// Unite the area or insert it if none is yet present.
78    pub fn unite_or_insert(&mut self, other: &Area) {
79        if let Some(dirty_area) = &mut self.0 {
80            *dirty_area = dirty_area.union(other);
81        } else {
82            self.0 = Some(*other);
83        }
84    }
85
86    /// Round the dirty area to the out bounds to prevent float pixel issues.
87    pub fn round_out(&mut self) {
88        if let Some(dirty_area) = &mut self.0 {
89            *dirty_area = dirty_area.round_out();
90        }
91    }
92
93    /// Checks if the area (in case of being any) interesects with another area.
94    pub fn intersects(&self, other: &Area) -> bool {
95        self.0
96            .map(|dirty_area| dirty_area.intersects(other))
97            .unwrap_or_default()
98    }
99}
100
101impl Deref for CompositorDirtyArea {
102    type Target = Option<Area>;
103
104    fn deref(&self) -> &Self::Target {
105        &self.0
106    }
107}
108
109#[derive(Debug)]
110pub struct Compositor {
111    full_render: bool,
112}
113
114impl Default for Compositor {
115    fn default() -> Self {
116        Self { full_render: true }
117    }
118}
119
120impl Compositor {
121    #[inline]
122    pub fn get_drawing_area(
123        node_id: NodeId,
124        layout: &Torin<NodeId>,
125        rdom: &DioxusDOM,
126        scale_factor: f32,
127    ) -> Option<Area> {
128        let layout_node = layout.get(node_id)?;
129        let node_ref = rdom.get(node_id)?;
130        let utils = node_ref.node_type().tag()?.utils()?;
131        let style_state = node_ref.get::<StyleState>().unwrap();
132        let viewport_state = node_ref.get::<ViewportState>().unwrap();
133        let transform_state = node_ref.get::<TransformState>().unwrap();
134        utils.drawing_area_with_viewports(
135            layout_node,
136            &node_ref,
137            layout,
138            scale_factor,
139            &style_state,
140            &viewport_state,
141            &transform_state,
142        )
143    }
144
145    #[inline]
146    pub fn with_utils<T>(
147        node_id: NodeId,
148        layout: &Torin<NodeId>,
149        rdom: &DioxusDOM,
150        run: impl FnOnce(DioxusNode, ElementWithUtils, &LayoutNode) -> T,
151    ) -> Option<T> {
152        let layout_node = layout.get(node_id)?;
153        let node = rdom.get(node_id)?;
154        let utils = node.node_type().tag()?.utils()?;
155
156        Some(run(node, utils, layout_node))
157    }
158
159    /// The compositor runs from the bottom layers to the top and viceversa to check what Nodes might be affected by the
160    /// dirty area. How a Node is checked is by calculating its drawing area which consists of its layout area plus any possible
161    /// outer effect such as shadows and borders.
162    /// Calculating the drawing area might get expensive so we cache them in the `cached_areas` map to make the second layers run faster
163    /// (top to bottom).
164    /// In addition to that, nodes that have already been united to the dirty area are removed from the `running_layers` to avoid being checked again
165    /// at the second layers (top to bottom).
166    #[allow(clippy::too_many_arguments)]
167    pub fn run<'a>(
168        &mut self,
169        dirty_nodes: &mut CompositorDirtyNodes,
170        dirty_area: &mut CompositorDirtyArea,
171        cache: &mut CompositorCache,
172        layers: &'a Layers,
173        dirty_layers: &'a mut Layers,
174        layout: &Torin<NodeId>,
175        rdom: &DioxusDOM,
176        scale_factor: f32,
177    ) -> &'a Layers {
178        if self.full_render {
179            rdom.raw_world().run(
180                |viewport_states: View<ViewportState>,
181                 style_states: View<StyleState>,
182                 transform_states: View<TransformState>| {
183                    for (viewport, style_state, transform_state) in
184                        (&viewport_states, &style_states, &transform_states).iter()
185                    {
186                        Self::with_utils(
187                            viewport.node_id,
188                            layout,
189                            rdom,
190                            |node_ref, utils, layout_node| {
191                                if utils.needs_cached_area(&node_ref, transform_state, style_state)
192                                {
193                                    let area = utils.drawing_area(
194                                        layout_node,
195                                        &node_ref,
196                                        layout,
197                                        scale_factor,
198                                        style_state,
199                                        transform_state,
200                                    );
201                                    // Cache the drawing area so it can be invalidated in the next frame
202                                    cache.insert(viewport.node_id, area);
203                                }
204                            },
205                        );
206                    }
207                },
208            );
209            self.full_render = false;
210            return layers;
211        }
212
213        let mut skipped_nodes = HashSet::<NodeId>::new();
214
215        loop {
216            let mut any_dirty = false;
217            rdom.raw_world().run(
218                |viewport_states: View<ViewportState>,
219                 layer_states: View<LayerState>,
220                 style_states: View<StyleState>,
221                 transform_states: View<TransformState>| {
222                    for (viewport, layer_state, style_state, transform_state) in (
223                        &viewport_states,
224                        &layer_states,
225                        &style_states,
226                        &transform_states,
227                    )
228                        .iter()
229                    {
230                        if skipped_nodes.contains(&viewport.node_id) {
231                            continue;
232                        }
233                        let skip = Self::with_utils(
234                            viewport.node_id,
235                            layout,
236                            rdom,
237                            |node_ref, utils, layout_node| {
238                                let Some(area) = utils.drawing_area_with_viewports(
239                                    layout_node,
240                                    &node_ref,
241                                    layout,
242                                    scale_factor,
243                                    style_state,
244                                    viewport,
245                                    transform_state,
246                                ) else {
247                                    return true;
248                                };
249
250                                let is_dirty = dirty_nodes.remove(&viewport.node_id);
251
252                                // Use the cached area to invalidate the previous frame area if necessary
253                                let mut invalidated_cache_area =
254                                    cache.get(&viewport.node_id).and_then(|cached_area| {
255                                        if is_dirty || dirty_area.intersects(cached_area) {
256                                            Some(*cached_area)
257                                        } else {
258                                            None
259                                        }
260                                    });
261
262                                let is_invalidated = is_dirty
263                                    || invalidated_cache_area.is_some()
264                                    || dirty_area.intersects(&area);
265
266                                if is_invalidated {
267                                    // Save this node to the layer it corresponds for rendering
268                                    dirty_layers
269                                        .insert_node_in_layer(viewport.node_id, layer_state.layer);
270
271                                    // Cache the drawing area so it can be invalidated in the next frame
272                                    if utils.needs_cached_area(
273                                        &node_ref,
274                                        transform_state,
275                                        style_state,
276                                    ) {
277                                        cache.insert(viewport.node_id, area);
278                                    }
279
280                                    if is_dirty {
281                                        // Expand the dirty area with the cached area
282                                        if let Some(invalidated_cache_area) =
283                                            invalidated_cache_area.take()
284                                        {
285                                            dirty_area.unite_or_insert(&invalidated_cache_area);
286                                        }
287
288                                        // Expand the dirty area with new area
289                                        dirty_area.unite_or_insert(&area);
290
291                                        // Run again in case this affects e.g ancestors
292                                        any_dirty = true;
293                                    }
294                                }
295
296                                is_invalidated
297                            },
298                        )
299                        .unwrap_or(true);
300
301                        if skip {
302                            skipped_nodes.insert(viewport.node_id);
303                        }
304                    }
305                },
306            );
307
308            if !any_dirty {
309                break;
310            }
311        }
312
313        dirty_nodes.drain();
314
315        dirty_layers
316    }
317
318    /// Reset the compositor, thus causing a full render in the next frame.
319    pub fn reset(&mut self) {
320        self.full_render = true;
321    }
322}
323
324#[cfg(test)]
325mod test {
326    use freya::{
327        core::{
328            layers::Layers,
329            render::Compositor,
330        },
331        prelude::*,
332    };
333    use freya_testing::prelude::*;
334    use itertools::sorted;
335
336    fn run_compositor(
337        utils: &TestingHandler<()>,
338        compositor: &mut Compositor,
339    ) -> (Layers, Layers, usize) {
340        let sdom = utils.sdom();
341        let fdom = sdom.get();
342        let layout = fdom.layout();
343        let layers = fdom.layers();
344        let rdom = fdom.rdom();
345        let mut compositor_dirty_area = fdom.compositor_dirty_area();
346        let mut compositor_dirty_nodes = fdom.compositor_dirty_nodes();
347        let mut compositor_cache = fdom.compositor_cache();
348
349        let mut dirty_layers = Layers::default();
350
351        // Process what nodes need to be rendered
352        let rendering_layers = compositor.run(
353            &mut compositor_dirty_nodes,
354            &mut compositor_dirty_area,
355            &mut compositor_cache,
356            &layers,
357            &mut dirty_layers,
358            &layout,
359            rdom,
360            1.0f32,
361        );
362
363        compositor_dirty_area.take();
364        compositor_dirty_nodes.clear();
365
366        let mut painted_nodes = 0;
367        for (_, nodes) in sorted(rendering_layers.iter()) {
368            for node_id in nodes {
369                if layout.get(*node_id).is_some() {
370                    painted_nodes += 1;
371                }
372            }
373        }
374
375        (layers.clone(), rendering_layers.clone(), painted_nodes)
376    }
377
378    #[tokio::test]
379    pub async fn button_drawing() {
380        fn compositor_app() -> Element {
381            let mut count = use_signal(|| 0);
382
383            rsx!(
384                rect {
385                    height: "50%",
386                    width: "100%",
387                    main_align: "center",
388                    cross_align: "center",
389                    background: "rgb(0, 119, 182)",
390                    color: "white",
391                    shadow: "0 4 20 5 rgb(0, 0, 0, 80)",
392                    label {
393                        font_size: "75",
394                        font_weight: "bold",
395                        "{count}"
396                    }
397                }
398                rect {
399                    height: "50%",
400                    width: "100%",
401                    main_align: "center",
402                    cross_align: "center",
403                    direction: "horizontal",
404                    Button {
405                        onclick: move |_| count += 1,
406                        label { "Increase" }
407                    }
408                }
409            )
410        }
411
412        let mut compositor = Compositor::default();
413        let mut utils = launch_test(compositor_app);
414        let root = utils.root();
415        let label = root.get(0).get(0);
416        utils.wait_for_update().await;
417
418        assert_eq!(label.get(0).text(), Some("0"));
419
420        let (layers, rendering_layers, _) = run_compositor(&utils, &mut compositor);
421        // First render is always a full render
422        assert_eq!(layers, rendering_layers);
423
424        utils.move_cursor((275., 375.)).await;
425
426        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
427
428        // Root + Second rect + Button's internal rect + Button's label
429        assert_eq!(painted_nodes, 4);
430
431        utils.click_cursor((275., 375.)).await;
432
433        assert_eq!(label.get(0).text(), Some("1"));
434    }
435
436    #[tokio::test]
437    pub async fn after_shadow_drawing() {
438        fn compositor_app() -> Element {
439            let mut height = use_signal(|| 200);
440            let mut shadow = use_signal(|| 20);
441
442            rsx!(
443                rect {
444                    height: "100",
445                    width: "200",
446                    background: "red",
447                    margin: "0 0 2 0",
448                    onclick: move |_| height += 10,
449                }
450                rect {
451                    height: "{height}",
452                    width: "200",
453                    background: "green",
454                    shadow: "0 {shadow} 1 0 rgb(0, 0, 0, 0.5)",
455                    margin: "0 0 2 0",
456                    onclick: move |_| height -= 10,
457                }
458                rect {
459                    height: "100",
460                    width: "200",
461                    background: "blue",
462                    onclick: move |_| shadow.set(-20),
463                }
464            )
465        }
466
467        let mut compositor = Compositor::default();
468        let mut utils = launch_test(compositor_app);
469        utils.wait_for_update().await;
470
471        let (layers, rendering_layers, _) = run_compositor(&utils, &mut compositor);
472        // First render is always a full render
473        assert_eq!(layers, rendering_layers);
474
475        utils.click_cursor((5., 5.)).await;
476
477        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
478
479        // Root + Second rect + Third rect
480        assert_eq!(painted_nodes, 3);
481
482        utils.click_cursor((5., 150.)).await;
483
484        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
485
486        // Root + Second rect + Third rect
487        assert_eq!(painted_nodes, 3);
488
489        utils.click_cursor((5., 350.)).await;
490
491        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
492
493        // Root + First rect + Second rect + Third Rect
494        assert_eq!(painted_nodes, 4);
495
496        utils.click_cursor((5., 150.)).await;
497
498        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
499
500        // Root + First rect + Second rect + Third Rect
501        assert_eq!(painted_nodes, 4);
502    }
503
504    #[tokio::test]
505    pub async fn paragraph_drawing() {
506        fn compositor_app() -> Element {
507            let mut msg_state = use_signal(|| true);
508            let mut shadow_state = use_signal(|| true);
509
510            let msg = if msg_state() { "12" } else { "23" };
511            let shadow = if shadow_state() {
512                "-40 0 20 black"
513            } else {
514                "none"
515            };
516
517            rsx!(
518                rect {
519                    height: "200",
520                    width: "200",
521                    direction: "horizontal",
522                    spacing: "2",
523                    rect {
524                        onclick: move |_| msg_state.toggle(),
525                        height: "200",
526                        width: "200",
527                        background: "red"
528                    }
529                    paragraph {
530                        onclick: move |_| shadow_state.toggle(),
531                        text {
532                            font_size: "75",
533                            font_weight: "bold",
534                            text_shadow: "{shadow}",
535                            "{msg}"
536                        }
537                    }
538                }
539            )
540        }
541
542        let mut compositor = Compositor::default();
543        let mut utils = launch_test(compositor_app);
544        let root = utils.root();
545        utils.wait_for_update().await;
546
547        assert_eq!(root.get(0).get(1).get(0).get(0).text(), Some("12"));
548
549        let (layers, rendering_layers, _) = run_compositor(&utils, &mut compositor);
550        // First render is always a full render
551        assert_eq!(layers, rendering_layers);
552
553        utils.click_cursor((5., 5.)).await;
554
555        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
556
557        // Root + First rect + Paragraph + Second rect
558        assert_eq!(painted_nodes, 4);
559
560        utils.click_cursor((205., 5.)).await;
561
562        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
563
564        // Root + First rect + Paragraph + Second rect
565        assert_eq!(painted_nodes, 4);
566
567        utils.click_cursor((5., 5.)).await;
568
569        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
570
571        // Root + First rect + Paragraph
572        assert_eq!(painted_nodes, 2);
573    }
574
575    #[tokio::test]
576    pub async fn rotated_drawing() {
577        fn compositor_app() -> Element {
578            let mut rotate = use_signal(|| 0);
579
580            rsx!(
581                rect {
582                    height: "50%",
583                    width: "100%",
584                    main_align: "center",
585                    cross_align: "center",
586                    background: "rgb(0, 119, 182)",
587                    color: "white",
588                    shadow: "0 4 20 5 rgb(0, 0, 0, 80)",
589                    label {
590                        rotate: "{rotate}deg",
591                        "Hello"
592                    }
593                    label {
594                        "World"
595                    }
596                }
597                rect {
598                    height: "50%",
599                    width: "100%",
600                    main_align: "center",
601                    cross_align: "center",
602                    direction: "horizontal",
603                    Button {
604                        theme: theme_with!(ButtonTheme {
605                            shadow: "0 4 5 0 rgb(0, 0, 0, 0.1)".into(),
606                            padding: "8 12".into(),
607                        }),
608                        onclick: move |_| rotate += 1,
609                        label { "Rotate" }
610                    }
611                }
612            )
613        }
614
615        let mut compositor = Compositor::default();
616        let mut utils = launch_test(compositor_app);
617        utils.wait_for_update().await;
618
619        let (layers, rendering_layers, _) = run_compositor(&utils, &mut compositor);
620        // First render is always a full render
621        assert_eq!(layers, rendering_layers);
622
623        utils.click_cursor((275., 375.)).await;
624
625        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
626
627        // Root + First rect + First Label + Second Label
628        assert_eq!(painted_nodes, 4);
629    }
630
631    #[tokio::test]
632    pub async fn rotated_shadow_drawing() {
633        fn compositor_app() -> Element {
634            let mut rotate = use_signal(|| 0);
635
636            rsx!(
637                rect {
638                    height: "50%",
639                    width: "100%",
640                    main_align: "center",
641                    cross_align: "center",
642                    background: "rgb(0, 119, 182)",
643                    color: "white",
644                    shadow: "0 4 20 5 rgb(0, 0, 0, 80)",
645                    label {
646                        rotate: "{rotate}deg",
647                        text_shadow: "0 180 12 rgb(0, 0, 0, 240)",
648                        "Hello"
649                    }
650                    label {
651                        "World"
652                    }
653                }
654                rect {
655                    height: "50%",
656                    width: "100%",
657                    main_align: "center",
658                    cross_align: "center",
659                    direction: "horizontal",
660                    Button {
661                        theme: theme_with!(ButtonTheme {
662                            shadow: "0 4 5 0 rgb(0, 0, 0, 0.1)".into(),
663                            padding: "8 12".into(),
664                        }),
665                        onclick: move |_| rotate += 1,
666                        label { "Rotate" }
667                    }
668                }
669            )
670        }
671
672        let mut compositor = Compositor::default();
673        let mut utils = launch_test(compositor_app);
674        utils.wait_for_update().await;
675
676        let (layers, rendering_layers, _) = run_compositor(&utils, &mut compositor);
677        // First render is always a full render
678        assert_eq!(layers, rendering_layers);
679
680        utils.click_cursor((275., 375.)).await;
681
682        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
683
684        // Everything
685        assert_eq!(painted_nodes, 7);
686    }
687
688    #[tokio::test]
689    pub async fn scale_drawing() {
690        fn compositor_app() -> Element {
691            let mut scale = use_signal(|| 1.);
692
693            rsx!(
694                rect {
695                    scale: "{scale()} {scale()}",
696                    height: "50%",
697                    width: "100%",
698                    main_align: "center",
699                    cross_align: "center",
700                    background: "rgb(0, 119, 182)",
701                    color: "white",
702                    shadow: "0 4 20 5 rgb(0, 0, 0, 80)",
703                    label {
704                        text_shadow: "0 180 12 rgb(0, 0, 0, 240)",
705                        "Hello"
706                    }
707                    label {
708                        "World"
709                    }
710                }
711                rect {
712                    height: "50%",
713                    width: "100%",
714                    main_align: "center",
715                    cross_align: "center",
716                    direction: "horizontal",
717                    Button {
718                        theme: theme_with!(ButtonTheme {
719                            shadow: "0 4 5 0 rgb(0, 0, 0, 0.1)".into(),
720                            padding: "8 12".into(),
721                        }),
722                        onclick: move |_| scale += 0.1,
723                        label { "More" }
724                    }
725                    Button {
726                        theme: theme_with!(ButtonTheme {
727                            shadow: "0 4 5 0 rgb(0, 0, 0, 0.1)".into(),
728                            padding: "8 12".into(),
729                        }),
730                        onclick: move |_| scale -= 0.1,
731                        label { "Less" }
732                    }
733                }
734            )
735        }
736
737        let mut compositor = Compositor::default();
738        let mut utils = launch_test_with_config(
739            compositor_app,
740            TestingConfig::<()> {
741                size: (400.0, 400.0).into(),
742                ..TestingConfig::default()
743            },
744        );
745        utils.wait_for_update().await;
746
747        let (layers, rendering_layers, _) = run_compositor(&utils, &mut compositor);
748        // First render is always a full render
749        assert_eq!(layers, rendering_layers);
750
751        utils.click_cursor((180., 310.)).await;
752        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
753        assert_eq!(painted_nodes, 9);
754
755        utils.click_cursor((250., 310.)).await;
756        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
757        assert_eq!(painted_nodes, 9);
758
759        utils.click_cursor((250., 310.)).await;
760        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
761        assert_eq!(painted_nodes, 7);
762
763        utils.click_cursor((250., 310.)).await;
764        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
765        assert_eq!(painted_nodes, 7);
766
767        utils.click_cursor((250., 310.)).await;
768        let (_, _, painted_nodes) = run_compositor(&utils, &mut compositor);
769        assert_eq!(painted_nodes, 5);
770    }
771}