1
//! CSS properties for managing content overflow.
2

            
3
use alloc::string::{String, ToString};
4
use crate::corety::{AzString, OptionF32};
5

            
6
use crate::props::formatter::PrintAsCssValue;
7

            
8
// +spec:overflow:647a7b - overflow property (visible/hidden/clip/scroll/auto), overflow-clip-margin, text-overflow defined in CSS Overflow 3
9
/// Represents an `overflow-x` or `overflow-y` property.
10
///
11
/// Determines what to do when content overflows an element's box.
12
// +spec:overflow:3526f7 - overflow property with scroll/clip/hidden/visible/auto values
13
// +spec:overflow:36c4f6 - overflow-x/overflow-y properties with clip value
14
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
15
#[repr(C)]
16
pub enum LayoutOverflow {
17
    /// Always shows a scroll bar, overflows on scroll.
18
    Scroll,
19
    /// Shows a scroll bar only when content overflows.
20
    Auto,
21
    /// Clips overflowing content. The rest of the content will be invisible.
22
    Hidden,
23
    /// Content is not clipped and renders outside the element's box. This is the CSS default.
24
    // +spec:overflow:236100 - initial value of 'overflow' is 'visible'
25
    #[default]
26
    Visible,
27
    /// Similar to `hidden`, clips the content at the box's edge.
28
    Clip,
29
}
30

            
31
impl LayoutOverflow {
32
    /// Returns whether this overflow value requires a scrollbar to be displayed.
33
    ///
34
    /// - `overflow: scroll` always shows the scrollbar.
35
    /// - `overflow: auto` only shows the scrollbar if the content is currently overflowing.
36
    /// - `overflow: hidden`, `overflow: visible`, and `overflow: clip` do not show any scrollbars.
37
    // +spec:overflow:2bf182 - overflow:scroll always shows scrollbar whether or not content is clipped
38
    // +spec:overflow:84cd40 - scroll value always displays scrollbar for accessing clipped content
39
    // +spec:overflow:8fcdd8 - auto causes scrolling mechanism for overflowing boxes (table exception is UA-level)
40
7
    pub fn needs_scrollbar(&self, currently_overflowing: bool) -> bool {
41
7
        match self {
42
2
            LayoutOverflow::Scroll => true,
43
2
            LayoutOverflow::Auto => currently_overflowing,
44
3
            LayoutOverflow::Hidden | LayoutOverflow::Visible | LayoutOverflow::Clip => false,
45
        }
46
7
    }
47

            
48
    // +spec:overflow:145749 - overflow:hidden clips content to containing element box
49
    // +spec:overflow:3dc18e - overflow:hidden clips content with no scrolling UI
50
    // +spec:overflow:81e306 - clipping region clips all aspects outside it; clipped content does not cause overflow
51
    // +spec:overflow:fd38ce - overflow properties specify whether a box's content is clipped / scroll container
52
    /// Returns `true` if this overflow value clips content (everything except `visible`).
53
    pub fn is_clipped(&self) -> bool {
54
        // All overflow values except 'visible' clip their content
55
        matches!(
56
            self,
57
            LayoutOverflow::Hidden
58
                | LayoutOverflow::Clip
59
                | LayoutOverflow::Auto
60
                | LayoutOverflow::Scroll
61
        )
62
    }
63

            
64
    // +spec:overflow:3be57c - overflow:hidden disables user scrolling but programmatic scrolling still works
65
    /// Returns `true` if the overflow type is `scroll`.
66
    pub fn is_scroll(&self) -> bool {
67
        matches!(self, LayoutOverflow::Scroll)
68
    }
69

            
70
    /// Returns `true` if the overflow type is `visible`, which is the only
71
    /// overflow type that doesn't clip its children.
72
    pub fn is_overflow_visible(&self) -> bool {
73
        *self == LayoutOverflow::Visible
74
    }
75

            
76
    /// Returns `true` if the overflow type is `hidden`.
77
    pub fn is_overflow_hidden(&self) -> bool {
78
        *self == LayoutOverflow::Hidden
79
    }
80

            
81
    // +spec:overflow:833078 - visible/clip compute to auto/hidden if other axis is scrollable
82
    /// Resolves the computed value per CSS Overflow 3 ยง 3.1:
83
    /// visible/clip values compute to auto/hidden (respectively)
84
    /// if the other axis is neither visible nor clip.
85
53620
    pub fn resolve_computed(self, other_axis: LayoutOverflow) -> LayoutOverflow {
86
53620
        let other_is_scrollable = !matches!(other_axis, LayoutOverflow::Visible | LayoutOverflow::Clip);
87
53620
        if other_is_scrollable {
88
210
            match self {
89
                LayoutOverflow::Visible => LayoutOverflow::Auto,
90
                LayoutOverflow::Clip => LayoutOverflow::Hidden,
91
210
                other => other,
92
            }
93
        } else {
94
53410
            self
95
        }
96
53620
    }
97
}
98

            
99
impl PrintAsCssValue for LayoutOverflow {
100
    fn print_as_css_value(&self) -> String {
101
        String::from(match self {
102
            LayoutOverflow::Scroll => "scroll",
103
            LayoutOverflow::Auto => "auto",
104
            LayoutOverflow::Hidden => "hidden",
105
            LayoutOverflow::Visible => "visible",
106
            LayoutOverflow::Clip => "clip",
107
        })
108
    }
109
}
110

            
111
// -- Parser
112

            
113
/// Error returned when parsing an `overflow` property fails.
114
#[derive(Clone, PartialEq, Eq)]
115
pub enum LayoutOverflowParseError<'a> {
116
    /// The provided value is not a valid `overflow` keyword.
