Skip to main content

visi_core/core/
style.rs

1use serde::{Deserialize, Serialize};
2
3/// Cell formatting style attributes (font color, background color, font styles, font family, font size).
4// No `Eq`: `font_size` is an `f64`, matching Excel's `Double`-typed
5// `Font.Size`. `PartialEq` is what the codebase actually uses.
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
7pub struct CellStyle {
8    /// Font color as Hex (e.g. "#FF0000" or "FF0000") or standard color name ("red", "blue", etc.)
9    pub font_color: Option<String>,
10    /// Background fill color as Hex or color name
11    pub bg_color: Option<String>,
12    /// Bold text style flag
13    pub bold: Option<bool>,
14    /// Italic text style flag
15    pub italic: Option<bool>,
16    /// Underline text style flag
17    pub underline: Option<bool>,
18    /// Font family name (e.g. "Arial", "Calibri", "Courier New")
19    pub font_family: Option<String>,
20    /// Font size in points (e.g. 11, 12, 14.5).
21    ///
22    /// `f64` rather than an integer because Excel's is: `Font.Size` reports
23    /// as a `Double` and a half-point size round-trips (`.Font.Size = 10.5`
24    /// reads back as `10.5`), both measured with `fuzz/vba_style_probe.py`.
25    pub font_size: Option<f64>,
26    /// Excel number-format code (e.g. `m/d/yy`, `yyyy-mm-dd`).
27    ///
28    /// This is how a date cell remembers the notation it was written in: the
29    /// value stays a plain numeric serial, exactly as in Excel, and the format
30    /// governs only how it renders. See `core::date`.
31    pub num_format: Option<String>,
32}
33
34impl CellStyle {
35    /// A style with nothing set.
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Whether no attribute is set. An empty style is stored as no style at
41    /// all rather than kept around.
42    pub fn is_empty(&self) -> bool {
43        self.font_color.is_none()
44            && self.bg_color.is_none()
45            && self.bold.is_none()
46            && self.italic.is_none()
47            && self.underline.is_none()
48            && self.font_family.is_none()
49            && self.font_size.is_none()
50            && self.num_format.is_none()
51    }
52
53    /// Overlays `other` onto this style, attribute by attribute.
54    ///
55    /// Only the attributes `other` actually sets are copied, so merging a
56    /// style that just sets `bold` leaves an existing font color alone.
57    pub fn merge(&mut self, other: &CellStyle) {
58        if other.font_color.is_some() {
59            self.font_color = other.font_color.clone();
60        }
61        if other.bg_color.is_some() {
62            self.bg_color = other.bg_color.clone();
63        }
64        if other.bold.is_some() {
65            self.bold = other.bold;
66        }
67        if other.italic.is_some() {
68            self.italic = other.italic;
69        }
70        if other.underline.is_some() {
71            self.underline = other.underline;
72        }
73        if other.font_family.is_some() {
74            self.font_family = other.font_family.clone();
75        }
76        if other.font_size.is_some() {
77            self.font_size = other.font_size;
78        }
79        if other.num_format.is_some() {
80            self.num_format = other.num_format.clone();
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_cell_style_is_empty() {
91        let style = CellStyle::new();
92        assert!(style.is_empty());
93
94        let style_bold = CellStyle {
95            bold: Some(true),
96            ..Default::default()
97        };
98        assert!(!style_bold.is_empty());
99    }
100
101    #[test]
102    fn test_cell_style_merge() {
103        let mut style1 = CellStyle {
104            font_color: Some("#FF0000".to_string()),
105            bold: Some(true),
106            ..Default::default()
107        };
108        let style2 = CellStyle {
109            bg_color: Some("#00FF00".to_string()),
110            font_color: Some("#0000FF".to_string()),
111            font_size: Some(14.0),
112            ..Default::default()
113        };
114
115        style1.merge(&style2);
116        assert_eq!(style1.font_color, Some("#0000FF".to_string()));
117        assert_eq!(style1.bg_color, Some("#00FF00".to_string()));
118        assert_eq!(style1.bold, Some(true));
119        assert_eq!(style1.font_size, Some(14.0));
120    }
121}