ailloli_ui_core/style/
shadow.rs

1//! Paint-only outer and inset box-shadow geometry.
2
3use crate::{EdgeInsets, Offset, Rect};
4
5use super::Color;
6
7/// Paint-only shadow cast by a widget box.
8///
9/// # Examples
10///
11/// ```
12/// use ailloli_ui_core::style::{BoxShadow, Color};
13/// let shadow = BoxShadow::new(0.0, 2.0, 8.0, 0.0, Color::BLACK);
14/// assert_eq!(shadow.offset.y, 2.0);
15/// ```
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct BoxShadow {
18    /// Shadow translation from the source box in logical pixels.
19    pub offset: Offset,
20    /// Non-negative blur inflation radius in logical pixels.
21    pub blur_radius: f32,
22    /// Non-negative pre-blur shape expansion in logical pixels.
23    pub spread: f32,
24    /// Linear-RGBA shadow color.
25    pub color: Color,
26    /// `true` paints inside the source box; `false` paints an outer shadow.
27    pub inset: bool,
28}
29
30impl BoxShadow {
31    /// Creates an outer box shadow.
32    ///
33    /// Negative blur/spread values and NaN are clamped to zero through
34    /// floating-point `max`; positive infinity remains infinite. Offsets and
35    /// color are stored verbatim.
36    ///
37    /// # Examples
38    ///
39    /// ```
40    /// use ailloli_ui_core::style::{BoxShadow, Color};
41    /// assert_eq!(BoxShadow::new(0.0, 0.0, -1.0, -2.0, Color::BLACK).blur_radius, 0.0);
42    /// ```
43    pub fn new(offset_x: f32, offset_y: f32, blur_radius: f32, spread: f32, color: Color) -> Self {
44        Self {
45            offset: Offset::new(offset_x, offset_y),
46            blur_radius: blur_radius.max(0.0),
47            spread: spread.max(0.0),
48            color,
49            inset: false,
50        }
51    }
52
53    /// Creates an inset box shadow with the same normalization as [`Self::new`].
54    ///
55    /// The model is available even where a widget does not expose an inset
56    /// builder. Inset shadows never expand [`Self::paint_bounds`].
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use ailloli_ui_core::style::{BoxShadow, Color};
62    /// assert!(BoxShadow::inset(0.0, 1.0, 2.0, 0.0, Color::BLACK).inset);
63    /// ```
64    pub fn inset(
65        offset_x: f32,
66        offset_y: f32,
67        blur_radius: f32,
68        spread: f32,
69        color: Color,
70    ) -> Self {
71        Self {
72            inset: true,
73            ..Self::new(offset_x, offset_y, blur_radius, spread, color)
74        }
75    }
76
77    /// Returns the small elevation preset: `(0, 1)`, blur `2`, alpha `0.18`.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use ailloli_ui_core::style::BoxShadow;
83    /// assert_eq!(BoxShadow::sm().blur_radius, 2.0);
84    /// ```
85    pub fn sm() -> Self {
86        Self::new(
87            0.0,
88            1.0,
89            2.0,
90            0.0,
91            Color::from_f32_const(0.0, 0.0, 0.0, 0.18),
92        )
93    }
94
95    /// Returns the medium elevation preset: `(0, 4)`, blur `12`, alpha `0.24`.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use ailloli_ui_core::style::BoxShadow;
101    /// assert_eq!(BoxShadow::md().offset.y, 4.0);
102    /// ```
103    pub fn md() -> Self {
104        Self::new(
105            0.0,
106            4.0,
107            12.0,
108            0.0,
109            Color::from_f32_const(0.0, 0.0, 0.0, 0.24),
110        )
111    }
112
113    /// Returns the large elevation preset: `(0, 10)`, blur `24`, alpha `0.28`.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use ailloli_ui_core::style::BoxShadow;
119    /// assert_eq!(BoxShadow::lg().blur_radius, 24.0);
120    /// ```
121    pub fn lg() -> Self {
122        Self::new(
123            0.0,
124            10.0,
125            24.0,
126            0.0,
127            Color::from_f32_const(0.0, 0.0, 0.0, 0.28),
128        )
129    }
130
131    /// Returns the extra-large preset: `(0, 16)`, blur `40`, alpha `0.32`.
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// use ailloli_ui_core::style::BoxShadow;
137    /// assert_eq!(BoxShadow::xl().blur_radius, 40.0);
138    /// ```
139    pub fn xl() -> Self {
140        Self::new(
141            0.0,
142            16.0,
143            40.0,
144            0.0,
145            Color::from_f32_const(0.0, 0.0, 0.0, 0.32),
146        )
147    }
148
149    /// Returns a centered outer glow with an 18-logical-pixel blur radius.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use ailloli_ui_core::style::{BoxShadow, Color};
155    /// assert_eq!(BoxShadow::glow(Color::WHITE).blur_radius, 18.0);
156    /// ```
157    pub fn glow(color: Color) -> Self {
158        Self::new(0.0, 0.0, 18.0, 0.0, color)
159    }
160
161    /// Returns the translated and spread-inflated box before blur inflation.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use ailloli_ui_core::Rect;
167    /// use ailloli_ui_core::style::{BoxShadow, Color};
168    /// assert_eq!(BoxShadow::new(2.0, 3.0, 0.0, 1.0, Color::BLACK).shape_rect(Rect::new(0.0, 0.0, 10.0, 10.0)), Rect::new(1.0, 2.0, 12.0, 12.0));
169    /// ```
170    pub fn shape_rect(&self, rect: Rect) -> Rect {
171        rect.translate(self.offset)
172            .inflate(self.spread, self.spread)
173    }
174
175    /// Returns conservative axis-aligned bounds needed to paint the shadow.
176    ///
177    /// Inset shadows return the original box. Outer shadows add the offset,
178    /// spread, and blur radius.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use ailloli_ui_core::Rect;
184    /// use ailloli_ui_core::style::{BoxShadow, Color};
185    /// let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
186    /// assert_eq!(BoxShadow::inset(2.0, 3.0, 8.0, 1.0, Color::BLACK).paint_bounds(rect), rect);
187    /// ```
188    pub fn paint_bounds(&self, rect: Rect) -> Rect {
189        if self.inset {
190            return rect;
191        }
192        self.shape_rect(rect)
193            .inflate(self.blur_radius, self.blur_radius)
194    }
195
196    /// Returns conservative per-side outer inflation in logical pixels.
197    ///
198    /// Inset shadows return four zeros. The values combine blur, spread, and
199    /// only the outward component of each signed offset.
200    ///
201    /// # Examples
202    ///
203    /// ```
204    /// use ailloli_ui_core::EdgeInsets;
205    /// use ailloli_ui_core::style::{BoxShadow, Color};
206    /// assert_eq!(BoxShadow::inset(0.0, 0.0, 8.0, 1.0, Color::BLACK).paint_insets(), EdgeInsets::all(0.0));
207    /// ```
208    pub fn paint_insets(&self) -> EdgeInsets {
209        if self.inset {
210            return EdgeInsets::all(0.0);
211        }
212        let extent = self.blur_radius + self.spread;
213        EdgeInsets::new(
214            extent + (-self.offset.x).max(0.0),
215            extent + (-self.offset.y).max(0.0),
216            extent + self.offset.x.max(0.0),
217            extent + self.offset.y.max(0.0),
218        )
219    }
220}
221
222/// Deprecated compatibility alias for [`BoxShadow`].
223///
224/// # Examples
225///
226/// ```
227/// #![allow(deprecated)]
228/// use ailloli_ui_core::style::{BoxShadow, Shadow};
229/// let shadow: Shadow = BoxShadow::sm();
230/// assert_eq!(shadow, BoxShadow::sm());
231/// ```
232#[deprecated(note = "use BoxShadow instead")]
233pub type Shadow = BoxShadow;
234
235#[cfg(test)]
236mod tests {
237    //! Covers outer bounds/insets, inset bounds, and constructor clamping.
238
239    use super::*;
240
241    #[test]
242    fn paint_bounds_include_offset_blur_and_spread() {
243        let shadow = BoxShadow::new(5.0, -3.0, 10.0, 2.0, Color::BLACK);
244        assert_eq!(
245            shadow.paint_bounds(Rect::new(20.0, 30.0, 100.0, 50.0)),
246            Rect::new(13.0, 15.0, 124.0, 74.0)
247        );
248        assert_eq!(
249            shadow.paint_insets(),
250            EdgeInsets::new(12.0, 15.0, 17.0, 12.0)
251        );
252    }
253
254    #[test]
255    fn inset_shadow_does_not_expand_paint_bounds() {
256        let shadow = BoxShadow::inset(4.0, 4.0, 20.0, 2.0, Color::BLACK);
257        let rect = Rect::new(0.0, 0.0, 40.0, 20.0);
258        assert_eq!(shadow.paint_bounds(rect), rect);
259        assert_eq!(shadow.paint_insets(), EdgeInsets::all(0.0));
260    }
261
262    #[test]
263    fn constructors_clamp_negative_blur_and_spread() {
264        let shadow = BoxShadow::new(0.0, 0.0, -10.0, -2.0, Color::BLACK);
265        assert_eq!(shadow.blur_radius, 0.0);
266        assert_eq!(shadow.spread, 0.0);
267    }
268}