117
    InvalidValue(&'a str),
118
}
119

            
120
impl_debug_as_display!(LayoutOverflowParseError<'a>);
121
impl_display! { LayoutOverflowParseError<'a>, {
122
    InvalidValue(val) => format!(
123
        "Invalid overflow value: \"{}\". Expected 'scroll', 'auto', 'hidden', 'visible', or 'clip'.", val
124
    ),
125
}}
126

            
127
/// An owned version of `LayoutOverflowParseError`.
128
#[derive(Debug, Clone, PartialEq, Eq)]
129
#[repr(C, u8)]
130
pub enum LayoutOverflowParseErrorOwned {
131
    InvalidValue(AzString),
132
}
133

            
134
impl<'a> LayoutOverflowParseError<'a> {
135
    /// Converts the borrowed error into an owned error.
136
    pub fn to_contained(&self) -> LayoutOverflowParseErrorOwned {
137
        match self {
138
            LayoutOverflowParseError::InvalidValue(s) => {
139
                LayoutOverflowParseErrorOwned::InvalidValue(s.to_string().into())
140
            }
141
        }
142
    }
143
}
144

            
145
impl LayoutOverflowParseErrorOwned {
146
    /// Converts the owned error back into a borrowed error.
147
    pub fn to_shared<'a>(&'a self) -> LayoutOverflowParseError<'a> {
148
        match self {
149
            LayoutOverflowParseErrorOwned::InvalidValue(s) => {
150
                LayoutOverflowParseError::InvalidValue(s.as_str())
151
            }
152
        }
153
    }
154
}
155

            
156
#[cfg(feature = "parser")]
157
/// Parses a `LayoutOverflow` from a string slice.
158
165
pub fn parse_layout_overflow<'a>(
159
165
    input: &'a str,
160
165
) -> Result<LayoutOverflow, LayoutOverflowParseError<'a>> {
161
165
    let input_trimmed = input.trim();
