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