Skip to main content

ferroui_base/rendering/composition/
composition_object.rs

1use super::animations::{ICompositionAnimation, ICompositionAnimationBase, ImplicitAnimationCollection};
2use super::expressions::ExpressionVariant;
3use super::server::{CompositionProperty, ServerObjectId};
4use super::{CompositionPropertySet, Compositor, ICompositorSerializable, PendingAnimations};
5use std::cell::{Cell, RefCell};
6use std::rc::Rc;
7
8/// The part every composition object embeds: what upstream's abstract
9/// `CompositionObject` holds.
10///
11/// A class "derives" from `CompositionObject` by embedding this value,
12/// implementing [`ICompositorSerializable`] through it, and forwarding the
13/// animation members to the functions of this module.
14///
15/// The server-side counterpart is known by id. Disposing the object
16/// disposes its counterpart with the next batch, which keeps it under its id
17/// while this object is alive: upstream the UI-thread object holds its
18/// server object, so jobs and animations that name it after the disposal
19/// still reach it, and the id cannot be reused meanwhile. When the object is
20/// dropped, its counterpart is disposed (if it was not) and released with a
21/// later batch: this stands in for the garbage collector, which reclaims a
22/// server object upstream once the UI-thread object is unreachable. A
23/// counterpart that was disposed out of band (a composition target) is kept
24/// and released the same way.
25pub struct CompositionObject {
26    compositor: Rc<Compositor>,
27    server: Option<ServerObjectId>,
28    is_disposed: Cell<bool>,
29    registered_for_serialization: Cell<bool>,
30    pending_animations: PendingAnimations,
31    implicit_animations: RefCell<Option<Rc<ImplicitAnimationCollection>>>,
32}
33
34impl CompositionObject {
35    pub fn new(compositor: &Rc<Compositor>, server: Option<ServerObjectId>) -> Self {
36        Self {
37            compositor: compositor.clone(),
38            server,
39            is_disposed: Cell::new(false),
40            registered_for_serialization: Cell::new(false),
41            pending_animations: PendingAnimations::new(),
42            implicit_animations: RefCell::new(None),
43        }
44    }
45
46    /// The collection of implicit animations attached to this object.
47    pub fn implicit_animations(&self) -> Option<Rc<ImplicitAnimationCollection>> {
48        self.implicit_animations.borrow().clone()
49    }
50
51    pub fn set_implicit_animations(&self, value: Option<Rc<ImplicitAnimationCollection>>) {
52        *self.implicit_animations.borrow_mut() = value;
53    }
54
55    /// The implicit animation attached to a property of the object, if
56    /// there is one (`ImplicitAnimations?.TryGetValue(name)` of the
57    /// generated setters).
58    pub fn implicit_animation(&self, property_name: &str) -> Option<Rc<dyn ICompositionAnimationBase>> {
59        let animations = self.implicit_animations.borrow().clone()?;
60        animations.try_get_value(property_name)
61    }
62
63    pub fn compositor(&self) -> &Rc<Compositor> {
64        &self.compositor
65    }
66
67    /// The id of the server-side counterpart, if the object has one.
68    pub fn server(&self) -> Option<ServerObjectId> {
69        self.server
70    }
71
72    /// The id of the server-side counterpart. Panics if there is none.
73    pub fn required_server(&self) -> ServerObjectId {
74        match self.server {
75            Some(server) => server,
76            None => panic!("There is no server-side counterpart for this object"),
77        }
78    }
79
80    /// `ICompositorSerializable.TryGetServer`. A disposed object has no
81    /// counterpart any more, so its pending changes are not written.
82    pub fn try_get_server(&self, c: &Compositor) -> Option<ServerObjectId> {
83        debug_assert!(std::ptr::eq(c, &*self.compositor));
84        if self.is_disposed.get() {
85            return None;
86        }
87        Some(self.required_server())
88    }
89
90    pub fn is_disposed(&self) -> bool {
91        self.is_disposed.get()
92    }
93
94    pub fn pending_animations(&self) -> &PendingAnimations {
95        &self.pending_animations
96    }
97
98    pub fn dispose(&self) {
99        if !self.is_disposed.get() {
100            if let Some(server) = self.server {
101                self.compositor.dispose_and_keep_on_next_batch(server);
102            }
103        }
104        self.is_disposed.set(true);
105    }
106
107    /// Marks the object disposed without queueing the disposal of its
108    /// server side, which is disposed out of band
109    /// ([`Compositor::oob_dispose`]). The server side stays under its id
110    /// until this object is dropped, as after [`dispose`](Self::dispose).
111    pub(crate) fn mark_disposed(&self) {
112        self.is_disposed.set(true);
113    }
114
115    /// Queues the object for serialization. `this` yields the handle of the
116    /// object that embeds this part.
117    pub fn register_for_serialization(&self, this: impl FnOnce() -> Option<Rc<dyn ICompositorSerializable>>) {
118        if self.server.is_none() {
119            panic!("The object doesn't have an associated server counterpart");
120        }
121        if self.registered_for_serialization.get() || self.is_disposed.get() {
122            return;
123        }
124        if let Some(this) = this() {
125            self.registered_for_serialization.set(true);
126            self.compositor.register_for_serialization(this);
127        }
128    }
129
130    /// The head of `ICompositorSerializable.SerializeChanges`: the object
131    /// may be registered again from now on.
132    pub fn begin_serialize_changes(&self, c: &Compositor) {
133        debug_assert!(std::ptr::eq(c, &*self.compositor));
134        self.registered_for_serialization.set(false);
135    }
136
137    /// `StopAnimation`: removes the animation of a property on the server.
138    /// `property` is the composition property the class resolved from the
139    /// property name. The server object is reached through a job of the
140    /// next batch.
141    pub fn stop_animation(&self, property: &'static CompositionProperty) {
142        let Some(server) = self.server else { return };
143        self.compositor.post_server_job(
144            move |compositor| {
145                if let Some(object) = compositor.get_animated_object(server) {
146                    if let Some(animations) = object.server_object().animations() {
147                        animations.remove_animation_for_property(property);
148                    }
149                }
150            },
151            false,
152        );
153    }
154}
155
156impl Drop for CompositionObject {
157    fn drop(&mut self) {
158        if let Some(server) = self.server {
159            if self.is_disposed.get() {
160                self.compositor.release_with_a_later_batch(server);
161            } else {
162                self.compositor.dispose_with_a_later_batch(server);
163            }
164        }
165    }
166}
167
168/// A composition object as code refers to any composition object: a
169/// reference parameter of an animation, an object stored in a property set
170/// (upstream `CompositionObject` as a parameter type).
171pub trait AsCompositionObject: 'static {
172    /// The embedded `CompositionObject`.
173    fn as_composition_object(&self) -> &CompositionObject;
174
175    /// The name of the class of the object, for messages.
176    fn composition_type_name(&self) -> &'static str;
177
178    /// The object as a property set, if it is one (the `is
179    /// CompositionPropertySet` test of upstream).
180    fn as_property_set(self: Rc<Self>) -> Option<Rc<CompositionPropertySet>> {
181        None
182    }
183}
184
185/// The animation members of `CompositionObject`, written against the class
186/// that embeds one.
187pub trait ICompositionObjectAnimations {
188    /// `StartAnimation(propertyName, animation, finalValue)` of the class:
189    /// returns `false` for a property the class does not know.
190    fn try_start_animation(
191        &self,
192        property_name: &str,
193        animation: &dyn ICompositionAnimation,
194        final_value: Option<ExpressionVariant>,
195    ) -> bool;
196
197    /// The composition property with the given name.
198    fn get_composition_property(&self, property_name: &str) -> Option<&'static CompositionProperty>;
199
200    /// The embedded `CompositionObject`.
201    fn composition_object(&self) -> &CompositionObject;
202
203    /// Connects an animation with the named property of the object and
204    /// starts the animation.
205    fn start_animation(&self, property_name: &str, animation: &dyn ICompositionAnimation) {
206        self.start_animation_with_final_value(property_name, animation, None)
207    }
208
209    fn start_animation_with_final_value(
210        &self,
211        property_name: &str,
212        animation: &dyn ICompositionAnimation,
213        final_value: Option<ExpressionVariant>,
214    ) {
215        if !self.try_start_animation(property_name, animation, final_value) {
216            panic!("Unknown property {property_name}");
217        }
218    }
219
220    /// Disconnects an animation from the named property and stops it.
221    fn stop_animation(&self, property_name: &str) {
222        let Some(property) = self.get_composition_property(property_name) else {
223            panic!("Unknown property {property_name}");
224        };
225        self.composition_object().stop_animation(property);
226    }
227
228    /// Starts an animation group.
229    /// The StartAnimationGroup method on CompositionObject lets you start CompositionAnimationGroup.
230    /// All the animations in the group will be started at the same time on the object.
231    fn start_animation_group(&self, grp: &dyn ICompositionAnimationBase) {
232        if let Some(animation) = grp.as_composition_animation() {
233            let Some(target) = animation.target() else { panic!("Animation Target can't be null") };
234            self.start_animation(&target, animation);
235        } else if let Some(group) = grp.as_composition_animation_group() {
236            for a in group.animations() {
237                let Some(target) = a.target() else { panic!("Animation Target can't be null") };
238                self.start_animation(&target, &*a);
239            }
240        }
241    }
242
243    /// `StartAnimationGroupPart`.
244    fn start_animation_group_part(
245        &self,
246        animation: &dyn ICompositionAnimation,
247        target: &str,
248        final_value: ExpressionVariant,
249    ) -> bool {
250        let Some(animation_target) = animation.target() else { panic!("Animation Target can't be null") };
251        if animation_target == target {
252            self.start_animation_with_final_value(&animation_target, animation, Some(final_value));
253            true
254        } else {
255            self.start_animation(&animation_target, animation);
256            false
257        }
258    }
259
260    /// Starts an animation or an animation group triggered by the change
261    /// of property `target` to `final_value`. Returns whether one of the
262    /// animations targets that property.
263    fn start_animation_group_for(
264        &self,
265        grp: &dyn ICompositionAnimationBase,
266        target: &str,
267        final_value: ExpressionVariant,
268    ) -> bool {
269        if let Some(animation) = grp.as_composition_animation() {
270            return self.start_animation_group_part(animation, target, final_value);
271        }
272        if let Some(group) = grp.as_composition_animation_group() {
273            let mut matched = false;
274            for a in group.animations() {
275                if a.target().is_none() {
276                    panic!("Animation Target can't be null");
277                }
278                if self.start_animation_group_part(&*a, target, final_value) {
279                    matched = true;
280                }
281            }
282            return matched;
283        }
284
285        panic!("the animation is neither an animation nor a group of animations");
286    }
287
288    /// Stops an animation group.
289    fn stop_animation_group(&self, grp: &dyn ICompositionAnimationBase) {
290        if let Some(animation) = grp.as_composition_animation() {
291            let Some(target) = animation.target() else { panic!("Animation Target can't be null") };
292            self.stop_animation(&target);
293        } else if let Some(group) = grp.as_composition_animation_group() {
294            for a in group.animations() {
295                let Some(target) = a.target() else { panic!("Animation Target can't be null") };
296                self.stop_animation(&target);
297            }
298        }
299    }
300}