162
165
    match input_trimmed {
163
165
        "scroll" => Ok(LayoutOverflow::Scroll),
164
160
        "auto" | "overlay" => Ok(LayoutOverflow::Auto), // +spec:overflow:6120e6 - "overlay" is a legacy value alias of "auto"
165
152
        "hidden" => Ok(LayoutOverflow::Hidden),
166
6
        "visible" => Ok(LayoutOverflow::Visible),
167
5
        "clip" => Ok(LayoutOverflow::Clip),
168
4
        _ => Err(LayoutOverflowParseError::InvalidValue(input)),
169
    }
170
165
}
171

            
172
// -- StyleScrollbarGutter --
173
// +spec:box-model:e98b7c - scrollbar gutter: space between inner border edge and outer padding edge
174

            
175
/// Represents the `scrollbar-gutter` CSS property.
176
///
177
/// Controls whether space is reserved for the scrollbar, preventing
178
/// layout shifts when content overflows.
179
// +spec:overflow:da4bbc - scrollbar-gutter affects gutter presence, not scrollbar visibility
180
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
181
#[repr(C)]
182
pub enum StyleScrollbarGutter {
183
    /// No scrollbar gutter is reserved.
184
    #[default]
185
    Auto,
186
    /// Space is reserved for the scrollbar on one edge.
187
    Stable,
188
    /// Space is reserved for the scrollbar on both edges.
189
    StableBothEdges,
190
}
191

            
192
impl PrintAsCssValue for StyleScrollbarGutter {
193
    fn print_as_css_value(&self) -> String {
194
        String::from(match self {
195
            StyleScrollbarGutter::Auto => "auto",
196
            StyleScrollbarGutter::Stable => "stable",
197
            StyleScrollbarGutter::StableBothEdges => "stable both-edges",
198
        })
199
    }
200
}
201

            
202
// -- Parser for StyleScrollbarGutter
203

            
204
/// Error returned when parsing a `scrollbar-gutter` property fails.
205
#[derive(Clone, PartialEq, Eq)]
206
pub enum StyleScrollbarGutterParseError<'a> {
207
    /// The provided value is not a valid `scrollbar-gutter` keyword.
208
    InvalidValue(&'a str),
209
}
210

            
211
impl_debug_as_display!(StyleScrollbarGutterParseError<'a>);
212
impl_display! { StyleScrollbarGutterParseError<'a>, {
213
    InvalidValue(val) => format!(
214
        "Invalid scrollbar-gutter value: \"{}\". Expected 'auto', 'stable', or 'stable both-edges'.", val
215
    ),
216
}}
217

            
218
/// An owned version of `StyleScrollbarGutterParseError`.
219
#[derive(Debug, Clone, PartialEq, Eq)]
220
#[repr(C, u8)]
221
pub enum StyleScrollbarGutterParseErrorOwned {
222
    InvalidValue(AzString),
223
}
224

            
225
impl<'a> StyleScrollbarGutterParseError<'a> {
226
    /// Converts the borrowed error into an owned error.
227
    pub fn to_contained(&self) -> StyleScrollbarGutterParseErrorOwned {
228
        match self {
229
            StyleScrollbarGutterParseError::InvalidValue(s) => {
230
                StyleScrollbarGutterParseErrorOwned::InvalidValue(s.to_string().into())
231
            }
232
        }
233
    }
234
}
235

            
236
impl StyleScrollbarGutterParseErrorOwned {
237
    /// Converts the owned error back into a borrowed error.
238
    pub fn to_shared<'a>(&'a self) -> StyleScrollbarGutterParseError<'a> {
239
        match self {
240
            StyleScrollbarGutterParseErrorOwned::InvalidValue(s) => {
241
                StyleScrollbarGutterParseError::InvalidValue(s.as_str())
242
            }
243
        }
244
    }
245
}
246

            
247
#[cfg(feature = "parser")]
248
/// Parses a `StyleScrollbarGutter` from a string slice.
249
pub fn parse_style_scrollbar_gutter<'a>(
250
    input: &'a str,
