Skip to main content

cranpose_ui/modifier/
shadow.rs

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