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