ailloli_ui_core/style/
border.rs

1//! Layout-aware per-edge borders and inner-box geometry.
2
3use crate::geometry::{EdgeInsets, Rect};
4
5use super::{Color, Radius};
6
7/// Per-edge border colors.
8///
9/// # Examples
10///
11/// ```
12/// use ailloli_ui_core::style::{Color, EdgeColors};
13/// assert_eq!(EdgeColors::all(Color::WHITE).left, Color::WHITE);
14/// ```
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct EdgeColors {
17    /// Left edge color.
18    pub left: Color,
19    /// Top edge color.
20    pub top: Color,
21    /// Right edge color.
22    pub right: Color,
23    /// Bottom edge color.
24    pub bottom: Color,
25}
26
27impl EdgeColors {
28    /// Per-side colors: left, top, right, bottom.
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// use ailloli_ui_core::style::{Color, EdgeColors};
34    /// assert_eq!(EdgeColors::new(Color::BLACK, Color::WHITE, Color::BLACK, Color::WHITE).top, Color::WHITE);
35    /// ```
36    pub const fn new(left: Color, top: Color, right: Color, bottom: Color) -> Self {
37        Self {
38            left,
39            top,
40            right,
41            bottom,
42        }
43    }
44
45    /// Same color on all sides.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use ailloli_ui_core::style::{Color, EdgeColors};
51    /// assert_eq!(EdgeColors::all(Color::WHITE).bottom, Color::WHITE);
52    /// ```
53    pub const fn all(color: Color) -> Self {
54        Self::new(color, color, color, color)
55    }
56}
57
58impl Default for EdgeColors {
59    fn default() -> Self {
60        Self::all(Color::TRANSPARENT)
61    }
62}
63
64/// Border stroke style.
65///
66/// Possible values are none, solid, dashed, and dotted.
67///
68/// # Examples
69///
70/// ```
71/// use ailloli_ui_core::style::BorderStyle;
72/// assert_eq!(BorderStyle::default(), BorderStyle::None);
73/// ```
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
75pub enum BorderStyle {
76    /// No painted border and no border contribution to layout; this is default.
77    #[default]
78    None,
79    /// Continuous stroke.
80    Solid,
81    /// Dashed stroke interpreted by the active renderer.
82    Dashed,
83    /// Dotted stroke interpreted by the active renderer.
84    Dotted,
85}
86
87/// Layout-aware box border.
88///
89/// # Examples
90///
91/// ```
92/// use ailloli_ui_core::style::{Border, Color};
93/// assert!(Border::new(1.0, Color::WHITE).is_visible());
94/// ```
95#[derive(Debug, Clone, Copy, PartialEq)]
96pub struct Border {
97    /// Requested per-edge thicknesses in logical pixels.
98    pub widths: EdgeInsets,
99    /// Per-edge linear-RGBA colors.
100    pub colors: EdgeColors,
101    /// Stroke pattern; [`BorderStyle::None`] disables all layout thickness.
102    pub style: BorderStyle,
103}
104
105impl Border {
106    /// Creates a uniform solid border with the given width and color.
107    ///
108    /// Width is stored verbatim; layout and visibility treat negative values as
109    /// zero through [`Self::layout_widths`].
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use ailloli_ui_core::style::{Border, BorderStyle, Color};
115    /// assert_eq!(Border::new(2.0, Color::WHITE).style, BorderStyle::Solid);
116    /// ```
117    pub const fn new(width: f32, color: Color) -> Self {
118        Self {
119            widths: EdgeInsets::all(width),
120            colors: EdgeColors::all(color),
121            style: BorderStyle::Solid,
122        }
123    }
124
125    /// Creates a transparent zero-width [`BorderStyle::None`] border.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use ailloli_ui_core::style::Border;
131    /// assert!(!Border::none().is_visible());
132    /// ```
133    pub const fn none() -> Self {
134        Self {
135            widths: EdgeInsets::all(0.0),
136            colors: EdgeColors::all(Color::TRANSPARENT),
137            style: BorderStyle::None,
138        }
139    }
140
141    /// Alias for [`Self::new`].
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use ailloli_ui_core::style::{Border, Color};
147    /// assert_eq!(Border::solid(2.0, Color::WHITE), Border::new(2.0, Color::WHITE));
148    /// ```
149    pub const fn solid(width: f32, color: Color) -> Self {
150        Self::new(width, color)
151    }
152
153    /// Replaces all edge widths and enables [`BorderStyle::Solid`] when positive.
154    ///
155    /// Zero or negative widths do not disable an already enabled style.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use ailloli_ui_core::style::Border;
161    /// assert_eq!(Border::none().with_width(2.0).layout_widths().left, 2.0);
162    /// ```
163    pub fn with_width(mut self, width: f32) -> Self {
164        self.widths = EdgeInsets::all(width);
165        if width > 0.0 && self.style == BorderStyle::None {
166            self.style = BorderStyle::Solid;
167        }
168        self
169    }
170
171    /// Replaces all edge colors and may enable a previously disabled border.
172    ///
173    /// Auto-enabling requires positive alpha and a positive sum of the raw
174    /// widths. A transparent color does not disable an existing style.
175    ///
176    /// # Examples
177    ///
178    /// ```
179    /// use ailloli_ui_core::style::{Border, Color};
180    /// assert_eq!(Border::new(1.0, Color::BLACK).with_color(Color::WHITE).uniform_color(), Some(Color::WHITE));
181    /// ```
182    pub fn with_color(mut self, color: Color) -> Self {
183        self.colors = EdgeColors::all(color);
184        if color.a > 0.0
185            && self.style == BorderStyle::None
186            && self.widths.horizontal() + self.widths.vertical() > 0.0
187        {
188            self.style = BorderStyle::Solid;
189        }
190        self
191    }
192
193    /// Replaces the left width/color and enables a disabled style if width is positive.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use ailloli_ui_core::style::{Border, Color};
199    /// assert_eq!(Border::none().with_left(2.0, Color::WHITE).layout_widths().left, 2.0);
200    /// ```
201    pub fn with_left(mut self, width: f32, color: Color) -> Self {
202        self.widths.left = width;
203        self.colors.left = color;
204        if width > 0.0 && self.style == BorderStyle::None {
205            self.style = BorderStyle::Solid;
206        }
207        self
208    }
209
210    /// Replaces the top width/color and enables a disabled style if width is positive.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use ailloli_ui_core::style::{Border, Color};
216    /// assert_eq!(Border::none().with_top(2.0, Color::WHITE).layout_widths().top, 2.0);
217    /// ```
218    pub fn with_top(mut self, width: f32, color: Color) -> Self {
219        self.widths.top = width;
220        self.colors.top = color;
221        if width > 0.0 && self.style == BorderStyle::None {
222            self.style = BorderStyle::Solid;
223        }
224        self
225    }
226
227    /// Replaces the right width/color and enables a disabled style if width is positive.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use ailloli_ui_core::style::{Border, Color};
233    /// assert_eq!(Border::none().with_right(2.0, Color::WHITE).layout_widths().right, 2.0);
234    /// ```
235    pub fn with_right(mut self, width: f32, color: Color) -> Self {
236        self.widths.right = width;
237        self.colors.right = color;
238        if width > 0.0 && self.style == BorderStyle::None {
239            self.style = BorderStyle::Solid;
240        }
241        self
242    }
243
244    /// Replaces the bottom width/color and enables a disabled style if width is positive.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// use ailloli_ui_core::style::{Border, Color};
250    /// assert_eq!(Border::none().with_bottom(2.0, Color::WHITE).layout_widths().bottom, 2.0);
251    /// ```
252    pub fn with_bottom(mut self, width: f32, color: Color) -> Self {
253        self.widths.bottom = width;
254        self.colors.bottom = color;
255        if width > 0.0 && self.style == BorderStyle::None {
256            self.style = BorderStyle::Solid;
257        }
258        self
259    }
260
261    /// Returns non-negative per-edge layout thicknesses in logical pixels.
262    ///
263    /// [`BorderStyle::None`] returns four zeros regardless of stored widths.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// use ailloli_ui_core::style::{Border, Color};
269    /// assert_eq!(Border::new(-2.0, Color::WHITE).layout_widths().left, 0.0);
270    /// ```
271    pub fn layout_widths(&self) -> EdgeInsets {
272        if self.style == BorderStyle::None {
273            return EdgeInsets::all(0.0);
274        }
275        EdgeInsets::new(
276            self.widths.left.max(0.0),
277            self.widths.top.max(0.0),
278            self.widths.right.max(0.0),
279            self.widths.bottom.max(0.0),
280        )
281    }
282
283    /// Returns whether at least one edge has positive width and alpha.
284    ///
285    /// A [`BorderStyle::None`] border is never visible.
286    ///
287    /// # Examples
288    ///
289    /// ```
290    /// use ailloli_ui_core::style::{Border, Color};
291    /// assert!(Border::new(1.0, Color::WHITE).is_visible());
292    /// ```
293    pub fn is_visible(&self) -> bool {
294        if self.style == BorderStyle::None {
295            return false;
296        }
297        let w = self.layout_widths();
298        (w.left > 0.0 && self.colors.left.a > 0.0)
299            || (w.top > 0.0 && self.colors.top.a > 0.0)
300            || (w.right > 0.0 && self.colors.right.a > 0.0)
301            || (w.bottom > 0.0 && self.colors.bottom.a > 0.0)
302    }
303
304    /// Returns whether effective widths and all four colors are exactly equal.
305    ///
306    /// This can be `true` for an invisible uniform border.
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// use ailloli_ui_core::style::Border;
312    /// assert!(Border::none().is_uniform());
313    /// ```
314    pub fn is_uniform(&self) -> bool {
315        self.uniform_width().is_some()
316            && self.colors.left == self.colors.top
317            && self.colors.top == self.colors.right
318            && self.colors.right == self.colors.bottom
319    }
320
321    /// Returns one effective width when all four layout widths are exactly equal.
322    ///
323    /// [`BorderStyle::None`] therefore returns `Some(0.0)`.
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// use ailloli_ui_core::style::{Border, Color};
329    /// assert_eq!(Border::new(2.0, Color::WHITE).uniform_width(), Some(2.0));
330    /// ```
331    pub fn uniform_width(&self) -> Option<f32> {
332        let w = self.layout_widths();
333        if w.left == w.top && w.top == w.right && w.right == w.bottom {
334            Some(w.left)
335        } else {
336            None
337        }
338    }
339
340    /// Returns one color when all four stored colors are exactly equal.
341    ///
342    /// # Examples
343    ///
344    /// ```
345    /// use ailloli_ui_core::style::{Border, Color};
346    /// assert_eq!(Border::new(2.0, Color::WHITE).uniform_color(), Some(Color::WHITE));
347    /// ```
348    pub fn uniform_color(&self) -> Option<Color> {
349        if self.colors.left == self.colors.top
350            && self.colors.top == self.colors.right
351            && self.colors.right == self.colors.bottom
352        {
353            Some(self.colors.left)
354        } else {
355            None
356        }
357    }
358
359    /// Insets a rectangle by effective border widths.
360    ///
361    /// The returned width and height are floored at zero; its origin still
362    /// advances by the left and top widths when the border exceeds the box.
363    ///
364    /// # Examples
365    ///
366    /// ```
367    /// use ailloli_ui_core::Rect;
368    /// use ailloli_ui_core::style::{Border, Color};
369    /// assert_eq!(Border::new(2.0, Color::WHITE).deflate_rect(Rect::new(0.0, 0.0, 10.0, 10.0)), Rect::new(2.0, 2.0, 6.0, 6.0));
370    /// ```
371    pub fn deflate_rect(&self, rect: Rect) -> Rect {
372        let w = self.layout_widths();
373        Rect::new(
374            rect.x + w.left,
375            rect.y + w.top,
376            (rect.w - w.left - w.right).max(0.0),
377            (rect.h - w.top - w.bottom).max(0.0),
378        )
379    }
380
381    /// Derives non-negative radii for the border's inner contour.
382    ///
383    /// Each outer corner subtracts the greater adjacent effective edge width.
384    ///
385    /// # Examples
386    ///
387    /// ```
388    /// use ailloli_ui_core::style::{Border, Color, Radius};
389    /// assert_eq!(Border::new(2.0, Color::WHITE).inner_radius(Radius::uniform(5.0)), Radius::uniform(3.0));
390    /// ```
391    pub fn inner_radius(&self, radius: Radius) -> Radius {
392        let w = self.layout_widths();
393        Radius::per_corner(
394            (radius.tl - w.left.max(w.top)).max(0.0),
395            (radius.tr - w.right.max(w.top)).max(0.0),
396            (radius.br - w.right.max(w.bottom)).max(0.0),
397            (radius.bl - w.left.max(w.bottom)).max(0.0),
398        )
399    }
400}
401
402impl Default for Border {
403    fn default() -> Self {
404        Self::none()
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    //! Covers uniform construction, effective visibility, and inner geometry.
411
412    use super::*;
413
414    #[test]
415    fn new_creates_uniform_solid_border() {
416        let border = Border::new(2.0, Color::WHITE);
417
418        assert_eq!(border.style, BorderStyle::Solid);
419        assert_eq!(border.widths, EdgeInsets::all(2.0));
420        assert_eq!(border.colors, EdgeColors::all(Color::WHITE));
421        assert!(border.is_visible());
422        assert!(border.is_uniform());
423        assert_eq!(border.uniform_width(), Some(2.0));
424    }
425
426    #[test]
427    fn none_and_zero_width_are_not_visible() {
428        assert!(!Border::none().is_visible());
429        assert!(!Border::new(0.0, Color::WHITE).is_visible());
430        assert!(!Border::new(1.0, Color::TRANSPARENT).is_visible());
431    }
432
433    #[test]
434    fn deflate_rect_and_inner_radius_clamp() {
435        let border = Border {
436            widths: EdgeInsets::new(2.0, 4.0, 6.0, 8.0),
437            colors: EdgeColors::all(Color::WHITE),
438            style: BorderStyle::Solid,
439        };
440
441        assert_eq!(
442            border.deflate_rect(Rect::new(10.0, 20.0, 100.0, 80.0)),
443            Rect::new(12.0, 24.0, 92.0, 68.0)
444        );
445        assert_eq!(
446            border.inner_radius(Radius::per_corner(12.0, 3.0, 9.0, 20.0)),
447            Radius::per_corner(8.0, 0.0, 1.0, 12.0)
448        );
449    }
450}