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}
48
49impl Default for Shadow {
50    fn default() -> Self {
51        Self {
52            radius: Dp(0.0),
53            spread: Dp(0.0),
54            offset: DpOffset::ZERO,
55            color: Color::BLACK,
56            brush: None,
57            alpha: 1.0,
58            blend_mode: BlendMode::SrcOver,
59        }
60    }
61}
62
63impl Shadow {
64    pub fn to_scope(&self, density: f32) -> ShadowScope {
65        ShadowScope {
66            radius: self.radius.to_px(density),
67            spread: self.spread.to_px(density),
68            offset: self.offset.to_px(density),
69            color: self.color,
70            brush: self.brush.clone(),
71            alpha: self.alpha,
72            blend_mode: self.blend_mode,
73        }
74    }
75}
76
77/// Pixel-space shadow scope used by block-based APIs.
78#[derive(Clone, Debug, PartialEq)]
79pub struct ShadowScope {
80    pub radius: f32,
81    pub spread: f32,
82    pub offset: Point,
83    pub color: Color,
84    pub brush: Option<Brush>,
85    pub alpha: f32,
86    pub blend_mode: BlendMode,
87}
88
89impl Default for ShadowScope {
90    fn default() -> Self {
91        Self {
92            radius: 0.0,
93            spread: 0.0,
94            offset: Point::ZERO,
95            color: Color::BLACK,
96            brush: None,
97            alpha: 1.0,
98            blend_mode: BlendMode::SrcOver,
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn dp_offset_converts_to_px() {
109        let offset = DpOffset::new(Dp(4.0), Dp(-2.5));
110        let px = offset.to_px(2.0);
111        assert_eq!(px, Point::new(8.0, -5.0));
112    }
113
114    #[test]
115    fn shadow_default_matches_black_src_over() {
116        let shadow = Shadow::default();
117        assert_eq!(shadow.radius, Dp(0.0));
118        assert_eq!(shadow.spread, Dp(0.0));
119        assert_eq!(shadow.offset, DpOffset::ZERO);
120        assert_eq!(shadow.color, Color::BLACK);
121        assert_eq!(shadow.brush, None);
122        assert!((shadow.alpha - 1.0).abs() < 1e-6);
123        assert_eq!(shadow.blend_mode, BlendMode::SrcOver);
124    }
125
126    #[test]
127    fn shadow_to_scope_uses_density() {
128        let shadow = Shadow {
129            radius: Dp(5.0),
130            spread: Dp(2.0),
131            offset: DpOffset::new(Dp(-1.0), Dp(3.0)),
132            color: Color::from_rgba_u8(10, 20, 30, 255),
133            brush: None,
134            alpha: 0.7,
135            blend_mode: BlendMode::Overlay,
136        };
137        let scope = shadow.to_scope(2.0);
138        assert!((scope.radius - 10.0).abs() < 1e-6);
139        assert!((scope.spread - 4.0).abs() < 1e-6);
140        assert_eq!(scope.offset, Point::new(-2.0, 6.0));
141        assert_eq!(scope.color, Color::from_rgba_u8(10, 20, 30, 255));
142        assert!((scope.alpha - 0.7).abs() < 1e-6);
143        assert_eq!(scope.blend_mode, BlendMode::Overlay);
144    }
145}