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 primitives = build_drop_shadow_primitives(scope.size(), shape, &shadow);
30            scope.push_recorded(primitives);
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 primitives = build_drop_shadow_primitives(scope.size(), shape, &shadow);
47            scope.push_recorded(primitives);
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 primitives = build_inner_shadow_primitives(scope.size(), shape, &shadow);
73            scope.push_recorded(primitives);
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 primitives = build_inner_shadow_primitives(scope.size(), shape, &shadow);
90            scope.push_recorded(primitives);
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_primitives(
140    size: Size,
141    shape: LayerShape,
142    scope: &ShadowScope,
143) -> Vec<DrawPrimitive> {
144    let Some(scope) = normalized_scope(scope) else {
145        return Vec::new();
146    };
147    if size.width <= 0.0 || size.height <= 0.0 {
148        return Vec::new();
149    }
150
151    let brush = alpha_modulated_brush(
152        scope.brush.unwrap_or_else(|| Brush::solid(scope.color)),
153        scope.alpha,
154    );
155
156    let spread = scope.spread;
157    let rect = Rect {
158        x: scope.offset.x - spread,
159        y: scope.offset.y - spread,
160        width: size.width + spread * 2.0,
161        height: size.height + spread * 2.0,
162    };
163    if rect.width <= 0.0 || rect.height <= 0.0 {
164        return Vec::new();
165    }
166
167    let Some(shape_prim) = primitive_for_shape(shape, rect, brush) else {
168        return Vec::new();
169    };
170
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}