Skip to main content

visi_core/core/
style.rs

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