Skip to main content

ferroui_base/visual/
theme_shadow.rs

1//! The `Shadow` of an element (WinUI `UIElement.Shadow`, phase 7 of
2//! docs/porting/winui-composition.md): the element casts the theme shadow of
3//! its elevation, drawn by a [`ThemeShadowVisual`] that the parent of the
4//! element shows just before the element's own composition visual.
5//!
6//! Ported from microsoft-ui-xaml (`HWCompNodeWinRT.cpp`: `NeedsDropShadowVisual`,
7//! `UpdateDropShadowVisual`, `UpdateDropShadowRecipeCornerRadius`,
8//! `GetThemeShadowFromDropShadowCaster`; `uielement.cpp`: the `Shadow` of a
9//! popup delegated to its child).
10
11use super::{ThemeShadowState, Visual};
12use crate::media::{DropShadowRecipe, ThemeShadow, ThemeShadowTheme};
13use crate::rendering::composition::{CompositionVisual, ThemeShadowVisual};
14use crate::{CornerRadius, FerroPropertyRegistry, Ref, Vector};
15use std::rc::Rc;
16
17impl Visual {
18    /// `UIElement.Shadow`: the shadow the element casts, a
19    /// [`ThemeShadow`](crate::media::ThemeShadow) (the one shadow there is).
20    /// The shadow of a popup is cast by the child of the popup.
21    pub fn shadow(&self) -> Option<Ref<crate::media::Shadow>> {
22        self.get_value(Self::shadow_property())
23    }
24
25    pub fn set_shadow(&self, value: Option<Ref<crate::media::Shadow>>) {
26        self.set_value(Self::shadow_property(), value)
27    }
28
29    /// The theme shadow the visual casts and the element whose elevation (the
30    /// Z of its `Translation`) the shadow has: its own `ThemeShadow`, or, for
31    /// the child of a popup with a theme shadow, the popup's (with the
32    /// popup's elevation, which wins over the child's own shadow). `None` for
33    /// a visual that delegates its shadow to its child (a popup) and for one
34    /// without a theme shadow.
35    pub fn theme_shadow_caster(&self) -> Option<(Ref<ThemeShadow>, Ref<Visual>)> {
36        let (popup, delegates) = self
37            .read_composition_extras(|extras| {
38                extras.theme_shadow.as_ref().map(|state| (state.popup.clone(), state.delegates_to_child))
39            })
40            .flatten()?;
41        if delegates {
42            return None;
43        }
44        if let Some(popup) = popup.and_then(|popup| popup.upgrade()) {
45            if let Some(shadow) = popup.shadow().and_then(|shadow| shadow.downcast::<ThemeShadow>().ok()) {
46                return Some((shadow, popup));
47            }
48        }
49        let shadow = self.shadow()?.downcast::<ThemeShadow>().ok()?;
50        Some((shadow, self.to_ref()))
51    }
52
53    /// Whether the visual casts a theme shadow, or may (a cheap test of the
54    /// composition state, without the property values).
55    pub(super) fn may_cast_theme_shadow(&self) -> bool {
56        self.read_composition_extras(|extras| extras.theme_shadow.is_some()).unwrap_or(false)
57    }
58
59    /// The visual that draws the theme shadow of this visual, while there is
60    /// one.
61    pub fn theme_shadow_visual(&self) -> Option<Rc<ThemeShadowVisual>> {
62        self.read_composition_extras(|extras| extras.theme_shadow.as_ref().and_then(|state| state.visual.clone()))
63            .flatten()
64    }
65
66    /// For a popup (WinUI's `Popup`, whose `Shadow` is cast by its child):
67    /// whether the shadow of this visual is cast by its child rather than by
68    /// itself.
69    pub fn set_theme_shadow_delegated_to_child(&self, value: bool) {
70        self.with_composition_extras(|extras| {
71            extras.theme_shadow.get_or_insert_with(ThemeShadowState::default).delegates_to_child = value;
72        });
73        self.theme_shadow_changed();
74    }
75
76    /// For the child of a popup: the popup whose `Shadow` the child casts as
77    /// its own (`NeedsDropShadowVisual`: "Child of Popup with Shadow is
78    /// implicitly a shadow caster").
79    pub fn set_theme_shadow_popup(&self, popup: Option<&Visual>) {
80        self.with_composition_extras(|extras| {
81            extras.theme_shadow.get_or_insert_with(ThemeShadowState::default).popup =
82                popup.map(|popup| popup.to_ref().downgrade());
83        });
84        self.theme_shadow_changed();
85    }
86
87    /// The `Shadow` of the visual changed: the shadow learns its new owner,
88    /// the visual is drawn again and its parent shows or drops the shadow
89    /// visual.
90    pub(super) fn on_shadow_changed(
91        &self,
92        old: Option<&Ref<crate::media::Shadow>>,
93        new: Option<&Ref<crate::media::Shadow>>,
94    ) {
95        if let Some(old) = old.and_then(|old| old.as_theme_shadow()) {
96            old.remove_owner(self);
97        }
98        if let Some(new) = new.and_then(|new| new.as_theme_shadow()) {
99            new.add_owner(self);
100        }
101        if new.is_some() {
102            self.with_composition_extras(|extras| {
103                extras.theme_shadow.get_or_insert_with(ThemeShadowState::default);
104            });
105        }
106        self.theme_shadow_changed();
107    }
108
109    /// Something that decides whether or how the visual casts a theme shadow
110    /// changed (outside of a render pass): the shadow visual of a visual that
111    /// casts none any more is released, and the visual and its parent are
112    /// synchronized again.
113    fn theme_shadow_changed(&self) {
114        if self.theme_shadow_caster().is_none() {
115            self.release_theme_shadow_visual();
116        }
117        self.invalidate_visual();
118        if let (Some(parent), Some(source)) = (self.visual_parent(), self.presentation_source()) {
119            source.renderer().recalculate_children(&parent);
120        }
121    }
122
123    /// The composition visual that draws the theme shadow of this visual,
124    /// created when the visual casts one and has a composition visual, and
125    /// brought up to date: what the parent of the visual shows just before
126    /// the visual's own composition visual.
127    pub(super) fn ensure_theme_shadow_composition_visual(&self) -> Option<Rc<CompositionVisual>> {
128        let (shadow, elevation) = self.theme_shadow_caster()?;
129        let caster = self.composition_visual()?;
130        let existing = self.theme_shadow_visual().filter(|existing| {
131            Rc::ptr_eq(existing.visual().compositor(), caster.compositor()) && !existing.visual().is_disposed()
132        });
133        let shadow_visual = match existing {
134            Some(existing) => existing,
135            None => {
136                self.release_theme_shadow_visual();
137                let created = Rc::new(ThemeShadowVisual::new(&caster, &self.theme_shadow_recipe(&elevation)));
138                self.with_composition_extras(|extras| {
139                    extras.theme_shadow.get_or_insert_with(ThemeShadowState::default).visual = Some(created.clone());
140                });
141                created
142            }
143        };
144        self.configure_theme_shadow_visual(&shadow_visual, &shadow, &elevation);
145        Some(shadow_visual.visual().clone())
146    }
147
148    /// Brings the shadow visual of the visual up to date, when it has one
149    /// (part of the synchronization of the composition properties).
150    pub(super) fn update_theme_shadow_visual(&self) {
151        let Some(shadow_visual) = self.theme_shadow_visual() else { return };
152        match self.theme_shadow_caster() {
153            Some((shadow, elevation)) => self.configure_theme_shadow_visual(&shadow_visual, &shadow, &elevation),
154            None => shadow_visual.update(&DropShadowRecipe::new(0.0, ThemeShadowTheme::Light), Vector::default(), false),
155        }
156    }
157
158    /// Releases the shadow visual (the visual casts no shadow any more, or
159    /// leaves its compositor).
160    pub(super) fn release_theme_shadow_visual(&self) {
161        let released = self
162            .with_composition_extras(|extras| extras.theme_shadow.as_mut().and_then(|state| state.visual.take()));
163        if let Some(released) = released {
164            released.dispose();
165        }
166    }
167
168    /// `UpdateDropShadowVisual`: the recipe of the elevation of `elevation`
169    /// (the Z of its translation) in the theme of this visual, with the
170    /// corner radius of the nearest rounded element, over the size of this
171    /// visual; hidden while this visual is hidden and in a high contrast
172    /// theme.
173    fn configure_theme_shadow_visual(
174        &self,
175        shadow_visual: &ThemeShadowVisual,
176        _shadow: &Ref<ThemeShadow>,
177        elevation: &Ref<Visual>,
178    ) {
179        let theme = ThemeShadowTheme::of(self);
180        let recipe = self.theme_shadow_recipe(elevation);
181        let size = self.bounds().size();
182        let visible = theme != ThemeShadowTheme::HighContrast
183            && self.composition_visual().is_some_and(|visual| visual.visible());
184        shadow_visual.update(&recipe, Vector::new(size.width, size.height), visible);
185    }
186
187    /// The recipe of the elevation of `elevation` in the theme of this visual, with the corner
188    /// radius of the nearest rounded element (`GetDropShadowRecipe`, `UpdateDropShadowRecipeCornerRadius`).
189    fn theme_shadow_recipe(&self, elevation: &Ref<Visual>) -> DropShadowRecipe {
190        let (radius_x, radius_y) = self.theme_shadow_corner_radius();
191        DropShadowRecipe::new(elevation.translation().z as f32, ThemeShadowTheme::of(self))
192            .with_corner_radius(radius_x, radius_y)
193    }
194
195    /// `UpdateDropShadowRecipeCornerRadius`: from the visual up through its
196    /// ancestors, the first element that is rounded: the radii of a
197    /// rectangle (`RadiusX`, `RadiusY`) or the largest corner of a
198    /// `CornerRadius` (a control, a border, a panel).
199    fn theme_shadow_corner_radius(&self) -> (f64, f64) {
200        let registry = FerroPropertyRegistry::instance();
201        let mut current = Some(self.to_ref());
202        while let Some(element) = current {
203            let object: &crate::FerroObject = &element;
204            if let (Some(radius_x), Some(radius_y)) =
205                (registry.find_registered_for(object, "RadiusX"), registry.find_registered_for(object, "RadiusY"))
206            {
207                let x = object.get_value_untyped(radius_x).downcast_ref::<f64>().copied().unwrap_or(0.0);
208                let y = object.get_value_untyped(radius_y).downcast_ref::<f64>().copied().unwrap_or(0.0);
209                if x != 0.0 || y != 0.0 {
210                    return (x, y);
211                }
212            } else if let Some(property) = registry.find_registered_for(object, "CornerRadius") {
213                if let Some(radius) = object.get_value_untyped(property).downcast_ref::<CornerRadius>() {
214                    let largest =
215                        radius.top_left.max(radius.top_right).max(radius.bottom_left.max(radius.bottom_right));
216                    if largest != 0.0 {
217                        return (largest, largest);
218                    }
219                }
220            }
221            current = element.visual_parent();
222        }
223        (0.0, 0.0)
224    }
225}