Skip to main content

fret_core/scene/
shadow.rs

1use super::*;
2use crate::Size;
3
4fn rect_inflate(rect: Rect, delta: Px) -> Rect {
5    let d = delta.0.max(0.0);
6    Rect::new(
7        Point::new(Px(rect.origin.x.0 - d), Px(rect.origin.y.0 - d)),
8        Size::new(
9            Px(rect.size.width.0 + d * 2.0),
10            Px(rect.size.height.0 + d * 2.0),
11        ),
12    )
13}
14
15fn rect_deflate(rect: Rect, delta: Px) -> Rect {
16    let d = delta.0.max(0.0);
17    let w = (rect.size.width.0 - d * 2.0).max(0.0);
18    let h = (rect.size.height.0 - d * 2.0).max(0.0);
19    Rect::new(
20        Point::new(Px(rect.origin.x.0 + d), Px(rect.origin.y.0 + d)),
21        Size::new(Px(w), Px(h)),
22    )
23}
24
25fn rect_expand(rect: Rect, delta: Px) -> Rect {
26    if delta.0 >= 0.0 {
27        rect_inflate(rect, delta)
28    } else {
29        rect_deflate(rect, Px(-delta.0))
30    }
31}
32
33fn corners_inflate(mut corners: Corners, delta: Px) -> Corners {
34    let d = delta.0.max(0.0);
35    corners.top_left = Px((corners.top_left.0 + d).max(0.0));
36    corners.top_right = Px((corners.top_right.0 + d).max(0.0));
37    corners.bottom_left = Px((corners.bottom_left.0 + d).max(0.0));
38    corners.bottom_right = Px((corners.bottom_right.0 + d).max(0.0));
39    corners
40}
41
42fn corners_deflate(mut corners: Corners, delta: Px) -> Corners {
43    let d = delta.0.max(0.0);
44    corners.top_left = Px((corners.top_left.0 - d).max(0.0));
45    corners.top_right = Px((corners.top_right.0 - d).max(0.0));
46    corners.bottom_left = Px((corners.bottom_left.0 - d).max(0.0));
47    corners.bottom_right = Px((corners.bottom_right.0 - d).max(0.0));
48    corners
49}
50
51fn corners_expand(corners: Corners, delta: Px) -> Corners {
52    if delta.0 >= 0.0 {
53        corners_inflate(corners, delta)
54    } else {
55        corners_deflate(corners, Px(-delta.0))
56    }
57}
58
59fn color_with_alpha(mut color: Color, alpha: f32) -> Color {
60    color.a = alpha.clamp(0.0, 1.0);
61    color
62}
63
64fn shadow_alpha_weight(step_index: usize) -> f32 {
65    1.0 / (1.0 + step_index as f32)
66}
67
68#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct ShadowRRectFallbackSpec {
70    pub order: DrawOrder,
71    pub rect: Rect,
72    pub corner_radii: Corners,
73    pub offset: Point,
74    pub spread: Px,
75    pub blur_radius: Px,
76    pub color: Color,
77}
78
79/// Return the deterministic quad-approximation fallback for a rounded-rect shadow primitive.
80///
81/// This helper exists for backends and tools that need an explicit degradation path for
82/// `SceneOp::ShadowRRect` without routing back through `fret-ui` authoring APIs.
83pub fn shadow_rrect_fallback_quads(spec: ShadowRRectFallbackSpec) -> Vec<SceneOp> {
84    let ShadowRRectFallbackSpec {
85        order,
86        rect,
87        corner_radii,
88        offset,
89        spread,
90        blur_radius,
91        color,
92    } = spec;
93    if rect.size.width.0 <= 0.0 || rect.size.height.0 <= 0.0 {
94        return Vec::new();
95    }
96    if !offset.x.0.is_finite()
97        || !offset.y.0.is_finite()
98        || !blur_radius.0.is_finite()
99        || !spread.0.is_finite()
100        || color.a <= 0.0
101    {
102        return Vec::new();
103    }
104
105    let blur = blur_radius.0.max(0.0);
106    let max_steps = 32_f32;
107    let steps = blur.ceil().clamp(0.0, max_steps) as usize;
108    let denom = (steps as f32).max(1.0);
109    let alpha_weight_sum = if steps == 0 {
110        1.0
111    } else {
112        (0..=steps)
113            .map(shadow_alpha_weight)
114            .sum::<f32>()
115            .max(f32::EPSILON)
116    };
117
118    let mut out = Vec::with_capacity(steps.saturating_add(1));
119    for i in (0..=steps).rev() {
120        let t = i as f32 / denom;
121        let layer_spread = spread.0 + blur * t;
122        let fallback_rect = {
123            let mut fallback_rect = rect_expand(rect, Px(layer_spread));
124            fallback_rect.origin.x = Px(fallback_rect.origin.x.0 + offset.x.0);
125            fallback_rect.origin.y = Px(fallback_rect.origin.y.0 + offset.y.0);
126            fallback_rect
127        };
128        if fallback_rect.size.width.0 <= 0.0 || fallback_rect.size.height.0 <= 0.0 {
129            continue;
130        }
131
132        let alpha = if steps == 0 {
133            color.a
134        } else {
135            color.a * (shadow_alpha_weight(i) / alpha_weight_sum)
136        };
137        out.push(SceneOp::Quad {
138            order,
139            rect: fallback_rect,
140            background: Paint::Solid(color_with_alpha(color, alpha)).into(),
141            border: Edges::all(Px(0.0)),
142            border_paint: Paint::Solid(Color::TRANSPARENT).into(),
143            corner_radii: corners_expand(corner_radii, Px(layer_spread)),
144        });
145    }
146
147    out
148}
149
150impl SceneRecording {
151    /// Replay the deterministic quad fallback for a rounded-rect shadow primitive into this scene.
152    pub fn push_shadow_rrect_quad_fallback(&mut self, spec: ShadowRRectFallbackSpec) {
153        for op in shadow_rrect_fallback_quads(spec) {
154            self.push(op);
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn shadow_rrect_fallback_quads_keep_expected_profile() {
165        let ops = shadow_rrect_fallback_quads(ShadowRRectFallbackSpec {
166            order: DrawOrder(3),
167            rect: Rect::new(
168                Point::new(Px(20.0), Px(10.0)),
169                Size::new(Px(40.0), Px(24.0)),
170            ),
171            corner_radii: Corners::all(Px(6.0)),
172            offset: Point::new(Px(0.0), Px(2.0)),
173            spread: Px(1.0),
174            blur_radius: Px(4.0),
175            color: Color {
176                r: 0.0,
177                g: 0.0,
178                b: 0.0,
179                a: 0.18,
180            },
181        });
182
183        assert_eq!(ops.len(), 5);
184        let SceneOp::Quad {
185            rect: outer_rect,
186            background: outer_background,
187            corner_radii: outer_radii,
188            ..
189        } = ops[0]
190        else {
191            panic!("expected outer fallback quad");
192        };
193        let SceneOp::Quad {
194            rect: inner_rect,
195            background: inner_background,
196            corner_radii: inner_radii,
197            ..
198        } = ops[4]
199        else {
200            panic!("expected inner fallback quad");
201        };
202
203        let Paint::Solid(outer_color) = outer_background.paint else {
204            panic!("expected outer fallback color");
205        };
206        let Paint::Solid(inner_color) = inner_background.paint else {
207            panic!("expected inner fallback color");
208        };
209
210        assert!(outer_rect.size.width.0 > inner_rect.size.width.0);
211        assert!(outer_rect.size.height.0 > inner_rect.size.height.0);
212        assert!(outer_radii.top_left.0 > inner_radii.top_left.0);
213        assert!(outer_color.a < inner_color.a);
214    }
215
216    #[test]
217    fn push_shadow_rrect_quad_fallback_replays_ops_into_scene() {
218        let mut scene = Scene::default();
219        scene.push_shadow_rrect_quad_fallback(ShadowRRectFallbackSpec {
220            order: DrawOrder(0),
221            rect: Rect::new(Point::new(Px(0.0), Px(0.0)), Size::new(Px(12.0), Px(8.0))),
222            corner_radii: Corners::all(Px(4.0)),
223            offset: Point::new(Px(1.0), Px(2.0)),
224            spread: Px(0.0),
225            blur_radius: Px(2.0),
226            color: Color {
227                r: 0.0,
228                g: 0.0,
229                b: 0.0,
230                a: 0.12,
231            },
232        });
233
234        assert_eq!(scene.ops_len(), 3);
235        assert!(
236            scene
237                .ops()
238                .iter()
239                .all(|op| matches!(op, SceneOp::Quad { .. }))
240        );
241    }
242}