1
//! CSS properties for visual effects (opacity, blending, cursor), box sizing
2
//! (object-fit, object-position, aspect-ratio), and text orientation.
3

            
4
use alloc::string::{String, ToString};
5
use core::fmt;
6

            
7
#[cfg(feature = "parser")]
8
use crate::props::basic::{
9
    error::{InvalidValueErr, InvalidValueErrOwned},
10
    length::parse_percentage_value,
11
};
12
use 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)]
22
pub struct StyleOpacity {
23
    pub inner: PercentageValue,
24
}
25

            
26
impl Default for StyleOpacity {
27
    fn default() -> Self {
28
        StyleOpacity {
29
            inner: PercentageValue::const_new(100),
30
        }
31
    }
32
}
33

            
34
impl PrintAsCssValue for StyleOpacity {
35
    fn print_as_css_value(&self) -> String {
36
        format!("{}", self.inner.normalized())
37
    }
38
}
39

            
40
#[cfg(feature = "parser")]
41
impl_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)]
49
pub enum StyleVisibility {
50
    #[default]
51
    Visible,
52
    Hidden,
53
    Collapse,
54
}
55

            
56

            
57
impl 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)]
74
pub 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

            
95
impl 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

            
122
impl 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)]
135
pub 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

            
170
impl 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")]
210
pub 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
134
    pub fn parse_style_opacity<'a>(input: &'a str) -> Result<StyleOpacity, OpacityParseError<'a>> {
266
134
        let val = parse_percentage_value(input)
267
134
            .map_err(|e| OpacityParseError::ParsePercentage(e, input))?;
268

            
269
133
        let normalized = val.normalized();
270
133
        if !(0.0..=1.0).contains(&normalized) {
271
2
            return Err(OpacityParseError::OutOfRange(input));
272
131
        }
273

            
274
131
        Ok(StyleOpacity { inner: val })
275
134
    }
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
25
    pub fn parse_style_visibility<'a>(
314
25
        input: &'a str,
315
25
    ) -> Result<StyleVisibility, StyleVisibilityParseError<'a>> {
316
25
        let input = input.trim();
317
25
        match input {
318
25
            "visible" => Ok(StyleVisibility::Visible),
319
23
            "hidden" => Ok(StyleVisibility::Hidden),
320
3
            "collapse" => Ok(StyleVisibility::Collapse),
321
2
            _ => Err(InvalidValueErr(input).into()),
322
        }
323
25
    }
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
4
    pub fn parse_style_mix_blend_mode<'a>(
362
4
        input: &'a str,
363
4
    ) -> Result<StyleMixBlendMode, MixBlendModeParseError<'a>> {
364
4
        let input = input.trim();
365
4
        match input {
366
4
            "normal" => Ok(StyleMixBlendMode::Normal),
367
4
            "multiply" => Ok(StyleMixBlendMode::Multiply),
368
3
            "screen" => Ok(StyleMixBlendMode::Screen),
369
2
            "overlay" => Ok(StyleMixBlendMode::Overlay),
370
2
            "darken" => Ok(StyleMixBlendMode::Darken),
371
2
            "lighten" => Ok(StyleMixBlendMode::Lighten),
372
2
            "color-dodge" => Ok(StyleMixBlendMode::ColorDodge),
373
1
            "color-burn" => Ok(StyleMixBlendMode::ColorBurn),
374
1
            "hard-light" => Ok(StyleMixBlendMode::HardLight),
375
1
            "soft-light" => Ok(StyleMixBlendMode::SoftLight),
376
1
            "difference" => Ok(StyleMixBlendMode::Difference),
377
1
            "exclusion" => Ok(StyleMixBlendMode::Exclusion),
378
1
            "hue" => Ok(StyleMixBlendMode::Hue),
379
1
            "saturation" => Ok(StyleMixBlendMode::Saturation),
380
1
            "color" => Ok(StyleMixBlendMode::Color),
381
1
            "luminosity" => Ok(StyleMixBlendMode::Luminosity),
382
1
            _ => Err(InvalidValueErr(input).into()),
383
        }
384
4
    }
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
5
    pub fn parse_style_cursor<'a>(input: &'a str) -> Result<StyleCursor, CursorParseError<'a>> {
421
5
        let input = input.trim();
