Skip to main content

azul_css/props/layout/
table.rs

1//! CSS properties for table layout and styling.
2//!
3//! This module contains properties specific to CSS table formatting:
4//! - `table-layout`: Controls the algorithm used to layout table cells, rows, and columns
5//! - `border-collapse`: Specifies whether cell borders are collapsed into a single border or
6//!   separated
7//! - `border-spacing`: Sets the distance between borders of adjacent cells (separate borders only)
8//! - `caption-side`: Specifies the placement of a table caption
9//! - `empty-cells`: Specifies whether or not to display borders on empty cells in a table
10
11use alloc::string::{String, ToString};
12
13use crate::{
14    format_rust_code::FormatAsRustCode,
15    props::{
16        basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
17        formatter::PrintAsCssValue,
18        macros::PixelValueTaker,
19    },
20};
21
22// table-layout
23
24/// Controls the algorithm used to lay out table cells, rows, and columns.
25///
26/// The `table-layout` property determines whether the browser should use:
27/// - **auto**: Column widths are determined by the content (slower but flexible)
28/// - **fixed**: Column widths are determined by the first row (faster and predictable)
29#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
30#[repr(C)]
31pub enum LayoutTableLayout {
32    /// Use automatic table layout algorithm (content-based, default).
33    /// Column width is set by the widest unbreakable content in the cells.
34    Auto,
35    /// Use fixed table layout algorithm (first-row-based).
36    /// Column width is set by the width property of the column or first-row cell.
37    /// Renders faster than auto.
38    Fixed,
39}
40
41impl Default for LayoutTableLayout {
42    fn default() -> Self {
43        Self::Auto
44    }
45}
46
47impl PrintAsCssValue for LayoutTableLayout {
48    fn print_as_css_value(&self) -> String {
49        match self {
50            LayoutTableLayout::Auto => "auto".to_string(),
51            LayoutTableLayout::Fixed => "fixed".to_string(),
52        }
53    }
54}
55
56impl FormatAsRustCode for LayoutTableLayout {
57    fn format_as_rust_code(&self, _tabs: usize) -> String {
58        match self {
59            LayoutTableLayout::Auto => "LayoutTableLayout::Auto".to_string(),
60            LayoutTableLayout::Fixed => "LayoutTableLayout::Fixed".to_string(),
61        }
62    }
63}
64
65// border-collapse
66
67/// Specifies whether cell borders are collapsed into a single border or separated.
68///
69/// The `border-collapse` property determines the border rendering model:
70/// - **separate**: Each cell has its own border (default, uses border-spacing)
71/// - **collapse**: Adjacent cells share borders (ignores border-spacing)
72#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
73#[repr(C)]
74pub enum StyleBorderCollapse {
75    /// Borders are separated (default). Each cell has its own border.
76    /// The `border-spacing` property defines the distance between borders.
77    Separate,
78    /// Borders are collapsed. Adjacent cells share a single border.
79    /// Border conflict resolution rules apply when borders differ.
80    Collapse,
81}
82
83impl Default for StyleBorderCollapse {
84    fn default() -> Self {
85        Self::Separate
86    }
87}
88
89impl PrintAsCssValue for StyleBorderCollapse {
90    fn print_as_css_value(&self) -> String {
91        match self {
92            StyleBorderCollapse::Separate => "separate".to_string(),
93            StyleBorderCollapse::Collapse => "collapse".to_string(),
94        }
95    }
96}
97
98impl FormatAsRustCode for StyleBorderCollapse {
99    fn format_as_rust_code(&self, _tabs: usize) -> String {
100        match self {
101            StyleBorderCollapse::Separate => "StyleBorderCollapse::Separate".to_string(),
102            StyleBorderCollapse::Collapse => "StyleBorderCollapse::Collapse".to_string(),
103        }
104    }
105}
106
107// border-spacing
108
109/// Sets the distance between the borders of adjacent cells.
110///
111/// The `border-spacing` property is only applicable when `border-collapse` is set to `separate`.
112/// It can have one or two values:
113/// - One value: Sets both horizontal and vertical spacing
114/// - Two values: First is horizontal, second is vertical
115///
116/// This struct represents a single spacing value (either horizontal or vertical).
117#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
118#[repr(C)]
119pub struct LayoutBorderSpacing {
120    /// Horizontal spacing between cell borders
121    pub horizontal: PixelValue,
122    /// Vertical spacing between cell borders
123    pub vertical: PixelValue,
124}
125
126impl Default for LayoutBorderSpacing {
127    fn default() -> Self {
128        // Default border-spacing is 0 (no spacing)
129        Self {
130            horizontal: PixelValue::const_px(0),
131            vertical: PixelValue::const_px(0),
132        }
133    }
134}
135
136impl LayoutBorderSpacing {
137    /// Creates a new border spacing with the same value for horizontal and vertical
138    pub const fn new(spacing: PixelValue) -> Self {
139        Self {
140            horizontal: spacing,
141            vertical: spacing,
142        }
143    }
144
145    /// Creates a new border spacing with different horizontal and vertical values
146    pub const fn new_separate(horizontal: PixelValue, vertical: PixelValue) -> Self {
147        Self {
148            horizontal,
149            vertical,
150        }
151    }
152
153    /// Creates a border spacing with zero spacing
154    pub const fn zero() -> Self {
155        Self {
156            horizontal: PixelValue::const_px(0),
157            vertical: PixelValue::const_px(0),
158        }
159    }
160}
161
162impl PrintAsCssValue for LayoutBorderSpacing {
163    fn print_as_css_value(&self) -> String {
164        if self.horizontal == self.vertical {
165            // Single value: same for both dimensions
166            self.horizontal.to_string()
167        } else {
168            // Two values: horizontal vertical
169            format!("{} {}", self.horizontal, self.vertical)
170        }
171    }
172}
173
174impl FormatAsRustCode for LayoutBorderSpacing {
175    fn format_as_rust_code(&self, _tabs: usize) -> String {
176        use crate::format_rust_code::format_pixel_value;
177        format!(
178            "LayoutBorderSpacing {{ horizontal: {}, vertical: {} }}",
179            format_pixel_value(&self.horizontal),
180            format_pixel_value(&self.vertical)
181        )
182    }
183}
184
185// caption-side
186
187/// Specifies the placement of a table caption.
188///
189/// The `caption-side` property positions the caption either above or below the table.
190#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
191#[repr(C)]
192pub enum StyleCaptionSide {
193    /// Caption is placed above the table (default)
194    Top,
195    /// Caption is placed below the table
196    Bottom,
197}
198
199impl Default for StyleCaptionSide {
200    fn default() -> Self {
201        Self::Top
202    }
203}
204
205impl PrintAsCssValue for StyleCaptionSide {
206    fn print_as_css_value(&self) -> String {
207        match self {
208            StyleCaptionSide::Top => "top".to_string(),
209            StyleCaptionSide::Bottom => "bottom".to_string(),
210        }
211    }
212}
213
214impl FormatAsRustCode for StyleCaptionSide {
215    fn format_as_rust_code(&self, _tabs: usize) -> String {
216        match self {
217            StyleCaptionSide::Top => "StyleCaptionSide::Top".to_string(),
218            StyleCaptionSide::Bottom => "StyleCaptionSide::Bottom".to_string(),
219        }
220    }
221}
222
223// empty-cells
224
225/// Specifies whether or not to display borders and background on empty cells.
226///
227/// The `empty-cells` property only applies when `border-collapse` is set to `separate`.
228/// A cell is considered empty if it contains no visible content.
229#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
230#[repr(C)]
231pub enum StyleEmptyCells {
232    /// Show borders and background on empty cells (default)
233    Show,
234    /// Hide borders and background on empty cells
235    Hide,
236}
237
238impl Default for StyleEmptyCells {
239    fn default() -> Self {
240        Self::Show
241    }
242}
243
244impl PrintAsCssValue for StyleEmptyCells {
245    fn print_as_css_value(&self) -> String {
246        match self {
247            StyleEmptyCells::Show => "show".to_string(),
248            StyleEmptyCells::Hide => "hide".to_string(),
249        }
250    }
251}
252
253impl FormatAsRustCode for StyleEmptyCells {
254    fn format_as_rust_code(&self, _tabs: usize) -> String {
255        match self {
256            StyleEmptyCells::Show => "StyleEmptyCells::Show".to_string(),
257            StyleEmptyCells::Hide => "StyleEmptyCells::Hide".to_string(),
258        }
259    }
260}
261
262// Parsing Functions
263
264/// Parse errors for table-layout property
265#[derive(Debug, Clone, PartialEq)]
266pub enum LayoutTableLayoutParseError<'a> {
267    InvalidKeyword(&'a str),
268}
269
270/// Parse a table-layout value from a string
271pub fn parse_table_layout<'a>(
272    input: &'a str,
273) -> Result<LayoutTableLayout, LayoutTableLayoutParseError<'a>> {
274    match input.trim() {
275        "auto" => Ok(LayoutTableLayout::Auto),
276        "fixed" => Ok(LayoutTableLayout::Fixed),
277        other => Err(LayoutTableLayoutParseError::InvalidKeyword(other)),
278    }
279}
280
281/// Parse errors for border-collapse property
282#[derive(Debug, Clone, PartialEq)]
283pub enum StyleBorderCollapseParseError<'a> {
284    InvalidKeyword(&'a str),
285}
286
287/// Parse a border-collapse value from a string
288pub fn parse_border_collapse<'a>(
289    input: &'a str,
290) -> Result<StyleBorderCollapse, StyleBorderCollapseParseError<'a>> {
291    match input.trim() {
292        "separate" => Ok(StyleBorderCollapse::Separate),
293        "collapse" => Ok(StyleBorderCollapse::Collapse),
294        other => Err(StyleBorderCollapseParseError::InvalidKeyword(other)),
295    }
296}
297
298/// Parse errors for border-spacing property
299#[derive(Debug, Clone, PartialEq)]
300pub enum LayoutBorderSpacingParseError<'a> {
301    PixelValue(CssPixelValueParseError<'a>),
302    InvalidFormat,
303}
304
305/// Parse a border-spacing value from a string
306/// Accepts: "5px" or "5px 10px"
307pub fn parse_border_spacing<'a>(
308    input: &'a str,
309) -> Result<LayoutBorderSpacing, LayoutBorderSpacingParseError<'a>> {
310    use crate::props::basic::parse_pixel_value;
311
312    let parts: Vec<&str> = input.trim().split_whitespace().collect();
313
314    match parts.len() {
315        1 => {
316            // Single value: use for both horizontal and vertical
317            let value =
318                parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
319            Ok(LayoutBorderSpacing::new(value))
320        }
321        2 => {
322            // Two values: horizontal vertical
323            let horizontal =
324                parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
325            let vertical =
326                parse_pixel_value(parts[1]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
327            Ok(LayoutBorderSpacing::new_separate(horizontal, vertical))
328        }
329        _ => Err(LayoutBorderSpacingParseError::InvalidFormat),
330    }
331}
332
333/// Parse errors for caption-side property
334#[derive(Debug, Clone, PartialEq)]
335pub enum StyleCaptionSideParseError<'a> {
336    InvalidKeyword(&'a str),
337}
338
339/// Parse a caption-side value from a string
340pub fn parse_caption_side<'a>(
341    input: &'a str,
342) -> Result<StyleCaptionSide, StyleCaptionSideParseError<'a>> {
343    match input.trim() {
344        "top" => Ok(StyleCaptionSide::Top),
345        "bottom" => Ok(StyleCaptionSide::Bottom),
346        other => Err(StyleCaptionSideParseError::InvalidKeyword(other)),
347    }
348}
349
350/// Parse errors for empty-cells property
351#[derive(Debug, Clone, PartialEq)]
352pub enum StyleEmptyCellsParseError<'a> {
353    InvalidKeyword(&'a str),
354}
355
356/// Parse an empty-cells value from a string
357pub fn parse_empty_cells<'a>(
358    input: &'a str,
359) -> Result<StyleEmptyCells, StyleEmptyCellsParseError<'a>> {
360    match input.trim() {
361        "show" => Ok(StyleEmptyCells::Show),
362        "hide" => Ok(StyleEmptyCells::Hide),
363        other => Err(StyleEmptyCellsParseError::InvalidKeyword(other)),
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_parse_table_layout() {
373        assert_eq!(parse_table_layout("auto").unwrap(), LayoutTableLayout::Auto);
374        assert_eq!(
375            parse_table_layout("fixed").unwrap(),
376            LayoutTableLayout::Fixed
377        );
378        assert!(parse_table_layout("invalid").is_err());
379    }
380
381    #[test]
382    fn test_parse_border_collapse() {
383        assert_eq!(
384            parse_border_collapse("separate").unwrap(),
385            StyleBorderCollapse::Separate
386        );
387        assert_eq!(
388            parse_border_collapse("collapse").unwrap(),
389            StyleBorderCollapse::Collapse
390        );
391        assert!(parse_border_collapse("invalid").is_err());
392    }
393
394    #[test]
395    fn test_parse_border_spacing() {
396        let spacing1 = parse_border_spacing("5px").unwrap();
397        assert_eq!(spacing1.horizontal, PixelValue::const_px(5));
398        assert_eq!(spacing1.vertical, PixelValue::const_px(5));
399
400        let spacing2 = parse_border_spacing("5px 10px").unwrap();
401        assert_eq!(spacing2.horizontal, PixelValue::const_px(5));
402        assert_eq!(spacing2.vertical, PixelValue::const_px(10));
403    }
404
405    #[test]
406    fn test_parse_caption_side() {
407        assert_eq!(parse_caption_side("top").unwrap(), StyleCaptionSide::Top);
408        assert_eq!(
409            parse_caption_side("bottom").unwrap(),
410            StyleCaptionSide::Bottom
411        );
412        assert!(parse_caption_side("invalid").is_err());
413    }
414
415    #[test]
416    fn test_parse_empty_cells() {
417        assert_eq!(parse_empty_cells("show").unwrap(), StyleEmptyCells::Show);
418        assert_eq!(parse_empty_cells("hide").unwrap(), StyleEmptyCells::Hide);
419        assert!(parse_empty_cells("invalid").is_err());
420    }
421}