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}