Skip to main content

ferroui_base/rendering/composition/server/server_composition_visual/
update.rs

1use super::walker::{walk, IServerTreeVisitor, TreeWalkContext};
2use super::ServerCompositionVisual;
3use crate::media::EffectExtensions;
4use crate::platform::LtrbRect;
5use crate::rendering::composition::server::{CompositorPools, IDirtyRectCollector};
6use crate::Matrix;
7use std::rc::Rc;
8
9struct UpdateContext {
10    context: TreeWalkContext,
11    dirty_region: Rc<dyn IDirtyRectCollector>,
12    dirty_region_disable_count: i32,
13    dirty_region_disable_count_stack: Vec<i32>,
14    dirty_region_collector_stack: Vec<Rc<dyn IDirtyRectCollector>>,
15}
16
17impl UpdateContext {
18    fn new(pools: &CompositorPools, dirty_rects: Rc<dyn IDirtyRectCollector>, transform: Matrix, clip: LtrbRect) -> Self {
19        Self {
20            dirty_region: dirty_rects,
21            context: TreeWalkContext::new(pools, transform, clip),
22            dirty_region_disable_count: 0,
23            dirty_region_disable_count_stack: pools.int_stack_pool.rent(),
24            dirty_region_collector_stack: pools.dirty_rect_collector_stack_pool.rent(),
25        }
26    }
27
28    fn are_dirty_regions_disabled(&self) -> bool {
29        self.dirty_region_disable_count != 0
30    }
31
32    fn push_cache_if_needed(&mut self, visual: &ServerCompositionVisual) {
33        if let Some(cache) = visual.cache() {
34            let collector: Rc<dyn IDirtyRectCollector> = cache;
35            self.dirty_region_collector_stack.push(std::mem::replace(&mut self.dirty_region, collector));
36            self.dirty_region_disable_count_stack.push(self.dirty_region_disable_count);
37            self.dirty_region_disable_count = 0;
38            self.context.push_set_transform(Matrix::IDENTITY);
39            self.context.reset_clip(LtrbRect::INFINITE);
40        }
41    }
42
43    fn pop_cache_if_needed(&mut self, visual: &ServerCompositionVisual) {
44        if let Some(cache) = visual.cache() {
45            self.context.pop_clip();
46            self.context.pop_transform();
47            self.dirty_region = self.dirty_region_collector_stack.pop().expect("a collector was pushed");
48            self.dirty_region_disable_count = self.dirty_region_disable_count_stack.pop().expect("a count was pushed");
49            if cache.is_dirty() {
50                self.add_to_dirty_region(visual.sub_tree_bounds.get());
51            }
52        }
53    }
54
55    fn need_to_push_bounds_affecting_properties(node: &ServerCompositionVisual) -> bool {
56        node.is_dirty_for_render_in_subgraph.get()
57            || node.needs_to_add_extra_dirty_rect_to_dirty_region.get()
58            || node.content_changed.get()
59    }
60
61    fn finalize_subtree_bounds(node: &ServerCompositionVisual) {
62        // WPF simply removes drawing commands from every visual in invisible subtree (on UI thread).
63        // We set the bounds to null when computing subtree bounds for invisible nodes.
64        if !node.visible() {
65            node.sub_tree_bounds.set(None);
66        }
67
68        if let Some(mut bounds) = node.sub_tree_bounds.get() {
69            if let Some(effect) = node.effect() {
70                bounds = bounds.inflate(EffectExtensions::get_effect_output_padding(Some(&*effect)));
71                node.sub_tree_bounds.set(Some(bounds));
72            }
73
74            if let Some(clip) = node.own_clip_rect.get() {
75                node.sub_tree_bounds.set(bounds.intersect_or_null(clip));
76            }
77        }
78
79        match (node.sub_tree_bounds.get(), node.own_transform.get()) {
80            (None, _) => node.transformed_sub_tree_bounds.set(None),
81            (Some(bounds), Some(transform)) => {
82                node.transformed_sub_tree_bounds.set(Some(bounds.transform_to_aabb(transform)))
83            }
84            (Some(bounds), None) => node.transformed_sub_tree_bounds.set(Some(bounds)),
85        }
86
87        node.enqueue_for_readback_update();
88    }
89
90    fn add_to_dirty_region(&self, bounds: Option<LtrbRect>) {
91        let Some(bounds) = bounds else { return };
92        if self.dirty_region_disable_count != 0 {
93            return;
94        }
95
96        let transformed = bounds.transform_to_aabb(self.context.transform).intersect_or_empty(self.context.clip);
97        if transformed.is_zero_size() {
98            return;
99        }
100
101        self.dirty_region.add_rect(transformed);
102    }
103
104    fn push_bounds_affecting_properties(&mut self, node: &ServerCompositionVisual) {
105        if let Some(transform) = node.own_transform.get() {
106            self.context.push_transform(transform);
107        }
108
109        if let Some(clip) = node.own_clip_rect.get() {
110            self.context.push_clip(clip.transform_to_aabb(self.context.transform));
111        }
112    }
113
114    fn pop_bounds_affecting_properties(&mut self, node: &ServerCompositionVisual) {
115        if node.own_transform.get().is_some() {
116            self.context.pop_transform();
117        }
118        if node.own_clip_rect.get().is_some() {
119            self.context.pop_clip();
120        }
121    }
122
123    fn dispose(self, pools: &CompositorPools) {
124        pools.int_stack_pool.return_stack(self.dirty_region_disable_count_stack);
125        pools.dirty_rect_collector_stack_pool.return_stack(self.dirty_region_collector_stack);
126        self.context.dispose(pools);
127    }
128}
129
130impl IServerTreeVisitor for UpdateContext {
131    fn pre_subgraph(&mut self, node: &Rc<ServerCompositionVisual>) -> bool {
132        let visit_children = node.is_dirty_for_render_in_subgraph.get() || node.needs_bounding_box_update.get();
133
134        // If this node has an alpha mask an we caused its inner bounds to change
135        // then treat the node as if _isDirtyForRender was set.
136        if node.needs_bounding_box_update.get() && node.opacity_mask_brush().is_some() {
137            node.is_dirty_for_render.set(true);
138        }
139
140        // Special handling for effects: just add the entire node's old subtree bounds as a dirty region
141        // WPF does this because they had legacy effects with non-affine transforms, we do this because
142        // it's something to be done in the future (maybe)
143        if node.is_dirty_for_render.get()
144            || (node.is_dirty_for_render_in_subgraph.get() && node.content.has_effect(node))
145        {
146            // If bounds haven't actually changed, there is no point in adding them now since they will be added
147            // again in PostSubgraph.
148            if node.needs_bounding_box_update.get() && !self.are_dirty_regions_disabled() {
149                // We add this node's bbox to the dirty region. Alternatively we could walk the sub-graph and add the
150                // bbox of each node's content to the dirty region. Note that this is much harder to do because if the
151                // transform changes we don't know anymore the old transform. We would have to use to a two phased dirty
152                // region algorithm.
153                self.add_to_dirty_region(node.transformed_sub_tree_bounds.get());
154            }
155
156            // If we added a node in the parent chain to the bbox we don't need to add anything below this node
157            // to the dirty region.
158            self.dirty_region_disable_count += 1;
159        }
160
161        // If a node in the sub-graph of this node is dirty for render and we haven't collected the bbox of one of pNode's
162        // ascendants as dirty region, then we need to maintain the transform and clip stack so that we have a world transform
163        // when we need to collect the bbox of the descendant node that is dirty for render.  If something has changed
164        // in the contents or subgraph, we need to update the cache on this node.
165        if Self::need_to_push_bounds_affecting_properties(node) {
166            // Dirty regions will be enabled if we haven't collected an ancestor's bbox or if they were re-enabled
167            // by an ancestor's cache.
168            if !self.are_dirty_regions_disabled() {
169                self.push_bounds_affecting_properties(node);
170            }
171
172            self.push_cache_if_needed(node);
173        }
174
175        if node.needs_bounding_box_update.get() {
176            // This node's bbox needs to be updated. We start out by setting his bbox to the bbox of its content. All its
177            // children will union their bbox into their parent's bbox. PostSubgraph will clip the bbox and transform it
178            // to outer space.
179            node.sub_tree_bounds.set(node.own_content_bounds.get());
180        }
181
182        visit_children
183    }
184
185    fn post_subgraph(&mut self, node: &Rc<ServerCompositionVisual>) {
186        let parent = node.parent();
187
188        if node.needs_bounding_box_update.get() {
189            //
190            // If pNode's bbox got recomputed it is at this point still in inner
191            // space. We need to apply the clip and transform.
192            //
193            Self::finalize_subtree_bounds(node);
194        }
195
196        //
197        // Update state on the parent node if we have a parent.
198        if let Some(parent) = parent {
199            // Update the bounding box on the parent.
200            if parent.needs_bounding_box_update.get() {
201                parent
202                    .sub_tree_bounds
203                    .set(LtrbRect::full_union(parent.sub_tree_bounds.get(), node.transformed_sub_tree_bounds.get()));
204            }
205        }
206
207        //
208        // If there are additional dirty regions, pick them up. (Additional dirty regions are
209        // specified before the tranform, i.e. in inner space, hence we have to pick them
210        // up before we pop the transform from the transform stack.
211        //
212        if node.needs_to_add_extra_dirty_rect_to_dirty_region.get() {
213            self.add_to_dirty_region(Some(node.extra_dirty_rect.get()));
214        }
215
216        // If we pushed transforms here, we need to pop them again.  If we're handling a cache we need
217        // to finish handling it here as well.
218        if Self::need_to_push_bounds_affecting_properties(node) {
219            self.pop_cache_if_needed(node);
220
221            if !self.are_dirty_regions_disabled() {
222                self.pop_bounds_affecting_properties(node);
223            }
224        }
225
226        // Special handling for effects: just add the entire node's old subtree bounds as a dirty region
227        // WPF does this because they had legacy effects with non-affine transforms, we do this because
228        // it's something to be done in the future (maybe)
229        if node.is_dirty_for_render.get() || (node.is_dirty_for_render_in_subgraph.get() && node.effect().is_some()) {
230            self.dirty_region_disable_count -= 1;
231            self.add_to_dirty_region(node.transformed_sub_tree_bounds.get());
232        }
233
234        node.is_dirty_for_render.set(false);
235        node.is_dirty_for_render_in_subgraph.set(false);
236        node.needs_bounding_box_update.set(false);
237        node.needs_to_add_extra_dirty_rect_to_dirty_region.set(false);
238        node.content_changed.set(false);
239    }
240}
241
242impl ServerCompositionVisual {
243    pub fn update_root(self: &Rc<Self>, tracker: Rc<dyn IDirtyRectCollector>, transform: Matrix, clip: LtrbRect) {
244        let Some(compositor) = self.compositor() else { return };
245        let pools = compositor.pools();
246        let mut context = UpdateContext::new(pools, tracker, transform, clip);
247        walk(&mut context, self, pools);
248        context.dispose(pools);
249    }
250}