Skip to main content

ferroui_base/rendering/composition/
element_composition_preview.rs

1use super::animations::ICompositionAnimationBase;
2use super::{
3    CompositionBatchTypes, CompositionVisual, Compositor, ICompositionObjectAnimations, ICompositionSurface,
4    LoadedImageSurface,
5};
6use crate::media::imaging::RenderTargetBitmap;
7use crate::platform::IPlatformRenderInterface;
8use crate::reactive::IDisposable;
9use crate::{FerroLocator, LocatorExtensions, PixelSize, Vector, Vector3D, Visual, VisualTreeAttachmentEventArgs, WeakRef};
10use std::cell::RefCell;
11use std::rc::Rc;
12
13crate::ferro_thread_local! {
14    /// The implicit show and hide animations of the elements that have one, and the elements
15    /// that enabled translation (`SetImplicitShowAnimation`, `SetImplicitHideAnimation`,
16    /// `SetIsTranslationEnabled` of WinUI's `ElementCompositionPreview`).
17    static IMPLICIT: RefCell<Vec<Rc<ImplicitEntry>>> = const { RefCell::new(Vec::new()) };
18}
19
20/// Access to the composition visuals of the visuals of the visual tree.
21pub struct ElementComposition;
22
23impl ElementComposition {
24    /// Gets the composition visual of a visual; `None` while the visual is
25    /// not attached to a compositor.
26    pub fn get_element_visual(visual: &Visual) -> Option<Rc<CompositionVisual>> {
27        visual.composition_visual().map(|v| (*v).clone())
28    }
29
30    /// Sets a composition visual that is shown as the last child of the
31    /// composition visual of `visual`.
32    pub fn set_element_child_visual(visual: &Visual, composition_visual: Option<Rc<CompositionVisual>>) {
33        if let (Some(child), Some(own)) = (&composition_visual, visual.composition_visual()) {
34            if !Rc::ptr_eq(child.compositor(), own.compositor()) {
35                panic!("Composition visuals belong to different compositor instances");
36            }
37        }
38
39        // Un-parent the previous child here rather than waiting for the renderer to
40        // reconcile the children, because that sync may never run again for this element:
41        // a child cleared from on_detached_from_visual_tree is cleared before
42        // detach_from_compositor discards the element's composition visual, and a child
43        // left parented there cannot be attached anywhere else.
44        if let Some(old_child) = visual.child_composition_visual() {
45            if !composition_visual.as_ref().is_some_and(|child| Rc::ptr_eq(child, &old_child)) {
46                if let Some(own) = visual.composition_visual() {
47                    own.children().remove(&old_child);
48                }
49            }
50        }
51
52        visual.set_child_composition_visual(composition_visual);
53        if let Some(source) = visual.presentation_source() {
54            source.renderer().recalculate_children(visual);
55        }
56    }
57
58    /// Gets the composition visual set with
59    /// [`set_element_child_visual`](Self::set_element_child_visual).
60    pub fn get_element_child_visual(visual: &Visual) -> Option<Rc<CompositionVisual>> {
61        visual.child_composition_visual()
62    }
63
64    /// `ElementCompositionPreview.SetImplicitShowAnimation`: the animation, or the group of
65    /// animations, started on the composition visual of `visual` when the visual is added to a
66    /// visual tree or its `IsVisible` becomes `true`. Every animation needs its `Target`.
67    pub fn set_implicit_show_animation(visual: &Visual, animation: Option<Rc<dyn ICompositionAnimationBase>>) {
68        ImplicitEntry::update(visual, |entry| *entry.show.borrow_mut() = animation);
69    }
70
71    /// The animation of [`set_implicit_show_animation`](Self::set_implicit_show_animation).
72    pub fn get_implicit_show_animation(visual: &Visual) -> Option<Rc<dyn ICompositionAnimationBase>> {
73        ImplicitEntry::find(visual).and_then(|entry| entry.show.borrow().clone())
74    }
75
76    /// `ElementCompositionPreview.SetImplicitHideAnimation`: the animation, or the group of
77    /// animations, started on the composition visual of `visual` when its `IsVisible` becomes
78    /// `false` or the visual is removed from its visual tree. The visual stays visible until the
79    /// animations complete. Every animation needs its `Target`.
80    ///
81    /// A visual removed from its tree loses its composition visual at once, so the hide
82    /// animation of a removal runs on an image of the visual taken when it is removed (WinUI
83    /// keeps the element itself in its unloading storage until the animation completes).
84    pub fn set_implicit_hide_animation(visual: &Visual, animation: Option<Rc<dyn ICompositionAnimationBase>>) {
85        ImplicitEntry::update(visual, |entry| *entry.hide.borrow_mut() = animation);
86    }
87
88    /// The animation of [`set_implicit_hide_animation`](Self::set_implicit_hide_animation).
89    pub fn get_implicit_hide_animation(visual: &Visual) -> Option<Rc<dyn ICompositionAnimationBase>> {
90        ImplicitEntry::find(visual).and_then(|entry| entry.hide.borrow().clone())
91    }
92
93    /// `ElementCompositionPreview.SetIsTranslationEnabled`. Every composition visual has a
94    /// `Translation` property applied after its other transforms, so translation is always
95    /// enabled; the request is recorded and changes nothing else.
96    pub fn set_is_translation_enabled(visual: &Visual, value: bool) {
97        ImplicitEntry::update(visual, |entry| entry.translation_enabled.set(value));
98    }
99
100    /// Whether [`set_is_translation_enabled`](Self::set_is_translation_enabled) was called with
101    /// `true` for `visual`.
102    pub fn is_translation_enabled(visual: &Visual) -> bool {
103        ImplicitEntry::find(visual).is_some_and(|entry| entry.translation_enabled.get())
104    }
105}
106
107/// The implicit animations of one visual and the subscriptions that run them.
108struct ImplicitEntry {
109    visual: WeakRef<Visual>,
110    show: RefCell<Option<Rc<dyn ICompositionAnimationBase>>>,
111    hide: RefCell<Option<Rc<dyn ICompositionAnimationBase>>>,
112    translation_enabled: std::cell::Cell<bool>,
113    /// While a hide animation runs: the subscription to the completion of its batch. The visual
114    /// holds a show request meanwhile.
115    hiding: RefCell<Option<Rc<dyn IDisposable>>>,
116    subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
117}
118
119impl ImplicitEntry {
120    fn find(visual: &Visual) -> Option<Rc<ImplicitEntry>> {
121        IMPLICIT.with(|entries| {
122            entries
123                .borrow()
124                .iter()
125                .find(|entry| entry.visual.upgrade().is_some_and(|v| std::ptr::eq(&*v, visual)))
126                .cloned()
127        })
128    }
129
130    /// The entry of `visual`, created with its subscriptions, changed by `f`. The entries of
131    /// visuals that are gone are dropped on the way.
132    fn update(visual: &Visual, f: impl FnOnce(&ImplicitEntry)) {
133        IMPLICIT.with(|entries| {
134            entries.borrow_mut().retain(|entry| {
135                let alive = entry.visual.upgrade().is_some();
136                if !alive {
137                    for subscription in entry.subscriptions.borrow_mut().drain(..) {
138                        subscription.dispose();
139                    }
140                }
141                alive
142            })
143        });
144        let entry = match Self::find(visual) {
145            Some(entry) => entry,
146            None => {
147                let entry = Rc::new(ImplicitEntry {
148                    visual: visual.to_ref().downgrade(),
149                    show: RefCell::new(None),
150                    hide: RefCell::new(None),
151                    translation_enabled: std::cell::Cell::new(false),
152                    hiding: RefCell::new(None),
153                    subscriptions: RefCell::new(Vec::new()),
154                });
155                entry.subscribe(visual);
156                IMPLICIT.with(|entries| entries.borrow_mut().push(entry.clone()));
157                entry
158            }
159        };
160        f(&entry);
161    }
162
163    fn subscribe(self: &Rc<Self>, visual: &Visual) {
164        let weak = Rc::downgrade(self);
165        let changed = visual.property_changed(move |e| {
166            if e.property() != Visual::is_visible_property().as_property() {
167                return;
168            }
169            let Some(entry) = weak.upgrade() else { return };
170            if e.get_new_value::<bool>() {
171                entry.on_shown();
172            } else {
173                entry.on_hidden();
174            }
175        });
176        let weak = Rc::downgrade(self);
177        let attached = visual.attached_to_visual_tree(move |_| {
178            if let Some(entry) = weak.upgrade() {
179                entry.on_shown();
180            }
181        });
182        let weak = Rc::downgrade(self);
183        let detached = visual.detached_from_visual_tree(move |e| {
184            if let Some(entry) = weak.upgrade() {
185                entry.on_removed(e);
186            }
187        });
188        self.subscriptions.borrow_mut().extend([changed, attached, detached]);
189    }
190
191    /// The compositor of the visual, while it is in a tree rendered by one.
192    fn compositor(visual: &Visual) -> Option<Rc<Compositor>> {
193        visual.presentation_source().and_then(|source| source.renderer().compositor())
194    }
195
196    /// Ends a running hide animation: the visual follows `IsVisible` again.
197    fn end_hiding(&self, visual: &Visual) {
198        if let Some(subscription) = self.hiding.borrow_mut().take() {
199            subscription.dispose();
200            visual.remove_composition_show_request();
201        }
202    }
203
204    /// The visual was added to a tree or became visible: the show animation starts on its
205    /// composition visual before the next commit, once the renderer created it.
206    fn on_shown(self: &Rc<Self>) {
207        let Some(visual) = self.visual.upgrade() else { return };
208        self.end_hiding(&visual);
209        if self.show.borrow().is_none() || !visual.is_visible() {
210            return;
211        }
212        let Some(compositor) = Self::compositor(&visual) else { return };
213        let weak = Rc::downgrade(self);
214        compositor.request_composition_update(move || {
215            let Some(entry) = weak.upgrade() else { return };
216            let Some(visual) = entry.visual.upgrade() else { return };
217            let (Some(animation), Some(composition_visual)) = (entry.show.borrow().clone(), visual.composition_visual())
218            else {
219                return;
220            };
221            composition_visual.start_animation_group(&*animation);
222        });
223    }
224
225    /// `IsVisible` became `false`: the hide animation runs in a scoped batch while the
226    /// composition visual is kept visible; its completion lets the visual hide.
227    fn on_hidden(self: &Rc<Self>) {
228        let Some(visual) = self.visual.upgrade() else { return };
229        let Some(animation) = self.hide.borrow().clone() else { return };
230        let Some(composition_visual) = visual.composition_visual() else { return };
231        self.end_hiding(&visual);
232        let compositor = composition_visual.compositor().clone();
233        visual.add_composition_show_request();
234        let batch = compositor.create_scoped_batch(CompositionBatchTypes::ANIMATION);
235        composition_visual.start_animation_group(&*animation);
236        batch.end();
237        let weak = Rc::downgrade(self);
238        let subscription = batch.completed(move |_| {
239            let Some(entry) = weak.upgrade() else { return };
240            let Some(visual) = entry.visual.upgrade() else { return };
241            entry.end_hiding(&visual);
242        });
243        *self.hiding.borrow_mut() = Some(subscription);
244    }
245
246    /// The visual was removed from its tree: its composition visual is gone, so the hide
247    /// animation plays on an image of it, shown by the former parent where the visual was
248    /// (WinUI keeps the element itself in unloading storage). Only the root of a removed subtree
249    /// plays its animation.
250    fn on_removed(self: &Rc<Self>, e: &VisualTreeAttachmentEventArgs) {
251        let Some(visual) = self.visual.upgrade() else { return };
252        self.end_hiding(&visual);
253        let Some(animation) = self.hide.borrow().clone() else { return };
254        let Some(parent) = e.attachment_point().cloned() else { return };
255        if visual.visual_parent().is_some_and(|own| !std::ptr::eq(&*own, &*parent)) {
256            return;
257        }
258        let (Some(parent_visual), Some(source)) = (parent.composition_visual(), parent.presentation_source()) else {
259            return;
260        };
261        if !visual.is_visible() || FerroLocator::current().get_service::<dyn IPlatformRenderInterface>().is_none() {
262            return;
263        }
264        let bounds = visual.bounds();
265        let scaling = source.render_scaling();
266        let pixel_size = PixelSize::new((bounds.width * scaling).ceil() as i32, (bounds.height * scaling).ceil() as i32);
267        if pixel_size.width <= 0 || pixel_size.height <= 0 {
268            return;
269        }
270        let image = RenderTargetBitmap::with_dpi(pixel_size, Vector::new(96.0 * scaling, 96.0 * scaling));
271        image.render(&visual.to_ref());
272
273        let compositor = parent_visual.compositor().clone();
274        let surface = LoadedImageSurface::from_bitmap(&compositor, &image);
275        let brush = compositor.create_surface_brush_with_surface(surface as Rc<dyn ICompositionSurface>);
276        let sprite = compositor.create_sprite_visual();
277        sprite.set_brush(Some((*brush).clone()));
278        sprite.set_size(Vector::new(bounds.width, bounds.height));
279        sprite.set_offset(Vector3D::new(bounds.x, bounds.y, 0.0));
280        sprite.set_opacity(visual.opacity() as f32);
281        let ghost: Rc<CompositionVisual> = (*sprite).clone();
282        parent.add_composition_unloading_visual(ghost.clone());
283
284        let batch = compositor.create_scoped_batch(CompositionBatchTypes::ANIMATION);
285        ghost.start_animation_group(&*animation);
286        batch.end();
287        let weak_parent = parent.downgrade();
288        let subscription: Rc<RefCell<Option<Rc<dyn IDisposable>>>> = Rc::new(RefCell::new(None));
289        let held = subscription.clone();
290        let completed = batch.completed(move |_| {
291            if let Some(parent) = weak_parent.upgrade() {
292                parent.remove_composition_unloading_visual(&ghost);
293            }
294            if let Some(subscription) = held.borrow_mut().take() {
295                subscription.dispose();
296            }
297        });
298        *subscription.borrow_mut() = Some(completed);
299    }
300}