ailloli_ui_core/style/box_style.rs
1//! Paint-only box decoration plus border geometry shared with layout.
2
3use crate::Rect;
4
5use super::{Background, Border, BoxShadow, Opacity, Radius};
6
7/// Returns the smallest axis-aligned rectangle containing both inputs.
8fn union_rect(a: Rect, b: Rect) -> Rect {
9 let x0 = a.x.min(b.x);
10 let y0 = a.y.min(b.y);
11 let x1 = a.right().max(b.right());
12 let y1 = a.bottom().max(b.bottom());
13 Rect::new(x0, y0, x1 - x0, y1 - y0)
14}
15
16/// Visual box decoration: background, border, radius, shadows, opacity.
17///
18/// # Examples
19///
20/// ```
21/// use ailloli_ui_core::style::{Background, BoxStyle, Color};
22/// let style = BoxStyle::new().background(Background::color(Color::WHITE));
23/// assert!(style.background.is_visible());
24/// ```
25#[derive(Debug, Clone, PartialEq)]
26pub struct BoxStyle {
27 /// Optional solid background fill.
28 pub background: Background,
29 /// Border whose effective widths also participate in layout.
30 pub border: Border,
31 /// Per-corner outer radius in logical pixels.
32 pub radius: Radius,
33 /// Shadow layers in caller-provided paint order.
34 pub shadows: Vec<BoxShadow>,
35 /// Opacity multiplier applied to the decorated box.
36 pub opacity: Opacity,
37}
38
39impl Default for BoxStyle {
40 fn default() -> Self {
41 Self {
42 background: Background::None,
43 border: Border::default(),
44 radius: Radius::zero(),
45 shadows: Vec::new(),
46 opacity: Opacity::default(),
47 }
48 }
49}
50
51impl BoxStyle {
52 /// Creates a transparent, borderless, shadowless, fully opaque box style.
53 ///
54 /// # Examples
55 ///
56 /// ```
57 /// use ailloli_ui_core::style::BoxStyle;
58 /// assert!(BoxStyle::new().shadows.is_empty());
59 /// ```
60 pub fn new() -> Self {
61 Self::default()
62 }
63
64 /// Replaces the background configuration.
65 ///
66 /// # Examples
67 ///
68 /// ```
69 /// use ailloli_ui_core::style::{Background, BoxStyle, Color};
70 /// assert!(BoxStyle::new().background(Background::color(Color::WHITE)).background.is_visible());
71 /// ```
72 pub fn background(mut self, background: Background) -> Self {
73 self.background = background;
74 self
75 }
76
77 /// Replaces the complete border configuration.
78 ///
79 /// # Examples
80 ///
81 /// ```
82 /// use ailloli_ui_core::style::{Border, BoxStyle, Color};
83 /// assert!(BoxStyle::new().border(Border::new(1.0, Color::WHITE)).border.is_visible());
84 /// ```
85 pub fn border(mut self, border: Border) -> Self {
86 self.border = border;
87 self
88 }
89
90 /// Replaces all corner radii.
91 ///
92 /// # Examples
93 ///
94 /// ```
95 /// use ailloli_ui_core::style::{BoxStyle, Radius};
96 /// assert_eq!(BoxStyle::new().radius(Radius::uniform(4.0)).radius, Radius::uniform(4.0));
97 /// ```
98 pub fn radius(mut self, radius: impl Into<Radius>) -> Self {
99 self.radius = radius.into();
100 self
101 }
102
103 /// Appends one shadow after existing layers.
104 ///
105 /// # Examples
106 ///
107 /// ```
108 /// use ailloli_ui_core::style::{BoxShadow, BoxStyle};
109 /// assert_eq!(BoxStyle::new().shadow(BoxShadow::sm()).shadows.len(), 1);
110 /// ```
111 pub fn shadow(mut self, shadow: BoxShadow) -> Self {
112 self.shadows.push(shadow);
113 self
114 }
115
116 /// Replaces all shadow layers while preserving vector order.
117 ///
118 /// # Examples
119 ///
120 /// ```
121 /// use ailloli_ui_core::style::{BoxShadow, BoxStyle};
122 /// assert_eq!(BoxStyle::new().shadows(vec![BoxShadow::sm(), BoxShadow::md()]).shadows.len(), 2);
123 /// ```
124 pub fn shadows(mut self, shadows: Vec<BoxShadow>) -> Self {
125 self.shadows = shadows;
126 self
127 }
128
129 /// Removes every shadow layer.
130 ///
131 /// # Examples
132 ///
133 /// ```
134 /// use ailloli_ui_core::style::{BoxShadow, BoxStyle};
135 /// assert!(BoxStyle::new().shadow(BoxShadow::sm()).clear_shadows().shadows.is_empty());
136 /// ```
137 pub fn clear_shadows(mut self) -> Self {
138 self.shadows.clear();
139 self
140 }
141
142 /// Returns the union of `rect` and every shadow's paint bounds.
143 ///
144 /// Inset shadows do not expand the result. Background, border, radius, and
145 /// opacity do not change these conservative axis-aligned bounds.
146 ///
147 /// # Performance
148 ///
149 /// Runs in linear time over the number of shadow layers without allocation.
150 ///
151 /// # Examples
152 ///
153 /// ```
154 /// use ailloli_ui_core::Rect;
155 /// use ailloli_ui_core::style::{BoxShadow, BoxStyle};
156 /// let bounds = BoxStyle::new().shadow(BoxShadow::sm()).visual_bounds(Rect::new(0.0, 0.0, 10.0, 10.0));
157 /// assert!(bounds.w >= 10.0);
158 /// ```
159 pub fn visual_bounds(&self, rect: Rect) -> Rect {
160 self.shadows.iter().fold(rect, |bounds, shadow| {
161 union_rect(bounds, shadow.paint_bounds(rect))
162 })
163 }
164
165 /// Replaces the box opacity multiplier.
166 ///
167 /// # Examples
168 ///
169 /// ```
170 /// use ailloli_ui_core::style::{BoxStyle, Opacity};
171 /// assert_eq!(BoxStyle::new().opacity(Opacity::new(0.5)).opacity, Opacity(0.5));
172 /// ```
173 pub fn opacity(mut self, opacity: Opacity) -> Self {
174 self.opacity = opacity;
175 self
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 //! Covers shadow order/replacement and conservative visual bounds.
182
183 use super::*;
184 use crate::Color;
185
186 #[test]
187 fn shadow_builder_appends_and_clear_shadows_removes_all() {
188 let style = BoxStyle::new()
189 .shadow(BoxShadow::sm())
190 .shadow(BoxShadow::glow(Color::WHITE));
191
192 assert_eq!(style.shadows.len(), 2);
193 assert!(style.clone().clear_shadows().shadows.is_empty());
194 }
195
196 #[test]
197 fn shadows_builder_replaces_shadow_layers() {
198 let style = BoxStyle::new()
199 .shadow(BoxShadow::sm())
200 .shadows(vec![BoxShadow::md()]);
201
202 assert_eq!(style.shadows, vec![BoxShadow::md()]);
203 }
204
205 #[test]
206 fn visual_bounds_include_outer_shadows() {
207 let style = BoxStyle::new().shadow(BoxShadow::new(5.0, -2.0, 8.0, 1.0, Color::BLACK));
208
209 assert_eq!(
210 style.visual_bounds(Rect::new(10.0, 20.0, 30.0, 10.0)),
211 Rect::new(6.0, 9.0, 48.0, 28.0)
212 );
213 }
214}