Skip to main content

ferroui_base/media/
theme_shadow.rs

1//! `Microsoft.UI.Xaml.Media.Shadow` and `ThemeShadow` of WinUI (phase 7 of
2//! docs/porting/winui-composition.md, not from upstream), ported from
3//! microsoft-ui-xaml: `dxaml/xcp/core/inc/ThemeShadow.h`,
4//! `dxaml/xcp/components/graphics/ThemeShadow.cpp`,
5//! `dxaml/xcp/components/Collection/UIElementWeakCollection.cpp` and
6//! `dxaml/xcp/components/graphics/inc/DropShadowRecipe.h`.
7//!
8//! An element casts the shadow of the `Shadow` property of
9//! [`Visual`](crate::Visual); how it is drawn is in
10//! `rendering::composition::theme_shadow_visual`.
11
12use crate::media::{BoxShadow, Color};
13use crate::rendering::composition::CompositionBrush;
14use crate::styling::ThemeVariant;
15use crate::{ferro_class, ferro_impl_classes, instantiate, FerroObject, FerroObjectImpl, Ref, Thickness, Visual, WeakRef};
16use std::cell::RefCell;
17use std::fmt;
18use std::rc::Rc;
19
20/// The base class of the shadows an element can cast (WinUI
21/// `Microsoft.UI.Xaml.Media.Shadow`). [`ThemeShadow`] is the one shadow.
22#[repr(C)]
23pub struct Shadow {
24    base: FerroObject,
25}
26
27ferro_class!(Shadow: FerroObject);
28ferro_impl_classes!(Shadow: FerroObjectImpl);
29
30impl Shadow {
31    /// Creates the class data.
32    pub fn construct() -> Self {
33        Self { base: FerroObject::construct() }
34    }
35
36    /// The shadow as a theme shadow, if it is one.
37    pub fn as_theme_shadow(&self) -> Option<&ThemeShadow> {
38        self.downcast_ref::<ThemeShadow>()
39    }
40}
41
42/// The shadow of the theme (WinUI `Microsoft.UI.Xaml.Media.ThemeShadow`): set
43/// as the `Shadow` of an element, the element casts the shadow of its
44/// elevation, the Z of its `Translation`, as the controls of WinUI do under
45/// their popups (32 for a flyout, 16 for a tool tip, 128 for a content
46/// dialog).
47///
48/// WinUI 3 draws every theme shadow as a drop shadow
49/// ([`is_drop_shadow_mode`](Self::is_drop_shadow_mode)): an ambient and a
50/// directional shadow around the element, the darker in the dark theme,
51/// none in a high contrast theme. A `ThemeShadow` can be shared by several
52/// elements; it has no appearance of its own.
53///
54/// [`receivers`](Self::receivers) are the elements the projected shadows of
55/// WinUI (the root and the open popups by default) would fall on. WinUI 3
56/// keeps the collection and its validation but does not draw projected
57/// shadows, so the receivers do not change the drawing here either.
58#[repr(C)]
59pub struct ThemeShadow {
60    base: Shadow,
61    receivers: UIElementWeakCollection,
62    /// The mask of the shadow (WinUI's internal `ThemeShadow.Mask`, used by
63    /// the projected shadows).
64    mask: RefCell<Option<Rc<CompositionBrush>>>,
65}
66
67ferro_class!(ThemeShadow: Shadow);
68crate::ferro_class_info!(ThemeShadow { new: ThemeShadow::new });
69ferro_impl_classes!(ThemeShadow: FerroObjectImpl);
70
71impl ThemeShadow {
72    /// Creates the class data.
73    pub fn construct() -> Self {
74        Self {
75            base: Shadow::construct(),
76            receivers: UIElementWeakCollection::new(true),
77            mask: RefCell::new(None),
78        }
79    }
80
81    pub fn new() -> Ref<Self> {
82        instantiate(Self::construct())
83    }
84
85    /// `ThemeShadow.IsDropShadowMode`: whether theme shadows are drawn as drop
86    /// shadows around their casters (`true` in WinUI 3, whose projected
87    /// shadows are disabled: `CThemeShadow::IsDropShadowMode`).
88    pub fn is_drop_shadow_mode() -> bool {
89        true
90    }
91
92    /// `ThemeShadow.Receivers`: the elements the shadow is cast on in the
93    /// projected mode. An ancestor of an element that casts the shadow
94    /// cannot be a receiver.
95    pub fn receivers(&self) -> &UIElementWeakCollection {
96        &self.receivers
97    }
98
99    /// Whether the receivers were set (WinUI's `HasCustomReceivers`): the
100    /// projected mode casts the shadow on the root and the popups otherwise.
101    pub fn has_custom_receivers(&self) -> bool {
102        self.receivers.size() > 0
103    }
104
105    /// The mask of the shadow (internal in WinUI, set by the connected
106    /// animations for the projected shadows).
107    pub fn mask(&self) -> Option<Rc<CompositionBrush>> {
108        self.mask.borrow().clone()
109    }
110
111    pub fn set_mask(&self, value: Option<Rc<CompositionBrush>>) {
112        *self.mask.borrow_mut() = value;
113    }
114
115    /// Called by [`Visual`] when the shadow becomes or stops being its
116    /// `Shadow`: the elements that use the shadow, for the validation of
117    /// the receivers.
118    pub(crate) fn add_owner(&self, owner: &Visual) {
119        self.receivers.owners.borrow_mut().push(owner.to_ref().downgrade());
120    }
121
122    pub(crate) fn remove_owner(&self, owner: &Visual) {
123        self.receivers.owners.borrow_mut().retain(|o| o.upgrade().is_some_and(|o| !std::ptr::eq(&*o, owner)));
124    }
125}
126
127/// The error of a change of the receivers of a [`ThemeShadow`].
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub enum ThemeShadowError {
130    /// `ERROR_ANCESTOR_ELEMENT_CANNOT_BE_SHADOW_RECEIVER`: the element is an
131    /// ancestor of an element that casts the shadow.
132    AncestorElementCannotBeShadowReceiver,
133    /// An index past the end of the collection.
134    IndexOutOfRange,
135}
136
137impl fmt::Display for ThemeShadowError {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        match self {
140            ThemeShadowError::AncestorElementCannotBeShadowReceiver => {
141                f.write_str("an ancestor of an element that casts the shadow cannot receive it")
142            }
143            ThemeShadowError::IndexOutOfRange => f.write_str("the index is out of range"),
144        }
145    }
146}
147
148impl std::error::Error for ThemeShadowError {}
149
150/// A collection of elements held weakly (WinUI
151/// `Microsoft.UI.Xaml.UIElementWeakCollection`, `IVector<UIElement>`): an
152/// element that is gone reads as `None` and stays in the collection until it
153/// is removed.
154pub struct UIElementWeakCollection {
155    items: RefCell<Vec<WeakRef<Visual>>>,
156    /// Whether ancestors of the owners are refused (the receivers of a theme
157    /// shadow).
158    disallow_ancestors: bool,
159    /// The elements that use the owner of the collection.
160    owners: RefCell<Vec<WeakRef<Visual>>>,
161}
162
163impl UIElementWeakCollection {
164    fn new(disallow_ancestors: bool) -> Self {
165        Self { items: RefCell::new(Vec::new()), disallow_ancestors, owners: RefCell::new(Vec::new()) }
166    }
167
168    /// `Size`.
169    pub fn size(&self) -> usize {
170        self.items.borrow().len()
171    }
172
173    /// `GetAt`: the element at `index`, `None` when it is gone.
174    pub fn get_at(&self, index: usize) -> Option<Ref<Visual>> {
175        self.items.borrow().get(index).and_then(WeakRef::upgrade)
176    }
177
178    /// `IndexOf`.
179    pub fn index_of(&self, element: &Visual) -> Option<usize> {
180        self.items.borrow().iter().position(|item| item.upgrade().is_some_and(|item| std::ptr::eq(&*item, element)))
181    }
182
183    /// Whether `element` is in the collection.
184    pub fn contains(&self, element: &Visual) -> bool {
185        self.index_of(element).is_some()
186    }
187
188    /// `Append`.
189    pub fn append(&self, element: &Visual) -> Result<(), ThemeShadowError> {
190        self.validate(element)?;
191        self.items.borrow_mut().push(element.to_ref().downgrade());
192        Ok(())
193    }
194
195    /// `InsertAt`.
196    pub fn insert_at(&self, index: usize, element: &Visual) -> Result<(), ThemeShadowError> {
197        if index > self.size() {
198            return Err(ThemeShadowError::IndexOutOfRange);
199        }
200        self.validate(element)?;
201        self.items.borrow_mut().insert(index, element.to_ref().downgrade());
202        Ok(())
203    }
204
205    /// `SetAt`.
206    pub fn set_at(&self, index: usize, element: &Visual) -> Result<(), ThemeShadowError> {
207        if index >= self.size() {
208            return Err(ThemeShadowError::IndexOutOfRange);
209        }
210        self.validate(element)?;
211        self.items.borrow_mut()[index] = element.to_ref().downgrade();
212        Ok(())
213    }
214
215    /// `RemoveAt`.
216    pub fn remove_at(&self, index: usize) -> Result<(), ThemeShadowError> {
217        if index >= self.size() {
218            return Err(ThemeShadowError::IndexOutOfRange);
219        }
220        self.items.borrow_mut().remove(index);
221        Ok(())
222    }
223
224    /// Removes `element`; `false` when it is not in the collection.
225    pub fn remove(&self, element: &Visual) -> bool {
226        match self.index_of(element) {
227            Some(index) => {
228                self.items.borrow_mut().remove(index);
229                true
230            }
231            None => false,
232        }
233    }
234
235    /// `RemoveAtEnd`.
236    pub fn remove_at_end(&self) -> Result<(), ThemeShadowError> {
237        match self.items.borrow_mut().pop() {
238            Some(_) => Ok(()),
239            None => Err(ThemeShadowError::IndexOutOfRange),
240        }
241    }
242
243    /// `Clear`.
244    pub fn clear(&self) {
245        self.items.borrow_mut().clear();
246    }
247
248    /// `GetMany` from the start: the elements that are still alive.
249    pub fn items(&self) -> Vec<Ref<Visual>> {
250        self.items.borrow().iter().filter_map(WeakRef::upgrade).collect()
251    }
252
253    /// `ReplaceAll`.
254    pub fn replace_all(&self, elements: &[&Visual]) -> Result<(), ThemeShadowError> {
255        for element in elements {
256            self.validate(element)?;
257        }
258        *self.items.borrow_mut() = elements.iter().map(|element| element.to_ref().downgrade()).collect();
259        Ok(())
260    }
261
262    /// `ValidateItem`: an element that is an ancestor of an element that
263    /// uses the owner of the collection is refused (`CheckForAncestor`,
264    /// which follows the parent of an element and the popup of a popup's
265    /// child).
266    fn validate(&self, element: &Visual) -> Result<(), ThemeShadowError> {
267        if !self.disallow_ancestors {
268            return Ok(());
269        }
270        for owner in self.owners.borrow().iter().filter_map(WeakRef::upgrade) {
271            let mut current = Some(owner);
272            while let Some(ancestor) = current {
273                if std::ptr::eq(&*ancestor, element) {
274                    return Err(ThemeShadowError::AncestorElementCannotBeShadowReceiver);
275                }
276                current = parent_following_popups(&ancestor);
277            }
278        }
279        Ok(())
280    }
281}
282
283/// The parent of an element, or the logical parent of the root of a popup
284/// (WinUI's `GetParentFollowPopups`).
285fn parent_following_popups(element: &Visual) -> Option<Ref<Visual>> {
286    if let Some(parent) = element.visual_parent() {
287        return Some(parent);
288    }
289    element.parent().and_then(|parent| parent.downcast::<Visual>().ok())
290}
291
292/// The theme a theme shadow is drawn for (WinUI's `ElementTheme` as
293/// `GetDropShadowRecipe` takes it).
294#[derive(Clone, Copy, Debug, PartialEq, Eq)]
295pub enum ThemeShadowTheme {
296    Light,
297    Dark,
298    /// A high contrast theme: no shadow (`NeedsDropShadowVisual`).
299    HighContrast,
300}
301
302impl ThemeShadowTheme {
303    /// The theme of the theme variant in effect for an element: a variant
304    /// that is or inherits `HighContrast` (the high contrast variant of the
305    /// WinUI theme) is high contrast, one that is or inherits `Dark` is
306    /// dark, anything else light.
307    pub fn of_variant(variant: Option<&ThemeVariant>) -> ThemeShadowTheme {
308        let mut current = variant.cloned();
309        let mut dark = false;
310        while let Some(variant) = current {
311            if variant.key().as_str() == Some("HighContrast") {
312                return ThemeShadowTheme::HighContrast;
313            }
314            if variant == ThemeVariant::dark() {
315                dark = true;
316            }
317            current = variant.inherit_variant();
318        }
319        if dark {
320            ThemeShadowTheme::Dark
321        } else {
322            ThemeShadowTheme::Light
323        }
324    }
325
326    /// The theme of `element`.
327    pub fn of(element: &Visual) -> ThemeShadowTheme {
328        Self::of_variant(element.actual_theme_variant().as_ref())
329    }
330}
331
332/// The values of the drop shadow of a theme shadow (WinUI's
333/// `DropShadowRecipe`, `GetDropShadowRecipe`): an ambient and a directional
334/// shadow of the elevation of an element.
335#[derive(Clone, Copy, Debug, PartialEq)]
336pub struct DropShadowRecipe {
337    /// The corner radii of the rounded rectangle that casts the shadow.
338    pub radius_x: f64,
339    pub radius_y: f64,
340    /// The elevation: half the Z of the translation, at most 64.
341    pub elevation: f32,
342    pub ambient_blur_radius: f32,
343    pub directional_blur_radius: f32,
344    /// How far each shadow is moved down.
345    pub ambient_y_offset: f32,
346    pub directional_y_offset: f32,
347    pub ambient_opacity: f32,
348    pub directional_opacity: f32,
349    /// Black with the opacity in the alpha (truncated, as WinUI converts it).
350    pub ambient_color: Color,
351    pub directional_color: Color,
352    /// How far the shadow reaches around the element.
353    pub insets: Thickness,
354}
355
356impl DropShadowRecipe {
357    /// `GetDropShadowRecipe(translationZ, theme)`: no shadow below an
358    /// elevation of 2; up to 16 a directional shadow only, of the opacity
359    /// `min(elevation / 100 + 0.06, 0.14)` in the light theme and 0.26 in the
360    /// dark one; above 16 also an ambient shadow, blurred by a third of the
361    /// elevation and moved down by 2, of the opacity 0.15 (light) or 0.37
362    /// (dark), the directional one 0.19 (light) or 0.37 (dark). The
363    /// directional shadow is blurred by the elevation and moved down by half
364    /// of it. The corner radii are zero; see
365    /// [`with_corner_radius`](Self::with_corner_radius).
366    ///
367    /// A high contrast theme has no shadow: both opacities are zero.
368    pub fn new(translation_z: f32, theme: ThemeShadowTheme) -> DropShadowRecipe {
369        let elevation = (translation_z / 2.0).min(64.0);
370        let mut ambient_blur_radius = 2.0f32;
371        let mut ambient_y_offset = 0.0f32;
372        let mut ambient_opacity = 0.0f32;
373        let mut directional_opacity = 0.0f32;
374
375        if elevation < 2.0 {
376            // No shadow.
377        } else if elevation <= 16.0 {
378            // The ambient shadow does not show.
379            directional_opacity = match theme {
380                ThemeShadowTheme::Light => (elevation / 100.0 + 0.06).min(0.14),
381                ThemeShadowTheme::Dark => 0.26,
382                ThemeShadowTheme::HighContrast => 0.0,
383            };
384        } else {
385            ambient_blur_radius = elevation / 3.0;
386            ambient_y_offset = 2.0;
387            (ambient_opacity, directional_opacity) = match theme {
388                ThemeShadowTheme::Light => (0.15, 0.19),
389                ThemeShadowTheme::Dark => (0.37, 0.37),
390                ThemeShadowTheme::HighContrast => (0.0, 0.0),
391            };
392        }
393
394        let black = |opacity: f32| Color::from_argb((opacity * 255.0) as u8, 0, 0, 0);
395        let directional_blur_radius = elevation;
396        let directional_y_offset = elevation * 0.5;
397        let max_blur_radius = ambient_blur_radius.max(directional_blur_radius).ceil() as f64;
398        let top = (ambient_blur_radius - ambient_y_offset).max(directional_blur_radius - directional_y_offset).ceil();
399        let bottom = (ambient_blur_radius + ambient_y_offset).max(directional_blur_radius + directional_y_offset).ceil();
400
401        DropShadowRecipe {
402            radius_x: 0.0,
403            radius_y: 0.0,
404            elevation,
405            ambient_blur_radius,
406            directional_blur_radius,
407            ambient_y_offset,
408            directional_y_offset,
409            ambient_opacity,
410            directional_opacity,
411            ambient_color: black(ambient_opacity),
412            directional_color: black(directional_opacity),
413            insets: Thickness::new(max_blur_radius, top as f64, max_blur_radius, bottom as f64),
414        }
415    }
416
417    /// The recipe with the corner radii of the rounded rectangle that casts
418    /// the shadow.
419    pub fn with_corner_radius(mut self, radius_x: f64, radius_y: f64) -> DropShadowRecipe {
420        self.radius_x = radius_x.max(0.0);
421        self.radius_y = radius_y.max(0.0);
422        self
423    }
424
425    /// Whether the recipe draws anything.
426    pub fn is_visible(&self) -> bool {
427        self.ambient_color.a > 0 || self.directional_color.a > 0
428    }
429
430    /// The two shadows as the box shadows of the rounded rectangle the shadow
431    /// is drawn around, the ambient first (`CreateDropShadowVisual` puts it
432    /// at the bottom). Each is blurred by one more than its radius, because
433    /// the rectangle is one pixel smaller on each side than the element
434    /// (`CreateDropShadowVisual` shrinks the dummy shape so that the
435    /// antialiased edge of the element lies over the shadow).
436    pub fn box_shadows(&self) -> Vec<BoxShadow> {
437        let mut shadows = Vec::with_capacity(2);
438        if self.ambient_color.a > 0 {
439            shadows.push(BoxShadow {
440                offset_x: 0.0,
441                offset_y: self.ambient_y_offset as f64,
442                blur: (self.ambient_blur_radius + 1.0) as f64,
443                spread: 0.0,
444                color: self.ambient_color,
445                is_inset: false,
446            });
447        }
448        if self.directional_color.a > 0 {
449            shadows.push(BoxShadow {
450                offset_x: 0.0,
451                offset_y: self.directional_y_offset as f64,
452                blur: (self.directional_blur_radius + 1.0) as f64,
453                spread: 0.0,
454                color: self.directional_color,
455                is_inset: false,
456            });
457        }
458        shadows
459    }
460}