Skip to main content

excel_ooxml/
model.rs

1//! The minimal in-memory model of a SpreadsheetML workbook: a workbook is a sequence of sheets,
2//! each a sequence of rows, each a sequence of cells — matching how a real `.xlsx` structures
3//! `xl/workbook.xml`'s `<sheets>` list and each `xl/worksheets/sheetN.xml`'s `<sheetData>`
4//! (`CT_Workbook`/`CT_Worksheet`/`CT_Row`/`CT_Cell`).
5//!
6//! Text, numeric, boolean, date, formula, and rich-text cell values are modeled
7//! (`CellValue::Text`/`::Number`/`::Boolean`/`::Date`/`::Formula`/`::RichText`), and a cell may
8//! carry a [`CellFormat`] (number format, basic font, solid fill, alignment). A sheet may also
9//! carry [`ConditionalFormattingRule`]s, [`DataValidationRule`]s, and embedded charts
10//! ([`SheetChart`]). A workbook may also carry [`DefinedName`]s (named ranges/formulas/constants).
11
12/// A `.xlsx` workbook: a sequence of sheets, in the order they appear in Excel's own sheet tabs.
13#[derive(Debug, Default, Clone, PartialEq)]
14pub struct Workbook {
15    /// The sheets that make up this workbook, in order.
16    pub sheets: Vec<Sheet>,
17    /// Named ranges/formulas/constants defined on this workbook (`<definedNames>`/`<definedName>`,
18    /// `CT_DefinedNames`/`CT_DefinedName`), in order. Unlike a sheet, this lives at the workbook
19    /// level even for a name scoped to a single sheet (see [`DefinedName::local_sheet_id`]).
20    pub defined_names: Vec<DefinedName>,
21    /// Document metadata (`docProps/core.xml`/`docProps/app.xml`). Both parts are always written by
22    /// this crate (they're required for a strictly valid package), but before this point every
23    /// field in them was left empty/boilerplate.
24    pub properties: DocumentProperties,
25    /// Workbook *structure* protection (`<workbookProtection>`, `CT_WorkbookProtection`). Distinct
26    /// from [`Sheet::protection`]: this locks whether sheets can be
27    /// added/renamed/hidden/reordered/deleted, not cell editing within a sheet.
28    pub protection: Option<WorkbookProtection>,
29    /// Write-protection / "read-only recommended" (`<fileSharing>`, `CT_FileSharing`). Distinct
30    /// from [`protection`](Workbook::protection) (`<workbookProtection>`, point 26, which locks
31    /// *structure*): this instead controls the "This file should be opened as read-only" prompt
32    /// Excel shows when opening the file, and/or the legacy 16-bit write-reservation password some
33    /// older workflows still use. Confirmed via `sml.xsd`: `CT_Workbook`'s sequence puts
34    /// `fileSharing?` *before* `workbookPr?`/ `workbookProtection?`/`bookViews?`, i.e. as the very
35    /// first possible child of `<workbook>`.
36    pub write_protection: Option<WriteProtection>,
37    /// The 0-based index into [`sheets`](Workbook::sheets) of the sheet shown as active/selected
38    /// when the workbook is opened (`<bookViews><workbookView activeTab="..">`). `None` leaves
39    /// Excel's own default (the first sheet).
40    pub active_tab: Option<usize>,
41    /// References to other workbooks used by this workbook's formulas (`<externalReferences>` + one
42    /// `xl/externalLinks/externalLinkN.xml` part per entry, `CT_ExternalReference`). Storage only,
43    /// same posture as this crate's own formula text: nothing here resolves or evaluates a
44    /// reference into the other workbook's actual values.
45    pub external_links: Vec<ExternalWorkbookLink>,
46    /// A VBA project's raw compiled bytes (`xl/vbaProject.bin`), if this workbook carries macros.
47    /// Preserved *opaquely*: this crate can round-trip an existing macro-enabled workbook's
48    /// `vbaProject.bin` unchanged but cannot create, parse, or modify VBA source from scratch; the
49    /// OLE2/ CFB container format `vbaProject.bin` is internally structured in isn't decoded at
50    /// all. When `Some`, `Workbook::write_to` switches the workbook part's content type to the
51    /// macro-enabled variant (`..sheet.macroEnabled.main+xml`) — the caller is still responsible
52    /// for using a `.xlsm` file name, this crate has no notion of file extensions.
53    pub vba_project: Option<Vec<u8>>,
54    /// Workbook-wide calculation properties (`<calcPr>`, `CT_CalcPr`). `None` leaves Excel's own
55    /// default (automatic recalculation, no iterative calculation).
56    pub calculation: Option<CalculationProperties>,
57    /// Custom table style definitions (`xl/styles.xml`'s `<tableStyles>`), referenced by name from
58    /// any sheet's [`ExcelTable::table_style_name`]. Workbook-wide (styles.xml is a single shared
59    /// part), same posture as `defined_names` living here rather than per-sheet even for a
60    /// sheet-scoped name.
61    pub table_styles: Vec<TableStyle>,
62    /// Named cell styles (`xl/styles.xml`'s `<cellStyles>`/`<cellStyleXfs>`,
63    /// `CT_CellStyles`/`CT_CellStyleXfs`): the "Cell Styles" gallery Excel's Home ribbon shows
64    /// (`Good`/ `Bad`/`Neutral`, `Heading 1`-`4`, `Input`/`Output`/`Calculation`,
65    /// `Currency`/`Percent`/`Comma`..). Before this point every `cellXfs` entry this crate wrote
66    /// implicitly pointed at a single, hardcoded `"Normal"` `cellStyleXfs` entry (`xfId="0"`) — a
67    /// cell format that set [`CellFormat::named_style`] had no way to actually link back to one of
68    /// these named entries, so the association was silently lost on write. A [`CellFormat`] used
69    /// anywhere in this workbook (cell, row/column default..) that sets `named_style` to a name
70    /// found here gets that entry's index written as its `cellXfs` `xfId`, instead of the default
71    /// `0`; a name not found here silently falls back to `0` (`"Normal"`), same "best-effort, not
72    /// validated" posture as `ExcelTable::table_style_name` referencing a name not in this same
73    /// list. Workbook-wide (styles.xml is a single shared part), same posture as
74    /// `table_styles`/`defined_names` living here rather than per-sheet.
75    pub named_cell_styles: Vec<NamedCellStyle>,
76}
77
78/// A single worksheet (`xl/worksheets/sheetN.xml`, `CT_Worksheet`): a name (shown on Excel's sheet
79/// tab) plus a sequence of rows.
80#[derive(Debug, Default, Clone, PartialEq)]
81pub struct Sheet {
82    /// This sheet's name (`<sheet name="..">` in `xl/workbook.xml`), shown on Excel's own sheet
83    /// tab. Excel itself limits this to 31 characters and disallows a handful of characters (`: \ /
84    /// ? * [ ]`) — neither limit is enforced here, same "caller's responsibility" posture as
85    /// `word-ooxml`'s `PageSetup.orientation` not auto-swapping dimensions.
86    pub name: String,
87    /// The rows that make up this sheet's data, in order. A row's own 1-based row number (`<row
88    /// r="..">`) is simply this row's position in this `Vec` plus one — not a separate field — so
89    /// there is no way to represent a sparse sheet (e.g. data starting at row 5 with rows 1-4
90    /// genuinely absent) when *writing*; reading back a sparse sheet from a real `.xlsx` still
91    /// works (missing rows are filled in as empty), see `reader.rs`.
92    pub rows: Vec<Row>,
93    /// Conditional formatting rules applied to ranges of this sheet
94    /// (`<conditionalFormatting>`/`<cfRule>`, `CT_ConditionalFormatting`/ `CT_CfRule`), in order.
95    /// Each rule gets its own dedicated `<conditionalFormatting>` block when written (see
96    /// `writer.rs::write_conditional_formatting`) — real Excel groups multiple rules sharing the
97    /// same range under one block, but nothing in the schema requires it, so this crate doesn't
98    /// bother.
99    pub conditional_formatting_rules: Vec<ConditionalFormattingRule>,
100    /// Data validation rules applied to ranges of this sheet
101    /// (`<dataValidations>`/`<dataValidation>`, `CT_DataValidations`/ `CT_DataValidation`), in
102    /// order.
103    pub data_validation_rules: Vec<DataValidationRule>,
104    /// Merged cell ranges (`<mergeCells>`/`<mergeCell>`, `CT_MergeCells`), each written verbatim as
105    /// a range reference (e.g. `"A1:C1"`). This crate does not validate that ranges don't overlap
106    /// or that only the top-left cell of a merged range carries a value (both are the caller's
107    /// responsibility, same posture as
108    /// `ConditionalFormattingRule::range`/`DataValidationRule::range`).
109    pub merged_ranges: Vec<String>,
110    /// Per-column width/visibility/outline settings (`<cols>`/`<col>`, `CT_Cols`/`CT_Col`). Each
111    /// entry covers exactly one column (`min == max` in the written XML) — this crate doesn't
112    /// bother collapsing adjacent identical entries into a single `<col min="1" max="3"../>` range
113    /// the way real Excel does, same "less compact but schema-valid" posture as
114    /// `ConditionalFormattingRule`'s one-block-per-rule choice.
115    pub column_settings: Vec<ColumnSetting>,
116    /// The autofilter range (`<autoFilter ref="..">`, `CT_AutoFilter`), if any. Only the
117    /// dropdown-arrow range is modeled, not per-column filter criteria
118    /// (`CT_FilterColumn`/`CT_Filters`) — out of scope.
119    pub auto_filter_range: Option<String>,
120    /// Frozen (or split) panes (`<sheetView><pane../></sheetView>`, `CT_Pane`), if any. Only
121    /// "frozen" panes are modeled (the common case, e.g. freezing a header row/column), not "split"
122    /// panes (a draggable divider with no scroll-locking) — `CT_Pane`'s `state="split"` variant is
123    /// out of scope.
124    pub freeze_panes: Option<FreezePane>,
125    /// This sheet's print/page-setup settings (print area, repeated rows/columns, page orientation,
126    /// fit-to-page, header/footer text). `PrintSettings::default()` (everything `None`) writes no
127    /// print-related XML at all.
128    pub print_settings: PrintSettings,
129    /// This sheet's protection settings (`<sheetProtection>`, `CT_SheetProtection`), if protected.
130    pub protection: Option<SheetProtection>,
131    /// Per-cell hyperlinks (`<hyperlinks>`/`<hyperlink>`, `CT_Hyperlinks`/ `CT_Hyperlink`).
132    pub hyperlinks: Vec<CellHyperlink>,
133    /// Per-cell comments (legacy `xl/comments*.xml` + VML drawing, `CT_Comments`/`CT_Comment`).
134    pub comments: Vec<CellComment>,
135    /// A structured (`ListObject`) table on this sheet (`<tableParts>`/`<tablePart>` + a dedicated
136    /// `xl/tables/tableN.xml` part, `CT_Table`), if any. Only one table per sheet is modeled (this
137    /// crate's own scope choice, not an ECMA-376 limit — real Excel allows several) since a single
138    /// example is enough to prove the feature out; a second table on the same sheet is simply never
139    /// written.
140    pub table: Option<ExcelTable>,
141    /// This sheet's tab visibility (`<sheet state=".">` in `xl/workbook.xml` — a `CT_Sheet`
142    /// attribute, not part of the worksheet part itself) — extended to a genuine three-state
143    /// `SheetVisibility` (was a plain `bool` before this).
144    pub visibility: SheetVisibility,
145    /// This sheet's own display zoom percentage (`<sheetView zoomScale="..">`, e.g. `100` for
146    /// 100%). `None` leaves Excel's own default (100%).
147    pub zoom_scale: Option<u32>,
148    /// This sheet's tab color (`<sheetPr><tabColor rgb=".."/></sheetPr>`), as an 8-hex-digit ARGB
149    /// string. `None` leaves Excel's own default (no color, the plain gray/white tab).
150    pub tab_color: Option<String>,
151    /// Whether this sheet's view shows gridlines (`<sheetView showGridLines="..">`). `None` leaves
152    /// Excel's own default (shown).
153    pub show_grid_lines: Option<bool>,
154    /// Whether this sheet's view shows row/column headers (`<sheetView showRowColHeaders="..">`).
155    /// `None` leaves Excel's own default (shown).
156    pub show_row_col_headers: Option<bool>,
157    /// Whether this sheet's view shows zero values, or leaves those cells blank (`<sheetView
158    /// showZeros="..">`). `None` leaves Excel's own default (shown).
159    pub show_zeros: Option<bool>,
160    /// Whether this sheet's view is laid out right-to-left (`<sheetView rightToLeft="..">`). `None`
161    /// leaves Excel's own default (left-to-right).
162    pub right_to_left: Option<bool>,
163    /// The cell selected/active when this sheet is shown (`<selection activeCell=".." sqref="..">`,
164    /// both attributes written the same — this crate only models a single selected cell, not a
165    /// multi-cell selection). Distinct from [`Workbook::active_tab`]: this is the cell selected
166    /// *within* this sheet, not which sheet tab is shown. `None` leaves Excel's own default (`A1`).
167    pub active_cell: Option<String>,
168    /// This sheet's default column width, in Excel's own character-width units (`<sheetFormatPr
169    /// defaultColWidth="..">`). Distinct from [`ColumnSetting::width`](ColumnSetting): this is the
170    /// width used for every column *without* its own explicit [`ColumnSetting`]. `None` leaves
171    /// Excel's own default.
172    pub default_column_width: Option<f64>,
173    /// This sheet's default row height, in points (`<sheetFormatPr defaultRowHeight="..">`).
174    /// Distinct from [`Row::height`]: this is the height used for every row *without* its own
175    /// explicit height. `None` leaves Excel's own default (`15`, i.e. `defaultRowHeight` is always
176    /// written with a concrete value — `CT_SheetFormatPr::defaultRowHeight` is mandatory whenever
177    /// `<sheetFormatPr>` is written at all, unlike every other field on this type).
178    pub default_row_height: Option<f64>,
179    /// Manual page breaks after these 1-based row numbers (`<rowBreaks>`/`<brk>`, `CT_PageBreak`).
180    pub row_breaks: Vec<u32>,
181    /// Manual page breaks after these 1-based column numbers (`<colBreaks>`/`<brk>`).
182    pub column_breaks: Vec<u32>,
183    /// "What-if" scenarios (`<scenarios>`/`<scenario>`, `CT_Scenario`).
184    pub scenarios: Vec<Scenario>,
185    /// Per-column filter criteria for [`auto_filter_range`](Sheet::auto_filter_range)
186    /// (`<filterColumn>`/`<filters>`, `CT_FilterColumn`/`CT_Filters`). A no-op if
187    /// `auto_filter_range` is `None`. Only the simplest form (a fixed list of checked/visible
188    /// values) is modeled — `CT_Filters`'s custom criteria, top10, and date-grouping filter forms
189    /// are not.
190    pub auto_filter_columns: Vec<AutoFilterColumn>,
191    /// Suppressed error-checking indicators (the little green triangle in a cell's corner and its
192    /// "Ignore Error" right-click option), (`<ignoredErrors>`/`<ignoredError>`, `CT_IgnoredErrors`/
193    /// `CT_IgnoredError`). Each entry covers one range and one or more error categories; this crate
194    /// writes one `<ignoredError>` element per [`IgnoredError`] entry, same "less compact but
195    /// schema-valid" posture as `ColumnSetting`/ `ConditionalFormattingRule` not bothering to merge
196    /// adjacent/ overlapping ranges the way real Excel's UI sometimes does.
197    pub ignored_errors: Vec<IgnoredError>,
198    /// Multiple named protected ranges (`<protectedRanges>`/ `<protectedRange>`,
199    /// `CT_ProtectedRanges`/`CT_ProtectedRange`). Distinct from [`Sheet::protection`] (point 13,
200    /// `<sheetProtection>`): that locks/unlocks editing for the *whole* sheet at once (based on
201    /// each cell's own `locked` [`CellFormat`] flag), while this is Excel's older "Allow Users to
202    /// Edit Ranges" feature — one or more named ranges, each optionally its own password, that stay
203    /// editable while the rest of a protected sheet is locked. `CT_Worksheet`'s sequence puts
204    /// `protectedRanges` right after `sheetProtection`, before `scenarios` (confirmed via
205    /// `sml.xsd`).
206    pub protected_ranges: Vec<ProtectedRange>,
207    /// A remembered sort order for [`auto_filter_range`](Sheet::auto_filter_range)
208    /// (`<autoFilter><sortState>..</sortState></autoFilter>`). A no-op if `auto_filter_range` is
209    /// `None` (same posture as [`auto_filter_columns`](Sheet::auto_filter_columns)).
210    /// `CT_AutoFilter`'s own sequence puts `sortState` *after* `filterColumn*`, nested inside
211    /// `<autoFilter>` — distinct from [`ExcelTable::sort_state`], which is a sibling of a table's
212    /// own `<autoFilter>`, not a child of it (see that field's doc comment for the schema source).
213    pub sort_state: Option<SortState>,
214    /// Whether an outline/grouping's summary rows sit below their detail rows, or above
215    /// (`<sheetPr><outlinePr summaryBelow="..">`, `CT_SheetPr`/`CT_OutlinePr`). Relevant only when
216    /// [`Row::outline_level`]/column grouping is used. `None` leaves Excel's own default (`true`,
217    /// summary below); `Some(false)` matches the "summary above" layout common in accounting
218    /// templates. Confirmed via `sml.xsd`: `outlinePr` is `CT_SheetPr`'s second child, right after
219    /// `tabColor?`.
220    pub outline_summary_below: Option<bool>,
221    /// Whether an outline/grouping's summary columns sit to the right of their detail columns, or
222    /// to the left (`<outlinePr summaryRight="..">`) — same point 55, the column-axis counterpart
223    /// of `outline_summary_below`. `None` leaves Excel's own default (`true`, summary to the
224    /// right).
225    pub outline_summary_right: Option<bool>,
226    /// This sheet's DrawingML drawing — embedded pictures and/or charts, each anchored to a cell
227    /// range (`<drawing r:id="..">` referencing a dedicated `xl/drawings/drawingN.xml` part,
228    /// `CT_Drawing`/`xdr:wsDr`). Only one drawing part per sheet is modeled (this crate's own scope
229    /// choice, not an ECMA-376 limit — real Excel only ever has one anyway), same posture as
230    /// [`Sheet::table`]. `None` writes no `<drawing>` element and no drawing part at all.
231    pub drawing: Option<SheetDrawing>,
232}
233
234/// A single row of cells (`<row>`, `CT_Row`).
235#[derive(Debug, Default, Clone, PartialEq)]
236pub struct Row {
237    /// The cells that make up this row, in column order starting at column A. Like `Sheet.rows`, a
238    /// cell's column letter is derived from its position in this `Vec`, not stored separately —
239    /// writing a row always produces a dense `A`, `B`, `C`. sequence with no gaps. Reading back a
240    /// sparse row from a real `.xlsx` (Excel itself never writes a `<c>` element for a genuinely
241    /// empty cell) still works: any skipped column is filled in as `CellValue::Empty` so a cell's
242    /// index in this `Vec` always matches its real column, see `reader.rs`.
243    pub cells: Vec<Cell>,
244    /// This row's explicit height, in points (`<row ht=".." customHeight="1">`) — `None` uses
245    /// Excel's own default row height.
246    pub height: Option<f64>,
247    /// Whether this row is hidden (`<row hidden="1">`).
248    pub hidden: bool,
249    /// This row's outline (grouping) level (`<row outlineLevel="..">`, 0-7).
250    pub outline_level: u8,
251    /// Whether this row's outline group is shown collapsed (`<row collapsed="1">`, the "-"
252    /// summary-only state of the little outline button). Distinct from `outline_level` itself
253    /// (which rows belong to the group) — this is only the group's current expanded/collapsed
254    /// *display* state.
255    pub collapsed: bool,
256    /// This row's default cell format (`<row s="n" customFormat="1">`, an index into
257    /// `xl/styles.xml`'s `cellXfs`). Same "applies to any cell without its own explicit format,
258    /// storage only, caller keeps things straight" posture as [`ColumnSetting::style`] — see its
259    /// doc comment for the precedence note. `CT_Row`'s `customFormat` flag is always written
260    /// together with `s` when this is `Some` (confirmed via `sml.xsd`: `s` alone with
261    /// `customFormat` left at its `false` default means the row number's own displayed style, not
262    /// every cell in the row — real Excel always pairs the two when a row-level format is actually
263    /// set through its UI).
264    pub default_format: Option<CellFormat>,
265}
266
267/// A single cell (`<c>`, `CT_Cell`): a value plus an optional style.
268#[derive(Debug, Default, Clone, PartialEq)]
269pub struct Cell {
270    /// This cell's value.
271    pub value: CellValue,
272    /// This cell's style, if any. `None` means the cell uses Excel's own default style
273    /// (`xl/styles.xml`'s `cellXfs` index 0, the "Normal" style) — this crate never writes a
274    /// `<c>`'s `s` attribute for a cell with no format, matching how real Excel omits `s="0"` too
275    /// (it's the implicit default). Distinct `CellFormat`s across a workbook are deduplicated into
276    /// shared `cellXfs` entries by the writer, exactly like `CellValue::Text`'s shared-strings
277    /// deduplication — see `writer.rs::collect_cell_formats`.
278    pub format: Option<CellFormat>,
279}
280
281/// A cell's value.
282#[derive(Debug, Default, Clone, PartialEq)]
283pub enum CellValue {
284    /// No value — an empty cell. This crate never writes a `<c>` element for one (matching real
285    /// Excel output, which omits genuinely empty cells entirely) *unless* the cell carries a
286    /// [`CellFormat`], in which case a self-closing `<c r=".." s=".."/>` is written (a
287    /// styled-but-empty cell — real Excel does this too, e.g. for a pre-formatted but not yet
288    /// filled-in cell). This variant also exists so a sparse row read back from a real `.xlsx` can
289    /// still be represented densely, keeping a cell's `Vec` position equal to its real column. The
290    /// default value for a freshly-constructed `Cell`.
291    #[default]
292    Empty,
293    /// A text value (`<c t="s"><v>N</v></c>`, an index into the shared strings table
294    /// `xl/sharedStrings.xml` — `CT_Cell`'s `t="str"` in-cell-formula-result and `t="inlineStr"`
295    /// variants are not modeled, matching Excel's own default behavior of always deduplicating
296    /// repeated text through the shared strings table rather than writing it inline).
297    Text(String),
298    /// A numeric value (`<c><v>..</v></c>`, `CT_Cell`'s default/`"n"` type — a plain `f64`, written
299    /// with Rust's own default floating-point-to-string formatting; not a `CT_Cell`'s `t="b"`
300    /// (boolean) or `t="e"` (error) variant).
301    Number(f64),
302    /// A formula cell (`<c><f>..</f><v>..</v></c>`, `CT_CellFormula` + cached `CT_Cell/@v`).
303    /// Formula *evaluation* is out of scope — only the literal formula text and, if present, its
304    /// last-computed cached value are stored, never recomputed.
305    ///
306    /// A cell built via [`Cell::formula`] always has `Some` formula text; writing a cell with
307    /// `formula: None` falls back to writing just the cached value with no `<f>` element at all
308    /// (see `writer.rs::write_cell`). A real `.xlsx`'s `t="shared"` formula cells (both the group's
309    /// master and its followers) now read back as [`SharedFormula`](CellValue::SharedFormula)
310    /// instead of this variant — point 52, which replaced this variant's earlier "drop the
311    /// `si`/`ref`, keep only the cached value" limit with a dedicated, round-trippable
312    /// representation.
313    Formula {
314        /// The formula text, without a leading `=` (e.g. `"A2*B2"`).
315        formula: Option<String>,
316        /// The formula's last-computed value, if known.
317        cached_value: Option<f64>,
318    },
319    /// A boolean value (`<c t="b"><v>0|1</v></c>`). Fully round-trippable (unlike `Date`, see
320    /// below).
321    Boolean(bool),
322    /// A date/time value. **Write side only**: [`Cell::date`] converts an [`ExcelDateTime`] to the
323    /// serial-day number Excel actually stores (`<c><v>..</v></c>`, a plain numeric cell,
324    /// `CT_Cell`'s default type — SpreadsheetML has no dedicated date cell type, a date is *always*
325    /// just a number that happens to carry a date-shaped `numFmtId`), so writing one produces
326    /// exactly the same XML shape as `CellValue::Number`. Reading a workbook back never
327    /// reconstructs this variant, even for a cell this crate's own writer produced — a
328    /// date-formatted cell reads back as `CellValue::Number(serial)`, the caller's own
329    /// responsibility to reinterpret using the cell's `CellFormat::number_format`, same honest
330    /// "storage, not clever inference" posture as this crate's formula-evaluation scope limit. Pair
331    /// `Cell::date` with a `CellFormat::with_number_format` (e.g. `"dd/mm/yyyy"`) for Excel to
332    /// actually display it as a date rather than a raw serial number — this crate does not do that
333    /// automatically (see `Cell::date`'s doc comment), matching `word-ooxml`'s
334    /// `PageSetup.orientation` "caller's responsibility" precedent.
335    Date(ExcelDateTime),
336    /// A text value with per-run character formatting (`<si><r>..</r></si>` in
337    /// `xl/sharedStrings.xml` — the "rich text" shared string form, as opposed to
338    /// `CellValue::Text`'s plain `<si><t>..</t></si>` form). Fully round-trippable.
339    RichText(Vec<RichTextRun>),
340    /// An array (CSE, "Ctrl+Shift+Enter") formula (`<c><f t="array" ref=".">.</f><v>.</v></c>`,
341    /// `CT_CellFormula`'s `t="array"` variant) — distinct from `Formula`'s plain/shared forms. Real
342    /// Excel writes the literal `<f t="array" ref=".">` only on the array range's top-left (anchor)
343    /// cell — every other cell the range covers gets no `<f>` at all, just its own cached `<v>`
344    /// (this crate's own writer only ever builds a `Cell` holding this variant for the anchor cell
345    /// itself; the caller is responsible for the other cells in `range` carrying plain
346    /// `CellValue::Number`/`Empty` cells with the right cached values, same "caller keeps things in
347    /// sync" posture as `ExcelTable::columns` needing to match the header row's actual text).
348    /// Reading a real `.xlsx`'s array-formula continuation cells (which carry no `<f>` at all)
349    /// simply reads them back as whatever plain value type their own `<v>`/`t` attribute says —
350    /// this crate does not reconstruct that they were once part of an array's range.
351    ArrayFormula {
352        /// The formula text, without a leading `=`.
353        formula: String,
354        /// The array's full range this formula fills (e.g. `"B2:B10"`) — a single-cell array
355        /// formula still needs its own cell as the range (e.g. `"B2:B2"`).
356        range: String,
357        /// The formula's last-computed value for this (the anchor) cell, if known.
358        cached_value: Option<f64>,
359    },
360    /// One cell of a shared-formula group (`<c><f t="shared" si=".." ref="..">..</f><v>..</v></c>`
361    /// on the group's master cell, or `<c><f t="shared" si=".."/><v>..</v></c>` on a follower cell
362    /// — `CT_CellFormula`'s `t="shared"` variant). Confirmed against `sml.xsd`: `si` (the group's
363    /// shared index) is the only attribute present on *every* cell in the group; `ref` (the group's
364    /// full range) is only meaningful — and, matching real Excel's own convention, only ever
365    /// written — on the master cell, the one carrying the actual formula text. This crate does
366    /// **not** compute or shift relative references for follower cells (e.g. deriving `"A3*B3"`
367    /// from a master's `"A2*B2"`) — same "storage, not evaluation" posture as
368    /// [`Formula`](CellValue::Formula) itself; the caller supplies each follower's own
369    /// already-correct formula text (or `None`, matching what real Excel itself writes for a
370    /// follower — see [`Cell::shared_formula_follower`]).
371    SharedFormula {
372        /// The formula text, without a leading `=` — `Some` on the group's master cell, `None` on a
373        /// follower (real Excel writes a follower's `<f>` with no text content at all, just
374        /// `t`/`si`).
375        formula: Option<String>,
376        /// This formula group's shared index (`si`) — identical across every cell (master and every
377        /// follower) belonging to the same group; a workbook may have several independent groups,
378        /// each with its own index starting from `0`.
379        group_index: u32,
380        /// The group's full range (`ref`) — `Some` only on the master cell (the one written first,
381        /// real Excel omits it on every follower).
382        range: Option<String>,
383        /// This cell's own last-computed value, if known.
384        cached_value: Option<f64>,
385    },
386}
387
388/// A calendar date/time, used to build a [`CellValue::Date`] (`Cell::date`) — deliberately not tied
389/// to any external date/time crate (this project's dependency policy only forbids third-party
390/// *OOXML-implementing* crates, but a plain field struct is simpler still and avoids the question
391/// entirely). Only the proleptic Gregorian calendar and Excel's default "1900 date system" are
392/// supported — the legacy "1904 date system" (an old Mac Excel option, `<workbookPr
393/// date1904="1"/>`) is not modeled, same "one well-defined behavior, not every historical Excel
394/// quirk" posture as elsewhere in this crate.
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub struct ExcelDateTime {
397    pub year: i32,
398    pub month: u32,
399    pub day: u32,
400    pub hour: u32,
401    pub minute: u32,
402    pub second: u32,
403}
404
405impl ExcelDateTime {
406    /// Creates a date with no time component (midnight).
407    pub fn date(year: i32, month: u32, day: u32) -> Self {
408        Self {
409            year,
410            month,
411            day,
412            hour: 0,
413            minute: 0,
414            second: 0,
415        }
416    }
417
418    /// Sets a time-of-day component and returns the date for chaining.
419    pub fn with_time(mut self, hour: u32, minute: u32, second: u32) -> Self {
420        self.hour = hour;
421        self.minute = minute;
422        self.second = second;
423        self
424    }
425}
426
427/// One run of a [`CellValue::RichText`] value: a span of text sharing the same character formatting
428/// (`<r><rPr>..</rPr><t>..</t></r>`, `CT_RElt`) — a deliberately scoped-down subset of
429/// `CT_RPrElt`'s full run-property surface (bold/italic/color/size only, same subset `CellFormat`'s
430/// own font fields already cover; underline/strike/ vertical-align/font-family are not modeled).
431#[derive(Debug, Clone, PartialEq)]
432pub struct RichTextRun {
433    pub text: String,
434    pub bold: bool,
435    pub italic: bool,
436    /// The run's font color, as an 8-hex-digit ARGB string.
437    pub font_color: Option<String>,
438    /// The run's font size, in points.
439    pub font_size: Option<f64>,
440    /// The run's font name/family (e.g. `"Calibri"`).
441    pub font_name: Option<String>,
442    /// Underline (plain single underline only, same posture as [`CellFormat::underline`]).
443    pub underline: bool,
444    /// Strikethrough.
445    pub strike: bool,
446}
447
448impl RichTextRun {
449    /// Creates a plain (no formatting) run.
450    pub fn new(text: impl Into<String>) -> Self {
451        Self {
452            text: text.into(),
453            bold: false,
454            italic: false,
455            font_color: None,
456            font_size: None,
457            font_name: None,
458            underline: false,
459            strike: false,
460        }
461    }
462
463    /// Sets bold and returns the run for chaining.
464    pub fn with_bold(mut self, bold: bool) -> Self {
465        self.bold = bold;
466        self
467    }
468
469    /// Sets italic and returns the run for chaining.
470    pub fn with_italic(mut self, italic: bool) -> Self {
471        self.italic = italic;
472        self
473    }
474
475    /// Sets the font color (8-hex-digit ARGB) and returns the run for chaining.
476    pub fn with_font_color(mut self, color: impl Into<String>) -> Self {
477        self.font_color = Some(color.into());
478        self
479    }
480
481    /// Sets the font size, in points, and returns the run for chaining.
482    pub fn with_font_size(mut self, size: f64) -> Self {
483        self.font_size = Some(size);
484        self
485    }
486
487    /// Sets the font name/family and returns the run for chaining.
488    pub fn with_font_name(mut self, font_name: impl Into<String>) -> Self {
489        self.font_name = Some(font_name.into());
490        self
491    }
492
493    /// Sets underline and returns the run for chaining.
494    pub fn with_underline(mut self, underline: bool) -> Self {
495        self.underline = underline;
496        self
497    }
498
499    /// Sets strikethrough and returns the run for chaining.
500    pub fn with_strike(mut self, strike: bool) -> Self {
501        self.strike = strike;
502        self
503    }
504}
505
506/// A cell's style: a number format, basic font attributes (bold/italic/size/color), a solid
507/// background fill color, borders, and horizontal/vertical alignment.
508///
509/// Not deduplicated on the model itself — the writer deduplicates identical `CellFormat`s into
510/// shared `xl/styles.xml` entries (`numFmts`/`fonts`/`fills`/`cellXfs`) when serializing, exactly
511/// like `Workbook`'s own shared-strings deduplication for repeated text (see
512/// `writer.rs::collect_cell_formats`/`build_styles`).
513#[derive(Debug, Default, Clone, PartialEq)]
514pub struct CellFormat {
515    /// A custom number format code (e.g. `"0.00"`, `"#,##0"`). Always registered as a *custom*
516    /// format (`numFmtId` >= 164) when written, even if the code happens to match one of Excel's
517    /// built-in format codes — this crate does not maintain the built-in-format lookup table
518    /// (`BuiltinFormats`), so it never reuses a built-in id. Reading back a real `.xlsx`'s built-in
519    /// number format (any `numFmtId` not declared in that file's own `<numFmts>`) is not modeled
520    /// either: it reads back as `None`.
521    pub number_format: Option<String>,
522    /// Bold font weight.
523    pub bold: bool,
524    /// Italic font style.
525    pub italic: bool,
526    /// Font size in points. `None` uses the workbook default (11pt) — note that writing *any* other
527    /// font attribute (bold, italic, or a color) also resolves this to the default 11pt in the
528    /// generated `xl/styles.xml` font entry (a single `<font>` element can't leave "unset"
529    /// attributes that fall back independently), so reading a round-tripped format back may see
530    /// `Some(11.0)` rather than `None` — a known, accepted round-trip nuance.
531    pub font_size: Option<f64>,
532    /// Font color, as an 8-hex-digit ARGB string (e.g. `"FFFF0000"` for opaque red). `None` uses
533    /// the workbook default (opaque black) — same round-trip nuance as `font_size` applies here too
534    /// (and to `font_name` below).
535    pub font_color: Option<String>,
536    /// Solid background fill color, as an 8-hex-digit ARGB string. `None` leaves the cell unfilled.
537    pub fill_color: Option<String>,
538    /// Horizontal alignment. `None` uses Excel's own default (`general` — text left-aligned,
539    /// numbers right-aligned).
540    pub horizontal_alignment: Option<HorizontalAlignment>,
541    /// Vertical alignment. `None` uses Excel's own default (`bottom`).
542    pub vertical_alignment: Option<VerticalAlignment>,
543    /// This cell's borders (top/bottom/left/right/diagonal). `Border::default()` (all
544    /// `None`/`false`) draws no border at all, matching every other `CellFormat` field's "unset
545    /// means Excel's own default" posture.
546    pub border: Border,
547    /// Font name/family (e.g. `"Calibri"`, `"Arial"`). `None` uses the workbook default (Excel's
548    /// own "Calibri"). Only a single Latin font name is modeled, not `CT_Font`'s separate
549    /// east-Asian/complex-script overrides (`family`/`scheme`/`charset` are not modeled either).
550    /// Same unset-resolves-to-the-default round-trip nuance as `font_size`/ `font_color` above
551    /// applies here too: writing any other font attribute also resolves this to `Some("Calibri")`
552    /// in the generated `<font>` entry, so an originally-`None` `font_name` may read back as
553    /// `Some("Calibri")` rather than `None`.
554    pub font_name: Option<String>,
555    /// Underline. Only a plain single underline is modeled (`CT_Font`'s `ST_UnderlineValues` has
556    /// several more: `double`/`singleAccounting`/ `doubleAccounting`) — same on/off posture
557    /// `RichTextRun`'s `bold`/`italic` already use (unlike `word-ooxml`'s much richer
558    /// `UnderlineStyle` enum).
559    pub underline: bool,
560    /// Strikethrough.
561    pub strike: bool,
562    /// Wraps text onto multiple lines within the cell (`wrapText`).
563    pub wrap_text: bool,
564    /// Indentation level (`indent`, roughly 3 character-widths per level).
565    pub indent: Option<u32>,
566    /// Text rotation, in Excel's own raw `ST_CellAlignment`/`textRotation` units — `0` to `90` for
567    /// counter-clockwise degrees, `91` to `180` for clockwise degrees (stored as `90 - angle`, e.g.
568    /// a plain `-45°` tilt is `135`), or the special value `255` for top-to-bottom vertical text.
569    /// Written and read back verbatim — converting a plain signed angle into this encoding is the
570    /// caller's responsibility, same "storage, not a conversion helper" posture as
571    /// `PrintSettings::header`'s raw `&L`/`&C`/`&R` codes.
572    pub text_rotation: Option<u16>,
573    /// Shrinks the font size to fit the cell's width instead of wrapping or overflowing
574    /// (`shrinkToFit`).
575    pub shrink_to_fit: bool,
576    /// A non-solid pattern fill (`<patternFill patternType="..">` with a foreground/background
577    /// color pair). Mutually exclusive with
578    /// [`fill_color`](CellFormat::fill_color)/[`gradient_fill`](CellFormat::gradient_fill) in
579    /// practice (Excel itself only ever uses one `<fill>` child); if more than one is set, the
580    /// writer prefers `gradient_fill`, then `pattern_fill`, then `fill_color`, see
581    /// `writer.rs::resolve_fill`.
582    pub pattern_fill: Option<PatternFill>,
583    /// A gradient fill (`<gradientFill>`). Only the linear (angle-based) gradient form is modeled,
584    /// not `CT_GradientFill`'s "path" (radial, from-center) form. See `pattern_fill`'s doc comment
585    /// for the precedence rule when more than one fill field is set.
586    pub gradient_fill: Option<GradientFill>,
587    /// Whether this cell is locked when the sheet is protected (`<protection locked=".">`,
588    /// `CT_CellProtection`) — independent of [`Sheet::protection`] actually being turned on: Excel
589    /// itself treats every cell as `locked` by default, so protection only has a visible effect
590    /// once [`Sheet::protection`] is `Some`. `None` leaves Excel's own default (locked).
591    pub locked: Option<bool>,
592    /// Whether this cell's formula is hidden from the formula bar when the sheet is protected
593    /// (`<protection hidden="..">`). `None` leaves Excel's own default (not hidden). Meaningless on
594    /// a non-formula cell, same as real Excel.
595    pub formula_hidden: Option<bool>,
596    /// Links this format to one of [`Workbook::named_cell_styles`] by name (resolved into that
597    /// entry's index, written as this format's `cellXfs` `xfId` attribute). This is what makes a
598    /// cell show up as e.g. "Good"/"Heading 1" in Excel's own Cell Styles gallery
599    /// (highlighted/selected) rather than merely happening to look the same. `None` (the default)
600    /// writes `xfId="0"`, i.e. based on the implicit `"Normal"` style, same as every `CellFormat`
601    /// before this point. A name not found in `Workbook::named_cell_styles` at write time silently
602    /// falls back to `0` too — see that field's own doc comment.
603    pub named_style: Option<String>,
604}
605
606/// A cell's horizontal alignment (`ST_HorizontalAlignment`) — only the three most common values are
607/// modeled; `general`/`fill`/`justify`/ `centerContinuous`/`distributed` are not (they read back as
608/// `None`).
609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
610pub enum HorizontalAlignment {
611    Left,
612    Center,
613    Right,
614}
615
616/// A cell's vertical alignment (`ST_VerticalAlignment`) — only the three most common values are
617/// modeled; `justify`/`distributed` are not (they read back as `None`).
618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
619pub enum VerticalAlignment {
620    Top,
621    Center,
622    Bottom,
623}
624
625/// A border line style (`ST_BorderStyle`) — only the handful of styles a real spreadsheet actually
626/// uses day to day are modeled; `hair`/`mediumDashed`/`dashDot`/`mediumDashDot`/`dashDotDot`/
627/// `mediumDashDotDot`/`slantDashDot` are not (out of scope, same scope-reduction posture as
628/// elsewhere in this crate).
629#[derive(Debug, Clone, Copy, PartialEq, Eq)]
630pub enum BorderStyle {
631    Thin,
632    Medium,
633    Thick,
634    Dashed,
635    Dotted,
636    Double,
637    Hair,
638}
639
640/// One edge of a [`Border`] (`<left>`/`<right>`/`<top>`/`<bottom>`/ `<diagonal>`, `CT_BorderPr`): a
641/// line style plus an optional color.
642#[derive(Debug, Clone, PartialEq)]
643pub struct BorderEdge {
644    pub style: BorderStyle,
645    /// The edge's color, as an 8-hex-digit ARGB string. `None` leaves the edge at Excel's own
646    /// default (opaque black).
647    pub color: Option<String>,
648}
649
650impl BorderEdge {
651    /// Creates a border edge with the given style, no explicit color (opaque black, Excel's own
652    /// default).
653    pub fn new(style: BorderStyle) -> Self {
654        Self { style, color: None }
655    }
656
657    /// Sets the edge's color (8-hex-digit ARGB) and returns it for chaining.
658    pub fn with_color(mut self, color: impl Into<String>) -> Self {
659        self.color = Some(color.into());
660        self
661    }
662}
663
664/// A cell's borders (`<border>`, `CT_Border`) — up to four edges plus an optional diagonal.
665/// `Border::default()` (every field `None`/`false`) draws no border at all.
666#[derive(Debug, Default, Clone, PartialEq)]
667pub struct Border {
668    pub top: Option<BorderEdge>,
669    pub bottom: Option<BorderEdge>,
670    pub left: Option<BorderEdge>,
671    pub right: Option<BorderEdge>,
672    /// The diagonal line's style/color — only drawn if [`diagonal_up`](Border::diagonal_up) and/or
673    /// [`diagonal_down`](Border::diagonal_down) is also set (`CT_Border`'s own
674    /// `diagonalUp`/`diagonalDown` attributes live on the parent `<border>` element, not
675    /// `<diagonal>` itself).
676    pub diagonal: Option<BorderEdge>,
677    /// Draws the diagonal from bottom-left to top-right.
678    pub diagonal_up: bool,
679    /// Draws the diagonal from top-left to bottom-right.
680    pub diagonal_down: bool,
681}
682
683impl Border {
684    /// Creates a `Border` with no edges at all.
685    pub fn new() -> Self {
686        Self::default()
687    }
688
689    /// Sets the top edge and returns the border for chaining.
690    pub fn with_top(mut self, edge: BorderEdge) -> Self {
691        self.top = Some(edge);
692        self
693    }
694
695    /// Sets the bottom edge and returns the border for chaining.
696    pub fn with_bottom(mut self, edge: BorderEdge) -> Self {
697        self.bottom = Some(edge);
698        self
699    }
700
701    /// Sets the left edge and returns the border for chaining.
702    pub fn with_left(mut self, edge: BorderEdge) -> Self {
703        self.left = Some(edge);
704        self
705    }
706
707    /// Sets the right edge and returns the border for chaining.
708    pub fn with_right(mut self, edge: BorderEdge) -> Self {
709        self.right = Some(edge);
710        self
711    }
712
713    /// Sets the diagonal edge and whether it runs bottom-left-to-top-right and/or
714    /// top-left-to-bottom-right, and returns the border for chaining.
715    pub fn with_diagonal(mut self, edge: BorderEdge, up: bool, down: bool) -> Self {
716        self.diagonal = Some(edge);
717        self.diagonal_up = up;
718        self.diagonal_down = down;
719        self
720    }
721
722    /// A convenience for the common case of the same style/color on all four sides (a full box
723    /// border) — equivalent to calling
724    /// [`with_top`](Border::with_top)/[`with_bottom`](Border::with_bottom)/
725    /// [`with_left`](Border::with_left)/[`with_right`](Border::with_right) with four identical
726    /// edges.
727    pub fn all(style: BorderStyle, color: impl Into<String>) -> Self {
728        let color = color.into();
729        Self {
730            top: Some(BorderEdge::new(style).with_color(color.clone())),
731            bottom: Some(BorderEdge::new(style).with_color(color.clone())),
732            left: Some(BorderEdge::new(style).with_color(color.clone())),
733            right: Some(BorderEdge::new(style).with_color(color)),
734            diagonal: None,
735            diagonal_up: false,
736            diagonal_down: false,
737        }
738    }
739}
740
741impl Workbook {
742    /// Creates an empty workbook (no sheets).
743    pub fn new() -> Self {
744        Self::default()
745    }
746
747    /// Appends a sheet and returns the workbook for chaining.
748    pub fn with_sheet(mut self, sheet: Sheet) -> Self {
749        self.sheets.push(sheet);
750        self
751    }
752
753    /// Appends an empty sheet with the given name and returns a mutable reference to it, to build
754    /// it up in place (e.g. `workbook.add_sheet("Feuil1").write_cell(1, 1, Cell::text("Hi"))`). A
755    /// `&mut self` counterpart to [`Self::with_sheet`], for building a workbook sheet-by-sheet in a
756    /// loop without the consuming builder's `workbook = workbook.with_sheet(..)` reassignment on
757    /// every iteration.
758    pub fn add_sheet(&mut self, name: impl Into<String>) -> &mut Sheet {
759        self.sheets.push(Sheet::new(name));
760        self.sheets.last_mut().expect("a sheet was just pushed")
761    }
762
763    /// Appends a defined name and returns the workbook for chaining.
764    pub fn with_defined_name(mut self, defined_name: DefinedName) -> Self {
765        self.defined_names.push(defined_name);
766        self
767    }
768
769    /// Sets this workbook's calculation properties and returns it for chaining.
770    pub fn with_calculation(mut self, calculation: CalculationProperties) -> Self {
771        self.calculation = Some(calculation);
772        self
773    }
774
775    /// Appends a custom table style definition and returns the workbook for chaining. See
776    /// [`ExcelTable::table_style_name`]'s doc comment for how a table references one of these by
777    /// name.
778    pub fn with_table_style(mut self, table_style: TableStyle) -> Self {
779        self.table_styles.push(table_style);
780        self
781    }
782
783    /// Sets this workbook's write-protection and returns it for chaining.
784    pub fn with_write_protection(mut self, write_protection: WriteProtection) -> Self {
785        self.write_protection = Some(write_protection);
786        self
787    }
788
789    /// Appends a named cell style definition and returns the workbook for chaining. See
790    /// [`CellFormat::named_style`]'s doc comment for how a format references one of these by name.
791    pub fn with_named_cell_style(mut self, named_cell_style: NamedCellStyle) -> Self {
792        self.named_cell_styles.push(named_cell_style);
793        self
794    }
795}
796
797/// A workbook's recalculation mode (`<calcPr calcMode="..">`, `ST_CalcMode`).
798#[derive(Debug, Clone, Copy, PartialEq, Eq)]
799pub enum CalculationMode {
800    /// Recalculates automatically whenever a dependency changes (Excel's own default).
801    Automatic,
802    /// Recalculates automatically except for data tables (`ST_CalcMode`'s `"autoNoTable"`) — data
803    /// tables (`TABLE()` what-if formulas) only recalculate on a manual trigger, everything else
804    /// stays automatic.
805    AutomaticExceptTables,
806    /// Only recalculates when the user explicitly triggers it (F9 or "Calculate Now").
807    Manual,
808}
809
810/// Workbook-wide calculation properties (`<calcPr>`, `CT_CalcPr`). This crate never evaluates
811/// formulas itself (see `CellValue::Formula`'s doc comment) — these settings are pure storage,
812/// telling *Excel* how to recalculate once the file is opened, same "storage, not evaluation"
813/// posture as a formula's own text.
814#[derive(Debug, Clone, Copy, PartialEq)]
815pub struct CalculationProperties {
816    /// The recalculation mode. `None` leaves Excel's own default (automatic).
817    pub mode: Option<CalculationMode>,
818    /// Enables iterative calculation, needed for formulas with intentional circular references
819    /// (`<calcPr iterate="1">`) — without this, Excel refuses to open a file containing a genuine
820    /// circular reference without warning the user first.
821    pub iterate: bool,
822}
823
824impl CalculationProperties {
825    /// Calculation properties with Excel's own defaults (automatic mode, no iterative calculation)
826    /// — only useful as a starting point for the `with_*` builders, since this is equivalent to
827    /// leaving `Workbook.calculation` as `None` entirely.
828    pub fn new() -> Self {
829        Self {
830            mode: None,
831            iterate: false,
832        }
833    }
834
835    /// Sets the recalculation mode and returns the properties for chaining.
836    pub fn with_mode(mut self, mode: CalculationMode) -> Self {
837        self.mode = Some(mode);
838        self
839    }
840
841    /// Enables iterative calculation and returns the properties for chaining.
842    pub fn with_iterate(mut self, iterate: bool) -> Self {
843        self.iterate = iterate;
844        self
845    }
846}
847
848impl Default for CalculationProperties {
849    fn default() -> Self {
850        Self::new()
851    }
852}
853
854impl Sheet {
855    /// Creates an empty sheet (no rows) with the given name.
856    pub fn new(name: impl Into<String>) -> Self {
857        Self {
858            name: name.into(),
859            rows: Vec::new(),
860            conditional_formatting_rules: Vec::new(),
861            data_validation_rules: Vec::new(),
862            merged_ranges: Vec::new(),
863            column_settings: Vec::new(),
864            auto_filter_range: None,
865            freeze_panes: None,
866            print_settings: PrintSettings::default(),
867            protection: None,
868            hyperlinks: Vec::new(),
869            comments: Vec::new(),
870            table: None,
871            visibility: SheetVisibility::Visible,
872            zoom_scale: None,
873            row_breaks: Vec::new(),
874            column_breaks: Vec::new(),
875            scenarios: Vec::new(),
876            auto_filter_columns: Vec::new(),
877            tab_color: None,
878            show_grid_lines: None,
879            show_row_col_headers: None,
880            show_zeros: None,
881            right_to_left: None,
882            active_cell: None,
883            default_column_width: None,
884            default_row_height: None,
885            ignored_errors: Vec::new(),
886            protected_ranges: Vec::new(),
887            sort_state: None,
888            outline_summary_below: None,
889            outline_summary_right: None,
890            drawing: None,
891        }
892    }
893
894    /// Appends a row and returns the sheet for chaining.
895    pub fn with_row(mut self, row: Row) -> Self {
896        self.rows.push(row);
897        self
898    }
899
900    /// Writes `cell` at the given 1-based `(row, col)` position — `(1, 1)` is `A1` — growing `rows`
901    /// and the target row's own `cells` with empty padding cells as needed to reach it, and returns
902    /// a mutable reference to the cell just written. Direct coordinate addressing, for the common
903    /// case of writing cells out of strict top-to-bottom, left-to-right order (or just one cell at
904    /// a time in a loop) without assembling a full [`Row`] by hand first. 1-based, to match this
905    /// crate's own existing convention — a row's or cell's position in its `Vec` plus one (see
906    /// [`Sheet::rows`]'s and [`Row::cells`]'s doc comments) — so a `Sheet` never mixes two
907    /// different numbering schemes depending on which method touched it.
908    ///
909    /// Padding cells are genuinely empty ([`Cell::new`]/[`Cell::default`]), not written out as
910    /// `<c>` elements at all unless later given a format — same posture as a sparse [`Row`] built
911    /// by hand.
912    ///
913    /// Accepts anything convertible to a [`Cell`] — a `Cell` itself (built via
914    /// [`Cell::text`]/[`Cell::formula`]/[`Cell::date`]/etc. for the cases below that need one made
915    /// explicitly), or, for the three unambiguous cases, the raw value directly: `&str`/`String` (→
916    /// [`CellValue::Text`]), `f64` (→ [`CellValue::Number`]), `bool` (→ [`CellValue::Boolean`]) —
917    /// the common case a caller writing cells one at a time reaches for most often. Formulas,
918    /// dates, rich text, and array formulas are deliberately *not* covered by an automatic
919    /// conversion — there's no unambiguous way to tell "this is a formula" from a plain string, or
920    /// "this number is a date" from a plain number, so those still go through their own `Cell::*`
921    /// constructor.
922    ///
923    /// # Panics
924    ///
925    /// Panics if `row` or `col` is `0` (1-based, so `0` is never a valid position).
926    pub fn write_cell(&mut self, row: u32, col: u32, cell: impl Into<Cell>) -> &mut Cell {
927        assert!(
928            row >= 1 && col >= 1,
929            "write_cell uses 1-based row/col indices, got ({row}, {col})"
930        );
931        let cell = cell.into();
932        let row_index = (row - 1) as usize;
933        let col_index = (col - 1) as usize;
934
935        if self.rows.len() <= row_index {
936            self.rows.resize_with(row_index + 1, Row::default);
937        }
938        let target_row = &mut self.rows[row_index];
939        if target_row.cells.len() <= col_index {
940            target_row.cells.resize_with(col_index + 1, Cell::default);
941        }
942        target_row.cells[col_index] = cell;
943        &mut target_row.cells[col_index]
944    }
945
946    /// Appends a conditional formatting rule and returns the sheet for chaining.
947    pub fn with_conditional_formatting_rule(mut self, rule: ConditionalFormattingRule) -> Self {
948        self.conditional_formatting_rules.push(rule);
949        self
950    }
951
952    /// Appends a data validation rule and returns the sheet for chaining.
953    pub fn with_data_validation_rule(mut self, rule: DataValidationRule) -> Self {
954        self.data_validation_rules.push(rule);
955        self
956    }
957
958    /// Appends a merged cell range (e.g. `"A1:C1"`) and returns the sheet for chaining.
959    pub fn with_merged_range(mut self, range: impl Into<String>) -> Self {
960        self.merged_ranges.push(range.into());
961        self
962    }
963
964    /// Appends a column setting and returns the sheet for chaining.
965    pub fn with_column_setting(mut self, setting: ColumnSetting) -> Self {
966        self.column_settings.push(setting);
967        self
968    }
969
970    /// Sets the autofilter range and returns the sheet for chaining.
971    pub fn with_auto_filter_range(mut self, range: impl Into<String>) -> Self {
972        self.auto_filter_range = Some(range.into());
973        self
974    }
975
976    /// Sets frozen panes and returns the sheet for chaining.
977    pub fn with_freeze_panes(mut self, freeze_panes: FreezePane) -> Self {
978        self.freeze_panes = Some(freeze_panes);
979        self
980    }
981
982    /// Sets this sheet's print settings and returns the sheet for chaining.
983    pub fn with_print_settings(mut self, print_settings: PrintSettings) -> Self {
984        self.print_settings = print_settings;
985        self
986    }
987
988    /// Sets this sheet's protection and returns the sheet for chaining.
989    pub fn with_protection(mut self, protection: SheetProtection) -> Self {
990        self.protection = Some(protection);
991        self
992    }
993
994    /// Appends a cell hyperlink and returns the sheet for chaining.
995    pub fn with_hyperlink(mut self, hyperlink: CellHyperlink) -> Self {
996        self.hyperlinks.push(hyperlink);
997        self
998    }
999
1000    /// Appends a cell comment and returns the sheet for chaining.
1001    pub fn with_comment(mut self, comment: CellComment) -> Self {
1002        self.comments.push(comment);
1003        self
1004    }
1005
1006    /// Sets this sheet's structured table and returns the sheet for chaining.
1007    pub fn with_table(mut self, table: ExcelTable) -> Self {
1008        self.table = Some(table);
1009        self
1010    }
1011
1012    /// Sets this sheet's drawing (embedded pictures/charts) and returns the sheet for chaining.
1013    pub fn with_drawing(mut self, drawing: SheetDrawing) -> Self {
1014        self.drawing = Some(drawing);
1015        self
1016    }
1017
1018    /// Sets this sheet's tab visibility and returns the sheet for chaining — replacing the old
1019    /// `with_hidden(bool)` (removed, pre-1.0 breaking change): use
1020    /// `with_visibility(SheetVisibility::Hidden)` for what used to be `with_hidden(true)`.
1021    pub fn with_visibility(mut self, visibility: SheetVisibility) -> Self {
1022        self.visibility = visibility;
1023        self
1024    }
1025
1026    /// Sets this sheet's display zoom percentage and returns the sheet for chaining.
1027    pub fn with_zoom_scale(mut self, zoom_scale: u32) -> Self {
1028        self.zoom_scale = Some(zoom_scale);
1029        self
1030    }
1031
1032    /// Sets this sheet's tab color (8-hex-digit ARGB) and returns the sheet for chaining.
1033    pub fn with_tab_color(mut self, color: impl Into<String>) -> Self {
1034        self.tab_color = Some(color.into());
1035        self
1036    }
1037
1038    /// Sets whether this sheet's view shows gridlines and returns the sheet for chaining.
1039    pub fn with_show_grid_lines(mut self, show: bool) -> Self {
1040        self.show_grid_lines = Some(show);
1041        self
1042    }
1043
1044    /// Sets whether this sheet's view shows row/column headers and returns the sheet for chaining.
1045    pub fn with_show_row_col_headers(mut self, show: bool) -> Self {
1046        self.show_row_col_headers = Some(show);
1047        self
1048    }
1049
1050    /// Sets whether this sheet's view shows zero values and returns the sheet for chaining.
1051    pub fn with_show_zeros(mut self, show: bool) -> Self {
1052        self.show_zeros = Some(show);
1053        self
1054    }
1055
1056    /// Sets whether this sheet's view is laid out right-to-left and returns the sheet for chaining.
1057    pub fn with_right_to_left(mut self, right_to_left: bool) -> Self {
1058        self.right_to_left = Some(right_to_left);
1059        self
1060    }
1061
1062    /// Sets the cell selected/active when this sheet is shown (e.g. `"B3"`) and returns the sheet
1063    /// for chaining.
1064    pub fn with_active_cell(mut self, cell: impl Into<String>) -> Self {
1065        self.active_cell = Some(cell.into());
1066        self
1067    }
1068
1069    /// Sets this sheet's default column width and returns the sheet for chaining.
1070    pub fn with_default_column_width(mut self, width: f64) -> Self {
1071        self.default_column_width = Some(width);
1072        self
1073    }
1074
1075    /// Sets this sheet's default row height and returns the sheet for chaining.
1076    pub fn with_default_row_height(mut self, height: f64) -> Self {
1077        self.default_row_height = Some(height);
1078        self
1079    }
1080
1081    /// Adds a manual page break after the given 1-based row number and returns the sheet for
1082    /// chaining.
1083    pub fn with_row_break(mut self, row: u32) -> Self {
1084        self.row_breaks.push(row);
1085        self
1086    }
1087
1088    /// Adds a manual page break after the given 1-based column number and returns the sheet for
1089    /// chaining.
1090    pub fn with_column_break(mut self, column: u32) -> Self {
1091        self.column_breaks.push(column);
1092        self
1093    }
1094
1095    /// Appends a "what-if" scenario and returns the sheet for chaining.
1096    pub fn with_scenario(mut self, scenario: Scenario) -> Self {
1097        self.scenarios.push(scenario);
1098        self
1099    }
1100
1101    /// Appends an autofilter column's criteria and returns the sheet for chaining.
1102    pub fn with_auto_filter_column(mut self, column: AutoFilterColumn) -> Self {
1103        self.auto_filter_columns.push(column);
1104        self
1105    }
1106
1107    /// Appends a suppressed error-checking entry and returns the sheet for chaining.
1108    pub fn with_ignored_error(mut self, ignored_error: IgnoredError) -> Self {
1109        self.ignored_errors.push(ignored_error);
1110        self
1111    }
1112
1113    /// Appends a named protected range and returns the sheet for chaining.
1114    pub fn with_protected_range(mut self, protected_range: ProtectedRange) -> Self {
1115        self.protected_ranges.push(protected_range);
1116        self
1117    }
1118
1119    /// Sets this sheet's (worksheet-level `<autoFilter>`) remembered sort order and returns the
1120    /// sheet for chaining. See [`Sheet::sort_state`]'s doc comment: a no-op unless
1121    /// [`with_auto_filter_range`](Sheet::with_auto_filter_range) is also used.
1122    pub fn with_sort_state(mut self, sort_state: SortState) -> Self {
1123        self.sort_state = Some(sort_state);
1124        self
1125    }
1126
1127    /// Sets whether outline/grouping summary rows sit below (`true`) or above (`false`) their
1128    /// detail rows, and returns the sheet for chaining.
1129    pub fn with_outline_summary_below(mut self, summary_below: bool) -> Self {
1130        self.outline_summary_below = Some(summary_below);
1131        self
1132    }
1133
1134    /// Sets whether outline/grouping summary columns sit to the right (`true`) or left (`false`) of
1135    /// their detail columns, and returns the sheet for chaining — point 55, the column-axis
1136    /// counterpart of `with_outline_summary_below`.
1137    pub fn with_outline_summary_right(mut self, summary_right: bool) -> Self {
1138        self.outline_summary_right = Some(summary_right);
1139        self
1140    }
1141}
1142
1143impl Row {
1144    /// Creates an empty row (no cells).
1145    pub fn new() -> Self {
1146        Self::default()
1147    }
1148
1149    /// Appends a cell and returns the row for chaining.
1150    pub fn with_cell(mut self, cell: Cell) -> Self {
1151        self.cells.push(cell);
1152        self
1153    }
1154
1155    /// Appends a text cell and returns the row for chaining — a convenience for the common case of
1156    /// not needing `Cell` directly.
1157    pub fn with_text(self, text: impl Into<String>) -> Self {
1158        self.with_cell(Cell::text(text))
1159    }
1160
1161    /// Appends a numeric cell and returns the row for chaining.
1162    pub fn with_number(self, number: f64) -> Self {
1163        self.with_cell(Cell::number(number))
1164    }
1165
1166    /// Appends a formula cell (no cached value) and returns the row for chaining.
1167    pub fn with_formula(self, formula: impl Into<String>) -> Self {
1168        self.with_cell(Cell::formula(formula))
1169    }
1170
1171    /// Sets this row's explicit height, in points, and returns it for chaining.
1172    pub fn with_height(mut self, height: f64) -> Self {
1173        self.height = Some(height);
1174        self
1175    }
1176
1177    /// Sets whether this row is hidden and returns it for chaining.
1178    pub fn with_hidden(mut self, hidden: bool) -> Self {
1179        self.hidden = hidden;
1180        self
1181    }
1182
1183    /// Sets this row's outline (grouping) level and returns it for chaining.
1184    pub fn with_outline_level(mut self, level: u8) -> Self {
1185        self.outline_level = level;
1186        self
1187    }
1188
1189    /// Sets whether this row's outline group is shown collapsed and returns it for chaining.
1190    pub fn with_collapsed(mut self, collapsed: bool) -> Self {
1191        self.collapsed = collapsed;
1192        self
1193    }
1194
1195    /// Sets this row's default cell format and returns it for chaining.
1196    pub fn with_style(mut self, style: CellFormat) -> Self {
1197        self.default_format = Some(style);
1198        self
1199    }
1200}
1201
1202impl Cell {
1203    /// Creates an empty cell.
1204    pub fn new() -> Self {
1205        Self::default()
1206    }
1207
1208    /// Creates a text cell.
1209    pub fn text(text: impl Into<String>) -> Self {
1210        Self {
1211            value: CellValue::Text(text.into()),
1212            format: None,
1213        }
1214    }
1215
1216    /// Creates a numeric cell.
1217    pub fn number(number: f64) -> Self {
1218        Self {
1219            value: CellValue::Number(number),
1220            format: None,
1221        }
1222    }
1223
1224    /// Creates a formula cell holding the given formula text (without a leading `=`) and no cached
1225    /// value yet — see [`Cell::with_cached_value`] to attach one. A formula with no cached value is
1226    /// still schema-valid (Excel simply recomputes it the next time the file is opened, its
1227    /// standard behavior for any formula it considers "dirty").
1228    pub fn formula(formula: impl Into<String>) -> Self {
1229        Self {
1230            value: CellValue::Formula {
1231                formula: Some(formula.into()),
1232                cached_value: None,
1233            },
1234            format: None,
1235        }
1236    }
1237
1238    /// Creates a boolean cell.
1239    pub fn boolean(value: bool) -> Self {
1240        Self {
1241            value: CellValue::Boolean(value),
1242            format: None,
1243        }
1244    }
1245
1246    /// Creates a date cell from an [`ExcelDateTime`] — see [`CellValue::Date`]'s doc comment: pair
1247    /// this with a `.with_format(CellFormat::new().with_number_format("dd/mm/yyyy"))` (or any other
1248    /// date-shaped format code) for Excel to actually display it as a date; without one, Excel
1249    /// shows the raw serial number.
1250    pub fn date(date: ExcelDateTime) -> Self {
1251        Self {
1252            value: CellValue::Date(date),
1253            format: None,
1254        }
1255    }
1256
1257    /// Creates a rich-text cell (a text value with per-run character formatting).
1258    pub fn rich_text(runs: Vec<RichTextRun>) -> Self {
1259        Self {
1260            value: CellValue::RichText(runs),
1261            format: None,
1262        }
1263    }
1264
1265    /// Creates an array (CSE) formula cell holding the given formula text (without a leading `=`)
1266    /// and range, and no cached value yet — see [`Cell::with_cached_value`] to attach one. This
1267    /// cell should be placed at the array range's top-left (anchor) position — see
1268    /// [`CellValue::ArrayFormula`]'s doc comment.
1269    pub fn array_formula(formula: impl Into<String>, range: impl Into<String>) -> Self {
1270        Self {
1271            value: CellValue::ArrayFormula {
1272                formula: formula.into(),
1273                range: range.into(),
1274                cached_value: None,
1275            },
1276            format: None,
1277        }
1278    }
1279
1280    /// Creates a shared-formula group's **master** cell — the one carrying the group's actual
1281    /// formula text and its full `range` — and no cached value yet — see
1282    /// [`Cell::with_cached_value`] to attach one. Pair with [`Cell::shared_formula_follower`] for
1283    /// the group's other cells, using the same `group_index`. See [`CellValue::SharedFormula`]'s
1284    /// doc comment: this crate does not compute each follower's own shifted formula text.
1285    pub fn shared_formula(
1286        formula: impl Into<String>,
1287        group_index: u32,
1288        range: impl Into<String>,
1289    ) -> Self {
1290        Self {
1291            value: CellValue::SharedFormula {
1292                formula: Some(formula.into()),
1293                group_index,
1294                range: Some(range.into()),
1295                cached_value: None,
1296            },
1297            format: None,
1298        }
1299    }
1300
1301    /// Creates a shared-formula group's **follower** cell — no formula text and no `range`
1302    /// (matching what real Excel itself writes for a follower, just `t="shared" si=".."`), only the
1303    /// group's shared `group_index`. See [`Cell::shared_formula`] for the group's master cell.
1304    pub fn shared_formula_follower(group_index: u32) -> Self {
1305        Self {
1306            value: CellValue::SharedFormula {
1307                formula: None,
1308                group_index,
1309                range: None,
1310                cached_value: None,
1311            },
1312            format: None,
1313        }
1314    }
1315
1316    /// Attaches a cached result value to a formula cell and returns the cell for chaining. A no-op
1317    /// if this cell doesn't hold
1318    /// `CellValue::Formula`/`CellValue::ArrayFormula`/`CellValue::SharedFormula` (e.g. called on a
1319    /// `Cell::number`).
1320    pub fn with_cached_value(mut self, value: f64) -> Self {
1321        match &mut self.value {
1322            CellValue::Formula { cached_value, .. }
1323            | CellValue::ArrayFormula { cached_value, .. }
1324            | CellValue::SharedFormula { cached_value, .. } => {
1325                *cached_value = Some(value);
1326            }
1327            _ => {}
1328        }
1329        self
1330    }
1331
1332    /// Attaches a cell style and returns the cell for chaining.
1333    pub fn with_format(mut self, format: CellFormat) -> Self {
1334        self.format = Some(format);
1335        self
1336    }
1337}
1338
1339/// Lets [`Sheet::write_cell`] accept a raw string directly (`sheet.write_cell(1, 1, "Bonjour")`)
1340/// instead of requiring `Cell::text("Bonjour")` — same rationale as the `f64`/`bool` impls below;
1341/// see [`Sheet::write_cell`]'s doc comment.
1342impl From<&str> for Cell {
1343    fn from(text: &str) -> Self {
1344        Cell::text(text)
1345    }
1346}
1347
1348/// See the `&str` impl just above.
1349impl From<String> for Cell {
1350    fn from(text: String) -> Self {
1351        Cell::text(text)
1352    }
1353}
1354
1355/// Lets [`Sheet::write_cell`] accept a raw `f64` directly (`sheet.write_cell(1, 1, 42.0)`) instead
1356/// of requiring `Cell::number(42.0)`.
1357impl From<f64> for Cell {
1358    fn from(number: f64) -> Self {
1359        Cell::number(number)
1360    }
1361}
1362
1363/// Lets [`Sheet::write_cell`] accept a raw `bool` directly (`sheet.write_cell(1, 1, true)`) instead
1364/// of requiring `Cell::boolean(true)`.
1365impl From<bool> for Cell {
1366    fn from(value: bool) -> Self {
1367        Cell::boolean(value)
1368    }
1369}
1370
1371impl CellFormat {
1372    /// Creates a default cell format (no overrides at all — equivalent to not setting a format,
1373    /// only useful as a starting point for the `with_*` builder methods).
1374    pub fn new() -> Self {
1375        Self::default()
1376    }
1377
1378    /// Sets a custom number format code.
1379    pub fn with_number_format(mut self, format: impl Into<String>) -> Self {
1380        self.number_format = Some(format.into());
1381        self
1382    }
1383
1384    /// Sets bold.
1385    pub fn with_bold(mut self, bold: bool) -> Self {
1386        self.bold = bold;
1387        self
1388    }
1389
1390    /// Sets italic.
1391    pub fn with_italic(mut self, italic: bool) -> Self {
1392        self.italic = italic;
1393        self
1394    }
1395
1396    /// Sets the font size, in points.
1397    pub fn with_font_size(mut self, size: f64) -> Self {
1398        self.font_size = Some(size);
1399        self
1400    }
1401
1402    /// Sets the font color (8-hex-digit ARGB, e.g. `"FFFF0000"`).
1403    pub fn with_font_color(mut self, color: impl Into<String>) -> Self {
1404        self.font_color = Some(color.into());
1405        self
1406    }
1407
1408    /// Sets the solid background fill color (8-hex-digit ARGB).
1409    pub fn with_fill_color(mut self, color: impl Into<String>) -> Self {
1410        self.fill_color = Some(color.into());
1411        self
1412    }
1413
1414    /// Sets horizontal alignment.
1415    pub fn with_horizontal_alignment(mut self, alignment: HorizontalAlignment) -> Self {
1416        self.horizontal_alignment = Some(alignment);
1417        self
1418    }
1419
1420    /// Sets vertical alignment.
1421    pub fn with_vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
1422        self.vertical_alignment = Some(alignment);
1423        self
1424    }
1425
1426    /// Sets this cell's borders.
1427    pub fn with_border(mut self, border: Border) -> Self {
1428        self.border = border;
1429        self
1430    }
1431
1432    /// Sets the font name/family.
1433    pub fn with_font_name(mut self, font_name: impl Into<String>) -> Self {
1434        self.font_name = Some(font_name.into());
1435        self
1436    }
1437
1438    /// Sets underline.
1439    pub fn with_underline(mut self, underline: bool) -> Self {
1440        self.underline = underline;
1441        self
1442    }
1443
1444    /// Sets strikethrough.
1445    pub fn with_strike(mut self, strike: bool) -> Self {
1446        self.strike = strike;
1447        self
1448    }
1449
1450    /// Sets whether text wraps onto multiple lines within the cell.
1451    pub fn with_wrap_text(mut self, wrap_text: bool) -> Self {
1452        self.wrap_text = wrap_text;
1453        self
1454    }
1455
1456    /// Sets the indentation level.
1457    pub fn with_indent(mut self, indent: u32) -> Self {
1458        self.indent = Some(indent);
1459        self
1460    }
1461
1462    /// Sets the raw Excel text rotation value (see [`text_rotation`](CellFormat::text_rotation)'s
1463    /// doc comment for the encoding).
1464    pub fn with_text_rotation(mut self, text_rotation: u16) -> Self {
1465        self.text_rotation = Some(text_rotation);
1466        self
1467    }
1468
1469    /// Sets whether the font size shrinks to fit the cell's width.
1470    pub fn with_shrink_to_fit(mut self, shrink_to_fit: bool) -> Self {
1471        self.shrink_to_fit = shrink_to_fit;
1472        self
1473    }
1474
1475    /// Sets a non-solid pattern fill.
1476    pub fn with_pattern_fill(mut self, pattern_fill: PatternFill) -> Self {
1477        self.pattern_fill = Some(pattern_fill);
1478        self
1479    }
1480
1481    /// Sets a gradient fill.
1482    pub fn with_gradient_fill(mut self, gradient_fill: GradientFill) -> Self {
1483        self.gradient_fill = Some(gradient_fill);
1484        self
1485    }
1486
1487    /// Sets whether this cell is locked when the sheet is protected.
1488    pub fn with_locked(mut self, locked: bool) -> Self {
1489        self.locked = Some(locked);
1490        self
1491    }
1492
1493    /// Sets whether this cell's formula is hidden when the sheet is protected.
1494    pub fn with_formula_hidden(mut self, formula_hidden: bool) -> Self {
1495        self.formula_hidden = Some(formula_hidden);
1496        self
1497    }
1498
1499    /// Links this format to a named cell style (by name, resolved against
1500    /// [`Workbook::named_cell_styles`] at write time) and returns it for chaining.
1501    pub fn with_named_style(mut self, name: impl Into<String>) -> Self {
1502        self.named_style = Some(name.into());
1503        self
1504    }
1505}
1506
1507/// A [`CellFormat::pattern_fill`]'s pattern (`ST_PatternType`) — the solid fill is deliberately
1508/// excluded here (that's [`CellFormat::fill_color`]'s job, the common case); only the non-solid
1509/// patterns are modeled.
1510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1511pub enum PatternType {
1512    Gray125,
1513    Gray0625,
1514    DarkGray,
1515    MediumGray,
1516    LightGray,
1517    DarkHorizontal,
1518    DarkVertical,
1519    DarkDown,
1520    DarkUp,
1521    DarkGrid,
1522    DarkTrellis,
1523    LightHorizontal,
1524    LightVertical,
1525    LightDown,
1526    LightUp,
1527    LightGrid,
1528    LightTrellis,
1529}
1530
1531/// A cell's non-solid pattern fill (`<patternFill patternType="..">`, `CT_PatternFill`).
1532#[derive(Debug, Clone, PartialEq)]
1533pub struct PatternFill {
1534    pub pattern_type: PatternType,
1535    /// The pattern's foreground color (8-hex-digit ARGB) — the color the pattern's own lines/dots
1536    /// are drawn in.
1537    pub fg_color: Option<String>,
1538    /// The pattern's background color (8-hex-digit ARGB) — the color showing through between the
1539    /// pattern's lines/dots.
1540    pub bg_color: Option<String>,
1541}
1542
1543impl PatternFill {
1544    /// Creates a pattern fill with no colors set yet (Excel's own defaults — black foreground,
1545    /// white background — apply).
1546    pub fn new(pattern_type: PatternType) -> Self {
1547        Self {
1548            pattern_type,
1549            fg_color: None,
1550            bg_color: None,
1551        }
1552    }
1553
1554    /// Sets the foreground color and returns the fill for chaining.
1555    pub fn with_fg_color(mut self, color: impl Into<String>) -> Self {
1556        self.fg_color = Some(color.into());
1557        self
1558    }
1559
1560    /// Sets the background color and returns the fill for chaining.
1561    pub fn with_bg_color(mut self, color: impl Into<String>) -> Self {
1562        self.bg_color = Some(color.into());
1563        self
1564    }
1565}
1566
1567/// One color stop of a [`GradientFill`] (`<stop position="..">`, `CT_GradientStop`).
1568#[derive(Debug, Clone, PartialEq)]
1569pub struct GradientStop {
1570    /// The stop's position along the gradient, `0.0` to `1.0`.
1571    pub position: f64,
1572    /// 8-hex-digit ARGB color.
1573    pub color: String,
1574}
1575
1576impl GradientStop {
1577    pub fn new(position: f64, color: impl Into<String>) -> Self {
1578        Self {
1579            position,
1580            color: color.into(),
1581        }
1582    }
1583}
1584
1585/// A cell's linear gradient fill (`<gradientFill degree="..">`, `CT_GradientFill`'s `type="linear"`
1586/// form — the default, no `type` attribute written). The "path" (radial) gradient form is not
1587/// modeled.
1588#[derive(Debug, Clone, PartialEq)]
1589pub struct GradientFill {
1590    /// The gradient's angle, in degrees (`0` = left to right).
1591    pub angle: f64,
1592    /// The gradient's color stops, in order. Excel itself always expects at least two (a start and
1593    /// end color) — not enforced here.
1594    pub stops: Vec<GradientStop>,
1595}
1596
1597impl GradientFill {
1598    pub fn new(angle: f64, stops: Vec<GradientStop>) -> Self {
1599        Self { angle, stops }
1600    }
1601}
1602
1603/// A relational comparison operator, shared between [`ConditionalFormattingCondition::CellIs`] and
1604/// [`DataValidationKind`]'s `WholeNumber`/`Decimal` variants — ECMA-376 defines two separate simple
1605/// types for these (`ST_ConditionalFormattingOperator`/ `ST_DataValidationOperator`), but both
1606/// share exactly the same 8 relational values, so one Rust enum covers both. Neither schema's full
1607/// enumeration is modeled: `ST_ConditionalFormattingOperator`'s
1608/// `containsText`/`notContains`/`beginsWith`/`endsWith` are, despite sharing the same XML
1609/// attribute, really distinct rule *types* in Excel's own "Highlight Cells Rules" UI, not `cellIs`
1610/// variants — out of scope.
1611#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1612pub enum ComparisonOperator {
1613    LessThan,
1614    LessThanOrEqual,
1615    Equal,
1616    NotEqual,
1617    GreaterThanOrEqual,
1618    GreaterThan,
1619    Between,
1620    NotBetween,
1621}
1622
1623/// A conditional formatting rule (`<cfRule>`, `CT_CfRule`) applied over a cell range
1624/// (`<conditionalFormatting sqref="..">`, `CT_ConditionalFormatting`).
1625///
1626/// Only the two most common rule types are modeled — see [`ConditionalFormattingCondition`].
1627/// `CT_CfRule`'s many other types (`colorScale`, `dataBar`, `iconSet`, `top10`, `uniqueValues`,
1628/// `duplicateValues`, `containsText`, `timePeriod`, `aboveAverage`..) are not modeled — this
1629/// crate's scope here is deliberately narrow.
1630#[derive(Debug, Clone, PartialEq)]
1631pub struct ConditionalFormattingRule {
1632    /// The cell range this rule applies to (`sqref`, e.g. `"A1:A10"`).
1633    pub range: String,
1634    /// The rule's condition.
1635    pub condition: ConditionalFormattingCondition,
1636    /// The formatting applied to cells in `range` when `condition` is met — reuses [`CellFormat`],
1637    /// the same subset already modeled for plain cell styles, as the shape of the underlying
1638    /// differential format record (`<dxf>`, `CT_Dxf` — confirmed via ECMA-376 to be a sequence of
1639    /// the same kind of optional `font`/`numFmt`/`fill`/ `alignment`/`border`/`protection` children
1640    /// `CellFormat` already covers, minus borders/protection). Distinct `CellFormat`s across a
1641    /// workbook's conditional formatting rules are deduplicated into shared `xl/styles.xml`
1642    /// `<dxfs>` entries by the writer, see `writer.rs::collect_dxfs`.
1643    pub format: CellFormat,
1644}
1645
1646/// A conditional formatting rule's condition — only `cellIs` (a direct value comparison) and
1647/// `expression` (an arbitrary custom formula) are modeled, `CT_CfRule`'s two simplest and most
1648/// common types.
1649#[derive(Debug, Clone, PartialEq)]
1650pub enum ConditionalFormattingCondition {
1651    /// `type="cellIs"`: compares the cell's own value against one or two formulas using a
1652    /// relational operator (e.g. "greater than 0.5", or "between 10 and 20" when `formula2` is
1653    /// set).
1654    CellIs {
1655        operator: ComparisonOperator,
1656        /// The first (or only) comparison value/formula.
1657        formula1: String,
1658        /// The second comparison value/formula — only meaningful (and required by Excel) for
1659        /// `Between`/`NotBetween`.
1660        formula2: Option<String>,
1661    },
1662    /// `type="expression"`: an arbitrary formula, evaluated per cell in `range`, with the format
1663    /// applied when it evaluates to `TRUE`.
1664    Expression {
1665        /// The formula text (e.g. `"MOD(ROW(),2)=0"` for zebra striping).
1666        formula: String,
1667    },
1668    /// A 2- or 3-stop color scale (`<colorScale>`, `CT_ColorScale`). Unlike every other condition
1669    /// variant, a color scale ignores [`ConditionalFormattingRule::format`] entirely (its coloring
1670    /// comes from the stops themselves, not a `dxf`) — the writer never allocates a `dxfId` for
1671    /// this variant, see `writer.rs::write_conditional_formatting`.
1672    ColorScale(Vec<ColorScaleStop>),
1673    /// A data bar (`<dataBar>`, `CT_DataBar`) — min/max stops plus a single fill color. Like
1674    /// `ColorScale`, ignores `format`.
1675    DataBar {
1676        min: CfvoPosition,
1677        max: CfvoPosition,
1678        color: String,
1679    },
1680    /// A 3-icon set (`<iconSet>`, `CT_IconSet`) using Excel's own default equal-thirds percent
1681    /// thresholds (0/33/67) — only the icon set's visual style is configurable, not custom
1682    /// thresholds (out of scope). Like `ColorScale`, ignores `format`.
1683    IconSet(IconSetType),
1684    /// `type="top10"`: highlights the top (or bottom) `rank` items/percent of `range`.
1685    Top10 {
1686        rank: u32,
1687        /// Whether `rank` is a percentage rather than a raw count.
1688        percent: bool,
1689        /// Highlights the *bottom* `rank` instead of the top.
1690        bottom: bool,
1691    },
1692    /// `type="aboveAverage"`: highlights cells above (or below) the range's average.
1693    AboveAverage {
1694        /// Highlights above-average cells (`true`) or below-average ones (`false`).
1695        above: bool,
1696        /// Also highlights cells exactly equal to the average.
1697        equal_average: bool,
1698    },
1699    /// `type="duplicateValues"`: highlights cells whose value appears more than once in `range`.
1700    DuplicateValues,
1701    /// `type="uniqueValues"`: highlights cells whose value appears exactly once in `range`.
1702    UniqueValues,
1703    /// `type="containsText"`/`"notContains"`/`"beginsWith"`/`"endsWith"` (four distinct `CT_CfRule`
1704    /// types sharing the same shape, see [`TextComparisonOperator`]): highlights cells whose text
1705    /// matches `text` per `operator`. Unlike `ConditionalFormattingCondition::CellIs`, Excel
1706    /// auto-generates the underlying formula from `text` and the range's first cell — this crate
1707    /// reproduces that (see `writer.rs::write_conditional_formatting`), it isn't the caller's
1708    /// formula to write.
1709    ContainsText {
1710        operator: TextComparisonOperator,
1711        text: String,
1712    },
1713}
1714
1715/// A [`ConditionalFormattingCondition::ColorScale`] stop: a threshold position plus the color
1716/// applied there (colors between two stops are interpolated by Excel itself, not written to the
1717/// file).
1718#[derive(Debug, Clone, PartialEq)]
1719pub struct ColorScaleStop {
1720    pub position: CfvoPosition,
1721    /// 8-hex-digit ARGB color.
1722    pub color: String,
1723}
1724
1725impl ColorScaleStop {
1726    pub fn new(position: CfvoPosition, color: impl Into<String>) -> Self {
1727        Self {
1728            position,
1729            color: color.into(),
1730        }
1731    }
1732}
1733
1734/// A threshold position for a [`ColorScaleStop`]/`DataBar` min or max (`<cfvo type=".." val="..">`,
1735/// `CT_Cfvo`) — only the three most common `ST_CfvoType` values are modeled;
1736/// `formula`/`percentile`/`autoMin`/ `autoMax` are not.
1737#[derive(Debug, Clone, PartialEq)]
1738pub enum CfvoPosition {
1739    /// `type="min"`: the range's own minimum value — `Min`/`Max` carry no `val` attribute at all
1740    /// when written.
1741    Min,
1742    /// `type="max"`: the range's own maximum value.
1743    Max,
1744    /// `type="percent"`, e.g. `Percent(50.0)` for the 50th percentile point.
1745    Percent(f64),
1746    /// `type="num"`: a fixed literal value.
1747    Number(f64),
1748}
1749
1750/// A [`ConditionalFormattingCondition::IconSet`]'s visual style (`ST_IconSetType`) — only the
1751/// handful of 3-icon sets most commonly used are modeled.
1752#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1753pub enum IconSetType {
1754    ThreeTrafficLights,
1755    ThreeArrows,
1756    ThreeFlags,
1757    ThreeSymbols,
1758}
1759
1760/// The comparison performed by a [`ConditionalFormattingCondition::ContainsText`] rule — shares the
1761/// `cellIs`-adjacent `operator` attribute name with [`ComparisonOperator`], but is a distinct,
1762/// disjoint set of values (`ST_ConditionalFormattingOperator`'s text-comparison subset), so it's
1763/// its own enum rather than reusing `ComparisonOperator`.
1764#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1765pub enum TextComparisonOperator {
1766    Contains,
1767    NotContains,
1768    BeginsWith,
1769    EndsWith,
1770}
1771
1772impl ConditionalFormattingRule {
1773    /// Creates a `cellIs` rule.
1774    pub fn cell_is(
1775        range: impl Into<String>,
1776        operator: ComparisonOperator,
1777        formula1: impl Into<String>,
1778        format: CellFormat,
1779    ) -> Self {
1780        Self {
1781            range: range.into(),
1782            condition: ConditionalFormattingCondition::CellIs {
1783                operator,
1784                formula1: formula1.into(),
1785                formula2: None,
1786            },
1787            format,
1788        }
1789    }
1790
1791    /// Sets the second comparison formula (for `Between`/`NotBetween`) and returns the rule for
1792    /// chaining. A no-op if this rule isn't a `CellIs` condition.
1793    pub fn with_second_formula(mut self, formula2: impl Into<String>) -> Self {
1794        if let ConditionalFormattingCondition::CellIs {
1795            formula2: existing, ..
1796        } = &mut self.condition
1797        {
1798            *existing = Some(formula2.into());
1799        }
1800        self
1801    }
1802
1803    /// Creates an `expression` rule.
1804    pub fn expression(
1805        range: impl Into<String>,
1806        formula: impl Into<String>,
1807        format: CellFormat,
1808    ) -> Self {
1809        Self {
1810            range: range.into(),
1811            condition: ConditionalFormattingCondition::Expression {
1812                formula: formula.into(),
1813            },
1814            format,
1815        }
1816    }
1817
1818    /// Creates a color scale rule. `format` is ignored for this variant (see
1819    /// [`ConditionalFormattingCondition::ColorScale`]'s doc comment) — always
1820    /// `CellFormat::default()`.
1821    pub fn color_scale(range: impl Into<String>, stops: Vec<ColorScaleStop>) -> Self {
1822        Self {
1823            range: range.into(),
1824            condition: ConditionalFormattingCondition::ColorScale(stops),
1825            format: CellFormat::default(),
1826        }
1827    }
1828
1829    /// Creates a data bar rule. `format` is ignored, same as `color_scale`.
1830    pub fn data_bar(
1831        range: impl Into<String>,
1832        min: CfvoPosition,
1833        max: CfvoPosition,
1834        color: impl Into<String>,
1835    ) -> Self {
1836        Self {
1837            range: range.into(),
1838            condition: ConditionalFormattingCondition::DataBar {
1839                min,
1840                max,
1841                color: color.into(),
1842            },
1843            format: CellFormat::default(),
1844        }
1845    }
1846
1847    /// Creates a 3-icon-set rule with Excel's own default equal-thirds percent thresholds. `format`
1848    /// is ignored, same as `color_scale`.
1849    pub fn icon_set(range: impl Into<String>, icon_set: IconSetType) -> Self {
1850        Self {
1851            range: range.into(),
1852            condition: ConditionalFormattingCondition::IconSet(icon_set),
1853            format: CellFormat::default(),
1854        }
1855    }
1856
1857    /// Creates a `top10` rule (top `rank` items/percent).
1858    pub fn top10(range: impl Into<String>, rank: u32, percent: bool, format: CellFormat) -> Self {
1859        Self {
1860            range: range.into(),
1861            condition: ConditionalFormattingCondition::Top10 {
1862                rank,
1863                percent,
1864                bottom: false,
1865            },
1866            format,
1867        }
1868    }
1869
1870    /// Sets `top10`'s `bottom` flag (highlight the bottom `rank` instead) and returns the rule for
1871    /// chaining. A no-op for any other condition.
1872    pub fn with_bottom(mut self, bottom: bool) -> Self {
1873        if let ConditionalFormattingCondition::Top10 {
1874            bottom: existing, ..
1875        } = &mut self.condition
1876        {
1877            *existing = bottom;
1878        }
1879        self
1880    }
1881
1882    /// Creates an `aboveAverage` rule.
1883    pub fn above_average(range: impl Into<String>, above: bool, format: CellFormat) -> Self {
1884        Self {
1885            range: range.into(),
1886            condition: ConditionalFormattingCondition::AboveAverage {
1887                above,
1888                equal_average: false,
1889            },
1890            format,
1891        }
1892    }
1893
1894    /// Creates a `duplicateValues` rule.
1895    pub fn duplicate_values(range: impl Into<String>, format: CellFormat) -> Self {
1896        Self {
1897            range: range.into(),
1898            condition: ConditionalFormattingCondition::DuplicateValues,
1899            format,
1900        }
1901    }
1902
1903    /// Creates a `uniqueValues` rule.
1904    pub fn unique_values(range: impl Into<String>, format: CellFormat) -> Self {
1905        Self {
1906            range: range.into(),
1907            condition: ConditionalFormattingCondition::UniqueValues,
1908            format,
1909        }
1910    }
1911
1912    /// Creates a text-comparison rule (`containsText`/`notContains`/ `beginsWith`/`endsWith`).
1913    pub fn contains_text(
1914        range: impl Into<String>,
1915        operator: TextComparisonOperator,
1916        text: impl Into<String>,
1917        format: CellFormat,
1918    ) -> Self {
1919        Self {
1920            range: range.into(),
1921            condition: ConditionalFormattingCondition::ContainsText {
1922                operator,
1923                text: text.into(),
1924            },
1925            format,
1926        }
1927    }
1928}
1929
1930/// An input or error message attached to a [`DataValidationRule`] (`promptTitle`/`prompt` or
1931/// `errorTitle`/`error` on `CT_DataValidation`).
1932#[derive(Debug, Clone, PartialEq)]
1933pub struct Message {
1934    /// The message's title (shown in bold in Excel's popup/tooltip).
1935    pub title: Option<String>,
1936    /// The message's body text.
1937    pub text: String,
1938}
1939
1940impl Message {
1941    /// Creates a message with just body text, no title.
1942    pub fn new(text: impl Into<String>) -> Self {
1943        Self {
1944            title: None,
1945            text: text.into(),
1946        }
1947    }
1948
1949    /// Sets a title and returns the message for chaining.
1950    pub fn with_title(mut self, title: impl Into<String>) -> Self {
1951        self.title = Some(title.into());
1952        self
1953    }
1954}
1955
1956/// A data validation rule (`<dataValidation>`, `CT_DataValidation`) applied over a cell range
1957/// (`sqref`).
1958///
1959/// Only three of `ST_DataValidationType`'s eight values are modeled — see [`DataValidationKind`];
1960/// `date`/`time`/`textLength`/`custom` are not. `showDropDown`/`imeMode` are not modeled either
1961/// (the former is a rarely-touched, confusingly-inverted flag most libraries leave alone; the
1962/// latter is IME-input-method behavior, irrelevant outside East Asian input).
1963#[derive(Debug, Clone, PartialEq)]
1964pub struct DataValidationRule {
1965    /// The cell range this rule applies to (`sqref`).
1966    pub range: String,
1967    /// The validation performed.
1968    pub kind: DataValidationKind,
1969    /// Whether a blank cell is considered valid (`allowBlank`).
1970    pub allow_blank: bool,
1971    /// An input message shown when the cell is selected (`showInputMessage` is written
1972    /// automatically when this is `Some`).
1973    pub input_message: Option<Message>,
1974    /// An error message shown when an invalid value is entered (`showErrorMessage` is written
1975    /// automatically when this is `Some`). Always written with `errorStyle="stop"` (blocking) — the
1976    /// `warning`/ `information` (non-blocking) styles are not modeled.
1977    pub error_message: Option<Message>,
1978}
1979
1980/// The validation performed by a [`DataValidationRule`].
1981#[derive(Debug, Clone, PartialEq)]
1982pub enum DataValidationKind {
1983    /// `type="list"`: the cell's value must match one entry from a fixed list — Excel shows a
1984    /// dropdown. `source` is written verbatim as `formula1`, so it can be either a literal
1985    /// comma-separated list wrapped in quotes (e.g. `"\"Yes,No,Maybe\""`) or a reference to a range
1986    /// holding the valid values (e.g. `"$D$1:$D$3"`).
1987    List { source: String },
1988    /// `type="whole"`: the cell's value must be a whole number satisfying `operator` against
1989    /// `formula1` (and `formula2`, for `Between`/ `NotBetween`).
1990    WholeNumber {
1991        operator: ComparisonOperator,
1992        formula1: String,
1993        formula2: Option<String>,
1994    },
1995    /// `type="decimal"`: like `WholeNumber`, but the value may have a fractional part.
1996    Decimal {
1997        operator: ComparisonOperator,
1998        formula1: String,
1999        formula2: Option<String>,
2000    },
2001    /// `type="date"`: the cell's value must be a date satisfying `operator` against
2002    /// `formula1`/`formula2`. `formula1`/`formula2` are written verbatim, so the caller is
2003    /// responsible for passing an Excel date serial number (as text, e.g. `"45000"`) or a
2004    /// date-returning formula — same "storage, not a conversion helper" posture as everywhere else
2005    /// formula text is stored in this crate; see [`crate::ExcelDateTime`] plus
2006    /// `writer.rs::excel_serial_date` if a caller needs to compute one.
2007    Date {
2008        operator: ComparisonOperator,
2009        formula1: String,
2010        formula2: Option<String>,
2011    },
2012    /// `type="time"`: like `Date`, but for a time-of-day value (a fractional day, e.g. `"0.5"` for
2013    /// noon).
2014    Time {
2015        operator: ComparisonOperator,
2016        formula1: String,
2017        formula2: Option<String>,
2018    },
2019    /// `type="textLength"`: the cell's text length must satisfy `operator` against
2020    /// `formula1`/`formula2`.
2021    TextLength {
2022        operator: ComparisonOperator,
2023        formula1: String,
2024        formula2: Option<String>,
2025    },
2026    /// `type="custom"`: an arbitrary formula, evaluated per cell, the value is valid when it
2027    /// evaluates to `TRUE` — no `operator`, unlike every other variant.
2028    Custom { formula: String },
2029}
2030
2031impl DataValidationRule {
2032    /// Creates a `list` validation rule with no messages, blanks allowed.
2033    pub fn list(range: impl Into<String>, source: impl Into<String>) -> Self {
2034        Self {
2035            range: range.into(),
2036            kind: DataValidationKind::List {
2037                source: source.into(),
2038            },
2039            allow_blank: true,
2040            input_message: None,
2041            error_message: None,
2042        }
2043    }
2044
2045    /// Creates a `whole` validation rule with no messages, blanks allowed.
2046    pub fn whole_number(
2047        range: impl Into<String>,
2048        operator: ComparisonOperator,
2049        formula1: impl Into<String>,
2050    ) -> Self {
2051        Self {
2052            range: range.into(),
2053            kind: DataValidationKind::WholeNumber {
2054                operator,
2055                formula1: formula1.into(),
2056                formula2: None,
2057            },
2058            allow_blank: true,
2059            input_message: None,
2060            error_message: None,
2061        }
2062    }
2063
2064    /// Creates a `decimal` validation rule with no messages, blanks allowed.
2065    pub fn decimal(
2066        range: impl Into<String>,
2067        operator: ComparisonOperator,
2068        formula1: impl Into<String>,
2069    ) -> Self {
2070        Self {
2071            range: range.into(),
2072            kind: DataValidationKind::Decimal {
2073                operator,
2074                formula1: formula1.into(),
2075                formula2: None,
2076            },
2077            allow_blank: true,
2078            input_message: None,
2079            error_message: None,
2080        }
2081    }
2082
2083    /// Creates a `date` validation rule with no messages, blanks allowed.
2084    pub fn date(
2085        range: impl Into<String>,
2086        operator: ComparisonOperator,
2087        formula1: impl Into<String>,
2088    ) -> Self {
2089        Self {
2090            range: range.into(),
2091            kind: DataValidationKind::Date {
2092                operator,
2093                formula1: formula1.into(),
2094                formula2: None,
2095            },
2096            allow_blank: true,
2097            input_message: None,
2098            error_message: None,
2099        }
2100    }
2101
2102    /// Creates a `time` validation rule with no messages, blanks allowed.
2103    pub fn time(
2104        range: impl Into<String>,
2105        operator: ComparisonOperator,
2106        formula1: impl Into<String>,
2107    ) -> Self {
2108        Self {
2109            range: range.into(),
2110            kind: DataValidationKind::Time {
2111                operator,
2112                formula1: formula1.into(),
2113                formula2: None,
2114            },
2115            allow_blank: true,
2116            input_message: None,
2117            error_message: None,
2118        }
2119    }
2120
2121    /// Creates a `textLength` validation rule with no messages, blanks allowed.
2122    pub fn text_length(
2123        range: impl Into<String>,
2124        operator: ComparisonOperator,
2125        formula1: impl Into<String>,
2126    ) -> Self {
2127        Self {
2128            range: range.into(),
2129            kind: DataValidationKind::TextLength {
2130                operator,
2131                formula1: formula1.into(),
2132                formula2: None,
2133            },
2134            allow_blank: true,
2135            input_message: None,
2136            error_message: None,
2137        }
2138    }
2139
2140    /// Creates a `custom` validation rule with no messages, blanks allowed.
2141    pub fn custom(range: impl Into<String>, formula: impl Into<String>) -> Self {
2142        Self {
2143            range: range.into(),
2144            kind: DataValidationKind::Custom {
2145                formula: formula.into(),
2146            },
2147            allow_blank: true,
2148            input_message: None,
2149            error_message: None,
2150        }
2151    }
2152
2153    /// Sets the second comparison formula (for `Between`/`NotBetween`) on a
2154    /// `WholeNumber`/`Decimal`/`Date`/`Time`/`TextLength` rule and returns it for chaining. A no-op
2155    /// for a `List`/`Custom` rule.
2156    pub fn with_second_formula(mut self, formula2: impl Into<String>) -> Self {
2157        match &mut self.kind {
2158            DataValidationKind::WholeNumber {
2159                formula2: existing, ..
2160            }
2161            | DataValidationKind::Decimal {
2162                formula2: existing, ..
2163            }
2164            | DataValidationKind::Date {
2165                formula2: existing, ..
2166            }
2167            | DataValidationKind::Time {
2168                formula2: existing, ..
2169            }
2170            | DataValidationKind::TextLength {
2171                formula2: existing, ..
2172            } => {
2173                *existing = Some(formula2.into());
2174            }
2175            DataValidationKind::List { .. } | DataValidationKind::Custom { .. } => {}
2176        }
2177        self
2178    }
2179
2180    /// Sets whether a blank cell is considered valid and returns the rule for chaining.
2181    pub fn with_allow_blank(mut self, allow_blank: bool) -> Self {
2182        self.allow_blank = allow_blank;
2183        self
2184    }
2185
2186    /// Sets the input message and returns the rule for chaining.
2187    pub fn with_input_message(mut self, message: Message) -> Self {
2188        self.input_message = Some(message);
2189        self
2190    }
2191
2192    /// Sets the error message and returns the rule for chaining.
2193    pub fn with_error_message(mut self, message: Message) -> Self {
2194        self.error_message = Some(message);
2195        self
2196    }
2197}
2198
2199/// A named range/formula/constant (`<definedName>`, `CT_DefinedName`) — unlike
2200/// [`ConditionalFormattingRule`]/[`DataValidationRule`], this is a workbook-level construct, not
2201/// attached to any particular [`Sheet`], even when [`local_sheet_id`](DefinedName::local_sheet_id)
2202/// scopes it to one.
2203///
2204/// Only `name`/`refers_to`/`local_sheet_id`/`hidden`/`comment` are modeled. `CT_DefinedName`'s
2205/// remaining attributes (`function`/
2206/// `vbProcedure`/`xlm`/`functionGroupId`/`shortcutKey`/`publishToServer`/
2207/// `workbookParameter`/`customMenu`/`description`/`help`/`statusBar`) are all macro/add-in/UI-text
2208/// concerns with no real demand seen yet, same scope-reduction posture as elsewhere in this crate.
2209/// SpreadsheetML's reserved `_xlnm.*` built-in names (print area, print titles, filter database..)
2210/// aren't given any special handling — a caller can write one like any other name, but nothing here
2211/// validates or treats it specially.
2212#[derive(Debug, Clone, PartialEq)]
2213pub struct DefinedName {
2214    /// The name shown in Excel's Name Manager (`name`, required).
2215    pub name: String,
2216    /// The formula, range reference, or constant this name refers to (e.g. `"Feuil1!$A$1:$B$10"`),
2217    /// written as the element's own text content — unlike a conditional formatting/data validation
2218    /// rule's formula, `CT_DefinedName` has simple content (`ST_Formula`), not a dedicated child
2219    /// element.
2220    pub refers_to: String,
2221    /// If `Some`, scopes this name to a single sheet rather than the whole workbook
2222    /// (`localSheetId`, a 0-based index into [`Workbook.sheets`](Workbook::sheets) in declaration
2223    /// order) — a sheet-scoped name can be duplicated across different sheets' scopes, unlike a
2224    /// workbook-scoped one. `None` means workbook-wide scope, the common case.
2225    pub local_sheet_id: Option<usize>,
2226    /// Whether this name is hidden in Excel's Name Manager UI (`hidden`).
2227    pub hidden: bool,
2228    /// An optional comment shown for this name (`comment`).
2229    pub comment: Option<String>,
2230}
2231
2232impl DefinedName {
2233    /// Creates a workbook-scoped defined name, not hidden, no comment.
2234    pub fn new(name: impl Into<String>, refers_to: impl Into<String>) -> Self {
2235        Self {
2236            name: name.into(),
2237            refers_to: refers_to.into(),
2238            local_sheet_id: None,
2239            hidden: false,
2240            comment: None,
2241        }
2242    }
2243
2244    /// Scopes this name to a single sheet (by its 0-based position in
2245    /// [`Workbook.sheets`](Workbook::sheets)) and returns it for chaining.
2246    pub fn with_local_sheet_id(mut self, local_sheet_id: usize) -> Self {
2247        self.local_sheet_id = Some(local_sheet_id);
2248        self
2249    }
2250
2251    /// Sets whether this name is hidden in Excel's Name Manager and returns it for chaining.
2252    pub fn with_hidden(mut self, hidden: bool) -> Self {
2253        self.hidden = hidden;
2254        self
2255    }
2256
2257    /// Sets this name's comment and returns it for chaining.
2258    pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
2259        self.comment = Some(comment.into());
2260        self
2261    }
2262}
2263
2264/// A sheet's tab visibility (`<sheet state=".">`, `ST_SheetState`) — replacing the plain `bool`
2265/// used since. `VeryHidden` (unlike `Hidden`) cannot be un-hidden from Excel's own "Unhide." dialog
2266/// — only from the VBA object model or by editing the file directly — real-world usage is normally
2267/// to hide implementation-detail sheets (lookup tables, staging data) from end users while still
2268/// letting formulas reference them.
2269#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2270pub enum SheetVisibility {
2271    #[default]
2272    Visible,
2273    Hidden,
2274    VeryHidden,
2275}
2276
2277/// A single column's width/visibility/outline setting (`<col>`, `CT_Col`) — extended for
2278/// `collapsed`.
2279#[derive(Debug, Clone, PartialEq)]
2280pub struct ColumnSetting {
2281    /// The 0-based column index this setting applies to (e.g. `0` for column A) — written as `min
2282    /// == max == column + 1` (see `Sheet.column_settings`'s doc comment for why this crate doesn't
2283    /// bother collapsing adjacent columns into a single ranged `<col>`).
2284    pub column: usize,
2285    /// The column's width, in Excel's own character-width units. `None` leaves the column at
2286    /// Excel's default width.
2287    pub width: Option<f64>,
2288    /// Whether this column is hidden.
2289    pub hidden: bool,
2290    /// This column's outline (grouping) level (0-7).
2291    pub outline_level: u8,
2292    /// Whether this column's outline group is shown collapsed (the "-" summary-only state of the
2293    /// little outline button). Distinct from `outline_level` itself (which columns belong to the
2294    /// group) — this is only the group's current expanded/collapsed *display* state.
2295    pub collapsed: bool,
2296    /// This column's default cell format (`<col style="n">`, an index into `xl/styles.xml`'s
2297    /// `cellXfs`). Applied by real Excel to any cell in this column that doesn't carry its own
2298    /// explicit `<c s="..">` — e.g. a brand-new cell typed into an empty row of this column picks
2299    /// up this format automatically. Distinct from [`Cell::format`]: a cell's own format always
2300    /// wins over its column's default when both are set (same precedence as real Excel's own
2301    /// column-formatting feature); this crate does not enforce or resolve that precedence itself,
2302    /// it only stores and writes each side's format independently, same "storage, not resolution"
2303    /// posture as elsewhere in this crate. Deduplicated into shared `cellXfs` entries by the writer
2304    /// exactly like `Cell::format` — see `writer.rs::collect_cell_formats`.
2305    pub style: Option<CellFormat>,
2306}
2307
2308impl ColumnSetting {
2309    /// Creates a column setting with no width/hidden/outline/style override yet (only useful as a
2310    /// starting point for the `with_*` builders).
2311    pub fn new(column: usize) -> Self {
2312        Self {
2313            column,
2314            width: None,
2315            hidden: false,
2316            outline_level: 0,
2317            collapsed: false,
2318            style: None,
2319        }
2320    }
2321
2322    /// Sets the column's width and returns it for chaining.
2323    pub fn with_width(mut self, width: f64) -> Self {
2324        self.width = Some(width);
2325        self
2326    }
2327
2328    /// Sets the column's default cell format and returns it for chaining.
2329    pub fn with_style(mut self, style: CellFormat) -> Self {
2330        self.style = Some(style);
2331        self
2332    }
2333
2334    /// Sets whether the column is hidden and returns it for chaining.
2335    pub fn with_hidden(mut self, hidden: bool) -> Self {
2336        self.hidden = hidden;
2337        self
2338    }
2339
2340    /// Sets whether this column's outline group is shown collapsed and returns it for chaining.
2341    pub fn with_collapsed(mut self, collapsed: bool) -> Self {
2342        self.collapsed = collapsed;
2343        self
2344    }
2345
2346    /// Sets the column's outline (grouping) level and returns it for chaining.
2347    pub fn with_outline_level(mut self, level: u8) -> Self {
2348        self.outline_level = level;
2349        self
2350    }
2351}
2352
2353/// Frozen panes (`<pane state="frozen">`, `CT_Pane`). See `Sheet.freeze_panes`'s doc comment for
2354/// why only "frozen" (not "split") panes are modeled.
2355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2356pub struct FreezePane {
2357    /// The number of columns frozen at the left (`0` = no column freeze).
2358    pub frozen_columns: usize,
2359    /// The number of rows frozen at the top (`0` = no row freeze).
2360    pub frozen_rows: usize,
2361}
2362
2363impl FreezePane {
2364    /// Creates a freeze-panes setting.
2365    pub fn new(frozen_columns: usize, frozen_rows: usize) -> Self {
2366        Self {
2367            frozen_columns,
2368            frozen_rows,
2369        }
2370    }
2371}
2372
2373/// A sheet's page orientation (`ST_Orientation`) — only `portrait`/ `landscape` are modeled,
2374/// `default` (a third schema value, treated by Excel as "portrait") is not, same posture as
2375/// `word-ooxml`'s own `PageOrientation`.
2376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2377pub enum PageOrientation {
2378    Portrait,
2379    Landscape,
2380}
2381
2382/// A sheet's print/page-setup settings. `PrintSettings::default()` (everything `None`) writes no
2383/// print-related XML at all.
2384///
2385/// `print_area`/`repeat_rows`/`repeat_columns` are, despite looking like plain worksheet settings,
2386/// actually implemented by real Excel as reserved sheet-scoped defined names (`_xlnm.Print_Area`/
2387/// `_xlnm.Print_Titles`, confirmed via ECMA-376 §18.2.6's "Print_Area"/"Print_Titles" built-in name
2388/// table) rather than a `<pageSetup>` attribute — this crate's writer synthesizes those
2389/// `DefinedName` entries automatically from each sheet's `Sheet.print_settings` when writing
2390/// `xl/workbook.xml`'s `<definedNames>` (see `writer.rs::synthesized_print_defined_names`), the
2391/// caller never constructs them directly.
2392#[derive(Debug, Default, Clone, PartialEq)]
2393pub struct PrintSettings {
2394    /// The print area (e.g. `"$A$1:$F$30"`) — implemented via `_xlnm.Print_Area`.
2395    pub print_area: Option<String>,
2396    /// Rows to repeat on every printed page (e.g. `"$1:$2"`) — implemented via
2397    /// `_xlnm.Print_Titles`.
2398    pub repeat_rows: Option<String>,
2399    /// Columns to repeat on every printed page (e.g. `"$A:$B"`) — implemented via
2400    /// `_xlnm.Print_Titles` (combined with `repeat_rows` into a single name's `refers_to` when both
2401    /// are set, exactly like real Excel's own "Rows to repeat at top" + "Columns to repeat at left"
2402    /// combined into one `Print_Titles` name).
2403    pub repeat_columns: Option<String>,
2404    /// Scales the printout to fit within this many pages wide. Setting either this or
2405    /// `fit_to_height` switches on Excel's "Fit to" print scaling mode (`<sheetPr><pageSetUpPr
2406    /// fitToPage="1"/></sheetPr>`); leaving the other `None` means "as many pages as needed" in
2407    /// that dimension, matching Excel's own "Fit to: N page(s) wide by (blank) tall" UI behavior
2408    /// (written as `fitToWidth="0"`/`fitToHeight="0"`, `CT_PageSetup`'s own convention for
2409    /// "unconstrained").
2410    pub fit_to_width: Option<u32>,
2411    /// Scales the printout to fit within this many pages tall.
2412    pub fit_to_height: Option<u32>,
2413    /// Scales the printout by this percentage (`<pageSetup scale="..">`, e.g. `90` for 90%).
2414    /// Distinct from `fit_to_width`/`fit_to_height`: real Excel's "Page Setup" dialog offers either
2415    /// "Adjust to: N%" (`scale`) or "Fit to: W x H page(s)" (`fit_to_width`/`fit_to_height`) as two
2416    /// mutually exclusive radio-button modes (`<sheetPr><pageSetUpPr fitToPage>` selects which of
2417    /// the two `<pageSetup>` honors) — setting both here is the caller's own responsibility to
2418    /// avoid, not validated.
2419    pub scale: Option<u32>,
2420    /// Page orientation. `None` uses Excel's own default (portrait).
2421    pub orientation: Option<PageOrientation>,
2422    /// The page header text (`<oddHeader>`), written verbatim — Excel's own `&L`/`&C`/`&R` section
2423    /// codes and `&P`/`&N`/`&D`/. field codes are the caller's responsibility to include if wanted,
2424    /// same "storage, not a template engine" posture as a formula's text.
2425    pub header: Option<String>,
2426    /// The page footer text (`<oddFooter>`), written verbatim.
2427    pub footer: Option<String>,
2428    /// A different header on the first printed page (`<firstHeader>`), written verbatim. Setting
2429    /// this (or [`footer_first`](PrintSettings::footer_first)) switches on `<headerFooter
2430    /// differentFirst="1">`.
2431    pub header_first: Option<String>,
2432    /// A different footer on the first printed page (`<firstFooter>`).
2433    pub footer_first: Option<String>,
2434    /// A different header on even printed pages (`<evenHeader>`). Setting this (or
2435    /// [`footer_even`](PrintSettings::footer_even)) switches on `<headerFooter
2436    /// differentOddEven="1">`; the existing
2437    /// [`header`](PrintSettings::header)/[`footer`](PrintSettings::footer) fields become the
2438    /// *odd*-page header/footer once this is set, same as real Excel's own
2439    /// `<oddHeader>`/`<oddFooter>` meaning "odd, or every page if `differentOddEven` isn't set".
2440    pub header_even: Option<String>,
2441    /// A different footer on even printed pages (`<evenFooter>`).
2442    pub footer_even: Option<String>,
2443}
2444
2445impl PrintSettings {
2446    /// Creates print settings with every field unset (equivalent to `PrintSettings::default()`,
2447    /// only useful as a starting point for the `with_*` builders).
2448    pub fn new() -> Self {
2449        Self::default()
2450    }
2451
2452    /// Sets the print area and returns the settings for chaining.
2453    pub fn with_print_area(mut self, area: impl Into<String>) -> Self {
2454        self.print_area = Some(area.into());
2455        self
2456    }
2457
2458    /// Sets the rows to repeat on every printed page and returns the settings for chaining.
2459    pub fn with_repeat_rows(mut self, rows: impl Into<String>) -> Self {
2460        self.repeat_rows = Some(rows.into());
2461        self
2462    }
2463
2464    /// Sets the columns to repeat on every printed page and returns the settings for chaining.
2465    pub fn with_repeat_columns(mut self, columns: impl Into<String>) -> Self {
2466        self.repeat_columns = Some(columns.into());
2467        self
2468    }
2469
2470    /// Sets the fit-to-width page count and returns the settings for chaining.
2471    pub fn with_fit_to_width(mut self, pages: u32) -> Self {
2472        self.fit_to_width = Some(pages);
2473        self
2474    }
2475
2476    /// Sets the fit-to-height page count and returns the settings for chaining.
2477    pub fn with_fit_to_height(mut self, pages: u32) -> Self {
2478        self.fit_to_height = Some(pages);
2479        self
2480    }
2481
2482    /// Sets the print scale percentage and returns the settings for chaining.
2483    pub fn with_scale(mut self, percent: u32) -> Self {
2484        self.scale = Some(percent);
2485        self
2486    }
2487
2488    /// Sets the page orientation and returns the settings for chaining.
2489    pub fn with_orientation(mut self, orientation: PageOrientation) -> Self {
2490        self.orientation = Some(orientation);
2491        self
2492    }
2493
2494    /// Sets the page header text and returns the settings for chaining.
2495    pub fn with_header(mut self, header: impl Into<String>) -> Self {
2496        self.header = Some(header.into());
2497        self
2498    }
2499
2500    /// Sets the page footer text and returns the settings for chaining.
2501    pub fn with_footer(mut self, footer: impl Into<String>) -> Self {
2502        self.footer = Some(footer.into());
2503        self
2504    }
2505
2506    /// Sets the first page's header text and returns the settings for chaining.
2507    pub fn with_header_first(mut self, header: impl Into<String>) -> Self {
2508        self.header_first = Some(header.into());
2509        self
2510    }
2511
2512    /// Sets the first page's footer text and returns the settings for chaining.
2513    pub fn with_footer_first(mut self, footer: impl Into<String>) -> Self {
2514        self.footer_first = Some(footer.into());
2515        self
2516    }
2517
2518    /// Sets the even pages' header text and returns the settings for chaining.
2519    pub fn with_header_even(mut self, header: impl Into<String>) -> Self {
2520        self.header_even = Some(header.into());
2521        self
2522    }
2523
2524    /// Sets the even pages' footer text and returns the settings for chaining.
2525    pub fn with_footer_even(mut self, footer: impl Into<String>) -> Self {
2526        self.footer_even = Some(footer.into());
2527        self
2528    }
2529}
2530
2531/// A sheet's protection settings (`<sheetProtection>`, `CT_SheetProtection`). Only the legacy
2532/// 16-bit password hash (`password="XXXX"`, still fully understood by every version of Excel even
2533/// though the UI has written the newer SHA-512 `hashValue`/`saltValue`/`spinCount` form since Excel
2534/// 2013 — see `writer.rs::legacy_password_hash`'s doc comment) is modeled. Every other
2535/// `CT_SheetProtection` flag (`formatCells`/`insertRows`/`sort`/ `autoFilter`/`objects`/..) is left
2536/// at Excel's own schema default — this crate only ever writes `sheet="1"` and, if set, `password`,
2537/// matching what checking Excel's own "Protect Sheet" dialog with no extra options touched
2538/// produces.
2539#[derive(Debug, Clone, PartialEq)]
2540pub struct SheetProtection {
2541    /// The password, in plain text — hashed into the legacy 16-bit form on write; never stored or
2542    /// written in plain text anywhere in the output file. `None` protects the sheet with no
2543    /// password at all (Excel's "Protect Sheet" with a blank password field — still blocks
2544    /// accidental edits through the UI, but anyone can immediately remove the protection since
2545    /// there's nothing to check a password against).
2546    pub password: Option<String>,
2547}
2548
2549impl SheetProtection {
2550    /// Protects the sheet with no password.
2551    pub fn locked() -> Self {
2552        Self { password: None }
2553    }
2554
2555    /// Protects the sheet with a password.
2556    pub fn with_password(password: impl Into<String>) -> Self {
2557        Self {
2558            password: Some(password.into()),
2559        }
2560    }
2561}
2562
2563/// Workbook *structure* protection (`<workbookProtection>`, `CT_WorkbookProtection`). Locks whether
2564/// sheets can be added/renamed/hidden/reordered/deleted (`lockStructure`) and/or whether the
2565/// workbook's window can be moved/resized/closed (`lockWindows`) — distinct from
2566/// [`SheetProtection`], which locks cell editing within one sheet. Uses the same legacy 16-bit
2567/// password hash as `SheetProtection` (see `writer.rs::legacy_password_hash`'s doc comment for the
2568/// algorithm and its caveats) — implemented but not yet verified against a real Excel installation.
2569#[derive(Debug, Clone, PartialEq)]
2570pub struct WorkbookProtection {
2571    /// The password, in plain text — hashed on write, never recoverable on read (same one-way
2572    /// posture as `SheetProtection::password`).
2573    pub password: Option<String>,
2574    /// Locks the workbook's structure (adding/renaming/hiding/reordering/ deleting sheets).
2575    pub lock_structure: bool,
2576    /// Locks the workbook's window (moving/resizing/closing).
2577    pub lock_windows: bool,
2578}
2579
2580impl WorkbookProtection {
2581    /// Locks the workbook's structure, no password, windows not locked.
2582    pub fn locked() -> Self {
2583        Self {
2584            password: None,
2585            lock_structure: true,
2586            lock_windows: false,
2587        }
2588    }
2589
2590    /// Sets a password and returns the protection for chaining.
2591    pub fn with_password(mut self, password: impl Into<String>) -> Self {
2592        self.password = Some(password.into());
2593        self
2594    }
2595
2596    /// Sets whether the workbook's window is locked and returns the protection for chaining.
2597    pub fn with_lock_windows(mut self, lock_windows: bool) -> Self {
2598        self.lock_windows = lock_windows;
2599        self
2600    }
2601}
2602
2603/// Write-protection / "read-only recommended" (`<fileSharing>`, `CT_FileSharing`) — point 54, see
2604/// [`Workbook::write_protection`]'s doc comment for how this differs from [`WorkbookProtection`].
2605/// Confirmed against `sml.xsd`: `readOnlyRecommended` defaults to `false`; `userName` is a plain
2606/// display string (shown in Excel's own "reserved by.." prompt); `reservationPassword` uses the
2607/// same legacy 16-bit hash as `WorkbookProtection`/`SheetProtection` (see
2608/// `writer.rs::legacy_password_hash`'s doc comment) — the newer SHA-512
2609/// `algorithmName`/`hashValue`/`saltValue`/`spinCount` form is not modeled, same "one well-defined
2610/// mechanism" posture as `ProtectedRange`.
2611#[derive(Debug, Default, Clone, PartialEq)]
2612pub struct WriteProtection {
2613    /// Recommends opening the file as read-only (`readOnlyRecommended`) — the checkbox behind
2614    /// Excel's Save As > Tools > General Options > "Read-only recommended".
2615    pub read_only_recommended: bool,
2616    /// The name shown in Excel's write-reservation prompt (`userName`), e.g. the name of the user
2617    /// who last saved with a reservation password set.
2618    pub user_name: Option<String>,
2619    /// The write-reservation password, in plain text — hashed into the legacy 16-bit form on write,
2620    /// never recoverable on read (same one-way posture as `WorkbookProtection::password`). Excel
2621    /// prompts for this password (or "Read Only") when opening the file.
2622    pub password: Option<String>,
2623}
2624
2625impl WriteProtection {
2626    /// Recommends opening the file as read-only, no password/user name set.
2627    pub fn recommended() -> Self {
2628        Self {
2629            read_only_recommended: true,
2630            user_name: None,
2631            password: None,
2632        }
2633    }
2634
2635    /// Sets the write-reservation password and returns it for chaining.
2636    pub fn with_password(mut self, password: impl Into<String>) -> Self {
2637        self.password = Some(password.into());
2638        self
2639    }
2640
2641    /// Sets the user name shown in the write-reservation prompt and returns it for chaining.
2642    pub fn with_user_name(mut self, user_name: impl Into<String>) -> Self {
2643        self.user_name = Some(user_name.into());
2644        self
2645    }
2646}
2647
2648/// One named, independently-passworded protected range (`<protectedRange>`, `CT_ProtectedRange`) —
2649/// point 51, see [`Sheet::protected_ranges`]'s doc comment for how this differs from
2650/// [`SheetProtection`]. Confirmed against `sml.xsd`: `name`/`sqref` are the only required
2651/// attributes; `password` uses the same legacy 16-bit hash as
2652/// `SheetProtection`/`WorkbookProtection` (see `writer.rs::legacy_password_hash`'s doc comment) —
2653/// the newer SHA-512 `algorithmName`/`hashValue`/`saltValue`/`spinCount` form and the optional
2654/// `securityDescriptor` child/attribute (a Windows ACL-based alternative to a password) are not
2655/// modeled, same "one well-defined mechanism, not every historical Excel option" posture as
2656/// `SheetProtection`.
2657#[derive(Debug, Clone, PartialEq)]
2658pub struct ProtectedRange {
2659    /// This range's name, shown in Excel's "Allow Users to Edit Ranges" dialog (must be unique
2660    /// within the sheet, not validated here).
2661    pub name: String,
2662    /// The range this entry covers (`sqref`, e.g. `"B2:D10"`) — like [`IgnoredError::range`],
2663    /// several disjoint cells can be packed into one `sqref` (space-separated) if the caller wants
2664    /// a single named range to cover them.
2665    pub range: String,
2666    /// The password unlocking this range while the sheet is otherwise protected, in plain text —
2667    /// hashed into the legacy 16-bit form on write, never stored or written in plain text (same
2668    /// one-way posture as `SheetProtection::password`). `None` leaves the range editable by anyone
2669    /// once the sheet itself is protected (no password prompt), same as
2670    /// `SheetProtection::password`'s `None` case.
2671    pub password: Option<String>,
2672}
2673
2674impl ProtectedRange {
2675    /// Creates a named protected range with no password.
2676    pub fn new(name: impl Into<String>, range: impl Into<String>) -> Self {
2677        Self {
2678            name: name.into(),
2679            range: range.into(),
2680            password: None,
2681        }
2682    }
2683
2684    /// Sets a password and returns the range for chaining.
2685    pub fn with_password(mut self, password: impl Into<String>) -> Self {
2686        self.password = Some(password.into());
2687        self
2688    }
2689}
2690
2691/// A per-cell hyperlink (`<hyperlink>`, `CT_Hyperlink`).
2692#[derive(Debug, Clone, PartialEq)]
2693pub struct CellHyperlink {
2694    /// The cell this hyperlink is attached to (e.g. `"A1"`).
2695    pub cell: String,
2696    /// The link's target.
2697    pub target: HyperlinkTarget,
2698    /// An optional tooltip shown on hover (`tooltip`).
2699    pub tooltip: Option<String>,
2700}
2701
2702/// A [`CellHyperlink`]'s target.
2703#[derive(Debug, Clone, PartialEq)]
2704pub enum HyperlinkTarget {
2705    /// An external URL (`r:id`, resolved through this worksheet's own `.rels` part,
2706    /// `TargetMode="External"` — same shape as `word-ooxml`'s `Hyperlink::External`).
2707    External(String),
2708    /// A location within the same workbook (`location`, e.g. `"Feuil2!A1"` or a defined name) — no
2709    /// relationship needed, same shape as `word-ooxml`'s `Hyperlink::Internal`.
2710    Internal(String),
2711}
2712
2713impl CellHyperlink {
2714    /// Creates an external hyperlink on the given cell.
2715    pub fn external(cell: impl Into<String>, url: impl Into<String>) -> Self {
2716        Self {
2717            cell: cell.into(),
2718            target: HyperlinkTarget::External(url.into()),
2719            tooltip: None,
2720        }
2721    }
2722
2723    /// Creates an internal hyperlink (to a cell/defined name elsewhere in this workbook) on the
2724    /// given cell.
2725    pub fn internal(cell: impl Into<String>, location: impl Into<String>) -> Self {
2726        Self {
2727            cell: cell.into(),
2728            target: HyperlinkTarget::Internal(location.into()),
2729            tooltip: None,
2730        }
2731    }
2732
2733    /// Sets a tooltip and returns the hyperlink for chaining.
2734    pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
2735        self.tooltip = Some(tooltip.into());
2736        self
2737    }
2738}
2739
2740/// A comment attached to a cell (legacy `xl/comments*.xml` + VML drawing, `CT_Comment`). Only
2741/// plain-text comments with a fixed author are modeled — `CT_Comment`'s rich-text (`CT_RElt` runs)
2742/// body and threaded-comment reply chains (`CT_ThreadedComment`, the modern Excel 365 "@mention"
2743/// comment form) are out of scope.
2744#[derive(Debug, Clone, PartialEq)]
2745pub struct CellComment {
2746    /// The cell this comment is attached to (e.g. `"B2"`).
2747    pub cell: String,
2748    /// The comment's author (shown in the comment box's first bold line by Excel's own convention,
2749    /// and stored as a separate `<author>` entry — not otherwise validated).
2750    pub author: String,
2751    /// The comment's body text — when [`rich_text`](CellComment::rich_text) is `Some`, this is
2752    /// simply the plain-text concatenation of every run's own text (kept in sync automatically by
2753    /// [`CellComment::with_rich_text`]), not a separate independent value.
2754    pub text: String,
2755    /// The comment's body as per-run formatted text (`<text><r>..</r></text>`, reusing
2756    /// [`RichTextRun`] — the same per-run bold/italic/color/size/ name/underline/strike model a
2757    /// rich-text cell's shared-string runs already use). `None` writes the plain single-run
2758    /// `<text><r><t>..</t></r></text>` form instead, using `text` verbatim.
2759    pub rich_text: Option<Vec<RichTextRun>>,
2760}
2761
2762impl CellComment {
2763    /// Creates a plain-text comment.
2764    pub fn new(
2765        cell: impl Into<String>,
2766        author: impl Into<String>,
2767        text: impl Into<String>,
2768    ) -> Self {
2769        Self {
2770            cell: cell.into(),
2771            author: author.into(),
2772            text: text.into(),
2773            rich_text: None,
2774        }
2775    }
2776
2777    /// Sets this comment's body as per-run formatted text and returns it for chaining. Also
2778    /// resolves `text` (the plain-text fallback) to the concatenation of every run's own text, so
2779    /// both fields stay consistent.
2780    pub fn with_rich_text(mut self, runs: Vec<RichTextRun>) -> Self {
2781        self.text = runs.iter().map(|run| run.text.as_str()).collect();
2782        self.rich_text = Some(runs);
2783        self
2784    }
2785}
2786
2787/// A custom table style definition (`<tableStyle>`, `CT_TableStyle`), declared workbook-wide in
2788/// `xl/styles.xml`'s `<tableStyles>` (`CT_TableStyles`) and referenced by name from an
2789/// [`ExcelTable`]'s own [`table_style_name`](ExcelTable::table_style_name). Confirmed against
2790/// `sml.xsd`: `CT_Stylesheet`'s own sequence puts `tableStyles` right after `dxfs`, before `colors`
2791/// (not written by this crate). This crate only ever writes `pivot="0"` (this style is for tables,
2792/// not pivot tables) and no `count` attribute (`CT_TableStyle`'s `count` is informational, Excel
2793/// recomputes it).
2794#[derive(Debug, Clone, PartialEq)]
2795pub struct TableStyle {
2796    /// This style's name, referenced by [`ExcelTable::table_style_name`] (must be unique within the
2797    /// workbook, not validated here — a name colliding with one of Excel's own dozens of built-in
2798    /// style names, e.g. `"TableStyleMedium2"`, silently shadows the built-in one for this
2799    /// workbook).
2800    pub name: String,
2801    /// The banding/region formats making up this style, in no particular required order (real Excel
2802    /// itself doesn't enforce one either).
2803    pub elements: Vec<TableStyleElement>,
2804}
2805
2806impl TableStyle {
2807    /// Creates a named table style with no elements yet.
2808    pub fn new(name: impl Into<String>) -> Self {
2809        Self {
2810            name: name.into(),
2811            elements: Vec::new(),
2812        }
2813    }
2814
2815    /// Appends a formatted element and returns the style for chaining.
2816    pub fn with_element(mut self, element: TableStyleElement) -> Self {
2817        self.elements.push(element);
2818        self
2819    }
2820}
2821
2822/// A named cell style (`<cellStyle>`/its paired `<cellStyleXfs>` `<xf>` entry,
2823/// `CT_CellStyle`/`CT_Xf`), declared workbook-wide in `xl/styles.xml` and referenced from any
2824/// [`CellFormat`] via [`CellFormat::named_style`]. This is Excel's "Cell Styles" gallery: built-in
2825/// entries like `Good`/`Bad`/ `Neutral`/`Heading 1`-`4`/`Input`/`Output`/`Calculation`/`Currency`/
2826/// `Percent`/`Comma`, or a caller's own custom-named style. Confirmed against `sml.xsd`:
2827/// `CT_CellStyle`'s only required attribute is `xfId` (this crate assigns it automatically, from
2828/// this entry's position in [`Workbook::named_cell_styles`], mirroring how a `dxfId`/table-style
2829/// `dxfId` is resolved rather than caller-supplied); `name`/`builtinId` are both optional but a
2830/// real style always sets `name` at least (an unnamed entry would show as blank in Excel's gallery,
2831/// so `name` is required here, not `Option`).
2832#[derive(Debug, Clone, PartialEq)]
2833pub struct NamedCellStyle {
2834    /// This style's name, shown in Excel's Cell Styles gallery and referenced by
2835    /// [`CellFormat::named_style`] (must be unique within the workbook, not validated here).
2836    pub name: String,
2837    /// The formatting this style applies (font/fill/border/number format/ alignment — the same
2838    /// [`CellFormat`] shape a regular cell format uses, written into its own dedicated
2839    /// `cellStyleXfs` entry rather than the shared `cellXfs` collection, mirroring how a `dxf`'s
2840    /// `CellFormat` is a separate index space too, see `writer.rs::collect_dxfs`'s doc comment).
2841    pub format: CellFormat,
2842    /// Identifies this as one of Excel's own built-in styles rather than a caller-defined one
2843    /// (`builtinId`, e.g. `0` = `"Normal"`, `3` = `"Good"`, `4` = `"Bad"`, `5` = `"Neutral"`,
2844    /// `7`-`10` = `"Heading 1"`-`"Heading 4"`. — `ST_CellStyleBuiltinId`'s full ~54-value table is
2845    /// not reproduced/validated here, this crate simply writes whatever `u32` the caller supplies
2846    /// verbatim). `None` for a purely custom style with no built-in equivalent.
2847    pub builtin_id: Option<u32>,
2848}
2849
2850impl NamedCellStyle {
2851    /// Creates a named cell style with the given name and format, no built-in id (a purely custom
2852    /// style).
2853    pub fn new(name: impl Into<String>, format: CellFormat) -> Self {
2854        Self {
2855            name: name.into(),
2856            format,
2857            builtin_id: None,
2858        }
2859    }
2860
2861    /// Sets the built-in id and returns the style for chaining.
2862    pub fn with_builtin_id(mut self, builtin_id: u32) -> Self {
2863        self.builtin_id = Some(builtin_id);
2864        self
2865    }
2866}
2867
2868/// One formatted region of a [`TableStyle`] (`<tableStyleElement>`, `CT_TableStyleElement`).
2869#[derive(Debug, Clone, PartialEq)]
2870pub struct TableStyleElement {
2871    /// Which region of the table this element formats.
2872    pub element_type: TableStyleElementType,
2873    /// The formatting applied to this region — written as a `<dxf>` entry in `xl/styles.xml`'s
2874    /// `<dxfs>` (the same shared, deduplicated collection a [`ConditionalFormattingRule`]'s own
2875    /// `format` uses, see `writer.rs::collect_dxfs`) and referenced here by `dxfId`, exactly like a
2876    /// conditional formatting rule.
2877    pub format: CellFormat,
2878}
2879
2880impl TableStyleElement {
2881    /// Creates a table style element.
2882    pub fn new(element_type: TableStyleElementType, format: CellFormat) -> Self {
2883        Self {
2884            element_type,
2885            format,
2886        }
2887    }
2888}
2889
2890/// Which region of a table a [`TableStyleElement`] formats (`ST_TableStyleType`) — only the most
2891/// commonly used values are modeled (whole-table default, header/total rows, and row/column
2892/// banding); `ST_TableStyleType`'s remaining values (`firstHeaderCell`/
2893/// `lastHeaderCell`/`firstTotalCell`/`lastTotalCell`/`firstSubtotalColumn`/
2894/// `secondSubtotalColumn`/`thirdSubtotalColumn`/`firstSubtotalRow`/
2895/// `secondSubtotalRow`/`thirdSubtotalRow`/`blankRow`/
2896/// `firstColumnSubheading`/`secondColumnSubheading`/`thirdColumnSubheading`/
2897/// `firstRowSubheading`/`secondRowSubheading`/`thirdRowSubheading`/
2898/// `pageFieldLabels`/`pageFieldValues`, confirmed via `sml.xsd`) cover niche PivotTable-report and
2899/// outline-subtotal layouts out of this crate's current scope.
2900#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2901pub enum TableStyleElementType {
2902    /// The whole table's default formatting (`"wholeTable"`).
2903    WholeTable,
2904    /// The header row (`"headerRow"`).
2905    HeaderRow,
2906    /// The totals row (`"totalRow"`).
2907    TotalRow,
2908    /// Odd data rows, 1-based from the header (`"firstRowStripe"`).
2909    FirstRowStripe,
2910    /// Even data rows (`"secondRowStripe"`).
2911    SecondRowStripe,
2912    /// Odd data columns (`"firstColumnStripe"`).
2913    FirstColumnStripe,
2914    /// Even data columns (`"secondColumnStripe"`).
2915    SecondColumnStripe,
2916    /// The table's first (leftmost) column (`"firstColumn"`).
2917    FirstColumn,
2918    /// The table's last (rightmost) column (`"lastColumn"`).
2919    LastColumn,
2920}
2921
2922/// A structured (`ListObject`) table (`<table>`, `CT_Table`) — extended for per-column
2923/// calculated-column formulas and totals-row functions, and for
2924/// [`table_style_name`](ExcelTable::table_style_name).
2925#[derive(Debug, Clone, PartialEq)]
2926pub struct ExcelTable {
2927    /// The table's name, shown in Excel's Name Manager/Table Design tab (must be unique within the
2928    /// workbook, not validated here).
2929    pub name: String,
2930    /// The table's full range, header row included (e.g. `"A1:D6"`).
2931    pub range: String,
2932    /// The table's columns, in order, matching the header row's actual cell text (not written into
2933    /// `<sheetData>` by this crate — the caller is responsible for the header row's own cell values
2934    /// matching each column's name, same "caller keeps the two in sync" posture as
2935    /// `ConditionalFormattingRule::range` referencing real cells).
2936    pub columns: Vec<TableColumn>,
2937    /// Whether the table shows a totals row as its last row.
2938    pub show_totals_row: bool,
2939    /// This table's remembered sort order (`<sortState>`, `CT_SortState`). Confirmed against
2940    /// `sml.xsd`: under `<table>` (`CT_Table`'s own sequence), `sortState` is a direct sibling of
2941    /// `autoFilter`, not nested inside it — distinct from [`Sheet::sort_state`], which *is* nested
2942    /// inside a plain worksheet-level `<autoFilter>` (`CT_AutoFilter`'s own sequence puts
2943    /// `sortState` after `filterColumn*`). Storage only: writing this records what Excel would show
2944    /// if the user reopened the table's sort dialog, it does not itself reorder `sheet.rows` — the
2945    /// caller remains responsible for the data actually being in that order, same "caller keeps
2946    /// things in sync" posture as `AutoFilterColumn`.
2947    pub sort_state: Option<SortState>,
2948    /// The named table style applied to this table (`<tableStyleInfo name="..">`). `None` keeps
2949    /// this crate's own long-standing default, `TableStyleMedium2` (Excel's built-in "Table Style
2950    /// Medium 2", the first entry in its table style gallery) — same behavior as before this point
2951    /// existed. `Some` can name either one of Excel's own dozens of built-in styles (e.g.
2952    /// `"TableStyleLight9"`) or a custom [`TableStyle`] declared in [`Workbook::table_styles`] —
2953    /// this crate does not itself validate that a `Some` name actually resolves to something Excel
2954    /// recognizes, same "caller's responsibility" posture as elsewhere (e.g. `Sheet.name`'s
2955    /// character-limit note).
2956    pub table_style_name: Option<String>,
2957}
2958
2959impl ExcelTable {
2960    /// Creates a table with no totals row, from a list of plain column names (no calculated-column
2961    /// formula, no totals-row function on any column yet) — the common case. Use
2962    /// [`ExcelTable::with_columns`] instead for full control over each [`TableColumn`].
2963    pub fn new<S: Into<String>>(
2964        name: impl Into<String>,
2965        range: impl Into<String>,
2966        columns: Vec<S>,
2967    ) -> Self {
2968        Self {
2969            name: name.into(),
2970            range: range.into(),
2971            columns: columns.into_iter().map(TableColumn::new).collect(),
2972            show_totals_row: false,
2973            sort_state: None,
2974            table_style_name: None,
2975        }
2976    }
2977
2978    /// Creates a table from a list of fully-specified [`TableColumn`]s.
2979    pub fn with_columns(
2980        name: impl Into<String>,
2981        range: impl Into<String>,
2982        columns: Vec<TableColumn>,
2983    ) -> Self {
2984        Self {
2985            name: name.into(),
2986            range: range.into(),
2987            columns,
2988            show_totals_row: false,
2989            sort_state: None,
2990            table_style_name: None,
2991        }
2992    }
2993
2994    /// Sets whether the table shows a totals row and returns it for chaining.
2995    pub fn with_totals_row(mut self, show: bool) -> Self {
2996        self.show_totals_row = show;
2997        self
2998    }
2999
3000    /// Sets this table's remembered sort order and returns it for chaining.
3001    pub fn with_sort_state(mut self, sort_state: SortState) -> Self {
3002        self.sort_state = Some(sort_state);
3003        self
3004    }
3005
3006    /// Sets this table's named style and returns it for chaining. See
3007    /// [`table_style_name`](ExcelTable::table_style_name)'s doc comment.
3008    pub fn with_table_style_name(mut self, name: impl Into<String>) -> Self {
3009        self.table_style_name = Some(name.into());
3010        self
3011    }
3012}
3013
3014/// One column of an [`ExcelTable`] (`<tableColumn>`, `CT_TableColumn`) — (plain `name` only),
3015/// extended for `calculated_formula`/ `totals_row_function`/`totals_row_label`.
3016#[derive(Debug, Clone, PartialEq)]
3017pub struct TableColumn {
3018    /// The column's name, matching the header row's actual cell text (see [`ExcelTable::columns`]'s
3019    /// doc comment).
3020    pub name: String,
3021    /// This column's calculated-column formula (`<calculatedColumnFormula>`), if any. Declaring a
3022    /// column calculated in the table definition only; this crate does **not** automatically
3023    /// propagate a `<f>` formula onto every data row's cell in this column — the caller remains
3024    /// responsible for each data row's own cell actually carrying a matching `CellValue::Formula`
3025    /// if the per-row formulas should appear too (storage only, same "caller keeps things in sync"
3026    /// posture as `ExcelTable::columns` itself).
3027    ///
3028    /// **Structured references must use the fully-qualified/expanded form, `TableName[[#This
3029    /// Row],[ColumnName]]` — not the shorthand `[@ColumnName]`.** Both are valid Excel syntax when
3030    /// *typed into the UI* (Excel silently normalizes one to the other), but a real Excel
3031    /// installation rejects and strips the whole `<table>` on open when the shorthand form is what
3032    /// ends up stored verbatim in `calculatedColumnFormula` (and/or a data row's own `<f>`) — a
3033    /// genuine Excel-authored file stores exactly this expanded form (`Table1[[#This
3034    /// Row],[ColumnName]]`) rather than `[@ColumnName]`. This crate does not rewrite or validate
3035    /// formula text — passing a shorthand structured reference here silently produces a file real
3036    /// Excel repairs on open.
3037    pub calculated_formula: Option<String>,
3038    /// This column's totals-row aggregate function (`<tableColumn totalsRowFunction="..">`), if
3039    /// any. Meaningless unless [`ExcelTable::show_totals_row`] is also `true`. Writes only the
3040    /// `totalsRowFunction=".."` attribute itself on `<tableColumn>` — unlike an earlier version of
3041    /// this crate, it does **not** auto-generate a matching
3042    /// `<totalsRowFormula>SUBTOTAL(n,[ColumnName])</totalsRowFormula>` *table-metadata child
3043    /// element* for a built-in function anymore: real Excel never writes that particular child
3044    /// element either (the function name alone determines what it computes internally).
3045    ///
3046    /// **This is a separate thing from the totals row's own cell in `sheetData`, which the caller
3047    /// remains responsible for** (same "caller keeps things in sync" posture as
3048    /// [`calculated_formula`](TableColumn::calculated_formula)): real Excel's own totals row is a
3049    /// genuine formula cell, not a plain cached number — confirmed against a real Excel-authored
3050    /// file, whose totals row cell for a `sum` column literally contains
3051    /// `<f>SUBTOTAL(109,TableName[ColumnName])</f>` with a cached `<v>`, and for `average`,
3052    /// `<f>SUBTOTAL(101,TableName[ColumnName])</f>`. The `10x`-prefixed `SUBTOTAL` function codes
3053    /// (ignoring manually-hidden rows) real Excel tables use are, in `TotalsRowFunction` order:
3054    /// `Average` → 101, `Count` → 102, `CountNums` → 103, `Max` → 104, `Min` → 105, `StdDev` → 107,
3055    /// `Var` → 110, `Sum` → 109 (`Custom` has no fixed code — the caller supplies
3056    /// [`totals_row_formula`](TableColumn::totals_row_formula) directly instead). A totals row cell
3057    /// left as a plain cached number instead of this formula is one of the concrete differences
3058    /// from real Excel output that causes a real Excel installation to reject and repair the file.
3059    /// Only [`TotalsRowFunction::Custom`] pairs with an explicit
3060    /// [`totals_row_formula`](TableColumn::totals_row_formula). Also see
3061    /// [`totals_row_label`](TableColumn::totals_row_label)'s doc comment: setting both on the same
3062    /// column, this field is silently dropped at write time.
3063    pub totals_row_function: Option<TotalsRowFunction>,
3064    /// This column's totals-row label (`<tableColumn totalsRowLabel="..">`), shown instead of a
3065    /// computed value — typically only meaningful on the *first* column of a totals row (e.g.
3066    /// `"Total"`). The same real Excel-authored reference file mentioned in `totals_row_function`'s
3067    /// doc comment also shows that the totals row's own cell for the label column carries the label
3068    /// text itself as a plain string value too (redundant with this attribute, but present) — the
3069    /// caller remains responsible for that cell's actual value matching. **Mutually exclusive with
3070    /// `totals_row_function` on the same column and enforced by the writer** (real Excel's own UI
3071    /// only ever offers one or the other per column): when both are set here, the label wins and
3072    /// `totals_row_function`/`totals_row_formula` are simply not written for that column.
3073    pub totals_row_label: Option<String>,
3074    /// This column's literal totals-row formula text (`<totalsRowFormula>`), only written when
3075    /// `totals_row_function` is [`TotalsRowFunction::Custom`] *and* `totals_row_label` is `None` —
3076    /// every other function variant writes no `<totalsRowFormula>` at all (see
3077    /// `totals_row_function`'s doc comment), and a label on the same column takes priority over
3078    /// both (see `totals_row_label`'s doc comment). If this formula uses a structured reference,
3079    /// see `calculated_formula`'s doc comment: use the expanded `TableName[[#This
3080    /// Row],[ColumnName]]` form, not `[@ColumnName]`.
3081    pub totals_row_formula: Option<String>,
3082}
3083
3084impl TableColumn {
3085    /// Creates a plain column with no calculated formula or totals-row function yet.
3086    pub fn new(name: impl Into<String>) -> Self {
3087        Self {
3088            name: name.into(),
3089            calculated_formula: None,
3090            totals_row_function: None,
3091            totals_row_label: None,
3092            totals_row_formula: None,
3093        }
3094    }
3095
3096    /// Sets this column's calculated-column formula and returns it for chaining.
3097    pub fn with_calculated_formula(mut self, formula: impl Into<String>) -> Self {
3098        self.calculated_formula = Some(formula.into());
3099        self
3100    }
3101
3102    /// Sets this column's totals-row aggregate function and returns it for chaining.
3103    pub fn with_totals_row_function(mut self, function: TotalsRowFunction) -> Self {
3104        self.totals_row_function = Some(function);
3105        self
3106    }
3107
3108    /// Sets this column's totals-row label and returns it for chaining.
3109    pub fn with_totals_row_label(mut self, label: impl Into<String>) -> Self {
3110        self.totals_row_label = Some(label.into());
3111        self
3112    }
3113
3114    /// Sets this column's literal totals-row formula text (only used when `totals_row_function` is
3115    /// [`TotalsRowFunction::Custom`]) and returns it for chaining.
3116    pub fn with_totals_row_formula(mut self, formula: impl Into<String>) -> Self {
3117        self.totals_row_formula = Some(formula.into());
3118        self
3119    }
3120}
3121
3122/// A remembered sort order (`<sortState>`, `CT_SortState`). Written in two different places
3123/// depending on context: as a child of a worksheet-level `<autoFilter>` ([`Sheet::sort_state`]) or
3124/// as a sibling of a table's own `<autoFilter>` ([`ExcelTable::sort_state`]) — see each field's own
3125/// doc comment for the exact schema positioning, confirmed via `sml.xsd`'s `CT_AutoFilter`/
3126/// `CT_Table` sequences. Only the simplest, most common shape is modeled: a plain value-based
3127/// ascending/descending sort per range (`ST_SortBy`'s `"value"`, the schema default) —
3128/// `sortBy="cellColor"/ "fontColor"/"icon"`, `customList`, and `dxfId`/`iconSet`/`iconId` (color/
3129/// icon-based custom sort criteria) are not modeled, same "the common case, not every historical
3130/// Excel option" posture as `SheetProtection`/`AutoFilterColumn`. Storage only — this crate does
3131/// not itself reorder any rows, see `ExcelTable.sort_state`'s doc comment.
3132#[derive(Debug, Clone, PartialEq)]
3133pub struct SortState {
3134    /// The overall range this sort applies to (`ref`, required) — e.g. the table's own data range,
3135    /// or the autofiltered range.
3136    pub range: String,
3137    /// Up to 64 sort keys, in priority order (`<sortCondition>`, `CT_SortState`'s own
3138    /// `maxOccurs="64"` limit — not enforced here, same "caller's responsibility" posture as
3139    /// elsewhere in this crate).
3140    pub conditions: Vec<SortCondition>,
3141}
3142
3143impl SortState {
3144    /// Creates a sort state over `range` with no sort keys yet.
3145    pub fn new(range: impl Into<String>) -> Self {
3146        Self {
3147            range: range.into(),
3148            conditions: Vec::new(),
3149        }
3150    }
3151
3152    /// Appends a sort key and returns the sort state for chaining.
3153    pub fn with_condition(mut self, condition: SortCondition) -> Self {
3154        self.conditions.push(condition);
3155        self
3156    }
3157}
3158
3159/// One sort key within a [`SortState`] (`<sortCondition>`, `CT_SortCondition`).
3160#[derive(Debug, Clone, PartialEq)]
3161pub struct SortCondition {
3162    /// The single column (or row, for a horizontal sort) this key sorts by, as a range reference
3163    /// (`ref`, required) — e.g. `"B2:B10"` for a sort keyed on column B's values.
3164    pub range: String,
3165    /// Whether this key sorts descending (`descending`, default ascending).
3166    pub descending: bool,
3167}
3168
3169impl SortCondition {
3170    /// Creates an ascending sort key over `range`.
3171    pub fn new(range: impl Into<String>) -> Self {
3172        Self {
3173            range: range.into(),
3174            descending: false,
3175        }
3176    }
3177
3178    /// Sets whether this key sorts descending and returns it for chaining.
3179    pub fn with_descending(mut self, descending: bool) -> Self {
3180        self.descending = descending;
3181        self
3182    }
3183}
3184
3185/// A table column's totals-row aggregate function (`ST_TotalsRowFunction`).
3186/// `ST_TotalsRowFunction`'s `"none"` value isn't modeled as its own variant, same convention as
3187/// elsewhere in this crate — `Option::None` on `TableColumn::totals_row_function` already expresses
3188/// it.
3189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3190pub enum TotalsRowFunction {
3191    Sum,
3192    Average,
3193    Count,
3194    CountNums,
3195    Max,
3196    Min,
3197    StdDev,
3198    Var,
3199    /// A totals-row function this crate doesn't auto-generate a formula for — pair with
3200    /// [`TableColumn::totals_row_formula`] set to the literal formula text the caller wants written
3201    /// verbatim.
3202    Custom,
3203}
3204
3205/// Document metadata (`docProps/core.xml`'s Dublin Core fields plus `docProps/app.xml`'s
3206/// `Company`/`Manager`/`HyperlinkBase`, and `docProps/custom.xml`'s user-defined properties) —
3207/// extended by. Before both `core.xml`/`app.xml` were always written but always empty/boilerplate
3208/// (see `writer.rs`'s `to_core_properties_xml`/ `to_app_properties_xml`). Only the handful of
3209/// fields a real document typically carries are modeled — `docProps/core.xml`'s
3210/// `lastModifiedBy`/`revision`/`created`/`modified`/`category`/`contentStatus` are still not.
3211#[derive(Debug, Default, Clone, PartialEq)]
3212pub struct DocumentProperties {
3213    /// `docProps/core.xml`'s `<dc:title>`.
3214    pub title: Option<String>,
3215    /// `docProps/core.xml`'s `<dc:creator>`.
3216    pub author: Option<String>,
3217    /// `docProps/core.xml`'s `<dc:subject>`.
3218    pub subject: Option<String>,
3219    /// `docProps/core.xml`'s `<cp:keywords>`.
3220    pub keywords: Option<String>,
3221    /// `docProps/app.xml`'s `<Company>`.
3222    pub company: Option<String>,
3223    /// `docProps/app.xml`'s `<Manager>`.
3224    pub manager: Option<String>,
3225    /// `docProps/app.xml`'s `<HyperlinkBase>` (the base URL relative hyperlinks in this workbook
3226    /// resolve against) — point 57.
3227    pub hyperlink_base: Option<String>,
3228    /// User-defined custom document properties (`docProps/custom.xml`,
3229    /// `CT_Properties`/`CT_Property`) — point 56, visible in Excel's File > Info > Properties >
3230    /// Advanced Properties > Custom tab. A `Vec` of `(name, value)` pairs, not a map: insertion
3231    /// order is preserved and determines the sequential `pid` each entry gets when written (`2`,
3232    /// `3`, `4`. — `0`/`1` are reserved by the format and never assigned), matching `word-ooxml`'s
3233    /// own `Document.custom_properties` convention exactly (including which 4
3234    /// [`CustomPropertyValue`] variants are modeled, for consistency across this workspace's
3235    /// crates). Only written if non-empty, same "optional part" posture as
3236    /// `table_styles`/`external_links` elsewhere in this crate.
3237    pub custom_properties: Vec<(String, CustomPropertyValue)>,
3238}
3239
3240/// A single custom document property's value (`docProps/custom.xml`, `CT_Property`'s value
3241/// `xsd:choice` — this crate models the same 4 of its many variant types as `word-ooxml`'s own
3242/// `CustomPropertyValue`: `vt:lpwstr` (`Text`), `vt:bool` (`Bool`), `vt:i4` (`Int`), `vt:r8`
3243/// (`Number`)). `CT_Property` also allows a date variant (`vt:filetime`) and several others
3244/// (vectors, blobs, currency..) not modeled here — same "simple case first, mirror the sibling
3245/// crate's own scope decision" posture as `word-ooxml`'s own doc comment for this exact type
3246/// explains.
3247#[derive(Debug, Clone, PartialEq)]
3248pub enum CustomPropertyValue {
3249    /// A text value (`vt:lpwstr`).
3250    Text(String),
3251    /// A boolean value (`vt:bool`).
3252    Bool(bool),
3253    /// A 32-bit signed integer value (`vt:i4`).
3254    Int(i32),
3255    /// A 64-bit floating-point value (`vt:r8`).
3256    Number(f64),
3257}
3258
3259impl DocumentProperties {
3260    /// Creates document properties with every field unset (equivalent to
3261    /// `DocumentProperties::default()`, only useful as a starting point for the `with_*` builders).
3262    pub fn new() -> Self {
3263        Self::default()
3264    }
3265
3266    /// Sets the title and returns the properties for chaining.
3267    pub fn with_title(mut self, title: impl Into<String>) -> Self {
3268        self.title = Some(title.into());
3269        self
3270    }
3271
3272    /// Sets the author and returns the properties for chaining.
3273    pub fn with_author(mut self, author: impl Into<String>) -> Self {
3274        self.author = Some(author.into());
3275        self
3276    }
3277
3278    /// Sets the subject and returns the properties for chaining.
3279    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
3280        self.subject = Some(subject.into());
3281        self
3282    }
3283
3284    /// Sets the keywords and returns the properties for chaining.
3285    pub fn with_keywords(mut self, keywords: impl Into<String>) -> Self {
3286        self.keywords = Some(keywords.into());
3287        self
3288    }
3289
3290    /// Sets the company and returns the properties for chaining.
3291    pub fn with_company(mut self, company: impl Into<String>) -> Self {
3292        self.company = Some(company.into());
3293        self
3294    }
3295
3296    /// Sets the manager and returns the properties for chaining.
3297    pub fn with_manager(mut self, manager: impl Into<String>) -> Self {
3298        self.manager = Some(manager.into());
3299        self
3300    }
3301
3302    /// Sets the hyperlink base and returns the properties for chaining — point 57.
3303    pub fn with_hyperlink_base(mut self, hyperlink_base: impl Into<String>) -> Self {
3304        self.hyperlink_base = Some(hyperlink_base.into());
3305        self
3306    }
3307
3308    /// Appends a custom document property and returns the properties for chaining. See
3309    /// `custom_properties`'s doc comment for why order matters (it determines each entry's `pid`).
3310    pub fn with_custom_property(
3311        mut self,
3312        name: impl Into<String>,
3313        value: CustomPropertyValue,
3314    ) -> Self {
3315        self.custom_properties.push((name.into(), value));
3316        self
3317    }
3318}
3319
3320/// A "what-if" scenario (`<scenario>`, `CT_Scenario`). Represents one named "what if these cells
3321/// held these values instead" snapshot, as managed through Excel's own Data > What-If Analysis >
3322/// Scenario Manager. This crate only stores the scenario's definition (name/inputs/comment); it
3323/// never evaluates the workbook under any scenario (same "storage, not evaluation" posture as
3324/// formulas).
3325#[derive(Debug, Clone, PartialEq)]
3326pub struct Scenario {
3327    /// The scenario's name, shown in Excel's Scenario Manager.
3328    pub name: String,
3329    /// The cells this scenario overrides, as `(cell reference, value text)` pairs
3330    /// (`<inputCells>`/`<inputCell r=".." val="..">`) — `val` is written verbatim as text, matching
3331    /// `CT_InputCells`'s own `xsd:string` typing (Excel re-parses it as a number itself).
3332    pub inputs: Vec<(String, String)>,
3333    /// An optional comment (`comment`).
3334    pub comment: Option<String>,
3335}
3336
3337impl Scenario {
3338    /// Creates a scenario with no inputs yet, no comment.
3339    pub fn new(name: impl Into<String>) -> Self {
3340        Self {
3341            name: name.into(),
3342            inputs: Vec::new(),
3343            comment: None,
3344        }
3345    }
3346
3347    /// Appends an input cell override and returns the scenario for chaining.
3348    pub fn with_input(mut self, cell: impl Into<String>, value: impl Into<String>) -> Self {
3349        self.inputs.push((cell.into(), value.into()));
3350        self
3351    }
3352
3353    /// Sets the scenario's comment and returns it for chaining.
3354    pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
3355        self.comment = Some(comment.into());
3356        self
3357    }
3358}
3359
3360/// One column's filter criteria within a [`Sheet::auto_filter_range`]
3361/// (`<filterColumn>`/`<filters>`/`<filter>`, `CT_FilterColumn`/ `CT_Filters`/`CT_Filter`). Only the
3362/// simplest, most common filter form is modeled: a fixed list of checked/visible values —
3363/// `CT_Filters`'s `CT_CustomFilters` (e.g. "greater than"), `CT_Top10`, `CT_DynamicFilter` (e.g.
3364/// "this month"), and `CT_ColorFilter`/`CT_IconFilter` forms are not.
3365#[derive(Debug, Clone, PartialEq)]
3366pub struct AutoFilterColumn {
3367    /// This column's 0-based offset from [`Sheet::auto_filter_range`]'s first column (`colId`) —
3368    /// e.g. `0` for the filter range's own first column, regardless of that column's real sheet
3369    /// position.
3370    pub column_offset: u32,
3371    /// The values that remain visible when this filter is applied (`<filter val="..">` per entry),
3372    /// written verbatim as text.
3373    pub visible_values: Vec<String>,
3374}
3375
3376impl AutoFilterColumn {
3377    /// Creates a filter column with the given visible values.
3378    pub fn new(column_offset: u32, visible_values: Vec<String>) -> Self {
3379        Self {
3380            column_offset,
3381            visible_values,
3382        }
3383    }
3384}
3385
3386/// A reference to another workbook, used by this workbook's formulas (`<externalReference>` in
3387/// `xl/workbook.xml`'s `<externalReferences>`, plus a dedicated
3388/// `xl/externalLinks/externalLinkN.xml` part, `CT_ExternalReference`/`CT_ExternalLink`). Storage
3389/// only: this crate neither resolves the referenced workbook nor recomputes any formula against it
3390/// — a formula cell referencing this link (e.g. `"[1]Feuil1!A1"` or `"'[Budget.xlsx]Feuil1'!A1"`)
3391/// is written/read as plain formula text like any other, this type only accounts for the *link
3392/// itself* (the part Excel needs to know the reference is legitimate and to cache a fallback value
3393/// while the other file is closed).
3394#[derive(Debug, Clone, PartialEq)]
3395pub struct ExternalWorkbookLink {
3396    /// The other workbook's path/URL, written as an external (`TargetMode="External"`) relationship
3397    /// target on this link's own `xl/externalLinks/_rels/externalLinkN.xml.rels` — same shape as
3398    /// [`HyperlinkTarget::External`]. Can be an absolute path (`"file:///C:/Budget.xlsx"`), a
3399    /// relative one (`"Budget.xlsx"`), or a URL.
3400    pub target: String,
3401    /// The other workbook's own sheet names, in order (`<sheetNames>`/`<sheetName val="..">`) —
3402    /// Excel uses these to resolve `[1]SheetName!..` formula references while the other file is
3403    /// closed.
3404    pub sheet_names: Vec<String>,
3405    /// Named ranges/constants defined in the other workbook that this one's formulas reference, as
3406    /// `(name, refersTo text)` pairs (`<definedNames>`/`<definedName name=".." refersTo="..">`).
3407    pub defined_names: Vec<(String, String)>,
3408}
3409
3410impl ExternalWorkbookLink {
3411    /// Creates an external workbook link with no sheet names/defined names yet.
3412    pub fn new(target: impl Into<String>) -> Self {
3413        Self {
3414            target: target.into(),
3415            sheet_names: Vec::new(),
3416            defined_names: Vec::new(),
3417        }
3418    }
3419
3420    /// Appends a sheet name and returns the link for chaining.
3421    pub fn with_sheet_name(mut self, name: impl Into<String>) -> Self {
3422        self.sheet_names.push(name.into());
3423        self
3424    }
3425
3426    /// Appends a defined name reference and returns the link for chaining.
3427    pub fn with_defined_name(
3428        mut self,
3429        name: impl Into<String>,
3430        refers_to: impl Into<String>,
3431    ) -> Self {
3432        self.defined_names.push((name.into(), refers_to.into()));
3433        self
3434    }
3435}
3436
3437/// A suppressed error-checking indicator over one range (`<ignoredError>`, `CT_IgnoredError`).
3438/// Confirmed against `sml.xsd` (`CT_IgnoredErrors`/`CT_IgnoredError`, ECMA-376 SpreadsheetML):
3439/// `sqref` is the only required attribute, every error-category flag is an optional boolean
3440/// defaulting to `false`. This crate models every flag `CT_IgnoredError` defines; real Excel's own
3441/// "Ignore Error" right-click menu only ever sets one flag at a time per range, but nothing in the
3442/// schema forbids several flags on the same entry, so this type allows it too (caller's choice,
3443/// same "don't second-guess the caller" posture as `ConditionalFormattingRule`).
3444#[derive(Debug, Clone, PartialEq)]
3445pub struct IgnoredError {
3446    /// The range this entry covers (`sqref`, e.g. `"B2:B10"` or a single cell `"C4"`) — like
3447    /// `ConditionalFormattingRule::range`, several disjoint cells can be packed into one `sqref`
3448    /// (space-separated) if the caller wants a single entry to cover them; this crate writes the
3449    /// string verbatim, no validation.
3450    pub range: String,
3451    /// Suppresses the "number stored as text" warning — by far the most common real-world use of
3452    /// `<ignoredError>` (e.g. a column of ZIP codes or reference numbers intentionally kept as
3453    /// text).
3454    pub number_stored_as_text: bool,
3455    /// Suppresses the "formula omits adjacent cells" warning.
3456    pub formula_range: bool,
3457    /// Suppresses the "inconsistent formula" warning (a formula that differs from the pattern of
3458    /// its neighboring cells).
3459    pub formula: bool,
3460    /// Suppresses the "unlocked cell containing a formula" warning.
3461    pub unlocked_formula: bool,
3462    /// Suppresses the "formula refers to an empty cell" warning.
3463    pub empty_cell_reference: bool,
3464    /// Suppresses the "value fails a list data validation rule" warning.
3465    pub list_data_validation: bool,
3466    /// Suppresses the "table's calculated column formula is inconsistent" warning.
3467    pub calculated_column: bool,
3468    /// Suppresses the "text year is ambiguous" warning (a two-digit year entered in a
3469    /// text-formatted cell).
3470    pub two_digit_text_year: bool,
3471    /// Suppresses the "formula results in an error" warning (e.g. a deliberate `#N/A` or
3472    /// `#DIV/0!`).
3473    pub eval_error: bool,
3474}
3475
3476impl IgnoredError {
3477    /// Creates an ignored-error entry over `range` with every flag unset — use the `with_*`
3478    /// builders to enable the specific warning(s) to suppress.
3479    pub fn new(range: impl Into<String>) -> Self {
3480        Self {
3481            range: range.into(),
3482            number_stored_as_text: false,
3483            formula_range: false,
3484            formula: false,
3485            unlocked_formula: false,
3486            empty_cell_reference: false,
3487            list_data_validation: false,
3488            calculated_column: false,
3489            two_digit_text_year: false,
3490            eval_error: false,
3491        }
3492    }
3493
3494    /// Suppresses the "number stored as text" warning and returns the entry for chaining — the
3495    /// common case (see this type's own doc comment).
3496    pub fn with_number_stored_as_text(mut self, value: bool) -> Self {
3497        self.number_stored_as_text = value;
3498        self
3499    }
3500
3501    /// Suppresses the "formula omits adjacent cells" warning and returns the entry for chaining.
3502    pub fn with_formula_range(mut self, value: bool) -> Self {
3503        self.formula_range = value;
3504        self
3505    }
3506
3507    /// Suppresses the "inconsistent formula" warning and returns the entry for chaining.
3508    pub fn with_formula(mut self, value: bool) -> Self {
3509        self.formula = value;
3510        self
3511    }
3512
3513    /// Suppresses the "unlocked cell containing a formula" warning and returns the entry for
3514    /// chaining.
3515    pub fn with_unlocked_formula(mut self, value: bool) -> Self {
3516        self.unlocked_formula = value;
3517        self
3518    }
3519
3520    /// Suppresses the "formula refers to an empty cell" warning and returns the entry for chaining.
3521    pub fn with_empty_cell_reference(mut self, value: bool) -> Self {
3522        self.empty_cell_reference = value;
3523        self
3524    }
3525
3526    /// Suppresses the "value fails a list data validation rule" warning and returns the entry for
3527    /// chaining.
3528    pub fn with_list_data_validation(mut self, value: bool) -> Self {
3529        self.list_data_validation = value;
3530        self
3531    }
3532
3533    /// Suppresses the "table's calculated column formula is inconsistent" warning and returns the
3534    /// entry for chaining.
3535    pub fn with_calculated_column(mut self, value: bool) -> Self {
3536        self.calculated_column = value;
3537        self
3538    }
3539
3540    /// Suppresses the "text year is ambiguous" warning and returns the entry for chaining.
3541    pub fn with_two_digit_text_year(mut self, value: bool) -> Self {
3542        self.two_digit_text_year = value;
3543        self
3544    }
3545
3546    /// Suppresses the "formula results in an error" warning and returns the entry for chaining.
3547    pub fn with_eval_error(mut self, value: bool) -> Self {
3548        self.eval_error = value;
3549        self
3550    }
3551}
3552
3553// ============================================================================,: sheet drawing
3554// (embedded pictures/charts).
3555// ============================================================================
3556
3557/// One corner of a cell-anchored drawing range (`<xdr:from>`/`<xdr:to>`, `CT_Marker`) — a cell
3558/// reference (0-based column/row, matching this crate's own 0-based [`Row`]/column conventions
3559/// elsewhere) plus an EMU offset *within* that cell, letting a picture/chart start or end partway
3560/// through a cell rather than exactly on a gridline.
3561#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3562pub struct CellAnchorPoint {
3563    pub column: u32,
3564    pub column_offset_emu: i64,
3565    pub row: u32,
3566    pub row_offset_emu: i64,
3567}
3568
3569impl CellAnchorPoint {
3570    /// A corner exactly on the gridline at `(column, row)`, no offset.
3571    pub fn new(column: u32, row: u32) -> Self {
3572        Self {
3573            column,
3574            row,
3575            column_offset_emu: 0,
3576            row_offset_emu: 0,
3577        }
3578    }
3579
3580    pub fn with_column_offset_emu(mut self, offset: i64) -> Self {
3581        self.column_offset_emu = offset;
3582        self
3583    }
3584
3585    pub fn with_row_offset_emu(mut self, offset: i64) -> Self {
3586        self.row_offset_emu = offset;
3587        self
3588    }
3589}
3590
3591/// A picture's raw image format — mirrors `word_ooxml::ImageFormat` exactly (same four formats,
3592/// same extension/content-type mapping), kept as its own type rather than shared across crates
3593/// since neither host crate depends on the other.
3594#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3595pub enum PictureFormat {
3596    Png,
3597    Jpeg,
3598    Gif,
3599    Bmp,
3600}
3601
3602impl PictureFormat {
3603    pub(crate) fn extension(self) -> &'static str {
3604        match self {
3605            PictureFormat::Png => "png",
3606            PictureFormat::Jpeg => "jpeg",
3607            PictureFormat::Gif => "gif",
3608            PictureFormat::Bmp => "bmp",
3609        }
3610    }
3611
3612    pub(crate) fn content_type(self) -> &'static str {
3613        match self {
3614            PictureFormat::Png => "image/png",
3615            PictureFormat::Jpeg => "image/jpeg",
3616            PictureFormat::Gif => "image/gif",
3617            PictureFormat::Bmp => "image/bmp",
3618        }
3619    }
3620
3621    pub(crate) fn from_extension(extension: &str) -> Option<Self> {
3622        Some(match extension.to_ascii_lowercase().as_str() {
3623            "png" => PictureFormat::Png,
3624            "jpeg" | "jpg" => PictureFormat::Jpeg,
3625            "gif" => PictureFormat::Gif,
3626            "bmp" => PictureFormat::Bmp,
3627            _ => return None,
3628        })
3629    }
3630}
3631
3632/// A picture embedded in a sheet's drawing (`<xdr:pic>`, `CT_Picture` — the spreadsheet-drawing
3633/// flavor, distinct from `a:CT_Picture`), anchored by its enclosing [`DrawingAnchor`].
3634///
3635/// Reuses `drawing::ShapeProperties`/`drawing::BlipFill` for the picture's own formatting
3636/// (fill/line/rotation via `spPr`), the same DrawingML content model `word-ooxml`'s own picture
3637/// support is being generalized toward — this is genuinely shared content, not a parallel
3638/// reimplementation. Raw image bytes are supplied directly (`data`/`format`), same "no separate
3639/// loader type" posture as `word_ooxml::Image`.
3640///
3641/// Not modeled: cropping (`a:srcRect`), picture-specific effects beyond what `ShapeProperties`
3642/// itself carries.
3643#[derive(Debug, Clone, PartialEq)]
3644pub struct SheetPicture {
3645    pub data: Vec<u8>,
3646    pub format: PictureFormat,
3647    /// Alt text / non-visual name (`<xdr:cNvPr name="..">`).
3648    pub name: String,
3649    pub shape_properties: Option<drawing::ShapeProperties>,
3650}
3651
3652impl SheetPicture {
3653    pub fn new(data: impl Into<Vec<u8>>, format: PictureFormat) -> Self {
3654        Self {
3655            data: data.into(),
3656            format,
3657            name: String::new(),
3658            shape_properties: None,
3659        }
3660    }
3661
3662    pub fn with_name(mut self, name: impl Into<String>) -> Self {
3663        self.name = name.into();
3664        self
3665    }
3666
3667    pub fn with_shape_properties(mut self, properties: drawing::ShapeProperties) -> Self {
3668        self.shape_properties = Some(properties);
3669        self
3670    }
3671}
3672
3673/// A chart embedded in a sheet's drawing (`<xdr:graphicFrame>` referencing a dedicated
3674/// `xl/charts/chartN.xml` part via `<c:chart r:id="..">`), anchored by its enclosing
3675/// [`DrawingAnchor`]. Wraps the whole `chart::ChartSpace` this crate's `chart` dependency already
3676/// knows how to read/write in full — no Excel-specific chart modeling needed here, and no embedded
3677/// workbook needed either (an Excel chart reads its series directly from this same workbook's
3678/// cells, via each series' own cell-reference formulas already carried by
3679/// `chart::NumericDataSource`/`StringDataSource`).
3680#[derive(Debug, Clone, PartialEq)]
3681pub struct SheetChart {
3682    pub chart_space: chart::ChartSpace,
3683    /// Non-visual name (`<xdr:cNvPr name="..">`).
3684    pub name: String,
3685}
3686
3687impl SheetChart {
3688    pub fn new(chart_space: chart::ChartSpace) -> Self {
3689        Self {
3690            chart_space,
3691            name: String::new(),
3692        }
3693    }
3694
3695    pub fn with_name(mut self, name: impl Into<String>) -> Self {
3696        self.name = name.into();
3697        self
3698    }
3699}
3700
3701/// The object anchored by a [`DrawingAnchor`] — a picture or a chart.
3702#[derive(Debug, Clone, PartialEq)]
3703pub enum DrawingObject {
3704    /// Boxed: an embedded picture's own encoded bytes make it, in turn, the largest variant once
3705    /// `Chart` below was boxed (clippy's `large_enum_variant` moves on to whichever variant is now
3706    /// biggest).
3707    Picture(Box<SheetPicture>),
3708    /// Boxed for the same reason as `word-ooxml`'s `RunContent::Chart` and `powerpoint-ooxml`'s
3709    /// `Shape::Chart` — a chart's own nested `ChartSpace` otherwise forces every `DrawingObject`,
3710    /// picture or chart alike, to pay for the largest variant's size.
3711    Chart(Box<SheetChart>),
3712}
3713
3714/// One cell-anchored drawing object (`<xdr:twoCellAnchor>`, `CT_TwoCellAnchor`).
3715///
3716/// Only the two-cell anchor is modeled (`from`/`to` cell markers, `editAs` always the schema
3717/// default `"twoCell"`) — by far the common case real spreadsheet tools generate
3718/// (`CT_OneCellAnchor`/ `CT_AbsoluteAnchor` exist in the schema but are rarely if ever generated,
3719/// only tolerated on read). A wholly absent anchor kind on read is simply skipped by this crate's
3720/// forgiving reader, consistent with the rest of this workspace's posture.
3721#[derive(Debug, Clone, PartialEq)]
3722pub struct DrawingAnchor {
3723    pub from: CellAnchorPoint,
3724    pub to: CellAnchorPoint,
3725    pub object: DrawingObject,
3726}
3727
3728impl DrawingAnchor {
3729    pub fn new(from: CellAnchorPoint, to: CellAnchorPoint, object: DrawingObject) -> Self {
3730        Self { from, to, object }
3731    }
3732}
3733
3734/// A sheet's drawing (`xl/drawings/drawingN.xml`, `CT_Drawing`/root `<xdr:wsDr>`) — an unbounded
3735/// list of anchored pictures/charts, matching the schema's own `EG_Anchor` choice group repeated
3736/// `maxOccurs="unbounded"`.
3737#[derive(Debug, Clone, PartialEq, Default)]
3738pub struct SheetDrawing {
3739    pub anchors: Vec<DrawingAnchor>,
3740}
3741
3742impl SheetDrawing {
3743    pub fn new() -> Self {
3744        Self::default()
3745    }
3746
3747    pub fn with_anchor(mut self, anchor: DrawingAnchor) -> Self {
3748        self.anchors.push(anchor);
3749        self
3750    }
3751}