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        Self {
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    #[allow(clippy::wildcard_imports)] // parser submodule reuses the parent module's value types
212    use super::*;
213    use crate::corety::AzString;
214    use crate::props::basic::error::{InvalidValueErr, InvalidValueErrOwned};
215
216    // -- Opacity Parser --
217
218    #[derive(Clone, PartialEq, Eq)]
219    pub enum OpacityParseError<'a> {
220        ParsePercentage(PercentageParseError, &'a str),
221        OutOfRange(&'a str),
222    }
223    impl_debug_as_display!(OpacityParseError<'a>);
224    impl_display! { OpacityParseError<'a>, {
225        ParsePercentage(e, s) => format!("Invalid opacity value \"{}\": {}", s, e),
226        OutOfRange(s) => format!("Invalid opacity value \"{}\": must be between 0 and 1", s),
227    }}
228
229    /// Wrapper for `PercentageParseError` with input string.
230    #[derive(Debug, Clone, PartialEq, Eq)]
231    #[repr(C)]
232    pub struct PercentageParseErrorWithInput {
233        pub error: PercentageParseError,
234        pub input: AzString,
235    }
236
237    #[derive(Debug, Clone, PartialEq, Eq)]
238    #[repr(C, u8)]
239    pub enum OpacityParseErrorOwned {
240        ParsePercentage(PercentageParseErrorWithInput),
241        OutOfRange(AzString),
242    }
243
244    impl OpacityParseError<'_> {
245        #[must_use] pub fn to_contained(&self) -> OpacityParseErrorOwned {
246            match self {
247                Self::ParsePercentage(err, s) => {
248                    OpacityParseErrorOwned::ParsePercentage(PercentageParseErrorWithInput { error: err.clone(), input: (*s).to_string().into() })
249                }
250                Self::OutOfRange(s) => OpacityParseErrorOwned::OutOfRange((*s).to_string().into()),
251            }
252        }
253    }
254
255    impl OpacityParseErrorOwned {
256        #[must_use] pub fn to_shared(&self) -> OpacityParseError<'_> {
257            match self {
258                Self::ParsePercentage(e) => {
259                    OpacityParseError::ParsePercentage(e.error.clone(), e.input.as_str())
260                }
261                Self::OutOfRange(s) => OpacityParseError::OutOfRange(s.as_str()),
262            }
263        }
264    }
265
266    /// # Errors
267    ///
268    /// Returns an error if `input` is not a valid CSS `opacity` value.
269    pub fn parse_style_opacity(input: &str) -> Result<StyleOpacity, OpacityParseError<'_>> {
270        let val = parse_percentage_value(input)
271            .map_err(|e| OpacityParseError::ParsePercentage(e, input))?;
272
273        let normalized = val.normalized();
274        if !(0.0..=1.0).contains(&normalized) {
275            return Err(OpacityParseError::OutOfRange(input));
276        }
277
278        Ok(StyleOpacity { inner: val })
279    }
280
281    // -- Visibility Parser --
282
283    #[derive(Clone, PartialEq, Eq)]
284    pub enum StyleVisibilityParseError<'a> {
285        InvalidValue(InvalidValueErr<'a>),
286    }
287    impl_debug_as_display!(StyleVisibilityParseError<'a>);
288    impl_display! { StyleVisibilityParseError<'a>, {
289        InvalidValue(e) => format!("Invalid visibility value: \"{}\"", e.0),
290    }}
291    impl_from!(InvalidValueErr<'a>, StyleVisibilityParseError::InvalidValue);
292
293    #[derive(Debug, Clone, PartialEq, Eq)]
294    #[repr(C, u8)]
295    pub enum StyleVisibilityParseErrorOwned {
296        InvalidValue(InvalidValueErrOwned),
297    }
298
299    impl StyleVisibilityParseError<'_> {
300        #[must_use] pub fn to_contained(&self) -> StyleVisibilityParseErrorOwned {
301            match self {
302                Self::InvalidValue(e) => {
303                    StyleVisibilityParseErrorOwned::InvalidValue(e.to_contained())
304                }
305            }
306        }
307    }
308
309    impl StyleVisibilityParseErrorOwned {
310        #[must_use] pub fn to_shared(&self) -> StyleVisibilityParseError<'_> {
311            match self {
312                Self::InvalidValue(e) => StyleVisibilityParseError::InvalidValue(e.to_shared()),
313            }
314        }
315    }
316
317    /// # Errors
318    ///
319    /// Returns an error if `input` is not a valid CSS `visibility` value.
320    pub fn parse_style_visibility(
321        input: &str,
322    ) -> Result<StyleVisibility, StyleVisibilityParseError<'_>> {
323        let input = input.trim();
324        match input {
325            "visible" => Ok(StyleVisibility::Visible),
326            "hidden" => Ok(StyleVisibility::Hidden),
327            "collapse" => Ok(StyleVisibility::Collapse),
328            _ => Err(InvalidValueErr(input).into()),
329        }
330    }
331
332    // -- Mix Blend Mode Parser --
333
334    #[derive(Clone, PartialEq, Eq)]
335    pub enum MixBlendModeParseError<'a> {
336        InvalidValue(InvalidValueErr<'a>),
337    }
338    impl_debug_as_display!(MixBlendModeParseError<'a>);
339    impl_display! { MixBlendModeParseError<'a>, {
340        InvalidValue(e) => format!("Invalid mix-blend-mode value: \"{}\"", e.0),
341    }}
342    impl_from!(InvalidValueErr<'a>, MixBlendModeParseError::InvalidValue);
343
344    #[derive(Debug, Clone, PartialEq, Eq)]
345    #[repr(C, u8)]
346    pub enum MixBlendModeParseErrorOwned {
347        InvalidValue(InvalidValueErrOwned),
348    }
349
350    impl MixBlendModeParseError<'_> {
351        #[must_use] pub fn to_contained(&self) -> MixBlendModeParseErrorOwned {
352            match self {
353                Self::InvalidValue(e) => {
354                    MixBlendModeParseErrorOwned::InvalidValue(e.to_contained())
355                }
356            }
357        }
358    }
359
360    impl MixBlendModeParseErrorOwned {
361        #[must_use] pub fn to_shared(&self) -> MixBlendModeParseError<'_> {
362            match self {
363                Self::InvalidValue(e) => MixBlendModeParseError::InvalidValue(e.to_shared()),
364            }
365        }
366    }
367
368    /// # Errors
369    ///
370    /// Returns an error if `input` is not a valid CSS `mix-blend-mode` value.
371    pub fn parse_style_mix_blend_mode(
372        input: &str,
373    ) -> Result<StyleMixBlendMode, MixBlendModeParseError<'_>> {
374        let input = input.trim();
375        match input {
376            "normal" => Ok(StyleMixBlendMode::Normal),
377            "multiply" => Ok(StyleMixBlendMode::Multiply),
378            "screen" => Ok(StyleMixBlendMode::Screen),
379            "overlay" => Ok(StyleMixBlendMode::Overlay),
380            "darken" => Ok(StyleMixBlendMode::Darken),
381            "lighten" => Ok(StyleMixBlendMode::Lighten),
382            "color-dodge" => Ok(StyleMixBlendMode::ColorDodge),
383            "color-burn" => Ok(StyleMixBlendMode::ColorBurn),
384            "hard-light" => Ok(StyleMixBlendMode::HardLight),
385            "soft-light" => Ok(StyleMixBlendMode::SoftLight),
386            "difference" => Ok(StyleMixBlendMode::Difference),
387            "exclusion" => Ok(StyleMixBlendMode::Exclusion),
388            "hue" => Ok(StyleMixBlendMode::Hue),
389            "saturation" => Ok(StyleMixBlendMode::Saturation),
390            "color" => Ok(StyleMixBlendMode::Color),
391            "luminosity" => Ok(StyleMixBlendMode::Luminosity),
392            _ => Err(InvalidValueErr(input).into()),
393        }
394    }
395
396    // -- Cursor Parser --
397
398    #[derive(Clone, PartialEq, Eq)]
399    pub enum CursorParseError<'a> {
400        InvalidValue(InvalidValueErr<'a>),
401    }
402    impl_debug_as_display!(CursorParseError<'a>);
403    impl_display! { CursorParseError<'a>, {
404        InvalidValue(e) => format!("Invalid cursor value: \"{}\"", e.0),
405    }}
406    impl_from!(InvalidValueErr<'a>, CursorParseError::InvalidValue);
407
408    #[derive(Debug, Clone, PartialEq, Eq)]
409    #[repr(C, u8)]
410    pub enum CursorParseErrorOwned {
411        InvalidValue(InvalidValueErrOwned),
412    }
413
414    impl CursorParseError<'_> {
415        #[must_use] pub fn to_contained(&self) -> CursorParseErrorOwned {
416            match self {
417                Self::InvalidValue(e) => CursorParseErrorOwned::InvalidValue(e.to_contained()),
418            }
419        }
420    }
421
422    impl CursorParseErrorOwned {
423        #[must_use] pub fn to_shared(&self) -> CursorParseError<'_> {
424            match self {
425                Self::InvalidValue(e) => CursorParseError::InvalidValue(e.to_shared()),
426            }
427        }
428    }
429
430    /// # Errors
431    ///
432    /// Returns an error if `input` is not a valid CSS `cursor` value.
433    pub fn parse_style_cursor(input: &str) -> Result<StyleCursor, CursorParseError<'_>> {
434        let input = input.trim();
435        match input {
436            "alias" => Ok(StyleCursor::Alias),
437            "all-scroll" => Ok(StyleCursor::AllScroll),
438            "cell" => Ok(StyleCursor::Cell),
439            "col-resize" => Ok(StyleCursor::ColResize),
440            "context-menu" => Ok(StyleCursor::ContextMenu),
441            "copy" => Ok(StyleCursor::Copy),
442            "crosshair" => Ok(StyleCursor::Crosshair),
443            "default" => Ok(StyleCursor::Default),
444            "e-resize" => Ok(StyleCursor::EResize),
445            "ew-resize" => Ok(StyleCursor::EwResize),
446            "grab" => Ok(StyleCursor::Grab),
447            "grabbing" => Ok(StyleCursor::Grabbing),
448            "help" => Ok(StyleCursor::Help),
449            "move" => Ok(StyleCursor::Move),
450            "n-resize" => Ok(StyleCursor::NResize),
451            "ns-resize" => Ok(StyleCursor::NsResize),
452            "nesw-resize" => Ok(StyleCursor::NeswResize),
453            "nwse-resize" => Ok(StyleCursor::NwseResize),
454            "pointer" => Ok(StyleCursor::Pointer),
455            "progress" => Ok(StyleCursor::Progress),
456            "row-resize" => Ok(StyleCursor::RowResize),
457            "s-resize" => Ok(StyleCursor::SResize),
458            "se-resize" => Ok(StyleCursor::SeResize),
459            "text" => Ok(StyleCursor::Text),
460            "unset" => Ok(StyleCursor::Unset),
461            "vertical-text" => Ok(StyleCursor::VerticalText),
462            "w-resize" => Ok(StyleCursor::WResize),
463            "wait" => Ok(StyleCursor::Wait),
464            "zoom-in" => Ok(StyleCursor::ZoomIn),
465            "zoom-out" => Ok(StyleCursor::ZoomOut),
466            _ => Err(InvalidValueErr(input).into()),
467        }
468    }
469}
470
471#[cfg(feature = "parser")]
472pub use self::parsers::*;
473
474#[cfg(all(test, feature = "parser"))]
475mod tests {
476    // Tests assert that parsed values equal the exact source literals.
477    #![allow(clippy::float_cmp)]
478    use super::*;
479
480    #[test]
481    fn test_parse_opacity() {
482        assert_eq!(parse_style_opacity("0.5").unwrap().inner.normalized(), 0.5);
483        assert_eq!(parse_style_opacity("1").unwrap().inner.normalized(), 1.0);
484        assert_eq!(parse_style_opacity("50%").unwrap().inner.normalized(), 0.5);
485        assert_eq!(parse_style_opacity("0").unwrap().inner.normalized(), 0.0);
486        assert_eq!(
487            parse_style_opacity("  75%  ").unwrap().inner.normalized(),
488            0.75
489        );
490        assert!(parse_style_opacity("1.1").is_err());
491        assert!(parse_style_opacity("-0.1").is_err());
492        assert!(parse_style_opacity("auto").is_err());
493    }
494
495    #[test]
496    fn test_parse_mix_blend_mode() {
497        assert_eq!(
498            parse_style_mix_blend_mode("multiply").unwrap(),
499            StyleMixBlendMode::Multiply
500        );
501        assert_eq!(
502            parse_style_mix_blend_mode("screen").unwrap(),
503            StyleMixBlendMode::Screen
504        );
505        assert_eq!(
506            parse_style_mix_blend_mode("color-dodge").unwrap(),
507            StyleMixBlendMode::ColorDodge
508        );
509        assert!(parse_style_mix_blend_mode("mix").is_err());
510    }
511
512    #[test]
513    fn test_parse_visibility() {
514        assert_eq!(
515            parse_style_visibility("visible").unwrap(),
516            StyleVisibility::Visible
517        );
518        assert_eq!(
519            parse_style_visibility("hidden").unwrap(),
520            StyleVisibility::Hidden
521        );
522        assert_eq!(
523            parse_style_visibility("collapse").unwrap(),
524            StyleVisibility::Collapse
525        );
526        assert_eq!(
527            parse_style_visibility("  visible  ").unwrap(),
528            StyleVisibility::Visible
529        );
530        assert!(parse_style_visibility("none").is_err());
531        assert!(parse_style_visibility("show").is_err());
532    }
533
534    #[test]
535    fn test_parse_cursor() {
536        assert_eq!(parse_style_cursor("pointer").unwrap(), StyleCursor::Pointer);
537        assert_eq!(parse_style_cursor("wait").unwrap(), StyleCursor::Wait);
538        assert_eq!(
539            parse_style_cursor("col-resize").unwrap(),
540            StyleCursor::ColResize
541        );
542        assert_eq!(parse_style_cursor("  text  ").unwrap(), StyleCursor::Text);
543        assert!(parse_style_cursor("hand").is_err()); // "hand" is a legacy IE value
544    }
545
546    #[test]
547    fn test_parse_object_fit() {
548        assert_eq!(parse_style_object_fit("fill").unwrap(), StyleObjectFit::Fill);
549        assert_eq!(parse_style_object_fit("contain").unwrap(), StyleObjectFit::Contain);
550        assert_eq!(parse_style_object_fit("cover").unwrap(), StyleObjectFit::Cover);
551        assert_eq!(parse_style_object_fit("none").unwrap(), StyleObjectFit::None);
552        assert_eq!(parse_style_object_fit("scale-down").unwrap(), StyleObjectFit::ScaleDown);
553        assert_eq!(parse_style_object_fit("  cover  ").unwrap(), StyleObjectFit::Cover);
554        assert!(parse_style_object_fit("stretch").is_err());
555        assert!(parse_style_object_fit("").is_err());
556    }
557
558    #[test]
559    fn test_parse_text_orientation() {
560        assert_eq!(parse_style_text_orientation("mixed").unwrap(), StyleTextOrientation::Mixed);
561        assert_eq!(parse_style_text_orientation("upright").unwrap(), StyleTextOrientation::Upright);
562        assert_eq!(parse_style_text_orientation("sideways").unwrap(), StyleTextOrientation::Sideways);
563        assert_eq!(parse_style_text_orientation("  mixed  ").unwrap(), StyleTextOrientation::Mixed);
564        assert!(parse_style_text_orientation("vertical").is_err());
565    }
566
567    #[test]
568    fn test_parse_object_position() {
569        use crate::props::style::background::{BackgroundPositionHorizontal, BackgroundPositionVertical};
570        let centered = parse_style_object_position("center").unwrap();
571        assert_eq!(centered, parse_style_object_position("center center").unwrap());
572
573        let lt = parse_style_object_position("left top").unwrap();
574        assert_eq!(lt.horizontal, BackgroundPositionHorizontal::Left);
575        assert_eq!(lt.vertical, BackgroundPositionVertical::Top);
576
577        let rb = parse_style_object_position("right bottom").unwrap();
578        assert_eq!(rb.horizontal, BackgroundPositionHorizontal::Right);
579        assert_eq!(rb.vertical, BackgroundPositionVertical::Bottom);
580
581        assert!(parse_style_object_position("left top center").is_err());
582        assert!(parse_style_object_position("invalid").is_err());
583    }
584
585    #[test]
586    fn test_parse_aspect_ratio() {
587        assert_eq!(parse_style_aspect_ratio("auto").unwrap(), StyleAspectRatio::Auto);
588        assert_eq!(
589            parse_style_aspect_ratio("16 / 9").unwrap(),
590            StyleAspectRatio::Ratio(AspectRatioValue { width: 16000, height: 9000 })
591        );
592        assert_eq!(
593            parse_style_aspect_ratio("16/9").unwrap(),
594            StyleAspectRatio::Ratio(AspectRatioValue { width: 16000, height: 9000 })
595        );
596        assert_eq!(
597            parse_style_aspect_ratio("1.5").unwrap(),
598            StyleAspectRatio::Ratio(AspectRatioValue { width: 1500, height: 1000 })
599        );
600        assert_eq!(
601            parse_style_aspect_ratio("  4 / 3  ").unwrap(),
602            StyleAspectRatio::Ratio(AspectRatioValue { width: 4000, height: 3000 })
603        );
604        assert!(parse_style_aspect_ratio("0 / 1").is_err());
605        assert!(parse_style_aspect_ratio("1 / 0").is_err());
606        assert!(parse_style_aspect_ratio("-1 / 1").is_err());
607        assert!(parse_style_aspect_ratio("abc").is_err());
608    }
609}
610
611// -- StyleObjectFit --
612
613/// CSS object-fit property: how replaced element content is fitted to its box.
614/// CSS Images Level 3 §5.5
615#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
616#[repr(C)]
617#[derive(Default)]
618pub enum StyleObjectFit {
619    #[default]
620    Fill,
621    Contain,
622    Cover,
623    None,
624    ScaleDown,
625}
626
627
628crate::impl_option!(StyleObjectFit, OptionStyleObjectFit, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
629
630impl PrintAsCssValue for StyleObjectFit {
631    fn print_as_css_value(&self) -> String {
632        String::from(match self {
633            Self::Fill => "fill",
634            Self::Contain => "contain",
635            Self::Cover => "cover",
636            Self::None => "none",
637            Self::ScaleDown => "scale-down",
638        })
639    }
640}
641
642#[cfg(feature = "parser")]
643#[derive(Clone, PartialEq, Eq)]
644pub enum StyleObjectFitParseError<'a> {
645    InvalidValue(&'a str),
646}
647
648#[cfg(feature = "parser")]
649crate::impl_debug_as_display!(StyleObjectFitParseError<'a>);
650
651#[cfg(feature = "parser")]
652crate::impl_display! { StyleObjectFitParseError<'a>, {
653    InvalidValue(val) => format!("Invalid object-fit value: \"{}\"", val),
654}}
655
656#[cfg(feature = "parser")]
657#[derive(Debug, Clone, PartialEq, Eq)]
658#[repr(C, u8)]
659pub enum StyleObjectFitParseErrorOwned {
660    InvalidValue(crate::AzString),
661}
662
663#[cfg(feature = "parser")]
664impl StyleObjectFitParseError<'_> {
665    #[must_use] pub fn to_contained(&self) -> StyleObjectFitParseErrorOwned {
666        match self {
667            Self::InvalidValue(s) => StyleObjectFitParseErrorOwned::InvalidValue((*s).to_string().into()),
668        }
669    }
670}
671
672#[cfg(feature = "parser")]
673impl StyleObjectFitParseErrorOwned {
674    #[must_use] pub fn to_shared(&self) -> StyleObjectFitParseError<'_> {
675        match self {
676            Self::InvalidValue(s) => StyleObjectFitParseError::InvalidValue(s.as_str()),
677        }
678    }
679}
680
681#[cfg(feature = "parser")]
682/// # Errors
683///
684/// Returns an error if `input` is not a valid CSS `object-fit` value.
685pub fn parse_style_object_fit(
686    input: &str,
687) -> Result<StyleObjectFit, StyleObjectFitParseError<'_>> {
688    let input = input.trim();
689    match input {
690        "fill" => Ok(StyleObjectFit::Fill),
691        "contain" => Ok(StyleObjectFit::Contain),
692        "cover" => Ok(StyleObjectFit::Cover),
693        "none" => Ok(StyleObjectFit::None),
694        "scale-down" => Ok(StyleObjectFit::ScaleDown),
695        _ => Err(StyleObjectFitParseError::InvalidValue(input)),
696    }
697}
698
699// -- StyleTextOrientation --
700
701/// CSS text-orientation property for vertical writing modes.
702/// CSS Writing Modes Level 4 §5.1
703#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
704#[repr(C)]
705#[derive(Default)]
706pub enum StyleTextOrientation {
707    #[default]
708    Mixed,
709    Upright,
710    Sideways,
711}
712
713
714crate::impl_option!(StyleTextOrientation, OptionStyleTextOrientation, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
715
716impl PrintAsCssValue for StyleTextOrientation {
717    fn print_as_css_value(&self) -> String {
718        String::from(match self {
719            Self::Mixed => "mixed",
720            Self::Upright => "upright",
721            Self::Sideways => "sideways",
722        })
723    }
724}
725
726#[cfg(feature = "parser")]
727#[derive(Clone, PartialEq, Eq)]
728pub enum StyleTextOrientationParseError<'a> {
729    InvalidValue(&'a str),
730}
731
732#[cfg(feature = "parser")]
733crate::impl_debug_as_display!(StyleTextOrientationParseError<'a>);
734
735#[cfg(feature = "parser")]
736crate::impl_display! { StyleTextOrientationParseError<'a>, {
737    InvalidValue(val) => format!("Invalid text-orientation value: \"{}\"", val),
738}}
739
740#[cfg(feature = "parser")]
741#[derive(Debug, Clone, PartialEq, Eq)]
742#[repr(C, u8)]
743pub enum StyleTextOrientationParseErrorOwned {
744    InvalidValue(crate::AzString),
745}
746
747#[cfg(feature = "parser")]
748impl StyleTextOrientationParseError<'_> {
749    #[must_use] pub fn to_contained(&self) -> StyleTextOrientationParseErrorOwned {
750        match self {
751            Self::InvalidValue(s) => StyleTextOrientationParseErrorOwned::InvalidValue((*s).to_string().into()),
752        }
753    }
754}
755
756#[cfg(feature = "parser")]
757impl StyleTextOrientationParseErrorOwned {
758    #[must_use] pub fn to_shared(&self) -> StyleTextOrientationParseError<'_> {
759        match self {
760            Self::InvalidValue(s) => StyleTextOrientationParseError::InvalidValue(s.as_str()),
761        }
762    }
763}
764
765#[cfg(feature = "parser")]
766/// # Errors
767///
768/// Returns an error if `input` is not a valid CSS `text-orientation` value.
769pub fn parse_style_text_orientation(
770    input: &str,
771) -> Result<StyleTextOrientation, StyleTextOrientationParseError<'_>> {
772    let input = input.trim();
773    match input {
774        "mixed" => Ok(StyleTextOrientation::Mixed),
775        "upright" => Ok(StyleTextOrientation::Upright),
776        "sideways" => Ok(StyleTextOrientation::Sideways),
777        _ => Err(StyleTextOrientationParseError::InvalidValue(input)),
778    }
779}
780
781// -- StyleObjectPosition --
782
783/// CSS object-position property: position of replaced element content within its box.
784/// CSS Images Level 3 §5.6 — default: `50% 50%` (centered)
785#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
786#[repr(C)]
787pub struct StyleObjectPosition {
788    pub horizontal: crate::props::style::background::BackgroundPositionHorizontal,
789    pub vertical: crate::props::style::background::BackgroundPositionVertical,
790}
791
792impl Default for StyleObjectPosition {
793    fn default() -> Self {
794        use crate::props::basic::pixel::PixelValue;
795        Self {
796            horizontal: crate::props::style::background::BackgroundPositionHorizontal::Exact(
797                PixelValue::percent(50.0),
798            ),
799            vertical: crate::props::style::background::BackgroundPositionVertical::Exact(
800                PixelValue::percent(50.0),
801            ),
802        }
803    }
804}
805
806crate::impl_option!(StyleObjectPosition, OptionStyleObjectPosition, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
807
808impl PrintAsCssValue for StyleObjectPosition {
809    fn print_as_css_value(&self) -> String {
810        format!(
811            "{} {}",
812            self.horizontal.print_as_css_value(),
813            self.vertical.print_as_css_value()
814        )
815    }
816}
817
818#[cfg(feature = "parser")]
819#[derive(Clone, PartialEq, Eq)]
820pub enum StyleObjectPositionParseError<'a> {
821    InvalidValue(&'a str),
822}
823
824#[cfg(feature = "parser")]
825crate::impl_debug_as_display!(StyleObjectPositionParseError<'a>);
826
827#[cfg(feature = "parser")]
828crate::impl_display! { StyleObjectPositionParseError<'a>, {
829    InvalidValue(val) => format!("Invalid object-position value: \"{}\"", val),
830}}
831
832#[cfg(feature = "parser")]
833#[derive(Debug, Clone, PartialEq, Eq)]
834#[repr(C, u8)]
835pub enum StyleObjectPositionParseErrorOwned {
836    InvalidValue(crate::AzString),
837}
838
839#[cfg(feature = "parser")]
840impl StyleObjectPositionParseError<'_> {
841    #[must_use] pub fn to_contained(&self) -> StyleObjectPositionParseErrorOwned {
842        match self {
843            Self::InvalidValue(s) => StyleObjectPositionParseErrorOwned::InvalidValue((*s).to_string().into()),
844        }
845    }
846}
847
848#[cfg(feature = "parser")]
849impl StyleObjectPositionParseErrorOwned {
850    #[must_use] pub fn to_shared(&self) -> StyleObjectPositionParseError<'_> {
851        match self {
852            Self::InvalidValue(s) => StyleObjectPositionParseError::InvalidValue(s.as_str()),
853        }
854    }
855}
856
857/// Parse object-position: accepts keyword pairs or percentage/length values.
858/// Examples: "center", "left top", "50% 50%", "10px 20px"
859#[cfg(feature = "parser")]
860/// # Errors
861///
862/// Returns an error if `input` is not a valid CSS `object-position` value.
863pub fn parse_style_object_position(
864    input: &str,
865) -> Result<StyleObjectPosition, StyleObjectPositionParseError<'_>> {
866    use crate::props::style::background::{
867        BackgroundPositionHorizontal, BackgroundPositionVertical,
868    };
869    use crate::props::basic::pixel::parse_pixel_value;
870
871    let input = input.trim();
872    let parts: Vec<&str> = input.split_whitespace().collect();
873
874    let (h, v) = match parts.len() {
875        1 => {
876            let val = parts[0];
877            match val {
878                "center" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Center),
879                "left" => (BackgroundPositionHorizontal::Left, BackgroundPositionVertical::Center),
880                "right" => (BackgroundPositionHorizontal::Right, BackgroundPositionVertical::Center),
881                "top" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Top),
882                "bottom" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Bottom),
883                _ => {
884                    let px = parse_pixel_value(val)
885                        .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
886                    (BackgroundPositionHorizontal::Exact(px), BackgroundPositionVertical::Exact(px))
887                }
888            }
889        }
890        2 => {
891            let h = match parts[0] {
892                "left" => BackgroundPositionHorizontal::Left,
893                "center" => BackgroundPositionHorizontal::Center,
894                "right" => BackgroundPositionHorizontal::Right,
895                other => {
896                    let px = parse_pixel_value(other)
897                        .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
898                    BackgroundPositionHorizontal::Exact(px)
899                }
900            };
901            let v = match parts[1] {
902                "top" => BackgroundPositionVertical::Top,
903                "center" => BackgroundPositionVertical::Center,
904                "bottom" => BackgroundPositionVertical::Bottom,
905                other => {
906                    let px = parse_pixel_value(other)
907                        .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
908                    BackgroundPositionVertical::Exact(px)
909                }
910            };
911            (h, v)
912        }
913        _ => return Err(StyleObjectPositionParseError::InvalidValue(input)),
914    };
915
916    Ok(StyleObjectPosition { horizontal: h, vertical: v })
917}
918
919// -- StyleAspectRatio --
920
921/// Width/height ratio stored as fixed-point (value * 1000).
922#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
923#[repr(C)]
924pub struct AspectRatioValue {
925    pub width: u32,
926    pub height: u32,
927}
928#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
929/// CSS aspect-ratio property: preferred aspect ratio for the box.
930/// CSS Box Sizing Level 4 §6 — values: `auto | <ratio>` (initial: `auto`)
931///
932/// Stored as width/height ratio. Auto means no preferred ratio.
933#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
934#[repr(C, u8)]
935#[derive(Default)]
936pub enum StyleAspectRatio {
937    /// No preferred aspect ratio
938    #[default]
939    Auto,
940    /// Fixed ratio (width / height), stored as fixed-point (value * 1000)
941    Ratio(AspectRatioValue),
942}
943
944
945crate::impl_option!(StyleAspectRatio, OptionStyleAspectRatio, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
946
947impl PrintAsCssValue for StyleAspectRatio {
948    fn print_as_css_value(&self) -> String {
949        match self {
950            Self::Auto => String::from("auto"),
951            Self::Ratio(r) => format!("{} / {}", r.width, r.height),
952        }
953    }
954}
955
956#[cfg(feature = "parser")]
957#[derive(Clone, PartialEq, Eq)]
958pub enum StyleAspectRatioParseError<'a> {
959    InvalidValue(&'a str),
960}
961
962#[cfg(feature = "parser")]
963crate::impl_debug_as_display!(StyleAspectRatioParseError<'a>);
964
965#[cfg(feature = "parser")]
966crate::impl_display! { StyleAspectRatioParseError<'a>, {
967    InvalidValue(val) => format!("Invalid aspect-ratio value: \"{}\"", val),
968}}
969
970#[cfg(feature = "parser")]
971#[derive(Debug, Clone, PartialEq, Eq)]
972#[repr(C, u8)]
973pub enum StyleAspectRatioParseErrorOwned {
974    InvalidValue(crate::AzString),
975}
976
977#[cfg(feature = "parser")]
978impl StyleAspectRatioParseError<'_> {
979    #[must_use] pub fn to_contained(&self) -> StyleAspectRatioParseErrorOwned {
980        match self {
981            Self::InvalidValue(s) => StyleAspectRatioParseErrorOwned::InvalidValue((*s).to_string().into()),
982        }
983    }
984}
985
986#[cfg(feature = "parser")]
987impl StyleAspectRatioParseErrorOwned {
988    #[must_use] pub fn to_shared(&self) -> StyleAspectRatioParseError<'_> {
989        match self {
990            Self::InvalidValue(s) => StyleAspectRatioParseError::InvalidValue(s.as_str()),
991        }
992    }
993}
994
995/// Truncating `f32` → `u32` for aspect-ratio values (callers validate the input
996/// is positive and bounded, so the value always fits). Rust's `as u32` saturates
997/// out-of-range floats; this isolates the one unavoidable float→int cast.
998#[cfg(feature = "parser")]
999#[inline]
1000#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1001const fn aspect_f32_to_u32(v: f32) -> u32 {
1002    v as u32
1003}
1004
1005/// Parse aspect-ratio: "auto", "16 / 9", "1.5", "4/3"
1006#[cfg(feature = "parser")]
1007/// # Errors
1008///
1009/// Returns an error if `input` is not a valid CSS `aspect-ratio` value.
1010pub fn parse_style_aspect_ratio(
1011    input: &str,
1012) -> Result<StyleAspectRatio, StyleAspectRatioParseError<'_>> {
1013    let input = input.trim();
1014    if input == "auto" {
1015        return Ok(StyleAspectRatio::Auto);
1016    }
1017    // Try "w / h" or "w/h" format
1018    if let Some(slash_pos) = input.find('/') {
1019        let w_str = input[..slash_pos].trim();
1020        let h_str = input[slash_pos + 1..].trim();
1021        let w: f32 = w_str.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
1022        let h: f32 = h_str.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
1023        if h <= 0.0 || w <= 0.0 || w > 100_000.0 || h > 100_000.0 {
1024            return Err(StyleAspectRatioParseError::InvalidValue(input));
1025        }
1026        return Ok(StyleAspectRatio::Ratio(AspectRatioValue {
1027            width: aspect_f32_to_u32((w * 1000.0).round()),
1028            height: aspect_f32_to_u32((h * 1000.0).round()),
1029        }));
1030    }
1031    // Try single number (width/1)
1032    let w: f32 = input.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
1033    if w <= 0.0 || w > 100_000.0 {
1034        return Err(StyleAspectRatioParseError::InvalidValue(input));
1035    }
1036    Ok(StyleAspectRatio::Ratio(AspectRatioValue {
1037        width: aspect_f32_to_u32((w * 1000.0).round()),
1038        height: 1000,
1039    }))
1040}