Skip to main content

cranpose_ui/modifier/
shadow.rs

1use super::{
2    inspector_metadata, Brush, Color, DrawCommand, LayerShape, Modifier, Point, Rect, Shadow,
3    ShadowScope, Size,
4};
5use crate::modifier_nodes::DrawCommandElement;
6use cranpose_ui_graphics::{DrawPrimitive, DrawScope as _, DrawScopeDefault, ShadowPrimitive};
7use std::rc::Rc;
8
9impl Modifier {
10    /// Draws a drop shadow behind the current content.
11    ///
12    /// This mirrors Compose 1.9's `dropShadow(shape) { ... }`.
13    ///
14    /// Backend note: the `pixels` renderer currently draws the shadow geometry
15    /// without Gaussian blur; `wgpu` applies the requested blur radius.
16    pub fn drop_shadow(
17        self,
18        shape: LayerShape,
19        block: impl Fn(&mut ShadowScope) + 'static,
20    ) -> Self {
21        let block = Rc::new(block);
22        let draw = Rc::new(move |scope: &mut DrawScopeDefault| {
23            let mut shadow = ShadowScope::default();
24            block(&mut shadow);
25            let primitives = build_drop_shadow_primitives(scope.size(), shape, &shadow);
26            scope.push_recorded(primitives);
27        });
28        let modifier = Self::with_element(DrawCommandElement::new(DrawCommand::Behind(draw)))
29            .with_inspector_metadata(inspector_metadata("dropShadow", move |info| {
30                info.add_property("shape", format!("{shape:?}"));
31                info.add_property("shadowKind", "block");
32            }));
33        self.then(modifier)
34    }
35
36    /// Static shadow configuration variant mirroring Compose's `dropShadow(shape, shadow)`.
37    pub fn drop_shadow_value(self, shape: LayerShape, shadow: Shadow) -> Self {
38        let shadow_value = shadow.clone();
39        let draw = Rc::new(move |scope: &mut DrawScopeDefault| {
40            let shadow = shadow_value.to_scope(crate::render_state::current_density());
41            let primitives = build_drop_shadow_primitives(scope.size(), shape, &shadow);
42            scope.push_recorded(primitives);
43        });
44        let modifier = Self::with_element(DrawCommandElement::new(DrawCommand::Behind(draw)))
45            .with_inspector_metadata(inspector_metadata("dropShadow", move |info| {
46                info.add_property("shape", format!("{shape:?}"));
47                info.add_property("shadowKind", "static");
48            }));
49        self.then(modifier)
50    }
51
52    /// Draws an inner shadow on top of current content.
53    ///
54    /// This mirrors Compose 1.9's `innerShadow(shape) { ... }`.
55    ///
56    /// Backend note: the `pixels` renderer currently draws the shadow geometry
57    /// without Gaussian blur; `wgpu` applies the requested blur radius.
58    pub fn inner_shadow(
59        self,
60        shape: LayerShape,
61        block: impl Fn(&mut ShadowScope) + 'static,
62    ) -> Self {
63        let block = Rc::new(block);
64        let draw = Rc::new(move |scope: &mut DrawScopeDefault| {
65            let mut shadow = ShadowScope::default();
66            block(&mut shadow);
67            let primitives = build_inner_shadow_primitives(scope.size(), shape, &shadow);
68            scope.push_recorded(primitives);
69        });
70        let modifier = Self::with_element(DrawCommandElement::new(DrawCommand::Overlay(draw)))
71            .with_inspector_metadata(inspector_metadata("innerShadow", move |info| {
72                info.add_property("shape", format!("{shape:?}"));
73                info.add_property("shadowKind", "block");
74            }));
75        self.then(modifier)
76    }
77
78    /// Static shadow configuration variant mirroring Compose's `innerShadow(shape, shadow)`.
79    pub fn inner_shadow_value(self, shape: LayerShape, shadow: Shadow) -> Self {
80        let shadow_value = shadow.clone();
81        let draw = Rc::new(move |scope: &mut DrawScopeDefault| {
82            let shadow = shadow_value.to_scope(crate::render_state::current_density());
83            let primitives = build_inner_shadow_primitives(scope.size(), shape, &shadow);
84            scope.push_recorded(primitives);
85        });
86        let modifier = Self::with_element(DrawCommandElement::new(DrawCommand::Overlay(draw)))
87            .with_inspector_metadata(inspector_metadata("innerShadow", move |info| {
88                info.add_property("shape", format!("{shape:?}"));
89                info.add_property("shadowKind", "static");
90            }));
91        self.then(modifier)
92    }
93}
94
95fn normalized_scope(scope: &ShadowScope) -> Option<ShadowScope> {
96    if !scope.alpha.is_finite() || scope.alpha <= 0.0 {
97        return None;
98    }
99    let radius = if scope.radius.is_finite() {
100        scope.radius.max(0.0)
101    } else {
102        0.0
103    };
104    let spread = if scope.spread.is_finite() {
105        scope.spread
106    } else {
107        0.0
108    };
109    let offset = Point {
110        x: if scope.offset.x.is_finite() {
111            scope.offset.x
112        } else {
113            0.0
114        },
115        y: if scope.offset.y.is_finite() {
116            scope.offset.y
117        } else {
118            0.0
119        },
120    };
121    Some(ShadowScope {
122        radius,
123        spread,
124        offset,
125        color: scope.color,
126        brush: scope.brush.clone(),
127        alpha: scope.alpha.clamp(0.0, 1.0),
128        blend_mode: scope.blend_mode,
129        cutout: scope.cutout,
130    })
131}
132
133fn build_drop_shadow_primitives(
134    size: Size,
135    shape: LayerShape,
136    scope: &ShadowScope,
137) -> Vec<DrawPrimitive> {
138    let Some(scope) = normalized_scope(scope) else {
139        return Vec::new();
140    };
141    if size.width <= 0.0 || size.height <= 0.0 {
142        return Vec::new();
143    }
144
145    let brush = alpha_modulated_brush(
146        scope.brush.unwrap_or_else(|| Brush::solid(scope.color)),
147        scope.alpha,
148    );
149
150    let spread = scope.spread;
151    let rect = Rect {
152        x: scope.offset.x - spread,
153        y: scope.offset.y - spread,
154        width: size.width + spread * 2.0,
155        height: size.height + spread * 2.0,
156    };
157    if rect.width <= 0.0 || rect.height <= 0.0 {
158        return Vec::new();
159    }
160
161    let Some(shape_prim) = primitive_for_shape(shape, rect, brush) else {
162        return Vec::new();
163    };
164
165    // Knockout for translucent surfaces: erase the element's own (unoffset,
166    // unspread) shape from the silhouette so the blurred shadow exists only
167    // outside it — a backdrop-sampling material must not refract its own
168    // shadow.
169    let cutout = if scope.cutout {
170        let element_rect = Rect {
171            x: 0.0,
172            y: 0.0,
173            width: size.width,
174            height: size.height,
175        };
176        primitive_for_shape(shape, element_rect, Brush::solid(Color::BLACK)).map(Box::new)
177    } else {
178        None
179    };
180
181    vec![DrawPrimitive::Shadow(ShadowPrimitive::Drop {
182        shape: Box::new(shape_prim),
183        cutout,
184        blur_radius: scope.radius,
185        blend_mode: scope.blend_mode,
186    })]
187}
188
189fn build_inner_shadow_primitives(
190    size: Size,
191    shape: LayerShape,
192    scope: &ShadowScope,
193) -> Vec<DrawPrimitive> {
194    let Some(scope) = normalized_scope(scope) else {
195        return Vec::new();
196    };
197    if size.width <= 0.0 || size.height <= 0.0 {
198        return Vec::new();
199    }
200    if scope.radius <= f32::EPSILON
201        && scope.spread.abs() <= f32::EPSILON
202        && scope.offset.x.abs() <= f32::EPSILON
203        && scope.offset.y.abs() <= f32::EPSILON
204    {
205        return Vec::new();
206    }
207
208    let brush = alpha_modulated_brush(
209        scope.brush.unwrap_or_else(|| Brush::solid(scope.color)),
210        scope.alpha,
211    );
212
213    let outer = Rect {
214        x: 0.0,
215        y: 0.0,
216        width: size.width,
217        height: size.height,
218    };
219    let left = scope.offset.x + scope.spread;
220    let top = scope.offset.y + scope.spread;
221    let right = (scope.offset.x + size.width - scope.spread).max(left);
222    let bottom = (scope.offset.y + size.height - scope.spread).max(top);
223    let inner = Rect {
224        x: left,
225        y: top,
226        width: right - left,
227        height: bottom - top,
228    };
229    if inner.width <= 0.0 || inner.height <= 0.0 {
230        return Vec::new();
231    }
232
233    let Some(fill) = primitive_for_shape(shape, outer, brush) else {
234        return Vec::new();
235    };
236    let Some(cutout) = primitive_for_shape(shape, inner, Brush::solid(Color::WHITE)) else {
237        return Vec::new();
238    };
239
240    vec![DrawPrimitive::Shadow(ShadowPrimitive::Inner {
241        fill: Box::new(fill),
242        cutout: Box::new(cutout),
243        blur_radius: scope.radius,
244        blend_mode: scope.blend_mode,
245        clip_rect: outer,
246    })]
247}
248
249fn primitive_for_shape(shape: LayerShape, rect: Rect, brush: Brush) -> Option<DrawPrimitive> {
250    if rect.width <= 0.0 || rect.height <= 0.0 {
251        return None;
252    }
253
254    Some(match shape {
255        LayerShape::Rectangle => DrawPrimitive::Rect {
256            rect,
257            brush,
258            stroke: None,
259        },
260        LayerShape::Rounded(shape) => {
261            let radii = shape.resolve(rect.width, rect.height);
262            DrawPrimitive::RoundRect {
263                rect,
264                brush,
265                radii,
266                stroke: None,
267            }
268        }
269    })
270}
271
272fn alpha_modulated_brush(brush: Brush, alpha: f32) -> Brush {
273    let alpha = alpha.clamp(0.0, 1.0);
274    match brush {
275        Brush::Solid(color) => Brush::Solid(color.with_alpha(color.a() * alpha)),
276        Brush::LinearGradient {
277            colors,
278            stops,
279            start,
280            end,
281            tile_mode,
282        } => Brush::LinearGradient {
283            colors: colors
284                .into_iter()
285                .map(|color| color.with_alpha(color.a() * alpha))
286                .collect(),
287            stops,
288            start,
289            end,
290            tile_mode,
291        },
292        Brush::RadialGradient {
293            colors,
294            stops,
295            center,
296            radius,
297            tile_mode,
298        } => Brush::RadialGradient {
299            colors: colors
300                .into_iter()
301                .map(|color| color.with_alpha(color.a() * alpha))
302                .collect(),
303            stops,
304            center,
305            radius,
306            tile_mode,
307        },
308        Brush::SweepGradient {
309            colors,
310            stops,
311            center,
312        } => Brush::SweepGradient {
313            colors: colors
314                .into_iter()
315                .map(|color| color.with_alpha(color.a() * alpha))
316                .collect(),
317            stops,
318            center,
319        },
320    }
321}