Skip to main content

cranpose_ui/modifier/
shadow.rs

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