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    pub fn needs_scrollbar(&self, currently_overflowing: bool) -> bool {
41        match self {
42            LayoutOverflow::Scroll => true,
43            LayoutOverflow::Auto => currently_overflowing,
44            LayoutOverflow::Hidden | LayoutOverflow::Visible | LayoutOverflow::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    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    pub fn resolve_computed(self, other_axis: LayoutOverflow) -> LayoutOverflow {
86        let other_is_scrollable = !matches!(other_axis, LayoutOverflow::Visible | LayoutOverflow::Clip);
87        if other_is_scrollable {
88            match self {
89                LayoutOverflow::Visible => LayoutOverflow::Auto,
90                LayoutOverflow::Clip => LayoutOverflow::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            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)]
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<'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
145impl 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.
158pub fn parse_layout_overflow<'a>(
159    input: &'a str,
160) -> Result<LayoutOverflow, LayoutOverflowParseError<'a>> {
161    let input_trimmed = input.trim();
162    match input_trimmed {
163        "scroll" => Ok(LayoutOverflow::Scroll),
164        "auto" | "overlay" => Ok(LayoutOverflow::Auto), // +spec:overflow:6120e6 - "overlay" is a legacy value alias of "auto"
165        "hidden" => Ok(LayoutOverflow::Hidden),
166        "visible" => Ok(LayoutOverflow::Visible),
167        "clip" => Ok(LayoutOverflow::Clip),
168        _ => Err(LayoutOverflowParseError::InvalidValue(input)),
169    }
170}
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)]
182pub 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
192impl 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)]
206pub enum StyleScrollbarGutterParseError<'a> {
207    /// The provided value is not a valid `scrollbar-gutter` keyword.
208    InvalidValue(&'a str),
209}
210
211impl_debug_as_display!(StyleScrollbarGutterParseError<'a>);
212impl_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)]
221pub enum StyleScrollbarGutterParseErrorOwned {
222    InvalidValue(AzString),
223}
224
225impl<'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
236impl 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.
249pub 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)]
270pub 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
280impl 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)]
300pub 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
307impl 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)]
324pub enum StyleOverflowClipMarginParseError<'a> {
325    /// The provided value is not a valid `overflow-clip-margin` value.
326    InvalidValue(&'a str),
327}
328
329impl_debug_as_display!(StyleOverflowClipMarginParseError<'a>);
330impl_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)]
337pub enum StyleOverflowClipMarginParseErrorOwned {
338    InvalidValue(AzString),
339}
340
341impl<'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
352impl 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.
369pub 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)]
417pub 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
428impl 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
460impl 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)]
482pub enum StyleClipRectParseError<'a> {
483    /// The provided value is not a valid `clip` value.
484    InvalidValue(&'a str),
485}
486
487impl_debug_as_display!(StyleClipRectParseError<'a>);
488impl_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)]
497pub enum StyleClipRectParseErrorOwned {
498    InvalidValue(AzString),
499}
500
501impl<'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
512impl 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")]
524fn parse_clip_edge<'a>(token: &'a str) -> Result<OptionF32, StyleClipRectParseError<'a>> {
525    use crate::props::basic::pixel::parse_pixel_value;
526
527    let token = token.trim();
528    if token.eq_ignore_ascii_case("auto") {
529        return Ok(OptionF32::None);
530    }
531    let pv = parse_pixel_value(token)
532        .map_err(|_| StyleClipRectParseError::InvalidValue(token))?;
533    Ok(OptionF32::Some(pv.number.get()))
534}
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.
545pub fn parse_clip_rect<'a>(input: &'a str) -> Result<StyleClipRect, StyleClipRectParseError<'a>> {
546    let trimmed = input.trim();
547
548    if trimmed.eq_ignore_ascii_case("auto") {
549        return Ok(StyleClipRect::default());
550    }
551
552    let inner = trimmed
553        .strip_prefix("rect(")
554        .or_else(|| trimmed.strip_prefix("RECT("))
555        .and_then(|s| s.strip_suffix(')'))
556        .ok_or(StyleClipRectParseError::InvalidValue(input))?;
557
558    let inner = inner.trim();
559    let parts: alloc::vec::Vec<&str> = if inner.contains(',') {
560        inner.split(',').map(|s| s.trim()).collect()
561    } else {
562        inner.split_whitespace().collect()
563    };
564
565    if parts.len() != 4 {
566        return Err(StyleClipRectParseError::InvalidValue(input));
567    }
568
569    Ok(StyleClipRect {
570        top: parse_clip_edge(parts[0])?,
571        right: parse_clip_edge(parts[1])?,
572        bottom: parse_clip_edge(parts[2])?,
573        left: parse_clip_edge(parts[3])?,
574    })
575}
576
577#[cfg(all(test, feature = "parser"))]
578mod tests {
579    use super::*;
580
581    #[test]
582    fn test_parse_layout_overflow_valid() {
583        assert_eq!(
584            parse_layout_overflow("visible").unwrap(),
585            LayoutOverflow::Visible
586        );
587        assert_eq!(
588            parse_layout_overflow("hidden").unwrap(),
589            LayoutOverflow::Hidden
590        );
591        assert_eq!(parse_layout_overflow("clip").unwrap(), LayoutOverflow::Clip);
592        assert_eq!(
593            parse_layout_overflow("scroll").unwrap(),
594            LayoutOverflow::Scroll
595        );
596        assert_eq!(parse_layout_overflow("auto").unwrap(), LayoutOverflow::Auto);
597    }
598
599    #[test]
600    fn test_parse_layout_overflow_whitespace() {
601        assert_eq!(
602            parse_layout_overflow("  scroll  ").unwrap(),
603            LayoutOverflow::Scroll
604        );
605    }
606
607    #[test]
608    fn test_parse_layout_overflow_invalid() {
609        assert!(parse_layout_overflow("none").is_err());
610        assert!(parse_layout_overflow("").is_err());
611        assert!(parse_layout_overflow("auto scroll").is_err());
612        assert!(parse_layout_overflow("hidden-x").is_err());
613    }
614
615    #[test]
616    fn test_needs_scrollbar() {
617        assert!(LayoutOverflow::Scroll.needs_scrollbar(false));
618        assert!(LayoutOverflow::Scroll.needs_scrollbar(true));
619        assert!(LayoutOverflow::Auto.needs_scrollbar(true));
620        assert!(!LayoutOverflow::Auto.needs_scrollbar(false));
621        assert!(!LayoutOverflow::Hidden.needs_scrollbar(true));
622        assert!(!LayoutOverflow::Visible.needs_scrollbar(true));
623        assert!(!LayoutOverflow::Clip.needs_scrollbar(true));
624    }
625
626    #[test]
627    fn test_parse_clip_rect_auto_keyword() {
628        let r = parse_clip_rect("auto").unwrap();
629        assert_eq!(r.top, OptionF32::None);
630        assert_eq!(r.right, OptionF32::None);
631        assert_eq!(r.bottom, OptionF32::None);
632        assert_eq!(r.left, OptionF32::None);
633    }
634
635    #[test]
636    fn test_parse_clip_rect_all_auto_in_rect() {
637        let r = parse_clip_rect("rect(auto, auto, auto, auto)").unwrap();
638        assert_eq!(r.top, OptionF32::None);
639        assert_eq!(r.right, OptionF32::None);
640        assert_eq!(r.bottom, OptionF32::None);
641        assert_eq!(r.left, OptionF32::None);
642    }
643
644    #[test]
645    fn test_parse_clip_rect_mixed_auto_and_lengths() {
646        let r = parse_clip_rect("rect(10px, auto, 30px, auto)").unwrap();
647        assert_eq!(r.top, OptionF32::Some(10.0));
648        assert_eq!(r.right, OptionF32::None);
649        assert_eq!(r.bottom, OptionF32::Some(30.0));
650        assert_eq!(r.left, OptionF32::None);
651    }
652
653    #[test]
654    fn test_parse_clip_rect_negative_lengths() {
655        let r = parse_clip_rect("rect(-5px, 0px, -10px, 0px)").unwrap();
656        assert_eq!(r.top, OptionF32::Some(-5.0));
657        assert_eq!(r.right, OptionF32::Some(0.0));
658        assert_eq!(r.bottom, OptionF32::Some(-10.0));
659        assert_eq!(r.left, OptionF32::Some(0.0));
660    }
661
662    #[test]
663    fn test_parse_clip_rect_legacy_space_separated() {
664        // Legacy CSS 2.1 syntax used spaces instead of commas.
665        let r = parse_clip_rect("rect(1px 2px 3px 4px)").unwrap();
666        assert_eq!(r.top, OptionF32::Some(1.0));
667        assert_eq!(r.right, OptionF32::Some(2.0));
668        assert_eq!(r.bottom, OptionF32::Some(3.0));
669        assert_eq!(r.left, OptionF32::Some(4.0));
670    }
671
672    #[test]
673    fn test_parse_clip_rect_malformed() {
674        assert!(parse_clip_rect("").is_err());
675        assert!(parse_clip_rect("none").is_err());
676        // Wrong number of edges.
677        assert!(parse_clip_rect("rect(10px, 20px, 30px)").is_err());
678        // Missing closing paren.
679        assert!(parse_clip_rect("rect(10px, 20px, 30px, 40px").is_err());
680        // Garbage edge.
681        assert!(parse_clip_rect("rect(10px, abc, 30px, 40px)").is_err());
682    }
683}