Skip to main content

sheetkit_core/
style.rs

1//! Style builder and runtime management.
2//!
3//! Provides high-level, ergonomic style types that map to the low-level XML
4//! stylesheet structures in `sheetkit-xml`. Styles are registered in the
5//! stylesheet with deduplication: identical style components share the same
6//! index.
7
8use sheetkit_xml::styles::{
9    Alignment, Border, BorderSide, Borders, Color, Fill, Fills, Font, Fonts, NumFmt, NumFmts,
10    PatternFill, Protection, StyleSheet, Xf,
11};
12
13use crate::error::{Error, Result};
14
15/// Maximum number of cell XFs Excel supports.
16const MAX_CELL_XFS: usize = 65430;
17
18/// First ID available for custom number formats (0-163 are reserved).
19const CUSTOM_NUM_FMT_BASE: u32 = 164;
20
21/// Built-in number format IDs.
22pub mod builtin_num_fmts {
23    /// General
24    pub const GENERAL: u32 = 0;
25    /// 0
26    pub const INTEGER: u32 = 1;
27    /// 0.00
28    pub const DECIMAL_2: u32 = 2;
29    /// #,##0
30    pub const THOUSANDS: u32 = 3;
31    /// #,##0.00
32    pub const THOUSANDS_DECIMAL: u32 = 4;
33    /// 0%
34    pub const PERCENT: u32 = 9;
35    /// 0.00%
36    pub const PERCENT_DECIMAL: u32 = 10;
37    /// 0.00E+00
38    pub const SCIENTIFIC: u32 = 11;
39    /// m/d/yyyy
40    pub const DATE_MDY: u32 = 14;
41    /// d-mmm-yy
42    pub const DATE_DMY: u32 = 15;
43    /// d-mmm
44    pub const DATE_DM: u32 = 16;
45    /// mmm-yy
46    pub const DATE_MY: u32 = 17;
47    /// h:mm AM/PM
48    pub const TIME_HM_AP: u32 = 18;
49    /// h:mm:ss AM/PM
50    pub const TIME_HMS_AP: u32 = 19;
51    /// h:mm
52    pub const TIME_HM: u32 = 20;
53    /// h:mm:ss
54    pub const TIME_HMS: u32 = 21;
55    /// m/d/yyyy h:mm
56    pub const DATETIME: u32 = 22;
57    /// @
58    pub const TEXT: u32 = 49;
59}
60
61/// User-facing style definition.
62#[derive(Debug, Clone, Default)]
63pub struct Style {
64    pub font: Option<FontStyle>,
65    pub fill: Option<FillStyle>,
66    pub border: Option<BorderStyle>,
67    pub alignment: Option<AlignmentStyle>,
68    pub num_fmt: Option<NumFmtStyle>,
69    pub protection: Option<ProtectionStyle>,
70}
71
72/// Font style definition.
73#[derive(Debug, Clone, Default)]
74pub struct FontStyle {
75    /// Font name, e.g. "Calibri", "Arial".
76    pub name: Option<String>,
77    /// Font size, e.g. 11.0.
78    pub size: Option<f64>,
79    /// Bold.
80    pub bold: bool,
81    /// Italic.
82    pub italic: bool,
83    /// Underline.
84    pub underline: bool,
85    /// Strikethrough.
86    pub strikethrough: bool,
87    /// Font color.
88    pub color: Option<StyleColor>,
89}
90
91/// Color specification.
92#[derive(Debug, Clone, PartialEq)]
93pub enum StyleColor {
94    /// ARGB hex color, e.g. "FF0000FF".
95    Rgb(String),
96    /// Theme color index.
97    Theme(u32),
98    /// Indexed color.
99    Indexed(u32),
100}
101
102/// Fill style definition.
103#[derive(Debug, Clone)]
104pub struct FillStyle {
105    /// Pattern type.
106    pub pattern: PatternType,
107    /// Foreground color.
108    pub fg_color: Option<StyleColor>,
109    /// Background color.
110    pub bg_color: Option<StyleColor>,
111    /// Gradient fill (mutually exclusive with pattern fill when gradient is set).
112    pub gradient: Option<GradientFillStyle>,
113}
114
115/// Gradient fill style definition.
116#[derive(Debug, Clone)]
117pub struct GradientFillStyle {
118    /// Gradient type: linear or path.
119    pub gradient_type: GradientType,
120    /// Rotation angle in degrees for linear gradients.
121    pub degree: Option<f64>,
122    /// Left coordinate for path gradients (0.0-1.0).
123    pub left: Option<f64>,
124    /// Right coordinate for path gradients (0.0-1.0).
125    pub right: Option<f64>,
126    /// Top coordinate for path gradients (0.0-1.0).
127    pub top: Option<f64>,
128    /// Bottom coordinate for path gradients (0.0-1.0).
129    pub bottom: Option<f64>,
130    /// Gradient stops defining the color transitions.
131    pub stops: Vec<GradientStop>,
132}
133
134/// Gradient type.
135#[derive(Debug, Clone, Copy, PartialEq)]
136pub enum GradientType {
137    Linear,
138    Path,
139}
140
141impl GradientType {
142    fn from_str(s: &str) -> Self {
143        match s {
144            "path" => GradientType::Path,
145            _ => GradientType::Linear,
146        }
147    }
148}
149
150/// A single stop in a gradient fill.
151#[derive(Debug, Clone)]
152pub struct GradientStop {
153    /// Position of this stop (0.0-1.0).
154    pub position: f64,
155    /// Color at this stop.
156    pub color: StyleColor,
157}
158
159/// Pattern fill type.
160#[derive(Debug, Clone, Copy, PartialEq)]
161pub enum PatternType {
162    None,
163    Solid,
164    Gray125,
165    DarkGray,
166    MediumGray,
167    LightGray,
168}
169
170impl PatternType {
171    fn as_str(&self) -> &str {
172        match self {
173            PatternType::None => "none",
174            PatternType::Solid => "solid",
175            PatternType::Gray125 => "gray125",
176            PatternType::DarkGray => "darkGray",
177            PatternType::MediumGray => "mediumGray",
178            PatternType::LightGray => "lightGray",
179        }
180    }
181
182    fn from_str(s: &str) -> Self {
183        match s {
184            "none" => PatternType::None,
185            "solid" => PatternType::Solid,
186            "gray125" => PatternType::Gray125,
187            "darkGray" => PatternType::DarkGray,
188            "mediumGray" => PatternType::MediumGray,
189            "lightGray" => PatternType::LightGray,
190            _ => PatternType::None,
191        }
192    }
193}
194
195/// Border style definition.
196#[derive(Debug, Clone, Default)]
197pub struct BorderStyle {
198    pub left: Option<BorderSideStyle>,
199    pub right: Option<BorderSideStyle>,
200    pub top: Option<BorderSideStyle>,
201    pub bottom: Option<BorderSideStyle>,
202    pub diagonal: Option<BorderSideStyle>,
203}
204
205/// Border side style definition.
206#[derive(Debug, Clone)]
207pub struct BorderSideStyle {
208    pub style: BorderLineStyle,
209    pub color: Option<StyleColor>,
210}
211
212/// Border line style.
213#[derive(Debug, Clone, Copy, PartialEq)]
214pub enum BorderLineStyle {
215    Thin,
216    Medium,
217    Thick,
218    Dashed,
219    Dotted,
220    Double,
221    Hair,
222    MediumDashed,
223    DashDot,
224    MediumDashDot,
225    DashDotDot,
226    MediumDashDotDot,
227    SlantDashDot,
228}
229
230impl BorderLineStyle {
231    fn as_str(&self) -> &str {
232        match self {
233            BorderLineStyle::Thin => "thin",
234            BorderLineStyle::Medium => "medium",
235            BorderLineStyle::Thick => "thick",
236            BorderLineStyle::Dashed => "dashed",
237            BorderLineStyle::Dotted => "dotted",
238            BorderLineStyle::Double => "double",
239            BorderLineStyle::Hair => "hair",
240            BorderLineStyle::MediumDashed => "mediumDashed",
241            BorderLineStyle::DashDot => "dashDot",
242            BorderLineStyle::MediumDashDot => "mediumDashDot",
243            BorderLineStyle::DashDotDot => "dashDotDot",
244            BorderLineStyle::MediumDashDotDot => "mediumDashDotDot",
245            BorderLineStyle::SlantDashDot => "slantDashDot",
246        }
247    }
248
249    fn from_str(s: &str) -> Option<Self> {
250        match s {
251            "thin" => Some(BorderLineStyle::Thin),
252            "medium" => Some(BorderLineStyle::Medium),
253            "thick" => Some(BorderLineStyle::Thick),
254            "dashed" => Some(BorderLineStyle::Dashed),
255            "dotted" => Some(BorderLineStyle::Dotted),
256            "double" => Some(BorderLineStyle::Double),
257            "hair" => Some(BorderLineStyle::Hair),
258            "mediumDashed" => Some(BorderLineStyle::MediumDashed),
259            "dashDot" => Some(BorderLineStyle::DashDot),
260            "mediumDashDot" => Some(BorderLineStyle::MediumDashDot),
261            "dashDotDot" => Some(BorderLineStyle::DashDotDot),
262            "mediumDashDotDot" => Some(BorderLineStyle::MediumDashDotDot),
263            "slantDashDot" => Some(BorderLineStyle::SlantDashDot),
264            _ => None,
265        }
266    }
267}
268
269/// Alignment style definition.
270#[derive(Debug, Clone, Default)]
271pub struct AlignmentStyle {
272    pub horizontal: Option<HorizontalAlign>,
273    pub vertical: Option<VerticalAlign>,
274    pub wrap_text: bool,
275    pub text_rotation: Option<u32>,
276    pub indent: Option<u32>,
277    pub shrink_to_fit: bool,
278}
279
280/// Horizontal alignment.
281#[derive(Debug, Clone, Copy, PartialEq)]
282pub enum HorizontalAlign {
283    General,
284    Left,
285    Center,
286    Right,
287    Fill,
288    Justify,
289    CenterContinuous,
290    Distributed,
291}
292
293impl HorizontalAlign {
294    fn as_str(&self) -> &str {
295        match self {
296            HorizontalAlign::General => "general",
297            HorizontalAlign::Left => "left",
298            HorizontalAlign::Center => "center",
299            HorizontalAlign::Right => "right",
300            HorizontalAlign::Fill => "fill",
301            HorizontalAlign::Justify => "justify",
302            HorizontalAlign::CenterContinuous => "centerContinuous",
303            HorizontalAlign::Distributed => "distributed",
304        }
305    }
306
307    fn from_str(s: &str) -> Option<Self> {
308        match s {
309            "general" => Some(HorizontalAlign::General),
310            "left" => Some(HorizontalAlign::Left),
311            "center" => Some(HorizontalAlign::Center),
312            "right" => Some(HorizontalAlign::Right),
313            "fill" => Some(HorizontalAlign::Fill),
314            "justify" => Some(HorizontalAlign::Justify),
315            "centerContinuous" => Some(HorizontalAlign::CenterContinuous),
316            "distributed" => Some(HorizontalAlign::Distributed),
317            _ => None,
318        }
319    }
320}
321
322/// Vertical alignment.
323#[derive(Debug, Clone, Copy, PartialEq)]
324pub enum VerticalAlign {
325    Top,
326    Center,
327    Bottom,
328    Justify,
329    Distributed,
330}
331
332impl VerticalAlign {
333    fn as_str(&self) -> &str {
334        match self {
335            VerticalAlign::Top => "top",
336            VerticalAlign::Center => "center",
337            VerticalAlign::Bottom => "bottom",
338            VerticalAlign::Justify => "justify",
339            VerticalAlign::Distributed => "distributed",
340        }
341    }
342
343    fn from_str(s: &str) -> Option<Self> {
344        match s {
345            "top" => Some(VerticalAlign::Top),
346            "center" => Some(VerticalAlign::Center),
347            "bottom" => Some(VerticalAlign::Bottom),
348            "justify" => Some(VerticalAlign::Justify),
349            "distributed" => Some(VerticalAlign::Distributed),
350            _ => None,
351        }
352    }
353}
354
355/// Number format style.
356#[derive(Debug, Clone)]
357pub enum NumFmtStyle {
358    /// Built-in format ID (0-49).
359    Builtin(u32),
360    /// Custom format code string.
361    Custom(String),
362}
363
364/// Protection style definition.
365#[derive(Debug, Clone)]
366pub struct ProtectionStyle {
367    pub locked: bool,
368    pub hidden: bool,
369}
370
371/// Builder for creating Style objects with a fluent API.
372///
373/// Each setter method initializes the relevant sub-struct if it has not been
374/// set yet, then applies the value. Call `build()` to obtain the final Style.
375pub struct StyleBuilder {
376    style: Style,
377}
378
379impl StyleBuilder {
380    /// Create a new StyleBuilder with all fields set to None.
381    pub fn new() -> Self {
382        Self {
383            style: Style::default(),
384        }
385    }
386
387    // -- Font methods --
388
389    /// Set the bold flag on the font.
390    pub fn bold(mut self, bold: bool) -> Self {
391        self.style.font.get_or_insert_with(FontStyle::default).bold = bold;
392        self
393    }
394
395    /// Set the italic flag on the font.
396    pub fn italic(mut self, italic: bool) -> Self {
397        self.style
398            .font
399            .get_or_insert_with(FontStyle::default)
400            .italic = italic;
401        self
402    }
403
404    /// Set the underline flag on the font.
405    pub fn underline(mut self, underline: bool) -> Self {
406        self.style
407            .font
408            .get_or_insert_with(FontStyle::default)
409            .underline = underline;
410        self
411    }
412
413    /// Set the strikethrough flag on the font.
414    pub fn strikethrough(mut self, strikethrough: bool) -> Self {
415        self.style
416            .font
417            .get_or_insert_with(FontStyle::default)
418            .strikethrough = strikethrough;
419        self
420    }
421
422    /// Set the font name (e.g. "Arial", "Calibri").
423    pub fn font_name(mut self, name: &str) -> Self {
424        self.style.font.get_or_insert_with(FontStyle::default).name = Some(name.to_string());
425        self
426    }
427
428    /// Set the font size in points.
429    pub fn font_size(mut self, size: f64) -> Self {
430        self.style.font.get_or_insert_with(FontStyle::default).size = Some(size);
431        self
432    }
433
434    /// Set the font color using a StyleColor value.
435    pub fn font_color(mut self, color: StyleColor) -> Self {
436        self.style.font.get_or_insert_with(FontStyle::default).color = Some(color);
437        self
438    }
439
440    /// Set the font color using an ARGB hex string (e.g. "FF0000FF").
441    pub fn font_color_rgb(self, rgb: &str) -> Self {
442        self.font_color(StyleColor::Rgb(rgb.to_string()))
443    }
444
445    // -- Fill methods --
446
447    /// Set the fill pattern type.
448    pub fn fill_pattern(mut self, pattern: PatternType) -> Self {
449        self.style
450            .fill
451            .get_or_insert(FillStyle {
452                pattern: PatternType::None,
453                fg_color: None,
454                bg_color: None,
455                gradient: None,
456            })
457            .pattern = pattern;
458        self
459    }
460
461    /// Set the fill foreground color.
462    pub fn fill_fg_color(mut self, color: StyleColor) -> Self {
463        self.style
464            .fill
465            .get_or_insert(FillStyle {
466                pattern: PatternType::None,
467                fg_color: None,
468                bg_color: None,
469                gradient: None,
470            })
471            .fg_color = Some(color);
472        self
473    }
474
475    /// Set the fill foreground color using an ARGB hex string.
476    pub fn fill_fg_color_rgb(self, rgb: &str) -> Self {
477        self.fill_fg_color(StyleColor::Rgb(rgb.to_string()))
478    }
479
480    /// Set the fill background color.
481    pub fn fill_bg_color(mut self, color: StyleColor) -> Self {
482        self.style
483            .fill
484            .get_or_insert(FillStyle {
485                pattern: PatternType::None,
486                fg_color: None,
487                bg_color: None,
488                gradient: None,
489            })
490            .bg_color = Some(color);
491        self
492    }
493
494    /// Convenience method: set a solid fill with the given ARGB foreground color.
495    pub fn solid_fill(mut self, rgb: &str) -> Self {
496        self.style.fill = Some(FillStyle {
497            pattern: PatternType::Solid,
498            fg_color: Some(StyleColor::Rgb(rgb.to_string())),
499            bg_color: self.style.fill.and_then(|f| f.bg_color),
500            gradient: None,
501        });
502        self
503    }
504
505    // -- Border methods --
506
507    /// Set the left border style and color.
508    pub fn border_left(mut self, style: BorderLineStyle, color: StyleColor) -> Self {
509        self.style
510            .border
511            .get_or_insert_with(BorderStyle::default)
512            .left = Some(BorderSideStyle {
513            style,
514            color: Some(color),
515        });
516        self
517    }
518
519    /// Set the right border style and color.
520    pub fn border_right(mut self, style: BorderLineStyle, color: StyleColor) -> Self {
521        self.style
522            .border
523            .get_or_insert_with(BorderStyle::default)
524            .right = Some(BorderSideStyle {
525            style,
526            color: Some(color),
527        });
528        self
529    }
530
531    /// Set the top border style and color.
532    pub fn border_top(mut self, style: BorderLineStyle, color: StyleColor) -> Self {
533        self.style
534            .border
535            .get_or_insert_with(BorderStyle::default)
536            .top = Some(BorderSideStyle {
537            style,
538            color: Some(color),
539        });
540        self
541    }
542
543    /// Set the bottom border style and color.
544    pub fn border_bottom(mut self, style: BorderLineStyle, color: StyleColor) -> Self {
545        self.style
546            .border
547            .get_or_insert_with(BorderStyle::default)
548            .bottom = Some(BorderSideStyle {
549            style,
550            color: Some(color),
551        });
552        self
553    }
554
555    /// Set all four border sides (left, right, top, bottom) to the same style and color.
556    pub fn border_all(mut self, style: BorderLineStyle, color: StyleColor) -> Self {
557        let side = || BorderSideStyle {
558            style,
559            color: Some(color.clone()),
560        };
561        let border = self.style.border.get_or_insert_with(BorderStyle::default);
562        border.left = Some(side());
563        border.right = Some(side());
564        border.top = Some(side());
565        border.bottom = Some(side());
566        self
567    }
568
569    // -- Alignment methods --
570
571    /// Set horizontal alignment.
572    pub fn horizontal_align(mut self, align: HorizontalAlign) -> Self {
573        self.style
574            .alignment
575            .get_or_insert_with(AlignmentStyle::default)
576            .horizontal = Some(align);
577        self
578    }
579
580    /// Set vertical alignment.
581    pub fn vertical_align(mut self, align: VerticalAlign) -> Self {
582        self.style
583            .alignment
584            .get_or_insert_with(AlignmentStyle::default)
585            .vertical = Some(align);
586        self
587    }
588
589    /// Set the wrap text flag.
590    pub fn wrap_text(mut self, wrap: bool) -> Self {
591        self.style
592            .alignment
593            .get_or_insert_with(AlignmentStyle::default)
594            .wrap_text = wrap;
595        self
596    }
597
598    /// Set text rotation in degrees.
599    pub fn text_rotation(mut self, degrees: u32) -> Self {
600        self.style
601            .alignment
602            .get_or_insert_with(AlignmentStyle::default)
603            .text_rotation = Some(degrees);
604        self
605    }
606
607    /// Set the indent level.
608    pub fn indent(mut self, indent: u32) -> Self {
609        self.style
610            .alignment
611            .get_or_insert_with(AlignmentStyle::default)
612            .indent = Some(indent);
613        self
614    }
615
616    /// Set the shrink to fit flag.
617    pub fn shrink_to_fit(mut self, shrink: bool) -> Self {
618        self.style
619            .alignment
620            .get_or_insert_with(AlignmentStyle::default)
621            .shrink_to_fit = shrink;
622        self
623    }
624
625    // -- Number format methods --
626
627    /// Set a built-in number format by ID (see `builtin_num_fmts` constants).
628    pub fn num_format_builtin(mut self, id: u32) -> Self {
629        self.style.num_fmt = Some(NumFmtStyle::Builtin(id));
630        self
631    }
632
633    /// Set a custom number format string (e.g. "#,##0.00").
634    pub fn num_format_custom(mut self, format: &str) -> Self {
635        self.style.num_fmt = Some(NumFmtStyle::Custom(format.to_string()));
636        self
637    }
638
639    // -- Protection methods --
640
641    /// Set the locked flag for cell protection.
642    pub fn locked(mut self, locked: bool) -> Self {
643        self.style
644            .protection
645            .get_or_insert(ProtectionStyle {
646                locked: true,
647                hidden: false,
648            })
649            .locked = locked;
650        self
651    }
652
653    /// Set the hidden flag for cell protection.
654    pub fn hidden(mut self, hidden: bool) -> Self {
655        self.style
656            .protection
657            .get_or_insert(ProtectionStyle {
658                locked: true,
659                hidden: false,
660            })
661            .hidden = hidden;
662        self
663    }
664
665    // -- Build --
666
667    /// Consume the builder and return the constructed Style.
668    pub fn build(self) -> Style {
669        self.style
670    }
671}
672
673impl Default for StyleBuilder {
674    fn default() -> Self {
675        Self::new()
676    }
677}
678
679/// Convert a `StyleColor` to the XML `Color` struct.
680fn style_color_to_xml(color: &StyleColor) -> Color {
681    match color {
682        StyleColor::Rgb(rgb) => Color {
683            auto: None,
684            indexed: None,
685            rgb: Some(rgb.clone()),
686            theme: None,
687            tint: None,
688        },
689        StyleColor::Theme(t) => Color {
690            auto: None,
691            indexed: None,
692            rgb: None,
693            theme: Some(*t),
694            tint: None,
695        },
696        StyleColor::Indexed(i) => Color {
697            auto: None,
698            indexed: Some(*i),
699            rgb: None,
700            theme: None,
701            tint: None,
702        },
703    }
704}
705
706/// Convert an XML `Color` back to a `StyleColor`.
707fn xml_color_to_style(color: &Color) -> Option<StyleColor> {
708    if let Some(ref rgb) = color.rgb {
709        Some(StyleColor::Rgb(rgb.clone()))
710    } else if let Some(theme) = color.theme {
711        Some(StyleColor::Theme(theme))
712    } else {
713        color.indexed.map(StyleColor::Indexed)
714    }
715}
716
717/// Convert a `FontStyle` to the XML `Font` struct.
718fn font_style_to_xml(font: &FontStyle) -> Font {
719    use sheetkit_xml::styles::{BoolVal, FontName, FontSize, Underline};
720
721    Font {
722        b: if font.bold {
723            Some(BoolVal { val: None })
724        } else {
725            None
726        },
727        i: if font.italic {
728            Some(BoolVal { val: None })
729        } else {
730            None
731        },
732        strike: if font.strikethrough {
733            Some(BoolVal { val: None })
734        } else {
735            None
736        },
737        u: if font.underline {
738            Some(Underline { val: None })
739        } else {
740            None
741        },
742        sz: font.size.map(|val| FontSize { val }),
743        color: font.color.as_ref().map(style_color_to_xml),
744        name: font.name.as_ref().map(|val| FontName { val: val.clone() }),
745        family: None,
746        scheme: None,
747    }
748}
749
750/// Convert an XML `Font` to a `FontStyle`.
751fn xml_font_to_style(font: &Font) -> FontStyle {
752    FontStyle {
753        name: font.name.as_ref().map(|n| n.val.clone()),
754        size: font.sz.as_ref().map(|s| s.val),
755        bold: font.b.is_some(),
756        italic: font.i.is_some(),
757        underline: font.u.is_some(),
758        strikethrough: font.strike.is_some(),
759        color: font.color.as_ref().and_then(xml_color_to_style),
760    }
761}
762
763/// Convert a `FillStyle` to the XML `Fill` struct.
764fn fill_style_to_xml(fill: &FillStyle) -> Fill {
765    if let Some(ref grad) = fill.gradient {
766        return Fill {
767            pattern_fill: None,
768            gradient_fill: Some(gradient_style_to_xml(grad)),
769        };
770    }
771    Fill {
772        pattern_fill: Some(PatternFill {
773            pattern_type: Some(fill.pattern.as_str().to_string()),
774            fg_color: fill.fg_color.as_ref().map(style_color_to_xml),
775            bg_color: fill.bg_color.as_ref().map(style_color_to_xml),
776        }),
777        gradient_fill: None,
778    }
779}
780
781/// Convert a `GradientFillStyle` to the XML `GradientFill` struct.
782fn gradient_style_to_xml(grad: &GradientFillStyle) -> sheetkit_xml::styles::GradientFill {
783    sheetkit_xml::styles::GradientFill {
784        gradient_type: match grad.gradient_type {
785            GradientType::Linear => None,
786            GradientType::Path => Some("path".to_string()),
787        },
788        degree: grad.degree,
789        left: grad.left,
790        right: grad.right,
791        top: grad.top,
792        bottom: grad.bottom,
793        stops: grad
794            .stops
795            .iter()
796            .map(|s| sheetkit_xml::styles::GradientStop {
797                position: s.position,
798                color: style_color_to_xml(&s.color),
799            })
800            .collect(),
801    }
802}
803
804/// Convert an XML `Fill` to a `FillStyle`.
805fn xml_fill_to_style(fill: &Fill) -> Option<FillStyle> {
806    if let Some(ref gf) = fill.gradient_fill {
807        let gradient_type = gf
808            .gradient_type
809            .as_ref()
810            .map(|s| GradientType::from_str(s))
811            .unwrap_or(GradientType::Linear);
812        let stops: Vec<GradientStop> = gf
813            .stops
814            .iter()
815            .filter_map(|s| {
816                xml_color_to_style(&s.color).map(|c| GradientStop {
817                    position: s.position,
818                    color: c,
819                })
820            })
821            .collect();
822        return Some(FillStyle {
823            pattern: PatternType::None,
824            fg_color: None,
825            bg_color: None,
826            gradient: Some(GradientFillStyle {
827                gradient_type,
828                degree: gf.degree,
829                left: gf.left,
830                right: gf.right,
831                top: gf.top,
832                bottom: gf.bottom,
833                stops,
834            }),
835        });
836    }
837    let pf = fill.pattern_fill.as_ref()?;
838    let pattern = pf
839        .pattern_type
840        .as_ref()
841        .map(|s| PatternType::from_str(s))
842        .unwrap_or(PatternType::None);
843    Some(FillStyle {
844        pattern,
845        fg_color: pf.fg_color.as_ref().and_then(xml_color_to_style),
846        bg_color: pf.bg_color.as_ref().and_then(xml_color_to_style),
847        gradient: None,
848    })
849}
850
851/// Convert a `BorderSideStyle` to the XML `BorderSide` struct.
852fn border_side_to_xml(side: &BorderSideStyle) -> BorderSide {
853    BorderSide {
854        style: Some(side.style.as_str().to_string()),
855        color: side.color.as_ref().map(style_color_to_xml),
856    }
857}
858
859/// Convert an XML `BorderSide` to a `BorderSideStyle`.
860fn xml_border_side_to_style(side: &BorderSide) -> Option<BorderSideStyle> {
861    let style_str = side.style.as_ref()?;
862    let style = BorderLineStyle::from_str(style_str)?;
863    Some(BorderSideStyle {
864        style,
865        color: side.color.as_ref().and_then(xml_color_to_style),
866    })
867}
868
869/// Convert a `BorderStyle` to the XML `Border` struct.
870fn border_style_to_xml(border: &BorderStyle) -> Border {
871    Border {
872        diagonal_up: None,
873        diagonal_down: None,
874        left: border.left.as_ref().map(border_side_to_xml),
875        right: border.right.as_ref().map(border_side_to_xml),
876        top: border.top.as_ref().map(border_side_to_xml),
877        bottom: border.bottom.as_ref().map(border_side_to_xml),
878        diagonal: border.diagonal.as_ref().map(border_side_to_xml),
879    }
880}
881
882/// Convert an XML `Border` to a `BorderStyle`.
883fn xml_border_to_style(border: &Border) -> BorderStyle {
884    BorderStyle {
885        left: border.left.as_ref().and_then(xml_border_side_to_style),
886        right: border.right.as_ref().and_then(xml_border_side_to_style),
887        top: border.top.as_ref().and_then(xml_border_side_to_style),
888        bottom: border.bottom.as_ref().and_then(xml_border_side_to_style),
889        diagonal: border.diagonal.as_ref().and_then(xml_border_side_to_style),
890    }
891}
892
893/// Convert an `AlignmentStyle` to the XML `Alignment` struct.
894fn alignment_style_to_xml(align: &AlignmentStyle) -> Alignment {
895    Alignment {
896        horizontal: align.horizontal.map(|h| h.as_str().to_string()),
897        vertical: align.vertical.map(|v| v.as_str().to_string()),
898        wrap_text: if align.wrap_text { Some(true) } else { None },
899        text_rotation: align.text_rotation,
900        indent: align.indent,
901        shrink_to_fit: if align.shrink_to_fit {
902            Some(true)
903        } else {
904            None
905        },
906    }
907}
908
909/// Convert an XML `Alignment` to an `AlignmentStyle`.
910fn xml_alignment_to_style(align: &Alignment) -> AlignmentStyle {
911    AlignmentStyle {
912        horizontal: align
913            .horizontal
914            .as_ref()
915            .and_then(|s| HorizontalAlign::from_str(s)),
916        vertical: align
917            .vertical
918            .as_ref()
919            .and_then(|s| VerticalAlign::from_str(s)),
920        wrap_text: align.wrap_text.unwrap_or(false),
921        text_rotation: align.text_rotation,
922        indent: align.indent,
923        shrink_to_fit: align.shrink_to_fit.unwrap_or(false),
924    }
925}
926
927/// Convert a `ProtectionStyle` to the XML `Protection` struct.
928fn protection_style_to_xml(prot: &ProtectionStyle) -> Protection {
929    Protection {
930        locked: Some(prot.locked),
931        hidden: Some(prot.hidden),
932    }
933}
934
935/// Convert an XML `Protection` to a `ProtectionStyle`.
936fn xml_protection_to_style(prot: &Protection) -> ProtectionStyle {
937    ProtectionStyle {
938        locked: prot.locked.unwrap_or(true), // Excel default: locked=true
939        hidden: prot.hidden.unwrap_or(false),
940    }
941}
942
943/// Check if two XML `Font` values are equivalent for deduplication purposes.
944fn fonts_equal(a: &Font, b: &Font) -> bool {
945    a.b.is_some() == b.b.is_some()
946        && a.i.is_some() == b.i.is_some()
947        && a.strike.is_some() == b.strike.is_some()
948        && a.u.is_some() == b.u.is_some()
949        && a.sz == b.sz
950        && a.color == b.color
951        && a.name == b.name
952}
953
954/// Check if two XML `Fill` values are equivalent for deduplication purposes.
955fn fills_equal(a: &Fill, b: &Fill) -> bool {
956    a.pattern_fill == b.pattern_fill && a.gradient_fill == b.gradient_fill
957}
958
959/// Check if two XML `Border` values are equivalent for deduplication purposes.
960fn borders_equal(a: &Border, b: &Border) -> bool {
961    a.left == b.left
962        && a.right == b.right
963        && a.top == b.top
964        && a.bottom == b.bottom
965        && a.diagonal == b.diagonal
966}
967
968/// Check if two XML `Xf` values are equivalent for deduplication purposes.
969fn xfs_equal(a: &Xf, b: &Xf) -> bool {
970    a.num_fmt_id == b.num_fmt_id
971        && a.font_id == b.font_id
972        && a.fill_id == b.fill_id
973        && a.border_id == b.border_id
974        && a.alignment == b.alignment
975        && a.protection == b.protection
976}
977
978/// Convert a `FontStyle` to the XML `Font` struct, find or add it in the fonts list.
979/// Returns the 0-based font index.
980fn add_or_find_font(fonts: &mut Fonts, font: &FontStyle) -> u32 {
981    let xml_font = font_style_to_xml(font);
982
983    for (i, existing) in fonts.fonts.iter().enumerate() {
984        if fonts_equal(existing, &xml_font) {
985            return i as u32;
986        }
987    }
988
989    let id = fonts.fonts.len() as u32;
990    fonts.fonts.push(xml_font);
991    fonts.count = Some(fonts.fonts.len() as u32);
992    id
993}
994
995/// Convert a `FillStyle` to the XML `Fill` struct, find or add it.
996/// Returns the 0-based fill index.
997fn add_or_find_fill(fills: &mut Fills, fill: &FillStyle) -> u32 {
998    let xml_fill = fill_style_to_xml(fill);
999
1000    for (i, existing) in fills.fills.iter().enumerate() {
1001        if fills_equal(existing, &xml_fill) {
1002            return i as u32;
1003        }
1004    }
1005
1006    let id = fills.fills.len() as u32;
1007    fills.fills.push(xml_fill);
1008    fills.count = Some(fills.fills.len() as u32);
1009    id
1010}
1011
1012/// Convert a `BorderStyle` to the XML `Border` struct, find or add it.
1013/// Returns the 0-based border index.
1014fn add_or_find_border(borders: &mut Borders, border: &BorderStyle) -> u32 {
1015    let xml_border = border_style_to_xml(border);
1016
1017    for (i, existing) in borders.borders.iter().enumerate() {
1018        if borders_equal(existing, &xml_border) {
1019            return i as u32;
1020        }
1021    }
1022
1023    let id = borders.borders.len() as u32;
1024    borders.borders.push(xml_border);
1025    borders.count = Some(borders.borders.len() as u32);
1026    id
1027}
1028
1029/// Register a custom number format, return its ID (starting from 164).
1030/// If an identical format code already exists, returns the existing ID.
1031fn add_or_find_num_fmt(stylesheet: &mut StyleSheet, fmt: &str) -> u32 {
1032    let num_fmts = stylesheet.num_fmts.get_or_insert_with(|| NumFmts {
1033        count: Some(0),
1034        num_fmts: Vec::new(),
1035    });
1036
1037    for nf in &num_fmts.num_fmts {
1038        if nf.format_code == fmt {
1039            return nf.num_fmt_id;
1040        }
1041    }
1042
1043    let next_id = num_fmts
1044        .num_fmts
1045        .iter()
1046        .map(|nf| nf.num_fmt_id)
1047        .max()
1048        .map(|max_id| max_id + 1)
1049        .unwrap_or(CUSTOM_NUM_FMT_BASE);
1050
1051    let next_id = next_id.max(CUSTOM_NUM_FMT_BASE);
1052
1053    num_fmts.num_fmts.push(NumFmt {
1054        num_fmt_id: next_id,
1055        format_code: fmt.to_string(),
1056    });
1057    num_fmts.count = Some(num_fmts.num_fmts.len() as u32);
1058
1059    next_id
1060}
1061
1062/// Convert a high-level `Style` to XML components and register in the stylesheet.
1063/// Returns the style ID (index into cellXfs).
1064pub fn add_style(stylesheet: &mut StyleSheet, style: &Style) -> Result<u32> {
1065    if stylesheet.cell_xfs.xfs.len() >= MAX_CELL_XFS {
1066        return Err(Error::CellStylesExceeded { max: MAX_CELL_XFS });
1067    }
1068
1069    let font_id = match &style.font {
1070        Some(font) => add_or_find_font(&mut stylesheet.fonts, font),
1071        None => 0, // default font
1072    };
1073
1074    let fill_id = match &style.fill {
1075        Some(fill) => add_or_find_fill(&mut stylesheet.fills, fill),
1076        None => 0, // default fill (none)
1077    };
1078
1079    let border_id = match &style.border {
1080        Some(border) => add_or_find_border(&mut stylesheet.borders, border),
1081        None => 0, // default border (empty)
1082    };
1083
1084    let num_fmt_id = match &style.num_fmt {
1085        Some(NumFmtStyle::Builtin(id)) => *id,
1086        Some(NumFmtStyle::Custom(code)) => add_or_find_num_fmt(stylesheet, code),
1087        None => 0, // General
1088    };
1089
1090    let alignment = style.alignment.as_ref().map(alignment_style_to_xml);
1091    let protection = style.protection.as_ref().map(protection_style_to_xml);
1092
1093    let xf = Xf {
1094        num_fmt_id: Some(num_fmt_id),
1095        font_id: Some(font_id),
1096        fill_id: Some(fill_id),
1097        border_id: Some(border_id),
1098        xf_id: Some(0),
1099        apply_number_format: if num_fmt_id != 0 { Some(true) } else { None },
1100        apply_font: if font_id != 0 { Some(true) } else { None },
1101        apply_fill: if fill_id != 0 { Some(true) } else { None },
1102        apply_border: if border_id != 0 { Some(true) } else { None },
1103        apply_alignment: if alignment.is_some() {
1104            Some(true)
1105        } else {
1106            None
1107        },
1108        alignment,
1109        protection,
1110    };
1111
1112    for (i, existing) in stylesheet.cell_xfs.xfs.iter().enumerate() {
1113        if xfs_equal(existing, &xf) {
1114            return Ok(i as u32);
1115        }
1116    }
1117
1118    let id = stylesheet.cell_xfs.xfs.len() as u32;
1119    stylesheet.cell_xfs.xfs.push(xf);
1120    stylesheet.cell_xfs.count = Some(stylesheet.cell_xfs.xfs.len() as u32);
1121
1122    Ok(id)
1123}
1124
1125/// Get the `Style` from a style ID (reverse lookup from XML components).
1126pub fn get_style(stylesheet: &StyleSheet, style_id: u32) -> Option<Style> {
1127    let xf = stylesheet.cell_xfs.xfs.get(style_id as usize)?;
1128
1129    let font = xf
1130        .font_id
1131        .and_then(|id| stylesheet.fonts.fonts.get(id as usize))
1132        .map(xml_font_to_style);
1133
1134    let fill = xf
1135        .fill_id
1136        .and_then(|id| stylesheet.fills.fills.get(id as usize))
1137        .and_then(xml_fill_to_style);
1138
1139    let border = xf
1140        .border_id
1141        .and_then(|id| stylesheet.borders.borders.get(id as usize))
1142        .map(xml_border_to_style);
1143
1144    let alignment = xf.alignment.as_ref().map(xml_alignment_to_style);
1145
1146    let num_fmt = xf.num_fmt_id.and_then(|id| {
1147        if id == 0 {
1148            return None;
1149        }
1150        // Check if it's a built-in format (0-163).
1151        if id < CUSTOM_NUM_FMT_BASE {
1152            Some(NumFmtStyle::Builtin(id))
1153        } else {
1154            // Look up custom format code.
1155            stylesheet
1156                .num_fmts
1157                .as_ref()
1158                .and_then(|nfs| nfs.num_fmts.iter().find(|nf| nf.num_fmt_id == id))
1159                .map(|nf| NumFmtStyle::Custom(nf.format_code.clone()))
1160        }
1161    });
1162
1163    let protection = xf.protection.as_ref().map(xml_protection_to_style);
1164
1165    Some(Style {
1166        font,
1167        fill,
1168        border,
1169        alignment,
1170        num_fmt,
1171        protection,
1172    })
1173}
1174
1175/// Precompute, for each entry in `stylesheet.cell_xfs`, whether the referenced
1176/// number format is a date/time format.
1177///
1178/// Returns a `Vec<bool>` parallel to `cell_xfs`, where `true` marks a style
1179/// that points at either a built-in date/time number format (IDs 14-22, 45-47)
1180/// or a custom number format whose format code contains date/time tokens.
1181///
1182/// Consumers like the streaming reader can index into the returned vector with
1183/// a cell's `s` attribute to cheaply decide whether a `t="n"` cell should be
1184/// promoted to [`CellValue::Date`](crate::cell::CellValue::Date) under the
1185/// [`DateInterpretation::NumFmt`](crate::workbook::open_options::DateInterpretation::NumFmt)
1186/// policy.
1187pub fn compute_style_is_date(stylesheet: &StyleSheet) -> Vec<bool> {
1188    stylesheet
1189        .cell_xfs
1190        .xfs
1191        .iter()
1192        .map(|xf| {
1193            let num_fmt_id = xf.num_fmt_id.unwrap_or(0);
1194            if crate::cell::is_date_num_fmt(num_fmt_id) {
1195                return true;
1196            }
1197            if num_fmt_id >= CUSTOM_NUM_FMT_BASE {
1198                if let Some(nfs) = stylesheet.num_fmts.as_ref() {
1199                    if let Some(nf) = nfs.num_fmts.iter().find(|nf| nf.num_fmt_id == num_fmt_id) {
1200                        return crate::cell::is_date_format_code(&nf.format_code);
1201                    }
1202                }
1203            }
1204            false
1205        })
1206        .collect()
1207}
1208
1209#[cfg(test)]
1210mod tests {
1211    use super::*;
1212
1213    /// Helper to create a fresh default stylesheet for tests.
1214    fn default_stylesheet() -> StyleSheet {
1215        StyleSheet::default()
1216    }
1217
1218    fn xf_with_num_fmt(id: u32) -> Xf {
1219        Xf {
1220            num_fmt_id: Some(id),
1221            font_id: None,
1222            fill_id: None,
1223            border_id: None,
1224            xf_id: None,
1225            apply_number_format: None,
1226            apply_font: None,
1227            apply_fill: None,
1228            apply_border: None,
1229            apply_alignment: None,
1230            alignment: None,
1231            protection: None,
1232        }
1233    }
1234
1235    #[test]
1236    fn test_compute_style_is_date_builtin() {
1237        let mut ss = default_stylesheet();
1238        // xf0 = General (0), xf1 = DATE_MDY (14), xf2 = TIME_HMS (21), xf3 = DECIMAL_2 (2)
1239        ss.cell_xfs.xfs = vec![
1240            xf_with_num_fmt(0),
1241            xf_with_num_fmt(14),
1242            xf_with_num_fmt(21),
1243            xf_with_num_fmt(2),
1244        ];
1245        ss.cell_xfs.count = Some(4);
1246        let flags = compute_style_is_date(&ss);
1247        assert_eq!(flags, vec![false, true, true, false]);
1248    }
1249
1250    #[test]
1251    fn test_compute_style_is_date_custom() {
1252        let mut ss = default_stylesheet();
1253        ss.num_fmts = Some(NumFmts {
1254            count: Some(2),
1255            num_fmts: vec![
1256                NumFmt {
1257                    num_fmt_id: 164,
1258                    format_code: "yyyy-mm-dd hh:mm".to_string(),
1259                },
1260                NumFmt {
1261                    num_fmt_id: 165,
1262                    format_code: "#,##0.00".to_string(),
1263                },
1264            ],
1265        });
1266        ss.cell_xfs.xfs = vec![xf_with_num_fmt(164), xf_with_num_fmt(165)];
1267        ss.cell_xfs.count = Some(2);
1268        let flags = compute_style_is_date(&ss);
1269        assert_eq!(flags, vec![true, false]);
1270    }
1271
1272    #[test]
1273    fn test_compute_style_is_date_missing_custom_format() {
1274        let mut ss = default_stylesheet();
1275        // Reference a custom numFmt ID with no matching entry.
1276        ss.cell_xfs.xfs = vec![xf_with_num_fmt(200)];
1277        ss.cell_xfs.count = Some(1);
1278        let flags = compute_style_is_date(&ss);
1279        assert_eq!(flags, vec![false]);
1280    }
1281
1282    #[test]
1283    fn test_compute_style_is_date_default_stylesheet() {
1284        // The default stylesheet ships with a single cellXf pointing at
1285        // numFmtId=0 (General), which is not a date format.
1286        let ss = default_stylesheet();
1287        let flags = compute_style_is_date(&ss);
1288        assert!(!flags.is_empty());
1289        assert!(flags.iter().all(|f| !*f));
1290    }
1291
1292    #[test]
1293    fn test_add_bold_font_style() {
1294        let mut ss = default_stylesheet();
1295        let style = Style {
1296            font: Some(FontStyle {
1297                bold: true,
1298                ..FontStyle::default()
1299            }),
1300            ..Style::default()
1301        };
1302
1303        let id = add_style(&mut ss, &style).unwrap();
1304        // The default stylesheet has one Xf at index 0, so the new one should be at 1.
1305        assert_eq!(id, 1);
1306        // Font list should have grown.
1307        assert_eq!(ss.fonts.fonts.len(), 2);
1308        assert!(ss.fonts.fonts[1].b.is_some());
1309    }
1310
1311    #[test]
1312    fn test_add_same_style_twice_deduplication() {
1313        let mut ss = default_stylesheet();
1314        let style = Style {
1315            font: Some(FontStyle {
1316                bold: true,
1317                ..FontStyle::default()
1318            }),
1319            ..Style::default()
1320        };
1321
1322        let id1 = add_style(&mut ss, &style).unwrap();
1323        let id2 = add_style(&mut ss, &style).unwrap();
1324        assert_eq!(id1, id2, "same style should return the same ID");
1325        // Only 2 fonts (default + the bold one).
1326        assert_eq!(ss.fonts.fonts.len(), 2);
1327        // Only 2 Xfs (default + the bold one).
1328        assert_eq!(ss.cell_xfs.xfs.len(), 2);
1329    }
1330
1331    #[test]
1332    fn test_add_different_styles_different_ids() {
1333        let mut ss = default_stylesheet();
1334
1335        let bold_style = Style {
1336            font: Some(FontStyle {
1337                bold: true,
1338                ..FontStyle::default()
1339            }),
1340            ..Style::default()
1341        };
1342        let italic_style = Style {
1343            font: Some(FontStyle {
1344                italic: true,
1345                ..FontStyle::default()
1346            }),
1347            ..Style::default()
1348        };
1349
1350        let id1 = add_style(&mut ss, &bold_style).unwrap();
1351        let id2 = add_style(&mut ss, &italic_style).unwrap();
1352        assert_ne!(id1, id2);
1353    }
1354
1355    #[test]
1356    fn test_font_italic() {
1357        let mut ss = default_stylesheet();
1358        let style = Style {
1359            font: Some(FontStyle {
1360                italic: true,
1361                ..FontStyle::default()
1362            }),
1363            ..Style::default()
1364        };
1365
1366        let id = add_style(&mut ss, &style).unwrap();
1367        assert!(id > 0);
1368        let font_id = ss.cell_xfs.xfs[id as usize].font_id.unwrap();
1369        assert!(ss.fonts.fonts[font_id as usize].i.is_some());
1370    }
1371
1372    #[test]
1373    fn test_font_underline() {
1374        let mut ss = default_stylesheet();
1375        let style = Style {
1376            font: Some(FontStyle {
1377                underline: true,
1378                ..FontStyle::default()
1379            }),
1380            ..Style::default()
1381        };
1382
1383        let id = add_style(&mut ss, &style).unwrap();
1384        assert!(id > 0);
1385        let font_id = ss.cell_xfs.xfs[id as usize].font_id.unwrap();
1386        assert!(ss.fonts.fonts[font_id as usize].u.is_some());
1387    }
1388
1389    #[test]
1390    fn test_font_strikethrough() {
1391        let mut ss = default_stylesheet();
1392        let style = Style {
1393            font: Some(FontStyle {
1394                strikethrough: true,
1395                ..FontStyle::default()
1396            }),
1397            ..Style::default()
1398        };
1399
1400        let id = add_style(&mut ss, &style).unwrap();
1401        let font_id = ss.cell_xfs.xfs[id as usize].font_id.unwrap();
1402        assert!(ss.fonts.fonts[font_id as usize].strike.is_some());
1403    }
1404
1405    #[test]
1406    fn test_font_custom_name_and_size() {
1407        let mut ss = default_stylesheet();
1408        let style = Style {
1409            font: Some(FontStyle {
1410                name: Some("Arial".to_string()),
1411                size: Some(14.0),
1412                ..FontStyle::default()
1413            }),
1414            ..Style::default()
1415        };
1416
1417        let id = add_style(&mut ss, &style).unwrap();
1418        let font_id = ss.cell_xfs.xfs[id as usize].font_id.unwrap();
1419        let xml_font = &ss.fonts.fonts[font_id as usize];
1420        assert_eq!(xml_font.name.as_ref().unwrap().val, "Arial");
1421        assert_eq!(xml_font.sz.as_ref().unwrap().val, 14.0);
1422    }
1423
1424    #[test]
1425    fn test_font_with_rgb_color() {
1426        let mut ss = default_stylesheet();
1427        let style = Style {
1428            font: Some(FontStyle {
1429                color: Some(StyleColor::Rgb("FFFF0000".to_string())),
1430                ..FontStyle::default()
1431            }),
1432            ..Style::default()
1433        };
1434
1435        let id = add_style(&mut ss, &style).unwrap();
1436        let font_id = ss.cell_xfs.xfs[id as usize].font_id.unwrap();
1437        let xml_font = &ss.fonts.fonts[font_id as usize];
1438        assert_eq!(
1439            xml_font.color.as_ref().unwrap().rgb,
1440            Some("FFFF0000".to_string())
1441        );
1442    }
1443
1444    #[test]
1445    fn test_fill_solid_color() {
1446        let mut ss = default_stylesheet();
1447        let style = Style {
1448            fill: Some(FillStyle {
1449                pattern: PatternType::Solid,
1450                fg_color: Some(StyleColor::Rgb("FFFFFF00".to_string())),
1451                bg_color: None,
1452                gradient: None,
1453            }),
1454            ..Style::default()
1455        };
1456
1457        let id = add_style(&mut ss, &style).unwrap();
1458        let fill_id = ss.cell_xfs.xfs[id as usize].fill_id.unwrap();
1459        let xml_fill = &ss.fills.fills[fill_id as usize];
1460        let pf = xml_fill.pattern_fill.as_ref().unwrap();
1461        assert_eq!(pf.pattern_type, Some("solid".to_string()));
1462        assert_eq!(
1463            pf.fg_color.as_ref().unwrap().rgb,
1464            Some("FFFFFF00".to_string())
1465        );
1466    }
1467
1468    #[test]
1469    fn test_fill_pattern() {
1470        let mut ss = default_stylesheet();
1471        let style = Style {
1472            fill: Some(FillStyle {
1473                pattern: PatternType::LightGray,
1474                fg_color: None,
1475                bg_color: None,
1476                gradient: None,
1477            }),
1478            ..Style::default()
1479        };
1480
1481        let id = add_style(&mut ss, &style).unwrap();
1482        let fill_id = ss.cell_xfs.xfs[id as usize].fill_id.unwrap();
1483        let xml_fill = &ss.fills.fills[fill_id as usize];
1484        let pf = xml_fill.pattern_fill.as_ref().unwrap();
1485        assert_eq!(pf.pattern_type, Some("lightGray".to_string()));
1486    }
1487
1488    #[test]
1489    fn test_fill_deduplication() {
1490        let mut ss = default_stylesheet();
1491        let style = Style {
1492            fill: Some(FillStyle {
1493                pattern: PatternType::Solid,
1494                fg_color: Some(StyleColor::Rgb("FFFF0000".to_string())),
1495                bg_color: None,
1496                gradient: None,
1497            }),
1498            ..Style::default()
1499        };
1500
1501        let id1 = add_style(&mut ss, &style).unwrap();
1502        let id2 = add_style(&mut ss, &style).unwrap();
1503        assert_eq!(id1, id2);
1504        // Default has 2 fills (none + gray125), we added 1 more.
1505        assert_eq!(ss.fills.fills.len(), 3);
1506    }
1507
1508    #[test]
1509    fn test_border_thin_all_sides() {
1510        let mut ss = default_stylesheet();
1511        let style = Style {
1512            border: Some(BorderStyle {
1513                left: Some(BorderSideStyle {
1514                    style: BorderLineStyle::Thin,
1515                    color: None,
1516                }),
1517                right: Some(BorderSideStyle {
1518                    style: BorderLineStyle::Thin,
1519                    color: None,
1520                }),
1521                top: Some(BorderSideStyle {
1522                    style: BorderLineStyle::Thin,
1523                    color: None,
1524                }),
1525                bottom: Some(BorderSideStyle {
1526                    style: BorderLineStyle::Thin,
1527                    color: None,
1528                }),
1529                diagonal: None,
1530            }),
1531            ..Style::default()
1532        };
1533
1534        let id = add_style(&mut ss, &style).unwrap();
1535        let border_id = ss.cell_xfs.xfs[id as usize].border_id.unwrap();
1536        let xml_border = &ss.borders.borders[border_id as usize];
1537        assert_eq!(
1538            xml_border.left.as_ref().unwrap().style,
1539            Some("thin".to_string())
1540        );
1541        assert_eq!(
1542            xml_border.right.as_ref().unwrap().style,
1543            Some("thin".to_string())
1544        );
1545        assert_eq!(
1546            xml_border.top.as_ref().unwrap().style,
1547            Some("thin".to_string())
1548        );
1549        assert_eq!(
1550            xml_border.bottom.as_ref().unwrap().style,
1551            Some("thin".to_string())
1552        );
1553    }
1554
1555    #[test]
1556    fn test_border_medium() {
1557        let mut ss = default_stylesheet();
1558        let style = Style {
1559            border: Some(BorderStyle {
1560                left: Some(BorderSideStyle {
1561                    style: BorderLineStyle::Medium,
1562                    color: Some(StyleColor::Rgb("FF000000".to_string())),
1563                }),
1564                right: None,
1565                top: None,
1566                bottom: None,
1567                diagonal: None,
1568            }),
1569            ..Style::default()
1570        };
1571
1572        let id = add_style(&mut ss, &style).unwrap();
1573        let border_id = ss.cell_xfs.xfs[id as usize].border_id.unwrap();
1574        let xml_border = &ss.borders.borders[border_id as usize];
1575        let left = xml_border.left.as_ref().unwrap();
1576        assert_eq!(left.style, Some("medium".to_string()));
1577        assert_eq!(
1578            left.color.as_ref().unwrap().rgb,
1579            Some("FF000000".to_string())
1580        );
1581    }
1582
1583    #[test]
1584    fn test_border_thick() {
1585        let mut ss = default_stylesheet();
1586        let style = Style {
1587            border: Some(BorderStyle {
1588                bottom: Some(BorderSideStyle {
1589                    style: BorderLineStyle::Thick,
1590                    color: None,
1591                }),
1592                ..BorderStyle::default()
1593            }),
1594            ..Style::default()
1595        };
1596
1597        let id = add_style(&mut ss, &style).unwrap();
1598        let border_id = ss.cell_xfs.xfs[id as usize].border_id.unwrap();
1599        let xml_border = &ss.borders.borders[border_id as usize];
1600        assert_eq!(
1601            xml_border.bottom.as_ref().unwrap().style,
1602            Some("thick".to_string())
1603        );
1604    }
1605
1606    #[test]
1607    fn test_num_fmt_builtin() {
1608        let mut ss = default_stylesheet();
1609        let style = Style {
1610            num_fmt: Some(NumFmtStyle::Builtin(builtin_num_fmts::PERCENT)),
1611            ..Style::default()
1612        };
1613
1614        let id = add_style(&mut ss, &style).unwrap();
1615        let xf = &ss.cell_xfs.xfs[id as usize];
1616        assert_eq!(xf.num_fmt_id, Some(builtin_num_fmts::PERCENT));
1617        assert_eq!(xf.apply_number_format, Some(true));
1618    }
1619
1620    #[test]
1621    fn test_num_fmt_custom() {
1622        let mut ss = default_stylesheet();
1623        let style = Style {
1624            num_fmt: Some(NumFmtStyle::Custom("#,##0.000".to_string())),
1625            ..Style::default()
1626        };
1627
1628        let id = add_style(&mut ss, &style).unwrap();
1629        let xf = &ss.cell_xfs.xfs[id as usize];
1630        let fmt_id = xf.num_fmt_id.unwrap();
1631        assert!(fmt_id >= CUSTOM_NUM_FMT_BASE);
1632
1633        // Verify the format code was stored.
1634        let num_fmts = ss.num_fmts.as_ref().unwrap();
1635        let nf = num_fmts
1636            .num_fmts
1637            .iter()
1638            .find(|nf| nf.num_fmt_id == fmt_id)
1639            .unwrap();
1640        assert_eq!(nf.format_code, "#,##0.000");
1641    }
1642
1643    #[test]
1644    fn test_num_fmt_custom_deduplication() {
1645        let mut ss = default_stylesheet();
1646        let style = Style {
1647            num_fmt: Some(NumFmtStyle::Custom("0.0%".to_string())),
1648            ..Style::default()
1649        };
1650
1651        let id1 = add_style(&mut ss, &style).unwrap();
1652        let id2 = add_style(&mut ss, &style).unwrap();
1653        assert_eq!(id1, id2);
1654
1655        // Only one custom format should exist.
1656        let num_fmts = ss.num_fmts.as_ref().unwrap();
1657        assert_eq!(num_fmts.num_fmts.len(), 1);
1658    }
1659
1660    #[test]
1661    fn test_alignment_horizontal_center() {
1662        let mut ss = default_stylesheet();
1663        let style = Style {
1664            alignment: Some(AlignmentStyle {
1665                horizontal: Some(HorizontalAlign::Center),
1666                ..AlignmentStyle::default()
1667            }),
1668            ..Style::default()
1669        };
1670
1671        let id = add_style(&mut ss, &style).unwrap();
1672        let xf = &ss.cell_xfs.xfs[id as usize];
1673        assert_eq!(xf.apply_alignment, Some(true));
1674        let align = xf.alignment.as_ref().unwrap();
1675        assert_eq!(align.horizontal, Some("center".to_string()));
1676    }
1677
1678    #[test]
1679    fn test_alignment_vertical_top() {
1680        let mut ss = default_stylesheet();
1681        let style = Style {
1682            alignment: Some(AlignmentStyle {
1683                vertical: Some(VerticalAlign::Top),
1684                ..AlignmentStyle::default()
1685            }),
1686            ..Style::default()
1687        };
1688
1689        let id = add_style(&mut ss, &style).unwrap();
1690        let xf = &ss.cell_xfs.xfs[id as usize];
1691        let align = xf.alignment.as_ref().unwrap();
1692        assert_eq!(align.vertical, Some("top".to_string()));
1693    }
1694
1695    #[test]
1696    fn test_alignment_wrap_text() {
1697        let mut ss = default_stylesheet();
1698        let style = Style {
1699            alignment: Some(AlignmentStyle {
1700                wrap_text: true,
1701                ..AlignmentStyle::default()
1702            }),
1703            ..Style::default()
1704        };
1705
1706        let id = add_style(&mut ss, &style).unwrap();
1707        let xf = &ss.cell_xfs.xfs[id as usize];
1708        let align = xf.alignment.as_ref().unwrap();
1709        assert_eq!(align.wrap_text, Some(true));
1710    }
1711
1712    #[test]
1713    fn test_alignment_text_rotation() {
1714        let mut ss = default_stylesheet();
1715        let style = Style {
1716            alignment: Some(AlignmentStyle {
1717                text_rotation: Some(90),
1718                ..AlignmentStyle::default()
1719            }),
1720            ..Style::default()
1721        };
1722
1723        let id = add_style(&mut ss, &style).unwrap();
1724        let xf = &ss.cell_xfs.xfs[id as usize];
1725        let align = xf.alignment.as_ref().unwrap();
1726        assert_eq!(align.text_rotation, Some(90));
1727    }
1728
1729    #[test]
1730    fn test_protection_locked() {
1731        let mut ss = default_stylesheet();
1732        let style = Style {
1733            protection: Some(ProtectionStyle {
1734                locked: true,
1735                hidden: false,
1736            }),
1737            ..Style::default()
1738        };
1739
1740        let id = add_style(&mut ss, &style).unwrap();
1741        let xf = &ss.cell_xfs.xfs[id as usize];
1742        let prot = xf.protection.as_ref().unwrap();
1743        assert_eq!(prot.locked, Some(true));
1744        assert_eq!(prot.hidden, Some(false));
1745    }
1746
1747    #[test]
1748    fn test_protection_hidden() {
1749        let mut ss = default_stylesheet();
1750        let style = Style {
1751            protection: Some(ProtectionStyle {
1752                locked: false,
1753                hidden: true,
1754            }),
1755            ..Style::default()
1756        };
1757
1758        let id = add_style(&mut ss, &style).unwrap();
1759        let xf = &ss.cell_xfs.xfs[id as usize];
1760        let prot = xf.protection.as_ref().unwrap();
1761        assert_eq!(prot.locked, Some(false));
1762        assert_eq!(prot.hidden, Some(true));
1763    }
1764
1765    #[test]
1766    fn test_combined_style_all_components() {
1767        let mut ss = default_stylesheet();
1768        let style = Style {
1769            font: Some(FontStyle {
1770                name: Some("Arial".to_string()),
1771                size: Some(12.0),
1772                bold: true,
1773                italic: false,
1774                underline: false,
1775                strikethrough: false,
1776                color: Some(StyleColor::Rgb("FF0000FF".to_string())),
1777            }),
1778            fill: Some(FillStyle {
1779                pattern: PatternType::Solid,
1780                fg_color: Some(StyleColor::Rgb("FFFFFF00".to_string())),
1781                bg_color: None,
1782                gradient: None,
1783            }),
1784            border: Some(BorderStyle {
1785                left: Some(BorderSideStyle {
1786                    style: BorderLineStyle::Thin,
1787                    color: Some(StyleColor::Rgb("FF000000".to_string())),
1788                }),
1789                right: Some(BorderSideStyle {
1790                    style: BorderLineStyle::Thin,
1791                    color: Some(StyleColor::Rgb("FF000000".to_string())),
1792                }),
1793                top: Some(BorderSideStyle {
1794                    style: BorderLineStyle::Medium,
1795                    color: None,
1796                }),
1797                bottom: Some(BorderSideStyle {
1798                    style: BorderLineStyle::Medium,
1799                    color: None,
1800                }),
1801                diagonal: None,
1802            }),
1803            alignment: Some(AlignmentStyle {
1804                horizontal: Some(HorizontalAlign::Center),
1805                vertical: Some(VerticalAlign::Center),
1806                wrap_text: true,
1807                text_rotation: None,
1808                indent: None,
1809                shrink_to_fit: false,
1810            }),
1811            num_fmt: Some(NumFmtStyle::Custom("#,##0.00".to_string())),
1812            protection: Some(ProtectionStyle {
1813                locked: true,
1814                hidden: false,
1815            }),
1816        };
1817
1818        let id = add_style(&mut ss, &style).unwrap();
1819        assert!(id > 0);
1820
1821        let xf = &ss.cell_xfs.xfs[id as usize];
1822        assert!(xf.font_id.unwrap() > 0);
1823        assert!(xf.fill_id.unwrap() > 0);
1824        assert!(xf.border_id.unwrap() > 0);
1825        assert!(xf.num_fmt_id.unwrap() >= CUSTOM_NUM_FMT_BASE);
1826        assert!(xf.alignment.is_some());
1827        assert!(xf.protection.is_some());
1828        assert_eq!(xf.apply_font, Some(true));
1829        assert_eq!(xf.apply_fill, Some(true));
1830        assert_eq!(xf.apply_border, Some(true));
1831        assert_eq!(xf.apply_number_format, Some(true));
1832        assert_eq!(xf.apply_alignment, Some(true));
1833    }
1834
1835    #[test]
1836    fn test_get_style_default() {
1837        let ss = default_stylesheet();
1838        let style = get_style(&ss, 0);
1839        assert!(style.is_some());
1840        let style = style.unwrap();
1841        // Default style should have the default font.
1842        assert!(style.font.is_some());
1843    }
1844
1845    #[test]
1846    fn test_get_style_invalid_id() {
1847        let ss = default_stylesheet();
1848        let style = get_style(&ss, 999);
1849        assert!(style.is_none());
1850    }
1851
1852    #[test]
1853    fn test_get_style_roundtrip_bold() {
1854        let mut ss = default_stylesheet();
1855        let original = Style {
1856            font: Some(FontStyle {
1857                bold: true,
1858                ..FontStyle::default()
1859            }),
1860            ..Style::default()
1861        };
1862
1863        let id = add_style(&mut ss, &original).unwrap();
1864        let retrieved = get_style(&ss, id).unwrap();
1865        assert!(retrieved.font.is_some());
1866        assert!(retrieved.font.as_ref().unwrap().bold);
1867    }
1868
1869    #[test]
1870    fn test_get_style_roundtrip_fill() {
1871        let mut ss = default_stylesheet();
1872        let original = Style {
1873            fill: Some(FillStyle {
1874                pattern: PatternType::Solid,
1875                fg_color: Some(StyleColor::Rgb("FFFF0000".to_string())),
1876                bg_color: None,
1877                gradient: None,
1878            }),
1879            ..Style::default()
1880        };
1881
1882        let id = add_style(&mut ss, &original).unwrap();
1883        let retrieved = get_style(&ss, id).unwrap();
1884        assert!(retrieved.fill.is_some());
1885        let fill = retrieved.fill.unwrap();
1886        assert_eq!(fill.pattern, PatternType::Solid);
1887        assert_eq!(fill.fg_color, Some(StyleColor::Rgb("FFFF0000".to_string())));
1888    }
1889
1890    #[test]
1891    fn test_get_style_roundtrip_alignment() {
1892        let mut ss = default_stylesheet();
1893        let original = Style {
1894            alignment: Some(AlignmentStyle {
1895                horizontal: Some(HorizontalAlign::Right),
1896                vertical: Some(VerticalAlign::Bottom),
1897                wrap_text: true,
1898                text_rotation: Some(45),
1899                indent: Some(2),
1900                shrink_to_fit: false,
1901            }),
1902            ..Style::default()
1903        };
1904
1905        let id = add_style(&mut ss, &original).unwrap();
1906        let retrieved = get_style(&ss, id).unwrap();
1907        assert!(retrieved.alignment.is_some());
1908        let align = retrieved.alignment.unwrap();
1909        assert_eq!(align.horizontal, Some(HorizontalAlign::Right));
1910        assert_eq!(align.vertical, Some(VerticalAlign::Bottom));
1911        assert!(align.wrap_text);
1912        assert_eq!(align.text_rotation, Some(45));
1913        assert_eq!(align.indent, Some(2));
1914    }
1915
1916    #[test]
1917    fn test_get_style_roundtrip_protection() {
1918        let mut ss = default_stylesheet();
1919        let original = Style {
1920            protection: Some(ProtectionStyle {
1921                locked: false,
1922                hidden: true,
1923            }),
1924            ..Style::default()
1925        };
1926
1927        let id = add_style(&mut ss, &original).unwrap();
1928        let retrieved = get_style(&ss, id).unwrap();
1929        assert!(retrieved.protection.is_some());
1930        let prot = retrieved.protection.unwrap();
1931        assert!(!prot.locked);
1932        assert!(prot.hidden);
1933    }
1934
1935    #[test]
1936    fn test_get_style_roundtrip_num_fmt_builtin() {
1937        let mut ss = default_stylesheet();
1938        let original = Style {
1939            num_fmt: Some(NumFmtStyle::Builtin(builtin_num_fmts::DATE_MDY)),
1940            ..Style::default()
1941        };
1942
1943        let id = add_style(&mut ss, &original).unwrap();
1944        let retrieved = get_style(&ss, id).unwrap();
1945        assert!(retrieved.num_fmt.is_some());
1946        match retrieved.num_fmt.unwrap() {
1947            NumFmtStyle::Builtin(fid) => assert_eq!(fid, builtin_num_fmts::DATE_MDY),
1948            _ => panic!("expected Builtin num fmt"),
1949        }
1950    }
1951
1952    #[test]
1953    fn test_get_style_roundtrip_num_fmt_custom() {
1954        let mut ss = default_stylesheet();
1955        let original = Style {
1956            num_fmt: Some(NumFmtStyle::Custom("yyyy-mm-dd".to_string())),
1957            ..Style::default()
1958        };
1959
1960        let id = add_style(&mut ss, &original).unwrap();
1961        let retrieved = get_style(&ss, id).unwrap();
1962        assert!(retrieved.num_fmt.is_some());
1963        match retrieved.num_fmt.unwrap() {
1964            NumFmtStyle::Custom(code) => assert_eq!(code, "yyyy-mm-dd"),
1965            _ => panic!("expected Custom num fmt"),
1966        }
1967    }
1968
1969    #[test]
1970    fn test_builtin_num_fmt_constants() {
1971        assert_eq!(builtin_num_fmts::GENERAL, 0);
1972        assert_eq!(builtin_num_fmts::INTEGER, 1);
1973        assert_eq!(builtin_num_fmts::DECIMAL_2, 2);
1974        assert_eq!(builtin_num_fmts::THOUSANDS, 3);
1975        assert_eq!(builtin_num_fmts::THOUSANDS_DECIMAL, 4);
1976        assert_eq!(builtin_num_fmts::PERCENT, 9);
1977        assert_eq!(builtin_num_fmts::PERCENT_DECIMAL, 10);
1978        assert_eq!(builtin_num_fmts::SCIENTIFIC, 11);
1979        assert_eq!(builtin_num_fmts::DATE_MDY, 14);
1980        assert_eq!(builtin_num_fmts::DATE_DMY, 15);
1981        assert_eq!(builtin_num_fmts::DATE_DM, 16);
1982        assert_eq!(builtin_num_fmts::DATE_MY, 17);
1983        assert_eq!(builtin_num_fmts::TIME_HM_AP, 18);
1984        assert_eq!(builtin_num_fmts::TIME_HMS_AP, 19);
1985        assert_eq!(builtin_num_fmts::TIME_HM, 20);
1986        assert_eq!(builtin_num_fmts::TIME_HMS, 21);
1987        assert_eq!(builtin_num_fmts::DATETIME, 22);
1988        assert_eq!(builtin_num_fmts::TEXT, 49);
1989    }
1990
1991    #[test]
1992    fn test_pattern_type_roundtrip() {
1993        let types = [
1994            PatternType::None,
1995            PatternType::Solid,
1996            PatternType::Gray125,
1997            PatternType::DarkGray,
1998            PatternType::MediumGray,
1999            PatternType::LightGray,
2000        ];
2001        for pt in &types {
2002            let s = pt.as_str();
2003            let back = PatternType::from_str(s);
2004            assert_eq!(*pt, back);
2005        }
2006    }
2007
2008    #[test]
2009    fn test_border_line_style_roundtrip() {
2010        let styles = [
2011            BorderLineStyle::Thin,
2012            BorderLineStyle::Medium,
2013            BorderLineStyle::Thick,
2014            BorderLineStyle::Dashed,
2015            BorderLineStyle::Dotted,
2016            BorderLineStyle::Double,
2017            BorderLineStyle::Hair,
2018            BorderLineStyle::MediumDashed,
2019            BorderLineStyle::DashDot,
2020            BorderLineStyle::MediumDashDot,
2021            BorderLineStyle::DashDotDot,
2022            BorderLineStyle::MediumDashDotDot,
2023            BorderLineStyle::SlantDashDot,
2024        ];
2025        for bls in &styles {
2026            let s = bls.as_str();
2027            let back = BorderLineStyle::from_str(s).unwrap();
2028            assert_eq!(*bls, back);
2029        }
2030    }
2031
2032    #[test]
2033    fn test_horizontal_align_roundtrip() {
2034        let aligns = [
2035            HorizontalAlign::General,
2036            HorizontalAlign::Left,
2037            HorizontalAlign::Center,
2038            HorizontalAlign::Right,
2039            HorizontalAlign::Fill,
2040            HorizontalAlign::Justify,
2041            HorizontalAlign::CenterContinuous,
2042            HorizontalAlign::Distributed,
2043        ];
2044        for ha in &aligns {
2045            let s = ha.as_str();
2046            let back = HorizontalAlign::from_str(s).unwrap();
2047            assert_eq!(*ha, back);
2048        }
2049    }
2050
2051    #[test]
2052    fn test_vertical_align_roundtrip() {
2053        let aligns = [
2054            VerticalAlign::Top,
2055            VerticalAlign::Center,
2056            VerticalAlign::Bottom,
2057            VerticalAlign::Justify,
2058            VerticalAlign::Distributed,
2059        ];
2060        for va in &aligns {
2061            let s = va.as_str();
2062            let back = VerticalAlign::from_str(s).unwrap();
2063            assert_eq!(*va, back);
2064        }
2065    }
2066
2067    #[test]
2068    fn test_style_color_rgb_roundtrip() {
2069        let color = StyleColor::Rgb("FF00FF00".to_string());
2070        let xml = style_color_to_xml(&color);
2071        let back = xml_color_to_style(&xml).unwrap();
2072        assert_eq!(color, back);
2073    }
2074
2075    #[test]
2076    fn test_style_color_theme_roundtrip() {
2077        let color = StyleColor::Theme(4);
2078        let xml = style_color_to_xml(&color);
2079        let back = xml_color_to_style(&xml).unwrap();
2080        assert_eq!(color, back);
2081    }
2082
2083    #[test]
2084    fn test_style_color_indexed_roundtrip() {
2085        let color = StyleColor::Indexed(10);
2086        let xml = style_color_to_xml(&color);
2087        let back = xml_color_to_style(&xml).unwrap();
2088        assert_eq!(color, back);
2089    }
2090
2091    #[test]
2092    fn test_font_deduplication() {
2093        let mut ss = default_stylesheet();
2094        let font = FontStyle {
2095            name: Some("Courier".to_string()),
2096            size: Some(10.0),
2097            bold: true,
2098            ..FontStyle::default()
2099        };
2100
2101        let id1 = add_or_find_font(&mut ss.fonts, &font);
2102        let id2 = add_or_find_font(&mut ss.fonts, &font);
2103        assert_eq!(id1, id2);
2104        // Default has 1 font, we added 1.
2105        assert_eq!(ss.fonts.fonts.len(), 2);
2106    }
2107
2108    #[test]
2109    fn test_multiple_custom_num_fmts() {
2110        let mut ss = default_stylesheet();
2111        let id1 = add_or_find_num_fmt(&mut ss, "0.0%");
2112        let id2 = add_or_find_num_fmt(&mut ss, "#,##0");
2113        assert_eq!(id1, 164);
2114        assert_eq!(id2, 165);
2115
2116        // Same format returns same id.
2117        let id3 = add_or_find_num_fmt(&mut ss, "0.0%");
2118        assert_eq!(id3, 164);
2119    }
2120
2121    #[test]
2122    fn test_xf_count_maintained() {
2123        let mut ss = default_stylesheet();
2124        assert_eq!(ss.cell_xfs.count, Some(1));
2125
2126        let style = Style {
2127            font: Some(FontStyle {
2128                bold: true,
2129                ..FontStyle::default()
2130            }),
2131            ..Style::default()
2132        };
2133        add_style(&mut ss, &style).unwrap();
2134        assert_eq!(ss.cell_xfs.count, Some(2));
2135    }
2136
2137    // -- StyleBuilder tests --
2138
2139    #[test]
2140    fn test_style_builder_empty() {
2141        let style = StyleBuilder::new().build();
2142        assert!(style.font.is_none());
2143        assert!(style.fill.is_none());
2144        assert!(style.border.is_none());
2145        assert!(style.alignment.is_none());
2146        assert!(style.num_fmt.is_none());
2147        assert!(style.protection.is_none());
2148    }
2149
2150    #[test]
2151    fn test_style_builder_default_equivalent() {
2152        let style = StyleBuilder::default().build();
2153        assert!(style.font.is_none());
2154        assert!(style.fill.is_none());
2155    }
2156
2157    #[test]
2158    fn test_style_builder_font() {
2159        let style = StyleBuilder::new()
2160            .bold(true)
2161            .italic(true)
2162            .font_size(14.0)
2163            .font_name("Arial")
2164            .font_color_rgb("FF0000FF")
2165            .build();
2166        let font = style.font.unwrap();
2167        assert!(font.bold);
2168        assert!(font.italic);
2169        assert_eq!(font.size, Some(14.0));
2170        assert_eq!(font.name, Some("Arial".to_string()));
2171        assert_eq!(font.color, Some(StyleColor::Rgb("FF0000FF".to_string())));
2172    }
2173
2174    #[test]
2175    fn test_style_builder_font_underline_strikethrough() {
2176        let style = StyleBuilder::new()
2177            .underline(true)
2178            .strikethrough(true)
2179            .build();
2180        let font = style.font.unwrap();
2181        assert!(font.underline);
2182        assert!(font.strikethrough);
2183    }
2184
2185    #[test]
2186    fn test_style_builder_font_color_typed() {
2187        let style = StyleBuilder::new().font_color(StyleColor::Theme(4)).build();
2188        let font = style.font.unwrap();
2189        assert_eq!(font.color, Some(StyleColor::Theme(4)));
2190    }
2191
2192    #[test]
2193    fn test_style_builder_solid_fill() {
2194        let style = StyleBuilder::new().solid_fill("FFFF0000").build();
2195        let fill = style.fill.unwrap();
2196        assert_eq!(fill.pattern, PatternType::Solid);
2197        assert_eq!(fill.fg_color, Some(StyleColor::Rgb("FFFF0000".to_string())));
2198    }
2199
2200    #[test]
2201    fn test_style_builder_fill_pattern_and_colors() {
2202        let style = StyleBuilder::new()
2203            .fill_pattern(PatternType::Gray125)
2204            .fill_fg_color_rgb("FFAABBCC")
2205            .fill_bg_color(StyleColor::Indexed(64))
2206            .build();
2207        let fill = style.fill.unwrap();
2208        assert_eq!(fill.pattern, PatternType::Gray125);
2209        assert_eq!(fill.fg_color, Some(StyleColor::Rgb("FFAABBCC".to_string())));
2210        assert_eq!(fill.bg_color, Some(StyleColor::Indexed(64)));
2211    }
2212
2213    #[test]
2214    fn test_style_builder_border_individual_sides() {
2215        let style = StyleBuilder::new()
2216            .border_left(
2217                BorderLineStyle::Thin,
2218                StyleColor::Rgb("FF000000".to_string()),
2219            )
2220            .border_right(
2221                BorderLineStyle::Medium,
2222                StyleColor::Rgb("FF111111".to_string()),
2223            )
2224            .border_top(
2225                BorderLineStyle::Thick,
2226                StyleColor::Rgb("FF222222".to_string()),
2227            )
2228            .border_bottom(
2229                BorderLineStyle::Dashed,
2230                StyleColor::Rgb("FF333333".to_string()),
2231            )
2232            .build();
2233        let border = style.border.unwrap();
2234
2235        let left = border.left.unwrap();
2236        assert_eq!(left.style, BorderLineStyle::Thin);
2237        assert_eq!(left.color, Some(StyleColor::Rgb("FF000000".to_string())));
2238
2239        let right = border.right.unwrap();
2240        assert_eq!(right.style, BorderLineStyle::Medium);
2241
2242        let top = border.top.unwrap();
2243        assert_eq!(top.style, BorderLineStyle::Thick);
2244
2245        let bottom = border.bottom.unwrap();
2246        assert_eq!(bottom.style, BorderLineStyle::Dashed);
2247    }
2248
2249    #[test]
2250    fn test_style_builder_border_all() {
2251        let style = StyleBuilder::new()
2252            .border_all(
2253                BorderLineStyle::Thin,
2254                StyleColor::Rgb("FF000000".to_string()),
2255            )
2256            .build();
2257        let border = style.border.unwrap();
2258        assert!(border.left.is_some());
2259        assert!(border.right.is_some());
2260        assert!(border.top.is_some());
2261        assert!(border.bottom.is_some());
2262        // diagonal should not be set by border_all
2263        assert!(border.diagonal.is_none());
2264
2265        let left = border.left.unwrap();
2266        assert_eq!(left.style, BorderLineStyle::Thin);
2267        assert_eq!(left.color, Some(StyleColor::Rgb("FF000000".to_string())));
2268    }
2269
2270    #[test]
2271    fn test_style_builder_alignment() {
2272        let style = StyleBuilder::new()
2273            .horizontal_align(HorizontalAlign::Center)
2274            .vertical_align(VerticalAlign::Center)
2275            .wrap_text(true)
2276            .text_rotation(45)
2277            .indent(2)
2278            .shrink_to_fit(true)
2279            .build();
2280        let align = style.alignment.unwrap();
2281        assert_eq!(align.horizontal, Some(HorizontalAlign::Center));
2282        assert_eq!(align.vertical, Some(VerticalAlign::Center));
2283        assert!(align.wrap_text);
2284        assert_eq!(align.text_rotation, Some(45));
2285        assert_eq!(align.indent, Some(2));
2286        assert!(align.shrink_to_fit);
2287    }
2288
2289    #[test]
2290    fn test_style_builder_num_format_builtin() {
2291        let style = StyleBuilder::new().num_format_builtin(2).build();
2292        match style.num_fmt.unwrap() {
2293            NumFmtStyle::Builtin(id) => assert_eq!(id, 2),
2294            _ => panic!("expected builtin format"),
2295        }
2296    }
2297
2298    #[test]
2299    fn test_style_builder_num_format_custom() {
2300        let style = StyleBuilder::new().num_format_custom("#,##0.00").build();
2301        match style.num_fmt.unwrap() {
2302            NumFmtStyle::Custom(fmt) => assert_eq!(fmt, "#,##0.00"),
2303            _ => panic!("expected custom format"),
2304        }
2305    }
2306
2307    #[test]
2308    fn test_style_builder_protection() {
2309        let style = StyleBuilder::new().locked(true).hidden(true).build();
2310        let prot = style.protection.unwrap();
2311        assert!(prot.locked);
2312        assert!(prot.hidden);
2313    }
2314
2315    #[test]
2316    fn test_style_builder_protection_unlock() {
2317        let style = StyleBuilder::new().locked(false).hidden(false).build();
2318        let prot = style.protection.unwrap();
2319        assert!(!prot.locked);
2320        assert!(!prot.hidden);
2321    }
2322
2323    #[test]
2324    fn test_style_builder_full_style() {
2325        let style = StyleBuilder::new()
2326            .bold(true)
2327            .font_size(12.0)
2328            .solid_fill("FFFFFF00")
2329            .border_all(
2330                BorderLineStyle::Thin,
2331                StyleColor::Rgb("FF000000".to_string()),
2332            )
2333            .horizontal_align(HorizontalAlign::Center)
2334            .num_format_builtin(2)
2335            .locked(true)
2336            .build();
2337        assert!(style.font.is_some());
2338        assert!(style.fill.is_some());
2339        assert!(style.border.is_some());
2340        assert!(style.alignment.is_some());
2341        assert!(style.num_fmt.is_some());
2342        assert!(style.protection.is_some());
2343
2344        // Verify specific values survived the chaining
2345        assert!(style.font.as_ref().unwrap().bold);
2346        assert_eq!(style.font.as_ref().unwrap().size, Some(12.0));
2347        assert_eq!(style.fill.as_ref().unwrap().pattern, PatternType::Solid);
2348    }
2349
2350    #[test]
2351    fn test_style_builder_integrates_with_add_style() {
2352        let mut ss = default_stylesheet();
2353        let style = StyleBuilder::new()
2354            .bold(true)
2355            .font_size(11.0)
2356            .solid_fill("FFFF0000")
2357            .horizontal_align(HorizontalAlign::Center)
2358            .build();
2359
2360        let id = add_style(&mut ss, &style).unwrap();
2361        assert!(id > 0);
2362
2363        // Round-trip: get the style back and verify
2364        let retrieved = get_style(&ss, id).unwrap();
2365        assert!(retrieved.font.as_ref().unwrap().bold);
2366        assert_eq!(retrieved.fill.as_ref().unwrap().pattern, PatternType::Solid);
2367        assert_eq!(
2368            retrieved.alignment.as_ref().unwrap().horizontal,
2369            Some(HorizontalAlign::Center)
2370        );
2371    }
2372}