mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-27 12:16:54 -06:00
refactor(model): move features.rs items to features/
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user