251
) -> Result<StyleScrollbarGutter, StyleScrollbarGutterParseError<'a>> {
252
    let input_trimmed = input.trim();
253
    match input_trimmed {
254
        "auto" => Ok(StyleScrollbarGutter::Auto),
255
        "stable" => Ok(StyleScrollbarGutter::Stable),
256
        "stable both-edges" => Ok(StyleScrollbarGutter::StableBothEdges),
257
        _ => Err(StyleScrollbarGutterParseError::InvalidValue(input)),
258
    }
259
}
260

            
261
// -- VisualBox --
262

            
263
// +spec:overflow:f6955f - box edge origin for overflow-clip-margin
264
/// Represents the `<visual-box>` value used as the overflow clip edge origin.
265
///
266
/// Specifies which box edge to use as the starting point for the clip region.
267
/// Defaults to `padding-box` per CSS Overflow Module Level 3.
268
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
269
#[repr(C)]
270
pub enum VisualBox {
271
    /// Clip edge starts at the content box edge.
272
    ContentBox,
273
    /// Clip edge starts at the padding box edge (default).
274
    #[default]
275
    PaddingBox,
276
    /// Clip edge starts at the border box edge.
277
    BorderBox,
278
}
279

            
280
impl PrintAsCssValue for VisualBox {
281
    fn print_as_css_value(&self) -> String {
282
        String::from(match self {
283
            VisualBox::ContentBox => "content-box",
284
            VisualBox::PaddingBox => "padding-box",
285
            VisualBox::BorderBox => "border-box",
286
        })
287
    }
288
}
289

            
290
// -- StyleOverflowClipMargin --
291

            
292
/// Represents the `overflow-clip-margin` CSS property.
293
///
294
/// Determines how far outside the element's box the content may paint
295
/// before being clipped when `overflow: clip` is used.
296
/// Syntax: `<visual-box> || <length [0,โˆž]>`
297
// +spec:overflow:455786 - overflow-clip-margin has no effect on hidden/scroll, only on clip
298
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
299
#[repr(C)]
300
pub struct StyleOverflowClipMargin {
301
    /// The box edge to use as the clip origin (content-box, padding-box, or border-box).
302
    pub clip_edge: VisualBox,
303
    /// The clip margin distance beyond the clip edge.
304
    pub inner: crate::props::basic::pixel::PixelValue,
305
}
306

            
307
impl PrintAsCssValue for StyleOverflowClipMargin {
308
    fn print_as_css_value(&self) -> String {
309
        let edge = self.clip_edge.print_as_css_value();
310
        let len = self.inner.print_as_css_value();
311
        #[allow(clippy::float_cmp)] // exact zero check: value is default-initialized, not computed
312
        if self.inner.number.get() == 0.0 {
313
            edge
314
        } else if self.clip_edge == VisualBox::PaddingBox {
315
            len
316
        } else {
317
            format!("{} {}", edge, len)
318
        }
319
    }
320
}
321

            
322
/// Error returned when parsing an `overflow-clip-margin` property fails.
323
#[derive(Clone, PartialEq, Eq)]
324
pub enum StyleOverflowClipMarginParseError<'a> {
325
    /// The provided value is not a valid `overflow-clip-margin` value.
