Skip to main content

azul_css/props/style/
effects.rs

1//! CSS properties for visual effects (opacity, blending, cursor), box sizing
2//! (object-fit, object-position, aspect-ratio), and text orientation.
3
4use alloc::string::{String, ToString};
5use core::fmt;
6
7#[cfg(feature = "parser")]
8use crate::props::basic::{
9    error::{InvalidValueErr, InvalidValueErrOwned},
10    length::parse_percentage_value,
11};
12use crate::props::{
13    basic::length::{PercentageParseError, PercentageValue},
14    formatter::PrintAsCssValue,
15};
16
17// -- Opacity --
18
19/// Represents an `opacity` attribute, a value from 0.0 to 1.0.
20#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
21#[repr(C)]
22pub struct StyleOpacity {
23    pub inner: PercentageValue,
24}
25
26impl Default for StyleOpacity {
27    fn default() -> Self {
28        StyleOpacity {
29            inner: PercentageValue::const_new(100),
30        }
31    }
32}
33
34impl PrintAsCssValue for StyleOpacity {
35    fn print_as_css_value(&self) -> String {
36        format!("{}", self.inner.normalized())
37    }
38}
39
40#[cfg(feature = "parser")]
41impl_percentage_value!(StyleOpacity);
42
43// -- Visibility --
44
45/// Represents a `visibility` attribute, controlling element visibility.
46#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
47#[repr(C)]
48#[derive(Default)]
49pub enum StyleVisibility {
50    #[default]
51    Visible,
52    Hidden,
53    Collapse,
54}
55
56
57impl PrintAsCssValue for StyleVisibility {
58    fn print_as_css_value(&self) -> String {
59        String::from(match self {
60            Self::Visible => "visible",
61            Self::Hidden => "hidden",
62            Self::Collapse => "collapse",
63        })
64    }
65}
66
67// -- Mix Blend Mode --
68
69/// Represents a `mix-blend-mode` attribute, which determines how an element's
70/// content should blend with the content of the element's parent.
71#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
72#[repr(C)]
73#[derive(Default)]
74pub enum StyleMixBlendMode {
75    #[default]
76    Normal,
77    Multiply,
78    Screen,
79    Overlay,
80    Darken,
81    Lighten,
82    ColorDodge,
83    ColorBurn,
84    HardLight,
85    SoftLight,
86    Difference,
87    Exclusion,
88    Hue,
89    Saturation,
90    Color,
91    Luminosity,
92}
93
94
95impl fmt::Display for StyleMixBlendMode {
96    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97        write!(
98            f,
99            "{}",
100            match self {
101                Self::Normal => "normal",
102                Self::Multiply => "multiply",
103                Self::Screen => "screen",
104                Self::Overlay => "overlay",
105                Self::Darken => "darken",
106                Self::Lighten => "lighten",
107                Self::ColorDodge => "color-dodge",
108                Self::ColorBurn => "color-burn",
109                Self::HardLight => "hard-light",
110                Self::SoftLight => "soft-light",
111                Self::Difference => "difference",
112                Self::Exclusion => "exclusion",
113                Self::Hue => "hue",
114                Self::Saturation => "saturation",
115                Self::Color => "color",
116                Self::Luminosity => "luminosity",
117            }
118        )
119    }
120}
121
122impl PrintAsCssValue for StyleMixBlendMode {
123    fn print_as_css_value(&self) -> String {
124        self.to_string()
125    }
126}
127
128// -- Cursor --
129
130/// Represents a `cursor` attribute, defining the mouse cursor to be displayed
131/// when pointing over an element.
132#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
133#[repr(C)]
134#[derive(Default)]
135pub enum StyleCursor {
136    Alias,
137    AllScroll,
138    Cell,
139    ColResize,
140    ContextMenu,
141    Copy,
142    Crosshair,
143    #[default]
144    Default,
145    EResize,
146    EwResize,
147    Grab,
148    Grabbing,
149    Help,
150    Move,
151    NResize,
152    NsResize,
153    NeswResize,
154    NwseResize,
155    Pointer,
156    Progress,
157    RowResize,
158    SResize,
159    SeResize,
160    Text,
161    Unset,
162    VerticalText,
163    WResize,
164    Wait,
165    ZoomIn,
166    ZoomOut,
167}
168
169
170impl PrintAsCssValue for StyleCursor {
171    fn print_as_css_value(&self) -> String {
172        String::from(match self {
173            Self::Alias => "alias",
174            Self::AllScroll => "all-scroll",
175            Self::Cell => "cell",
176            Self::ColResize => "col-resize",
177            Self::ContextMenu => "context-menu",
178            Self::Copy => "copy",
179            Self::Crosshair => "crosshair",
180            Self::Default => "default",
181            Self::EResize => "e-resize",
182            Self::EwResize => "ew-resize",
183            Self::Grab => "grab",
184            Self::Grabbing => "grabbing",
185            Self::Help => "help",
186            Self::Move => "move",
187            Self::NResize => "n-resize",
188            Self::NsResize => "ns-resize",
189            Self::NeswResize => "nesw-resize",
190            Self::NwseResize => "nwse-resize",
191            Self::Pointer => "pointer",
192            Self::Progress => "progress",
193            Self::RowResize => "row-resize",
194            Self::SResize => "s-resize",
195            Self::SeResize => "se-resize",
196            Self::Text => "text",
197            Self::Unset => "unset",
198            Self::VerticalText => "vertical-text",
199            Self::WResize => "w-resize",
200            Self::Wait => "wait",
201            Self::ZoomIn => "zoom-in",
202            Self::ZoomOut => "zoom-out",
203        })
204    }
205}
206
207// --- PARSERS ---
208
209#[cfg(feature = "parser")]
210pub mod parsers {
211    use super::*;
212    use crate::corety::AzString;
213    use crate::props::basic::error::{InvalidValueErr, InvalidValueErrOwned};
214
215    // -- Opacity Parser --
216
217    #[derive(Clone, PartialEq)]
218    pub enum OpacityParseError<'a> {
219        ParsePercentage(PercentageParseError, &'a str),
220        OutOfRange(&'a str),
221    }
222    impl_debug_as_display!(OpacityParseError<'a>);
223    impl_display! { OpacityParseError<'a>, {
224        ParsePercentage(e, s) => format!("Invalid opacity value \"{}\": {}", s, e),
225        OutOfRange(s) => format!("Invalid opacity value \"{}\": must be between 0 and 1", s),
226    }}
227
228    /// Wrapper for PercentageParseError with input string.
229    #[derive(Debug, Clone, PartialEq)]
230    #[repr(C)]
231    pub struct PercentageParseErrorWithInput {
232        pub error: PercentageParseError,
233        pub input: AzString,
234    }
235
236    #[derive(Debug, Clone, PartialEq)]
237    #[repr(C, u8)]
238    pub enum OpacityParseErrorOwned {
239        ParsePercentage(PercentageParseErrorWithInput),
240        OutOfRange(AzString),
241    }
242
243    impl<'a> OpacityParseError<'a> {
244        pub fn to_contained(&self) -> OpacityParseErrorOwned {
245            match self {
246                Self::ParsePercentage(err, s) => {
247                    OpacityParseErrorOwned::ParsePercentage(PercentageParseErrorWithInput { error: err.clone(), input: s.to_string().into() })
248                }
249                Self::OutOfRange(s) => OpacityParseErrorOwned::OutOfRange(s.to_string().into()),
250            }
251        }
252    }
253
254    impl OpacityParseErrorOwned {
255        pub fn to_shared<'a>(&'a self) -> OpacityParseError<'a> {
256            match self {
257                Self::ParsePercentage(e) => {
258                    OpacityParseError::ParsePercentage(e.error.clone(), e.input.as_str())
259                }
260                Self::OutOfRange(s) => OpacityParseError::OutOfRange(s.as_str()),
261            }
262        }
263    }
264
265    pub fn parse_style_opacity<'a>(input: &'a str) -> Result<StyleOpacity, OpacityParseError<'a>> {
266        let val = parse_percentage_value(input)
267            .map_err(|e| OpacityParseError::ParsePercentage(e, input))?;
268
269        let normalized = val.normalized();
270        if !(0.0..=1.0).contains(&normalized) {
271            return Err(OpacityParseError::OutOfRange(input));
272        }
273
274        Ok(StyleOpacity { inner: val })
275    }
276
277    // -- Visibility Parser --
278
279    #[derive(Clone, PartialEq)]
280    pub enum StyleVisibilityParseError<'a> {
281        InvalidValue(InvalidValueErr<'a>),
282    }
283    impl_debug_as_display!(StyleVisibilityParseError<'a>);
284    impl_display! { StyleVisibilityParseError<'a>, {
285        InvalidValue(e) => format!("Invalid visibility value: \"{}\"", e.0),
286    }}
287    impl_from!(InvalidValueErr<'a>, StyleVisibilityParseError::InvalidValue);
288
289    #[derive(Debug, Clone, PartialEq)]
290    #[repr(C, u8)]
291    pub enum StyleVisibilityParseErrorOwned {
292        InvalidValue(InvalidValueErrOwned),
293    }
294
295    impl<'a> StyleVisibilityParseError<'a> {
296        pub fn to_contained(&self) -> StyleVisibilityParseErrorOwned {
297            match self {
298                Self::InvalidValue(e) => {
299                    StyleVisibilityParseErrorOwned::InvalidValue(e.to_contained())
300                }
301            }
302        }
303    }
304
305    impl StyleVisibilityParseErrorOwned {
306        pub fn to_shared<'a>(&'a self) -> StyleVisibilityParseError<'a> {
307            match self {
308                Self::InvalidValue(e) => StyleVisibilityParseError::InvalidValue(e.to_shared()),
309            }
310        }
311    }
312
313    pub fn parse_style_visibility<'a>(
314        input: &'a str,
315    ) -> Result<StyleVisibility, StyleVisibilityParseError<'a>> {
316        let input = input.trim();
317        match input {
318            "visible" => Ok(StyleVisibility::Visible),
319            "hidden" => Ok(StyleVisibility::Hidden),
320            "collapse" => Ok(StyleVisibility::Collapse),
321            _ => Err(InvalidValueErr(input).into()),
322        }
323    }
324
325    // -- Mix Blend Mode Parser --
326
327    #[derive(Clone, PartialEq)]
328    pub enum MixBlendModeParseError<'a> {
329        InvalidValue(InvalidValueErr<'a>),
330    }
331    impl_debug_as_display!(MixBlendModeParseError<'a>);
332    impl_display! { MixBlendModeParseError<'a>, {
333        InvalidValue(e) => format!("Invalid mix-blend-mode value: \"{}\"", e.0),
334    }}
335    impl_from!(InvalidValueErr<'a>, MixBlendModeParseError::InvalidValue);
336
337    #[derive(Debug, Clone, PartialEq)]
338    #[repr(C, u8)]
339    pub enum MixBlendModeParseErrorOwned {
340        InvalidValue(InvalidValueErrOwned),
341    }
342
343    impl<'a> MixBlendModeParseError<'a> {
344        pub fn to_contained(&self) -> MixBlendModeParseErrorOwned {
345            match self {
346                Self::InvalidValue(e) => {
347                    MixBlendModeParseErrorOwned::InvalidValue(e.to_contained())
348                }
349            }
350        }
351    }
352
353    impl MixBlendModeParseErrorOwned {
354        pub fn to_shared<'a>(&'a self) -> MixBlendModeParseError<'a> {
355            match self {
356                Self::InvalidValue(e) => MixBlendModeParseError::InvalidValue(e.to_shared()),
357            }
358        }
359    }
360
361    pub fn parse_style_mix_blend_mode<'a>(
362        input: &'a str,
363    ) -> Result<StyleMixBlendMode, MixBlendModeParseError<'a>> {
364        let input = input.trim();
365        match input {
366            "normal" => Ok(StyleMixBlendMode::Normal),
367            "multiply" => Ok(StyleMixBlendMode::Multiply),
368            "screen" => Ok(StyleMixBlendMode::Screen),
369            "overlay" => Ok(StyleMixBlendMode::Overlay),
370            "darken" => Ok(StyleMixBlendMode::Darken),
371            "lighten" => Ok(StyleMixBlendMode::Lighten),
372            "color-dodge" => Ok(StyleMixBlendMode::ColorDodge),
373            "color-burn" => Ok(StyleMixBlendMode::ColorBurn),
374            "hard-light" => Ok(StyleMixBlendMode::HardLight),
375            "soft-light" => Ok(StyleMixBlendMode::SoftLight),
376            "difference" => Ok(StyleMixBlendMode::Difference),
377            "exclusion" => Ok(StyleMixBlendMode::Exclusion),
378            "hue" => Ok(StyleMixBlendMode::Hue),
379            "saturation" => Ok(StyleMixBlendMode::Saturation),
380            "color" => Ok(StyleMixBlendMode::Color),
381            "luminosity" => Ok(StyleMixBlendMode::Luminosity),
382            _ => Err(InvalidValueErr(input).into()),
383        }
384    }
385
386    // -- Cursor Parser --
387
388    #[derive(Clone, PartialEq)]
389    pub enum CursorParseError<'a> {
390        InvalidValue(InvalidValueErr<'a>),
391    }
392    impl_debug_as_display!(CursorParseError<'a>);
393    impl_display! { CursorParseError<'a>, {
394        InvalidValue(e) => format!("Invalid cursor value: \"{}\"", e.0),
395    }}
396    impl_from!(InvalidValueErr<'a>, CursorParseError::InvalidValue);
397
398    #[derive(Debug, Clone, PartialEq)]
399    #[repr(C, u8)]
400    pub enum CursorParseErrorOwned {
401        InvalidValue(InvalidValueErrOwned),
402    }
403
404    impl<'a> CursorParseError<'a> {
405        pub fn to_contained(&self) -> CursorParseErrorOwned {
406            match self {
407                Self::InvalidValue(e) => CursorParseErrorOwned::InvalidValue(e.to_contained()),
408            }
409        }
410    }
411
412    impl CursorParseErrorOwned {
413        pub fn to_shared<'a>(&'a self) -> CursorParseError<'a> {
414            match self {
415                Self::InvalidValue(e) => CursorParseError::InvalidValue(e.to_shared()),
416            }
417        }
418    }
419
420    pub fn parse_style_cursor<'a>(input: &'a str) -> Result<StyleCursor, CursorParseError<'a>> {
421        let input = input.trim();
422        match input {
423            "alias" => Ok(StyleCursor::Alias),
424            "all-scroll" => Ok(StyleCursor::AllScroll),
425            "cell" => Ok(StyleCursor::Cell),
426            "col-resize" => Ok(StyleCursor::ColResize),
427            "context-menu" => Ok(StyleCursor::ContextMenu),
428            "copy" => Ok(StyleCursor::Copy),
429            "crosshair" => Ok(StyleCursor::Crosshair),
430            "default" => Ok(StyleCursor::Default),
431            "e-resize" => Ok(StyleCursor::EResize),
432            "ew-resize" => Ok(StyleCursor::EwResize),
433            "grab" => Ok(StyleCursor::Grab),
434            "grabbing" => Ok(StyleCursor::Grabbing),
435            "help" => Ok(StyleCursor::Help),
436            "move" => Ok(StyleCursor::Move),
437            "n-resize" => Ok(StyleCursor::NResize),
438            "ns-resize" => Ok(StyleCursor::NsResize),
439            "nesw-resize" => Ok(StyleCursor::NeswResize),
440            "nwse-resize" => Ok(StyleCursor::NwseResize),
441            "pointer" => Ok(StyleCursor::Pointer),
442            "progress" => Ok(StyleCursor::Progress),
443            "row-resize" => Ok(StyleCursor::RowResize),
444            "s-resize" => Ok(StyleCursor::SResize),
445            "se-resize" => Ok(StyleCursor::SeResize),
446            "text" => Ok(StyleCursor::Text),
447            "unset" => Ok(StyleCursor::Unset),
448            "vertical-text" => Ok(StyleCursor::VerticalText),
449            "w-resize" => Ok(StyleCursor::WResize),
450            "wait" => Ok(StyleCursor::Wait),
451            "zoom-in" => Ok(StyleCursor::ZoomIn),
452            "zoom-out" => Ok(StyleCursor::ZoomOut),
453            _ => Err(InvalidValueErr(input).into()),
454        }
455    }
456}
457
458#[cfg(feature = "parser")]
459pub use self::parsers::*;
460
461#[cfg(all(test, feature = "parser"))]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn test_parse_opacity() {
467        assert_eq!(parse_style_opacity("0.5").unwrap().inner.normalized(), 0.5);
468        assert_eq!(parse_style_opacity("1").unwrap().inner.normalized(), 1.0);
469        assert_eq!(parse_style_opacity("50%").unwrap().inner.normalized(), 0.5);
470        assert_eq!(parse_style_opacity("0").unwrap().inner.normalized(), 0.0);
471        assert_eq!(
472            parse_style_opacity("  75%  ").unwrap().inner.normalized(),
473            0.75
474        );
475        assert!(parse_style_opacity("1.1").is_err());
476        assert!(parse_style_opacity("-0.1").is_err());
477        assert!(parse_style_opacity("auto").is_err());
478    }
479
480    #[test]
481    fn test_parse_mix_blend_mode() {
482        assert_eq!(
483            parse_style_mix_blend_mode("multiply").unwrap(),
484            StyleMixBlendMode::Multiply
485        );
486        assert_eq!(
487            parse_style_mix_blend_mode("screen").unwrap(),
488            StyleMixBlendMode::Screen
489        );
490        assert_eq!(
491            parse_style_mix_blend_mode("color-dodge").unwrap(),
492            StyleMixBlendMode::ColorDodge
493        );
494        assert!(parse_style_mix_blend_mode("mix").is_err());
495    }
496
497    #[test]
498    fn test_parse_visibility() {
499        assert_eq!(
500            parse_style_visibility("visible").unwrap(),
501            StyleVisibility::Visible
502        );
503        assert_eq!(
504            parse_style_visibility("hidden").unwrap(),
505            StyleVisibility::Hidden
506        );
507        assert_eq!(
508            parse_style_visibility("collapse").unwrap(),
509            StyleVisibility::Collapse
510        );
511        assert_eq!(
512            parse_style_visibility("  visible  ").unwrap(),
513            StyleVisibility::Visible
514        );
515        assert!(parse_style_visibility("none").is_err());
516        assert!(parse_style_visibility("show").is_err());
517    }
518
519    #[test]
520    fn test_parse_cursor() {
521        assert_eq!(parse_style_cursor("pointer").unwrap(), StyleCursor::Pointer);
522        assert_eq!(parse_style_cursor("wait").unwrap(), StyleCursor::Wait);
523        assert_eq!(
524            parse_style_cursor("col-resize").unwrap(),
525            StyleCursor::ColResize
526        );
527        assert_eq!(parse_style_cursor("  text  ").unwrap(), StyleCursor::Text);
528        assert!(parse_style_cursor("hand").is_err()); // "hand" is a legacy IE value
529    }
530
531    #[test]
532    fn test_parse_object_fit() {
533        assert_eq!(parse_style_object_fit("fill").unwrap(), StyleObjectFit::Fill);
534        assert_eq!(parse_style_object_fit("contain").unwrap(), StyleObjectFit::Contain);
535        assert_eq!(parse_style_object_fit("cover").unwrap(), StyleObjectFit::Cover);
536        assert_eq!(parse_style_object_fit("none").unwrap(), StyleObjectFit::None);
537        assert_eq!(parse_style_object_fit("scale-down").unwrap(), StyleObjectFit::ScaleDown);
538        assert_eq!(parse_style_object_fit("  cover  ").unwrap(), StyleObjectFit::Cover);
539        assert!(parse_style_object_fit("stretch").is_err());
540        assert!(parse_style_object_fit("").is_err());
541    }
542
543    #[test]
544    fn test_parse_text_orientation() {
545        assert_eq!(parse_style_text_orientation("mixed").unwrap(), StyleTextOrientation::Mixed);
546        assert_eq!(parse_style_text_orientation("upright").unwrap(), StyleTextOrientation::Upright);
547        assert_eq!(parse_style_text_orientation("sideways").unwrap(), StyleTextOrientation::Sideways);
548        assert_eq!(parse_style_text_orientation("  mixed  ").unwrap(), StyleTextOrientation::Mixed);
549        assert!(parse_style_text_orientation("vertical").is_err());
550    }
551
552    #[test]
553    fn test_parse_object_position() {
554        let centered = parse_style_object_position("center").unwrap();
555        assert_eq!(centered, parse_style_object_position("center center").unwrap());
556
557        let lt = parse_style_object_position("left top").unwrap();
558        use crate::props::style::background::{BackgroundPositionHorizontal, BackgroundPositionVertical};
559        assert_eq!(lt.horizontal, BackgroundPositionHorizontal::Left);
560        assert_eq!(lt.vertical, BackgroundPositionVertical::Top);
561
562        let rb = parse_style_object_position("right bottom").unwrap();
563        assert_eq!(rb.horizontal, BackgroundPositionHorizontal::Right);
564        assert_eq!(rb.vertical, BackgroundPositionVertical::Bottom);
565
566        assert!(parse_style_object_position("left top center").is_err());
567        assert!(parse_style_object_position("invalid").is_err());
568    }
569
570    #[test]
571    fn test_parse_aspect_ratio() {
572        assert_eq!(parse_style_aspect_ratio("auto").unwrap(), StyleAspectRatio::Auto);
573        assert_eq!(
574            parse_style_aspect_ratio("16 / 9").unwrap(),
575            StyleAspectRatio::Ratio(AspectRatioValue { width: 16000, height: 9000 })
576        );
577        assert_eq!(
578            parse_style_aspect_ratio("16/9").unwrap(),
579            StyleAspectRatio::Ratio(AspectRatioValue { width: 16000, height: 9000 })
580        );
581        assert_eq!(
582            parse_style_aspect_ratio("1.5").unwrap(),
583            StyleAspectRatio::Ratio(AspectRatioValue { width: 1500, height: 1000 })
584        );
585        assert_eq!(
586            parse_style_aspect_ratio("  4 / 3  ").unwrap(),
587            StyleAspectRatio::Ratio(AspectRatioValue { width: 4000, height: 3000 })
588        );
589        assert!(parse_style_aspect_ratio("0 / 1").is_err());
590        assert!(parse_style_aspect_ratio("1 / 0").is_err());
591        assert!(parse_style_aspect_ratio("-1 / 1").is_err());
592        assert!(parse_style_aspect_ratio("abc").is_err());
593    }
594}
595
596// -- StyleObjectFit --
597
598/// CSS object-fit property: how replaced element content is fitted to its box.
599/// CSS Images Level 3 §5.5
600#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
601#[repr(C)]
602#[derive(Default)]
603pub enum StyleObjectFit {
604    #[default]
605    Fill,
606    Contain,
607    Cover,
608    None,
609    ScaleDown,
610}
611
612
613crate::impl_option!(StyleObjectFit, OptionStyleObjectFit, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
614
615impl PrintAsCssValue for StyleObjectFit {
616    fn print_as_css_value(&self) -> String {
617        String::from(match self {
618            StyleObjectFit::Fill => "fill",
619            StyleObjectFit::Contain => "contain",
620            StyleObjectFit::Cover => "cover",
621            StyleObjectFit::None => "none",
622            StyleObjectFit::ScaleDown => "scale-down",
623        })
624    }
625}
626
627#[cfg(feature = "parser")]
628#[derive(Clone, PartialEq)]
629pub enum StyleObjectFitParseError<'a> {
630    InvalidValue(&'a str),
631}
632
633#[cfg(feature = "parser")]
634crate::impl_debug_as_display!(StyleObjectFitParseError<'a>);
635
636#[cfg(feature = "parser")]
637crate::impl_display! { StyleObjectFitParseError<'a>, {
638    InvalidValue(val) => format!("Invalid object-fit value: \"{}\"", val),
639}}
640
641#[cfg(feature = "parser")]
642#[derive(Debug, Clone, PartialEq)]
643#[repr(C, u8)]
644pub enum StyleObjectFitParseErrorOwned {
645    InvalidValue(crate::AzString),
646}
647
648#[cfg(feature = "parser")]
649impl<'a> StyleObjectFitParseError<'a> {
650    pub fn to_contained(&self) -> StyleObjectFitParseErrorOwned {
651        match self {
652            Self::InvalidValue(s) => StyleObjectFitParseErrorOwned::InvalidValue(s.to_string().into()),
653        }
654    }
655}
656
657#[cfg(feature = "parser")]
658impl StyleObjectFitParseErrorOwned {
659    pub fn to_shared<'a>(&'a self) -> StyleObjectFitParseError<'a> {
660        match self {
661            Self::InvalidValue(s) => StyleObjectFitParseError::InvalidValue(s.as_str()),
662        }
663    }
664}
665
666#[cfg(feature = "parser")]
667pub fn parse_style_object_fit<'a>(
668    input: &'a str,
669) -> Result<StyleObjectFit, StyleObjectFitParseError<'a>> {
670    let input = input.trim();
671    match input {
672        "fill" => Ok(StyleObjectFit::Fill),
673        "contain" => Ok(StyleObjectFit::Contain),
674        "cover" => Ok(StyleObjectFit::Cover),
675        "none" => Ok(StyleObjectFit::None),
676        "scale-down" => Ok(StyleObjectFit::ScaleDown),
677        _ => Err(StyleObjectFitParseError::InvalidValue(input)),
678    }
679}
680
681// -- StyleTextOrientation --
682
683/// CSS text-orientation property for vertical writing modes.
684/// CSS Writing Modes Level 4 §5.1
685#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
686#[repr(C)]
687#[derive(Default)]
688pub enum StyleTextOrientation {
689    #[default]
690    Mixed,
691    Upright,
692    Sideways,
693}
694
695
696crate::impl_option!(StyleTextOrientation, OptionStyleTextOrientation, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
697
698impl PrintAsCssValue for StyleTextOrientation {
699    fn print_as_css_value(&self) -> String {
700        String::from(match self {
701            StyleTextOrientation::Mixed => "mixed",
702            StyleTextOrientation::Upright => "upright",
703            StyleTextOrientation::Sideways => "sideways",
704        })
705    }
706}
707
708#[cfg(feature = "parser")]
709#[derive(Clone, PartialEq)]
710pub enum StyleTextOrientationParseError<'a> {
711    InvalidValue(&'a str),
712}
713
714#[cfg(feature = "parser")]
715crate::impl_debug_as_display!(StyleTextOrientationParseError<'a>);
716
717#[cfg(feature = "parser")]
718crate::impl_display! { StyleTextOrientationParseError<'a>, {
719    InvalidValue(val) => format!("Invalid text-orientation value: \"{}\"", val),
720}}
721
722#[cfg(feature = "parser")]
723#[derive(Debug, Clone, PartialEq)]
724#[repr(C, u8)]
725pub enum StyleTextOrientationParseErrorOwned {
726    InvalidValue(crate::AzString),
727}
728
729#[cfg(feature = "parser")]
730impl<'a> StyleTextOrientationParseError<'a> {
731    pub fn to_contained(&self) -> StyleTextOrientationParseErrorOwned {
732        match self {
733            Self::InvalidValue(s) => StyleTextOrientationParseErrorOwned::InvalidValue(s.to_string().into()),
734        }
735    }
736}
737
738#[cfg(feature = "parser")]
739impl StyleTextOrientationParseErrorOwned {
740    pub fn to_shared<'a>(&'a self) -> StyleTextOrientationParseError<'a> {
741        match self {
742            Self::InvalidValue(s) => StyleTextOrientationParseError::InvalidValue(s.as_str()),
743        }
744    }
745}
746
747#[cfg(feature = "parser")]
748pub fn parse_style_text_orientation<'a>(
749    input: &'a str,
750) -> Result<StyleTextOrientation, StyleTextOrientationParseError<'a>> {
751    let input = input.trim();
752    match input {
753        "mixed" => Ok(StyleTextOrientation::Mixed),
754        "upright" => Ok(StyleTextOrientation::Upright),
755        "sideways" => Ok(StyleTextOrientation::Sideways),
756        _ => Err(StyleTextOrientationParseError::InvalidValue(input)),
757    }
758}
759
760// -- StyleObjectPosition --
761
762/// CSS object-position property: position of replaced element content within its box.
763/// CSS Images Level 3 §5.6 — default: `50% 50%` (centered)
764#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
765#[repr(C)]
766pub struct StyleObjectPosition {
767    pub horizontal: crate::props::style::background::BackgroundPositionHorizontal,
768    pub vertical: crate::props::style::background::BackgroundPositionVertical,
769}
770
771impl Default for StyleObjectPosition {
772    fn default() -> Self {
773        use crate::props::basic::pixel::PixelValue;
774        Self {
775            horizontal: crate::props::style::background::BackgroundPositionHorizontal::Exact(
776                PixelValue::percent(50.0),
777            ),
778            vertical: crate::props::style::background::BackgroundPositionVertical::Exact(
779                PixelValue::percent(50.0),
780            ),
781        }
782    }
783}
784
785crate::impl_option!(StyleObjectPosition, OptionStyleObjectPosition, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
786
787impl PrintAsCssValue for StyleObjectPosition {
788    fn print_as_css_value(&self) -> String {
789        format!(
790            "{} {}",
791            self.horizontal.print_as_css_value(),
792            self.vertical.print_as_css_value()
793        )
794    }
795}
796
797#[cfg(feature = "parser")]
798#[derive(Clone, PartialEq)]
799pub enum StyleObjectPositionParseError<'a> {
800    InvalidValue(&'a str),
801}
802
803#[cfg(feature = "parser")]
804crate::impl_debug_as_display!(StyleObjectPositionParseError<'a>);
805
806#[cfg(feature = "parser")]
807crate::impl_display! { StyleObjectPositionParseError<'a>, {
808    InvalidValue(val) => format!("Invalid object-position value: \"{}\"", val),
809}}
810
811#[cfg(feature = "parser")]
812#[derive(Debug, Clone, PartialEq)]
813#[repr(C, u8)]
814pub enum StyleObjectPositionParseErrorOwned {
815    InvalidValue(crate::AzString),
816}
817
818#[cfg(feature = "parser")]
819impl<'a> StyleObjectPositionParseError<'a> {
820    pub fn to_contained(&self) -> StyleObjectPositionParseErrorOwned {
821        match self {
822            Self::InvalidValue(s) => StyleObjectPositionParseErrorOwned::InvalidValue(s.to_string().into()),
823        }
824    }
825}
826
827#[cfg(feature = "parser")]
828impl StyleObjectPositionParseErrorOwned {
829    pub fn to_shared<'a>(&'a self) -> StyleObjectPositionParseError<'a> {
830        match self {
831            Self::InvalidValue(s) => StyleObjectPositionParseError::InvalidValue(s.as_str()),
832        }
833    }
834}
835
836/// Parse object-position: accepts keyword pairs or percentage/length values.
837/// Examples: "center", "left top", "50% 50%", "10px 20px"
838#[cfg(feature = "parser")]
839pub fn parse_style_object_position<'a>(
840    input: &'a str,
841) -> Result<StyleObjectPosition, StyleObjectPositionParseError<'a>> {
842    use crate::props::style::background::{
843        BackgroundPositionHorizontal, BackgroundPositionVertical,
844    };
845    use crate::props::basic::pixel::parse_pixel_value;
846
847    let input = input.trim();
848    let parts: Vec<&str> = input.split_whitespace().collect();
849
850    let (h, v) = match parts.len() {
851        1 => {
852            let val = parts[0];
853            match val {
854                "center" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Center),
855                "left" => (BackgroundPositionHorizontal::Left, BackgroundPositionVertical::Center),
856                "right" => (BackgroundPositionHorizontal::Right, BackgroundPositionVertical::Center),
857                "top" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Top),
858                "bottom" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Bottom),
859                _ => {
860                    let px = parse_pixel_value(val)
861                        .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
862                    (BackgroundPositionHorizontal::Exact(px), BackgroundPositionVertical::Exact(px))
863                }
864            }
865        }
866        2 => {
867            let h = match parts[0] {
868                "left" => BackgroundPositionHorizontal::Left,
869                "center" => BackgroundPositionHorizontal::Center,
870                "right" => BackgroundPositionHorizontal::Right,
871                other => {
872                    let px = parse_pixel_value(other)
873                        .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
874                    BackgroundPositionHorizontal::Exact(px)
875                }
876            };
877            let v = match parts[1] {
878                "top" => BackgroundPositionVertical::Top,
879                "center" => BackgroundPositionVertical::Center,
880                "bottom" => BackgroundPositionVertical::Bottom,
881                other => {
882                    let px = parse_pixel_value(other)
883                        .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
884                    BackgroundPositionVertical::Exact(px)
885                }
886            };
887            (h, v)
888        }
889        _ => return Err(StyleObjectPositionParseError::InvalidValue(input)),
890    };
891
892    Ok(StyleObjectPosition { horizontal: h, vertical: v })
893}
894
895// -- StyleAspectRatio --
896
897/// Width/height ratio stored as fixed-point (value * 1000).
898#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
899#[repr(C)]
900pub struct AspectRatioValue {
901    pub width: u32,
902    pub height: u32,
903}
904
905/// CSS aspect-ratio property: preferred aspect ratio for the box.
906/// CSS Box Sizing Level 4 §6 — values: `auto | <ratio>` (initial: `auto`)
907///
908/// Stored as width/height ratio. Auto means no preferred ratio.
909#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
910#[repr(C, u8)]
911#[derive(Default)]
912pub enum StyleAspectRatio {
913    /// No preferred aspect ratio
914    #[default]
915    Auto,
916    /// Fixed ratio (width / height), stored as fixed-point (value * 1000)
917    Ratio(AspectRatioValue),
918}
919
920
921crate::impl_option!(StyleAspectRatio, OptionStyleAspectRatio, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
922
923impl PrintAsCssValue for StyleAspectRatio {
924    fn print_as_css_value(&self) -> String {
925        match self {
926            StyleAspectRatio::Auto => String::from("auto"),
927            StyleAspectRatio::Ratio(r) => format!("{} / {}", r.width, r.height),
928        }
929    }
930}
931
932#[cfg(feature = "parser")]
933#[derive(Clone, PartialEq)]
934pub enum StyleAspectRatioParseError<'a> {
935    InvalidValue(&'a str),
936}
937
938#[cfg(feature = "parser")]
939crate::impl_debug_as_display!(StyleAspectRatioParseError<'a>);
940
941#[cfg(feature = "parser")]
942crate::impl_display! { StyleAspectRatioParseError<'a>, {
943    InvalidValue(val) => format!("Invalid aspect-ratio value: \"{}\"", val),
944}}
945
946#[cfg(feature = "parser")]
947#[derive(Debug, Clone, PartialEq)]
948#[repr(C, u8)]
949pub enum StyleAspectRatioParseErrorOwned {
950    InvalidValue(crate::AzString),
951}
952
953#[cfg(feature = "parser")]
954impl<'a> StyleAspectRatioParseError<'a> {
955    pub fn to_contained(&self) -> StyleAspectRatioParseErrorOwned {
956        match self {
957            Self::InvalidValue(s) => StyleAspectRatioParseErrorOwned::InvalidValue(s.to_string().into()),
958        }
959    }
960}
961
962#[cfg(feature = "parser")]
963impl StyleAspectRatioParseErrorOwned {
964    pub fn to_shared<'a>(&'a self) -> StyleAspectRatioParseError<'a> {
965        match self {
966            Self::InvalidValue(s) => StyleAspectRatioParseError::InvalidValue(s.as_str()),
967        }
968    }
969}
970
971/// Parse aspect-ratio: "auto", "16 / 9", "1.5", "4/3"
972#[cfg(feature = "parser")]
973pub fn parse_style_aspect_ratio<'a>(
974    input: &'a str,
975) -> Result<StyleAspectRatio, StyleAspectRatioParseError<'a>> {
976    let input = input.trim();
977    if input == "auto" {
978        return Ok(StyleAspectRatio::Auto);
979    }
980    // Try "w / h" or "w/h" format
981    if let Some(slash_pos) = input.find('/') {
982        let w_str = input[..slash_pos].trim();
983        let h_str = input[slash_pos + 1..].trim();
984        let w: f32 = w_str.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
985        let h: f32 = h_str.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
986        if h <= 0.0 || w <= 0.0 || w > 100_000.0 || h > 100_000.0 {
987            return Err(StyleAspectRatioParseError::InvalidValue(input));
988        }
989        return Ok(StyleAspectRatio::Ratio(AspectRatioValue {
990            width: (w * 1000.0).round() as u32,
991            height: (h * 1000.0).round() as u32,
992        }));
993    }
994    // Try single number (width/1)
995    let w: f32 = input.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
996    if w <= 0.0 || w > 100_000.0 {
997        return Err(StyleAspectRatioParseError::InvalidValue(input));
998    }
999    Ok(StyleAspectRatio::Ratio(AspectRatioValue {
1000        width: (w * 1000.0).round() as u32,
1001        height: 1000,
1002    }))
1003}