Skip to main content

ironcalc_base/
types.rs

1use bitcode::{Decode, Encode};
2use serde::{Deserialize, Serialize};
3use std::{collections::HashMap, fmt::Display};
4
5use crate::{cf_types::ConditionalFormatting, expressions::token::Error};
6
7fn default_as_false() -> bool {
8    false
9}
10
11fn is_false(b: &bool) -> bool {
12    !*b
13}
14
15#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
16#[serde(untagged)]
17pub enum Color {
18    Rgb(String),
19    /// Theme slot index and tint. Tint ∈ [-1, 1]: positive lightens, negative darkens.
20    Theme(i32, f64),
21    /// No color — equivalent to OOXML `<color auto="1"/>` or absence of `<color>`.
22    #[default]
23    None,
24}
25
26/// Valid hex colors are #FFAABB
27/// #fff is not valid
28fn is_valid_hex_color(color: &str) -> bool {
29    if color.chars().count() != 7 {
30        return false;
31    }
32    if !color.starts_with('#') {
33        return false;
34    }
35    if let Ok(z) = i32::from_str_radix(&color[1..], 16) {
36        if (0..=0xffffff).contains(&z) {
37            return true;
38        }
39    }
40    false
41}
42
43impl Color {
44    pub fn is_none(&self) -> bool {
45        matches!(self, Color::None)
46    }
47
48    pub fn is_some(&self) -> bool {
49        !matches!(self, Color::None)
50    }
51
52    /// Resolves the color to a `#RRGGBB` string, consulting the workbook theme when needed.
53    /// Returns an empty string for `Color::None`.
54    pub fn to_rgb(&self, theme: &Theme) -> String {
55        match self {
56            Color::Rgb(s) => s.clone(),
57            Color::Theme(idx, tint) => theme.resolve(*idx, *tint),
58            Color::None => String::new(),
59        }
60    }
61
62    pub fn from_rgb(color: &str) -> Result<Self, String> {
63        if is_valid_hex_color(color) {
64            return Ok(Color::Rgb(color.to_string()));
65        }
66        Err(format!("Invalid color: '{}'.", color))
67    }
68
69    /// Parses a color from the JS/WASM parameter format:
70    /// - `""` => `Color::None`
71    /// - `"#RRGGBB"` => `Color::Rgb(...)`
72    /// - `"[index, tint]"` => `Color::Theme(index, tint)`
73    pub fn from_param(s: &str) -> Result<Self, String> {
74        if s.is_empty() {
75            return Ok(Color::None);
76        }
77        if is_valid_hex_color(s) {
78            return Ok(Color::Rgb(s.to_string()));
79        }
80        if let Some(inner) = s.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
81            let mut parts = inner.splitn(2, ',');
82            if let (Some(idx_str), Some(tint_str)) = (parts.next(), parts.next()) {
83                if let (Ok(idx), Ok(tint)) = (
84                    idx_str.trim().parse::<i32>(),
85                    tint_str.trim().parse::<f64>(),
86                ) {
87                    return Ok(Color::Theme(idx, tint));
88                }
89            }
90        }
91        Err(format!("Invalid color: '{}'.", s))
92    }
93}
94
95#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
96pub struct Metadata {
97    pub application: String,
98    pub app_version: String,
99    pub creator: String,
100    pub last_modified_by: String,
101    pub created: String,       // "2020-08-06T21:20:53Z",
102    pub last_modified: String, //"2020-11-20T16:24:35"
103}
104
105#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
106pub struct WorkbookSettings {
107    pub tz: String,
108    pub locale: String,
109}
110
111/// A Workbook View tracks of the selected sheet for each view
112#[derive(Encode, Decode, Debug, PartialEq, Clone)]
113pub struct WorkbookView {
114    /// The index of the currently selected sheet.
115    pub sheet: u32,
116    /// The current width of the window
117    pub window_width: i64,
118    /// The current height of the window
119    pub window_height: i64,
120}
121
122/// An internal representation of an IronCalc Workbook
123#[derive(Encode, Decode, Debug, PartialEq, Clone)]
124pub struct Workbook {
125    pub shared_strings: Vec<String>,
126    pub defined_names: Vec<DefinedName>,
127    pub worksheets: Vec<Worksheet>,
128    pub styles: Styles,
129    pub name: String,
130    pub settings: WorkbookSettings,
131    pub metadata: Metadata,
132    pub tables: HashMap<String, Table>,
133    pub views: HashMap<u32, WorkbookView>,
134    pub theme: Theme,
135}
136
137/// A defined name. The `sheet_id` is the sheet index in case the name is local
138#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
139pub struct DefinedName {
140    pub name: String,
141    pub formula: String,
142    pub sheet_id: Option<u32>,
143}
144
145/// * state:
146///   18.18.68 ST_SheetState (Sheet Visibility Types)
147///   hidden, veryHidden, visible
148#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
149pub enum SheetState {
150    Visible,
151    Hidden,
152    VeryHidden,
153}
154
155impl Display for SheetState {
156    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
157        match self {
158            SheetState::Visible => write!(formatter, "visible"),
159            SheetState::Hidden => write!(formatter, "hidden"),
160            SheetState::VeryHidden => write!(formatter, "veryHidden"),
161        }
162    }
163}
164
165/// Represents the state of the worksheet as seen by the user. This includes
166/// details such as the currently selected cell, the visible range, and the
167/// position of the viewport.
168#[derive(Encode, Decode, Debug, PartialEq, Clone)]
169pub struct WorksheetView {
170    /// The row index of the currently selected cell.
171    pub row: i32,
172    /// The column index of the currently selected cell.
173    pub column: i32,
174    /// The selected range in the worksheet, specified as [start_row, start_column, end_row, end_column].
175    pub range: [i32; 4],
176    /// The row index of the topmost visible cell in the worksheet view.
177    pub top_row: i32,
178    /// The column index of the leftmost visible cell in the worksheet view.
179    pub left_column: i32,
180}
181
182/// Internal representation of a worksheet Excel object
183#[derive(Encode, Decode, Debug, PartialEq, Clone)]
184pub struct Worksheet {
185    pub dimension: String,
186    pub cols: Vec<Col>,
187    pub rows: Vec<Row>,
188    pub name: String,
189    pub sheet_data: SheetData,
190    pub shared_formulas: Vec<String>,
191    pub sheet_id: u32,
192    pub state: SheetState,
193    pub color: Color,
194    pub merge_cells: Vec<String>,
195    pub comments: Vec<Comment>,
196    pub frozen_rows: i32,
197    pub frozen_columns: i32,
198    pub views: HashMap<u32, WorksheetView>,
199    /// Whether or not to show the grid lines in the worksheet
200    pub show_grid_lines: bool,
201    pub conditional_formatting: Vec<ConditionalFormatting>,
202}
203
204/// Internal representation of Excel's sheet_data
205/// It is row first and because of this all of our API's should be row first
206pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
207
208// ECMA-376-1:2016 section 18.3.1.73
209#[derive(Encode, Decode, Debug, PartialEq, Clone)]
210pub struct Row {
211    /// Row index
212    pub r: i32,
213    pub height: f64,
214    pub custom_format: bool,
215    pub custom_height: bool,
216    pub s: i32,
217    pub hidden: bool,
218}
219
220// ECMA-376-1:2016 section 18.3.1.13
221#[derive(Encode, Decode, Debug, PartialEq, Clone)]
222pub struct Col {
223    // Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
224    /// First column affected by this record. Settings apply to column in \[min, max\] range.
225    pub min: i32,
226    /// Last column affected by this record. Settings apply to column in \[min, max\] range.
227    pub max: i32,
228    pub width: f64,
229    pub custom_width: bool,
230    pub hidden: bool,
231    pub style: Option<i32>,
232}
233
234/// Cell type enum matching Excel TYPE() function values.
235#[derive(Debug, Eq, PartialEq)]
236pub enum CellType {
237    Number = 1,
238    Text = 2,
239    LogicalValue = 4,
240    ErrorValue = 16,
241    Array = 64,
242    CompoundData = 128,
243}
244
245/// The evaluated value stored in a formula cell.
246/// `Unevaluated` is a transient state that only exists during evaluation.
247#[derive(Encode, Decode, Debug, Clone, PartialEq)]
248pub enum FormulaValue {
249    Unevaluated,
250    Boolean(bool),
251    Number(f64),
252    Text(String),
253    Error {
254        ei: Error,
255        // Origin cell reference, e.g. "Sheet3!C4"
256        o: String,
257        // Human-readable error message, e.g. "Not implemented function"
258        m: String,
259    },
260}
261
262/// The value stored in a spill cell (no formula, no origin tracking).
263#[derive(Encode, Decode, Debug, Clone, PartialEq)]
264pub enum SpillValue {
265    Boolean(bool),
266    Number(f64),
267    Text(String),
268    Error(Error),
269}
270
271/// Whether an array formula is a CSE (Ctrl+Shift+Enter) formula or a dynamic formula.
272#[derive(Encode, Decode, Debug, Clone, PartialEq)]
273pub enum ArrayKind {
274    /// Ctrl+Shift+Enter array formula: fills a fixed declared range.
275    Cse,
276    /// Dynamic array formula: spills into adjacent cells automatically.
277    Dynamic,
278}
279
280// A cell in a worksheet.
281// Every cell has a style index (s) pointing to cell_xfs in the workbook styles.
282// Other fields:
283// * `f`    — formula index into the sheet's shared_formulas list
284// * `si`   — shared string index (SharedString cells only)
285// * `v`    — evaluated value (formula/spill cells)
286// * `r`    — spill range (width, height) for array/dynamic formula anchors
287// * `kind` — Cse or Dynamic for array formula anchors
288// * `a`    — anchor cell (row, column) for spill cells
289#[derive(Encode, Decode, Debug, Clone, PartialEq)]
290pub enum Cell {
291    EmptyCell {
292        s: i32,
293    },
294    BooleanCell {
295        v: bool,
296        s: i32,
297    },
298    NumberCell {
299        v: f64,
300        s: i32,
301    },
302    // Maybe we should not have this type. In Excel this is just a string
303    ErrorCell {
304        ei: Error,
305        s: i32,
306    },
307    // Always a shared string
308    SharedString {
309        si: i32,
310        s: i32,
311    },
312    // A regular (non-array) formula cell.
313    // `v` is `Unevaluated` transiently during evaluation, then holds the result.
314    CellFormula {
315        f: i32,
316        s: i32,
317        v: FormulaValue,
318    },
319    // The anchor of an array or dynamic formula.
320    // `kind` distinguishes CSE from dynamic; `r` is the spill range (width, height).
321    // `v` is `Unevaluated` transiently during evaluation, then holds the anchor cell result.
322    ArrayFormula {
323        f: i32,
324        s: i32,
325        r: (i32, i32),
326        kind: ArrayKind,
327        v: FormulaValue,
328    },
329    // A spill cell: holds a value produced by an array/dynamic formula at `a` (row, column).
330    SpillCell {
331        s: i32,
332        a: (i32, i32),
333        v: SpillValue,
334    },
335}
336
337impl Default for Cell {
338    fn default() -> Self {
339        Cell::EmptyCell { s: 0 }
340    }
341}
342
343#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
344pub struct Comment {
345    pub text: String,
346    pub author_name: String,
347    pub author_id: Option<String>,
348    pub cell_ref: String,
349}
350
351// ECMA-376-1:2016 section 18.5.1.2
352#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
353pub struct Table {
354    pub name: String,
355    pub display_name: String,
356    pub sheet_name: String,
357    pub reference: String,
358    pub totals_row_count: u32,
359    pub header_row_count: u32,
360    pub header_row_dxf_id: Option<u32>,
361    pub data_dxf_id: Option<u32>,
362    pub totals_row_dxf_id: Option<u32>,
363    pub columns: Vec<TableColumn>,
364    pub style_info: TableStyleInfo,
365    pub has_filters: bool,
366}
367
368// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
369// the totals_row_function is an enum not String methinks
370#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
371pub struct TableColumn {
372    pub id: u32,
373    pub name: String,
374    pub totals_row_label: Option<String>,
375    pub header_row_dxf_id: Option<u32>,
376    pub data_dxf_id: Option<u32>,
377    pub totals_row_dxf_id: Option<u32>,
378    pub totals_row_function: Option<String>,
379}
380
381impl Default for TableColumn {
382    fn default() -> Self {
383        TableColumn {
384            id: 0,
385            name: "Column".to_string(),
386            totals_row_label: None,
387            totals_row_function: None,
388            data_dxf_id: None,
389            header_row_dxf_id: None,
390            totals_row_dxf_id: None,
391        }
392    }
393}
394
395#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
396pub struct TableStyleInfo {
397    pub name: Option<String>,
398    pub show_first_column: bool,
399    pub show_last_column: bool,
400    pub show_row_stripes: bool,
401    pub show_column_stripes: bool,
402}
403
404#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
405pub struct DxfFont {
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub strike: Option<bool>,
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub u: Option<bool>,
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub b: Option<bool>,
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub i: Option<bool>,
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub sz: Option<i32>,
416    #[serde(skip_serializing_if = "Color::is_none")]
417    #[serde(default)]
418    pub color: Color,
419}
420
421// Dxf stands for "Differential Formatting". It is used in places like:
422// * conditional formatting
423// * tables
424// to specify partial formatting that overrides the cell formatting.
425#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
426pub struct Dxf {
427    pub font: Option<DxfFont>,
428    pub fill: Option<Fill>,
429    pub border: Option<Border>,
430    pub num_fmt: Option<NumFmt>,
431    pub alignment: Option<Alignment>,
432}
433
434#[derive(Encode, Decode, Debug, PartialEq, Clone)]
435pub struct Styles {
436    pub num_fmts: Vec<NumFmt>,
437    pub fonts: Vec<Font>,
438    pub fills: Vec<Fill>,
439    pub borders: Vec<Border>,
440    pub cell_style_xfs: Vec<CellStyleXfs>,
441    pub cell_xfs: Vec<CellXfs>,
442    pub cell_styles: Vec<CellStyles>,
443    pub dxfs: Vec<Dxf>,
444}
445
446impl Default for Styles {
447    fn default() -> Self {
448        Styles {
449            num_fmts: vec![],
450            fonts: vec![Default::default()],
451            fills: vec![Default::default(), Default::default()],
452            borders: vec![Default::default()],
453            cell_style_xfs: vec![Default::default()],
454            cell_xfs: vec![Default::default()],
455            cell_styles: vec![Default::default()],
456            dxfs: vec![],
457        }
458    }
459}
460
461#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
462pub struct Style {
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub alignment: Option<Alignment>,
465    pub num_fmt: String,
466    pub fill: Fill,
467    pub font: Font,
468    pub border: Border,
469    pub quote_prefix: bool,
470}
471
472impl Default for Style {
473    fn default() -> Self {
474        Style {
475            alignment: None,
476            num_fmt: "general".to_string(),
477            fill: Fill::default(),
478            font: Font::default(),
479            border: Border::default(),
480            quote_prefix: false,
481        }
482    }
483}
484
485#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
486pub struct NumFmt {
487    pub num_fmt_id: i32,
488    pub format_code: String,
489}
490
491impl Default for NumFmt {
492    fn default() -> Self {
493        NumFmt {
494            num_fmt_id: 0,
495            format_code: "general".to_string(),
496        }
497    }
498}
499
500// ST_FontScheme simple type (§18.18.33).
501// Usually major fonts are used for styles like headings,
502// and minor fonts are used for body and paragraph text.
503#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
504#[serde(rename_all = "lowercase")]
505#[derive(Default)]
506pub enum FontScheme {
507    #[default]
508    Minor,
509    Major,
510    None,
511}
512
513impl Display for FontScheme {
514    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
515        match self {
516            FontScheme::Minor => write!(formatter, "minor"),
517            FontScheme::Major => write!(formatter, "major"),
518            FontScheme::None => write!(formatter, "none"),
519        }
520    }
521}
522
523#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
524pub struct Font {
525    #[serde(default = "default_as_false")]
526    #[serde(skip_serializing_if = "is_false")]
527    pub strike: bool,
528    #[serde(default = "default_as_false")]
529    #[serde(skip_serializing_if = "is_false")]
530    pub u: bool, // seems that Excel supports a bit more - double underline / account underline etc.
531    #[serde(default = "default_as_false")]
532    #[serde(skip_serializing_if = "is_false")]
533    pub b: bool,
534    #[serde(default = "default_as_false")]
535    #[serde(skip_serializing_if = "is_false")]
536    pub i: bool,
537    pub sz: i32,
538    #[serde(skip_serializing_if = "Color::is_none")]
539    #[serde(default)]
540    pub color: Color,
541    pub name: String,
542    // This is the font family fallback
543    // 1 -> serif
544    // 2 -> sans serif
545    // 3 -> monospaced
546    // ...
547    pub family: i32,
548    pub scheme: FontScheme,
549}
550
551impl Default for Font {
552    fn default() -> Self {
553        Font {
554            strike: false,
555            u: false,
556            b: false,
557            i: false,
558            sz: 12,
559            color: Color::None,
560            name: "Inter".to_string(),
561            family: 2,
562            scheme: FontScheme::Minor,
563        }
564    }
565}
566
567#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
568pub struct Fill {
569    #[serde(skip_serializing_if = "Color::is_none")]
570    #[serde(default)]
571    pub color: Color,
572}
573
574#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
575#[serde(rename_all = "lowercase")]
576#[derive(Default)]
577pub enum HorizontalAlignment {
578    Center,
579    CenterContinuous,
580    Distributed,
581    Fill,
582    #[default]
583    General,
584    Justify,
585    Left,
586    Right,
587}
588
589// Note that alignment in "General" depends on type
590
591impl HorizontalAlignment {
592    fn is_default(&self) -> bool {
593        self == &HorizontalAlignment::default()
594    }
595}
596
597// FIXME: Is there a way to generate this automatically?
598impl Display for HorizontalAlignment {
599    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
600        match self {
601            HorizontalAlignment::Center => write!(formatter, "center"),
602            HorizontalAlignment::CenterContinuous => write!(formatter, "centerContinuous"),
603            HorizontalAlignment::Distributed => write!(formatter, "distributed"),
604            HorizontalAlignment::Fill => write!(formatter, "fill"),
605            HorizontalAlignment::General => write!(formatter, "general"),
606            HorizontalAlignment::Justify => write!(formatter, "justify"),
607            HorizontalAlignment::Left => write!(formatter, "left"),
608            HorizontalAlignment::Right => write!(formatter, "right"),
609        }
610    }
611}
612
613#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
614#[serde(rename_all = "lowercase")]
615#[derive(Default)]
616pub enum VerticalAlignment {
617    #[default]
618    Bottom,
619    Center,
620    Distributed,
621    Justify,
622    Top,
623}
624
625impl VerticalAlignment {
626    fn is_default(&self) -> bool {
627        self == &VerticalAlignment::default()
628    }
629}
630
631impl Display for VerticalAlignment {
632    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
633        match self {
634            VerticalAlignment::Bottom => write!(formatter, "bottom"),
635            VerticalAlignment::Center => write!(formatter, "center"),
636            VerticalAlignment::Distributed => write!(formatter, "distributed"),
637            VerticalAlignment::Justify => write!(formatter, "justify"),
638            VerticalAlignment::Top => write!(formatter, "top"),
639        }
640    }
641}
642
643// 1762
644#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
645pub struct Alignment {
646    #[serde(default)]
647    #[serde(skip_serializing_if = "HorizontalAlignment::is_default")]
648    pub horizontal: HorizontalAlignment,
649    #[serde(skip_serializing_if = "VerticalAlignment::is_default")]
650    #[serde(default)]
651    pub vertical: VerticalAlignment,
652    #[serde(default = "default_as_false")]
653    #[serde(skip_serializing_if = "is_false")]
654    pub wrap_text: bool,
655}
656
657#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
658pub struct CellStyleXfs {
659    pub num_fmt_id: i32,
660    pub font_id: i32,
661    pub fill_id: i32,
662    pub border_id: i32,
663    pub apply_number_format: bool,
664    pub apply_border: bool,
665    pub apply_alignment: bool,
666    pub apply_protection: bool,
667    pub apply_font: bool,
668    pub apply_fill: bool,
669}
670
671impl Default for CellStyleXfs {
672    fn default() -> Self {
673        CellStyleXfs {
674            num_fmt_id: 0,
675            font_id: 0,
676            fill_id: 0,
677            border_id: 0,
678            apply_number_format: true,
679            apply_border: true,
680            apply_alignment: true,
681            apply_protection: true,
682            apply_font: true,
683            apply_fill: true,
684        }
685    }
686}
687
688/// The formatting categories a named style includes — Excel's "Style Includes"
689/// checkboxes, stored as the `apply*` attributes of the style's `cellStyleXfs`
690/// record. Applying the style to a cell only stamps the included categories.
691/// The default (like "Normal") includes everything; the built-in "Percent",
692/// for example, includes only the number format.
693#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Copy)]
694#[serde(default)]
695pub struct StyleIncludes {
696    pub number_format: bool,
697    pub font: bool,
698    pub fill: bool,
699    pub border: bool,
700    pub alignment: bool,
701    pub protection: bool,
702}
703
704impl Default for StyleIncludes {
705    fn default() -> Self {
706        StyleIncludes {
707            number_format: true,
708            font: true,
709            fill: true,
710            border: true,
711            alignment: true,
712            protection: true,
713        }
714    }
715}
716
717#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
718pub struct CellXfs {
719    pub xf_id: i32,
720    pub num_fmt_id: i32,
721    pub font_id: i32,
722    pub fill_id: i32,
723    pub border_id: i32,
724    pub apply_number_format: bool,
725    pub apply_border: bool,
726    pub apply_alignment: bool,
727    pub apply_protection: bool,
728    pub apply_font: bool,
729    pub apply_fill: bool,
730    pub quote_prefix: bool,
731    pub alignment: Option<Alignment>,
732}
733
734#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
735pub struct CellStyles {
736    pub name: String,
737    pub xf_id: i32,
738    pub builtin_id: i32,
739}
740
741impl Default for CellStyles {
742    fn default() -> Self {
743        CellStyles {
744            name: "normal".to_string(),
745            xf_id: 0,
746            builtin_id: 0,
747        }
748    }
749}
750
751#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Clone)]
752#[serde(rename_all = "lowercase")]
753pub enum BorderStyle {
754    Thin,
755    Medium,
756    Thick,
757    Double,
758    Dotted,
759    SlantDashDot,
760    MediumDashed,
761    MediumDashDotDot,
762    MediumDashDot,
763}
764
765impl Display for BorderStyle {
766    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
767        match self {
768            BorderStyle::Thin => write!(formatter, "thin"),
769            BorderStyle::Thick => write!(formatter, "thick"),
770            BorderStyle::SlantDashDot => write!(formatter, "slantdashdot"),
771            BorderStyle::MediumDashed => write!(formatter, "mediumdashed"),
772            BorderStyle::MediumDashDotDot => write!(formatter, "mediumdashdotdot"),
773            BorderStyle::MediumDashDot => write!(formatter, "mediumdashdot"),
774            BorderStyle::Medium => write!(formatter, "medium"),
775            BorderStyle::Double => write!(formatter, "double"),
776            BorderStyle::Dotted => write!(formatter, "dotted"),
777        }
778    }
779}
780
781#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
782pub struct BorderItem {
783    pub style: BorderStyle,
784    #[serde(skip_serializing_if = "Color::is_none")]
785    #[serde(default)]
786    pub color: Color,
787}
788
789#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
790pub struct Border {
791    #[serde(default = "default_as_false")]
792    #[serde(skip_serializing_if = "is_false")]
793    pub diagonal_up: bool,
794    #[serde(default = "default_as_false")]
795    #[serde(skip_serializing_if = "is_false")]
796    pub diagonal_down: bool,
797    #[serde(skip_serializing_if = "Option::is_none")]
798    pub left: Option<BorderItem>,
799    #[serde(skip_serializing_if = "Option::is_none")]
800    pub right: Option<BorderItem>,
801    #[serde(skip_serializing_if = "Option::is_none")]
802    pub top: Option<BorderItem>,
803    #[serde(skip_serializing_if = "Option::is_none")]
804    pub bottom: Option<BorderItem>,
805    #[serde(skip_serializing_if = "Option::is_none")]
806    pub diagonal: Option<BorderItem>,
807}
808
809/// Information need to show a sheet tab in the UI
810/// The color is serialized only if it is not Color::None
811#[derive(Serialize, Deserialize, Debug, PartialEq)]
812pub struct SheetProperties {
813    pub name: String,
814    pub state: String,
815    pub sheet_id: u32,
816    #[serde(skip_serializing_if = "Color::is_none")]
817    #[serde(default)]
818    pub color: Color,
819}
820
821#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
822pub struct Theme {
823    pub name: String,
824    pub dk1: String,
825    pub lt1: String,
826    pub dk2: String,
827    pub lt2: String,
828    pub accent1: String,
829    pub accent2: String,
830    pub accent3: String,
831    pub accent4: String,
832    pub accent5: String,
833    pub accent6: String,
834    pub hlink: String,
835    pub fol_hlink: String,
836}
837
838impl Default for Theme {
839    fn default() -> Self {
840        Theme {
841            name: "Office".to_string(),
842            dk1: "#000000".to_string(),
843            lt1: "#FFFFFF".to_string(),
844            dk2: "#44546A".to_string(),
845            lt2: "#E7E6E6".to_string(),
846            accent1: "#4472C4".to_string(),
847            accent2: "#ED7D31".to_string(),
848            accent3: "#A5A5A5".to_string(),
849            accent4: "#FFC000".to_string(),
850            accent5: "#5B9BD5".to_string(),
851            accent6: "#70AD47".to_string(),
852            hlink: "#0563C1".to_string(),
853            fol_hlink: "#954F72".to_string(),
854        }
855    }
856}
857
858impl Theme {
859    /// Resolves a `theme="N"` attribute (and optional `tint`) to an `#RRGGBB` string.
860    /// Applies the OOXML dk/lt swap for indices 0–3.
861    pub fn resolve(&self, theme_index: i32, tint: f64) -> String {
862        use crate::colors::hex_with_tint_to_rgb;
863        let color = match theme_index {
864            0 => &self.lt1,
865            1 => &self.dk1,
866            2 => &self.lt2,
867            3 => &self.dk2,
868            4 => &self.accent1,
869            5 => &self.accent2,
870            6 => &self.accent3,
871            7 => &self.accent4,
872            8 => &self.accent5,
873            9 => &self.accent6,
874            10 => &self.hlink,
875            11 => &self.fol_hlink,
876            _ => &self.dk1,
877        };
878        hex_with_tint_to_rgb(color, tint)
879    }
880}
881
882#[cfg(test)]
883mod test {
884    use super::*;
885    #[test]
886    fn test_is_valid_hex_color() {
887        assert!(is_valid_hex_color("#000000"));
888        assert!(is_valid_hex_color("#ffffff"));
889
890        assert!(!is_valid_hex_color("000000"));
891        assert!(!is_valid_hex_color("ffffff"));
892
893        assert!(!is_valid_hex_color("#gggggg"));
894
895        // Not obvious cases unrecognized as colors
896        assert!(!is_valid_hex_color("#ffffff "));
897        assert!(!is_valid_hex_color("#fff")); // CSS shorthand
898        assert!(!is_valid_hex_color("#ffffff00")); // with alpha channel
899    }
900}