326
    InvalidValue(&'a str),
327
}
328

            
329
impl_debug_as_display!(StyleOverflowClipMarginParseError<'a>);
330
impl_display! { StyleOverflowClipMarginParseError<'a>, {
331
    InvalidValue(val) => format!("Invalid overflow-clip-margin value: \"{}\"", val),
332
}}
333

            
334
/// An owned version of `StyleOverflowClipMarginParseError`.
335
#[derive(Debug, Clone, PartialEq, Eq)]
336
#[repr(C, u8)]
337
pub enum StyleOverflowClipMarginParseErrorOwned {
338
    InvalidValue(AzString),
339
}
340

            
341
impl<'a> StyleOverflowClipMarginParseError<'a> {
342
    /// Converts the borrowed error into an owned error.
343
    pub fn to_contained(&self) -> StyleOverflowClipMarginParseErrorOwned {
344
        match self {
345
            StyleOverflowClipMarginParseError::InvalidValue(s) => {
346
                StyleOverflowClipMarginParseErrorOwned::InvalidValue(s.to_string().into())
347
            }
348
        }
349
    }
350
}
351

            
352
impl StyleOverflowClipMarginParseErrorOwned {
353
    /// Converts the owned error back into a borrowed error.
354
    pub fn to_shared<'a>(&'a self) -> StyleOverflowClipMarginParseError<'a> {
355
        match self {
356
            StyleOverflowClipMarginParseErrorOwned::InvalidValue(s) => {
357
                StyleOverflowClipMarginParseError::InvalidValue(s.as_str())
358
            }
359
        }
360
    }
361
}
362

            
363
#[cfg(feature = "parser")]
364
/// Parses a `StyleOverflowClipMargin` from a string slice.
365
///
366
/// Syntax: `<visual-box> || <length [0,โˆž]>`
367
/// The `<visual-box>` defaults to `padding-box` if omitted.
368
/// The `<length>` defaults to `0px` if omitted.
369
pub fn parse_style_overflow_clip_margin<'a>(
370
    input: &'a str,
371
) -> Result<StyleOverflowClipMargin, StyleOverflowClipMarginParseError<'a>> {
372
    use crate::props::basic::pixel::parse_pixel_value;
373

            
374
    let input_trimmed = input.trim();
375
    let mut clip_edge = None;
376
    let mut length = None;
377

            
378
    for token in input_trimmed.split_whitespace() {
379
        match token {
380
            "content-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::ContentBox),
381
            "padding-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::PaddingBox),
382
            "border-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::BorderBox),
383
            _ if length.is_none() => {
384
                match parse_pixel_value(token) {
385
                    Ok(pv) => length = Some(pv),
386
                    Err(_) => return Err(StyleOverflowClipMarginParseError::InvalidValue(input)),
387
                }
388
            }
389
            _ => return Err(StyleOverflowClipMarginParseError::InvalidValue(input)),
390
        }
391
    }
392

            
393
    if clip_edge.is_none() && length.is_none() {
394
        return Err(StyleOverflowClipMarginParseError::InvalidValue(input));
395
    }
396

            
397
    Ok(StyleOverflowClipMargin {
398
        clip_edge: clip_edge.unwrap_or_default(),
399
        inner: length.unwrap_or_default(),
400
    })
