Skip to main content

cranpose_ui_graphics/
shadow.rs

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