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