422
5
        match input {
423
5
            "alias" => Ok(StyleCursor::Alias),
424
5
            "all-scroll" => Ok(StyleCursor::AllScroll),
425
5
            "cell" => Ok(StyleCursor::Cell),
426
5
            "col-resize" => Ok(StyleCursor::ColResize),
427
4
            "context-menu" => Ok(StyleCursor::ContextMenu),
428
4
            "copy" => Ok(StyleCursor::Copy),
429
4
            "crosshair" => Ok(StyleCursor::Crosshair),
430
4
            "default" => Ok(StyleCursor::Default),
431
4
            "e-resize" => Ok(StyleCursor::EResize),
432
4
            "ew-resize" => Ok(StyleCursor::EwResize),
433
4
            "grab" => Ok(StyleCursor::Grab),
434
4
            "grabbing" => Ok(StyleCursor::Grabbing),
435
4
            "help" => Ok(StyleCursor::Help),
436
4
            "move" => Ok(StyleCursor::Move),
437
4
            "n-resize" => Ok(StyleCursor::NResize),
438
4
            "ns-resize" => Ok(StyleCursor::NsResize),
439
4
            "nesw-resize" => Ok(StyleCursor::NeswResize),
440
4
            "nwse-resize" => Ok(StyleCursor::NwseResize),
441
4
            "pointer" => Ok(StyleCursor::Pointer),
442
3
            "progress" => Ok(StyleCursor::Progress),
443
3
            "row-resize" => Ok(StyleCursor::RowResize),
444
3
            "s-resize" => Ok(StyleCursor::SResize),
445
3
            "se-resize" => Ok(StyleCursor::SeResize),
446
3
            "text" => Ok(StyleCursor::Text),
447
2
            "unset" => Ok(StyleCursor::Unset),
448
2
            "vertical-text" => Ok(StyleCursor::VerticalText),
449
2
            "w-resize" => Ok(StyleCursor::WResize),
450
2
            "wait" => Ok(StyleCursor::Wait),
451
1
            "zoom-in" => Ok(StyleCursor::ZoomIn),
452
1
            "zoom-out" => Ok(StyleCursor::ZoomOut),
453
1
            _ => Err(InvalidValueErr(input).into()),
454
        }
455
5
    }
