Skip to main content

ferroui_base/visual/
composition_facades.rs

1//! The composition facades of WinUI's `UIElement` on [`Visual`] (`Rotation`, `RotationAxis`,
2//! `Scale`, `CenterPoint`, `TransformMatrix`; `Translation` is next to the other composition
3//! state in `visual.rs`) and their transitions (`OpacityTransition`, `RotationTransition`,
4//! `ScaleTransition`, `TranslationTransition`, with `ScalarTransition` and `Vector3Transition` of
5//! the `Microsoft.UI.Xaml` namespace).
6//!
7//! A facade is a plain property of the element, not a styled property, as in WinUI. Setting it
8//! hands the matching property of the element's composition visual to the facade, which writes it
9//! whenever the element is rendered again; while a facade was never set, the composition property
10//! is left to the application. The transitions are implicit animations of the element's
11//! composition visual (see `docs/porting/winui-composition.md`, section 3.10).
12
13use super::Visual;
14use crate::numerics::{Matrix4x4, Quaternion, Vector3};
15use crate::rendering::composition::animations::{ICompositionAnimationBase, ImplicitAnimationCollection};
16use crate::rendering::composition::{AsCompositionObject, CompositionVisual, Compositor};
17use crate::Matrix;
18use bitflags::bitflags;
19use std::rc::{Rc, Weak};
20use std::time::Duration;
21
22/// `Microsoft.UI.Xaml.ScalarTransition`: the animation of a change of a scalar facade of an
23/// element (`UIElement.OpacityTransition`, `UIElement.RotationTransition`).
24///
25/// Deviation (winui-composition.md): WinUI's transition is a class whose `Duration` can be
26/// changed after it was assigned to an element; here it is a value, copied when it is assigned,
27/// so a changed duration is assigned again.
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29pub struct ScalarTransition {
30    duration: Duration,
31}
32
33impl ScalarTransition {
34    /// The default duration of a transition: 300 milliseconds.
35    pub const DEFAULT_DURATION: Duration = Duration::from_millis(300);
36
37    /// A transition of the default duration (300 milliseconds).
38    pub const fn new() -> ScalarTransition {
39        ScalarTransition { duration: Self::DEFAULT_DURATION }
40    }
41
42    /// A transition of `duration`.
43    pub const fn with_duration(duration: Duration) -> ScalarTransition {
44        ScalarTransition { duration }
45    }
46
47    /// `Duration`: how long the animation of a change takes. 300 milliseconds by default.
48    pub fn duration(&self) -> Duration {
49        self.duration
50    }
51
52    /// Sets [`duration`](Self::duration).
53    pub fn set_duration(&mut self, value: Duration) {
54        self.duration = value;
55    }
56}
57
58impl Default for ScalarTransition {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64bitflags! {
65    /// `Microsoft.UI.Xaml.Vector3TransitionComponents`: the components of a `Vector3` facade that a
66    /// [`Vector3Transition`] animates.
67    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
68    pub struct Vector3TransitionComponents: u32 {
69        /// The X component.
70        const X = 1;
71        /// The Y component.
72        const Y = 2;
73        /// The Z component.
74        const Z = 4;
75    }
76}
77
78impl Default for Vector3TransitionComponents {
79    /// All three components, the default of `Vector3Transition.Components`.
80    fn default() -> Self {
81        Self::all()
82    }
83}
84
85/// `Microsoft.UI.Xaml.Vector3Transition`: the animation of a change of a `Vector3` facade of an
86/// element (`UIElement.ScaleTransition`, `UIElement.TranslationTransition`).
87///
88/// Deviation (winui-composition.md): a value, as [`ScalarTransition`].
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90pub struct Vector3Transition {
91    duration: Duration,
92    components: Vector3TransitionComponents,
93}
94
95impl Vector3Transition {
96    /// A transition of the default duration (300 milliseconds) animating every component.
97    pub const fn new() -> Vector3Transition {
98        Vector3Transition { duration: ScalarTransition::DEFAULT_DURATION, components: Vector3TransitionComponents::all() }
99    }
100
101    /// A transition of `duration` animating `components`.
102    pub const fn with(duration: Duration, components: Vector3TransitionComponents) -> Vector3Transition {
103        Vector3Transition { duration, components }
104    }
105
106    /// `Duration`: how long the animation of a change takes. 300 milliseconds by default.
107    pub fn duration(&self) -> Duration {
108        self.duration
109    }
110
111    /// Sets [`duration`](Self::duration).
112    pub fn set_duration(&mut self, value: Duration) {
113        self.duration = value;
114    }
115
116    /// `Components`: the components that animate; the others take their new value at once. All
117    /// three by default.
118    pub fn components(&self) -> Vector3TransitionComponents {
119        self.components
120    }
121
122    /// Sets [`components`](Self::components).
123    pub fn set_components(&mut self, value: Vector3TransitionComponents) {
124        self.components = value;
125    }
126}
127
128impl Default for Vector3Transition {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134/// The facades and transitions of an element, kept in the composition extras of its visual.
135/// `None` is a facade that was never set.
136#[derive(Default)]
137pub(super) struct CompositionFacades {
138    rotation: Option<f32>,
139    rotation_axis: Option<Vector3>,
140    scale: Option<Vector3>,
141    center_point: Option<Vector3>,
142    transform_matrix: Option<Matrix4x4>,
143    transitions: FacadeTransitions,
144    /// The implicit animations built from the transitions, on the compositor of the composition
145    /// visual they were built for; dropped when a transition changes.
146    transition_animations: Option<Rc<ImplicitAnimationCollection>>,
147    /// A composition visual created for the element and not synchronized yet: its first
148    /// synchronization writes the initial values before the implicit animations are installed.
149    fresh_visual: Option<Weak<CompositionVisual>>,
150}
151
152#[derive(Clone, Copy, Default)]
153struct FacadeTransitions {
154    opacity: Option<ScalarTransition>,
155    rotation: Option<ScalarTransition>,
156    scale: Option<Vector3Transition>,
157    translation: Option<Vector3Transition>,
158}
159
160impl FacadeTransitions {
161    fn is_empty(&self) -> bool {
162        self.opacity.is_none() && self.rotation.is_none() && self.scale.is_none() && self.translation.is_none()
163    }
164}
165
166impl Visual {
167    /// `UIElement.Rotation`: the rotation of the element in degrees, clockwise, around
168    /// [`rotation_axis`](Self::rotation_axis) through [`center_point`](Self::center_point). Zero
169    /// until set.
170    ///
171    /// Around the Z axis it is the `RotationAngle` of the composition visual (in radians);
172    /// around another axis it is the `Orientation` of the composition visual, a quaternion.
173    /// While neither the rotation nor its axis was set, both are left to the application.
174    pub fn rotation(&self) -> f32 {
175        self.read_composition_extras(|extras| extras.facades.rotation).flatten().unwrap_or(0.0)
176    }
177
178    /// Sets [`rotation`](Self::rotation).
179    pub fn set_rotation(&self, value: f32) {
180        self.with_composition_extras(|extras| extras.facades.rotation = Some(value));
181        self.invalidate_visual();
182    }
183
184    /// `UIElement.RotationAxis`: the axis [`rotation`](Self::rotation) turns the element around.
185    /// The Z axis `(0, 0, 1)` until set.
186    ///
187    /// Deviation (winui-composition.md): the renderers are 2D, so a rotation around another axis
188    /// shows only its projection on the plane of the element (no perspective). A zero axis is
189    /// taken as the Z axis.
190    pub fn rotation_axis(&self) -> Vector3 {
191        self.read_composition_extras(|extras| extras.facades.rotation_axis).flatten().unwrap_or(Vector3::UNIT_Z)
192    }
193
194    /// Sets [`rotation_axis`](Self::rotation_axis).
195    pub fn set_rotation_axis(&self, value: Vector3) {
196        self.with_composition_extras(|extras| extras.facades.rotation_axis = Some(value));
197        self.invalidate_visual();
198    }
199
200    /// `UIElement.Scale`: the scale of the element around [`center_point`](Self::center_point),
201    /// the `Scale` of its composition visual. `(1, 1, 1)` until set.
202    pub fn scale(&self) -> Vector3 {
203        self.read_composition_extras(|extras| extras.facades.scale).flatten().unwrap_or(Vector3::ONE)
204    }
205
206    /// Sets [`scale`](Self::scale).
207    pub fn set_scale(&self, value: Vector3) {
208        self.with_composition_extras(|extras| extras.facades.scale = Some(value));
209        self.invalidate_visual();
210    }
211
212    /// `UIElement.CenterPoint`: the point, in the coordinates of the element, that
213    /// [`scale`](Self::scale) and [`rotation`](Self::rotation) are around; the `CenterPoint` of
214    /// its composition visual. The origin until set.
215    pub fn center_point(&self) -> Vector3 {
216        self.read_composition_extras(|extras| extras.facades.center_point).flatten().unwrap_or(Vector3::ZERO)
217    }
218
219    /// Sets [`center_point`](Self::center_point).
220    pub fn set_center_point(&self, value: Vector3) {
221        self.with_composition_extras(|extras| extras.facades.center_point = Some(value));
222        self.invalidate_visual();
223    }
224
225    /// `UIElement.TransformMatrix`: a transform of the element in addition to the other facades.
226    /// The identity until set.
227    ///
228    /// The composition visual has a 2D `TransformMatrix`, which the renderer fills with the
229    /// render transform of the element; the 2D affine part of this matrix (`M11`, `M12`, `M21`,
230    /// `M22`, `M41`, `M42`) is applied after the render transform.
231    ///
232    /// Deviation (winui-composition.md): WinUI applies `TransformMatrix` after the other facade
233    /// transforms; the composition visual applies its `TransformMatrix` before `Scale`,
234    /// `RotationAngle`, `Orientation` and `Translation`, so here the matrix acts before those
235    /// facades. The 3D part of the matrix is ignored.
236    pub fn transform_matrix(&self) -> Matrix4x4 {
237        self.read_composition_extras(|extras| extras.facades.transform_matrix).flatten().unwrap_or(Matrix4x4::IDENTITY)
238    }
239
240    /// Sets [`transform_matrix`](Self::transform_matrix).
241    pub fn set_transform_matrix(&self, value: Matrix4x4) {
242        self.with_composition_extras(|extras| extras.facades.transform_matrix = Some(value));
243        self.invalidate_visual();
244    }
245
246    /// `UIElement.OpacityTransition`: animates the changes of the `Opacity` of the element.
247    /// `None` (no animation) by default.
248    pub fn opacity_transition(&self) -> Option<ScalarTransition> {
249        self.read_composition_extras(|extras| extras.facades.transitions.opacity).flatten()
250    }
251
252    /// Sets [`opacity_transition`](Self::opacity_transition).
253    pub fn set_opacity_transition(&self, value: Option<ScalarTransition>) {
254        self.with_composition_extras(|extras| extras.facades.transitions.opacity = value);
255        self.composition_transitions_changed();
256    }
257
258    /// `UIElement.RotationTransition`: animates the changes of [`rotation`](Self::rotation) (and
259    /// of [`rotation_axis`](Self::rotation_axis)). `None` by default.
260    pub fn rotation_transition(&self) -> Option<ScalarTransition> {
261        self.read_composition_extras(|extras| extras.facades.transitions.rotation).flatten()
262    }
263
264    /// Sets [`rotation_transition`](Self::rotation_transition).
265    pub fn set_rotation_transition(&self, value: Option<ScalarTransition>) {
266        self.with_composition_extras(|extras| extras.facades.transitions.rotation = value);
267        self.composition_transitions_changed();
268    }
269
270    /// `UIElement.ScaleTransition`: animates the changes of [`scale`](Self::scale). `None` by
271    /// default.
272    pub fn scale_transition(&self) -> Option<Vector3Transition> {
273        self.read_composition_extras(|extras| extras.facades.transitions.scale).flatten()
274    }
275
276    /// Sets [`scale_transition`](Self::scale_transition).
277    pub fn set_scale_transition(&self, value: Option<Vector3Transition>) {
278        self.with_composition_extras(|extras| extras.facades.transitions.scale = value);
279        self.composition_transitions_changed();
280    }
281
282    /// `UIElement.TranslationTransition`: animates the changes of
283    /// [`translation`](Self::translation). `None` by default.
284    pub fn translation_transition(&self) -> Option<Vector3Transition> {
285        self.read_composition_extras(|extras| extras.facades.transitions.translation).flatten()
286    }
287
288    /// Sets [`translation_transition`](Self::translation_transition).
289    pub fn set_translation_transition(&self, value: Option<Vector3Transition>) {
290        self.with_composition_extras(|extras| extras.facades.transitions.translation = value);
291        self.composition_transitions_changed();
292    }
293
294    /// The `TransformMatrix` facade as the 2D matrix of the composition visual, while it is set.
295    pub(super) fn composition_facade_transform_matrix(&self) -> Option<Matrix> {
296        let matrix = self.read_composition_extras(|extras| extras.facades.transform_matrix).flatten()?;
297        Some(Matrix::new(
298            matrix.m11 as f64,
299            matrix.m12 as f64,
300            matrix.m21 as f64,
301            matrix.m22 as f64,
302            matrix.m41 as f64,
303            matrix.m42 as f64,
304        ))
305    }
306
307    /// Writes the facades that were set to the composition visual of the element (the
308    /// `TransformMatrix` facade is combined with the render transform by the caller).
309    pub(super) fn synchronize_composition_facades(&self, comp: &Rc<CompositionVisual>) {
310        let Some((rotation, rotation_axis, scale, center_point)) = self.read_composition_extras(|extras| {
311            let facades = &extras.facades;
312            (facades.rotation, facades.rotation_axis, facades.scale, facades.center_point)
313        }) else {
314            return;
315        };
316
317        if rotation.is_some() || rotation_axis.is_some() {
318            let (angle, orientation) =
319                rotation_to_composition(rotation.unwrap_or(0.0), rotation_axis.unwrap_or(Vector3::UNIT_Z));
320            comp.set_rotation_angle(angle);
321            comp.set_orientation(orientation);
322        }
323        if let Some(scale) = scale {
324            comp.set_scale(scale.into());
325        }
326        if let Some(center_point) = center_point {
327            comp.set_center_point(center_point.into());
328        }
329    }
330
331    /// Records that `comp` was just created for the element, so that its first synchronization
332    /// writes the initial values without animating them. Nothing is recorded for an element that
333    /// uses no composition facade.
334    pub(super) fn composition_visual_created(&self, comp: &Rc<CompositionVisual>) {
335        if self.read_composition_extras(|_| ()).is_some() {
336            self.with_composition_extras(|extras| extras.facades.fresh_visual = Some(Rc::downgrade(comp)));
337        }
338    }
339
340    /// Installs the implicit animations of the transitions on the composition visual of the
341    /// element, before the properties are written (`after_writes` false) so that a change made
342    /// with a transition animates, and again after them (`after_writes` true). On the first
343    /// synchronization of a composition visual created while the element had facades, only after
344    /// them, so that the initial values are written without animating.
345    ///
346    /// Deviation (winui-composition.md): the collection replaces implicit animations the
347    /// application set on the element's composition visual while a transition is set (WinUI does
348    /// not allow a facade transition and an animation of the handoff visual's property together).
349    pub(super) fn apply_composition_transitions(&self, comp: &Rc<CompositionVisual>, after_writes: bool) {
350        let Some(fresh) = self.read_composition_extras(|extras| {
351            extras.facades.fresh_visual.as_ref().is_some_and(|visual| std::ptr::eq(visual.as_ptr(), Rc::as_ptr(comp)))
352        }) else {
353            return;
354        };
355        if fresh {
356            if !after_writes {
357                return;
358            }
359            self.with_composition_extras(|extras| extras.facades.fresh_visual = None);
360        }
361
362        if let Some(animations) = self.composition_transition_animations(comp.compositor()) {
363            if !comp.implicit_animations().is_some_and(|current| Rc::ptr_eq(&current, &animations)) {
364                comp.set_implicit_animations(Some(animations));
365            }
366        }
367    }
368
369    /// The implicit animations of the transitions on `compositor`, built on first use; `None`
370    /// while no transition is set.
371    fn composition_transition_animations(&self, compositor: &Rc<Compositor>) -> Option<Rc<ImplicitAnimationCollection>> {
372        let (transitions, cached) = self.read_composition_extras(|extras| {
373            (extras.facades.transitions, extras.facades.transition_animations.clone())
374        })?;
375        if transitions.is_empty() {
376            return None;
377        }
378        if let Some(cached) = cached {
379            if Rc::ptr_eq(cached.as_composition_object().compositor(), compositor) {
380                return Some(cached);
381            }
382        }
383        let built = build_transition_animations(compositor, &transitions);
384        self.with_composition_extras(|extras| extras.facades.transition_animations = Some(built.clone()));
385        Some(built)
386    }
387
388    /// Drops the implicit animations built from the previous transitions (and takes them off the
389    /// composition visual if they are still there); the next synchronization installs the new
390    /// ones before it writes the properties, so a change made with the transition animates.
391    fn composition_transitions_changed(&self) {
392        let old = self.with_composition_extras(|extras| extras.facades.transition_animations.take());
393        if let (Some(old), Some(comp)) = (old, self.composition_visual()) {
394            if comp.implicit_animations().is_some_and(|current| Rc::ptr_eq(&current, &old)) {
395                comp.set_implicit_animations(None);
396            }
397        }
398        self.invalidate_visual();
399    }
400}
401
402/// The `RotationAngle` (radians) and `Orientation` of a composition visual for the `Rotation`
403/// (degrees) and `RotationAxis` facades: around the Z axis (or a zero axis) the angle and the
404/// identity, around another axis no angle and the quaternion of the rotation.
405fn rotation_to_composition(degrees: f32, axis: Vector3) -> (f32, Quaternion) {
406    let radians = degrees.to_radians();
407    if axis.x == 0.0 && axis.y == 0.0 {
408        let angle = if axis.z < 0.0 { -radians } else { radians };
409        (angle, Quaternion::IDENTITY)
410    } else {
411        (0.0, Quaternion::create_from_axis_angle(Vector3::normalize(axis), radians))
412    }
413}
414
415/// The duration of a transition as a key frame animation accepts it (1 ms to 24 days).
416fn key_frame_duration(duration: Duration) -> Duration {
417    duration.clamp(Duration::from_millis(1), Duration::from_secs(24 * 60 * 60))
418}
419
420/// The implicit animations of `transitions`: per property of the composition visual a key frame
421/// animation from the value the property had to the new value (`this.FinalValue`) over the
422/// duration of the transition, with the default easing of the compositor.
423fn build_transition_animations(compositor: &Rc<Compositor>, transitions: &FacadeTransitions) -> Rc<ImplicitAnimationCollection> {
424    let collection = compositor.create_implicit_animation_collection();
425
426    if let Some(transition) = transitions.opacity {
427        collection.set("Opacity", scalar_animation(compositor, "Opacity", transition.duration()));
428    }
429    if let Some(transition) = transitions.rotation {
430        collection.set("RotationAngle", scalar_animation(compositor, "RotationAngle", transition.duration()));
431        let orientation = compositor.create_quaternion_key_frame_animation();
432        orientation.set_target(Some("Orientation".to_owned()));
433        orientation.insert_expression_key_frame(1.0, "this.FinalValue", None);
434        orientation.set_duration(key_frame_duration(transition.duration()));
435        let orientation: Rc<dyn ICompositionAnimationBase> = orientation;
436        collection.set("Orientation", orientation);
437    }
438    if let Some(transition) = transitions.scale {
439        if let Some(animation) = vector3_animation(compositor, "Scale", transition) {
440            collection.set("Scale", animation);
441        }
442    }
443    if let Some(transition) = transitions.translation {
444        if let Some(animation) = vector3_animation(compositor, "Translation", transition) {
445            collection.set("Translation", animation);
446        }
447    }
448    collection
449}
450
451fn scalar_animation(compositor: &Rc<Compositor>, target: &str, duration: Duration) -> Rc<dyn ICompositionAnimationBase> {
452    let animation = compositor.create_scalar_key_frame_animation();
453    animation.set_target(Some(target.to_owned()));
454    animation.insert_expression_key_frame(1.0, "this.FinalValue", None);
455    animation.set_duration(key_frame_duration(duration));
456    animation as Rc<dyn ICompositionAnimationBase>
457}
458
459/// The animation of a `Vector3Transition`; `None` when it animates no component (the property
460/// then takes its new value at once).
461///
462/// With only some components, a key frame at the start sets the other components to their final
463/// value (`this.FinalValue`) and keeps the animated ones at their starting value
464/// (`this.StartingValue`); the frame at the end is the final value. The other components therefore
465/// jump at the start of the animation.
466fn vector3_animation(compositor: &Rc<Compositor>, target: &str, transition: Vector3Transition) -> Option<Rc<dyn ICompositionAnimationBase>> {
467    let components = transition.components();
468    if components.is_empty() {
469        return None;
470    }
471    let animation = compositor.create_vector3d_key_frame_animation();
472    animation.set_target(Some(target.to_owned()));
473    if components != Vector3TransitionComponents::all() {
474        animation.insert_expression_key_frame(0.0, &partial_start_expression(components), None);
475    }
476    animation.insert_expression_key_frame(1.0, "this.FinalValue", None);
477    animation.set_duration(key_frame_duration(transition.duration()));
478    let animation: Rc<dyn ICompositionAnimationBase> = animation;
479    Some(animation)
480}
481
482/// The start frame of a transition of some components: the animated components at their starting
483/// value, the others at their final value.
484fn partial_start_expression(components: Vector3TransitionComponents) -> String {
485    let component = |flag: Vector3TransitionComponents, name: &str| {
486        if components.contains(flag) {
487            format!("this.StartingValue.{name}")
488        } else {
489            format!("this.FinalValue.{name}")
490        }
491    };
492    format!(
493        "Vector3({}, {}, {})",
494        component(Vector3TransitionComponents::X, "X"),
495        component(Vector3TransitionComponents::Y, "Y"),
496        component(Vector3TransitionComponents::Z, "Z"),
497    )
498}
499
500#[cfg(test)]
501pub(super) fn partial_start_expression_for_tests(components: Vector3TransitionComponents) -> String {
502    partial_start_expression(components)
503}