Skip to main content

azul_css/props/style/
border.rs

1//! CSS properties for border style, width, and color.
2
3use alloc::string::{String, ToString};
4use core::fmt;
5use crate::corety::AzString;
6
7#[cfg(feature = "parser")]
8use crate::props::basic::{color::parse_css_color, pixel::parse_pixel_value};
9use crate::{
10    css::PrintAsCssValue,
11    props::{
12        basic::{
13            color::{ColorU, CssColorParseError, CssColorParseErrorOwned},
14            pixel::{
15                CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue,
16                MEDIUM_BORDER_THICKNESS, THICK_BORDER_THICKNESS, THIN_BORDER_THICKNESS,
17            },
18        },
19        macros::PixelValueTaker,
20    },
21};
22
23/// Style of a `border`: solid, double, dash, ridge, etc.
24#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
25#[repr(C)]
26// +spec:box-model:28fad6 - Border style variants including groove/ridge/inset/outset for separated/collapsing border models
27#[derive(Default)]
28pub enum BorderStyle {
29    #[default]
30    None,
31    Solid,
32    Double,
33    Dotted,
34    Dashed,
35    Hidden,
36    Groove,
37    Ridge,
38    Inset,
39    Outset,
40}
41
42
43impl fmt::Display for BorderStyle {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(
46            f,
47            "{}",
48            match self {
49                Self::None => "none",
50                Self::Solid => "solid",
51                Self::Double => "double",
52                Self::Dotted => "dotted",
53                Self::Dashed => "dashed",
54                Self::Hidden => "hidden",
55                Self::Groove => "groove",
56                Self::Ridge => "ridge",
57                Self::Inset => "inset",
58                Self::Outset => "outset",
59            }
60        )
61    }
62}
63
64impl PrintAsCssValue for BorderStyle {
65    fn print_as_css_value(&self) -> String {
66        self.to_string()
67    }
68}
69
70/// Internal macro to reduce boilerplate for defining border-top, -right, -bottom, -left properties.
71macro_rules! define_border_side_property {
72    // For types that have a simple inner value and can be formatted with Display
73    ($struct_name:ident, $inner_type:ty, $default:expr) => {
74        #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
75        #[repr(C)]
76        pub struct $struct_name {
77            pub inner: $inner_type,
78        }
79        impl ::core::fmt::Debug for $struct_name {
80            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
81                write!(f, "{}", self.inner)
82            }
83        }
84        impl Default for $struct_name {
85            fn default() -> Self {
86                Self { inner: $default }
87            }
88        }
89    };
90    // Specialization for ColorU
91    ($struct_name:ident,ColorU) => {
92        #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
93        #[repr(C)]
94        pub struct $struct_name {
95            pub inner: ColorU,
96        }
97        impl ::core::fmt::Debug for $struct_name {
98            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
99                write!(f, "{}", self.inner.to_hash())
100            }
101        }
102        // The default border color is 'currentcolor', but for simplicity we default to BLACK.
103        // The style property resolver should handle the 'currentcolor' logic.
104        impl Default for $struct_name {
105            fn default() -> Self {
106                Self {
107                    inner: ColorU::BLACK,
108                }
109            }
110        }
111        impl $struct_name {
112            #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
113                Self {
114                    inner: self.inner.interpolate(&other.inner, t),
115                }
116            }
117        }
118    };
119    // NOTE: no separate `PixelValue` specialization arm — the generic
120    // `($struct_name, $inner_type:ty, $default)` arm above already matches
121    // `define_border_side_property!(.., PixelValue, ..)` (PixelValue is a `:ty`),
122    // so a 3-arg PixelValue arm here would be unreachable (unused_macro_rules).
123}
124
125// --- Individual Property Structs ---
126
127// +spec:box-model:8c49fe - Border style properties (none, solid, double, dashed, etc.) and border color defaulting to element's color
128// Border Style (border-*-style)
129/// CSS `border-top-style` property (e.g. `solid`, `dashed`, `none`).
130define_border_side_property!(StyleBorderTopStyle, BorderStyle, BorderStyle::None);
131/// CSS `border-right-style` property (e.g. `solid`, `dashed`, `none`).
132define_border_side_property!(StyleBorderRightStyle, BorderStyle, BorderStyle::None);
133/// CSS `border-bottom-style` property (e.g. `solid`, `dashed`, `none`).
134define_border_side_property!(StyleBorderBottomStyle, BorderStyle, BorderStyle::None);
135/// CSS `border-left-style` property (e.g. `solid`, `dashed`, `none`).
136define_border_side_property!(StyleBorderLeftStyle, BorderStyle, BorderStyle::None);
137
138// Formatting implementations for border side style values
139impl crate::codegen::format::FormatAsRustCode for StyleBorderTopStyle {
140    fn format_as_rust_code(&self, tabs: usize) -> String {
141        format!(
142            "StyleBorderTopStyle {{ inner: {} }}",
143            &self.inner.format_as_rust_code(tabs)
144        )
145    }
146}
147
148impl crate::codegen::format::FormatAsRustCode for StyleBorderRightStyle {
149    fn format_as_rust_code(&self, tabs: usize) -> String {
150        format!(
151            "StyleBorderRightStyle {{ inner: {} }}",
152            &self.inner.format_as_rust_code(tabs)
153        )
154    }
155}
156
157impl crate::codegen::format::FormatAsRustCode for StyleBorderLeftStyle {
158    fn format_as_rust_code(&self, tabs: usize) -> String {
159        format!(
160            "StyleBorderLeftStyle {{ inner: {} }}",
161            &self.inner.format_as_rust_code(tabs)
162        )
163    }
164}
165
166impl crate::codegen::format::FormatAsRustCode for StyleBorderBottomStyle {
167    fn format_as_rust_code(&self, tabs: usize) -> String {
168        format!(
169            "StyleBorderBottomStyle {{ inner: {} }}",
170            &self.inner.format_as_rust_code(tabs)
171        )
172    }
173}
174
175// Border Color (border-*-color)
176/// CSS `border-top-color` property. Defaults to `ColorU::BLACK`.
177define_border_side_property!(StyleBorderTopColor, ColorU);
178/// CSS `border-right-color` property. Defaults to `ColorU::BLACK`.
179define_border_side_property!(StyleBorderRightColor, ColorU);
180/// CSS `border-bottom-color` property. Defaults to `ColorU::BLACK`.
181define_border_side_property!(StyleBorderBottomColor, ColorU);
182/// CSS `border-left-color` property. Defaults to `ColorU::BLACK`.
183define_border_side_property!(StyleBorderLeftColor, ColorU);
184
185// Border Width (border-*-width)
186// The default width is 'medium', which corresponds to 3px.
187// Import from pixel.rs for consistency.
188/// CSS `border-top-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
189define_border_side_property!(LayoutBorderTopWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
190/// CSS `border-right-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
191define_border_side_property!(LayoutBorderRightWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
192/// CSS `border-bottom-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
193define_border_side_property!(LayoutBorderBottomWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
194/// CSS `border-left-width` property. Defaults to `MEDIUM_BORDER_THICKNESS` (3px).
195define_border_side_property!(LayoutBorderLeftWidth, PixelValue, MEDIUM_BORDER_THICKNESS);
196
197macro_rules! impl_border_width_helpers {
198    ($($t:ty),+) => { $(
199        impl $t {
200            #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
201                Self { inner: self.inner.interpolate(&other.inner, t) }
202            }
203            #[must_use] pub const fn const_px(value: isize) -> Self {
204                Self { inner: PixelValue::const_px(value) }
205            }
206        }
207    )+ };
208}
209
210impl_border_width_helpers!(
211    LayoutBorderTopWidth,
212    LayoutBorderRightWidth,
213    LayoutBorderBottomWidth,
214    LayoutBorderLeftWidth
215);
216
217/// Represents the three components of a border shorthand property, used as an intermediate
218/// representation during parsing.
219#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
220pub struct StyleBorderSide {
221    pub border_width: PixelValue,
222    pub border_style: BorderStyle,
223    pub border_color: ColorU,
224}
225
226// --- PARSERS ---
227
228// -- BorderStyle Parser --
229
230#[cfg(feature = "parser")]
231#[derive(Clone, PartialEq, Eq)]
232pub enum CssBorderStyleParseError<'a> {
233    InvalidStyle(&'a str),
234}
235
236#[cfg(feature = "parser")]
237impl_debug_as_display!(CssBorderStyleParseError<'a>);
238#[cfg(feature = "parser")]
239impl_display! { CssBorderStyleParseError<'a>, {
240    InvalidStyle(val) => format!("Invalid border style: \"{}\"", val),
241}}
242
243#[cfg(feature = "parser")]
244#[derive(Debug, Clone, PartialEq, Eq)]
245#[repr(C, u8)]
246pub enum CssBorderStyleParseErrorOwned {
247    InvalidStyle(AzString),
248}
249
250#[cfg(feature = "parser")]
251impl CssBorderStyleParseError<'_> {
252    #[must_use] pub fn to_contained(&self) -> CssBorderStyleParseErrorOwned {
253        match self {
254            CssBorderStyleParseError::InvalidStyle(s) => {
255                CssBorderStyleParseErrorOwned::InvalidStyle((*s).to_string().into())
256            }
257        }
258    }
259}
260
261#[cfg(feature = "parser")]
262impl CssBorderStyleParseErrorOwned {
263    #[must_use] pub fn to_shared(&self) -> CssBorderStyleParseError<'_> {
264        match self {
265            Self::InvalidStyle(s) => {
266                CssBorderStyleParseError::InvalidStyle(s.as_str())
267            }
268        }
269    }
270}
271
272#[cfg(feature = "parser")]
273/// # Errors
274///
275/// Returns an error if `input` is not a valid CSS `border-style` value.
276pub fn parse_border_style(input: &str) -> Result<BorderStyle, CssBorderStyleParseError<'_>> {
277    match input.trim() {
278        "none" => Ok(BorderStyle::None),
279        "solid" => Ok(BorderStyle::Solid),
280        "double" => Ok(BorderStyle::Double),
281        "dotted" => Ok(BorderStyle::Dotted),
282        "dashed" => Ok(BorderStyle::Dashed),
283        "hidden" => Ok(BorderStyle::Hidden),
284        "groove" => Ok(BorderStyle::Groove),
285        "ridge" => Ok(BorderStyle::Ridge),
286        "inset" => Ok(BorderStyle::Inset),
287        "outset" => Ok(BorderStyle::Outset),
288        _ => Err(CssBorderStyleParseError::InvalidStyle(input)),
289    }
290}
291
292// -- Shorthand Parser (for `border`, `border-top`, etc.) --
293
294#[cfg(feature = "parser")]
295#[derive(Clone, PartialEq)]
296pub enum CssBorderSideParseError<'a> {
297    InvalidDeclaration(&'a str),
298    Width(CssPixelValueParseError<'a>),
299    Style(CssBorderStyleParseError<'a>),
300    Color(CssColorParseError<'a>),
301}
302
303#[cfg(feature = "parser")]
304impl_debug_as_display!(CssBorderSideParseError<'a>);
305#[cfg(feature = "parser")]
306impl_display! { CssBorderSideParseError<'a>, {
307    InvalidDeclaration(e) => format!("Invalid border declaration: \"{}\"", e),
308    Width(e) => format!("Invalid border-width component: {}", e),
309    Style(e) => format!("Invalid border-style component: {}", e),
310    Color(e) => format!("Invalid border-color component: {}", e),
311}}
312
313#[cfg(feature = "parser")]
314impl_from!(CssPixelValueParseError<'a>, CssBorderSideParseError::Width);
315#[cfg(feature = "parser")]
316impl_from!(CssBorderStyleParseError<'a>, CssBorderSideParseError::Style);
317#[cfg(feature = "parser")]
318impl_from!(CssColorParseError<'a>, CssBorderSideParseError::Color);
319
320#[cfg(feature = "parser")]
321#[derive(Debug, Clone, PartialEq)]
322#[repr(C, u8)]
323pub enum CssBorderSideParseErrorOwned {
324    InvalidDeclaration(AzString),
325    Width(CssPixelValueParseErrorOwned),
326    Style(CssBorderStyleParseErrorOwned),
327    Color(CssColorParseErrorOwned),
328}
329
330#[cfg(feature = "parser")]
331impl CssBorderSideParseError<'_> {
332    #[must_use] pub fn to_contained(&self) -> CssBorderSideParseErrorOwned {
333        match self {
334            CssBorderSideParseError::InvalidDeclaration(s) => {
335                CssBorderSideParseErrorOwned::InvalidDeclaration((*s).to_string().into())
336            }
337            CssBorderSideParseError::Width(e) => {
338                CssBorderSideParseErrorOwned::Width(e.to_contained())
339            }
340            CssBorderSideParseError::Style(e) => {
341                CssBorderSideParseErrorOwned::Style(e.to_contained())
342            }
343            CssBorderSideParseError::Color(e) => {
344                CssBorderSideParseErrorOwned::Color(e.to_contained())
345            }
346        }
347    }
348}
349
350#[cfg(feature = "parser")]
351impl CssBorderSideParseErrorOwned {
352    #[must_use] pub fn to_shared(&self) -> CssBorderSideParseError<'_> {
353        match self {
354            Self::InvalidDeclaration(s) => {
355                CssBorderSideParseError::InvalidDeclaration(s.as_str())
356            }
357            Self::Width(e) => CssBorderSideParseError::Width(e.to_shared()),
358            Self::Style(e) => CssBorderSideParseError::Style(e.to_shared()),
359            Self::Color(e) => CssBorderSideParseError::Color(e.to_shared()),
360        }
361    }
362}
363
364// Type alias for compatibility with old code
365#[cfg(feature = "parser")]
366pub type CssBorderParseError<'a> = CssBorderSideParseError<'a>;
367
368/// Newtype wrapper around `CssBorderSideParseErrorOwned` for the `border` shorthand.
369#[cfg(feature = "parser")]
370#[derive(Debug, Clone, PartialEq)]
371#[repr(C)]
372pub struct CssBorderParseErrorOwned {
373    pub inner: CssBorderSideParseErrorOwned,
374}
375
376#[cfg(feature = "parser")]
377impl From<CssBorderSideParseErrorOwned> for CssBorderParseErrorOwned {
378    fn from(v: CssBorderSideParseErrorOwned) -> Self {
379        Self { inner: v }
380    }
381}
382
383/// Parses a border shorthand property such as "1px solid red".
384/// Handles any order of components and applies defaults for missing values.
385#[cfg(feature = "parser")]
386fn parse_border_side(
387    input: &str,
388) -> Result<StyleBorderSide, CssBorderSideParseError<'_>> {
389    let mut width = None;
390    let mut style = None;
391    let mut color = None;
392
393    if input.trim().is_empty() {
394        return Err(CssBorderSideParseError::InvalidDeclaration(input));
395    }
396
397    for part in input.split_whitespace() {
398        // Try to parse as a width.
399        if width.is_none() {
400            if let Ok(w) = parse_border_width_value(part) {
401                width = Some(w);
402                continue;
403            }
404        }
405
406        // Try to parse as a style.
407        if style.is_none() {
408            if let Ok(s) = parse_border_style(part) {
409                style = Some(s);
410                continue;
411            }
412        }
413
414        // Try to parse as a color.
415        if color.is_none() {
416            if let Ok(c) = parse_css_color(part) {
417                color = Some(c);
418                continue;
419            }
420        }
421
422        // If we get here, the part didn't match anything, or a value was specified twice.
423        return Err(CssBorderSideParseError::InvalidDeclaration(input));
424    }
425
426    Ok(StyleBorderSide {
427        border_width: width.unwrap_or(MEDIUM_BORDER_THICKNESS),
428        border_style: style.unwrap_or(BorderStyle::None),
429        border_color: color.unwrap_or(ColorU::BLACK),
430    })
431}
432
433// --- Individual Property Parsers ---
434
435#[cfg(feature = "parser")]
436fn parse_border_width_value(
437    input: &str,
438) -> Result<PixelValue, CssPixelValueParseError<'_>> {
439    match input.trim() {
440        "thin" => Ok(THIN_BORDER_THICKNESS),
441        "medium" => Ok(MEDIUM_BORDER_THICKNESS),
442        "thick" => Ok(THICK_BORDER_THICKNESS),
443        _ => parse_pixel_value(input),
444    }
445}
446
447#[cfg(feature = "parser")]
448/// # Errors
449///
450/// Returns an error if `input` is not a valid CSS `border-top-width` value.
451pub fn parse_border_top_width(
452    input: &str,
453) -> Result<LayoutBorderTopWidth, CssPixelValueParseError<'_>> {
454    parse_border_width_value(input).map(|inner| LayoutBorderTopWidth { inner })
455}
456
457#[cfg(feature = "parser")]
458/// # Errors
459///
460/// Returns an error if `input` is not a valid CSS `border-right-width` value.
461pub fn parse_border_right_width(
462    input: &str,
463) -> Result<LayoutBorderRightWidth, CssPixelValueParseError<'_>> {
464    parse_border_width_value(input).map(|inner| LayoutBorderRightWidth { inner })
465}
466
467#[cfg(feature = "parser")]
468/// # Errors
469///
470/// Returns an error if `input` is not a valid CSS `border-bottom-width` value.
471pub fn parse_border_bottom_width(
472    input: &str,
473) -> Result<LayoutBorderBottomWidth, CssPixelValueParseError<'_>> {
474    parse_border_width_value(input).map(|inner| LayoutBorderBottomWidth { inner })
475}
476
477#[cfg(feature = "parser")]
478/// # Errors
479///
480/// Returns an error if `input` is not a valid CSS `border-left-width` value.
481pub fn parse_border_left_width(
482    input: &str,
483) -> Result<LayoutBorderLeftWidth, CssPixelValueParseError<'_>> {
484    parse_border_width_value(input).map(|inner| LayoutBorderLeftWidth { inner })
485}
486
487#[cfg(feature = "parser")]
488/// # Errors
489///
490/// Returns an error if `input` is not a valid CSS `border-top-style` value.
491pub fn parse_border_top_style(
492    input: &str,
493) -> Result<StyleBorderTopStyle, CssBorderStyleParseError<'_>> {
494    parse_border_style(input).map(|inner| StyleBorderTopStyle { inner })
495}
496#[cfg(feature = "parser")]
497/// # Errors
498///
499/// Returns an error if `input` is not a valid CSS `border-right-style` value.
500pub fn parse_border_right_style(
501    input: &str,
502) -> Result<StyleBorderRightStyle, CssBorderStyleParseError<'_>> {
503    parse_border_style(input).map(|inner| StyleBorderRightStyle { inner })
504}
505#[cfg(feature = "parser")]
506/// # Errors
507///
508/// Returns an error if `input` is not a valid CSS `border-bottom-style` value.
509pub fn parse_border_bottom_style(
510    input: &str,
511) -> Result<StyleBorderBottomStyle, CssBorderStyleParseError<'_>> {
512    parse_border_style(input).map(|inner| StyleBorderBottomStyle { inner })
513}
514#[cfg(feature = "parser")]
515/// # Errors
516///
517/// Returns an error if `input` is not a valid CSS `border-left-style` value.
518pub fn parse_border_left_style(
519    input: &str,
520) -> Result<StyleBorderLeftStyle, CssBorderStyleParseError<'_>> {
521    parse_border_style(input).map(|inner| StyleBorderLeftStyle { inner })
522}
523
524#[cfg(feature = "parser")]
525/// # Errors
526///
527/// Returns an error if `input` is not a valid CSS `border-top-color` value.
528pub fn parse_border_top_color(
529    input: &str,
530) -> Result<StyleBorderTopColor, CssColorParseError<'_>> {
531    parse_css_color(input).map(|inner| StyleBorderTopColor { inner })
532}
533#[cfg(feature = "parser")]
534/// # Errors
535///
536/// Returns an error if `input` is not a valid CSS `border-right-color` value.
537pub fn parse_border_right_color(
538    input: &str,
539) -> Result<StyleBorderRightColor, CssColorParseError<'_>> {
540    parse_css_color(input).map(|inner| StyleBorderRightColor { inner })
541}
542#[cfg(feature = "parser")]
543/// # Errors
544///
545/// Returns an error if `input` is not a valid CSS `border-bottom-color` value.
546pub fn parse_border_bottom_color(
547    input: &str,
548) -> Result<StyleBorderBottomColor, CssColorParseError<'_>> {
549    parse_css_color(input).map(|inner| StyleBorderBottomColor { inner })
550}
551#[cfg(feature = "parser")]
552/// # Errors
553///
554/// Returns an error if `input` is not a valid CSS `border-left-color` value.
555pub fn parse_border_left_color(
556    input: &str,
557) -> Result<StyleBorderLeftColor, CssColorParseError<'_>> {
558    parse_css_color(input).map(|inner| StyleBorderLeftColor { inner })
559}
560
561// --- Border Color Shorthand ---
562
563/// Parsed result of `border-color` shorthand (1-4 color values)
564#[cfg(feature = "parser")]
565#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
566pub struct StyleBorderColors {
567    pub top: ColorU,
568    pub right: ColorU,
569    pub bottom: ColorU,
570    pub left: ColorU,
571}
572
573/// Parses `border-color` shorthand: 1-4 color values
574/// - 1 value: all sides
575/// - 2 values: top/bottom, left/right
576/// - 3 values: top, left/right, bottom
577/// - 4 values: top, right, bottom, left
578#[cfg(feature = "parser")]
579/// # Errors
580///
581/// Returns an error if `input` is not a valid CSS `border-color` value.
582pub fn parse_style_border_color(
583    input: &str,
584) -> Result<StyleBorderColors, CssColorParseError<'_>> {
585    let input = input.trim();
586    let parts: Vec<&str> = input.split_whitespace().collect();
587
588    match parts.len() {
589        1 => {
590            let color = parse_css_color(parts[0])?;
591            Ok(StyleBorderColors {
592                top: color,
593                right: color,
594                bottom: color,
595                left: color,
596            })
597        }
598        2 => {
599            let top_bottom = parse_css_color(parts[0])?;
600            let left_right = parse_css_color(parts[1])?;
601            Ok(StyleBorderColors {
602                top: top_bottom,
603                right: left_right,
604                bottom: top_bottom,
605                left: left_right,
606            })
607        }
608        3 => {
609            let top = parse_css_color(parts[0])?;
610            let left_right = parse_css_color(parts[1])?;
611            let bottom = parse_css_color(parts[2])?;
612            Ok(StyleBorderColors {
613                top,
614                right: left_right,
615                bottom,
616                left: left_right,
617            })
618        }
619        4 => {
620            let top = parse_css_color(parts[0])?;
621            let right = parse_css_color(parts[1])?;
622            let bottom = parse_css_color(parts[2])?;
623            let left = parse_css_color(parts[3])?;
624            Ok(StyleBorderColors {
625                top,
626                right,
627                bottom,
628                left,
629            })
630        }
631        _ => Err(CssColorParseError::InvalidColor(input)),
632    }
633}
634
635// --- Border Style Shorthand ---
636
637/// Parsed result of `border-style` shorthand (1-4 style values)
638#[cfg(feature = "parser")]
639#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
640pub struct StyleBorderStyles {
641    pub top: BorderStyle,
642    pub right: BorderStyle,
643    pub bottom: BorderStyle,
644    pub left: BorderStyle,
645}
646
647/// Parses `border-style` shorthand: 1-4 style values
648#[cfg(feature = "parser")]
649/// # Errors
650///
651/// Returns an error if `input` is not a valid CSS `border-style` value.
652pub fn parse_style_border_style(
653    input: &str,
654) -> Result<StyleBorderStyles, CssBorderStyleParseError<'_>> {
655    let input = input.trim();
656    let parts: Vec<&str> = input.split_whitespace().collect();
657
658    match parts.len() {
659        1 => {
660            let style = parse_border_style(parts[0])?;
661            Ok(StyleBorderStyles {
662                top: style,
663                right: style,
664                bottom: style,
665                left: style,
666            })
667        }
668        2 => {
669            let top_bottom = parse_border_style(parts[0])?;
670            let left_right = parse_border_style(parts[1])?;
671            Ok(StyleBorderStyles {
672                top: top_bottom,
673                right: left_right,
674                bottom: top_bottom,
675                left: left_right,
676            })
677        }
678        3 => {
679            let top = parse_border_style(parts[0])?;
680            let left_right = parse_border_style(parts[1])?;
681            let bottom = parse_border_style(parts[2])?;
682            Ok(StyleBorderStyles {
683                top,
684                right: left_right,
685                bottom,
686                left: left_right,
687            })
688        }
689        4 => {
690            let top = parse_border_style(parts[0])?;
691            let right = parse_border_style(parts[1])?;
692            let bottom = parse_border_style(parts[2])?;
693            let left = parse_border_style(parts[3])?;
694            Ok(StyleBorderStyles {
695                top,
696                right,
697                bottom,
698                left,
699            })
700        }
701        _ => Err(CssBorderStyleParseError::InvalidStyle(input)),
702    }
703}
704
705// --- Border Width Shorthand ---
706
707/// Parsed result of `border-width` shorthand (1-4 width values)
708#[cfg(feature = "parser")]
709#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
710pub struct StyleBorderWidths {
711    pub top: PixelValue,
712    pub right: PixelValue,
713    pub bottom: PixelValue,
714    pub left: PixelValue,
715}
716
717/// Parses `border-width` shorthand: 1-4 width values
718#[cfg(feature = "parser")]
719/// # Errors
720///
721/// Returns an error if `input` is not a valid CSS `border-width` value.
722pub fn parse_style_border_width(
723    input: &str,
724) -> Result<StyleBorderWidths, CssPixelValueParseError<'_>> {
725    let input = input.trim();
726    let parts: Vec<&str> = input.split_whitespace().collect();
727
728    match parts.len() {
729        1 => {
730            let width = parse_pixel_value(parts[0])?;
731            Ok(StyleBorderWidths {
732                top: width,
733                right: width,
734                bottom: width,
735                left: width,
736            })
737        }
738        2 => {
739            let top_bottom = parse_pixel_value(parts[0])?;
740            let left_right = parse_pixel_value(parts[1])?;
741            Ok(StyleBorderWidths {
742                top: top_bottom,
743                right: left_right,
744                bottom: top_bottom,
745                left: left_right,
746            })
747        }
748        3 => {
749            let top = parse_pixel_value(parts[0])?;
750            let left_right = parse_pixel_value(parts[1])?;
751            let bottom = parse_pixel_value(parts[2])?;
752            Ok(StyleBorderWidths {
753                top,
754                right: left_right,
755                bottom,
756                left: left_right,
757            })
758        }
759        4 => {
760            let top = parse_pixel_value(parts[0])?;
761            let right = parse_pixel_value(parts[1])?;
762            let bottom = parse_pixel_value(parts[2])?;
763            let left = parse_pixel_value(parts[3])?;
764            Ok(StyleBorderWidths {
765                top,
766                right,
767                bottom,
768                left,
769            })
770        }
771        _ => Err(CssPixelValueParseError::InvalidPixelValue(input)),
772    }
773}
774
775// Compatibility alias
776#[cfg(feature = "parser")]
777/// # Errors
778///
779/// Returns an error if `input` is not a valid CSS `border` value.
780pub fn parse_style_border(input: &str) -> Result<StyleBorderSide, CssBorderParseError<'_>> {
781    parse_border_side(input)
782}
783
784#[cfg(all(test, feature = "parser"))]
785mod tests {
786    use super::*;
787
788    #[test]
789    fn test_parse_border_style() {
790        assert_eq!(parse_border_style("solid").unwrap(), BorderStyle::Solid);
791        assert_eq!(parse_border_style("dotted").unwrap(), BorderStyle::Dotted);
792        assert_eq!(parse_border_style("none").unwrap(), BorderStyle::None);
793        assert_eq!(
794            parse_border_style("  dashed  ").unwrap(),
795            BorderStyle::Dashed
796        );
797        assert!(parse_border_style("solidd").is_err());
798    }
799
800    #[test]
801    fn test_parse_border_side_shorthand() {
802        // Full
803        let result = parse_border_side("2px dotted #ff0000").unwrap();
804        assert_eq!(result.border_width, PixelValue::px(2.0));
805        assert_eq!(result.border_style, BorderStyle::Dotted);
806        assert_eq!(result.border_color, ColorU::new_rgb(255, 0, 0));
807
808        // Different order
809        let result = parse_border_side("solid green 1em").unwrap();
810        assert_eq!(result.border_width, PixelValue::em(1.0));
811        assert_eq!(result.border_style, BorderStyle::Solid);
812        assert_eq!(result.border_color, ColorU::new_rgb(0, 128, 0));
813
814        // Missing width
815        let result = parse_border_side("ridge #f0f").unwrap();
816        assert_eq!(result.border_width, MEDIUM_BORDER_THICKNESS); // default
817        assert_eq!(result.border_style, BorderStyle::Ridge);
818        assert_eq!(result.border_color, ColorU::new_rgb(255, 0, 255));
819
820        // Missing style
821        let result = parse_border_side("5pt blue").unwrap();
822        assert_eq!(result.border_width, PixelValue::pt(5.0));
823        assert_eq!(result.border_style, BorderStyle::None); // default
824        assert_eq!(result.border_color, ColorU::BLUE);
825
826        // Missing color
827        let result = parse_border_side("thick double").unwrap();
828        assert_eq!(result.border_width, PixelValue::px(5.0));
829        assert_eq!(result.border_style, BorderStyle::Double);
830        assert_eq!(result.border_color, ColorU::BLACK); // default
831
832        // Only one value
833        let result = parse_border_side("inset").unwrap();
834        assert_eq!(result.border_width, MEDIUM_BORDER_THICKNESS);
835        assert_eq!(result.border_style, BorderStyle::Inset);
836        assert_eq!(result.border_color, ColorU::BLACK);
837    }
838
839    #[test]
840    fn test_parse_border_side_invalid() {
841        // Two widths
842        assert!(parse_border_side("1px 2px solid red").is_err());
843        // Two styles
844        assert!(parse_border_side("solid dashed red").is_err());
845        // Two colors
846        assert!(parse_border_side("red blue solid").is_err());
847        // Empty
848        assert!(parse_border_side("").is_err());
849        // Unknown keyword
850        assert!(parse_border_side("1px unknown red").is_err());
851    }
852
853    #[test]
854    fn test_parse_longhand_border() {
855        assert_eq!(
856            parse_border_top_width("1.5em").unwrap().inner,
857            PixelValue::em(1.5)
858        );
859        assert_eq!(
860            parse_border_left_style("groove").unwrap().inner,
861            BorderStyle::Groove
862        );
863        assert_eq!(
864            parse_border_right_color("rgba(10, 20, 30, 0.5)")
865                .unwrap()
866                .inner,
867            ColorU::new(10, 20, 30, 128)
868        );
869    }
870}