From bbb570dc32fe81f7219208cdbf7f2f2b0a9c7189 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 20:24:58 +0000 Subject: [PATCH] refactor(model): move features.rs items to features/ --- crates/model/src/features.rs | 396 +---------------------------- crates/model/src/features/pivot.rs | 176 +++++++++++++ crates/model/src/features/print.rs | 216 ++++++++++++++++ 3 files changed, 398 insertions(+), 390 deletions(-) create mode 100644 crates/model/src/features/pivot.rs create mode 100644 crates/model/src/features/print.rs diff --git a/crates/model/src/features.rs b/crates/model/src/features.rs index 00db5e879..2394319c3 100644 --- a/crates/model/src/features.rs +++ b/crates/model/src/features.rs @@ -6,6 +6,12 @@ use serde::{Deserialize, Serialize}; use crate::style::{Color, Style}; +mod pivot; +mod print; + +pub use pivot::{PivotCalcField, PivotDateGroup, PivotField, PivotFilter, PivotFunc, PivotLayout, PivotShowAs, PivotSort, PivotTable, PivotValue}; +pub use print::{EditRange, HfPage, LineInfo, Orientation, PrintSettings, SheetProtection, password_hash}; + // ---------------------------------------------------------------- tables #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -716,396 +722,6 @@ pub struct Slicer { pub columns: u32, } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum Orientation { - #[default] - Portrait, - Landscape, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct PrintSettings { - pub orientation: Orientation, - /// Paper size name (`Letter`, `A4`, `Legal`…). - pub paper: String, - /// Margins in inches: left, right, top, bottom, header, footer. - pub margins: [f32; 6], - pub print_area: Option, - pub title_rows: Option<(u32, u32)>, - pub title_cols: Option<(u32, u32)>, - /// Percent scale, or fit to pages (wide, tall). - pub scale: u16, - pub fit_to: Option<(u16, u16)>, - pub gridlines: bool, - pub headings: bool, - pub center_h: bool, - pub center_v: bool, - /// The header and footer of odd pages, and of every page unless the variants below apply. - pub header: String, - pub footer: String, - pub row_breaks: Vec, - pub col_breaks: Vec, - /// Different First Page: the sheet's first printed page uses `first_header`/`first_footer`. - #[serde(default)] - pub different_first: bool, - /// Different Odd & Even Pages: even-numbered pages use `even_header`/`even_footer`. - #[serde(default)] - pub different_odd_even: bool, - #[serde(default)] - pub first_header: String, - #[serde(default)] - pub first_footer: String, - #[serde(default)] - pub even_header: String, - #[serde(default)] - pub even_footer: String, - /// Scale with Document / Align with Page Margins (both on unless a file turned them off). - #[serde(default = "yes")] - pub hf_scale_with_doc: bool, - #[serde(default = "yes")] - pub hf_align_with_margins: bool, -} - -fn yes() -> bool { - true -} - -/// Which header/footer pair a printed page uses. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum HfPage { - Odd, - Even, - First, -} - -impl HfPage { - pub fn id(self) -> &'static str { - match self { - HfPage::Odd => "odd", - HfPage::Even => "even", - HfPage::First => "first", - } - } - pub fn parse(id: &str) -> Option { - match id { - "odd" | "all" => Some(HfPage::Odd), - "even" => Some(HfPage::Even), - "first" => Some(HfPage::First), - _ => None, - } - } -} - -impl PrintSettings { - /// The pair used by page `number` (1-based, as `&P` prints it); `first` is the sheet's first page. - pub fn hf_page(&self, first: bool, number: usize) -> HfPage { - if self.different_first && first { - HfPage::First - } else if self.different_odd_even && number.is_multiple_of(2) { - HfPage::Even - } else { - HfPage::Odd - } - } - /// The header (`footer` false) or footer of a page kind. - pub fn hf(&self, page: HfPage, footer: bool) -> &String { - match (page, footer) { - (HfPage::Odd, false) => &self.header, - (HfPage::Odd, true) => &self.footer, - (HfPage::Even, false) => &self.even_header, - (HfPage::Even, true) => &self.even_footer, - (HfPage::First, false) => &self.first_header, - (HfPage::First, true) => &self.first_footer, - } - } - pub fn hf_mut(&mut self, page: HfPage, footer: bool) -> &mut String { - match (page, footer) { - (HfPage::Odd, false) => &mut self.header, - (HfPage::Odd, true) => &mut self.footer, - (HfPage::Even, false) => &mut self.even_header, - (HfPage::Even, true) => &mut self.even_footer, - (HfPage::First, false) => &mut self.first_header, - (HfPage::First, true) => &mut self.first_footer, - } - } - /// Any header or footer text at all (on any kind of page). - pub fn has_header_footer(&self) -> bool { - [&self.header, &self.footer, &self.first_header, &self.first_footer, &self.even_header, &self.even_footer].iter().any(|t| !t.is_empty()) - } -} - -impl Default for PrintSettings { - fn default() -> Self { - PrintSettings { - orientation: Orientation::Portrait, - paper: "Letter".into(), - margins: [0.7, 0.7, 0.75, 0.75, 0.3, 0.3], - print_area: None, - title_rows: None, - title_cols: None, - scale: 100, - fit_to: None, - gridlines: false, - headings: false, - center_h: false, - center_v: false, - header: String::new(), - footer: String::new(), - row_breaks: vec![], - col_breaks: vec![], - different_first: false, - different_odd_even: false, - first_header: String::new(), - first_footer: String::new(), - even_header: String::new(), - even_footer: String::new(), - hf_scale_with_doc: true, - hf_align_with_margins: true, - } - } -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct SheetProtection { - /// Hash of the password (Excel-compatible, written to XLSX `sheetProtection`; never the - /// password itself). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub password: Option, - /// The internal hash older GridCraft JSON workbooks stored ([`password_hash`]); still - /// verified when unprotecting, never written for new passwords. - #[serde(rename = "password_hash", default, skip_serializing_if = "Option::is_none")] - pub legacy_password_hash: Option, - pub select_locked: bool, - pub select_unlocked: bool, - pub format_cells: bool, - pub format_columns: bool, - pub format_rows: bool, - pub insert_columns: bool, - pub insert_rows: bool, - pub delete_columns: bool, - pub delete_rows: bool, - pub sort: bool, - pub autofilter: bool, -} - -/// Review › Allow Edit Ranges: cells that stay editable on a protected sheet, with or without a -/// password (XLSX ``). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct EditRange { - pub title: String, - pub ranges: Vec, - pub password: Option, - /// Windows permissions (`securityDescriptor`), kept as read from the file. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub security_descriptor: Option, -} - -impl EditRange { - pub fn contains(&self, c: CellRef) -> bool { - self.ranges.iter().any(|r| r.contains(c)) - } -} - -/// The non-cryptographic password hash older GridCraft versions stored for sheet protection -/// (only verified now, see [`SheetProtection::legacy_password_hash`]). -pub fn password_hash(pw: &str) -> u64 { - let mut h: u64 = 0xcbf29ce484222325 ^ 0x5348_4545_5443; - for b in pw.bytes() { - h ^= b as u64; - h = h.wrapping_mul(0x100000001b3); - } - h -} - -/// Outline grouping info for a row or column. -#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct LineInfo { - /// Size in pixels at 96 dpi; `None` = default (rows auto-fit to content). - pub size: Option, - pub hidden: bool, - pub outline: u8, - pub collapsed: bool, - pub style: Option, - /// The size was set by the user (rows): automatic heights follow the content. - #[serde(default)] - pub custom: bool, -} - -// ---------------------------------------------------------------- pivot tables - -/// How a PivotTable lays out its row fields. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PivotLayout { - /// All row fields in one column, inner items indented ("Row Labels" header). - #[default] - Compact, - /// One column per row field; each item on its own row. - Outline, - /// One column per row field; inner items start on their parent's row, totals below. - Tabular, -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PivotSort { - #[default] - Asc, - Desc, - /// Source order (first appearance). - None, -} - -/// Grouping of a date field (values are date serials). -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PivotDateGroup { - #[default] - None, - Years, - Quarters, - Months, - Days, -} - -/// A field placed in the Rows or Columns area. -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(default)] -pub struct PivotField { - /// Source column header name. - pub source_col: String, - pub sort: PivotSort, - pub date_group: PivotDateGroup, - /// Item labels whose detail is hidden (collapsed). - pub collapsed_items: Vec, -} - -/// Summarize-values-by function. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PivotFunc { - #[default] - Sum, - Count, - Average, - Max, - Min, - Product, - CountNumbers, - StdDev, - StdDevP, - Var, - VarP, -} - -/// Show-values-as transform. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PivotShowAs { - #[default] - Normal, - PercentOfGrandTotal, - PercentOfColumnTotal, - PercentOfRowTotal, - /// Running total in the first row field. - RunningTotal, - /// Rank largest to smallest among sibling row items. - Rank, -} - -/// A field in the Values area. -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(default)] -pub struct PivotValue { - pub source_col: String, - pub func: PivotFunc, - /// Caption, e.g. "Sum of Sales". - pub name: String, - pub show_as: PivotShowAs, - pub number_format: Option, -} - -/// A report filter (or, for a field also in Rows/Columns, its hidden-items filter). -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(default)] -pub struct PivotFilter { - pub source_col: String, - /// Item labels shown; `None` = all. - pub selected: Option>, -} - -/// A PivotTable: its definition plus where its output was last written. The output itself is -/// static cell values and styles on the sheet, rewritten on every refresh. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(default)] -pub struct PivotTable { - pub id: u32, - pub name: String, - /// Source: range formula text (`Sheet1!$A$1:$E$200`), a table name or a defined name. - pub source: String, - /// Top-left of the output body on the pivot's sheet. - pub anchor: CellRef, - pub rows: Vec, - pub columns: Vec, - pub values: Vec, - pub filters: Vec, - pub layout: PivotLayout, - /// PivotTable Options › Data › Enable show details: Show Details lists a value cell's - /// source records only while this is on. - pub enable_show_details: bool, - /// Grand total for each row (the "Grand Total" column), like Excel's "On for Rows". - pub grand_totals_rows: bool, - /// Grand total for each column (the "Grand Total" row), like Excel's "On for Columns". - pub grand_totals_cols: bool, - pub subtotals: bool, - /// Subtotals at the top of each group (Compact/Outline); otherwise below it. - pub subtotals_top: bool, - pub style: String, - pub show_headers: bool, - /// Area written by the last refresh. - pub last_range: Option, - /// Calculated fields, usable in the Values area by name. - pub calculated_fields: Vec, - /// The Σ Values pseudo-field (it exists with two or more value fields) sits in Rows rather - /// than Columns (Excel's "values on rows"). - pub values_on_rows: bool, - /// Position of Σ Values among the row fields (`None` = after the last). Only used with - /// `values_on_rows`: in Columns Σ Values always comes after the last column field. - pub values_position: Option, -} - -/// A calculated field: a formula over other fields (`=Sales*0.1`, `='Unit Price'*Units`). As in -/// Excel, it is evaluated on the sums of the fields it names for each report cell, not per record. -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(default)] -pub struct PivotCalcField { - pub name: String, - /// Formula text, starting with `=`. - pub formula: String, -} - -impl Default for PivotTable { - fn default() -> Self { - PivotTable { - id: 0, - name: "PivotTable1".into(), - source: String::new(), - anchor: CellRef::default(), - rows: vec![], - columns: vec![], - values: vec![], - filters: vec![], - layout: PivotLayout::Compact, - enable_show_details: true, - grand_totals_rows: true, - grand_totals_cols: true, - subtotals: true, - subtotals_top: true, - style: "PivotStyleLight16".into(), - show_headers: true, - last_range: None, - calculated_fields: vec![], - values_on_rows: false, - values_position: None, - } - } -} - #[cfg(test)] mod trendline_forecast_tests { use super::Trendline; diff --git a/crates/model/src/features/pivot.rs b/crates/model/src/features/pivot.rs new file mode 100644 index 000000000..368aa1f53 --- /dev/null +++ b/crates/model/src/features/pivot.rs @@ -0,0 +1,176 @@ +use super::*; + +// ---------------------------------------------------------------- pivot tables + +/// How a PivotTable lays out its row fields. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PivotLayout { + /// All row fields in one column, inner items indented ("Row Labels" header). + #[default] + Compact, + /// One column per row field; each item on its own row. + Outline, + /// One column per row field; inner items start on their parent's row, totals below. + Tabular, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PivotSort { + #[default] + Asc, + Desc, + /// Source order (first appearance). + None, +} + +/// Grouping of a date field (values are date serials). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PivotDateGroup { + #[default] + None, + Years, + Quarters, + Months, + Days, +} + +/// A field placed in the Rows or Columns area. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct PivotField { + /// Source column header name. + pub source_col: String, + pub sort: PivotSort, + pub date_group: PivotDateGroup, + /// Item labels whose detail is hidden (collapsed). + pub collapsed_items: Vec, +} + +/// Summarize-values-by function. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PivotFunc { + #[default] + Sum, + Count, + Average, + Max, + Min, + Product, + CountNumbers, + StdDev, + StdDevP, + Var, + VarP, +} + +/// Show-values-as transform. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PivotShowAs { + #[default] + Normal, + PercentOfGrandTotal, + PercentOfColumnTotal, + PercentOfRowTotal, + /// Running total in the first row field. + RunningTotal, + /// Rank largest to smallest among sibling row items. + Rank, +} + +/// A field in the Values area. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct PivotValue { + pub source_col: String, + pub func: PivotFunc, + /// Caption, e.g. "Sum of Sales". + pub name: String, + pub show_as: PivotShowAs, + pub number_format: Option, +} + +/// A report filter (or, for a field also in Rows/Columns, its hidden-items filter). +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct PivotFilter { + pub source_col: String, + /// Item labels shown; `None` = all. + pub selected: Option>, +} + +/// A PivotTable: its definition plus where its output was last written. The output itself is +/// static cell values and styles on the sheet, rewritten on every refresh. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct PivotTable { + pub id: u32, + pub name: String, + /// Source: range formula text (`Sheet1!$A$1:$E$200`), a table name or a defined name. + pub source: String, + /// Top-left of the output body on the pivot's sheet. + pub anchor: CellRef, + pub rows: Vec, + pub columns: Vec, + pub values: Vec, + pub filters: Vec, + pub layout: PivotLayout, + /// PivotTable Options › Data › Enable show details: Show Details lists a value cell's + /// source records only while this is on. + pub enable_show_details: bool, + /// Grand total for each row (the "Grand Total" column), like Excel's "On for Rows". + pub grand_totals_rows: bool, + /// Grand total for each column (the "Grand Total" row), like Excel's "On for Columns". + pub grand_totals_cols: bool, + pub subtotals: bool, + /// Subtotals at the top of each group (Compact/Outline); otherwise below it. + pub subtotals_top: bool, + pub style: String, + pub show_headers: bool, + /// Area written by the last refresh. + pub last_range: Option, + /// Calculated fields, usable in the Values area by name. + pub calculated_fields: Vec, + /// The Σ Values pseudo-field (it exists with two or more value fields) sits in Rows rather + /// than Columns (Excel's "values on rows"). + pub values_on_rows: bool, + /// Position of Σ Values among the row fields (`None` = after the last). Only used with + /// `values_on_rows`: in Columns Σ Values always comes after the last column field. + pub values_position: Option, +} + +/// A calculated field: a formula over other fields (`=Sales*0.1`, `='Unit Price'*Units`). As in +/// Excel, it is evaluated on the sums of the fields it names for each report cell, not per record. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct PivotCalcField { + pub name: String, + /// Formula text, starting with `=`. + pub formula: String, +} + +impl Default for PivotTable { + fn default() -> Self { + PivotTable { + id: 0, + name: "PivotTable1".into(), + source: String::new(), + anchor: CellRef::default(), + rows: vec![], + columns: vec![], + values: vec![], + filters: vec![], + layout: PivotLayout::Compact, + enable_show_details: true, + grand_totals_rows: true, + grand_totals_cols: true, + subtotals: true, + subtotals_top: true, + style: "PivotStyleLight16".into(), + show_headers: true, + last_range: None, + calculated_fields: vec![], + values_on_rows: false, + values_position: None, + } + } +} diff --git a/crates/model/src/features/print.rs b/crates/model/src/features/print.rs new file mode 100644 index 000000000..6a4ab0be0 --- /dev/null +++ b/crates/model/src/features/print.rs @@ -0,0 +1,216 @@ +use super::*; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum Orientation { + #[default] + Portrait, + Landscape, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct PrintSettings { + pub orientation: Orientation, + /// Paper size name (`Letter`, `A4`, `Legal`…). + pub paper: String, + /// Margins in inches: left, right, top, bottom, header, footer. + pub margins: [f32; 6], + pub print_area: Option, + pub title_rows: Option<(u32, u32)>, + pub title_cols: Option<(u32, u32)>, + /// Percent scale, or fit to pages (wide, tall). + pub scale: u16, + pub fit_to: Option<(u16, u16)>, + pub gridlines: bool, + pub headings: bool, + pub center_h: bool, + pub center_v: bool, + /// The header and footer of odd pages, and of every page unless the variants below apply. + pub header: String, + pub footer: String, + pub row_breaks: Vec, + pub col_breaks: Vec, + /// Different First Page: the sheet's first printed page uses `first_header`/`first_footer`. + #[serde(default)] + pub different_first: bool, + /// Different Odd & Even Pages: even-numbered pages use `even_header`/`even_footer`. + #[serde(default)] + pub different_odd_even: bool, + #[serde(default)] + pub first_header: String, + #[serde(default)] + pub first_footer: String, + #[serde(default)] + pub even_header: String, + #[serde(default)] + pub even_footer: String, + /// Scale with Document / Align with Page Margins (both on unless a file turned them off). + #[serde(default = "yes")] + pub hf_scale_with_doc: bool, + #[serde(default = "yes")] + pub hf_align_with_margins: bool, +} + +fn yes() -> bool { + true +} + +/// Which header/footer pair a printed page uses. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HfPage { + Odd, + Even, + First, +} + +impl HfPage { + pub fn id(self) -> &'static str { + match self { + HfPage::Odd => "odd", + HfPage::Even => "even", + HfPage::First => "first", + } + } + pub fn parse(id: &str) -> Option { + match id { + "odd" | "all" => Some(HfPage::Odd), + "even" => Some(HfPage::Even), + "first" => Some(HfPage::First), + _ => None, + } + } +} + +impl PrintSettings { + /// The pair used by page `number` (1-based, as `&P` prints it); `first` is the sheet's first page. + pub fn hf_page(&self, first: bool, number: usize) -> HfPage { + if self.different_first && first { + HfPage::First + } else if self.different_odd_even && number.is_multiple_of(2) { + HfPage::Even + } else { + HfPage::Odd + } + } + /// The header (`footer` false) or footer of a page kind. + pub fn hf(&self, page: HfPage, footer: bool) -> &String { + match (page, footer) { + (HfPage::Odd, false) => &self.header, + (HfPage::Odd, true) => &self.footer, + (HfPage::Even, false) => &self.even_header, + (HfPage::Even, true) => &self.even_footer, + (HfPage::First, false) => &self.first_header, + (HfPage::First, true) => &self.first_footer, + } + } + pub fn hf_mut(&mut self, page: HfPage, footer: bool) -> &mut String { + match (page, footer) { + (HfPage::Odd, false) => &mut self.header, + (HfPage::Odd, true) => &mut self.footer, + (HfPage::Even, false) => &mut self.even_header, + (HfPage::Even, true) => &mut self.even_footer, + (HfPage::First, false) => &mut self.first_header, + (HfPage::First, true) => &mut self.first_footer, + } + } + /// Any header or footer text at all (on any kind of page). + pub fn has_header_footer(&self) -> bool { + [&self.header, &self.footer, &self.first_header, &self.first_footer, &self.even_header, &self.even_footer].iter().any(|t| !t.is_empty()) + } +} + +impl Default for PrintSettings { + fn default() -> Self { + PrintSettings { + orientation: Orientation::Portrait, + paper: "Letter".into(), + margins: [0.7, 0.7, 0.75, 0.75, 0.3, 0.3], + print_area: None, + title_rows: None, + title_cols: None, + scale: 100, + fit_to: None, + gridlines: false, + headings: false, + center_h: false, + center_v: false, + header: String::new(), + footer: String::new(), + row_breaks: vec![], + col_breaks: vec![], + different_first: false, + different_odd_even: false, + first_header: String::new(), + first_footer: String::new(), + even_header: String::new(), + even_footer: String::new(), + hf_scale_with_doc: true, + hf_align_with_margins: true, + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct SheetProtection { + /// Hash of the password (Excel-compatible, written to XLSX `sheetProtection`; never the + /// password itself). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub password: Option, + /// The internal hash older GridCraft JSON workbooks stored ([`password_hash`]); still + /// verified when unprotecting, never written for new passwords. + #[serde(rename = "password_hash", default, skip_serializing_if = "Option::is_none")] + pub legacy_password_hash: Option, + pub select_locked: bool, + pub select_unlocked: bool, + pub format_cells: bool, + pub format_columns: bool, + pub format_rows: bool, + pub insert_columns: bool, + pub insert_rows: bool, + pub delete_columns: bool, + pub delete_rows: bool, + pub sort: bool, + pub autofilter: bool, +} + +/// Review › Allow Edit Ranges: cells that stay editable on a protected sheet, with or without a +/// password (XLSX ``). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct EditRange { + pub title: String, + pub ranges: Vec, + pub password: Option, + /// Windows permissions (`securityDescriptor`), kept as read from the file. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub security_descriptor: Option, +} + +impl EditRange { + pub fn contains(&self, c: CellRef) -> bool { + self.ranges.iter().any(|r| r.contains(c)) + } +} + +/// The non-cryptographic password hash older GridCraft versions stored for sheet protection +/// (only verified now, see [`SheetProtection::legacy_password_hash`]). +pub fn password_hash(pw: &str) -> u64 { + let mut h: u64 = 0xcbf29ce484222325 ^ 0x5348_4545_5443; + for b in pw.bytes() { + h ^= b as u64; + h = h.wrapping_mul(0x100000001b3); + } + h +} + +/// Outline grouping info for a row or column. +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct LineInfo { + /// Size in pixels at 96 dpi; `None` = default (rows auto-fit to content). + pub size: Option, + pub hidden: bool, + pub outline: u8, + pub collapsed: bool, + pub style: Option, + /// The size was set by the user (rows): automatic heights follow the content. + #[serde(default)] + pub custom: bool, +}