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    })
126}
127
128fn build_drop_shadow_primitives(
129    size: Size,
130    shape: LayerShape,
131    scope: &ShadowScope,
132) -> Vec<DrawPrimitive> {
133    let Some(scope) = normalized_scope(scope) else {
134        return Vec::new();
135    };
136    if size.width <= 0.0 || size.height <= 0.0 {
137        return Vec::new();
138    }
139
140    let brush = alpha_modulated_brush(
141        scope.brush.unwrap_or_else(|| Brush::solid(scope.color)),
142        scope.alpha,
143    );
144
145    let spread = scope.spread;
146    let rect = Rect {
147        x: scope.offset.x - spread,
148        y: scope.offset.y - spread,
149        width: size.width + spread * 2.0,
150        height: size.height + spread * 2.0,
151    };
152    if rect.width <= 0.0 || rect.height <= 0.0 {
153        return Vec::new();
154    }
155
156    let Some(shape_prim) = primitive_for_shape(shape, rect, brush) else {
157        return Vec::new();
158    };
159
160    vec![DrawPrimitive::Shadow(ShadowPrimitive::Drop {
161        shape: Box::new(shape_prim),
162        blur_radius: scope.radius,
163        blend_mode: scope.blend_mode,
164    })]
165}
166
167fn build_inner_shadow_primitives(
168    size: Size,
169    shape: LayerShape,
170    scope: &ShadowScope,
171) -> Vec<DrawPrimitive> {
172    let Some(scope) = normalized_scope(scope) else {
173        return Vec::new();
174    };
175    if size.width <= 0.0 || size.height <= 0.0 {
176        return Vec::new();
177    }
178    if scope.radius <= f32::EPSILON
179        && scope.spread.abs() <= f32::EPSILON
180        && scope.offset.x.abs() <= f32::EPSILON
181        && scope.offset.y.abs() <= f32::EPSILON
182    {
183        return Vec::new();
184    }
185
186    let brush = alpha_modulated_brush(
187        scope.brush.unwrap_or_else(|| Brush::solid(scope.color)),
188        scope.alpha,
189    );
190
191    let outer = Rect {
192        x: 0.0,
193        y: 0.0,
194        width: size.width,
195        height: size.height,
196    };
197    let left = scope.offset.x + scope.spread;
198    let top = scope.offset.y + scope.spread;
199    let right = (scope.offset.x + size.width - scope.spread).max(left);
200    let bottom = (scope.offset.y + size.height - scope.spread).max(top);
201    let inner = Rect {
202        x: left,
203        y: top,
204        width: right - left,
205        height: bottom - top,
206    };
207    if inner.width <= 0.0 || inner.height <= 0.0 {
208        return Vec::new();
209    }
210
211    let Some(fill) = primitive_for_shape(shape, outer, brush) else {
212        return Vec::new();
213    };
214    let Some(cutout) = primitive_for_shape(shape, inner, Brush::solid(Color::WHITE)) else {
215        return Vec::new();
216    };
217
218    vec![DrawPrimitive::Shadow(ShadowPrimitive::Inner {
219        fill: Box::new(fill),
220        cutout: Box::new(cutout),
221        blur_radius: scope.radius,
222        blend_mode: scope.blend_mode,
223        clip_rect: outer,
224    })]
225}
226
227fn primitive_for_shape(shape: LayerShape, rect: Rect, brush: Brush) -> Option<DrawPrimitive> {
228    if rect.width <= 0.0 || rect.height <= 0.0 {
229        return None;
230    }
231
232    Some(match shape {
233        LayerShape::Rectangle => DrawPrimitive::Rect { rect, brush },
234        LayerShape::Rounded(shape) => {
235            let radii = shape.resolve(rect.width, rect.height);
236            DrawPrimitive::RoundRect { rect, brush, radii }
237        }
238    })
239}
240
241fn alpha_modulated_brush(brush: Brush, alpha: f32) -> Brush {
242    let alpha = alpha.clamp(0.0, 1.0);
243    match brush {
244        Brush::Solid(color) => Brush::Solid(color.with_alpha(color.a() * alpha)),
245        Brush::LinearGradient {
246            colors,
247            stops,
248            start,
249            end,
250            tile_mode,
251        } => Brush::LinearGradient {
252            colors: colors
253                .into_iter()
254                .map(|color| color.with_alpha(color.a() * alpha))
255                .collect(),
256            stops,
257            start,
258            end,
259            tile_mode,
260        },
261        Brush::RadialGradient {
262            colors,
263            stops,
264            center,
265            radius,
266            tile_mode,
267        } => Brush::RadialGradient {
268            colors: colors
269                .into_iter()
270                .map(|color| color.with_alpha(color.a() * alpha))
271                .collect(),
272            stops,
273            center,
274            radius,
275            tile_mode,
276        },
277        Brush::SweepGradient {
278            colors,
279            stops,
280            center,
281        } => Brush::SweepGradient {
282            colors: colors
283                .into_iter()
284                .map(|color| color.with_alpha(color.a() * alpha))
285                .collect(),
286            stops,
287            center,
288        },
289    }
290}