Skip to main content

ferroui_base/rendering/composition/
theme_shadow_visual.rs

1//! The visual that draws the theme shadow of an element (phase 7, not from
2//! upstream): the counterpart of the drop shadow visual of
3//! `HWCompTreeNodeWinRT` in microsoft-ui-xaml
4//! (`dxaml/xcp/components/comptree/HWCompNodeWinRT.cpp`: `NeedsDropShadowVisual`,
5//! `UpdateDropShadowVisual`, `CreateDropShadowVisual`).
6//!
7//! WinUI draws the shadow of a caster on a sprite visual parented under the
8//! prepend visual of the caster, so that it escapes the caster's own clips
9//! but not those of its ancestors, with the caster's primary visual as its
10//! `ParentForTransform` and the caster's opacity. Its brush is a nine-grid of
11//! an image of the recipe: a rounded rectangle (one pixel smaller than the
12//! caster on each side) with an ambient and a directional drop shadow, the
13//! rectangle itself cut out. Here the visual is a custom visual inserted just
14//! before the caster's own visual among the visuals of the caster's parent,
15//! with the same transform parent and opacity; it draws the two shadows of
16//! the recipe as the box shadows of that rounded rectangle, which the drawing
17//! contexts draw outside the rectangle only: the same image, without the
18//! nine-grid.
19
20use super::{
21    CompositionCustomVisual, CompositionCustomVisualHandler, CompositionVisual, ICompositionCustomVisualHandler,
22    ICompositionObjectAnimations,
23};
24use crate::media::{BoxShadow, BoxShadows, DropShadowRecipe, ImmediateDrawingContext};
25use crate::{Rect, RoundedRect, Vector};
26use std::any::Any;
27use std::cell::{Cell, RefCell};
28use std::rc::Rc;
29use std::sync::Arc;
30
31/// What the visual draws: the box shadows of the rounded rectangle and its
32/// radii.
33#[derive(Clone, Debug, PartialEq)]
34struct ShadowShape {
35    shadows: Vec<BoxShadow>,
36    radius_x: f64,
37    radius_y: f64,
38}
39
40impl ShadowShape {
41    fn of(recipe: &DropShadowRecipe) -> ShadowShape {
42        ShadowShape { shadows: recipe.box_shadows(), radius_x: recipe.radius_x, radius_y: recipe.radius_y }
43    }
44}
45
46/// The shadow visual of a caster: created by the caster's
47/// [`Visual`](crate::Visual), shown by its parent before the caster's own
48/// visual.
49pub struct ThemeShadowVisual {
50    visual: CompositionCustomVisual,
51    /// The shape last sent to the handler.
52    shape: RefCell<Option<ShadowShape>>,
53    size: Cell<Vector>,
54}
55
56impl ThemeShadowVisual {
57    /// Creates the shadow visual of `caster` (the composition visual of the
58    /// caster): its transform is taken in the space of the caster
59    /// (`ParentForTransform`), its opacity follows the caster's through an
60    /// expression (as `UpdateDropShadowVisualOpacity` does for an animated
61    /// opacity), and it is not hit-testable.
62    ///
63    /// The handler starts with the shape of `recipe`, so that the first frame that shows the
64    /// visual draws the shadow (a message reaches the handler a frame later).
65    pub fn new(caster: &Rc<CompositionVisual>, recipe: &DropShadowRecipe) -> ThemeShadowVisual {
66        let compositor = caster.compositor().clone();
67        let shape = ShadowShape::of(recipe);
68        let initial = shape.clone();
69        let visual = compositor.create_custom_visual(move || {
70            Rc::new(ThemeShadowHandler { base: CompositionCustomVisualHandler::default(), shape: RefCell::new(Some(initial)) })
71                as Rc<dyn ICompositionCustomVisualHandler>
72        });
73        visual.set_is_hit_test_visible(false);
74        // The shadow lies outside the bounds of the caster.
75        visual.set_clip_to_bounds(false);
76        visual.set_parent_for_transform(Some(caster.clone()));
77        let opacity = compositor.create_expression_animation_with("caster.Opacity");
78        opacity.set_reference_parameter("caster", caster.clone());
79        visual.start_animation("Opacity", &*opacity);
80        ThemeShadowVisual { visual, shape: RefCell::new(Some(shape)), size: Cell::new(Vector::default()) }
81    }
82
83    /// The composition visual.
84    pub fn visual(&self) -> &Rc<CompositionVisual> {
85        &self.visual
86    }
87
88    /// Shows `recipe` around a caster of `size`, or nothing when `visible` is
89    /// false or the recipe draws nothing.
90    pub fn update(&self, recipe: &DropShadowRecipe, size: Vector, visible: bool) {
91        let visible = visible && recipe.is_visible() && size.x > 0.0 && size.y > 0.0;
92        self.visual.set_visible(visible);
93        if self.size.get() != size {
94            self.size.set(size);
95            self.visual.set_size(size);
96        }
97        let shape = ShadowShape::of(recipe);
98        if self.shape.borrow().as_ref() == Some(&shape) {
99            return;
100        }
101        *self.shape.borrow_mut() = Some(shape.clone());
102        self.visual.send_handler_message(Arc::new(shape));
103    }
104
105    /// The box shadows the visual draws (for tests).
106    pub fn box_shadows(&self) -> Vec<BoxShadow> {
107        self.shape.borrow().as_ref().map(|shape| shape.shadows.clone()).unwrap_or_default()
108    }
109
110    /// Releases the visual.
111    pub fn dispose(&self) {
112        self.visual.dispose();
113    }
114}
115
116/// Draws the shape of the shadow on the server.
117#[derive(Default)]
118struct ThemeShadowHandler {
119    base: CompositionCustomVisualHandler,
120    shape: RefCell<Option<ShadowShape>>,
121}
122
123impl ThemeShadowHandler {
124    /// The rounded rectangle the shadows are cast by: the caster one pixel
125    /// smaller on each side, so that the antialiased edge of the caster lies
126    /// over the shadow (`CreateDropShadowVisual`).
127    fn caster_rect(&self) -> Option<RoundedRect> {
128        let size = self.base.effective_size();
129        if size.x <= 2.0 || size.y <= 2.0 {
130            return None;
131        }
132        let shape = self.shape.borrow();
133        let shape = shape.as_ref()?;
134        let rect = Rect::new(1.0, 1.0, size.x - 2.0, size.y - 2.0);
135        Some(RoundedRect::from_radius_xy(rect, shape.radius_x, shape.radius_y))
136    }
137
138    fn box_shadows(&self) -> Option<BoxShadows> {
139        let shape = self.shape.borrow();
140        let shadows = &shape.as_ref()?.shadows;
141        let (first, rest) = shadows.split_first()?;
142        Some(BoxShadows::with_rest(*first, rest))
143    }
144}
145
146impl ICompositionCustomVisualHandler for ThemeShadowHandler {
147    fn handler_base(&self) -> &CompositionCustomVisualHandler {
148        &self.base
149    }
150
151    fn on_message(&self, message: Arc<dyn Any + Send + Sync>) {
152        if let Some(shape) = message.downcast_ref::<ShadowShape>() {
153            *self.shape.borrow_mut() = Some(shape.clone());
154            self.base.invalidate();
155        }
156    }
157
158    fn on_render(&self, drawing_context: &mut ImmediateDrawingContext<'_>) {
159        let (Some(rect), Some(shadows)) = (self.caster_rect(), self.box_shadows()) else { return };
160        drawing_context.platform_impl().draw_rectangle(None, None, rect, &shadows);
161    }
162
163    /// The caster with the reach of its shadows.
164    fn get_render_bounds(&self) -> Rect {
165        let size = self.base.effective_size();
166        let caster = Rect::new(0.0, 0.0, size.x, size.y);
167        match (self.caster_rect(), self.box_shadows()) {
168            (Some(rect), Some(shadows)) => caster.union(shadows.transform_bounds(rect.rect)),
169            _ => caster,
170        }
171    }
172}