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