Skip to main content

ferroui_base/rendering/composition/server/
server_layer_visual.rs

1use super::{
2    CompositionProperty, IServerObject, IServerRenderResourceObserver, IServerVisualContent, ServerCompositionEffectBrush,
3    ServerCompositionVisual, ServerDropShadow, ServerSizeDependantVisual, ServerVisualRenderContext,
4};
5use crate::media::{Color, EffectExtensions, IEffect, ImmutableDropShadowEffect};
6use crate::platform::{IDrawingContextImpl, LtrbRect};
7use crate::rendering::composition::generated::{
8    ServerCompositionContainerVisualProps, ServerLayerVisualHooks, ServerLayerVisualProps,
9};
10use crate::rendering::composition::transport::BatchStreamReader;
11use crate::{Rect, Thickness};
12use std::any::{Any, TypeId};
13use std::rc::Rc;
14use std::time::Duration;
15
16/// Starts or stops observing a render resource a visual shows: a change of
17/// the resource (an animation included) reaches the content of the visual
18/// through `dependency_queued_invalidate`.
19pub(crate) fn observe_visual_resource(
20    visual: &ServerCompositionVisual,
21    resource: Option<Rc<dyn IServerObject>>,
22    observe: bool,
23) {
24    let Some(resource) = resource.and_then(|resource| resource.as_render_resource()) else { return };
25    let Some(this) = visual.weak().upgrade() else { return };
26    let observer: Rc<dyn IServerRenderResourceObserver> = this;
27    if observe {
28        resource.add_observer(&observer);
29    } else {
30        resource.remove_observer(&observer);
31    }
32}
33
34/// The drop shadow of the alpha of pushed content that draws `shadow`: its
35/// offset (X and Y), blur radius, colour and opacity. The offset is the one of
36/// the shadow, outside the plane of any visual
37/// ([`drop_shadow_effect_in`] projects it into the plane of a visual).
38pub fn drop_shadow_effect(shadow: &ServerDropShadow) -> ImmutableDropShadowEffect {
39    drop_shadow_effect_in(shadow, None)
40}
41
42/// The drop shadow of the alpha of pushed content that draws `shadow` for
43/// `visual`: its offset in the plane of the visual
44/// ([`ServerDropShadow::plane_offset`]), blur radius, colour and opacity.
45pub fn drop_shadow_effect_in(shadow: &ServerDropShadow, visual: Option<&ServerCompositionVisual>) -> ImmutableDropShadowEffect {
46    let props = shadow.props();
47    let offset = shadow.plane_offset(visual);
48    let color: Color = props.color();
49    ImmutableDropShadowEffect::new(
50        offset.x,
51        offset.y,
52        props.blur_radius().max(0.0) as f64,
53        color,
54        props.opacity().clamp(0.0, 1.0) as f64,
55    )
56}
57
58/// How a layer visual composes its children.
59enum LayerMode {
60    /// Through a drop shadow effect of the drawing context: the shadow follows
61    /// the alpha of the composed children.
62    Shadow(ImmutableDropShadowEffect),
63    /// Into a plain layer.
64    Layer,
65}
66
67/// Server-side counterpart of a
68/// [`LayerVisual`](crate::rendering::composition::LayerVisual): its children
69/// are composed into an offscreen layer (`push_layer` over the bounds of the
70/// subtree), which the opacity of the visual applies to as a whole.
71///
72/// With a shadow, the children are composed through a drop shadow effect
73/// when the drawing context has effects, so the shadow follows the alpha of
74/// the composed content; otherwise the box shadow of the rectangle of the
75/// visual is drawn under the layer. A shadow with a `Mask` (and the
76/// `Default` source policy) is the shadow of the alpha of the mask over the
77/// rectangle of the visual, drawn under the layer
78/// ([`ServerDropShadow::render_under_rect`]).
79///
80/// With an effect (an effect brush), the composed children are the first
81/// source parameter of its graph: what the graph draws under the layer and
82/// what wraps it are drawn and pushed before the children, inside the layer
83/// (or the shadow), and popped and drawn after them.
84///
85/// Deviation (winui-composition.md): without effect support in the drawing
86/// context the shadow is the box shadow of the rectangle of the visual, as
87/// for a sprite visual.
88#[derive(Default)]
89pub struct ServerLayerVisual {
90    props: ServerLayerVisualProps,
91}
92
93impl ServerLayerVisual {
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// The properties of the visual.
99    pub fn props(&self) -> &ServerLayerVisualProps {
100        &self.props
101    }
102
103    /// The shadow.
104    pub fn shadow(&self) -> Option<Rc<ServerDropShadow>> {
105        self.props.shadow().and_then(|shadow| shadow.into_any_rc().downcast::<ServerDropShadow>().ok())
106    }
107
108    /// The effect brush applied to the composed children (the WinUI
109    /// `Effect`).
110    pub fn effect_brush(&self) -> Option<Rc<ServerCompositionEffectBrush>> {
111        self.props
112            .effect_brush()
113            .and_then(|brush| brush.into_any_rc().downcast::<ServerCompositionEffectBrush>().ok())
114    }
115
116    /// The shadow, when it is visible.
117    fn visible_shadow(&self) -> Option<Rc<ServerDropShadow>> {
118        self.shadow().filter(|shadow| shadow.is_visible())
119    }
120
121    fn rect(visual: &ServerCompositionVisual) -> Rect {
122        let size = visual.size();
123        Rect::new(0.0, 0.0, size.x, size.y)
124    }
125
126    /// How the children are composed on `canvas`; `None` when the subtree has
127    /// no bounds (nothing is pushed). The same before and after the
128    /// children, so what is pushed is popped.
129    fn layer_mode(&self, visual: &ServerCompositionVisual, canvas: &mut dyn IDrawingContextImpl) -> Option<(LayerMode, Rect)> {
130        let bounds = visual.sub_tree_bounds()?.to_rect();
131        // A shadow with a mask (and the `Default` source policy) is the
132        // shadow of the mask, drawn under a plain layer.
133        let mode = match self.visible_shadow() {
134            Some(shadow)
135                if shadow.effective_mask().is_none() && canvas.as_drawing_context_impl_with_effects().is_some() =>
136            {
137                LayerMode::Shadow(drop_shadow_effect_in(&shadow, Some(visual)))
138            }
139            _ => LayerMode::Layer,
140        };
141        Some((mode, bounds))
142    }
143}
144
145impl ServerLayerVisualHooks for ServerCompositionVisual {
146    fn base_deserialize_changes_core(&self, reader: &mut BatchStreamReader<'_>, committed_at: Duration) {
147        ServerCompositionVisual::base_deserialize_changes_core(self, reader, committed_at);
148    }
149
150    fn on_shadow_changing(&self) {
151        if let Some(content) = self.content_as::<ServerLayerVisual>() {
152            observe_visual_resource(self, content.props.shadow(), false);
153        }
154    }
155
156    fn on_shadow_changed(&self) {
157        if let Some(content) = self.content_as::<ServerLayerVisual>() {
158            observe_visual_resource(self, content.props.shadow(), true);
159        }
160        self.invalidate_content();
161    }
162
163    fn on_effect_brush_changing(&self) {
164        if let Some(content) = self.content_as::<ServerLayerVisual>() {
165            observe_visual_resource(self, content.props.effect_brush(), false);
166        }
167    }
168
169    fn on_effect_brush_changed(&self) {
170        if let Some(content) = self.content_as::<ServerLayerVisual>() {
171            observe_visual_resource(self, content.props.effect_brush(), true);
172        }
173        self.invalidate_content();
174    }
175}
176
177impl IServerVisualContent for ServerLayerVisual {
178    fn deserialize_changes_core(
179        &self,
180        visual: &ServerCompositionVisual,
181        reader: &mut BatchStreamReader<'_>,
182        committed_at: Duration,
183    ) {
184        self.props.deserialize_changes_core(visual, reader, committed_at);
185    }
186
187    /// The area of the box shadow of the rectangle, when there is a shadow
188    /// (what the fallback draws); a layer visual has no content of its own.
189    fn compute_own_content_bounds(&self, visual: &ServerCompositionVisual) -> Option<LtrbRect> {
190        let shadow = self.visible_shadow()?;
191        let bounds = ServerSizeDependantVisual::compute_own_content_bounds(visual)?;
192        Some(LtrbRect::from_rect(shadow.bounds_in(Some(visual), bounds.to_rect())))
193    }
194
195    fn size_changed(&self, visual: &ServerCompositionVisual) {
196        ServerSizeDependantVisual::size_changed(visual);
197    }
198
199    /// The shadow of the composed children extends the subtree as the drop
200    /// shadow effect does, and the blurs of the effect around the layer as
201    /// the blur effect does.
202    fn sub_tree_bounds_padding(&self, visual: &ServerCompositionVisual) -> Option<Thickness> {
203        let shadow = self.visible_shadow().map(|shadow| {
204            let effect = drop_shadow_effect_in(&shadow, Some(visual));
205            EffectExtensions::get_effect_output_padding(Some(&effect as &dyn IEffect))
206        });
207        let effect = self.effect_brush().map(|effect| effect.layer_padding());
208        match (shadow, effect) {
209            (None, None) => None,
210            (shadow, effect) => Some(shadow.unwrap_or_default() + effect.unwrap_or_default()),
211        }
212    }
213
214    /// A shadow or an effect depends on every change of the subtree, as an
215    /// effect does.
216    fn has_effect(&self, visual: &ServerCompositionVisual) -> bool {
217        visual.effect().is_some() || self.visible_shadow().is_some() || self.effect_brush().is_some()
218    }
219
220    fn render_core(
221        &self,
222        visual: &ServerCompositionVisual,
223        context: &mut ServerVisualRenderContext<'_>,
224        _current_transformed_clip: LtrbRect,
225    ) {
226        let canvas = context.canvas();
227        let Some((mode, bounds)) = self.layer_mode(visual, canvas) else { return };
228        match mode {
229            LayerMode::Shadow(effect) => {
230                if let Some(effects) = canvas.as_drawing_context_impl_with_effects() {
231                    effects.push_effect(Some(bounds), &effect);
232                }
233            }
234            LayerMode::Layer => {
235                if let Some(shadow) = self.visible_shadow() {
236                    shadow.render_under_rect_in(canvas, Some(visual), Self::rect(visual));
237                }
238                canvas.push_layer(bounds);
239            }
240        }
241        // The effect, inside the layer: the composed children are its first
242        // source parameter.
243        if let Some(effect) = self.effect_brush() {
244            effect.push_for_layer(canvas, bounds);
245        }
246    }
247
248    fn render_after_children(&self, visual: &ServerCompositionVisual, context: &mut ServerVisualRenderContext<'_>) {
249        let canvas = context.canvas();
250        let Some((mode, bounds)) = self.layer_mode(visual, canvas) else { return };
251        if let Some(effect) = self.effect_brush() {
252            effect.pop_for_layer(canvas, bounds);
253        }
254        match mode {
255            LayerMode::Shadow(_) => {
256                if let Some(effects) = canvas.as_drawing_context_impl_with_effects() {
257                    effects.pop_effect();
258                }
259            }
260            LayerMode::Layer => canvas.pop_layer(),
261        }
262    }
263
264    /// The shadow or the effect changed.
265    fn dependency_queued_invalidate(&self, visual: &ServerCompositionVisual) {
266        visual.invalidate_content();
267    }
268
269    fn dispose(&self, visual: &ServerCompositionVisual) {
270        observe_visual_resource(visual, self.props.shadow(), false);
271        observe_visual_resource(visual, self.props.effect_brush(), false);
272    }
273
274    fn find_props(&self, type_id: TypeId) -> Option<&dyn Any> {
275        self.props.find_props(type_id)
276    }
277
278    fn get_composition_property(&self, name: &str) -> Option<&'static CompositionProperty> {
279        ServerCompositionContainerVisualProps::get_composition_property(name)
280    }
281
282    fn as_any(&self) -> &dyn Any {
283        self
284    }
285}