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            // What the content adds around its subtree (not from upstream).
74            if let Some(padding) = node.content.sub_tree_bounds_padding(node) {
75                bounds = bounds.inflate(padding);
76                node.sub_tree_bounds.set(Some(bounds));
77            }
78
79            if let Some(clip) = node.own_clip_rect.get() {
80                node.sub_tree_bounds.set(bounds.intersect_or_null(clip));
81            }
82        }
83
84        match (node.sub_tree_bounds.get(), node.own_transform.get()) {
85            (None, _) => node.transformed_sub_tree_bounds.set(None),
86            (Some(bounds), Some(transform)) => {
87                node.transformed_sub_tree_bounds.set(Some(bounds.transform_to_aabb(transform)))
88            }
89            (Some(bounds), None) => node.transformed_sub_tree_bounds.set(Some(bounds)),
90        }
91
92        node.enqueue_for_readback_update();
93    }
94
95    fn add_to_dirty_region(&self, bounds: Option<LtrbRect>) {
96        let Some(bounds) = bounds else { return };
97        if self.dirty_region_disable_count != 0 {
98            return;
99        }
100
101        let transformed = bounds.transform_to_aabb(self.context.transform).intersect_or_empty(self.context.clip);
102        if transformed.is_zero_size() {
103            return;
104        }
105
106        self.dirty_region.add_rect(transformed);
107    }
108
109    fn push_bounds_affecting_properties(&mut self, node: &ServerCompositionVisual) {
110        if let Some(transform) = node.own_transform.get() {
111            self.context.push_transform(transform);
112        }
113
114        if let Some(clip) = node.own_clip_rect.get() {
115            self.context.push_clip(clip.transform_to_aabb(self.context.transform));
116        }
117    }
118
119    fn pop_bounds_affecting_properties(&mut self, node: &ServerCompositionVisual) {
120        if node.own_transform.get().is_some() {
121            self.context.pop_transform();
122        }
123        if node.own_clip_rect.get().is_some() {
124            self.context.pop_clip();
125        }
126    }
127
128    fn dispose(self, pools: &CompositorPools) {
129        pools.int_stack_pool.return_stack(self.dirty_region_disable_count_stack);
130        pools.dirty_rect_collector_stack_pool.return_stack(self.dirty_region_collector_stack);
131        self.context.dispose(pools);
132    }
133}
134
135impl IServerTreeVisitor for UpdateContext {
136    fn pre_subgraph(&mut self, node: &Rc<ServerCompositionVisual>) -> bool {
137        let visit_children = node.is_dirty_for_render_in_subgraph.get() || node.needs_bounding_box_update.get();
138
139        // If this node has an alpha mask an we caused its inner bounds to change
140        // then treat the node as if _isDirtyForRender was set.
141        if node.needs_bounding_box_update.get() && node.opacity_mask_brush().is_some() {
142            node.is_dirty_for_render.set(true);
143        }
144
145        // Special handling for effects: just add the entire node's old subtree bounds as a dirty region
146        // WPF does this because they had legacy effects with non-affine transforms, we do this because
147        // it's something to be done in the future (maybe)
148        if node.is_dirty_for_render.get()
149            || (node.is_dirty_for_render_in_subgraph.get() && node.content.has_effect(node))
150        {
151            // If bounds haven't actually changed, there is no point in adding them now since they will be added
152            // again in PostSubgraph.
153            if node.needs_bounding_box_update.get() && !self.are_dirty_regions_disabled() {
154                // We add this node's bbox to the dirty region. Alternatively we could walk the sub-graph and add the
155                // bbox of each node's content to the dirty region. Note that this is much harder to do because if the
156                // transform changes we don't know anymore the old transform. We would have to use to a two phased dirty
157                // region algorithm.
158                self.add_to_dirty_region(node.transformed_sub_tree_bounds.get());
159            }
160
161            // If we added a node in the parent chain to the bbox we don't need to add anything below this node
162            // to the dirty region.
163            self.dirty_region_disable_count += 1;
164        }
165
166        // 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
167        // ascendants as dirty region, then we need to maintain the transform and clip stack so that we have a world transform
168        // when we need to collect the bbox of the descendant node that is dirty for render.  If something has changed
169        // in the contents or subgraph, we need to update the cache on this node.
170        if Self::need_to_push_bounds_affecting_properties(node) {
171            // Dirty regions will be enabled if we haven't collected an ancestor's bbox or if they were re-enabled
172            // by an ancestor's cache.
173            if !self.are_dirty_regions_disabled() {
174                self.push_bounds_affecting_properties(node);
175            }
176
177            self.push_cache_if_needed(node);
178        }
179
180        if node.needs_bounding_box_update.get() {
181            // This node's bbox needs to be updated. We start out by setting his bbox to the bbox of its content. All its
182            // children will union their bbox into their parent's bbox. PostSubgraph will clip the bbox and transform it
183            // to outer space.
184            // Bounds that depend on other visuals are taken now (not from
185            // upstream: a redirect visual covers its source).
186            if node.content.has_update_pass_content_bounds() {
187                node.own_content_bounds
188                    .set(node.compute_own_content_bounds().and_then(LtrbRect::null_if_zero_size));
189            }
190            node.sub_tree_bounds.set(node.own_content_bounds.get());
191        }
192
193        visit_children
194    }
195
196    fn post_subgraph(&mut self, node: &Rc<ServerCompositionVisual>) {
197        let parent = node.parent();
198
199        if node.needs_bounding_box_update.get() {
200            //
201            // If pNode's bbox got recomputed it is at this point still in inner
202            // space. We need to apply the clip and transform.
203            //
204            Self::finalize_subtree_bounds(node);
205        }
206
207        //
208        // Update state on the parent node if we have a parent.
209        if let Some(parent) = parent {
210            // Update the bounding box on the parent.
211            if parent.needs_bounding_box_update.get() {
212                parent
213                    .sub_tree_bounds
214                    .set(LtrbRect::full_union(parent.sub_tree_bounds.get(), node.transformed_sub_tree_bounds.get()));
215            }
216        }
217
218        //
219        // If there are additional dirty regions, pick them up. (Additional dirty regions are
220        // specified before the tranform, i.e. in inner space, hence we have to pick them
221        // up before we pop the transform from the transform stack.
222        //
223        if node.needs_to_add_extra_dirty_rect_to_dirty_region.get() {
224            self.add_to_dirty_region(Some(node.extra_dirty_rect.get()));
225        }
226
227        // If we pushed transforms here, we need to pop them again.  If we're handling a cache we need
228        // to finish handling it here as well.
229        if Self::need_to_push_bounds_affecting_properties(node) {
230            self.pop_cache_if_needed(node);
231
232            if !self.are_dirty_regions_disabled() {
233                self.pop_bounds_affecting_properties(node);
234            }
235        }
236
237        // Special handling for effects: just add the entire node's old subtree bounds as a dirty region
238        // WPF does this because they had legacy effects with non-affine transforms, we do this because
239        // it's something to be done in the future (maybe)
240        // As in PreSubgraph: the content says whether it has an effect (the
241        // default is `effect().is_some()`), so the count stays balanced.
242        if node.is_dirty_for_render.get()
243            || (node.is_dirty_for_render_in_subgraph.get() && node.content.has_effect(node))
244        {
245            self.dirty_region_disable_count -= 1;
246            self.add_to_dirty_region(node.transformed_sub_tree_bounds.get());
247        }
248
249        node.is_dirty_for_render.set(false);
250        node.is_dirty_for_render_in_subgraph.set(false);
251        node.needs_bounding_box_update.set(false);
252        node.needs_to_add_extra_dirty_rect_to_dirty_region.set(false);
253        node.content_changed.set(false);
254    }
255}
256
257impl ServerCompositionVisual {
258    pub fn update_root(self: &Rc<Self>, tracker: Rc<dyn IDirtyRectCollector>, transform: Matrix, clip: LtrbRect) {
259        let Some(compositor) = self.compositor() else { return };
260        let pools = compositor.pools();
261        let mut context = UpdateContext::new(pools, tracker, transform, clip);
262        walk(&mut context, self, pools);
263        context.dispose(pools);
264    }
265}