Skip to main content

azul_css/props/layout/
overflow.rs

1//! CSS properties for managing content overflow.
2
3use alloc::string::{String, ToString};
4use crate::corety::{AzString, OptionF32};
5
6use 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)]
16pub 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
31impl 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    #[must_use] pub const fn needs_scrollbar(&self, currently_overflowing: bool) -> bool {
41        match self {
42            Self::Scroll => true,
43            Self::Auto => currently_overflowing,
44            Self::Hidden | Self::Visible | Self::Clip => false,
45        }
46    }
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    #[must_use] pub const fn is_clipped(&self) -> bool {
54        // All overflow values except 'visible' clip their content
55        matches!(
56            self,
57            Self::Hidden
58                | Self::Clip
59                | Self::Auto
60                | Self::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    #[must_use] pub const fn is_scroll(&self) -> bool {
67        matches!(self, Self::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    #[must_use] pub fn is_overflow_visible(&self) -> bool {
73        *self == Self::Visible
74    }
75
76    /// Returns `true` if the overflow type is `hidden`.
77    #[must_use] pub fn is_overflow_hidden(&self) -> bool {
78        *self == Self::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    #[must_use] pub const fn resolve_computed(self, other_axis: Self) -> Self {
86        let other_is_scrollable = !matches!(other_axis, Self::Visible | Self::Clip);
87        if other_is_scrollable {
88            match self {
89                Self::Visible => Self::Auto,
90                Self::Clip => Self::Hidden,
91                other => other,
92            }
93        } else {
94            self
95        }
96    }
97}
98
99impl PrintAsCssValue for LayoutOverflow {
100    fn print_as_css_value(&self) -> String {
101        String::from(match self {
102            Self::Scroll => "scroll",
103            Self::Auto => "auto",
104            Self::Hidden => "hidden",
105            Self::Visible => "visible",
106            Self::Clip => "clip",
107        })
108    }
109}
110
111// -- Parser
112
113/// Error returned when parsing an `overflow` property fails.
114#[derive(Clone, PartialEq, Eq)]
115pub enum LayoutOverflowParseError<'a> {
116    /// The provided value is not a valid `overflow` keyword.
117    InvalidValue(&'a str),
118}
119
120impl_debug_as_display!(LayoutOverflowParseError<'a>);
121impl_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)]
130pub enum LayoutOverflowParseErrorOwned {
131    InvalidValue(AzString),
132}
133
134impl LayoutOverflowParseError<'_> {
135    /// Converts the borrowed error into an owned error.
136    #[must_use] 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
145impl LayoutOverflowParseErrorOwned {
146    /// Converts the owned error back into a borrowed error.
147    #[must_use] pub fn to_shared(&self) -> LayoutOverflowParseError<'_> {
148        match self {
149            Self::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/// # Errors
159///
160/// Returns an error if `input` is not a valid CSS `overflow` value.
161pub fn parse_layout_overflow(
162    input: &str,
163) -> Result<LayoutOverflow, LayoutOverflowParseError<'_>> {
164    let input_trimmed = input.trim();
165    match input_trimmed {
166        "scroll" => Ok(LayoutOverflow::Scroll),
167        "auto" | "overlay" => Ok(LayoutOverflow::Auto), // +spec:overflow:6120e6 - "overlay" is a legacy value alias of "auto"
168        "hidden" => Ok(LayoutOverflow::Hidden),
169        "visible" => Ok(LayoutOverflow::Visible),
170        "clip" => Ok(LayoutOverflow::Clip),
171        _ => Err(LayoutOverflowParseError::InvalidValue(input)),
172    }
173}
174
175// -- StyleScrollbarGutter --
176// +spec:box-model:e98b7c - scrollbar gutter: space between inner border edge and outer padding edge
177
178/// Represents the `scrollbar-gutter` CSS property.
179///
180/// Controls whether space is reserved for the scrollbar, preventing
181/// layout shifts when content overflows.
182// +spec:overflow:da4bbc - scrollbar-gutter affects gutter presence, not scrollbar visibility
183#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
184#[repr(C)]
185pub enum StyleScrollbarGutter {
186    /// No scrollbar gutter is reserved.
187    #[default]
188    Auto,
189    /// Space is reserved for the scrollbar on one edge.
190    Stable,
191    /// Space is reserved for the scrollbar on both edges.
192    StableBothEdges,
193}
194
195impl PrintAsCssValue for StyleScrollbarGutter {
196    fn print_as_css_value(&self) -> String {
197        String::from(match self {
198            Self::Auto => "auto",
199            Self::Stable => "stable",
200            Self::StableBothEdges => "stable both-edges",
201        })
202    }
203}
204
205// -- Parser for StyleScrollbarGutter
206
207/// Error returned when parsing a `scrollbar-gutter` property fails.
208#[derive(Clone, PartialEq, Eq)]
209pub enum StyleScrollbarGutterParseError<'a> {
210    /// The provided value is not a valid `scrollbar-gutter` keyword.
211    InvalidValue(&'a str),
212}
213
214impl_debug_as_display!(StyleScrollbarGutterParseError<'a>);
215impl_display! { StyleScrollbarGutterParseError<'a>, {
216    InvalidValue(val) => format!(
217        "Invalid scrollbar-gutter value: \"{}\". Expected 'auto', 'stable', or 'stable both-edges'.", val
218    ),
219}}
220
221/// An owned version of `StyleScrollbarGutterParseError`.
222#[derive(Debug, Clone, PartialEq, Eq)]
223#[repr(C, u8)]
224pub enum StyleScrollbarGutterParseErrorOwned {
225    InvalidValue(AzString),
226}
227
228impl StyleScrollbarGutterParseError<'_> {
229    /// Converts the borrowed error into an owned error.
230    #[must_use] pub fn to_contained(&self) -> StyleScrollbarGutterParseErrorOwned {
231        match self {
232            StyleScrollbarGutterParseError::InvalidValue(s) => {
233                StyleScrollbarGutterParseErrorOwned::InvalidValue((*s).to_string().into())
234            }
235        }
236    }
237}
238
239impl StyleScrollbarGutterParseErrorOwned {
240    /// Converts the owned error back into a borrowed error.
241    #[must_use] pub fn to_shared(&self) -> StyleScrollbarGutterParseError<'_> {
242        match self {
243            Self::InvalidValue(s) => {
244                StyleScrollbarGutterParseError::InvalidValue(s.as_str())
245            }
246        }
247    }
248}
249
250#[cfg(feature = "parser")]
251/// Parses a `StyleScrollbarGutter` from a string slice.
252/// # Errors
253///
254/// Returns an error if `input` is not a valid CSS `scrollbar-gutter` value.
255pub fn parse_style_scrollbar_gutter(
256    input: &str,
257) -> Result<StyleScrollbarGutter, StyleScrollbarGutterParseError<'_>> {
258    let input_trimmed = input.trim();
259    match input_trimmed {
260        "auto" => Ok(StyleScrollbarGutter::Auto),
261        "stable" => Ok(StyleScrollbarGutter::Stable),
262        "stable both-edges" => Ok(StyleScrollbarGutter::StableBothEdges),
263        _ => Err(StyleScrollbarGutterParseError::InvalidValue(input)),
264    }
265}
266
267// -- VisualBox --
268
269// +spec:overflow:f6955f - box edge origin for overflow-clip-margin
270/// Represents the `<visual-box>` value used as the overflow clip edge origin.
271///
272/// Specifies which box edge to use as the starting point for the clip region.
273/// Defaults to `padding-box` per CSS Overflow Module Level 3.
274#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
275#[repr(C)]
276pub enum VisualBox {
277    /// Clip edge starts at the content box edge.
278    ContentBox,
279    /// Clip edge starts at the padding box edge (default).
280    #[default]
281    PaddingBox,
282    /// Clip edge starts at the border box edge.
283    BorderBox,
284}
285
286impl PrintAsCssValue for VisualBox {
287    fn print_as_css_value(&self) -> String {
288        String::from(match self {
289            Self::ContentBox => "content-box",
290            Self::PaddingBox => "padding-box",
291            Self::BorderBox => "border-box",
292        })
293    }
294}
295
296// -- StyleOverflowClipMargin --
297
298/// Represents the `overflow-clip-margin` CSS property.
299///
300/// Determines how far outside the element's box the content may paint
301/// before being clipped when `overflow: clip` is used.
302/// Syntax: `<visual-box> || <length [0,∞]>`
303// +spec:overflow:455786 - overflow-clip-margin has no effect on hidden/scroll, only on clip
304#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
305#[repr(C)]
306pub struct StyleOverflowClipMargin {
307    /// The box edge to use as the clip origin (content-box, padding-box, or border-box).
308    pub clip_edge: VisualBox,
309    /// The clip margin distance beyond the clip edge.
310    pub inner: crate::props::basic::pixel::PixelValue,
311}
312
313impl PrintAsCssValue for StyleOverflowClipMargin {
314    fn print_as_css_value(&self) -> String {
315        let edge = self.clip_edge.print_as_css_value();
316        let len = self.inner.print_as_css_value();
317        #[allow(clippy::float_cmp)] // exact zero check: value is default-initialized, not computed
318        if self.inner.number.get() == 0.0 {
319            edge
320        } else if self.clip_edge == VisualBox::PaddingBox {
321            len
322        } else {
323            format!("{edge} {len}")
324        }
325    }
326}
327
328/// Error returned when parsing an `overflow-clip-margin` property fails.
329#[derive(Clone, PartialEq, Eq)]
330pub enum StyleOverflowClipMarginParseError<'a> {
331    /// The provided value is not a valid `overflow-clip-margin` value.
332    InvalidValue(&'a str),
333}
334
335impl_debug_as_display!(StyleOverflowClipMarginParseError<'a>);
336impl_display! { StyleOverflowClipMarginParseError<'a>, {
337    InvalidValue(val) => format!("Invalid overflow-clip-margin value: \"{}\"", val),
338}}
339
340/// An owned version of `StyleOverflowClipMarginParseError`.
341#[derive(Debug, Clone, PartialEq, Eq)]
342#[repr(C, u8)]
343pub enum StyleOverflowClipMarginParseErrorOwned {
344    InvalidValue(AzString),
345}
346
347impl StyleOverflowClipMarginParseError<'_> {
348    /// Converts the borrowed error into an owned error.
349    #[must_use] pub fn to_contained(&self) -> StyleOverflowClipMarginParseErrorOwned {
350        match self {
351            StyleOverflowClipMarginParseError::InvalidValue(s) => {
352                StyleOverflowClipMarginParseErrorOwned::InvalidValue((*s).to_string().into())
353            }
354        }
355    }
356}
357
358impl StyleOverflowClipMarginParseErrorOwned {
359    /// Converts the owned error back into a borrowed error.
360    #[must_use] pub fn to_shared(&self) -> StyleOverflowClipMarginParseError<'_> {
361        match self {
362            Self::InvalidValue(s) => {
363                StyleOverflowClipMarginParseError::InvalidValue(s.as_str())
364            }
365        }
366    }
367}
368
369#[cfg(feature = "parser")]
370/// Parses a `StyleOverflowClipMargin` from a string slice.
371///
372/// Syntax: `<visual-box> || <length [0,∞]>`
373/// The `<visual-box>` defaults to `padding-box` if omitted.
374/// The `<length>` defaults to `0px` if omitted.
375/// # Errors
376///
377/// Returns an error if `input` is not a valid CSS `overflow-clip-margin` value.
378pub fn parse_style_overflow_clip_margin(
379    input: &str,
380) -> Result<StyleOverflowClipMargin, StyleOverflowClipMarginParseError<'_>> {
381    use crate::props::basic::pixel::parse_pixel_value;
382
383    let input_trimmed = input.trim();
384    let mut clip_edge = None;
385    let mut length = None;
386
387    for token in input_trimmed.split_whitespace() {
388        match token {
389            "content-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::ContentBox),
390            "padding-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::PaddingBox),
391            "border-box" if clip_edge.is_none() => clip_edge = Some(VisualBox::BorderBox),
392            _ if length.is_none() => {
393                match parse_pixel_value(token) {
394                    Ok(pv) => length = Some(pv),
395                    Err(_) => return Err(StyleOverflowClipMarginParseError::InvalidValue(input)),
396                }
397            }
398            _ => return Err(StyleOverflowClipMarginParseError::InvalidValue(input)),
399        }
400    }
401
402    if clip_edge.is_none() && length.is_none() {
403        return Err(StyleOverflowClipMarginParseError::InvalidValue(input));
404    }
405
406    Ok(StyleOverflowClipMargin {
407        clip_edge: clip_edge.unwrap_or_default(),
408        inner: length.unwrap_or_default(),
409    })
410}
411
412// -- StyleClipRect --
413
414/// Represents the deprecated CSS `clip` property value `rect(top, right, bottom, left)`.
415///
416/// Each edge can be a length or `auto`. When `auto`, the edge matches the
417/// element's generated border box edge:
418/// - `auto` for top/left = 0
419/// - `auto` for bottom = used height + vertical padding + vertical border
420/// - `auto` for right = used width + horizontal padding + horizontal border
421///
422/// Negative lengths are permitted.
423// +spec:overflow:297dc3 - clip rect() auto values resolve to border box edges
424#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
425#[repr(C)]
426pub struct StyleClipRect {
427    /// Top edge offset in pixels. `None` means `auto` (= 0).
428    pub top: OptionF32,
429    /// Right edge offset in pixels. `None` means `auto` (= used width + horiz padding + horiz border).
430    pub right: OptionF32,
431    /// Bottom edge offset in pixels. `None` means `auto` (= used height + vert padding + vert border).
432    pub bottom: OptionF32,
433    /// Left edge offset in pixels. `None` means `auto` (= 0).
434    pub left: OptionF32,
435}
436
437impl StyleClipRect {
438    /// Resolves `auto` values to border box edges given the element's
439    /// used width/height and padding/border sizes.
440    ///
441    /// Returns `(top, right, bottom, left)` in pixels.
442    #[must_use] pub fn resolve(
443        &self,
444        used_width: f32,
445        used_height: f32,
446        padding_left: f32,
447        padding_right: f32,
448        padding_top: f32,
449        padding_bottom: f32,
450        border_left: f32,
451        border_right: f32,
452        border_top: f32,
453        border_bottom: f32,
454    ) -> (f32, f32, f32, f32) {
455        let top = self.top.into_option().unwrap_or(0.0);
456        let left = self.left.into_option().unwrap_or(0.0);
457        let bottom = self
458            .bottom
459            .into_option()
460            .unwrap_or(used_height + padding_top + padding_bottom + border_top + border_bottom);
461        let right = self
462            .right
463            .into_option()
464            .unwrap_or(used_width + padding_left + padding_right + border_left + border_right);
465        (top, right, bottom, left)
466    }
467}
468
469impl PrintAsCssValue for StyleClipRect {
470    fn print_as_css_value(&self) -> String {
471        fn fmt_edge(o: OptionF32) -> String {
472            o.into_option()
473                .map_or_else(|| String::from("auto"), |v| format!("{v}px"))
474        }
475        format!(
476            "rect({}, {}, {}, {})",
477            fmt_edge(self.top),
478            fmt_edge(self.right),
479            fmt_edge(self.bottom),
480            fmt_edge(self.left)
481        )
482    }
483}
484
485// -- Parser for StyleClipRect
486
487/// Error returned when parsing a CSS `clip` property value fails.
488#[derive(Clone, PartialEq, Eq)]
489pub enum StyleClipRectParseError<'a> {
490    /// The provided value is not a valid `clip` value.
491    InvalidValue(&'a str),
492}
493
494impl_debug_as_display!(StyleClipRectParseError<'a>);
495impl_display! { StyleClipRectParseError<'a>, {
496    InvalidValue(val) => format!(
497        "Invalid clip value: \"{}\". Expected 'auto' or 'rect(<top>, <right>, <bottom>, <left>)'.", val
498    ),
499}}
500
501/// An owned version of `StyleClipRectParseError`.
502#[derive(Debug, Clone, PartialEq, Eq)]
503#[repr(C, u8)]
504pub enum StyleClipRectParseErrorOwned {
505    InvalidValue(AzString),
506}
507
508impl StyleClipRectParseError<'_> {
509    /// Converts the borrowed error into an owned error.
510    #[must_use] pub fn to_contained(&self) -> StyleClipRectParseErrorOwned {
511        match self {
512            StyleClipRectParseError::InvalidValue(s) => {
513                StyleClipRectParseErrorOwned::InvalidValue((*s).to_string().into())
514            }
515        }
516    }
517}
518
519impl StyleClipRectParseErrorOwned {
520    /// Converts the owned error back into a borrowed error.
521    #[must_use] pub fn to_shared(&self) -> StyleClipRectParseError<'_> {
522        match self {
523            Self::InvalidValue(s) => {
524                StyleClipRectParseError::InvalidValue(s.as_str())
525            }
526        }
527    }
528}
529
530#[cfg(feature = "parser")]
531fn parse_clip_edge(token: &str) -> Result<OptionF32, StyleClipRectParseError<'_>> {
532    use crate::props::basic::pixel::parse_pixel_value;
533
534    let token = token.trim();
535    if token.eq_ignore_ascii_case("auto") {
536        return Ok(OptionF32::None);
537    }
538    let pv = parse_pixel_value(token)
539        .map_err(|_| StyleClipRectParseError::InvalidValue(token))?;
540    Ok(OptionF32::Some(pv.number.get()))
541}
542
543#[cfg(feature = "parser")]
544/// Parses a `StyleClipRect` from a string slice.
545///
546/// Accepts:
547/// - `auto` — equivalent to `rect(auto, auto, auto, auto)`.
548/// - `rect(<top>, <right>, <bottom>, <left>)` — comma-separated form.
549/// - `rect(<top> <right> <bottom> <left>)` — legacy space-separated form.
550///
551/// Each edge is either `auto` or a `<length>`. Negative lengths are permitted.
552/// # Errors
553///
554/// Returns an error if `input` is not a valid CSS `clip-rect` value.
555pub fn parse_clip_rect(input: &str) -> Result<StyleClipRect, StyleClipRectParseError<'_>> {
556    let trimmed = input.trim();
557
558    if trimmed.eq_ignore_ascii_case("auto") {
559        return Ok(StyleClipRect::default());
560    }
561
562    let inner = trimmed
563        .strip_prefix("rect(")
564        .or_else(|| trimmed.strip_prefix("RECT("))
565        .and_then(|s| s.strip_suffix(')'))
566        .ok_or(StyleClipRectParseError::InvalidValue(input))?;
567
568    let inner = inner.trim();
569    let parts: Vec<&str> = if inner.contains(',') {
570        inner.split(',').map(str::trim).collect()
571    } else {
572        inner.split_whitespace().collect()
573    };
574
575    if parts.len() != 4 {
576        return Err(StyleClipRectParseError::InvalidValue(input));
577    }
578
579    Ok(StyleClipRect {
580        top: parse_clip_edge(parts[0])?,
581        right: parse_clip_edge(parts[1])?,
582        bottom: parse_clip_edge(parts[2])?,
583        left: parse_clip_edge(parts[3])?,
584    })
585}
586
587#[cfg(all(test, feature = "parser"))]
588mod tests {
589    use super::*;
590
591    #[test]
592    fn test_parse_layout_overflow_valid() {
593        assert_eq!(
594            parse_layout_overflow("visible").unwrap(),
595            LayoutOverflow::Visible
596        );
597        assert_eq!(
598            parse_layout_overflow("hidden").unwrap(),
599            LayoutOverflow::Hidden
600        );
601        assert_eq!(parse_layout_overflow("clip").unwrap(), LayoutOverflow::Clip);
602        assert_eq!(
603            parse_layout_overflow("scroll").unwrap(),
604            LayoutOverflow::Scroll
605        );
606        assert_eq!(parse_layout_overflow("auto").unwrap(), LayoutOverflow::Auto);
607    }
608
609    #[test]
610    fn test_parse_layout_overflow_whitespace() {
611        assert_eq!(
612            parse_layout_overflow("  scroll  ").unwrap(),
613            LayoutOverflow::Scroll
614        );
615    }
616
617    #[test]
618    fn test_parse_layout_overflow_invalid() {
619        assert!(parse_layout_overflow("none").is_err());
620        assert!(parse_layout_overflow("").is_err());
621        assert!(parse_layout_overflow("auto scroll").is_err());
622        assert!(parse_layout_overflow("hidden-x").is_err());
623    }
624
625    #[test]
626    fn test_needs_scrollbar() {
627        assert!(LayoutOverflow::Scroll.needs_scrollbar(false));
628        assert!(LayoutOverflow::Scroll.needs_scrollbar(true));
629        assert!(LayoutOverflow::Auto.needs_scrollbar(true));
630        assert!(!LayoutOverflow::Auto.needs_scrollbar(false));
631        assert!(!LayoutOverflow::Hidden.needs_scrollbar(true));
632        assert!(!LayoutOverflow::Visible.needs_scrollbar(true));
633        assert!(!LayoutOverflow::Clip.needs_scrollbar(true));
634    }
635
636    #[test]
637    fn test_parse_clip_rect_auto_keyword() {
638        let r = parse_clip_rect("auto").unwrap();
639        assert_eq!(r.top, OptionF32::None);
640        assert_eq!(r.right, OptionF32::None);
641        assert_eq!(r.bottom, OptionF32::None);
642        assert_eq!(r.left, OptionF32::None);
643    }
644
645    #[test]
646    fn test_parse_clip_rect_all_auto_in_rect() {
647        let r = parse_clip_rect("rect(auto, auto, auto, auto)").unwrap();
648        assert_eq!(r.top, OptionF32::None);
649        assert_eq!(r.right, OptionF32::None);
650        assert_eq!(r.bottom, OptionF32::None);
651        assert_eq!(r.left, OptionF32::None);
652    }
653
654    #[test]
655    fn test_parse_clip_rect_mixed_auto_and_lengths() {
656        let r = parse_clip_rect("rect(10px, auto, 30px, auto)").unwrap();
657        assert_eq!(r.top, OptionF32::Some(10.0));
658        assert_eq!(r.right, OptionF32::None);
659        assert_eq!(r.bottom, OptionF32::Some(30.0));
660        assert_eq!(r.left, OptionF32::None);
661    }
662
663    #[test]
664    fn test_parse_clip_rect_negative_lengths() {
665        let r = parse_clip_rect("rect(-5px, 0px, -10px, 0px)").unwrap();
666        assert_eq!(r.top, OptionF32::Some(-5.0));
667        assert_eq!(r.right, OptionF32::Some(0.0));
668        assert_eq!(r.bottom, OptionF32::Some(-10.0));
669        assert_eq!(r.left, OptionF32::Some(0.0));
670    }
671
672    #[test]
673    fn test_parse_clip_rect_legacy_space_separated() {
674        // Legacy CSS 2.1 syntax used spaces instead of commas.
675        let r = parse_clip_rect("rect(1px 2px 3px 4px)").unwrap();
676        assert_eq!(r.top, OptionF32::Some(1.0));
677        assert_eq!(r.right, OptionF32::Some(2.0));
678        assert_eq!(r.bottom, OptionF32::Some(3.0));
679        assert_eq!(r.left, OptionF32::Some(4.0));
680    }
681
682    #[test]
683    fn test_parse_clip_rect_malformed() {
684        assert!(parse_clip_rect("").is_err());
685        assert!(parse_clip_rect("none").is_err());
686        // Wrong number of edges.
687        assert!(parse_clip_rect("rect(10px, 20px, 30px)").is_err());
688        // Missing closing paren.
689        assert!(parse_clip_rect("rect(10px, 20px, 30px, 40px").is_err());
690        // Garbage edge.
691        assert!(parse_clip_rect("rect(10px, abc, 30px, 40px)").is_err());
692    }
693}