401
}
402

            
403
// -- StyleClipRect --
404

            
405
/// Represents the deprecated CSS `clip` property value `rect(top, right, bottom, left)`.
406
///
407
/// Each edge can be a length or `auto`. When `auto`, the edge matches the
408
/// element's generated border box edge:
409
/// - `auto` for top/left = 0
410
/// - `auto` for bottom = used height + vertical padding + vertical border
411
/// - `auto` for right = used width + horizontal padding + horizontal border
412
///
413
/// Negative lengths are permitted.
414
// +spec:overflow:297dc3 - clip rect() auto values resolve to border box edges
415
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
416
#[repr(C)]
417
pub struct StyleClipRect {
418
    /// Top edge offset in pixels. `None` means `auto` (= 0).
419
    pub top: OptionF32,
420
    /// Right edge offset in pixels. `None` means `auto` (= used width + horiz padding + horiz border).
421
    pub right: OptionF32,
422
    /// Bottom edge offset in pixels. `None` means `auto` (= used height + vert padding + vert border).
423
    pub bottom: OptionF32,
424
    /// Left edge offset in pixels. `None` means `auto` (= 0).
425
    pub left: OptionF32,
426
}
427

            
428
impl StyleClipRect {
429
    /// Resolves `auto` values to border box edges given the element's
430
    /// used width/height and padding/border sizes.
431
    ///
432
    /// Returns `(top, right, bottom, left)` in pixels.
433
    pub fn resolve(
434
        &self,
435
        used_width: f32,
436
        used_height: f32,
437
        padding_left: f32,
438
        padding_right: f32,
439
        padding_top: f32,
440
        padding_bottom: f32,
441
        border_left: f32,
442
        border_right: f32,
443
        border_top: f32,
444
        border_bottom: f32,
445
    ) -> (f32, f32, f32, f32) {
446
        let top = self.top.into_option().unwrap_or(0.0);
447
        let left = self.left.into_option().unwrap_or(0.0);
448
        let bottom = self
449
            .bottom
450
            .into_option()
451
            .unwrap_or(used_height + padding_top + padding_bottom + border_top + border_bottom);
452
        let right = self
453
            .right
454
            .into_option()
455
            .unwrap_or(used_width + padding_left + padding_right + border_left + border_right);
456
        (top, right, bottom, left)
457
    }
458
}
459

            
460
impl PrintAsCssValue for StyleClipRect {
461
    fn print_as_css_value(&self) -> String {
462
        fn fmt_edge(o: &OptionF32) -> String {
463
            match o.into_option() {
464
                Some(v) => format!("{}px", v),
465
                None => String::from("auto"),
466
            }
467
        }
468
        format!(
469
            "rect({}, {}, {}, {})",
470
            fmt_edge(&self.top),
471
            fmt_edge(&self.right),
472
            fmt_edge(&self.bottom),
473
            fmt_edge(&self.left)
474
        )
475
    }
476
}
477

            
478
// -- Parser for StyleClipRect
479

            
480
/// Error returned when parsing a CSS `clip` property value fails.
481
#[derive(Clone, PartialEq, Eq)]
482
pub enum StyleClipRectParseError<'a> {
483
    /// The provided value is not a valid `clip` value.
484
    InvalidValue(&'a str),
485
}
486

            
487
impl_debug_as_display!(StyleClipRectParseError<'a>);
488
impl_display! { StyleClipRectParseError<'a>, {
489
    InvalidValue(val) => format!(
490
        "Invalid clip value: \"{}\". Expected 'auto' or 'rect(<top>, <right>, <bottom>, <left>)'.", val
491
    ),
492
}}
493

            
494
/// An owned version of `StyleClipRectParseError`.
495
#[derive(Debug, Clone, PartialEq, Eq)]
496
#[repr(C, u8)]
497
pub enum StyleClipRectParseErrorOwned {
498
    InvalidValue(AzString),
499
}
500

            
501
impl<'a> StyleClipRectParseError<'a> {
502
    /// Converts the borrowed error into an owned error.
503
    pub fn to_contained(&self) -> StyleClipRectParseErrorOwned {
504
        match self {
505
            StyleClipRectParseError::InvalidValue(s) => {
506
                StyleClipRectParseErrorOwned::InvalidValue(s.to_string().into())
507
            }
508
        }
509
    }
510
}
511

            
512
impl StyleClipRectParseErrorOwned {
513
    /// Converts the owned error back into a borrowed error.
514
    pub fn to_shared<'a>(&'a self) -> StyleClipRectParseError<'a> {
515
        match self {
516
            StyleClipRectParseErrorOwned::InvalidValue(s) => {
517
                StyleClipRectParseError::InvalidValue(s.as_str())
518
            }
519
        }
520
    }
521
}
522

            
523
#[cfg(feature = "parser")]
524
18
fn parse_clip_edge<'a>(token: &'a str) -> Result<OptionF32, StyleClipRectParseError<'a>> {
525
    use crate::props::basic::pixel::parse_pixel_value;
526

            
527
18
    let token = token.trim();