456
}
457

            
458
#[cfg(feature = "parser")]
459
pub use self::parsers::*;
460

            
461
#[cfg(all(test, feature = "parser"))]
462
mod tests {
463
    use super::*;
464

            
465
    #[test]
466
1
    fn test_parse_opacity() {
467
1
        assert_eq!(parse_style_opacity("0.5").unwrap().inner.normalized(), 0.5);
468
1
        assert_eq!(parse_style_opacity("1").unwrap().inner.normalized(), 1.0);
469
1
        assert_eq!(parse_style_opacity("50%").unwrap().inner.normalized(), 0.5);
470
1
        assert_eq!(parse_style_opacity("0").unwrap().inner.normalized(), 0.0);
471
1
        assert_eq!(
472
1
            parse_style_opacity("  75%  ").unwrap().inner.normalized(),
473
            0.75
474
        );
475
1
        assert!(parse_style_opacity("1.1").is_err());
476
1
        assert!(parse_style_opacity("-0.1").is_err());
477
1
        assert!(parse_style_opacity("auto").is_err());
478
1
    }
479

            
480
    #[test]
481
1
    fn test_parse_mix_blend_mode() {
482
1
        assert_eq!(
483
1
            parse_style_mix_blend_mode("multiply").unwrap(),
484
            StyleMixBlendMode::Multiply
485
        );
486
1
        assert_eq!(
487
1
            parse_style_mix_blend_mode("screen").unwrap(),
488
            StyleMixBlendMode::Screen
489
        );
490
1
        assert_eq!(
491
1
            parse_style_mix_blend_mode("color-dodge").unwrap(),
492
            StyleMixBlendMode::ColorDodge
493
        );
494
1
        assert!(parse_style_mix_blend_mode("mix").is_err());
495
1
    }
496

            
497
    #[test]
498
1
    fn test_parse_visibility() {
499
1
        assert_eq!(
500
1
            parse_style_visibility("visible").unwrap(),
501
            StyleVisibility::Visible
502
        );
503
1
        assert_eq!(
504
1
            parse_style_visibility("hidden").unwrap(),
505
            StyleVisibility::Hidden
506
        );
507
1
        assert_eq!(
508
1
            parse_style_visibility("collapse").unwrap(),
509
            StyleVisibility::Collapse
510
        );
511
1
        assert_eq!(
512
1
            parse_style_visibility("  visible  ").unwrap(),
513
            StyleVisibility::Visible
514
        );
515
1
        assert!(parse_style_visibility("none").is_err());
516
1
        assert!(parse_style_visibility("show").is_err());
517
1
    }
518

            
519
    #[test]
520
1
    fn test_parse_cursor() {
521
1
        assert_eq!(parse_style_cursor("pointer").unwrap(), StyleCursor::Pointer);
522
1
        assert_eq!(parse_style_cursor("wait").unwrap(), StyleCursor::Wait);
523
1
        assert_eq!(
524
1
            parse_style_cursor("col-resize").unwrap(),
525
            StyleCursor::ColResize
526
        );
527
1
        assert_eq!(parse_style_cursor("  text  ").unwrap(), StyleCursor::Text);
528
1
        assert!(parse_style_cursor("hand").is_err()); // "hand" is a legacy IE value
529
1
    }
530

            
531
    #[test]
532
1
    fn test_parse_object_fit() {
533
1
        assert_eq!(parse_style_object_fit("fill").unwrap(), StyleObjectFit::Fill);
534
1
        assert_eq!(parse_style_object_fit("contain").unwrap(), StyleObjectFit::Contain);
535
1
        assert_eq!(parse_style_object_fit("cover").unwrap(), StyleObjectFit::Cover);
536
1
        assert_eq!(parse_style_object_fit("none").unwrap(), StyleObjectFit::None);
537
1
        assert_eq!(parse_style_object_fit("scale-down").unwrap(), StyleObjectFit::ScaleDown);
538
1
        assert_eq!(parse_style_object_fit("  cover  ").unwrap(), StyleObjectFit::Cover);
539
1
        assert!(parse_style_object_fit("stretch").is_err());
540
1
        assert!(parse_style_object_fit("").is_err());
541
1
    }
542

            
543
    #[test]
544
1
    fn test_parse_text_orientation() {
545
1
        assert_eq!(parse_style_text_orientation("mixed").unwrap(), StyleTextOrientation::Mixed);
546
1
        assert_eq!(parse_style_text_orientation("upright").unwrap(), StyleTextOrientation::Upright);
547
1
        assert_eq!(parse_style_text_orientation("sideways").unwrap(), StyleTextOrientation::Sideways);
548
1
        assert_eq!(parse_style_text_orientation("  mixed  ").unwrap(), StyleTextOrientation::Mixed);
549
1
        assert!(parse_style_text_orientation("vertical").is_err());
550
1
    }
551

            
552
    #[test]
553
1
    fn test_parse_object_position() {
554
1
        let centered = parse_style_object_position("center").unwrap();
555
1
        assert_eq!(centered, parse_style_object_position("center center").unwrap());
556

            
557
1
        let lt = parse_style_object_position("left top").unwrap();
558
        use crate::props::style::background::{BackgroundPositionHorizontal, BackgroundPositionVertical};
559
1
        assert_eq!(lt.horizontal, BackgroundPositionHorizontal::Left);
560
1
        assert_eq!(lt.vertical, BackgroundPositionVertical::Top);
561

            
562
1
        let rb = parse_style_object_position("right bottom").unwrap();
563
1
        assert_eq!(rb.horizontal, BackgroundPositionHorizontal::Right);
564
1
        assert_eq!(rb.vertical, BackgroundPositionVertical::Bottom);
565

            
566
1
        assert!(parse_style_object_position("left top center").is_err());
567
1
        assert!(parse_style_object_position("invalid").is_err());
568
1
    }
569

            
570
    #[test]
571
1
    fn test_parse_aspect_ratio() {
572
1
        assert_eq!(parse_style_aspect_ratio("auto").unwrap(), StyleAspectRatio::Auto);
573
1
        assert_eq!(
574
1
            parse_style_aspect_ratio("16 / 9").unwrap(),
575
            StyleAspectRatio::Ratio(AspectRatioValue { width: 16000, height: 9000 })
576
        );
577
1
        assert_eq!(
578
1
            parse_style_aspect_ratio("16/9").unwrap(),
579
            StyleAspectRatio::Ratio(AspectRatioValue { width: 16000, height: 9000 })
580
        );
581
1
        assert_eq!(
582
1
            parse_style_aspect_ratio("1.5").unwrap(),
583
            StyleAspectRatio::Ratio(AspectRatioValue { width: 1500, height: 1000 })
584
        );
585
1
        assert_eq!(
586
1
            parse_style_aspect_ratio("  4 / 3  ").unwrap(),
587
            StyleAspectRatio::Ratio(AspectRatioValue { width: 4000, height: 3000 })
588
        );
589
1
        assert!(parse_style_aspect_ratio("0 / 1").is_err());
590
1
        assert!(parse_style_aspect_ratio("1 / 0").is_err());
591
1
        assert!(parse_style_aspect_ratio("-1 / 1").is_err());
592
1
        assert!(parse_style_aspect_ratio("abc").is_err());
593
1
    }
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)]
603
pub enum StyleObjectFit {
604
    #[default]
605
    Fill,
606
    Contain,
607
    Cover,
608
    None,
609
    ScaleDown,
610
}
611

            
612

            
613
crate::impl_option!(StyleObjectFit, OptionStyleObjectFit, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
614

            
615
impl 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)]
629
pub enum StyleObjectFitParseError<'a> {
630
    InvalidValue(&'a str),
631
}
632

            
633
#[cfg(feature = "parser")]
634
crate::impl_debug_as_display!(StyleObjectFitParseError<'a>);
635

            
636
#[cfg(feature = "parser")]
637
crate::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)]
644
pub enum StyleObjectFitParseErrorOwned {
645
    InvalidValue(crate::AzString),
646
}
647

            
648
#[cfg(feature = "parser")]
649
impl<'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")]
658
impl 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")]
667
8
pub fn parse_style_object_fit<'a>(
668
8
    input: &'a str,
