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 draw = Rc::new(move |scope: &mut DrawScopeDefault| {
43            let shadow =
44                shadow.to_scope(Density::from_scale(crate::render_state::current_density()));
45            let primitive = build_drop_shadow_primitive(scope.size(), shape, &shadow);
46            scope.push_recorded(primitive);
47        });
48        let modifier = Self::with_element(DrawCommandElement::new(DrawCommand::Behind(draw)))
49            .with_inspector_metadata(inspector_metadata("dropShadow", move |info| {
50                info.add_property("shape", format!("{shape:?}"));
51                info.add_property("shadowKind", "static");
52            }));
53        self.then(modifier)
54    }
55
56    /// Draws an inner shadow on top of current content.
57    ///
58    /// This mirrors Compose 1.9's `innerShadow(shape) { ... }`.
59    ///
60    /// Backend note: the `pixels` renderer currently draws the shadow geometry
61    /// without Gaussian blur; `wgpu` applies the requested blur radius.
62    pub fn inner_shadow(
63        self,
64        shape: LayerShape,
65        block: impl Fn(&mut ShadowScope) + 'static,
66    ) -> Self {
67        let block = Rc::new(block);
68        let draw = Rc::new(move |scope: &mut DrawScopeDefault| {
69            let mut shadow = ShadowScope::default();
70            block(&mut shadow);
71            let primitive = build_inner_shadow_primitive(scope.size(), shape, &shadow);
72            scope.push_recorded(primitive);
73        });
74        let modifier = Self::with_element(DrawCommandElement::new(DrawCommand::Overlay(draw)))
75            .with_inspector_metadata(inspector_metadata("innerShadow", move |info| {
76                info.add_property("shape", format!("{shape:?}"));
77                info.add_property("shadowKind", "block");
78            }));
79        self.then(modifier)
80    }
81
82    /// Static shadow configuration variant mirroring Compose's `innerShadow(shape, shadow)`.
83    pub fn inner_shadow_value(self, shape: LayerShape, shadow: Shadow) -> Self {
84        let draw = Rc::new(move |scope: &mut DrawScopeDefault| {
85            let shadow =
86                shadow.to_scope(Density::from_scale(crate::render_state::current_density()));
87            let primitive = build_inner_shadow_primitive(scope.size(), shape, &shadow);
88            scope.push_recorded(primitive);
89        });
90        let modifier = Self::with_element(DrawCommandElement::new(DrawCommand::Overlay(draw)))
91            .with_inspector_metadata(inspector_metadata("innerShadow", move |info| {
92                info.add_property("shape", format!("{shape:?}"));
93                info.add_property("shadowKind", "static");
94            }));
95        self.then(modifier)
96    }
97}
98
99fn normalized_scope(scope: &ShadowScope) -> Option<ShadowScope> {
100    if !scope.alpha.is_finite() || scope.alpha <= 0.0 {
101        return None;
102    }
103    let radius = if scope.radius.is_finite() {
104        scope.radius.max(0.0)
105    } else {
106        0.0
107    };
108    let spread = if scope.spread.is_finite() {
109        scope.spread
110    } else {
111        0.0
112    };
113    let offset = Point {
114        x: if scope.offset.x.is_finite() {
115            scope.offset.x
116        } else {
117            0.0
118        },
119        y: if scope.offset.y.is_finite() {
120            scope.offset.y
121        } else {
122            0.0
123        },
124    };
125    Some(ShadowScope {
126        radius,
127        spread,
128        offset,
129        color: scope.color,
130        brush: scope.brush.clone(),
131        alpha: scope.alpha.clamp(0.0, 1.0),
132        blend_mode: scope.blend_mode,
133        cutout: scope.cutout,
134    })
135}
136
137fn build_drop_shadow_primitive(
138    size: Size,
139    shape: LayerShape,
140    scope: &ShadowScope,
141) -> Option<DrawPrimitive> {
142    let scope = normalized_scope(scope)?;
143    if size.width <= 0.0 || size.height <= 0.0 {
144        return None;
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 None;
161    }
162
163    let shape_prim = primitive_for_shape(shape, rect, brush)?;
164
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    Some(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_primitive(
186    size: Size,
187    shape: LayerShape,
188    scope: &ShadowScope,
189) -> Option<DrawPrimitive> {
190    let scope = normalized_scope(scope)?;
191    if size.width <= 0.0 || size.height <= 0.0 {
192        return None;
193    }
194    if scope.radius <= f32::EPSILON
195        && scope.spread.abs() <= f32::EPSILON
196        && scope.offset.x.abs() <= f32::EPSILON
197        && scope.offset.y.abs() <= f32::EPSILON
198    {
199        return None;
200    }
201
202    let brush = alpha_modulated_brush(
203        scope.brush.unwrap_or_else(|| Brush::solid(scope.color)),
204        scope.alpha,
205    );
206
207    let outer = Rect {
208        x: 0.0,
209        y: 0.0,
210        width: size.width,
211        height: size.height,
212    };
213    let left = scope.offset.x + scope.spread;
214    let top = scope.offset.y + scope.spread;
215    let right = (scope.offset.x + size.width - scope.spread).max(left);
216    let bottom = (scope.offset.y + size.height - scope.spread).max(top);
217    let inner = Rect {
218        x: left,
219        y: top,
220        width: right - left,
221        height: bottom - top,
222    };
223    if inner.width <= 0.0 || inner.height <= 0.0 {
224        return None;
225    }
226
227    let fill = primitive_for_shape(shape, outer, brush)?;
228    let cutout = primitive_for_shape(shape, inner, Brush::solid(Color::WHITE))?;
229
230    Some(DrawPrimitive::Shadow(ShadowPrimitive::Inner {
231        fill: Box::new(fill),
232        cutout: Box::new(cutout),
233        blur_radius: scope.radius,
234        blend_mode: scope.blend_mode,
235        clip_rect: outer,
236    }))
237}
238
239fn primitive_for_shape(shape: LayerShape, rect: Rect, brush: Brush) -> Option<DrawPrimitive> {
240    if rect.width <= 0.0 || rect.height <= 0.0 {
241        return None;
242    }
243
244    Some(match shape {
245        LayerShape::Rectangle => DrawPrimitive::Rect {
246            rect,
247            brush,
248            stroke: None,
249        },
250        LayerShape::Rounded(shape) => {
251            let radii = shape.resolve(rect.width, rect.height);
252            DrawPrimitive::RoundRect {
253                rect,
254                brush,
255                radii,
256                stroke: None,
257            }
258        }
259    })
260}
261
262fn alpha_modulated_brush(brush: Brush, alpha: f32) -> Brush {
263    let alpha = alpha.clamp(0.0, 1.0);
264    match brush {
265        Brush::Solid(color) => Brush::Solid(color.with_alpha(color.a() * alpha)),
266        Brush::LinearGradient {
267            colors,
268            stops,
269            start,
270            end,
271            tile_mode,
272        } => Brush::LinearGradient {
273            colors: colors
274                .into_iter()
275                .map(|color| color.with_alpha(color.a() * alpha))
276                .collect(),
277            stops,
278            start,
279            end,
280            tile_mode,
281        },
282        Brush::RadialGradient {
283            colors,
284            stops,
285            center,
286            radius,
287            tile_mode,
288        } => Brush::RadialGradient {
289            colors: colors
290                .into_iter()
291                .map(|color| color.with_alpha(color.a() * alpha))
292                .collect(),
293            stops,
294            center,
295            radius,
296            tile_mode,
297        },
298        Brush::SweepGradient {
299            colors,
300            stops,
301            center,
302        } => Brush::SweepGradient {
303            colors: colors
304                .into_iter()
305                .map(|color| color.with_alpha(color.a() * alpha))
306                .collect(),
307            stops,
308            center,
309        },
310    }
311}