528
18
    if token.eq_ignore_ascii_case("auto") {
529
6
        return Ok(OptionF32::None);
530
12
    }
531
12
    let pv = parse_pixel_value(token)
532
12
        .map_err(|_| StyleClipRectParseError::InvalidValue(token))?;
533
11
    Ok(OptionF32::Some(pv.number.get()))
534
18
}
535

            
536
#[cfg(feature = "parser")]
537
/// Parses a `StyleClipRect` from a string slice.
538
///
539
/// Accepts:
540
/// - `auto` โ€” equivalent to `rect(auto, auto, auto, auto)`.
541
/// - `rect(<top>, <right>, <bottom>, <left>)` โ€” comma-separated form.
542
/// - `rect(<top> <right> <bottom> <left>)` โ€” legacy space-separated form.
543
///
544
/// Each edge is either `auto` or a `<length>`. Negative lengths are permitted.
545
10
pub fn parse_clip_rect<'a>(input: &'a str) -> Result<StyleClipRect, StyleClipRectParseError<'a>> {
546
10
    let trimmed = input.trim();
547

            
548
10
    if trimmed.eq_ignore_ascii_case("auto") {
549
1
        return Ok(StyleClipRect::default());
550
9
    }
551

            
552
9
    let inner = trimmed
553
9
        .strip_prefix("rect(")
554
9
        .or_else(|| trimmed.strip_prefix("RECT("))
555
9
        .and_then(|s| s.strip_suffix(')'))
556
9
        .ok_or(StyleClipRectParseError::InvalidValue(input))?;
557

            
558
6
    let inner = inner.trim();
559
6
    let parts: alloc::vec::Vec<&str> = if inner.contains(',') {
560
19
        inner.split(',').map(|s| s.trim()).collect()
561
    } else {
562
1
        inner.split_whitespace().collect()
563
    };
564

            
565
6
    if parts.len() != 4 {
566
1
        return Err(StyleClipRectParseError::InvalidValue(input));
567
5
    }
568

            
569
    Ok(StyleClipRect {
570
5
        top: parse_clip_edge(parts[0])?,
571
5
        right: parse_clip_edge(parts[1])?,
572
4
        bottom: parse_clip_edge(parts[2])?,
573
4
        left: parse_clip_edge(parts[3])?,
574
    })
