Skip to main content

cranpose_ui_graphics/
shadow.rs

1//! Shadow configuration models used by drop/inner shadow modifiers.
2
3use crate::Point;
4use crate::{BlendMode, Brush, Color, Dp};
5
6/// Density-independent offset for shadows.
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct DpOffset {
9    pub x: Dp,
10    pub y: Dp,
11}
12
13impl DpOffset {
14    pub const fn new(x: Dp, y: Dp) -> Self {
15        Self { x, y }
16    }
17
18    pub const ZERO: DpOffset = DpOffset {
19        x: Dp(0.0),
20        y: Dp(0.0),
21    };
22
23    pub fn to_px(self, density: f32) -> Point {
24        Point::new(self.x.to_px(density), self.y.to_px(density))
25    }
26}
27
28impl Default for DpOffset {
29    fn default() -> Self {
30        Self::ZERO
31    }
32}
33
34/// Static shadow configuration.
35///
36/// This mirrors Compose's static `Shadow` object where radius/spread/offset
37/// are provided in density-independent units and converted to pixels at draw time.
38#[derive(Clone, Debug, PartialEq)]
39pub struct Shadow {
40    pub radius: Dp,
41    pub spread: Dp,
42    pub offset: DpOffset,
43    pub color: Color,
44    pub brush: Option<Brush>,
45    pub alpha: f32,
46    pub blend_mode: BlendMode,
47    /// Knocks the unoffset, unspread shape out of the shadow silhouette
48    /// before blurring, leaving shadow only OUTSIDE the element. Required
49    /// for translucent surfaces (glass): a backdrop-sampling material must
50    /// not refract its own shadow.
51    pub cutout: bool,
52}
53
54impl Default for Shadow {
55    fn default() -> Self {
56        Self {
57            radius: Dp(0.0),
58            spread: Dp(0.0),
59            offset: DpOffset::ZERO,
60            color: Color::BLACK,
61            brush: None,
62            alpha: 1.0,
63            blend_mode: BlendMode::SrcOver,
64            cutout: false,
65        }
66    }
67}
68
69impl Shadow {
70    pub fn to_scope(&self, density: f32) -> ShadowScope {
71        ShadowScope {
72            radius: self.radius.to_px(density),
73            spread: self.spread.to_px(density),
74            offset: self.offset.to_px(density),
75            color: self.color,
76            brush: self.brush.clone(),
77            alpha: self.alpha,
78            blend_mode: self.blend_mode,
79            cutout: self.cutout,
80        }
81    }
82}
83
84/// Pixel-space shadow scope used by block-based APIs.
85#[derive(Clone, Debug, PartialEq)]
86pub struct ShadowScope {
87    pub radius: f32,
88    pub spread: f32,
89    pub offset: Point,
90    pub color: Color,
91    pub brush: Option<Brush>,
92    pub alpha: f32,
93    pub blend_mode: BlendMode,
94    /// See [`Shadow::cutout`]: knock the element's own shape out of the
95    /// silhouette so the shadow exists only outside it.
96    pub cutout: bool,
97}
98
99impl Default for ShadowScope {
100    fn default() -> Self {
101        Self {
102            radius: 0.0,
103            spread: 0.0,
104            offset: Point::ZERO,
105            color: Color::BLACK,
106            brush: None,
107            alpha: 1.0,
108            blend_mode: BlendMode::SrcOver,
109            cutout: false,
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn dp_offset_converts_to_px() {
120        let offset = DpOffset::new(Dp(4.0), Dp(-2.5));
121        let px = offset.to_px(2.0);
122        assert_eq!(px, Point::new(8.0, -5.0));
123    }
124
125    #[test]
126    fn shadow_default_matches_black_src_over() {
127        let shadow = Shadow::default();
128        assert_eq!(shadow.radius, Dp(0.0));
129        assert_eq!(shadow.spread, Dp(0.0));
130        assert_eq!(shadow.offset, DpOffset::ZERO);
131        assert_eq!(shadow.color, Color::BLACK);
132        assert_eq!(shadow.brush, None);
133        assert!((shadow.alpha - 1.0).abs() < 1e-6);
134        assert_eq!(shadow.blend_mode, BlendMode::SrcOver);
135    }
136
137    #[test]
138    fn shadow_to_scope_uses_density() {
139        let shadow = Shadow {
140            radius: Dp(5.0),
141            spread: Dp(2.0),
142            offset: DpOffset::new(Dp(-1.0), Dp(3.0)),
143            color: Color::from_rgba_u8(10, 20, 30, 255),
144            brush: None,
145            alpha: 0.7,
146            blend_mode: BlendMode::Overlay,
147            cutout: false,
148        };
149        let scope = shadow.to_scope(2.0);
150        assert!((scope.radius - 10.0).abs() < 1e-6);
151        assert!((scope.spread - 4.0).abs() < 1e-6);
152        assert_eq!(scope.offset, Point::new(-2.0, 6.0));
153        assert_eq!(scope.color, Color::from_rgba_u8(10, 20, 30, 255));
154        assert!((scope.alpha - 0.7).abs() < 1e-6);
155        assert_eq!(scope.blend_mode, BlendMode::Overlay);
156    }
157}