669
8
) -> Result<StyleObjectFit, StyleObjectFitParseError<'a>> {
670
8
    let input = input.trim();
671
8
    match input {
672
8
        "fill" => Ok(StyleObjectFit::Fill),
673
7
        "contain" => Ok(StyleObjectFit::Contain),
674
6
        "cover" => Ok(StyleObjectFit::Cover),
675
4
        "none" => Ok(StyleObjectFit::None),
676
3
        "scale-down" => Ok(StyleObjectFit::ScaleDown),
677
2
        _ => Err(StyleObjectFitParseError::InvalidValue(input)),
678
    }
679
8
}
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)]
688
pub enum StyleTextOrientation {
689
    #[default]
690
    Mixed,
691
    Upright,
692
    Sideways,
693
}
694

            
695

            
696
crate::impl_option!(StyleTextOrientation, OptionStyleTextOrientation, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
697

            
698
impl 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)]
710
pub enum StyleTextOrientationParseError<'a> {
711
    InvalidValue(&'a str),
712
}
713

            
714
#[cfg(feature = "parser")]
715
crate::impl_debug_as_display!(StyleTextOrientationParseError<'a>);
716

            
717
#[cfg(feature = "parser")]
718
crate::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)]
725
pub enum StyleTextOrientationParseErrorOwned {
726
    InvalidValue(crate::AzString),
727
}
728

            
729
#[cfg(feature = "parser")]
730
impl<'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")]
739
impl 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")]
748
5
pub fn parse_style_text_orientation<'a>(
749
5
    input: &'a str,
750
5
) -> Result<StyleTextOrientation, StyleTextOrientationParseError<'a>> {
751
5
    let input = input.trim();
752
5
    match input {
753
5
        "mixed" => Ok(StyleTextOrientation::Mixed),
754
3
        "upright" => Ok(StyleTextOrientation::Upright),
755
2
        "sideways" => Ok(StyleTextOrientation::Sideways),
756
1
        _ => Err(StyleTextOrientationParseError::InvalidValue(input)),
757
    }
