1use alloc::string::{String, ToString};
4use crate::corety::{AzString, OptionF32};
5
6use crate::props::formatter::PrintAsCssValue;
7
8#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
15#[repr(C)]
16pub enum LayoutOverflow {
17 Scroll,
19 Auto,
21 Hidden,
23 #[default]
26 Visible,
27 Clip,
29}
30
31impl LayoutOverflow {
32 #[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 #[must_use] pub const fn is_clipped(&self) -> bool {
54 matches!(
56 self,
57 Self::Hidden
58 | Self::Clip
59 | Self::Auto
60 | Self::Scroll
61 )
62 }
63
64 #[must_use] pub const fn is_scroll(&self) -> bool {
67 matches!(self, Self::Scroll)
68 }
69
70 #[must_use] pub fn is_overflow_visible(&self) -> bool {
73 *self == Self::Visible
74 }
75
76 #[must_use] pub fn is_overflow_hidden(&self) -> bool {
78 *self == Self::Hidden
79 }
80
81 #[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#[derive(Clone, PartialEq, Eq)]
115pub enum LayoutOverflowParseError<'a> {
116 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#[derive(Debug, Clone, PartialEq, Eq)]
129#[repr(C, u8)]
130pub enum LayoutOverflowParseErrorOwned {
131 InvalidValue(AzString),
132}
133
134impl LayoutOverflowParseError<'_> {
135 #[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 #[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")]
157pub 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), "hidden" => Ok(LayoutOverflow::Hidden),
169 "visible" => Ok(LayoutOverflow::Visible),
170 "clip" => Ok(LayoutOverflow::Clip),
171 _ => Err(LayoutOverflowParseError::InvalidValue(input)),
172 }
173}
174
175#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
184#[repr(C)]
185pub enum StyleScrollbarGutter {
186 #[default]
188 Auto,
189 Stable,
191 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#[derive(Clone, PartialEq, Eq)]
209pub enum StyleScrollbarGutterParseError<'a> {
210 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#[derive(Debug, Clone, PartialEq, Eq)]
223#[repr(C, u8)]
224pub enum StyleScrollbarGutterParseErrorOwned {
225 InvalidValue(AzString),
226}
227
228impl StyleScrollbarGutterParseError<'_> {
229 #[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 #[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")]
251pub 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#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
275#[repr(C)]
276pub enum VisualBox {
277 ContentBox,
279 #[default]
281 PaddingBox,
282 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#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
305#[repr(C)]
306pub struct StyleOverflowClipMargin {
307 pub clip_edge: VisualBox,
309 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)] 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#[derive(Clone, PartialEq, Eq)]
330pub enum StyleOverflowClipMarginParseError<'a> {
331 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#[derive(Debug, Clone, PartialEq, Eq)]
342#[repr(C, u8)]
343pub enum StyleOverflowClipMarginParseErrorOwned {
344 InvalidValue(AzString),
345}
346
347impl StyleOverflowClipMarginParseError<'_> {
348 #[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 #[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")]
370pub 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#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
425#[repr(C)]
426pub struct StyleClipRect {
427 pub top: OptionF32,
429 pub right: OptionF32,
431 pub bottom: OptionF32,
433 pub left: OptionF32,
435}
436
437impl StyleClipRect {
438 #[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#[derive(Clone, PartialEq, Eq)]
489pub enum StyleClipRectParseError<'a> {
490 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#[derive(Debug, Clone, PartialEq, Eq)]
503#[repr(C, u8)]
504pub enum StyleClipRectParseErrorOwned {
505 InvalidValue(AzString),
506}
507
508impl StyleClipRectParseError<'_> {
509 #[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 #[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")]
544pub 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 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 assert!(parse_clip_rect("rect(10px, 20px, 30px)").is_err());
688 assert!(parse_clip_rect("rect(10px, 20px, 30px, 40px").is_err());
690 assert!(parse_clip_rect("rect(10px, abc, 30px, 40px)").is_err());
692 }
693}