Skip to main content

calamine_styles/
style.rs

1// SPDX-License-Identifier: MIT
2//
3// Copyright 2016-2025, Johann Tuffe.
4
5use std::collections::BTreeMap;
6use std::fmt;
7
8/// Represents a color in ARGB format
9#[derive(Debug, Clone, Copy, PartialEq, Default)]
10pub struct Color {
11    /// Alpha channel (0-255)
12    pub alpha: u8,
13    /// Red channel (0-255)
14    pub red: u8,
15    /// Green channel (0-255)
16    pub green: u8,
17    /// Blue channel (0-255)
18    pub blue: u8,
19}
20
21impl Color {
22    /// Create a new color from ARGB values
23    pub fn new(alpha: u8, red: u8, green: u8, blue: u8) -> Self {
24        Self {
25            alpha,
26            red,
27            green,
28            blue,
29        }
30    }
31
32    /// Create a color from RGB values (alpha = 255)
33    pub fn rgb(red: u8, green: u8, blue: u8) -> Self {
34        Self::new(255, red, green, blue)
35    }
36
37    /// Create a color from ARGB integer
38    pub fn from_argb(argb: u32) -> Self {
39        Self {
40            alpha: ((argb >> 24) & 0xFF) as u8,
41            red: ((argb >> 16) & 0xFF) as u8,
42            green: ((argb >> 8) & 0xFF) as u8,
43            blue: (argb & 0xFF) as u8,
44        }
45    }
46
47    /// Convert to ARGB integer
48    pub fn to_argb(&self) -> u32 {
49        ((self.alpha as u32) << 24)
50            | ((self.red as u32) << 16)
51            | ((self.green as u32) << 8)
52            | (self.blue as u32)
53    }
54
55    /// Check if the color is black
56    pub fn is_black(&self) -> bool {
57        self.red == 0 && self.green == 0 && self.blue == 0
58    }
59
60    /// Check if the color is white
61    pub fn is_white(&self) -> bool {
62        self.red == 255 && self.green == 255 && self.blue == 255
63    }
64}
65
66impl fmt::Display for Color {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        write!(f, "#{:02X}{:02X}{:02X}", self.red, self.green, self.blue)
69    }
70}
71
72/// Border style enumeration
73#[derive(Debug, Clone, Copy, PartialEq, Default)]
74pub enum BorderStyle {
75    /// No border
76    #[default]
77    None,
78    /// Thin border
79    Thin,
80    /// Medium border
81    Medium,
82    /// Thick border
83    Thick,
84    /// Double border
85    Double,
86    /// Hair border
87    Hair,
88    /// Dashed border
89    Dashed,
90    /// Dotted border
91    Dotted,
92    /// Medium dashed border
93    MediumDashed,
94    /// Dash dot border
95    DashDot,
96    /// Dash dot dot border
97    DashDotDot,
98    /// Slant dash dot border
99    SlantDashDot,
100}
101
102/// Border side
103#[derive(Debug, Clone, Default, PartialEq)]
104pub struct Border {
105    /// Border style
106    pub style: BorderStyle,
107    /// Border color
108    pub color: Option<Color>,
109}
110
111impl Border {
112    /// Create a new border with style
113    pub fn new(style: BorderStyle) -> Self {
114        Self { style, color: None }
115    }
116
117    /// Create a new border with style and color
118    pub fn with_color(style: BorderStyle, color: Color) -> Self {
119        Self {
120            style,
121            color: Some(color),
122        }
123    }
124
125    /// Check if border is visible
126    pub fn is_visible(&self) -> bool {
127        self.style != BorderStyle::None
128    }
129}
130
131/// All borders for a cell
132#[derive(Debug, Clone, Default, PartialEq)]
133pub struct Borders {
134    /// Left border
135    pub left: Border,
136    /// Right border
137    pub right: Border,
138    /// Top border
139    pub top: Border,
140    /// Bottom border
141    pub bottom: Border,
142    /// Diagonal down border
143    pub diagonal_down: Border,
144    /// Diagonal up border
145    pub diagonal_up: Border,
146}
147
148impl Borders {
149    /// Create new borders
150    pub fn new() -> Self {
151        Self::default()
152    }
153
154    /// Check if any border is visible
155    pub fn has_visible_borders(&self) -> bool {
156        self.left.is_visible()
157            || self.right.is_visible()
158            || self.top.is_visible()
159            || self.bottom.is_visible()
160            || self.diagonal_down.is_visible()
161            || self.diagonal_up.is_visible()
162    }
163}
164
165/// Font weight
166#[derive(Debug, Clone, Copy, PartialEq, Default)]
167pub enum FontWeight {
168    /// Normal weight
169    #[default]
170    Normal,
171    /// Bold weight
172    Bold,
173}
174
175/// Font style
176#[derive(Debug, Clone, Copy, PartialEq, Default)]
177pub enum FontStyle {
178    /// Normal style
179    #[default]
180    Normal,
181    /// Italic style
182    Italic,
183}
184
185/// Underline style
186#[derive(Debug, Clone, Copy, PartialEq, Default)]
187pub enum UnderlineStyle {
188    /// No underline
189    #[default]
190    None,
191    /// Single underline
192    Single,
193    /// Double underline
194    Double,
195    /// Single accounting underline
196    SingleAccounting,
197    /// Double accounting underline
198    DoubleAccounting,
199}
200
201/// Font properties
202#[derive(Debug, Clone, Default, PartialEq)]
203pub struct Font {
204    /// Font name
205    pub name: Option<String>,
206    /// Font size in points
207    pub size: Option<f64>,
208    /// Font weight
209    pub weight: FontWeight,
210    /// Font style
211    pub style: FontStyle,
212    /// Underline style
213    pub underline: UnderlineStyle,
214    /// Strikethrough
215    pub strikethrough: bool,
216    /// Font color
217    pub color: Option<Color>,
218    /// Font family
219    pub family: Option<String>,
220}
221
222impl Font {
223    /// Create a new font
224    pub fn new() -> Self {
225        Self::default()
226    }
227
228    /// Set font name
229    pub fn with_name(mut self, name: String) -> Self {
230        self.name = Some(name);
231        self
232    }
233
234    /// Set font size
235    pub fn with_size(mut self, size: f64) -> Self {
236        self.size = Some(size);
237        self
238    }
239
240    /// Set font weight
241    pub fn with_weight(mut self, weight: FontWeight) -> Self {
242        self.weight = weight;
243        self
244    }
245
246    /// Set font style
247    pub fn with_style(mut self, style: FontStyle) -> Self {
248        self.style = style;
249        self
250    }
251
252    /// Set underline
253    pub fn with_underline(mut self, underline: UnderlineStyle) -> Self {
254        self.underline = underline;
255        self
256    }
257
258    /// Set strikethrough
259    pub fn with_strikethrough(mut self, strikethrough: bool) -> Self {
260        self.strikethrough = strikethrough;
261        self
262    }
263
264    /// Set font color
265    pub fn with_color(mut self, color: Color) -> Self {
266        self.color = Some(color);
267        self
268    }
269
270    /// Set font family
271    pub fn with_family(mut self, family: String) -> Self {
272        self.family = Some(family);
273        self
274    }
275
276    /// Check if font is bold
277    pub fn is_bold(&self) -> bool {
278        self.weight == FontWeight::Bold
279    }
280
281    /// Check if font is italic
282    pub fn is_italic(&self) -> bool {
283        self.style == FontStyle::Italic
284    }
285
286    /// Check if font has underline
287    pub fn has_underline(&self) -> bool {
288        self.underline != UnderlineStyle::None
289    }
290
291    /// Check if font has strikethrough
292    pub fn has_strikethrough(&self) -> bool {
293        self.strikethrough
294    }
295}
296
297/// A run of text with its own formatting within a rich text cell
298#[derive(Debug, Clone, PartialEq, Default)]
299pub struct TextRun {
300    /// The text content of this run
301    pub text: String,
302    /// Font properties for this run (None means inherit cell default)
303    pub font: Option<Font>,
304}
305
306impl TextRun {
307    /// Create a new text run with just text (no formatting)
308    pub fn new(text: String) -> Self {
309        Self { text, font: None }
310    }
311
312    /// Create a new text run with text and font
313    pub fn with_font(text: String, font: Font) -> Self {
314        Self {
315            text,
316            font: Some(font),
317        }
318    }
319
320    /// Check if this run has any formatting
321    pub fn has_formatting(&self) -> bool {
322        self.font.is_some()
323    }
324}
325
326/// Rich text content with multiple formatted runs
327///
328/// Rich text allows different parts of a cell's text to have different
329/// formatting (bold, italic, colors, etc.). This is common in spreadsheets
330/// where users want to emphasize certain words within a cell.
331#[derive(Debug, Clone, PartialEq, Default)]
332pub struct RichText {
333    /// The text runs in order
334    pub runs: Vec<TextRun>,
335}
336
337impl RichText {
338    /// Create a new empty rich text
339    pub fn new() -> Self {
340        Self::default()
341    }
342
343    /// Create rich text from a vector of runs
344    pub fn from_runs(runs: Vec<TextRun>) -> Self {
345        Self { runs }
346    }
347
348    /// Add a text run
349    pub fn push(&mut self, run: TextRun) {
350        self.runs.push(run);
351    }
352
353    /// Add a plain text run (no formatting)
354    pub fn push_text(&mut self, text: String) {
355        self.runs.push(TextRun::new(text));
356    }
357
358    /// Add a formatted text run
359    pub fn push_formatted(&mut self, text: String, font: Font) {
360        self.runs.push(TextRun::with_font(text, font));
361    }
362
363    /// Get the plain text content (all runs concatenated)
364    pub fn plain_text(&self) -> String {
365        self.runs.iter().map(|r| r.text.as_str()).collect()
366    }
367
368    /// Check if the rich text is empty
369    pub fn is_empty(&self) -> bool {
370        self.runs.is_empty() || self.runs.iter().all(|r| r.text.is_empty())
371    }
372
373    /// Get the number of runs
374    pub fn len(&self) -> usize {
375        self.runs.len()
376    }
377
378    /// Check if any run has formatting
379    pub fn has_formatting(&self) -> bool {
380        self.runs.iter().any(|r| r.has_formatting())
381    }
382
383    /// Iterate over the runs
384    pub fn iter(&self) -> impl Iterator<Item = &TextRun> {
385        self.runs.iter()
386    }
387}
388
389impl std::fmt::Display for RichText {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        write!(f, "{}", self.plain_text())
392    }
393}
394
395/// Horizontal alignment
396#[derive(Debug, Clone, Copy, PartialEq, Default)]
397pub enum HorizontalAlignment {
398    /// Left alignment
399    Left,
400    /// Center alignment
401    Center,
402    /// Right alignment
403    Right,
404    /// Justify alignment
405    Justify,
406    /// Distributed alignment
407    Distributed,
408    /// Fill alignment
409    Fill,
410    /// General alignment (default)
411    #[default]
412    General,
413}
414
415/// Vertical alignment
416#[derive(Debug, Clone, Copy, PartialEq, Default)]
417pub enum VerticalAlignment {
418    /// Top alignment
419    Top,
420    /// Center alignment
421    Center,
422    /// Bottom alignment
423    #[default]
424    Bottom,
425    /// Justify alignment
426    Justify,
427    /// Distributed alignment
428    Distributed,
429}
430
431/// Text rotation in degrees
432#[derive(Debug, Clone, Copy, PartialEq, Default)]
433pub enum TextRotation {
434    /// No rotation
435    #[default]
436    None,
437    /// Rotated by degrees (0-180)
438    Degrees(u16),
439    /// Stacked text
440    Stacked,
441}
442
443/// Cell alignment properties
444#[derive(Debug, Clone, Default, PartialEq)]
445pub struct Alignment {
446    /// Horizontal alignment
447    pub horizontal: HorizontalAlignment,
448    /// Vertical alignment
449    pub vertical: VerticalAlignment,
450    /// Text rotation
451    pub text_rotation: TextRotation,
452    /// Wrap text
453    pub wrap_text: bool,
454    /// Indent level
455    pub indent: Option<u8>,
456    /// Shrink to fit
457    pub shrink_to_fit: bool,
458}
459
460impl Alignment {
461    /// Create new alignment
462    pub fn new() -> Self {
463        Self::default()
464    }
465
466    /// Set horizontal alignment
467    pub fn with_horizontal(mut self, horizontal: HorizontalAlignment) -> Self {
468        self.horizontal = horizontal;
469        self
470    }
471
472    /// Set vertical alignment
473    pub fn with_vertical(mut self, vertical: VerticalAlignment) -> Self {
474        self.vertical = vertical;
475        self
476    }
477
478    /// Set text rotation
479    pub fn with_text_rotation(mut self, rotation: TextRotation) -> Self {
480        self.text_rotation = rotation;
481        self
482    }
483
484    /// Set wrap text
485    pub fn with_wrap_text(mut self, wrap: bool) -> Self {
486        self.wrap_text = wrap;
487        self
488    }
489
490    /// Set indent level
491    pub fn with_indent(mut self, indent: u8) -> Self {
492        self.indent = Some(indent);
493        self
494    }
495
496    /// Set shrink to fit
497    pub fn with_shrink_to_fit(mut self, shrink: bool) -> Self {
498        self.shrink_to_fit = shrink;
499        self
500    }
501}
502
503/// Fill pattern type
504#[derive(Debug, Clone, Copy, PartialEq, Default)]
505pub enum FillPattern {
506    /// No fill
507    #[default]
508    None,
509    /// Solid fill
510    Solid,
511    /// Dark gray pattern
512    DarkGray,
513    /// Medium gray pattern
514    MediumGray,
515    /// Light gray pattern
516    LightGray,
517    /// Gray 125 pattern
518    Gray125,
519    /// Gray 0625 pattern
520    Gray0625,
521    /// Dark horizontal pattern
522    DarkHorizontal,
523    /// Dark vertical pattern
524    DarkVertical,
525    /// Dark down pattern
526    DarkDown,
527    /// Dark up pattern
528    DarkUp,
529    /// Dark grid pattern
530    DarkGrid,
531    /// Dark trellis pattern
532    DarkTrellis,
533    /// Light horizontal pattern
534    LightHorizontal,
535    /// Light vertical pattern
536    LightVertical,
537    /// Light down pattern
538    LightDown,
539    /// Light up pattern
540    LightUp,
541    /// Light grid pattern
542    LightGrid,
543    /// Light trellis pattern
544    LightTrellis,
545}
546
547/// Fill properties
548#[derive(Debug, Clone, Default, PartialEq)]
549pub struct Fill {
550    /// Fill pattern
551    pub pattern: FillPattern,
552    /// Foreground color
553    pub foreground_color: Option<Color>,
554    /// Background color
555    pub background_color: Option<Color>,
556}
557
558impl Fill {
559    /// Create new fill
560    pub fn new() -> Self {
561        Self::default()
562    }
563
564    /// Create solid fill with color
565    pub fn solid(color: Color) -> Self {
566        Self {
567            pattern: FillPattern::Solid,
568            foreground_color: Some(color),
569            background_color: None,
570        }
571    }
572
573    /// Set pattern
574    pub fn with_pattern(mut self, pattern: FillPattern) -> Self {
575        self.pattern = pattern;
576        self
577    }
578
579    /// Set foreground color
580    pub fn with_foreground_color(mut self, color: Color) -> Self {
581        self.foreground_color = Some(color);
582        self
583    }
584
585    /// Set background color
586    pub fn with_background_color(mut self, color: Color) -> Self {
587        self.background_color = Some(color);
588        self
589    }
590
591    /// Check if fill is visible
592    pub fn is_visible(&self) -> bool {
593        self.pattern != FillPattern::None
594    }
595
596    /// Get the main fill color (foreground if available, otherwise background)
597    pub fn get_color(&self) -> Option<Color> {
598        self.foreground_color.or(self.background_color)
599    }
600}
601
602/// Number format
603#[derive(Debug, Clone, PartialEq)]
604pub struct NumberFormat {
605    /// Format code
606    pub format_code: String,
607    /// Format ID
608    pub format_id: Option<u32>,
609}
610
611impl NumberFormat {
612    /// Create new number format
613    pub fn new(format_code: String) -> Self {
614        Self {
615            format_code,
616            format_id: None,
617        }
618    }
619
620    /// Create with format ID
621    pub fn with_id(mut self, format_id: u32) -> Self {
622        self.format_id = Some(format_id);
623        self
624    }
625}
626
627impl Default for NumberFormat {
628    fn default() -> Self {
629        Self {
630            format_code: "General".to_string(),
631            format_id: None,
632        }
633    }
634}
635
636/// Cell protection properties
637#[derive(Debug, Clone, Default, PartialEq)]
638pub struct Protection {
639    /// Cell is locked
640    pub locked: bool,
641    /// Cell is hidden
642    pub hidden: bool,
643}
644
645impl Protection {
646    /// Create new protection
647    pub fn new() -> Self {
648        Self::default()
649    }
650
651    /// Set locked
652    pub fn with_locked(mut self, locked: bool) -> Self {
653        self.locked = locked;
654        self
655    }
656
657    /// Set hidden
658    pub fn with_hidden(mut self, hidden: bool) -> Self {
659        self.hidden = hidden;
660        self
661    }
662}
663
664/// Column width information
665#[derive(Debug, Clone, PartialEq)]
666pub struct ColumnWidth {
667    /// Column index (0-based)
668    pub column: u32,
669    /// Width in Excel units (characters)
670    pub width: f64,
671    /// Whether the width is custom (manually set)
672    pub custom_width: bool,
673    /// Whether the column is hidden
674    pub hidden: bool,
675    /// Best fit width
676    pub best_fit: bool,
677}
678
679impl ColumnWidth {
680    /// Create a new column width
681    pub fn new(column: u32, width: f64) -> Self {
682        Self {
683            column,
684            width,
685            custom_width: false,
686            hidden: false,
687            best_fit: false,
688        }
689    }
690
691    /// Set custom width flag
692    pub fn with_custom_width(mut self, custom: bool) -> Self {
693        self.custom_width = custom;
694        self
695    }
696
697    /// Set hidden flag
698    pub fn with_hidden(mut self, hidden: bool) -> Self {
699        self.hidden = hidden;
700        self
701    }
702
703    /// Set best fit flag
704    pub fn with_best_fit(mut self, best_fit: bool) -> Self {
705        self.best_fit = best_fit;
706        self
707    }
708
709    /// Check if column is visible
710    pub fn is_visible(&self) -> bool {
711        !self.hidden
712    }
713}
714
715/// Row height information
716#[derive(Debug, Clone, PartialEq)]
717pub struct RowHeight {
718    /// Row index (0-based)
719    pub row: u32,
720    /// Height in points
721    pub height: f64,
722    /// Whether the height is custom (manually set)
723    pub custom_height: bool,
724    /// Whether the row is hidden
725    pub hidden: bool,
726    /// Thick top border
727    pub thick_top: bool,
728    /// Thick bottom border
729    pub thick_bottom: bool,
730}
731
732impl RowHeight {
733    /// Create a new row height
734    pub fn new(row: u32, height: f64) -> Self {
735        Self {
736            row,
737            height,
738            custom_height: false,
739            hidden: false,
740            thick_top: false,
741            thick_bottom: false,
742        }
743    }
744
745    /// Set custom height flag
746    pub fn with_custom_height(mut self, custom: bool) -> Self {
747        self.custom_height = custom;
748        self
749    }
750
751    /// Set hidden flag
752    pub fn with_hidden(mut self, hidden: bool) -> Self {
753        self.hidden = hidden;
754        self
755    }
756
757    /// Set thick top border
758    pub fn with_thick_top(mut self, thick_top: bool) -> Self {
759        self.thick_top = thick_top;
760        self
761    }
762
763    /// Set thick bottom border
764    pub fn with_thick_bottom(mut self, thick_bottom: bool) -> Self {
765        self.thick_bottom = thick_bottom;
766        self
767    }
768
769    /// Check if row is visible
770    pub fn is_visible(&self) -> bool {
771        !self.hidden
772    }
773}
774
775/// Worksheet layout information
776#[derive(Debug, Clone, Default, PartialEq)]
777pub struct WorksheetLayout {
778    /// Column widths (keyed by column index)
779    pub column_widths: BTreeMap<u32, ColumnWidth>,
780    /// Row heights (keyed by row index)
781    pub row_heights: BTreeMap<u32, RowHeight>,
782    /// Default column width
783    pub default_column_width: Option<f64>,
784    /// Default row height
785    pub default_row_height: Option<f64>,
786}
787
788impl WorksheetLayout {
789    /// Create a new worksheet layout
790    pub fn new() -> Self {
791        Self::default()
792    }
793
794    /// Add a column width
795    pub fn add_column_width(mut self, column_width: ColumnWidth) -> Self {
796        self.column_widths.insert(column_width.column, column_width);
797        self
798    }
799
800    /// Add a row height
801    pub fn add_row_height(mut self, row_height: RowHeight) -> Self {
802        self.row_heights.insert(row_height.row, row_height);
803        self
804    }
805
806    /// Set default column width
807    pub fn with_default_column_width(mut self, width: f64) -> Self {
808        self.default_column_width = Some(width);
809        self
810    }
811
812    /// Set default row height
813    pub fn with_default_row_height(mut self, height: f64) -> Self {
814        self.default_row_height = Some(height);
815        self
816    }
817
818    /// Get column width for a specific column (O(log n) lookup)
819    pub fn get_column_width(&self, column: u32) -> Option<&ColumnWidth> {
820        self.column_widths.get(&column)
821    }
822
823    /// Get row height for a specific row (O(log n) lookup)
824    pub fn get_row_height(&self, row: u32) -> Option<&RowHeight> {
825        self.row_heights.get(&row)
826    }
827
828    /// Get effective column width (custom or default).
829    ///
830    /// Returns the column width in Excel's character-based units. If no custom
831    /// width is set, returns the worksheet's default column width, or 8.43 if
832    /// no default is specified.
833    ///
834    /// **Note:** Excel column widths are stored in character units relative to
835    /// the workbook's default font, not pixels. Converting to pixels requires
836    /// font metrics and is font-dependent. The value 8.43 is Excel's standard
837    /// default for Calibri 11pt.
838    pub fn get_effective_column_width(&self, column: u32) -> f64 {
839        self.get_column_width(column)
840            .map(|cw| cw.width)
841            .or(self.default_column_width)
842            .unwrap_or(8.43)
843    }
844
845    /// Get effective row height (custom or default).
846    ///
847    /// Returns the row height in points. If no custom height is set, returns
848    /// the worksheet's default row height, or 15.0 if no default is specified.
849    ///
850    /// **Note:** Row heights in Excel are stored in points (1/72 inch), but
851    /// the actual displayed height may vary slightly depending on the default
852    /// font. The value 15.0 is Excel's standard default for Calibri 11pt.
853    pub fn get_effective_row_height(&self, row: u32) -> f64 {
854        self.get_row_height(row)
855            .map(|rh| rh.height)
856            .or(self.default_row_height)
857            .unwrap_or(15.0)
858    }
859
860    /// Check if layout has any custom dimensions
861    pub fn has_custom_dimensions(&self) -> bool {
862        !self.column_widths.is_empty()
863            || !self.row_heights.is_empty()
864            || self.default_column_width.is_some()
865            || self.default_row_height.is_some()
866    }
867}
868
869/// Complete cell style
870#[derive(Debug, Clone, Default, PartialEq)]
871pub struct Style {
872    /// Font properties
873    pub font: Option<Font>,
874    /// Fill properties
875    pub fill: Option<Fill>,
876    /// Border properties
877    pub borders: Option<Borders>,
878    /// Alignment properties
879    pub alignment: Option<Alignment>,
880    /// Number format
881    pub number_format: Option<NumberFormat>,
882    /// Protection properties
883    pub protection: Option<Protection>,
884    /// Style ID (for internal use)
885    pub style_id: Option<u32>,
886}
887
888impl Style {
889    /// Create new style
890    pub fn new() -> Self {
891        Self::default()
892    }
893
894    /// Set font
895    pub fn with_font(mut self, font: Font) -> Self {
896        self.font = Some(font);
897        self
898    }
899
900    /// Set fill
901    pub fn with_fill(mut self, fill: Fill) -> Self {
902        self.fill = Some(fill);
903        self
904    }
905
906    /// Set borders
907    pub fn with_borders(mut self, borders: Borders) -> Self {
908        self.borders = Some(borders);
909        self
910    }
911
912    /// Set alignment
913    pub fn with_alignment(mut self, alignment: Alignment) -> Self {
914        self.alignment = Some(alignment);
915        self
916    }
917
918    /// Set number format
919    pub fn with_number_format(mut self, number_format: NumberFormat) -> Self {
920        self.number_format = Some(number_format);
921        self
922    }
923
924    /// Set protection
925    pub fn with_protection(mut self, protection: Protection) -> Self {
926        self.protection = Some(protection);
927        self
928    }
929
930    /// Set style ID
931    pub fn with_style_id(mut self, style_id: u32) -> Self {
932        self.style_id = Some(style_id);
933        self
934    }
935
936    /// Get font
937    pub fn get_font(&self) -> Option<&Font> {
938        self.font.as_ref()
939    }
940
941    /// Get fill
942    pub fn get_fill(&self) -> Option<&Fill> {
943        self.fill.as_ref()
944    }
945
946    /// Get borders
947    pub fn get_borders(&self) -> Option<&Borders> {
948        self.borders.as_ref()
949    }
950
951    /// Get alignment
952    pub fn get_alignment(&self) -> Option<&Alignment> {
953        self.alignment.as_ref()
954    }
955
956    /// Get number format
957    pub fn get_number_format(&self) -> Option<&NumberFormat> {
958        self.number_format.as_ref()
959    }
960
961    /// Get protection
962    pub fn get_protection(&self) -> Option<&Protection> {
963        self.protection.as_ref()
964    }
965
966    /// Check if style is empty (no properties set)
967    pub fn is_empty(&self) -> bool {
968        self.font.is_none()
969            && self.fill.is_none()
970            && self.borders.is_none()
971            && self.alignment.is_none()
972            && self.number_format.is_none()
973            && self.protection.is_none()
974    }
975
976    /// Check if style has any visible properties
977    pub fn has_visible_properties(&self) -> bool {
978        (self
979            .font
980            .as_ref()
981            .is_some_and(|f| f.color.is_some() || f.is_bold() || f.is_italic()))
982            || (self.fill.as_ref().is_some_and(|f| f.is_visible()))
983            || (self
984                .borders
985                .as_ref()
986                .is_some_and(|b| b.has_visible_borders()))
987            || (self.alignment.as_ref().is_some_and(|a| {
988                a.horizontal != HorizontalAlignment::General
989                    || a.vertical != VerticalAlignment::Bottom
990                    || a.text_rotation != TextRotation::None
991                    || a.wrap_text
992                    || a.indent.is_some()
993                    || a.shrink_to_fit
994            }))
995    }
996}
997
998/// A run of consecutive cells with the same style (row-major order)
999#[derive(Debug, Clone)]
1000struct StyleRun {
1001    /// Index into the palette (0 = no style/default)
1002    style_id: u16,
1003    /// Number of consecutive cells with this style
1004    count: u32,
1005}
1006
1007/// RLE-compressed style storage for a worksheet range.
1008///
1009/// Instead of storing one Style per cell (which wastes memory when many cells
1010/// share the same style), this stores:
1011/// - A palette of unique styles
1012/// - Runs of consecutive cells (row-major) that share the same style
1013///
1014/// This dramatically reduces memory usage and clone overhead for large worksheets.
1015#[derive(Debug, Clone, Default)]
1016pub struct StyleRange {
1017    start: (u32, u32),
1018    end: (u32, u32),
1019    /// Palette of unique styles. Index 0 is reserved for "no style" (empty).
1020    palette: Vec<Style>,
1021    /// RLE-encoded runs in row-major order
1022    runs: Vec<StyleRun>,
1023    /// Total cell count (for validation)
1024    total_cells: u64,
1025}
1026
1027impl StyleRange {
1028    /// Create an empty StyleRange
1029    pub fn empty() -> Self {
1030        Self::default()
1031    }
1032
1033    /// Create a StyleRange from style IDs and a palette (zero-copy).
1034    ///
1035    /// This is more efficient than `from_sparse` as it avoids cloning styles.
1036    ///
1037    /// - `cells`: Vec of (row, col, style_id) where style_id indexes into palette
1038    /// - `palette`: The shared palette of unique styles (taken ownership)
1039    pub fn from_style_ids(cells: Vec<(u32, u32, usize)>, palette: Vec<Style>) -> Self {
1040        if cells.is_empty() {
1041            return Self::empty();
1042        }
1043
1044        // Find bounds
1045        let mut row_start = u32::MAX;
1046        let mut row_end = 0;
1047        let mut col_start = u32::MAX;
1048        let mut col_end = 0;
1049        for (r, c, _) in &cells {
1050            row_start = row_start.min(*r);
1051            row_end = row_end.max(*r);
1052            col_start = col_start.min(*c);
1053            col_end = col_end.max(*c);
1054        }
1055
1056        let width = (col_end - col_start + 1) as usize;
1057        let height = (row_end - row_start + 1) as usize;
1058        let total_cells = (width * height) as u64;
1059
1060        // Create dense style ID array (temporary)
1061        let mut style_ids = vec![0u16; width * height];
1062
1063        for (r, c, style_id) in cells {
1064            let row = (r - row_start) as usize;
1065            let col = (c - col_start) as usize;
1066            let idx = row * width + col;
1067            // style_id is already an index, just need to fit in u16
1068            style_ids[idx] = style_id.min(u16::MAX as usize) as u16;
1069        }
1070
1071        // Compress into RLE runs
1072        let mut runs = Vec::new();
1073        if !style_ids.is_empty() {
1074            let mut current_style = style_ids[0];
1075            let mut count = 1u32;
1076
1077            for &style_id in &style_ids[1..] {
1078                if style_id == current_style {
1079                    count += 1;
1080                } else {
1081                    runs.push(StyleRun {
1082                        style_id: current_style,
1083                        count,
1084                    });
1085                    current_style = style_id;
1086                    count = 1;
1087                }
1088            }
1089            runs.push(StyleRun {
1090                style_id: current_style,
1091                count,
1092            });
1093        }
1094
1095        runs.shrink_to_fit();
1096
1097        StyleRange {
1098            start: (row_start, col_start),
1099            end: (row_end, col_end),
1100            palette,
1101            runs,
1102            total_cells,
1103        }
1104    }
1105
1106    /// Create a StyleRange from sparse cell data
1107    ///
1108    /// Takes cells with positions and styles, compresses into RLE format.
1109    pub fn from_sparse(cells: Vec<(u32, u32, Style)>) -> Self {
1110        if cells.is_empty() {
1111            return Self::empty();
1112        }
1113
1114        // Find bounds
1115        let mut row_start = u32::MAX;
1116        let mut row_end = 0;
1117        let mut col_start = u32::MAX;
1118        let mut col_end = 0;
1119        for (r, c, _) in &cells {
1120            row_start = row_start.min(*r);
1121            row_end = row_end.max(*r);
1122            col_start = col_start.min(*c);
1123            col_end = col_end.max(*c);
1124        }
1125
1126        let width = (col_end - col_start + 1) as usize;
1127        let height = (row_end - row_start + 1) as usize;
1128        let total_cells = (width * height) as u64;
1129
1130        // Build palette and map styles to IDs
1131        // Use style_id from Excel if available, otherwise assign sequential IDs
1132        let mut palette: Vec<Style> = vec![Style::default()]; // Index 0 = empty/default
1133        let mut style_to_id: std::collections::HashMap<u32, u16> = std::collections::HashMap::new();
1134
1135        // Create dense style ID array (temporary)
1136        let mut style_ids = vec![0u16; width * height];
1137
1138        for (r, c, style) in cells {
1139            let row = (r - row_start) as usize;
1140            let col = (c - col_start) as usize;
1141            let idx = row * width + col;
1142
1143            if style.is_empty() {
1144                continue; // Leave as 0
1145            }
1146
1147            // Use Excel's style_id if available for deduplication
1148            // This groups cells with the same formatting together
1149            let excel_style_id = style.style_id.unwrap_or_else(|| {
1150                // Fallback: use palette length as unique ID (no dedup for these)
1151                palette.len() as u32
1152            });
1153
1154            let style_id = if let Some(&id) = style_to_id.get(&excel_style_id) {
1155                id
1156            } else {
1157                let id = palette.len() as u16;
1158                palette.push(style);
1159                style_to_id.insert(excel_style_id, id);
1160                id
1161            };
1162
1163            style_ids[idx] = style_id;
1164        }
1165
1166        // Compress into RLE runs
1167        let mut runs = Vec::new();
1168        if !style_ids.is_empty() {
1169            let mut current_style = style_ids[0];
1170            let mut count = 1u32;
1171
1172            for &style_id in &style_ids[1..] {
1173                if style_id == current_style {
1174                    count += 1;
1175                } else {
1176                    runs.push(StyleRun {
1177                        style_id: current_style,
1178                        count,
1179                    });
1180                    current_style = style_id;
1181                    count = 1;
1182                }
1183            }
1184            // Push final run
1185            runs.push(StyleRun {
1186                style_id: current_style,
1187                count,
1188            });
1189        }
1190
1191        runs.shrink_to_fit();
1192        palette.shrink_to_fit();
1193
1194        StyleRange {
1195            start: (row_start, col_start),
1196            end: (row_end, col_end),
1197            palette,
1198            runs,
1199            total_cells,
1200        }
1201    }
1202
1203    /// Get the start position of the range
1204    pub fn start(&self) -> Option<(u32, u32)> {
1205        if self.is_empty() {
1206            None
1207        } else {
1208            Some(self.start)
1209        }
1210    }
1211
1212    /// Get the end position of the range
1213    pub fn end(&self) -> Option<(u32, u32)> {
1214        if self.is_empty() {
1215            None
1216        } else {
1217            Some(self.end)
1218        }
1219    }
1220
1221    /// Check if the range is empty
1222    pub fn is_empty(&self) -> bool {
1223        self.runs.is_empty()
1224    }
1225
1226    /// Get width of the range
1227    pub fn width(&self) -> usize {
1228        if self.is_empty() {
1229            0
1230        } else {
1231            (self.end.1 - self.start.1 + 1) as usize
1232        }
1233    }
1234
1235    /// Get height of the range
1236    pub fn height(&self) -> usize {
1237        if self.is_empty() {
1238            0
1239        } else {
1240            (self.end.0 - self.start.0 + 1) as usize
1241        }
1242    }
1243
1244    /// Get style at a position (relative to range start)
1245    ///
1246    /// Returns None if position is out of bounds, or reference to style.
1247    pub fn get(&self, pos: (usize, usize)) -> Option<&Style> {
1248        let width = self.width();
1249        let height = self.height();
1250
1251        if pos.0 >= height || pos.1 >= width {
1252            return None;
1253        }
1254
1255        let linear_idx = pos.0 * width + pos.1;
1256        let style_id = self.style_id_at(linear_idx)?;
1257        self.palette.get(style_id as usize)
1258    }
1259
1260    /// Get style ID at a linear index using binary search on runs
1261    fn style_id_at(&self, linear_idx: usize) -> Option<u16> {
1262        let mut offset = 0usize;
1263        for run in &self.runs {
1264            let run_end = offset + run.count as usize;
1265            if linear_idx < run_end {
1266                return Some(run.style_id);
1267            }
1268            offset = run_end;
1269        }
1270        None
1271    }
1272
1273    /// Iterate over all cells with their positions and styles
1274    pub fn cells(&self) -> StyleRangeCells<'_> {
1275        StyleRangeCells {
1276            range: self,
1277            run_idx: 0,
1278            run_offset: 0,
1279            linear_idx: 0,
1280        }
1281    }
1282
1283    /// Get number of unique styles (excluding empty)
1284    pub fn unique_style_count(&self) -> usize {
1285        self.palette.len().saturating_sub(1)
1286    }
1287
1288    /// Get number of RLE runs (for diagnostics)
1289    pub fn run_count(&self) -> usize {
1290        self.runs.len()
1291    }
1292
1293    /// Get compression ratio (cells / runs)
1294    pub fn compression_ratio(&self) -> f64 {
1295        if self.runs.is_empty() {
1296            0.0
1297        } else {
1298            self.total_cells as f64 / self.runs.len() as f64
1299        }
1300    }
1301}
1302
1303/// Iterator over cells in a StyleRange
1304pub struct StyleRangeCells<'a> {
1305    range: &'a StyleRange,
1306    run_idx: usize,
1307    run_offset: u32,
1308    linear_idx: u64,
1309}
1310
1311impl<'a> Iterator for StyleRangeCells<'a> {
1312    type Item = (usize, usize, &'a Style);
1313
1314    fn next(&mut self) -> Option<Self::Item> {
1315        if self.run_idx >= self.range.runs.len() {
1316            return None;
1317        }
1318
1319        let width = self.range.width();
1320        if width == 0 {
1321            return None;
1322        }
1323
1324        let row = (self.linear_idx / width as u64) as usize;
1325        let col = (self.linear_idx % width as u64) as usize;
1326
1327        let run = &self.range.runs[self.run_idx];
1328        let style = self.range.palette.get(run.style_id as usize)?;
1329
1330        self.linear_idx += 1;
1331        self.run_offset += 1;
1332
1333        if self.run_offset >= run.count {
1334            self.run_idx += 1;
1335            self.run_offset = 0;
1336        }
1337
1338        Some((row, col, style))
1339    }
1340}
1341
1342#[cfg(test)]
1343mod tests {
1344    use super::*;
1345
1346    #[test]
1347    fn test_color() {
1348        let color = Color::rgb(255, 0, 128);
1349        assert_eq!(color.red, 255);
1350        assert_eq!(color.green, 0);
1351        assert_eq!(color.blue, 128);
1352        assert_eq!(color.alpha, 255);
1353        assert_eq!(color.to_string(), "#FF0080");
1354    }
1355
1356    #[test]
1357    fn test_font() {
1358        let font = Font::new()
1359            .with_name("Arial".to_string())
1360            .with_size(12.0)
1361            .with_weight(FontWeight::Bold)
1362            .with_color(Color::rgb(255, 0, 0));
1363
1364        assert_eq!(font.name, Some("Arial".to_string()));
1365        assert_eq!(font.size, Some(12.0));
1366        assert!(font.is_bold());
1367        assert_eq!(font.color, Some(Color::rgb(255, 0, 0)));
1368    }
1369
1370    #[test]
1371    fn test_style() {
1372        let style = Style::new()
1373            .with_font(Font::new().with_name("Arial".to_string()))
1374            .with_fill(Fill::solid(Color::rgb(255, 255, 0)));
1375
1376        assert!(!style.is_empty());
1377        assert!(style.get_font().is_some());
1378        assert!(style.get_fill().is_some());
1379    }
1380
1381    #[test]
1382    fn test_border_with_color() {
1383        let border = Border::with_color(BorderStyle::Thin, Color::rgb(255, 0, 0));
1384        assert_eq!(border.style, BorderStyle::Thin);
1385        assert_eq!(border.color, Some(Color::rgb(255, 0, 0)));
1386        assert!(border.is_visible());
1387    }
1388
1389    #[test]
1390    fn test_border_without_color() {
1391        let border = Border::new(BorderStyle::Medium);
1392        assert_eq!(border.style, BorderStyle::Medium);
1393        assert_eq!(border.color, None);
1394        assert!(border.is_visible());
1395    }
1396
1397    #[test]
1398    fn test_borders_with_mixed_colors() {
1399        let mut borders = Borders::new();
1400        borders.left = Border::with_color(BorderStyle::Thin, Color::rgb(255, 0, 0));
1401        borders.right = Border::new(BorderStyle::Medium);
1402        borders.top = Border::with_color(BorderStyle::Thick, Color::rgb(0, 255, 0));
1403
1404        assert_eq!(borders.left.color, Some(Color::rgb(255, 0, 0)));
1405        assert_eq!(borders.right.color, None);
1406        assert_eq!(borders.top.color, Some(Color::rgb(0, 255, 0)));
1407        assert!(borders.has_visible_borders());
1408    }
1409
1410    #[test]
1411    fn test_column_width() {
1412        let column_width = ColumnWidth::new(5, 12.5)
1413            .with_custom_width(true)
1414            .with_hidden(false)
1415            .with_best_fit(true);
1416
1417        assert_eq!(column_width.column, 5);
1418        assert_eq!(column_width.width, 12.5);
1419        assert!(column_width.custom_width);
1420        assert!(!column_width.hidden);
1421        assert!(column_width.best_fit);
1422        assert!(column_width.is_visible());
1423    }
1424
1425    #[test]
1426    fn test_row_height() {
1427        let row_height = RowHeight::new(10, 20.0)
1428            .with_custom_height(true)
1429            .with_hidden(false)
1430            .with_thick_top(true)
1431            .with_thick_bottom(false);
1432
1433        assert_eq!(row_height.row, 10);
1434        assert_eq!(row_height.height, 20.0);
1435        assert!(row_height.custom_height);
1436        assert!(!row_height.hidden);
1437        assert!(row_height.thick_top);
1438        assert!(!row_height.thick_bottom);
1439        assert!(row_height.is_visible());
1440    }
1441
1442    #[test]
1443    fn test_worksheet_layout() {
1444        let layout = WorksheetLayout::new()
1445            .add_column_width(ColumnWidth::new(0, 10.0))
1446            .add_column_width(ColumnWidth::new(1, 15.0))
1447            .add_row_height(RowHeight::new(0, 18.0))
1448            .add_row_height(RowHeight::new(1, 22.0))
1449            .with_default_column_width(8.43)
1450            .with_default_row_height(15.0);
1451
1452        assert_eq!(layout.column_widths.len(), 2);
1453        assert_eq!(layout.row_heights.len(), 2);
1454        assert_eq!(layout.default_column_width, Some(8.43));
1455        assert_eq!(layout.default_row_height, Some(15.0));
1456        assert!(layout.has_custom_dimensions());
1457
1458        // Test getting specific column width
1459        let col_width = layout.get_column_width(0).unwrap();
1460        assert_eq!(col_width.width, 10.0);
1461
1462        // Test getting specific row height
1463        let row_height = layout.get_row_height(1).unwrap();
1464        assert_eq!(row_height.height, 22.0);
1465
1466        // Test effective widths/heights
1467        assert_eq!(layout.get_effective_column_width(0), 10.0); // Custom width
1468        assert_eq!(layout.get_effective_column_width(5), 8.43); // Default width
1469        assert_eq!(layout.get_effective_row_height(0), 18.0); // Custom height
1470        assert_eq!(layout.get_effective_row_height(5), 15.0); // Default height
1471    }
1472
1473    #[test]
1474    fn test_worksheet_layout_defaults() {
1475        let layout = WorksheetLayout::new();
1476
1477        assert!(!layout.has_custom_dimensions());
1478        assert_eq!(layout.get_effective_column_width(0), 8.43); // Excel default
1479        assert_eq!(layout.get_effective_row_height(0), 15.0); // Excel default
1480    }
1481}