refactor(model): move features.rs items to features/

This commit is contained in:
aiongg
2026-10-26 20:43:39 +00:00
parent 7f1ccf2e81
commit bbb570dc32
3 changed files with 398 additions and 390 deletions
+6 -390
View File
@@ -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<RangeRef>,
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<u32>,
pub col_breaks: Vec<u32>,
/// 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<HfPage> {
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<crate::PasswordHash>,
/// 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<u64>,
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 `<protectedRange>`).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EditRange {
pub title: String,
pub ranges: Vec<RangeRef>,
pub password: Option<crate::PasswordHash>,
/// Windows permissions (`securityDescriptor`), kept as read from the file.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub security_descriptor: Option<String>,
}
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<f32>,
pub hidden: bool,
pub outline: u8,
pub collapsed: bool,
pub style: Option<crate::style::StyleId>,
/// 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<String>,
}
/// 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<String>,
}
/// 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<Vec<String>>,
}
/// 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<PivotField>,
pub columns: Vec<PivotField>,
pub values: Vec<PivotValue>,
pub filters: Vec<PivotFilter>,
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<RangeRef>,
/// Calculated fields, usable in the Values area by name.
pub calculated_fields: Vec<PivotCalcField>,
/// 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<usize>,
}
/// 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;
+176
View File
@@ -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<String>,
}
/// 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<String>,
}
/// 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<Vec<String>>,
}
/// 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<PivotField>,
pub columns: Vec<PivotField>,
pub values: Vec<PivotValue>,
pub filters: Vec<PivotFilter>,
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<RangeRef>,
/// Calculated fields, usable in the Values area by name.
pub calculated_fields: Vec<PivotCalcField>,
/// 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<usize>,
}
/// 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,
}
}
}
+216
View File
@@ -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<RangeRef>,
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<u32>,
pub col_breaks: Vec<u32>,
/// 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<HfPage> {
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<crate::PasswordHash>,
/// 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<u64>,
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 `<protectedRange>`).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EditRange {
pub title: String,
pub ranges: Vec<RangeRef>,
pub password: Option<crate::PasswordHash>,
/// Windows permissions (`securityDescriptor`), kept as read from the file.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub security_descriptor: Option<String>,
}
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<f32>,
pub hidden: bool,
pub outline: u8,
pub collapsed: bool,
pub style: Option<crate::style::StyleId>,
/// The size was set by the user (rows): automatic heights follow the content.
#[serde(default)]
pub custom: bool,
}