Skip to main content

azul_css/props/layout/
fragmentation.rs

1//! CSS properties for controlling fragmentation (page/column breaks).
2//!
3//! Defines [`PageBreak`], [`BreakInside`], [`Widows`], [`Orphans`], and
4//! [`BoxDecorationBreak`]. The `parser` sub-module (behind the `parser`
5//! feature) provides CSS-value parsing for each type.
6
7use alloc::string::{String, ToString};
8
9use crate::props::formatter::PrintAsCssValue;
10
11// --- break-before / break-after ---
12
13/// Represents a `break-before` or `break-after` CSS property value.
14#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
15#[repr(C)]
16#[derive(Default)]
17pub enum PageBreak {
18    #[default]
19    Auto,
20    Avoid,
21    Always,
22    All,
23    Page,
24    AvoidPage,
25    Left,
26    Right,
27    Recto,
28    Verso,
29    Column,
30    AvoidColumn,
31}
32
33
34impl PrintAsCssValue for PageBreak {
35    fn print_as_css_value(&self) -> String {
36        String::from(match self {
37            Self::Auto => "auto",
38            Self::Avoid => "avoid",
39            Self::Always => "always",
40            Self::All => "all",
41            Self::Page => "page",
42            Self::AvoidPage => "avoid-page",
43            Self::Left => "left",
44            Self::Right => "right",
45            Self::Recto => "recto",
46            Self::Verso => "verso",
47            Self::Column => "column",
48            Self::AvoidColumn => "avoid-column",
49        })
50    }
51}
52
53// --- break-inside ---
54
55/// Represents a `break-inside` CSS property value.
56#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57#[repr(C)]
58#[derive(Default)]
59pub enum BreakInside {
60    #[default]
61    Auto,
62    Avoid,
63    AvoidPage,
64    AvoidColumn,
65}
66
67
68impl PrintAsCssValue for BreakInside {
69    fn print_as_css_value(&self) -> String {
70        String::from(match self {
71            Self::Auto => "auto",
72            Self::Avoid => "avoid",
73            Self::AvoidPage => "avoid-page",
74            Self::AvoidColumn => "avoid-column",
75        })
76    }
77}
78
79// --- widows / orphans ---
80
81/// CSS `widows` property - minimum number of lines in a block container
82/// that must be shown at the top of a page, region, or column.
83#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
84#[repr(C)]
85pub struct Widows {
86    pub inner: u32,
87}
88
89impl Default for Widows {
90    fn default() -> Self {
91        Self { inner: 2 }
92    }
93}
94
95impl PrintAsCssValue for Widows {
96    fn print_as_css_value(&self) -> String {
97        self.inner.to_string()
98    }
99}
100
101/// CSS `orphans` property - minimum number of lines in a block container
102/// that must be shown at the bottom of a page, region, or column.
103#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
104#[repr(C)]
105pub struct Orphans {
106    pub inner: u32,
107}
108
109impl Default for Orphans {
110    fn default() -> Self {
111        Self { inner: 2 }
112    }
113}
114
115impl PrintAsCssValue for Orphans {
116    fn print_as_css_value(&self) -> String {
117        self.inner.to_string()
118    }
119}
120
121// --- box-decoration-break ---
122
123/// Represents a `box-decoration-break` CSS property value.
124#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125#[repr(C)]
126#[derive(Default)]
127pub enum BoxDecorationBreak {
128    #[default]
129    Slice,
130    Clone,
131}
132
133
134impl PrintAsCssValue for BoxDecorationBreak {
135    fn print_as_css_value(&self) -> String {
136        String::from(match self {
137            Self::Slice => "slice",
138            Self::Clone => "clone",
139        })
140    }
141}
142
143// Formatting to Rust code
144impl crate::format_rust_code::FormatAsRustCode for PageBreak {
145    fn format_as_rust_code(&self, _tabs: usize) -> String {
146        match self {
147            PageBreak::Auto => String::from("PageBreak::Auto"),
148            PageBreak::Avoid => String::from("PageBreak::Avoid"),
149            PageBreak::Always => String::from("PageBreak::Always"),
150            PageBreak::All => String::from("PageBreak::All"),
151            PageBreak::Page => String::from("PageBreak::Page"),
152            PageBreak::AvoidPage => String::from("PageBreak::AvoidPage"),
153            PageBreak::Left => String::from("PageBreak::Left"),
154            PageBreak::Right => String::from("PageBreak::Right"),
155            PageBreak::Recto => String::from("PageBreak::Recto"),
156            PageBreak::Verso => String::from("PageBreak::Verso"),
157            PageBreak::Column => String::from("PageBreak::Column"),
158            PageBreak::AvoidColumn => String::from("PageBreak::AvoidColumn"),
159        }
160    }
161}
162
163impl crate::format_rust_code::FormatAsRustCode for BreakInside {
164    fn format_as_rust_code(&self, _tabs: usize) -> String {
165        match self {
166            BreakInside::Auto => String::from("BreakInside::Auto"),
167            BreakInside::Avoid => String::from("BreakInside::Avoid"),
168            BreakInside::AvoidPage => String::from("BreakInside::AvoidPage"),
169            BreakInside::AvoidColumn => String::from("BreakInside::AvoidColumn"),
170        }
171    }
172}
173
174impl crate::format_rust_code::FormatAsRustCode for Widows {
175    fn format_as_rust_code(&self, _tabs: usize) -> String {
176        format!("Widows {{ inner: {} }}", self.inner)
177    }
178}
179
180impl crate::format_rust_code::FormatAsRustCode for Orphans {
181    fn format_as_rust_code(&self, _tabs: usize) -> String {
182        format!("Orphans {{ inner: {} }}", self.inner)
183    }
184}
185
186impl crate::format_rust_code::FormatAsRustCode for BoxDecorationBreak {
187    fn format_as_rust_code(&self, _tabs: usize) -> String {
188        match self {
189            BoxDecorationBreak::Slice => String::from("BoxDecorationBreak::Slice"),
190            BoxDecorationBreak::Clone => String::from("BoxDecorationBreak::Clone"),
191        }
192    }
193}
194
195// --- PARSERS ---
196
197#[cfg(feature = "parser")]
198pub mod parser {
199    use super::*;
200    use core::num::ParseIntError;
201    use crate::corety::AzString;
202    use crate::props::layout::position::ParseIntErrorWithInput;
203
204    // -- PageBreak parser (`break-before`, `break-after`)
205
206    /// Error returned when parsing a `break-before` or `break-after` value.
207    #[derive(Clone, PartialEq)]
208    pub enum PageBreakParseError<'a> {
209        InvalidValue(&'a str),
210    }
211
212    impl_debug_as_display!(PageBreakParseError<'a>);
213    impl_display! { PageBreakParseError<'a>, {
214        InvalidValue(v) => format!("Invalid break value: \"{}\"", v),
215    }}
216
217    /// Owned version of [`PageBreakParseError`] for FFI and storage.
218    #[derive(Debug, Clone, PartialEq)]
219    #[repr(C, u8)]
220    pub enum PageBreakParseErrorOwned {
221        InvalidValue(AzString),
222    }
223
224    impl<'a> PageBreakParseError<'a> {
225        pub fn to_contained(&self) -> PageBreakParseErrorOwned {
226            match self {
227                Self::InvalidValue(s) => PageBreakParseErrorOwned::InvalidValue(s.to_string().into()),
228            }
229        }
230    }
231
232    impl PageBreakParseErrorOwned {
233        pub fn to_shared<'a>(&'a self) -> PageBreakParseError<'a> {
234            match self {
235                Self::InvalidValue(s) => PageBreakParseError::InvalidValue(s.as_str()),
236            }
237        }
238    }
239
240    pub fn parse_page_break<'a>(input: &'a str) -> Result<PageBreak, PageBreakParseError<'a>> {
241        match input.trim() {
242            "auto" => Ok(PageBreak::Auto),
243            "avoid" => Ok(PageBreak::Avoid),
244            "always" => Ok(PageBreak::Always),
245            "all" => Ok(PageBreak::All),
246            "page" => Ok(PageBreak::Page),
247            "avoid-page" => Ok(PageBreak::AvoidPage),
248            "left" => Ok(PageBreak::Left),
249            "right" => Ok(PageBreak::Right),
250            "recto" => Ok(PageBreak::Recto),
251            "verso" => Ok(PageBreak::Verso),
252            "column" => Ok(PageBreak::Column),
253            "avoid-column" => Ok(PageBreak::AvoidColumn),
254            _ => Err(PageBreakParseError::InvalidValue(input)),
255        }
256    }
257
258    // -- BreakInside parser
259
260    /// Error returned when parsing a `break-inside` value.
261    #[derive(Clone, PartialEq)]
262    pub enum BreakInsideParseError<'a> {
263        InvalidValue(&'a str),
264    }
265
266    impl_debug_as_display!(BreakInsideParseError<'a>);
267    impl_display! { BreakInsideParseError<'a>, {
268        InvalidValue(v) => format!("Invalid break-inside value: \"{}\"", v),
269    }}
270
271    /// Owned version of [`BreakInsideParseError`] for FFI and storage.
272    #[derive(Debug, Clone, PartialEq)]
273    #[repr(C, u8)]
274    pub enum BreakInsideParseErrorOwned {
275        InvalidValue(AzString),
276    }
277
278    impl<'a> BreakInsideParseError<'a> {
279        pub fn to_contained(&self) -> BreakInsideParseErrorOwned {
280            match self {
281                Self::InvalidValue(s) => BreakInsideParseErrorOwned::InvalidValue(s.to_string().into()),
282            }
283        }
284    }
285
286    impl BreakInsideParseErrorOwned {
287        pub fn to_shared<'a>(&'a self) -> BreakInsideParseError<'a> {
288            match self {
289                Self::InvalidValue(s) => BreakInsideParseError::InvalidValue(s.as_str()),
290            }
291        }
292    }
293
294    pub fn parse_break_inside<'a>(
295        input: &'a str,
296    ) -> Result<BreakInside, BreakInsideParseError<'a>> {
297        match input.trim() {
298            "auto" => Ok(BreakInside::Auto),
299            "avoid" => Ok(BreakInside::Avoid),
300            "avoid-page" => Ok(BreakInside::AvoidPage),
301            "avoid-column" => Ok(BreakInside::AvoidColumn),
302            _ => Err(BreakInsideParseError::InvalidValue(input)),
303        }
304    }
305
306    // -- Widows / Orphans parsers
307
308    macro_rules! define_widow_orphan_parser {
309        ($fn_name:ident, $struct_name:ident, $error_name:ident, $error_owned_name:ident, $prop_name:expr) => {
310            #[derive(Clone, PartialEq)]
311            pub enum $error_name<'a> {
312                ParseInt(ParseIntError, &'a str),
313                ParseIntOwned(&'a str, &'a str),
314                NegativeValue(&'a str),
315            }
316
317            impl_debug_as_display!($error_name<'a>);
318            impl_display! { $error_name<'a>, {
319                ParseInt(e, s) => format!("Invalid integer for {}: \"{}\". Reason: {}", $prop_name, s, e),
320                ParseIntOwned(e, s) => format!("Invalid integer for {}: \"{}\". Reason: {}", $prop_name, s, e),
321                NegativeValue(s) => format!("Invalid value for {}: \"{}\". Value cannot be negative.", $prop_name, s),
322            }}
323
324            #[derive(Debug, Clone, PartialEq)]
325            #[repr(C, u8)]
326            pub enum $error_owned_name {
327                ParseInt(ParseIntErrorWithInput),
328                NegativeValue(AzString),
329            }
330
331            impl<'a> $error_name<'a> {
332                pub fn to_contained(&self) -> $error_owned_name {
333                    match self {
334                        Self::ParseInt(e, s) => $error_owned_name::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: s.to_string().into() }),
335                        Self::ParseIntOwned(e, s) => $error_owned_name::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: s.to_string().into() }),
336                        Self::NegativeValue(s) => $error_owned_name::NegativeValue(s.to_string().into()),
337                    }
338                }
339            }
340
341            impl $error_owned_name {
342                pub fn to_shared<'a>(&'a self) -> $error_name<'a> {
343                     match self {
344                        Self::ParseInt(e) => $error_name::ParseIntOwned(e.error.as_str(), e.input.as_str()),
345                        Self::NegativeValue(s) => $error_name::NegativeValue(s),
346                    }
347                }
348            }
349
350            pub fn $fn_name<'a>(input: &'a str) -> Result<$struct_name, $error_name<'a>> {
351                let trimmed = input.trim();
352                let val: i32 = trimmed.parse().map_err(|e| $error_name::ParseInt(e, trimmed))?;
353                if val < 0 {
354                    return Err($error_name::NegativeValue(trimmed));
355                }
356                Ok($struct_name { inner: val as u32 })
357            }
358        };
359    }
360
361    define_widow_orphan_parser!(
362        parse_widows,
363        Widows,
364        WidowsParseError,
365        WidowsParseErrorOwned,
366        "widows"
367    );
368    define_widow_orphan_parser!(
369        parse_orphans,
370        Orphans,
371        OrphansParseError,
372        OrphansParseErrorOwned,
373        "orphans"
374    );
375
376    // -- BoxDecorationBreak parser
377
378    /// Error returned when parsing a `box-decoration-break` value.
379    #[derive(Clone, PartialEq)]
380    pub enum BoxDecorationBreakParseError<'a> {
381        InvalidValue(&'a str),
382    }
383
384    impl_debug_as_display!(BoxDecorationBreakParseError<'a>);
385    impl_display! { BoxDecorationBreakParseError<'a>, {
386        InvalidValue(v) => format!("Invalid box-decoration-break value: \"{}\"", v),
387    }}
388
389    /// Owned version of [`BoxDecorationBreakParseError`] for FFI and storage.
390    #[derive(Debug, Clone, PartialEq)]
391    #[repr(C, u8)]
392    pub enum BoxDecorationBreakParseErrorOwned {
393        InvalidValue(AzString),
394    }
395
396    impl<'a> BoxDecorationBreakParseError<'a> {
397        pub fn to_contained(&self) -> BoxDecorationBreakParseErrorOwned {
398            match self {
399                Self::InvalidValue(s) => {
400                    BoxDecorationBreakParseErrorOwned::InvalidValue(s.to_string().into())
401                }
402            }
403        }
404    }
405
406    impl BoxDecorationBreakParseErrorOwned {
407        pub fn to_shared<'a>(&'a self) -> BoxDecorationBreakParseError<'a> {
408            match self {
409                Self::InvalidValue(s) => BoxDecorationBreakParseError::InvalidValue(s.as_str()),
410            }
411        }
412    }
413
414    pub fn parse_box_decoration_break<'a>(
415        input: &'a str,
416    ) -> Result<BoxDecorationBreak, BoxDecorationBreakParseError<'a>> {
417        match input.trim() {
418            "slice" => Ok(BoxDecorationBreak::Slice),
419            "clone" => Ok(BoxDecorationBreak::Clone),
420            _ => Err(BoxDecorationBreakParseError::InvalidValue(input)),
421        }
422    }
423}
424
425#[cfg(feature = "parser")]
426pub use parser::*;
427
428#[cfg(all(test, feature = "parser"))]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn test_parse_page_break() {
434        assert_eq!(parse_page_break("auto").unwrap(), PageBreak::Auto);
435        assert_eq!(parse_page_break("page").unwrap(), PageBreak::Page);
436        assert_eq!(
437            parse_page_break("avoid-column").unwrap(),
438            PageBreak::AvoidColumn
439        );
440        assert!(parse_page_break("invalid").is_err());
441    }
442
443    #[test]
444    fn test_parse_break_inside() {
445        assert_eq!(parse_break_inside("auto").unwrap(), BreakInside::Auto);
446        assert_eq!(parse_break_inside("avoid").unwrap(), BreakInside::Avoid);
447        assert!(parse_break_inside("always").is_err());
448    }
449
450    #[test]
451    fn test_parse_widows_orphans() {
452        assert_eq!(parse_widows("3").unwrap().inner, 3);
453        assert_eq!(parse_orphans("  1  ").unwrap().inner, 1);
454        assert!(parse_widows("-2").is_err());
455        assert!(parse_orphans("auto").is_err());
456    }
457
458    #[test]
459    fn test_parse_box_decoration_break() {
460        assert_eq!(
461            parse_box_decoration_break("slice").unwrap(),
462            BoxDecorationBreak::Slice
463        );
464        assert_eq!(
465            parse_box_decoration_break("clone").unwrap(),
466            BoxDecorationBreak::Clone
467        );
468        assert!(parse_box_decoration_break("copy").is_err());
469    }
470}