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#[cfg(test)]
1176mod tests {
1177    use super::*;
1178
1179    /// Helper to create a fresh default stylesheet for tests.
1180    fn default_stylesheet() -> StyleSheet {
1181        StyleSheet::default()
1182    }
1183
1184    #[test]
1185    fn test_add_bold_font_style() {
1186        let mut ss = default_stylesheet();
1187        let style = Style {
1188            font: Some(FontStyle {
1189                bold: true,
1190                ..FontStyle::default()
1191            }),
1192            ..Style::default()
1193        };
1194
1195        let id = add_style(&mut ss, &style).unwrap();
1196        // The default stylesheet has one Xf at index 0, so the new one should be at 1.
1197        assert_eq!(id, 1);
1198        // Font list should have grown.
1199        assert_eq!(ss.fonts.fonts.len(), 2);
1200        assert!(ss.fonts.fonts[1].b.is_some());
1201    }
1202
1203    #[test]
1204    fn test_add_same_style_twice_deduplication() {
1205        let mut ss = default_stylesheet();
1206        let style = Style {
1207            font: Some(FontStyle {
1208                bold: true,
1209                ..FontStyle::default()
1210            }),
1211            ..Style::default()
1212        };
1213
1214        let id1 = add_style(&mut ss, &style).unwrap();
1215        let id2 = add_style(&mut ss, &style).unwrap();
1216        assert_eq!(id1, id2, "same style should return the same ID");
1217        // Only 2 fonts (default + the bold one).
1218        assert_eq!(ss.fonts.fonts.len(), 2);
1219        // Only 2 Xfs (default + the bold one).
1220        assert_eq!(ss.cell_xfs.xfs.len(), 2);
1221    }
1222
1223    #[test]
1224    fn test_add_different_styles_different_ids() {
1225        let mut ss = default_stylesheet();
1226
1227        let bold_style = Style {
1228            font: Some(FontStyle {
1229                bold: true,
1230                ..FontStyle::default()
1231            }),
1232            ..Style::default()
1233        };
1234        let italic_style = Style {
1235            font: Some(FontStyle {
1236                italic: true,
1237                ..FontStyle::default()
1238            }),
1239            ..Style::default()
1240        };
1241
1242        let id1 = add_style(&mut ss, &bold_style).unwrap();
1243        let id2 = add_style(&mut ss, &italic_style).unwrap();
1244        assert_ne!(id1, id2);
1245    }
1246
1247    #[test]
1248    fn test_font_italic() {
1249        let mut ss = default_stylesheet();
1250        let style = Style {
1251            font: Some(FontStyle {
1252                italic: true,
1253                ..FontStyle::default()
1254            }),
1255            ..Style::default()
1256        };
1257
1258        let id = add_style(&mut ss, &style).unwrap();
1259        assert!(id > 0);
1260        let font_id = ss.cell_xfs.xfs[id as usize].font_id.unwrap();
1261        assert!(ss.fonts.fonts[font_id as usize].i.is_some());
1262    }
1263
1264    #[test]
1265    fn test_font_underline() {
1266        let mut ss = default_stylesheet();
1267        let style = Style {
1268            font: Some(FontStyle {
1269                underline: true,
1270                ..FontStyle::default()
1271            }),
1272            ..Style::default()
1273        };
1274
1275        let id = add_style(&mut ss, &style).unwrap();
1276        assert!(id > 0);
1277        let font_id = ss.cell_xfs.xfs[id as usize].font_id.unwrap();
1278        assert!(ss.fonts.fonts[font_id as usize].u.is_some());
1279    }
1280
1281    #[test]
1282    fn test_font_strikethrough() {
1283        let mut ss = default_stylesheet();
1284        let style = Style {
1285            font: Some(FontStyle {
1286                strikethrough: true,
1287                ..FontStyle::default()
1288            }),
1289            ..Style::default()
1290        };
1291
1292        let id = add_style(&mut ss, &style).unwrap();
1293        let font_id = ss.cell_xfs.xfs[id as usize].font_id.unwrap();
1294        assert!(ss.fonts.fonts[font_id as usize].strike.is_some());
1295    }
1296
1297    #[test]
1298    fn test_font_custom_name_and_size() {
1299        let mut ss = default_stylesheet();
1300        let style = Style {
1301            font: Some(FontStyle {
1302                name: Some("Arial".to_string()),
1303                size: Some(14.0),
1304                ..FontStyle::default()
1305            }),
1306            ..Style::default()
1307        };
1308
1309        let id = add_style(&mut ss, &style).unwrap();
1310        let font_id = ss.cell_xfs.xfs[id as usize].font_id.unwrap();
1311        let xml_font = &ss.fonts.fonts[font_id as usize];
1312        assert_eq!(xml_font.name.as_ref().unwrap().val, "Arial");
1313        assert_eq!(xml_font.sz.as_ref().unwrap().val, 14.0);
1314    }
1315
1316    #[test]
1317    fn test_font_with_rgb_color() {
1318        let mut ss = default_stylesheet();
1319        let style = Style {
1320            font: Some(FontStyle {
1321                color: Some(StyleColor::Rgb("FFFF0000".to_string())),
1322                ..FontStyle::default()
1323            }),
1324            ..Style::default()
1325        };
1326
1327        let id = add_style(&mut ss, &style).unwrap();
1328        let font_id = ss.cell_xfs.xfs[id as usize].font_id.unwrap();
1329        let xml_font = &ss.fonts.fonts[font_id as usize];
1330        assert_eq!(
1331            xml_font.color.as_ref().unwrap().rgb,
1332            Some("FFFF0000".to_string())
1333        );
1334    }
1335
1336    #[test]
1337    fn test_fill_solid_color() {
1338        let mut ss = default_stylesheet();
1339        let style = Style {
1340            fill: Some(FillStyle {
1341                pattern: PatternType::Solid,
1342                fg_color: Some(StyleColor::Rgb("FFFFFF00".to_string())),
1343                bg_color: None,
1344                gradient: None,
1345            }),
1346            ..Style::default()
1347        };
1348
1349        let id = add_style(&mut ss, &style).unwrap();
1350        let fill_id = ss.cell_xfs.xfs[id as usize].fill_id.unwrap();
1351        let xml_fill = &ss.fills.fills[fill_id as usize];
1352        let pf = xml_fill.pattern_fill.as_ref().unwrap();
1353        assert_eq!(pf.pattern_type, Some("solid".to_string()));
1354        assert_eq!(
1355            pf.fg_color.as_ref().unwrap().rgb,
1356            Some("FFFFFF00".to_string())
1357        );
1358    }
1359
1360    #[test]
1361    fn test_fill_pattern() {
1362        let mut ss = default_stylesheet();
1363        let style = Style {
1364            fill: Some(FillStyle {
1365                pattern: PatternType::LightGray,
1366                fg_color: None,
1367                bg_color: None,
1368                gradient: None,
1369            }),
1370            ..Style::default()
1371        };
1372
1373        let id = add_style(&mut ss, &style).unwrap();
1374        let fill_id = ss.cell_xfs.xfs[id as usize].fill_id.unwrap();
1375        let xml_fill = &ss.fills.fills[fill_id as usize];
1376        let pf = xml_fill.pattern_fill.as_ref().unwrap();
1377        assert_eq!(pf.pattern_type, Some("lightGray".to_string()));
1378    }
1379
1380    #[test]
1381    fn test_fill_deduplication() {
1382        let mut ss = default_stylesheet();
1383        let style = Style {
1384            fill: Some(FillStyle {
1385                pattern: PatternType::Solid,
1386                fg_color: Some(StyleColor::Rgb("FFFF0000".to_string())),
1387                bg_color: None,
1388                gradient: None,
1389            }),
1390            ..Style::default()
1391        };
1392
1393        let id1 = add_style(&mut ss, &style).unwrap();
1394        let id2 = add_style(&mut ss, &style).unwrap();
1395        assert_eq!(id1, id2);
1396        // Default has 2 fills (none + gray125), we added 1 more.
1397        assert_eq!(ss.fills.fills.len(), 3);
1398    }
1399
1400    #[test]
1401    fn test_border_thin_all_sides() {
1402        let mut ss = default_stylesheet();
1403        let style = Style {
1404            border: Some(BorderStyle {
1405                left: Some(BorderSideStyle {
1406                    style: BorderLineStyle::Thin,
1407                    color: None,
1408                }),
1409                right: Some(BorderSideStyle {
1410                    style: BorderLineStyle::Thin,
1411                    color: None,
1412                }),
1413                top: Some(BorderSideStyle {
1414                    style: BorderLineStyle::Thin,
1415                    color: None,
1416                }),
1417                bottom: Some(BorderSideStyle {
1418                    style: BorderLineStyle::Thin,
1419                    color: None,
1420                }),
1421                diagonal: None,
1422            }),
1423            ..Style::default()
1424        };
1425
1426        let id = add_style(&mut ss, &style).unwrap();
1427        let border_id = ss.cell_xfs.xfs[id as usize].border_id.unwrap();
1428        let xml_border = &ss.borders.borders[border_id as usize];
1429        assert_eq!(
1430            xml_border.left.as_ref().unwrap().style,
1431            Some("thin".to_string())
1432        );
1433        assert_eq!(
1434            xml_border.right.as_ref().unwrap().style,
1435            Some("thin".to_string())
1436        );
1437        assert_eq!(
1438            xml_border.top.as_ref().unwrap().style,
1439            Some("thin".to_string())
1440        );
1441        assert_eq!(
1442            xml_border.bottom.as_ref().unwrap().style,
1443            Some("thin".to_string())
1444        );
1445    }
1446
1447    #[test]
1448    fn test_border_medium() {
1449        let mut ss = default_stylesheet();
1450        let style = Style {
1451            border: Some(BorderStyle {
1452                left: Some(BorderSideStyle {
1453                    style: BorderLineStyle::Medium,
1454                    color: Some(StyleColor::Rgb("FF000000".to_string())),
1455                }),
1456                right: None,
1457                top: None,
1458                bottom: None,
1459                diagonal: None,
1460            }),
1461            ..Style::default()
1462        };
1463
1464        let id = add_style(&mut ss, &style).unwrap();
1465        let border_id = ss.cell_xfs.xfs[id as usize].border_id.unwrap();
1466        let xml_border = &ss.borders.borders[border_id as usize];
1467        let left = xml_border.left.as_ref().unwrap();
1468        assert_eq!(left.style, Some("medium".to_string()));
1469        assert_eq!(
1470            left.color.as_ref().unwrap().rgb,
1471            Some("FF000000".to_string())
1472        );
1473    }
1474
1475    #[test]
1476    fn test_border_thick() {
1477        let mut ss = default_stylesheet();
1478        let style = Style {
1479            border: Some(BorderStyle {
1480                bottom: Some(BorderSideStyle {
1481                    style: BorderLineStyle::Thick,
1482                    color: None,
1483                }),
1484                ..BorderStyle::default()
1485            }),
1486            ..Style::default()
1487        };
1488
1489        let id = add_style(&mut ss, &style).unwrap();
1490        let border_id = ss.cell_xfs.xfs[id as usize].border_id.unwrap();
1491        let xml_border = &ss.borders.borders[border_id as usize];
1492        assert_eq!(
1493            xml_border.bottom.as_ref().unwrap().style,
1494            Some("thick".to_string())
1495        );
1496    }
1497
1498    #[test]
1499    fn test_num_fmt_builtin() {
1500        let mut ss = default_stylesheet();
1501        let style = Style {
1502            num_fmt: Some(NumFmtStyle::Builtin(builtin_num_fmts::PERCENT)),
1503            ..Style::default()
1504        };
1505
1506        let id = add_style(&mut ss, &style).unwrap();
1507        let xf = &ss.cell_xfs.xfs[id as usize];
1508        assert_eq!(xf.num_fmt_id, Some(builtin_num_fmts::PERCENT));
1509        assert_eq!(xf.apply_number_format, Some(true));
1510    }
1511
1512    #[test]
1513    fn test_num_fmt_custom() {
1514        let mut ss = default_stylesheet();
1515        let style = Style {
1516            num_fmt: Some(NumFmtStyle::Custom("#,##0.000".to_string())),
1517            ..Style::default()
1518        };
1519
1520        let id = add_style(&mut ss, &style).unwrap();
1521        let xf = &ss.cell_xfs.xfs[id as usize];
1522        let fmt_id = xf.num_fmt_id.unwrap();
1523        assert!(fmt_id >= CUSTOM_NUM_FMT_BASE);
1524
1525        // Verify the format code was stored.
1526        let num_fmts = ss.num_fmts.as_ref().unwrap();
1527        let nf = num_fmts
1528            .num_fmts
1529            .iter()
1530            .find(|nf| nf.num_fmt_id == fmt_id)
1531            .unwrap();
1532        assert_eq!(nf.format_code, "#,##0.000");
1533    }
1534
1535    #[test]
1536    fn test_num_fmt_custom_deduplication() {
1537        let mut ss = default_stylesheet();
1538        let style = Style {
1539            num_fmt: Some(NumFmtStyle::Custom("0.0%".to_string())),
1540            ..Style::default()
1541        };
1542
1543        let id1 = add_style(&mut ss, &style).unwrap();
1544        let id2 = add_style(&mut ss, &style).unwrap();
1545        assert_eq!(id1, id2);
1546
1547        // Only one custom format should exist.
1548        let num_fmts = ss.num_fmts.as_ref().unwrap();
1549        assert_eq!(num_fmts.num_fmts.len(), 1);
1550    }
1551
1552    #[test]
1553    fn test_alignment_horizontal_center() {
1554        let mut ss = default_stylesheet();
1555        let style = Style {
1556            alignment: Some(AlignmentStyle {
1557                horizontal: Some(HorizontalAlign::Center),
1558                ..AlignmentStyle::default()
1559            }),
1560            ..Style::default()
1561        };
1562
1563        let id = add_style(&mut ss, &style).unwrap();
1564        let xf = &ss.cell_xfs.xfs[id as usize];
1565        assert_eq!(xf.apply_alignment, Some(true));
1566        let align = xf.alignment.as_ref().unwrap();
1567        assert_eq!(align.horizontal, Some("center".to_string()));
1568    }
1569
1570    #[test]
1571    fn test_alignment_vertical_top() {
1572        let mut ss = default_stylesheet();
1573        let style = Style {
1574            alignment: Some(AlignmentStyle {
1575                vertical: Some(VerticalAlign::Top),
1576                ..AlignmentStyle::default()
1577            }),
1578            ..Style::default()
1579        };
1580
1581        let id = add_style(&mut ss, &style).unwrap();
1582        let xf = &ss.cell_xfs.xfs[id as usize];
1583        let align = xf.alignment.as_ref().unwrap();
1584        assert_eq!(align.vertical, Some("top".to_string()));
1585    }
1586
1587    #[test]
1588    fn test_alignment_wrap_text() {
1589        let mut ss = default_stylesheet();
1590        let style = Style {
1591            alignment: Some(AlignmentStyle {
1592                wrap_text: true,
1593                ..AlignmentStyle::default()
1594            }),
1595            ..Style::default()
1596        };
1597
1598        let id = add_style(&mut ss, &style).unwrap();
1599        let xf = &ss.cell_xfs.xfs[id as usize];
1600        let align = xf.alignment.as_ref().unwrap();
1601        assert_eq!(align.wrap_text, Some(true));
1602    }
1603
1604    #[test]
1605    fn test_alignment_text_rotation() {
1606        let mut ss = default_stylesheet();
1607        let style = Style {
1608            alignment: Some(AlignmentStyle {
1609                text_rotation: Some(90),
1610                ..AlignmentStyle::default()
1611            }),
1612            ..Style::default()
1613        };
1614
1615        let id = add_style(&mut ss, &style).unwrap();
1616        let xf = &ss.cell_xfs.xfs[id as usize];
1617        let align = xf.alignment.as_ref().unwrap();
1618        assert_eq!(align.text_rotation, Some(90));
1619    }
1620
1621    #[test]
1622    fn test_protection_locked() {
1623        let mut ss = default_stylesheet();
1624        let style = Style {
1625            protection: Some(ProtectionStyle {
1626                locked: true,
1627                hidden: false,
1628            }),
1629            ..Style::default()
1630        };
1631
1632        let id = add_style(&mut ss, &style).unwrap();
1633        let xf = &ss.cell_xfs.xfs[id as usize];
1634        let prot = xf.protection.as_ref().unwrap();
1635        assert_eq!(prot.locked, Some(true));
1636        assert_eq!(prot.hidden, Some(false));
1637    }
1638
1639    #[test]
1640    fn test_protection_hidden() {
1641        let mut ss = default_stylesheet();
1642        let style = Style {
1643            protection: Some(ProtectionStyle {
1644                locked: false,
1645                hidden: true,
1646            }),
1647            ..Style::default()
1648        };
1649
1650        let id = add_style(&mut ss, &style).unwrap();
1651        let xf = &ss.cell_xfs.xfs[id as usize];
1652        let prot = xf.protection.as_ref().unwrap();
1653        assert_eq!(prot.locked, Some(false));
1654        assert_eq!(prot.hidden, Some(true));
1655    }
1656
1657    #[test]
1658    fn test_combined_style_all_components() {
1659        let mut ss = default_stylesheet();
1660        let style = Style {
1661            font: Some(FontStyle {
1662                name: Some("Arial".to_string()),
1663                size: Some(12.0),
1664                bold: true,
1665                italic: false,
1666                underline: false,
1667                strikethrough: false,
1668                color: Some(StyleColor::Rgb("FF0000FF".to_string())),
1669            }),
1670            fill: Some(FillStyle {
1671                pattern: PatternType::Solid,
1672                fg_color: Some(StyleColor::Rgb("FFFFFF00".to_string())),
1673                bg_color: None,
1674                gradient: None,
1675            }),
1676            border: Some(BorderStyle {
1677                left: Some(BorderSideStyle {
1678                    style: BorderLineStyle::Thin,
1679                    color: Some(StyleColor::Rgb("FF000000".to_string())),
1680                }),
1681                right: Some(BorderSideStyle {
1682                    style: BorderLineStyle::Thin,
1683                    color: Some(StyleColor::Rgb("FF000000".to_string())),
1684                }),
1685                top: Some(BorderSideStyle {
1686                    style: BorderLineStyle::Medium,
1687                    color: None,
1688                }),
1689                bottom: Some(BorderSideStyle {
1690                    style: BorderLineStyle::Medium,
1691                    color: None,
1692                }),
1693                diagonal: None,
1694            }),
1695            alignment: Some(AlignmentStyle {
1696                horizontal: Some(HorizontalAlign::Center),
1697                vertical: Some(VerticalAlign::Center),
1698                wrap_text: true,
1699                text_rotation: None,
1700                indent: None,
1701                shrink_to_fit: false,
1702            }),
1703            num_fmt: Some(NumFmtStyle::Custom("#,##0.00".to_string())),
1704            protection: Some(ProtectionStyle {
1705                locked: true,
1706                hidden: false,
1707            }),
1708        };
1709
1710        let id = add_style(&mut ss, &style).unwrap();
1711        assert!(id > 0);
1712
1713        let xf = &ss.cell_xfs.xfs[id as usize];
1714        assert!(xf.font_id.unwrap() > 0);
1715        assert!(xf.fill_id.unwrap() > 0);
1716        assert!(xf.border_id.unwrap() > 0);
1717        assert!(xf.num_fmt_id.unwrap() >= CUSTOM_NUM_FMT_BASE);
1718        assert!(xf.alignment.is_some());
1719        assert!(xf.protection.is_some());
1720        assert_eq!(xf.apply_font, Some(true));
1721        assert_eq!(xf.apply_fill, Some(true));
1722        assert_eq!(xf.apply_border, Some(true));
1723        assert_eq!(xf.apply_number_format, Some(true));
1724        assert_eq!(xf.apply_alignment, Some(true));
1725    }
1726
1727    #[test]
1728    fn test_get_style_default() {
1729        let ss = default_stylesheet();
1730        let style = get_style(&ss, 0);
1731        assert!(style.is_some());
1732        let style = style.unwrap();
1733        // Default style should have the default font.
1734        assert!(style.font.is_some());
1735    }
1736
1737    #[test]
1738    fn test_get_style_invalid_id() {
1739        let ss = default_stylesheet();
1740        let style = get_style(&ss, 999);
1741        assert!(style.is_none());
1742    }
1743
1744    #[test]
1745    fn test_get_style_roundtrip_bold() {
1746        let mut ss = default_stylesheet();
1747        let original = Style {
1748            font: Some(FontStyle {
1749                bold: true,
1750                ..FontStyle::default()
1751            }),
1752            ..Style::default()
1753        };
1754
1755        let id = add_style(&mut ss, &original).unwrap();
1756        let retrieved = get_style(&ss, id).unwrap();
1757        assert!(retrieved.font.is_some());
1758        assert!(retrieved.font.as_ref().unwrap().bold);
1759    }
1760
1761    #[test]
1762    fn test_get_style_roundtrip_fill() {
1763        let mut ss = default_stylesheet();
1764        let original = Style {
1765            fill: Some(FillStyle {
1766                pattern: PatternType::Solid,
1767                fg_color: Some(StyleColor::Rgb("FFFF0000".to_string())),
1768                bg_color: None,
1769                gradient: None,
1770            }),
1771            ..Style::default()
1772        };
1773
1774        let id = add_style(&mut ss, &original).unwrap();
1775        let retrieved = get_style(&ss, id).unwrap();
1776        assert!(retrieved.fill.is_some());
1777        let fill = retrieved.fill.unwrap();
1778        assert_eq!(fill.pattern, PatternType::Solid);
1779        assert_eq!(fill.fg_color, Some(StyleColor::Rgb("FFFF0000".to_string())));
1780    }
1781
1782    #[test]
1783    fn test_get_style_roundtrip_alignment() {
1784        let mut ss = default_stylesheet();
1785        let original = Style {
1786            alignment: Some(AlignmentStyle {
1787                horizontal: Some(HorizontalAlign::Right),
1788                vertical: Some(VerticalAlign::Bottom),
1789                wrap_text: true,
1790                text_rotation: Some(45),
1791                indent: Some(2),
1792                shrink_to_fit: false,
1793            }),
1794            ..Style::default()
1795        };
1796
1797        let id = add_style(&mut ss, &original).unwrap();
1798        let retrieved = get_style(&ss, id).unwrap();
1799        assert!(retrieved.alignment.is_some());
1800        let align = retrieved.alignment.unwrap();
1801        assert_eq!(align.horizontal, Some(HorizontalAlign::Right));
1802        assert_eq!(align.vertical, Some(VerticalAlign::Bottom));
1803        assert!(align.wrap_text);
1804        assert_eq!(align.text_rotation, Some(45));
1805        assert_eq!(align.indent, Some(2));
1806    }
1807
1808    #[test]
1809    fn test_get_style_roundtrip_protection() {
1810        let mut ss = default_stylesheet();
1811        let original = Style {
1812            protection: Some(ProtectionStyle {
1813                locked: false,
1814                hidden: true,
1815            }),
1816            ..Style::default()
1817        };
1818
1819        let id = add_style(&mut ss, &original).unwrap();
1820        let retrieved = get_style(&ss, id).unwrap();
1821        assert!(retrieved.protection.is_some());
1822        let prot = retrieved.protection.unwrap();
1823        assert!(!prot.locked);
1824        assert!(prot.hidden);
1825    }
1826
1827    #[test]
1828    fn test_get_style_roundtrip_num_fmt_builtin() {
1829        let mut ss = default_stylesheet();
1830        let original = Style {
1831            num_fmt: Some(NumFmtStyle::Builtin(builtin_num_fmts::DATE_MDY)),
1832            ..Style::default()
1833        };
1834
1835        let id = add_style(&mut ss, &original).unwrap();
1836        let retrieved = get_style(&ss, id).unwrap();
1837        assert!(retrieved.num_fmt.is_some());
1838        match retrieved.num_fmt.unwrap() {
1839            NumFmtStyle::Builtin(fid) => assert_eq!(fid, builtin_num_fmts::DATE_MDY),
1840            _ => panic!("expected Builtin num fmt"),
1841        }
1842    }
1843
1844    #[test]
1845    fn test_get_style_roundtrip_num_fmt_custom() {
1846        let mut ss = default_stylesheet();
1847        let original = Style {
1848            num_fmt: Some(NumFmtStyle::Custom("yyyy-mm-dd".to_string())),
1849            ..Style::default()
1850        };
1851
1852        let id = add_style(&mut ss, &original).unwrap();
1853        let retrieved = get_style(&ss, id).unwrap();
1854        assert!(retrieved.num_fmt.is_some());
1855        match retrieved.num_fmt.unwrap() {
1856            NumFmtStyle::Custom(code) => assert_eq!(code, "yyyy-mm-dd"),
1857            _ => panic!("expected Custom num fmt"),
1858        }
1859    }
1860
1861    #[test]
1862    fn test_builtin_num_fmt_constants() {
1863        assert_eq!(builtin_num_fmts::GENERAL, 0);
1864        assert_eq!(builtin_num_fmts::INTEGER, 1);
1865        assert_eq!(builtin_num_fmts::DECIMAL_2, 2);
1866        assert_eq!(builtin_num_fmts::THOUSANDS, 3);
1867        assert_eq!(builtin_num_fmts::THOUSANDS_DECIMAL, 4);
1868        assert_eq!(builtin_num_fmts::PERCENT, 9);
1869        assert_eq!(builtin_num_fmts::PERCENT_DECIMAL, 10);
1870        assert_eq!(builtin_num_fmts::SCIENTIFIC, 11);
1871        assert_eq!(builtin_num_fmts::DATE_MDY, 14);
1872        assert_eq!(builtin_num_fmts::DATE_DMY, 15);
1873        assert_eq!(builtin_num_fmts::DATE_DM, 16);
1874        assert_eq!(builtin_num_fmts::DATE_MY, 17);
1875        assert_eq!(builtin_num_fmts::TIME_HM_AP, 18);
1876        assert_eq!(builtin_num_fmts::TIME_HMS_AP, 19);
1877        assert_eq!(builtin_num_fmts::TIME_HM, 20);
1878        assert_eq!(builtin_num_fmts::TIME_HMS, 21);
1879        assert_eq!(builtin_num_fmts::DATETIME, 22);
1880        assert_eq!(builtin_num_fmts::TEXT, 49);
1881    }
1882
1883    #[test]
1884    fn test_pattern_type_roundtrip() {
1885        let types = [
1886            PatternType::None,
1887            PatternType::Solid,
1888            PatternType::Gray125,
1889            PatternType::DarkGray,
1890            PatternType::MediumGray,
1891            PatternType::LightGray,
1892        ];
1893        for pt in &types {
1894            let s = pt.as_str();
1895            let back = PatternType::from_str(s);
1896            assert_eq!(*pt, back);
1897        }
1898    }
1899
1900    #[test]
1901    fn test_border_line_style_roundtrip() {
1902        let styles = [
1903            BorderLineStyle::Thin,
1904            BorderLineStyle::Medium,
1905            BorderLineStyle::Thick,
1906            BorderLineStyle::Dashed,
1907            BorderLineStyle::Dotted,
1908            BorderLineStyle::Double,
1909            BorderLineStyle::Hair,
1910            BorderLineStyle::MediumDashed,
1911            BorderLineStyle::DashDot,
1912            BorderLineStyle::MediumDashDot,
1913            BorderLineStyle::DashDotDot,
1914            BorderLineStyle::MediumDashDotDot,
1915            BorderLineStyle::SlantDashDot,
1916        ];
1917        for bls in &styles {
1918            let s = bls.as_str();
1919            let back = BorderLineStyle::from_str(s).unwrap();
1920            assert_eq!(*bls, back);
1921        }
1922    }
1923
1924    #[test]
1925    fn test_horizontal_align_roundtrip() {
1926        let aligns = [
1927            HorizontalAlign::General,
1928            HorizontalAlign::Left,
1929            HorizontalAlign::Center,
1930            HorizontalAlign::Right,
1931            HorizontalAlign::Fill,
1932            HorizontalAlign::Justify,
1933            HorizontalAlign::CenterContinuous,
1934            HorizontalAlign::Distributed,
1935        ];
1936        for ha in &aligns {
1937            let s = ha.as_str();
1938            let back = HorizontalAlign::from_str(s).unwrap();
1939            assert_eq!(*ha, back);
1940        }
1941    }
1942
1943    #[test]
1944    fn test_vertical_align_roundtrip() {
1945        let aligns = [
1946            VerticalAlign::Top,
1947            VerticalAlign::Center,
1948            VerticalAlign::Bottom,
1949            VerticalAlign::Justify,
1950            VerticalAlign::Distributed,
1951        ];
1952        for va in &aligns {
1953            let s = va.as_str();
1954            let back = VerticalAlign::from_str(s).unwrap();
1955            assert_eq!(*va, back);
1956        }
1957    }
1958
1959    #[test]
1960    fn test_style_color_rgb_roundtrip() {
1961        let color = StyleColor::Rgb("FF00FF00".to_string());
1962        let xml = style_color_to_xml(&color);
1963        let back = xml_color_to_style(&xml).unwrap();
1964        assert_eq!(color, back);
1965    }
1966
1967    #[test]
1968    fn test_style_color_theme_roundtrip() {
1969        let color = StyleColor::Theme(4);
1970        let xml = style_color_to_xml(&color);
1971        let back = xml_color_to_style(&xml).unwrap();
1972        assert_eq!(color, back);
1973    }
1974
1975    #[test]
1976    fn test_style_color_indexed_roundtrip() {
1977        let color = StyleColor::Indexed(10);
1978        let xml = style_color_to_xml(&color);
1979        let back = xml_color_to_style(&xml).unwrap();
1980        assert_eq!(color, back);
1981    }
1982
1983    #[test]
1984    fn test_font_deduplication() {
1985        let mut ss = default_stylesheet();
1986        let font = FontStyle {
1987            name: Some("Courier".to_string()),
1988            size: Some(10.0),
1989            bold: true,
1990            ..FontStyle::default()
1991        };
1992
1993        let id1 = add_or_find_font(&mut ss.fonts, &font);
1994        let id2 = add_or_find_font(&mut ss.fonts, &font);
1995        assert_eq!(id1, id2);
1996        // Default has 1 font, we added 1.
1997        assert_eq!(ss.fonts.fonts.len(), 2);
1998    }
1999
2000    #[test]
2001    fn test_multiple_custom_num_fmts() {
2002        let mut ss = default_stylesheet();
2003        let id1 = add_or_find_num_fmt(&mut ss, "0.0%");
2004        let id2 = add_or_find_num_fmt(&mut ss, "#,##0");
2005        assert_eq!(id1, 164);
2006        assert_eq!(id2, 165);
2007
2008        // Same format returns same id.
2009        let id3 = add_or_find_num_fmt(&mut ss, "0.0%");
2010        assert_eq!(id3, 164);
2011    }
2012
2013    #[test]
2014    fn test_xf_count_maintained() {
2015        let mut ss = default_stylesheet();
2016        assert_eq!(ss.cell_xfs.count, Some(1));
2017
2018        let style = Style {
2019            font: Some(FontStyle {
2020                bold: true,
2021                ..FontStyle::default()
2022            }),
2023            ..Style::default()
2024        };
2025        add_style(&mut ss, &style).unwrap();
2026        assert_eq!(ss.cell_xfs.count, Some(2));
2027    }
2028
2029    // -- StyleBuilder tests --
2030
2031    #[test]
2032    fn test_style_builder_empty() {
2033        let style = StyleBuilder::new().build();
2034        assert!(style.font.is_none());
2035        assert!(style.fill.is_none());
2036        assert!(style.border.is_none());
2037        assert!(style.alignment.is_none());
2038        assert!(style.num_fmt.is_none());
2039        assert!(style.protection.is_none());
2040    }
2041
2042    #[test]
2043    fn test_style_builder_default_equivalent() {
2044        let style = StyleBuilder::default().build();
2045        assert!(style.font.is_none());
2046        assert!(style.fill.is_none());
2047    }
2048
2049    #[test]
2050    fn test_style_builder_font() {
2051        let style = StyleBuilder::new()
2052            .bold(true)
2053            .italic(true)
2054            .font_size(14.0)
2055            .font_name("Arial")
2056            .font_color_rgb("FF0000FF")
2057            .build();
2058        let font = style.font.unwrap();
2059        assert!(font.bold);
2060        assert!(font.italic);
2061        assert_eq!(font.size, Some(14.0));
2062        assert_eq!(font.name, Some("Arial".to_string()));
2063        assert_eq!(font.color, Some(StyleColor::Rgb("FF0000FF".to_string())));
2064    }
2065
2066    #[test]
2067    fn test_style_builder_font_underline_strikethrough() {
2068        let style = StyleBuilder::new()
2069            .underline(true)
2070            .strikethrough(true)
2071            .build();
2072        let font = style.font.unwrap();
2073        assert!(font.underline);
2074        assert!(font.strikethrough);
2075    }
2076
2077    #[test]
2078    fn test_style_builder_font_color_typed() {
2079        let style = StyleBuilder::new().font_color(StyleColor::Theme(4)).build();
2080        let font = style.font.unwrap();
2081        assert_eq!(font.color, Some(StyleColor::Theme(4)));
2082    }
2083
2084    #[test]
2085    fn test_style_builder_solid_fill() {
2086        let style = StyleBuilder::new().solid_fill("FFFF0000").build();
2087        let fill = style.fill.unwrap();
2088        assert_eq!(fill.pattern, PatternType::Solid);
2089        assert_eq!(fill.fg_color, Some(StyleColor::Rgb("FFFF0000".to_string())));
2090    }
2091
2092    #[test]
2093    fn test_style_builder_fill_pattern_and_colors() {
2094        let style = StyleBuilder::new()
2095            .fill_pattern(PatternType::Gray125)
2096            .fill_fg_color_rgb("FFAABBCC")
2097            .fill_bg_color(StyleColor::Indexed(64))
2098            .build();
2099        let fill = style.fill.unwrap();
2100        assert_eq!(fill.pattern, PatternType::Gray125);
2101        assert_eq!(fill.fg_color, Some(StyleColor::Rgb("FFAABBCC".to_string())));
2102        assert_eq!(fill.bg_color, Some(StyleColor::Indexed(64)));
2103    }
2104
2105    #[test]
2106    fn test_style_builder_border_individual_sides() {
2107        let style = StyleBuilder::new()
2108            .border_left(
2109                BorderLineStyle::Thin,
2110                StyleColor::Rgb("FF000000".to_string()),
2111            )
2112            .border_right(
2113                BorderLineStyle::Medium,
2114                StyleColor::Rgb("FF111111".to_string()),
2115            )
2116            .border_top(
2117                BorderLineStyle::Thick,
2118                StyleColor::Rgb("FF222222".to_string()),
2119            )
2120            .border_bottom(
2121                BorderLineStyle::Dashed,
2122                StyleColor::Rgb("FF333333".to_string()),
2123            )
2124            .build();
2125        let border = style.border.unwrap();
2126
2127        let left = border.left.unwrap();
2128        assert_eq!(left.style, BorderLineStyle::Thin);
2129        assert_eq!(left.color, Some(StyleColor::Rgb("FF000000".to_string())));
2130
2131        let right = border.right.unwrap();
2132        assert_eq!(right.style, BorderLineStyle::Medium);
2133
2134        let top = border.top.unwrap();
2135        assert_eq!(top.style, BorderLineStyle::Thick);
2136
2137        let bottom = border.bottom.unwrap();
2138        assert_eq!(bottom.style, BorderLineStyle::Dashed);
2139    }
2140
2141    #[test]
2142    fn test_style_builder_border_all() {
2143        let style = StyleBuilder::new()
2144            .border_all(
2145                BorderLineStyle::Thin,
2146                StyleColor::Rgb("FF000000".to_string()),
2147            )
2148            .build();
2149        let border = style.border.unwrap();
2150        assert!(border.left.is_some());
2151        assert!(border.right.is_some());
2152        assert!(border.top.is_some());
2153        assert!(border.bottom.is_some());
2154        // diagonal should not be set by border_all
2155        assert!(border.diagonal.is_none());
2156
2157        let left = border.left.unwrap();
2158        assert_eq!(left.style, BorderLineStyle::Thin);
2159        assert_eq!(left.color, Some(StyleColor::Rgb("FF000000".to_string())));
2160    }
2161
2162    #[test]
2163    fn test_style_builder_alignment() {
2164        let style = StyleBuilder::new()
2165            .horizontal_align(HorizontalAlign::Center)
2166            .vertical_align(VerticalAlign::Center)
2167            .wrap_text(true)
2168            .text_rotation(45)
2169            .indent(2)
2170            .shrink_to_fit(true)
2171            .build();
2172        let align = style.alignment.unwrap();
2173        assert_eq!(align.horizontal, Some(HorizontalAlign::Center));
2174        assert_eq!(align.vertical, Some(VerticalAlign::Center));
2175        assert!(align.wrap_text);
2176        assert_eq!(align.text_rotation, Some(45));
2177        assert_eq!(align.indent, Some(2));
2178        assert!(align.shrink_to_fit);
2179    }
2180
2181    #[test]
2182    fn test_style_builder_num_format_builtin() {
2183        let style = StyleBuilder::new().num_format_builtin(2).build();
2184        match style.num_fmt.unwrap() {
2185            NumFmtStyle::Builtin(id) => assert_eq!(id, 2),
2186            _ => panic!("expected builtin format"),
2187        }
2188    }
2189
2190    #[test]
2191    fn test_style_builder_num_format_custom() {
2192        let style = StyleBuilder::new().num_format_custom("#,##0.00").build();
2193        match style.num_fmt.unwrap() {
2194            NumFmtStyle::Custom(fmt) => assert_eq!(fmt, "#,##0.00"),
2195            _ => panic!("expected custom format"),
2196        }
2197    }
2198
2199    #[test]
2200    fn test_style_builder_protection() {
2201        let style = StyleBuilder::new().locked(true).hidden(true).build();
2202        let prot = style.protection.unwrap();
2203        assert!(prot.locked);
2204        assert!(prot.hidden);
2205    }
2206
2207    #[test]
2208    fn test_style_builder_protection_unlock() {
2209        let style = StyleBuilder::new().locked(false).hidden(false).build();
2210        let prot = style.protection.unwrap();
2211        assert!(!prot.locked);
2212        assert!(!prot.hidden);
2213    }
2214
2215    #[test]
2216    fn test_style_builder_full_style() {
2217        let style = StyleBuilder::new()
2218            .bold(true)
2219            .font_size(12.0)
2220            .solid_fill("FFFFFF00")
2221            .border_all(
2222                BorderLineStyle::Thin,
2223                StyleColor::Rgb("FF000000".to_string()),
2224            )
2225            .horizontal_align(HorizontalAlign::Center)
2226            .num_format_builtin(2)
2227            .locked(true)
2228            .build();
2229        assert!(style.font.is_some());
2230        assert!(style.fill.is_some());
2231        assert!(style.border.is_some());
2232        assert!(style.alignment.is_some());
2233        assert!(style.num_fmt.is_some());
2234        assert!(style.protection.is_some());
2235
2236        // Verify specific values survived the chaining
2237        assert!(style.font.as_ref().unwrap().bold);
2238        assert_eq!(style.font.as_ref().unwrap().size, Some(12.0));
2239        assert_eq!(style.fill.as_ref().unwrap().pattern, PatternType::Solid);
2240    }
2241
2242    #[test]
2243    fn test_style_builder_integrates_with_add_style() {
2244        let mut ss = default_stylesheet();
2245        let style = StyleBuilder::new()
2246            .bold(true)
2247            .font_size(11.0)
2248            .solid_fill("FFFF0000")
2249            .horizontal_align(HorizontalAlign::Center)
2250            .build();
2251
2252        let id = add_style(&mut ss, &style).unwrap();
2253        assert!(id > 0);
2254
2255        // Round-trip: get the style back and verify
2256        let retrieved = get_style(&ss, id).unwrap();
2257        assert!(retrieved.font.as_ref().unwrap().bold);
2258        assert_eq!(retrieved.fill.as_ref().unwrap().pattern, PatternType::Solid);
2259        assert_eq!(
2260            retrieved.alignment.as_ref().unwrap().horizontal,
2261            Some(HorizontalAlign::Center)
2262        );
2263    }
2264}