758
5
}
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)]
766
pub struct StyleObjectPosition {
767
    pub horizontal: crate::props::style::background::BackgroundPositionHorizontal,
768
    pub vertical: crate::props::style::background::BackgroundPositionVertical,
769
}
770

            
771
impl 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

            
785
crate::impl_option!(StyleObjectPosition, OptionStyleObjectPosition, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
786

            
787
impl 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)]
799
pub enum StyleObjectPositionParseError<'a> {
800
    InvalidValue(&'a str),
801
}
802

            
803
#[cfg(feature = "parser")]
804
crate::impl_debug_as_display!(StyleObjectPositionParseError<'a>);
805

            
806
#[cfg(feature = "parser")]
807
crate::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)]
814
pub enum StyleObjectPositionParseErrorOwned {
815
    InvalidValue(crate::AzString),
816
}
817

            
818
#[cfg(feature = "parser")]
819
impl<'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")]
828
impl 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")]
839
6
pub fn parse_style_object_position<'a>(
840
6
    input: &'a str,
841
6
) -> 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
6
    let input = input.trim();
848
6
    let parts: Vec<&str> = input.split_whitespace().collect();
849

            
850
6
    let (h, v) = match parts.len() {
851
        1 => {
852
2
            let val = parts[0];
853
2
            match val {
854
2
                "center" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Center),
855
1
                "left" => (BackgroundPositionHorizontal::Left, BackgroundPositionVertical::Center),
856
1
                "right" => (BackgroundPositionHorizontal::Right, BackgroundPositionVertical::Center),
857
1
                "top" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Top),
858
1
                "bottom" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Bottom),
859
                _ => {
860
1
                    let px = parse_pixel_value(val)
861
1
                        .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
862
                    (BackgroundPositionHorizontal::Exact(px), BackgroundPositionVertical::Exact(px))
863
                }
864
            }
865
        }
866
        2 => {
867
3
            let h = match parts[0] {
868
3
                "left" => BackgroundPositionHorizontal::Left,
869
2
                "center" => BackgroundPositionHorizontal::Center,
870
1
                "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
3
            let v = match parts[1] {
878
3
                "top" => BackgroundPositionVertical::Top,
879
2
                "center" => BackgroundPositionVertical::Center,
880
1
                "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
3
            (h, v)
888
        }
889
1
        _ => return Err(StyleObjectPositionParseError::InvalidValue(input)),
890
    };
891

            
892
4
    Ok(StyleObjectPosition { horizontal: h, vertical: v })
893
6
}
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)]
900
pub 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)]
912
pub 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

            
921
crate::impl_option!(StyleAspectRatio, OptionStyleAspectRatio, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
922

            
923
impl 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)]
934
pub enum StyleAspectRatioParseError<'a> {
935
    InvalidValue(&'a str),
936
}
937

            
938
#[cfg(feature = "parser")]
939
crate::impl_debug_as_display!(StyleAspectRatioParseError<'a>);
940

            
941
#[cfg(feature = "parser")]
942
crate::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)]
949
pub enum StyleAspectRatioParseErrorOwned {
950
    InvalidValue(crate::AzString),
951
}
952

            
953
#[cfg(feature = "parser")]
954
impl<'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")]
963
impl 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")]
973
9
pub fn parse_style_aspect_ratio<'a>(
974
9
    input: &'a str,
975
9
) -> Result<StyleAspectRatio, StyleAspectRatioParseError<'a>> {
976
9
    let input = input.trim();
977
9
    if input == "auto" {
978
1
        return Ok(StyleAspectRatio::Auto);
979
8
    }
980
    // Try "w / h" or "w/h" format
981
8
    if let Some(slash_pos) = input.find('/') {
982
6
        let w_str = input[..slash_pos].trim();
983
6
        let h_str = input[slash_pos + 1..].trim();
984
6
        let w: f32 = w_str.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
985
6
        let h: f32 = h_str.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
986
6
        if h <= 0.0 || w <= 0.0 || w > 100_000.0 || h > 100_000.0 {
987
3
            return Err(StyleAspectRatioParseError::InvalidValue(input));
988
3
        }
989
3
        return Ok(StyleAspectRatio::Ratio(AspectRatioValue {
990
3
            width: (w * 1000.0).round() as u32,
991
3
            height: (h * 1000.0).round() as u32,
992
3
        }));
993
2
    }
994
    // Try single number (width/1)
995
2
    let w: f32 = input.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
996
1
    if w <= 0.0 || w > 100_000.0 {
997
        return Err(StyleAspectRatioParseError::InvalidValue(input));
998
1
    }
999
1
    Ok(StyleAspectRatio::Ratio(AspectRatioValue {
1
        width: (w * 1000.0).round() as u32,
1
        height: 1000,
1
    }))
9
}