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}