1use 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
15const MAX_CELL_XFS: usize = 65430;
17
18const CUSTOM_NUM_FMT_BASE: u32 = 164;
20
21pub mod builtin_num_fmts {
23 pub const GENERAL: u32 = 0;
25 pub const INTEGER: u32 = 1;
27 pub const DECIMAL_2: u32 = 2;
29 pub const THOUSANDS: u32 = 3;
31 pub const THOUSANDS_DECIMAL: u32 = 4;
33 pub const PERCENT: u32 = 9;
35 pub const PERCENT_DECIMAL: u32 = 10;
37 pub const SCIENTIFIC: u32 = 11;
39 pub const DATE_MDY: u32 = 14;
41 pub const DATE_DMY: u32 = 15;
43 pub const DATE_DM: u32 = 16;
45 pub const DATE_MY: u32 = 17;
47 pub const TIME_HM_AP: u32 = 18;
49 pub const TIME_HMS_AP: u32 = 19;
51 pub const TIME_HM: u32 = 20;
53 pub const TIME_HMS: u32 = 21;
55 pub const DATETIME: u32 = 22;
57 pub const TEXT: u32 = 49;
59}
60
61#[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#[derive(Debug, Clone, Default)]
74pub struct FontStyle {
75 pub name: Option<String>,
77 pub size: Option<f64>,
79 pub bold: bool,
81 pub italic: bool,
83 pub underline: bool,
85 pub strikethrough: bool,
87 pub color: Option<StyleColor>,
89}
90
91#[derive(Debug, Clone, PartialEq)]
93pub enum StyleColor {
94 Rgb(String),
96 Theme(u32),
98 Indexed(u32),
100}
101
102#[derive(Debug, Clone)]
104pub struct FillStyle {
105 pub pattern: PatternType,
107 pub fg_color: Option<StyleColor>,
109 pub bg_color: Option<StyleColor>,
111 pub gradient: Option<GradientFillStyle>,
113}
114
115#[derive(Debug, Clone)]
117pub struct GradientFillStyle {
118 pub gradient_type: GradientType,
120 pub degree: Option<f64>,
122 pub left: Option<f64>,
124 pub right: Option<f64>,
126 pub top: Option<f64>,
128 pub bottom: Option<f64>,
130 pub stops: Vec<GradientStop>,
132}
133
134#[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#[derive(Debug, Clone)]
152pub struct GradientStop {
153 pub position: f64,
155 pub color: StyleColor,
157}
158
159#[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#[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#[derive(Debug, Clone)]
207pub struct BorderSideStyle {
208 pub style: BorderLineStyle,
209 pub color: Option<StyleColor>,
210}
211
212#[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#[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#[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#[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#[derive(Debug, Clone)]
357pub enum NumFmtStyle {
358 Builtin(u32),
360 Custom(String),
362}
363
364#[derive(Debug, Clone)]
366pub struct ProtectionStyle {
367 pub locked: bool,
368 pub hidden: bool,
369}
370
371pub struct StyleBuilder {
376 style: Style,
377}
378
379impl StyleBuilder {
380 pub fn new() -> Self {
382 Self {
383 style: Style::default(),
384 }
385 }
386
387 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 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 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 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 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 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 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 pub fn font_color_rgb(self, rgb: &str) -> Self {
442 self.font_color(StyleColor::Rgb(rgb.to_string()))
443 }
444
445 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 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 pub fn fill_fg_color_rgb(self, rgb: &str) -> Self {
477 self.fill_fg_color(StyleColor::Rgb(rgb.to_string()))
478 }
479
480 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn num_format_builtin(mut self, id: u32) -> Self {
629 self.style.num_fmt = Some(NumFmtStyle::Builtin(id));
630 self
631 }
632
633 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 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 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 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
679fn 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
706fn 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
717fn 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
750fn 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
763fn 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
781fn 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
804fn 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
851fn 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
859fn 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
869fn 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
882fn 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
893fn 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
909fn 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
927fn protection_style_to_xml(prot: &ProtectionStyle) -> Protection {
929 Protection {
930 locked: Some(prot.locked),
931 hidden: Some(prot.hidden),
932 }
933}
934
935fn xml_protection_to_style(prot: &Protection) -> ProtectionStyle {
937 ProtectionStyle {
938 locked: prot.locked.unwrap_or(true), hidden: prot.hidden.unwrap_or(false),
940 }
941}
942
943fn 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
954fn fills_equal(a: &Fill, b: &Fill) -> bool {
956 a.pattern_fill == b.pattern_fill && a.gradient_fill == b.gradient_fill
957}
958
959fn 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
968fn 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
978fn 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
995fn 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
1012fn 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
1029fn 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
1062pub 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, };
1073
1074 let fill_id = match &style.fill {
1075 Some(fill) => add_or_find_fill(&mut stylesheet.fills, fill),
1076 None => 0, };
1078
1079 let border_id = match &style.border {
1080 Some(border) => add_or_find_border(&mut stylesheet.borders, border),
1081 None => 0, };
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, };
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
1125pub 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 if id < CUSTOM_NUM_FMT_BASE {
1152 Some(NumFmtStyle::Builtin(id))
1153 } else {
1154 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 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 assert_eq!(id, 1);
1198 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 assert_eq!(ss.fonts.fonts.len(), 2);
1219 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 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 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 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 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 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 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 #[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 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 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 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}