575
10
}
576

            
577
#[cfg(all(test, feature = "parser"))]
578
mod tests {
579
    use super::*;
580

            
581
    #[test]
582
1
    fn test_parse_layout_overflow_valid() {
583
1
        assert_eq!(
584
1
            parse_layout_overflow("visible").unwrap(),
585
            LayoutOverflow::Visible
586
        );
587
1
        assert_eq!(
588
1
            parse_layout_overflow("hidden").unwrap(),
589
            LayoutOverflow::Hidden
590
        );
591
1
        assert_eq!(parse_layout_overflow("clip").unwrap(), LayoutOverflow::Clip);
592
1
        assert_eq!(
593
1
            parse_layout_overflow("scroll").unwrap(),
594
            LayoutOverflow::Scroll
595
        );
596
1
        assert_eq!(parse_layout_overflow("auto").unwrap(), LayoutOverflow::Auto);
597
1
    }
598

            
599
    #[test]
600
1
    fn test_parse_layout_overflow_whitespace() {
601
1
        assert_eq!(
602
1
            parse_layout_overflow("  scroll  ").unwrap(),
603
            LayoutOverflow::Scroll
604
        );
605
1
    }
606

            
607
    #[test]
608
1
    fn test_parse_layout_overflow_invalid() {
609
1
        assert!(parse_layout_overflow("none").is_err());
610
1
        assert!(parse_layout_overflow("").is_err());
611
1
        assert!(parse_layout_overflow("auto scroll").is_err());
612
1
        assert!(parse_layout_overflow("hidden-x").is_err());
613
1
    }
614

            
615
    #[test]
616
1
    fn test_needs_scrollbar() {
617
1
        assert!(LayoutOverflow::Scroll.needs_scrollbar(false));
618
1
        assert!(LayoutOverflow::Scroll.needs_scrollbar(true));
619
1
        assert!(LayoutOverflow::Auto.needs_scrollbar(true));
620
1
        assert!(!LayoutOverflow::Auto.needs_scrollbar(false));
621
1
        assert!(!LayoutOverflow::Hidden.needs_scrollbar(true));
622
1
        assert!(!LayoutOverflow::Visible.needs_scrollbar(true));
623
1
        assert!(!LayoutOverflow::Clip.needs_scrollbar(true));
624
1
    }
625

            
626
    #[test]
627
1
    fn test_parse_clip_rect_auto_keyword() {
628
1
        let r = parse_clip_rect("auto").unwrap();
629
1
        assert_eq!(r.top, OptionF32::None);
630
1
        assert_eq!(r.right, OptionF32::None);
631
1
        assert_eq!(r.bottom, OptionF32::None);
632
1
        assert_eq!(r.left, OptionF32::None);
633
1
    }
634

            
635
    #[test]
636
1
    fn test_parse_clip_rect_all_auto_in_rect() {
637
1
        let r = parse_clip_rect("rect(auto, auto, auto, auto)").unwrap();
638
1
        assert_eq!(r.top, OptionF32::None);
639
1
        assert_eq!(r.right, OptionF32::None);
640
1
        assert_eq!(r.bottom, OptionF32::None);
641
1
        assert_eq!(r.left, OptionF32::None);
642
1
    }
643

            
644
    #[test]
645
1
    fn test_parse_clip_rect_mixed_auto_and_lengths() {
646
1
        let r = parse_clip_rect("rect(10px, auto, 30px, auto)").unwrap();
647
1
        assert_eq!(r.top, OptionF32::Some(10.0));
648
1
        assert_eq!(r.right, OptionF32::None);
649
1
        assert_eq!(r.bottom, OptionF32::Some(30.0));
650
1
        assert_eq!(r.left, OptionF32::None);
651
1
    }
652

            
653
    #[test]
654
1
    fn test_parse_clip_rect_negative_lengths() {
655
1
        let r = parse_clip_rect("rect(-5px, 0px, -10px, 0px)").unwrap();
656
1
        assert_eq!(r.top, OptionF32::Some(-5.0));
657
1
        assert_eq!(r.right, OptionF32::Some(0.0));
658
1
        assert_eq!(r.bottom, OptionF32::Some(-10.0));
659
1
        assert_eq!(r.left, OptionF32::Some(0.0));
660
1
    }
661

            
662
    #[test]
663
1
    fn test_parse_clip_rect_legacy_space_separated() {
664
        // Legacy CSS 2.1 syntax used spaces instead of commas.
665
1
        let r = parse_clip_rect("rect(1px 2px 3px 4px)").unwrap();
666
1
        assert_eq!(r.top, OptionF32::Some(1.0));
667
1
        assert_eq!(r.right, OptionF32::Some(2.0));
668
1
        assert_eq!(r.bottom, OptionF32::Some(3.0));
669
1
        assert_eq!(r.left, OptionF32::Some(4.0));
670
1
    }
671

            
672
    #[test]
673
1
    fn test_parse_clip_rect_malformed() {
674
1
        assert!(parse_clip_rect("").is_err());
675
1
        assert!(parse_clip_rect("none").is_err());
676
        // Wrong number of edges.
677
1
        assert!(parse_clip_rect("rect(10px, 20px, 30px)").is_err());
678
        // Missing closing paren.
679
1
        assert!(parse_clip_rect("rect(10px, 20px, 30px, 40px").is_err());
680
        // Garbage edge.
681
1
        assert!(parse_clip_rect("rect(10px, abc, 30px, 40px)").is_err());
682
1
    }
683
}