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/// Represents a hyperlink in the worksheet, which can be either external or internal.
183/// The display text is not part of the link, it is the content of the cell the link is
184/// attached to. Links are just cell metadata.
185#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
186#[serde(tag = "type")]
187pub enum Link {
188    /// A link to a resource outside the workbook: an URL, a mailto: URI or a file.
189    /// If the target points to a location inside another document it is written
190    /// as `target#location` (e.g. `file.xlsx#Sheet1!A1`).
191    External {
192        target: String,
193        tooltip: Option<String>,
194    },
195    /// A link to a location in this workbook: a cell reference like `Sheet1!A30`
196    /// or a defined name.
197    Internal {
198        location: String,
199        tooltip: Option<String>,
200    },
201}
202
203/// Internal representation of a worksheet Excel object
204#[derive(Encode, Decode, Debug, PartialEq, Clone)]
205pub struct Worksheet {
206    pub dimension: String,
207    pub cols: Vec<Col>,
208    pub rows: Vec<Row>,
209    pub name: String,
210    pub sheet_data: SheetData,
211    pub shared_formulas: Vec<String>,
212    pub sheet_id: u32,
213    pub state: SheetState,
214    pub color: Color,
215    pub merge_cells: Vec<String>,
216    pub comments: Vec<Comment>,
217    pub frozen_rows: i32,
218    pub frozen_columns: i32,
219    pub views: HashMap<u32, WorksheetView>,
220    /// Whether or not to show the grid lines in the worksheet
221    pub show_grid_lines: bool,
222    pub conditional_formatting: Vec<ConditionalFormatting>,
223    /// Hyperlinks in the worksheet, keyed by (row, column) of the cell they are attached to
224    pub links: HashMap<(i32, i32), Link>,
225}
226
227/// Internal representation of Excel's sheet_data
228/// It is row first and because of this all of our API's should be row first
229pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
230
231// ECMA-376-1:2016 section 18.3.1.73
232#[derive(Encode, Decode, Debug, PartialEq, Clone)]
233pub struct Row {
234    /// Row index
235    pub r: i32,
236    pub height: f64,
237    pub custom_format: bool,
238    pub custom_height: bool,
239    pub s: i32,
240    pub hidden: bool,
241}
242
243// ECMA-376-1:2016 section 18.3.1.13
244#[derive(Encode, Decode, Debug, PartialEq, Clone)]
245pub struct Col {
246    // Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
247    /// First column affected by this record. Settings apply to column in \[min, max\] range.
248    pub min: i32,
249    /// Last column affected by this record. Settings apply to column in \[min, max\] range.
250    pub max: i32,
251    pub width: f64,
252    pub custom_width: bool,
253    pub hidden: bool,
254    pub style: Option<i32>,
255}
256
257/// Cell type enum matching Excel TYPE() function values.
258#[derive(Debug, Eq, PartialEq)]
259pub enum CellType {
260    Number = 1,
261    Text = 2,
262    LogicalValue = 4,
263    ErrorValue = 16,
264    Array = 64,
265    CompoundData = 128,
266}
267
268/// The evaluated value stored in a formula cell.
269/// `Unevaluated` is a transient state that only exists during evaluation.
270#[derive(Encode, Decode, Debug, Clone, PartialEq)]
271pub enum FormulaValue {
272    Unevaluated,
273    Boolean(bool),
274    Number(f64),
275    Text(String),
276    Error {
277        ei: Error,
278        // Origin cell reference, e.g. "Sheet3!C4"
279        o: String,
280        // Human-readable error message, e.g. "Not implemented function"
281        m: String,
282    },
283}
284
285/// The value stored in a spill cell (no formula, no origin tracking).
286#[derive(Encode, Decode, Debug, Clone, PartialEq)]
287pub enum SpillValue {
288    Boolean(bool),
289    Number(f64),
290    Text(String),
291    Error(Error),
292}
293
294/// Whether an array formula is a CSE (Ctrl+Shift+Enter) formula or a dynamic formula.
295#[derive(Encode, Decode, Debug, Clone, PartialEq)]
296pub enum ArrayKind {
297    /// Ctrl+Shift+Enter array formula: fills a fixed declared range.
298    Cse,
299    /// Dynamic array formula: spills into adjacent cells automatically.
300    Dynamic,
301}
302
303// A cell in a worksheet.
304// Every cell has a style index (s) pointing to cell_xfs in the workbook styles.
305// Other fields:
306// * `f`    — formula index into the sheet's shared_formulas list
307// * `si`   — shared string index (SharedString cells only)
308// * `v`    — evaluated value (formula/spill cells)
309// * `r`    — spill range (width, height) for array/dynamic formula anchors
310// * `kind` — Cse or Dynamic for array formula anchors
311// * `a`    — anchor cell (row, column) for spill cells
312#[derive(Encode, Decode, Debug, Clone, PartialEq)]
313pub enum Cell {
314    EmptyCell {
315        s: i32,
316    },
317    BooleanCell {
318        v: bool,
319        s: i32,
320    },
321    NumberCell {
322        v: f64,
323        s: i32,
324    },
325    // Maybe we should not have this type. In Excel this is just a string
326    ErrorCell {
327        ei: Error,
328        s: i32,
329    },
330    // Always a shared string
331    SharedString {
332        si: i32,
333        s: i32,
334    },
335    // A regular (non-array) formula cell.
336    // `v` is `Unevaluated` transiently during evaluation, then holds the result.
337    CellFormula {
338        f: i32,
339        s: i32,
340        v: FormulaValue,
341    },
342    // The anchor of an array or dynamic formula.
343    // `kind` distinguishes CSE from dynamic; `r` is the spill range (width, height).
344    // `v` is `Unevaluated` transiently during evaluation, then holds the anchor cell result.
345    ArrayFormula {
346        f: i32,
347        s: i32,
348        r: (i32, i32),
349        kind: ArrayKind,
350        v: FormulaValue,
351    },
352    // A spill cell: holds a value produced by an array/dynamic formula at `a` (row, column).
353    SpillCell {
354        s: i32,
355        a: (i32, i32),
356        v: SpillValue,
357    },
358}
359
360impl Default for Cell {
361    fn default() -> Self {
362        Cell::EmptyCell { s: 0 }
363    }
364}
365
366#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
367pub struct Comment {
368    pub text: String,
369    pub author_name: String,
370    pub author_id: Option<String>,
371    pub cell_ref: String,
372}
373
374// ECMA-376-1:2016 section 18.5.1.2
375#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
376pub struct Table {
377    pub name: String,
378    pub display_name: String,
379    pub sheet_name: String,
380    pub reference: String,
381    pub totals_row_count: u32,
382    pub header_row_count: u32,
383    pub header_row_dxf_id: Option<u32>,
384    pub data_dxf_id: Option<u32>,
385    pub totals_row_dxf_id: Option<u32>,
386    pub columns: Vec<TableColumn>,
387    pub style_info: TableStyleInfo,
388    pub has_filters: bool,
389}
390
391// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
392// the totals_row_function is an enum not String methinks
393#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
394pub struct TableColumn {
395    pub id: u32,
396    pub name: String,
397    pub totals_row_label: Option<String>,
398    pub header_row_dxf_id: Option<u32>,
399    pub data_dxf_id: Option<u32>,
400    pub totals_row_dxf_id: Option<u32>,
401    pub totals_row_function: Option<String>,
402}
403
404impl Default for TableColumn {
405    fn default() -> Self {
406        TableColumn {
407            id: 0,
408            name: "Column".to_string(),
409            totals_row_label: None,
410            totals_row_function: None,
411            data_dxf_id: None,
412            header_row_dxf_id: None,
413            totals_row_dxf_id: None,
414        }
415    }
416}
417
418#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
419pub struct TableStyleInfo {
420    pub name: Option<String>,
421    pub show_first_column: bool,
422    pub show_last_column: bool,
423    pub show_row_stripes: bool,
424    pub show_column_stripes: bool,
425}
426
427#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
428pub struct DxfFont {
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub strike: Option<bool>,
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub u: Option<bool>,
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub b: Option<bool>,
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub i: Option<bool>,
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub sz: Option<i32>,
439    #[serde(skip_serializing_if = "Color::is_none")]
440    #[serde(default)]
441    pub color: Color,
442}
443
444// Dxf stands for "Differential Formatting". It is used in places like:
445// * conditional formatting
446// * tables
447// to specify partial formatting that overrides the cell formatting.
448#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
449pub struct Dxf {
450    pub font: Option<DxfFont>,
451    pub fill: Option<Fill>,
452    pub border: Option<Border>,
453    pub num_fmt: Option<NumFmt>,
454    pub alignment: Option<Alignment>,
455}
456
457#[derive(Encode, Decode, Debug, PartialEq, Clone)]
458pub struct Styles {
459    pub num_fmts: Vec<NumFmt>,
460    pub fonts: Vec<Font>,
461    pub fills: Vec<Fill>,
462    pub borders: Vec<Border>,
463    pub cell_style_xfs: Vec<CellStyleXfs>,
464    pub cell_xfs: Vec<CellXfs>,
465    pub cell_styles: Vec<CellStyles>,
466    pub dxfs: Vec<Dxf>,
467}
468
469impl Default for Styles {
470    fn default() -> Self {
471        Styles {
472            num_fmts: vec![],
473            fonts: vec![Default::default()],
474            fills: vec![Default::default(), Default::default()],
475            borders: vec![Default::default()],
476            cell_style_xfs: vec![Default::default()],
477            cell_xfs: vec![Default::default()],
478            cell_styles: vec![Default::default()],
479            dxfs: vec![],
480        }
481    }
482}
483
484#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
485pub struct Style {
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub alignment: Option<Alignment>,
488    pub num_fmt: String,
489    pub fill: Fill,
490    pub font: Font,
491    pub border: Border,
492    pub quote_prefix: bool,
493}
494
495impl Default for Style {
496    fn default() -> Self {
497        Style {
498            alignment: None,
499            num_fmt: "general".to_string(),
500            fill: Fill::default(),
501            font: Font::default(),
502            border: Border::default(),
503            quote_prefix: false,
504        }
505    }
506}
507
508#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
509pub struct NumFmt {
510    pub num_fmt_id: i32,
511    pub format_code: String,
512}
513
514impl Default for NumFmt {
515    fn default() -> Self {
516        NumFmt {
517            num_fmt_id: 0,
518            format_code: "general".to_string(),
519        }
520    }
521}
522
523// ST_FontScheme simple type (§18.18.33).
524// Usually major fonts are used for styles like headings,
525// and minor fonts are used for body and paragraph text.
526#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
527#[serde(rename_all = "lowercase")]
528#[derive(Default)]
529pub enum FontScheme {
530    #[default]
531    Minor,
532    Major,
533    None,
534}
535
536impl Display for FontScheme {
537    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
538        match self {
539            FontScheme::Minor => write!(formatter, "minor"),
540            FontScheme::Major => write!(formatter, "major"),
541            FontScheme::None => write!(formatter, "none"),
542        }
543    }
544}
545
546#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
547pub struct Font {
548    #[serde(default = "default_as_false")]
549    #[serde(skip_serializing_if = "is_false")]
550    pub strike: bool,
551    #[serde(default = "default_as_false")]
552    #[serde(skip_serializing_if = "is_false")]
553    pub u: bool, // seems that Excel supports a bit more - double underline / account underline etc.
554    #[serde(default = "default_as_false")]
555    #[serde(skip_serializing_if = "is_false")]
556    pub b: bool,
557    #[serde(default = "default_as_false")]
558    #[serde(skip_serializing_if = "is_false")]
559    pub i: bool,
560    pub sz: i32,
561    #[serde(skip_serializing_if = "Color::is_none")]
562    #[serde(default)]
563    pub color: Color,
564    pub name: String,
565    // This is the font family fallback
566    // 1 -> serif
567    // 2 -> sans serif
568    // 3 -> monospaced
569    // ...
570    pub family: i32,
571    pub scheme: FontScheme,
572}
573
574impl Default for Font {
575    fn default() -> Self {
576        Font {
577            strike: false,
578            u: false,
579            b: false,
580            i: false,
581            sz: 12,
582            color: Color::None,
583            name: "Inter".to_string(),
584            family: 2,
585            scheme: FontScheme::Minor,
586        }
587    }
588}
589
590#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
591pub struct Fill {
592    #[serde(skip_serializing_if = "Color::is_none")]
593    #[serde(default)]
594    pub color: Color,
595}
596
597#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
598#[serde(rename_all = "lowercase")]
599#[derive(Default)]
600pub enum HorizontalAlignment {
601    Center,
602    CenterContinuous,
603    Distributed,
604    Fill,
605    #[default]
606    General,
607    Justify,
608    Left,
609    Right,
610}
611
612// Note that alignment in "General" depends on type
613
614impl HorizontalAlignment {
615    fn is_default(&self) -> bool {
616        self == &HorizontalAlignment::default()
617    }
618}
619
620// FIXME: Is there a way to generate this automatically?
621impl Display for HorizontalAlignment {
622    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
623        match self {
624            HorizontalAlignment::Center => write!(formatter, "center"),
625            HorizontalAlignment::CenterContinuous => write!(formatter, "centerContinuous"),
626            HorizontalAlignment::Distributed => write!(formatter, "distributed"),
627            HorizontalAlignment::Fill => write!(formatter, "fill"),
628            HorizontalAlignment::General => write!(formatter, "general"),
629            HorizontalAlignment::Justify => write!(formatter, "justify"),
630            HorizontalAlignment::Left => write!(formatter, "left"),
631            HorizontalAlignment::Right => write!(formatter, "right"),
632        }
633    }
634}
635
636#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
637#[serde(rename_all = "lowercase")]
638#[derive(Default)]
639pub enum VerticalAlignment {
640    #[default]
641    Bottom,
642    Center,
643    Distributed,
644    Justify,
645    Top,
646}
647
648impl VerticalAlignment {
649    fn is_default(&self) -> bool {
650        self == &VerticalAlignment::default()
651    }
652}
653
654impl Display for VerticalAlignment {
655    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
656        match self {
657            VerticalAlignment::Bottom => write!(formatter, "bottom"),
658            VerticalAlignment::Center => write!(formatter, "center"),
659            VerticalAlignment::Distributed => write!(formatter, "distributed"),
660            VerticalAlignment::Justify => write!(formatter, "justify"),
661            VerticalAlignment::Top => write!(formatter, "top"),
662        }
663    }
664}
665
666// 1762
667#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
668pub struct Alignment {
669    #[serde(default)]
670    #[serde(skip_serializing_if = "HorizontalAlignment::is_default")]
671    pub horizontal: HorizontalAlignment,
672    #[serde(skip_serializing_if = "VerticalAlignment::is_default")]
673    #[serde(default)]
674    pub vertical: VerticalAlignment,
675    #[serde(default = "default_as_false")]
676    #[serde(skip_serializing_if = "is_false")]
677    pub wrap_text: bool,
678}
679
680#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
681pub struct CellStyleXfs {
682    pub num_fmt_id: i32,
683    pub font_id: i32,
684    pub fill_id: i32,
685    pub border_id: i32,
686    pub apply_number_format: bool,
687    pub apply_border: bool,
688    pub apply_alignment: bool,
689    pub apply_protection: bool,
690    pub apply_font: bool,
691    pub apply_fill: bool,
692}
693
694impl Default for CellStyleXfs {
695    fn default() -> Self {
696        CellStyleXfs {
697            num_fmt_id: 0,
698            font_id: 0,
699            fill_id: 0,
700            border_id: 0,
701            apply_number_format: true,
702            apply_border: true,
703            apply_alignment: true,
704            apply_protection: true,
705            apply_font: true,
706            apply_fill: true,
707        }
708    }
709}
710
711/// The formatting categories a named style includes — Excel's "Style Includes"
712/// checkboxes, stored as the `apply*` attributes of the style's `cellStyleXfs`
713/// record. Applying the style to a cell only stamps the included categories.
714/// The default (like "Normal") includes everything; the built-in "Percent",
715/// for example, includes only the number format.
716#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Copy)]
717#[serde(default)]
718pub struct StyleIncludes {
719    pub number_format: bool,
720    pub font: bool,
721    pub fill: bool,
722    pub border: bool,
723    pub alignment: bool,
724    pub protection: bool,
725}
726
727impl Default for StyleIncludes {
728    fn default() -> Self {
729        StyleIncludes {
730            number_format: true,
731            font: true,
732            fill: true,
733            border: true,
734            alignment: true,
735            protection: true,
736        }
737    }
738}
739
740#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
741pub struct CellXfs {
742    pub xf_id: i32,
743    pub num_fmt_id: i32,
744    pub font_id: i32,
745    pub fill_id: i32,
746    pub border_id: i32,
747    pub apply_number_format: bool,
748    pub apply_border: bool,
749    pub apply_alignment: bool,
750    pub apply_protection: bool,
751    pub apply_font: bool,
752    pub apply_fill: bool,
753    pub quote_prefix: bool,
754    pub alignment: Option<Alignment>,
755}
756
757#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
758pub struct CellStyles {
759    pub name: String,
760    pub xf_id: i32,
761    pub builtin_id: i32,
762}
763
764impl Default for CellStyles {
765    fn default() -> Self {
766        CellStyles {
767            name: "normal".to_string(),
768            xf_id: 0,
769            builtin_id: 0,
770        }
771    }
772}
773
774#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Clone)]
775#[serde(rename_all = "lowercase")]
776pub enum BorderStyle {
777    Thin,
778    Medium,
779    Thick,
780    Double,
781    Dotted,
782    SlantDashDot,
783    MediumDashed,
784    MediumDashDotDot,
785    MediumDashDot,
786}
787
788impl Display for BorderStyle {
789    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
790        match self {
791            BorderStyle::Thin => write!(formatter, "thin"),
792            BorderStyle::Thick => write!(formatter, "thick"),
793            BorderStyle::SlantDashDot => write!(formatter, "slantdashdot"),
794            BorderStyle::MediumDashed => write!(formatter, "mediumdashed"),
795            BorderStyle::MediumDashDotDot => write!(formatter, "mediumdashdotdot"),
796            BorderStyle::MediumDashDot => write!(formatter, "mediumdashdot"),
797            BorderStyle::Medium => write!(formatter, "medium"),
798            BorderStyle::Double => write!(formatter, "double"),
799            BorderStyle::Dotted => write!(formatter, "dotted"),
800        }
801    }
802}
803
804#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
805pub struct BorderItem {
806    pub style: BorderStyle,
807    #[serde(skip_serializing_if = "Color::is_none")]
808    #[serde(default)]
809    pub color: Color,
810}
811
812#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
813pub struct Border {
814    #[serde(default = "default_as_false")]
815    #[serde(skip_serializing_if = "is_false")]
816    pub diagonal_up: bool,
817    #[serde(default = "default_as_false")]
818    #[serde(skip_serializing_if = "is_false")]
819    pub diagonal_down: bool,
820    #[serde(skip_serializing_if = "Option::is_none")]
821    pub left: Option<BorderItem>,
822    #[serde(skip_serializing_if = "Option::is_none")]
823    pub right: Option<BorderItem>,
824    #[serde(skip_serializing_if = "Option::is_none")]
825    pub top: Option<BorderItem>,
826    #[serde(skip_serializing_if = "Option::is_none")]
827    pub bottom: Option<BorderItem>,
828    #[serde(skip_serializing_if = "Option::is_none")]
829    pub diagonal: Option<BorderItem>,
830}
831
832/// Information need to show a sheet tab in the UI
833/// The color is serialized only if it is not Color::None
834#[derive(Serialize, Deserialize, Debug, PartialEq)]
835pub struct SheetProperties {
836    pub name: String,
837    pub state: String,
838    pub sheet_id: u32,
839    #[serde(skip_serializing_if = "Color::is_none")]
840    #[serde(default)]
841    pub color: Color,
842}
843
844#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
845pub struct Theme {
846    pub name: String,
847    pub dk1: String,
848    pub lt1: String,
849    pub dk2: String,
850    pub lt2: String,
851    pub accent1: String,
852    pub accent2: String,
853    pub accent3: String,
854    pub accent4: String,
855    pub accent5: String,
856    pub accent6: String,
857    pub hlink: String,
858    pub fol_hlink: String,
859}
860
861impl Default for Theme {
862    fn default() -> Self {
863        Theme {
864            name: "Office".to_string(),
865            dk1: "#000000".to_string(),
866            lt1: "#FFFFFF".to_string(),
867            dk2: "#44546A".to_string(),
868            lt2: "#E7E6E6".to_string(),
869            accent1: "#4472C4".to_string(),
870            accent2: "#ED7D31".to_string(),
871            accent3: "#A5A5A5".to_string(),
872            accent4: "#FFC000".to_string(),
873            accent5: "#5B9BD5".to_string(),
874            accent6: "#70AD47".to_string(),
875            hlink: "#0563C1".to_string(),
876            fol_hlink: "#954F72".to_string(),
877        }
878    }
879}
880
881impl Theme {
882    /// Resolves a `theme="N"` attribute (and optional `tint`) to an `#RRGGBB` string.
883    /// Applies the OOXML dk/lt swap for indices 0–3.
884    pub fn resolve(&self, theme_index: i32, tint: f64) -> String {
885        use crate::colors::hex_with_tint_to_rgb;
886        let color = match theme_index {
887            0 => &self.lt1,
888            1 => &self.dk1,
889            2 => &self.lt2,
890            3 => &self.dk2,
891            4 => &self.accent1,
892            5 => &self.accent2,
893            6 => &self.accent3,
894            7 => &self.accent4,
895            8 => &self.accent5,
896            9 => &self.accent6,
897            10 => &self.hlink,
898            11 => &self.fol_hlink,
899            _ => &self.dk1,
900        };
901        hex_with_tint_to_rgb(color, tint)
902    }
903}
904
905#[cfg(test)]
906mod test {
907    use super::*;
908    #[test]
909    fn test_is_valid_hex_color() {
910        assert!(is_valid_hex_color("#000000"));
911        assert!(is_valid_hex_color("#ffffff"));
912
913        assert!(!is_valid_hex_color("000000"));
914        assert!(!is_valid_hex_color("ffffff"));
915
916        assert!(!is_valid_hex_color("#gggggg"));
917
918        // Not obvious cases unrecognized as colors
919        assert!(!is_valid_hex_color("#ffffff "));
920        assert!(!is_valid_hex_color("#fff")); // CSS shorthand
921        assert!(!is_valid_hex_color("#ffffff00")); // with alpha channel
922    }
923}