Skip to main content

ferroui_base/rendering/composition/
composition_object.rs

1use super::animations::{
2    extract_subchannel, parse_subchannel, AnimationController, ICompositionAnimation, ICompositionAnimationBase,
3    ImplicitAnimationCollection, PreparedAnimation, SubchannelAnimation,
4};
5use super::expressions::ExpressionVariant;
6use super::server::{CompositionProperty, ServerObjectId};
7use super::{CompositionPropertySet, Compositor, ICompositorSerializable, PendingAnimations};
8use std::cell::{Cell, RefCell};
9use std::rc::Rc;
10
11/// The part every composition object embeds: what upstream's abstract
12/// `CompositionObject` holds.
13///
14/// A class "derives" from `CompositionObject` by embedding this value,
15/// implementing [`ICompositorSerializable`] through it, and forwarding the
16/// animation members to the functions of this module.
17///
18/// The server-side counterpart is known by id. Disposing the object
19/// disposes its counterpart with the next batch, which keeps it under its id
20/// while this object is alive: upstream the UI-thread object holds its
21/// server object, so jobs and animations that name it after the disposal
22/// still reach it, and the id cannot be reused meanwhile. When the object is
23/// dropped, its counterpart is disposed (if it was not) and released with a
24/// later batch: this stands in for the garbage collector, which reclaims a
25/// server object upstream once the UI-thread object is unreachable. A
26/// counterpart that was disposed out of band (a composition target) is kept
27/// and released the same way.
28pub struct CompositionObject {
29    compositor: Rc<Compositor>,
30    server: Option<ServerObjectId>,
31    is_disposed: Cell<bool>,
32    registered_for_serialization: Cell<bool>,
33    pending_animations: PendingAnimations,
34    implicit_animations: RefCell<Option<Rc<ImplicitAnimationCollection>>>,
35    /// `Properties`, created when it is first asked for.
36    properties: RefCell<Option<Rc<CompositionPropertySet>>>,
37    /// `Comment`.
38    comment: RefCell<String>,
39    /// The properties animated through this object, by the name the
40    /// animation was started with, and the controller of the animation if it
41    /// has one (see `TryGetAnimationController`).
42    animation_controllers: RefCell<Vec<(String, Option<Rc<AnimationController>>)>>,
43}
44
45impl CompositionObject {
46    pub fn new(compositor: &Rc<Compositor>, server: Option<ServerObjectId>) -> Self {
47        Self {
48            compositor: compositor.clone(),
49            server,
50            is_disposed: Cell::new(false),
51            registered_for_serialization: Cell::new(false),
52            pending_animations: PendingAnimations::new(),
53            implicit_animations: RefCell::new(None),
54            properties: RefCell::new(None),
55            comment: RefCell::new(String::new()),
56            animation_controllers: RefCell::new(Vec::new()),
57        }
58    }
59
60    /// The property set of the object (WinUI `CompositionObject.Properties`):
61    /// a live property set that belongs to the object, for the values an
62    /// application keeps with it. Animations and expressions use it as any
63    /// property set (`set_reference_parameter("props", obj.properties())`,
64    /// then `props.Name`). Created when it is first asked for.
65    //
66    // Deviation (winui-composition.md): in WinUI an expression that refers to
67    // an object reads the entries of its `Properties` as members of the
68    // object (`visual.Name`); here they are read through a reference to the
69    // property set itself.
70    pub fn properties(&self) -> Rc<CompositionPropertySet> {
71        self.properties
72            .borrow_mut()
73            .get_or_insert_with(|| CompositionPropertySet::new(&self.compositor))
74            .clone()
75    }
76
77    /// A string the application associates with the object, for debugging
78    /// (WinUI `Comment`); empty by default.
79    pub fn comment(&self) -> String {
80        self.comment.borrow().clone()
81    }
82
83    pub fn set_comment(&self, value: String) {
84        *self.comment.borrow_mut() = value;
85    }
86
87    /// Records that an animation was started on `property_name` (as given to
88    /// `StartAnimation`), with its controller if it has one.
89    pub(crate) fn note_animation_started(&self, property_name: &str, controller: Option<Rc<AnimationController>>) {
90        let mut controllers = self.animation_controllers.borrow_mut();
91        match controllers.iter_mut().find(|(name, _)| name == property_name) {
92            Some(entry) => entry.1 = controller,
93            None => controllers.push((property_name.to_owned(), controller)),
94        }
95    }
96
97    /// Forgets the animation of `property_name`: it was stopped.
98    pub(crate) fn note_animation_stopped(&self, property_name: &str) {
99        self.animation_controllers.borrow_mut().retain(|(name, _)| name != property_name);
100    }
101
102    /// The controller of the animation started on `property_name`: `None`
103    /// when no animation was started on it, `Some(None)` when it was started
104    /// without a controller.
105    pub(crate) fn animation_controller_entry(&self, property_name: &str) -> Option<Option<Rc<AnimationController>>> {
106        self.animation_controllers
107            .borrow()
108            .iter()
109            .find(|(name, _)| name == property_name)
110            .map(|(_, controller)| controller.clone())
111    }
112
113    /// Puts the running animation of `property` under `controller` on the
114    /// server, and records the controller for `property_name`.
115    pub(crate) fn attach_animation_controller(
116        &self,
117        property_name: &str,
118        property: &'static CompositionProperty,
119        controller: Rc<AnimationController>,
120    ) {
121        let controller_id = controller.server();
122        self.note_animation_started(property_name, Some(controller));
123        let Some(server) = self.server else { return };
124        self.compositor.post_server_job(
125            move |compositor| {
126                if let Some(object) = compositor.get_animated_object(server) {
127                    if let Some(animations) = object.server_object().animations() {
128                        animations.attach_controller(property, controller_id);
129                    }
130                }
131            },
132            false,
133        );
134    }
135
136    /// The collection of implicit animations attached to this object.
137    pub fn implicit_animations(&self) -> Option<Rc<ImplicitAnimationCollection>> {
138        self.implicit_animations.borrow().clone()
139    }
140
141    pub fn set_implicit_animations(&self, value: Option<Rc<ImplicitAnimationCollection>>) {
142        *self.implicit_animations.borrow_mut() = value;
143    }
144
145    /// The implicit animation attached to a property of the object, if
146    /// there is one (`ImplicitAnimations?.TryGetValue(name)` of the
147    /// generated setters).
148    pub fn implicit_animation(&self, property_name: &str) -> Option<Rc<dyn ICompositionAnimationBase>> {
149        let animations = self.implicit_animations.borrow().clone()?;
150        animations.try_get_value(property_name)
151    }
152
153    pub fn compositor(&self) -> &Rc<Compositor> {
154        &self.compositor
155    }
156
157    /// The id of the server-side counterpart, if the object has one.
158    pub fn server(&self) -> Option<ServerObjectId> {
159        self.server
160    }
161
162    /// The id of the server-side counterpart. Panics if there is none.
163    pub fn required_server(&self) -> ServerObjectId {
164        match self.server {
165            Some(server) => server,
166            None => panic!("There is no server-side counterpart for this object"),
167        }
168    }
169
170    /// `ICompositorSerializable.TryGetServer`. A disposed object has no
171    /// counterpart any more, so its pending changes are not written.
172    pub fn try_get_server(&self, c: &Compositor) -> Option<ServerObjectId> {
173        debug_assert!(std::ptr::eq(c, &*self.compositor));
174        if self.is_disposed.get() {
175            return None;
176        }
177        Some(self.required_server())
178    }
179
180    pub fn is_disposed(&self) -> bool {
181        self.is_disposed.get()
182    }
183
184    pub fn pending_animations(&self) -> &PendingAnimations {
185        &self.pending_animations
186    }
187
188    pub fn dispose(&self) {
189        if !self.is_disposed.get() {
190            if let Some(server) = self.server {
191                self.compositor.dispose_and_keep_on_next_batch(server);
192            }
193        }
194        // The animations of a disposed object are over for the batches they
195        // belong to (WinUI stops them).
196        if let (false, Some(server), true) =
197            (self.is_disposed.get(), self.server, self.compositor.animation_batches().used())
198        {
199            self.compositor.post_server_job(
200                move |compositor| {
201                    if let Some(object) = compositor.get_animated_object(server) {
202                        if let Some(animations) = object.server_object().animations() {
203                            animations.release_completions();
204                        }
205                    }
206                },
207                false,
208            );
209        }
210        self.is_disposed.set(true);
211    }
212
213    /// Marks the object disposed without queueing the disposal of its
214    /// server side, which is disposed out of band
215    /// ([`Compositor::oob_dispose`]). The server side stays under its id
216    /// until this object is dropped, as after [`dispose`](Self::dispose).
217    pub(crate) fn mark_disposed(&self) {
218        self.is_disposed.set(true);
219    }
220
221    /// Queues the object for serialization. `this` yields the handle of the
222    /// object that embeds this part.
223    pub fn register_for_serialization(&self, this: impl FnOnce() -> Option<Rc<dyn ICompositorSerializable>>) {
224        if self.server.is_none() {
225            panic!("The object doesn't have an associated server counterpart");
226        }
227        if self.registered_for_serialization.get() || self.is_disposed.get() {
228            return;
229        }
230        if let Some(this) = this() {
231            self.registered_for_serialization.set(true);
232            self.compositor.register_for_serialization(this);
233        }
234    }
235
236    /// The head of `ICompositorSerializable.SerializeChanges`: the object
237    /// may be registered again from now on.
238    pub fn begin_serialize_changes(&self, c: &Compositor) {
239        debug_assert!(std::ptr::eq(c, &*self.compositor));
240        self.registered_for_serialization.set(false);
241    }
242
243    /// `StopAnimation`: removes the animation of a property on the server
244    /// and applies its stop behavior. `property` is the composition
245    /// property the class resolved from the property name. The server
246    /// object is reached through a job of the next batch.
247    pub fn stop_animation(&self, property: &'static CompositionProperty) {
248        let Some(server) = self.server else { return };
249        self.compositor.post_server_job(
250            move |compositor| {
251                if let Some(object) = compositor.get_animated_object(server) {
252                    if let Some(animations) = object.server_object().animations() {
253                        animations.stop_animation_for_property(property);
254                    }
255                }
256            },
257            false,
258        );
259    }
260
261    /// `StopAnimation` of a sub-channel (`Offset.X`): stops the animations
262    /// of the sub-channels of `property` that share a component with
263    /// `components` (indices, X = 0) on the server.
264    pub fn stop_subchannel_animation(&self, property: &'static CompositionProperty, components: Vec<usize>) {
265        let Some(server) = self.server else { return };
266        self.compositor.post_server_job(
267            move |compositor| {
268                if let Some(object) = compositor.get_animated_object(server) {
269                    if let Some(animations) = object.server_object().animations() {
270                        animations.stop_subchannel_animation(property, &components);
271                    }
272                }
273            },
274            false,
275        );
276    }
277}
278
279impl Drop for CompositionObject {
280    fn drop(&mut self) {
281        if let Some(server) = self.server {
282            if self.is_disposed.get() {
283                self.compositor.release_with_a_later_batch(server);
284            } else {
285                self.compositor.dispose_with_a_later_batch(server);
286            }
287        }
288    }
289}
290
291/// A composition object as code refers to any composition object: a
292/// reference parameter of an animation, an object stored in a property set
293/// (upstream `CompositionObject` as a parameter type).
294pub trait AsCompositionObject: 'static {
295    /// The embedded `CompositionObject`.
296    fn as_composition_object(&self) -> &CompositionObject;
297
298    /// The name of the class of the object, for messages.
299    fn composition_type_name(&self) -> &'static str;
300
301    /// The object as a property set, if it is one (the `is
302    /// CompositionPropertySet` test of upstream).
303    fn as_property_set(self: Rc<Self>) -> Option<Rc<CompositionPropertySet>> {
304        None
305    }
306}
307
308/// The animation members of `CompositionObject`, written against the class
309/// that embeds one.
310pub trait ICompositionObjectAnimations {
311    /// `StartAnimation(propertyName, animation, finalValue)` of the class:
312    /// returns `false` for a property the class does not know.
313    fn try_start_animation(
314        &self,
315        property_name: &str,
316        animation: &dyn ICompositionAnimation,
317        final_value: Option<ExpressionVariant>,
318    ) -> bool;
319
320    /// The composition property with the given name.
321    fn get_composition_property(&self, property_name: &str) -> Option<&'static CompositionProperty>;
322
323    /// The embedded `CompositionObject`.
324    fn composition_object(&self) -> &CompositionObject;
325
326    /// Connects an animation with the named property of the object and
327    /// starts the animation.
328    fn start_animation(&self, property_name: &str, animation: &dyn ICompositionAnimation) {
329        self.start_animation_with_final_value(property_name, animation, None)
330    }
331
332    /// See [`start_animation`](Self::start_animation). `property_name` may
333    /// name a sub-channel of a vector property (`Offset.X`, `Scale.XY`,
334    /// components X, Y, Z, W in any case): the animation then animates a
335    /// value of the sub-channel's type (a scalar for one component, a
336    /// `Vector2` for two, ...) and leaves the other components alone. Several
337    /// sub-channels of one property run together; an animation of the whole
338    /// property replaces them.
339    fn start_animation_with_final_value(
340        &self,
341        property_name: &str,
342        animation: &dyn ICompositionAnimation,
343        final_value: Option<ExpressionVariant>,
344    ) {
345        start_animation_core(self, property_name, animation, final_value, None)
346    }
347
348    /// `StartAnimation(propertyName, animation, animationController)`: starts
349    /// the animation under `controller`, which can pause, resume, scrub and
350    /// speed it up. `property_name` may name a sub-channel, as for
351    /// [`start_animation`](Self::start_animation).
352    fn start_animation_with_controller(
353        &self,
354        property_name: &str,
355        animation: &dyn ICompositionAnimation,
356        controller: &Rc<AnimationController>,
357    ) {
358        start_animation_core(self, property_name, animation, None, Some(controller))
359    }
360
361    /// The controller of the animation of the named property
362    /// (`TryGetAnimationController`): the controller it was started with, or
363    /// a new controller that takes the running animation over. `None` when no
364    /// animation was started on the property through this object, or it was
365    /// stopped.
366    //
367    // Deviation (winui-composition.md): the UI thread does not know when an
368    // animation ends on the server, so an animation that ended by itself
369    // still gets a controller, which controls nothing.
370    fn try_get_animation_controller(&self, property_name: &str) -> Option<Rc<AnimationController>> {
371        let object = self.composition_object();
372        if let Some(controller) = object.animation_controller_entry(property_name)? {
373            return Some(controller);
374        }
375        let property = match parse_subchannel(property_name) {
376            Some((property, _)) => property,
377            None => property_name,
378        };
379        let property = self.get_composition_property(property)?;
380        let controller = AnimationController::new(object.compositor());
381        object.attach_animation_controller(property_name, property, controller.clone());
382        Some(controller)
383    }
384
385    /// Disconnects an animation from the named property and stops it,
386    /// applying its stop behavior. `property_name` may name a sub-channel
387    /// (`Offset.X`): the animations of the sub-channels that share a
388    /// component with it are stopped.
389    fn stop_animation(&self, property_name: &str) {
390        self.composition_object().note_animation_stopped(property_name);
391        if let Some((property, components)) = parse_subchannel(property_name) {
392            let Some(property) = self.get_composition_property(property) else {
393                panic!("Unknown property {property_name}");
394            };
395            self.composition_object().stop_subchannel_animation(property, components);
396            return;
397        }
398        let Some(property) = self.get_composition_property(property_name) else {
399            panic!("Unknown property {property_name}");
400        };
401        self.composition_object().stop_animation(property);
402    }
403
404    /// Starts an animation group.
405    /// The StartAnimationGroup method on CompositionObject lets you start CompositionAnimationGroup.
406    /// All the animations in the group will be started at the same time on the object.
407    fn start_animation_group(&self, grp: &dyn ICompositionAnimationBase) {
408        if let Some(animation) = grp.as_composition_animation() {
409            let Some(target) = animation.target() else { panic!("Animation Target can't be null") };
410            self.start_animation(&target, animation);
411        } else if let Some(group) = grp.as_composition_animation_group() {
412            for a in group.animations() {
413                let Some(target) = a.target() else { panic!("Animation Target can't be null") };
414                self.start_animation(&target, &*a);
415            }
416        }
417    }
418
419    /// `StartAnimationGroupPart`.
420    fn start_animation_group_part(
421        &self,
422        animation: &dyn ICompositionAnimation,
423        target: &str,
424        final_value: ExpressionVariant,
425    ) -> bool {
426        let Some(animation_target) = animation.target() else { panic!("Animation Target can't be null") };
427        if animation_target == target {
428            self.start_animation_with_final_value(&animation_target, animation, Some(final_value));
429            true
430        } else {
431            self.start_animation(&animation_target, animation);
432            false
433        }
434    }
435
436    /// Starts an animation or an animation group triggered by the change
437    /// of property `target` to `final_value`. Returns whether one of the
438    /// animations targets that property.
439    fn start_animation_group_for(
440        &self,
441        grp: &dyn ICompositionAnimationBase,
442        target: &str,
443        final_value: ExpressionVariant,
444    ) -> bool {
445        if let Some(animation) = grp.as_composition_animation() {
446            return self.start_animation_group_part(animation, target, final_value);
447        }
448        if let Some(group) = grp.as_composition_animation_group() {
449            let mut matched = false;
450            for a in group.animations() {
451                if a.target().is_none() {
452                    panic!("Animation Target can't be null");
453                }
454                if self.start_animation_group_part(&*a, target, final_value) {
455                    matched = true;
456                }
457            }
458            return matched;
459        }
460
461        panic!("the animation is neither an animation nor a group of animations");
462    }
463
464    /// Stops an animation group.
465    fn stop_animation_group(&self, grp: &dyn ICompositionAnimationBase) {
466        if let Some(animation) = grp.as_composition_animation() {
467            let Some(target) = animation.target() else { panic!("Animation Target can't be null") };
468            self.stop_animation(&target);
469        } else if let Some(group) = grp.as_composition_animation_group() {
470            for a in group.animations() {
471                let Some(target) = a.target() else { panic!("Animation Target can't be null") };
472                self.stop_animation(&target);
473            }
474        }
475    }
476}
477
478/// `StartAnimation` with an optional final value (implicit animations) and an
479/// optional controller.
480fn start_animation_core<O: ICompositionObjectAnimations + ?Sized>(
481    this: &O,
482    property_name: &str,
483    animation: &dyn ICompositionAnimation,
484    final_value: Option<ExpressionVariant>,
485    controller: Option<&Rc<AnimationController>>,
486) {
487    if let Some((property, components)) = parse_subchannel(property_name) {
488        // The instance of the animation is wrapped in an animation of
489        // the whole property, which the start path of the property sends
490        // as usual.
491        let target = this.composition_object().required_server();
492        let channel_final_value = final_value.map(|value| extract_subchannel(value, &components));
493        let mut inner = animation.create_instance(target, channel_final_value);
494        if let Some(controller) = controller {
495            inner = controller.wrap(inner);
496        }
497        // An animation of the property started earlier in this frame
498        // would be replaced by the start path: the wrapper takes it over,
499        // so sub-channels started in one frame run together.
500        let previous = this
501            .get_composition_property(property)
502            .and_then(|p| this.composition_object().pending_animations().get_and_remove(p));
503        let wrapper = SubchannelAnimation::new(components, inner, previous);
504        if !this.try_start_animation(property, &wrapper, None) {
505            panic!("Unknown property {property_name}");
506        }
507    } else if let Some(controller) = controller {
508        // The instance is made here, wrapped so that it comes under the
509        // controller, and handed to the start path ready-made.
510        let Some(target) = this.composition_object().server() else { panic!("Unknown property {property_name}") };
511        let prepared = PreparedAnimation::new(controller.wrap(animation.create_instance(target, final_value)));
512        if !this.try_start_animation(property_name, &prepared, None) {
513            panic!("Unknown property {property_name}");
514        }
515    } else if !this.try_start_animation(property_name, animation, final_value) {
516        panic!("Unknown property {property_name}");
517    }
518    this.composition_object().note_animation_started(property_name, controller.cloned());
519}