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