Merge origin/main (25881a8e) into w/objects

ROADMAP.md, docs/chart-parity.md, docs/gaps.md, docs/target-app-parity.md, docs/ui-parity.md: the header change notes combine this branch's note with main's, dated today; main's revision and progress rows stay and this branch's rows are re-dated on top. Code merged without conflict.
This commit is contained in:
prxnvt
2026-11-02 01:30:43 -05:00
87 changed files with 4165 additions and 152 deletions
+43 -1
View File
@@ -10,7 +10,7 @@ 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};
pub use print::{EditRange, HfPage, LineInfo, Orientation, PageOrder, PrintSettings, SheetProtection, password_hash};
// ---------------------------------------------------------------- tables
@@ -540,6 +540,22 @@ impl Trendline {
}
}
/// A chart's data table (`c:dTable`), independent of the chart legend.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ChartDataTable {
pub show_keys: bool,
pub horizontal_border: bool,
pub vertical_border: bool,
pub outline: bool,
}
impl Default for ChartDataTable {
fn default() -> Self {
Self { show_keys: true, horizontal_border: true, vertical_border: true, outline: true }
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Chart {
pub id: u32,
@@ -549,6 +565,8 @@ pub struct Chart {
pub series: Vec<Series>,
pub legend: LegendPos,
pub data_labels: bool,
#[serde(default)]
pub data_table: Option<ChartDataTable>,
pub gridlines: bool,
pub style: u32,
pub x_title: Option<String>,
@@ -562,6 +580,30 @@ pub struct Chart {
pub pivot: Option<String>,
}
impl Chart {
/// Data tables belong to category-axis column, bar, line and area charts.
pub fn supports_data_table(&self) -> bool {
let eligible = |kind| {
matches!(
kind,
ChartKind::ColumnClustered
| ChartKind::ColumnStacked
| ChartKind::ColumnStacked100
| ChartKind::BarClustered
| ChartKind::BarStacked
| ChartKind::BarStacked100
| ChartKind::Line
| ChartKind::LineMarkers
| ChartKind::LineStacked
| ChartKind::Area
| ChartKind::AreaStacked
| ChartKind::AreaStacked100
)
};
eligible(self.kind) || (self.kind == ChartKind::Combo && self.series.iter().all(|s| eligible(s.kind.unwrap_or(ChartKind::Line))))
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Image {
pub id: u32,
+67
View File
@@ -49,12 +49,52 @@ pub struct PrintSettings {
pub hf_scale_with_doc: bool,
#[serde(default = "yes")]
pub hf_align_with_margins: bool,
/// Page Setup › Sheet › Page order: the sequence pages are numbered and printed in when the
/// sheet is more than one page wide and tall.
#[serde(default)]
pub page_order: PageOrder,
}
fn yes() -> bool {
true
}
/// The sequence of a sheet's pages (XLSX `pageSetup pageOrder`, ECMA-376 `ST_PageOrder`).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PageOrder {
/// Down the first column of pages, then down the next one (Excel's default).
#[default]
DownThenOver,
/// Across the first row of pages, then across the next one.
OverThenDown,
}
impl PageOrder {
/// The id `pageLayout.pageSetup` takes and XLSX writes.
pub fn id(self) -> &'static str {
match self {
PageOrder::DownThenOver => "downThenOver",
PageOrder::OverThenDown => "overThenDown",
}
}
pub fn parse(id: &str) -> Option<PageOrder> {
match id {
"downThenOver" => Some(PageOrder::DownThenOver),
"overThenDown" => Some(PageOrder::OverThenDown),
_ => None,
}
}
/// The 0-based place in the printed sequence of the page in page row `row` and page column
/// `col`, on a sheet `rows` pages tall and `cols` pages wide.
pub fn index(self, row: usize, col: usize, rows: usize, cols: usize) -> usize {
match self {
PageOrder::DownThenOver => col.saturating_mul(rows).saturating_add(row),
PageOrder::OverThenDown => row.saturating_mul(cols).saturating_add(col),
}
}
}
/// Which header/footer pair a printed page uses.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HfPage {
@@ -146,6 +186,7 @@ impl Default for PrintSettings {
even_footer: String::new(),
hf_scale_with_doc: true,
hf_align_with_margins: true,
page_order: PageOrder::DownThenOver,
}
}
}
@@ -215,3 +256,29 @@ pub struct LineInfo {
#[serde(default)]
pub custom: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn page_order_numbers_pages_and_old_settings_load_down_then_over() {
// Two pages tall, three wide: (row, col) → place in the printed sequence.
let place = |order: PageOrder| [(0, 0), (1, 0), (0, 1), (1, 1), (0, 2), (1, 2)].map(|(r, c)| order.index(r, c, 2, 3));
assert_eq!(place(PageOrder::DownThenOver), [0, 1, 2, 3, 4, 5]);
assert_eq!(place(PageOrder::OverThenDown), [0, 3, 1, 4, 2, 5]);
assert_eq!(PageOrder::OverThenDown.index(usize::MAX, usize::MAX, usize::MAX, usize::MAX), usize::MAX);
for order in [PageOrder::DownThenOver, PageOrder::OverThenDown] {
assert_eq!(PageOrder::parse(order.id()), Some(order));
}
assert_eq!(PageOrder::parse("OverThenDown"), None);
// Saved under the same id the command takes; settings saved before the field existed load
// with Excel's default.
let mut p = PrintSettings { page_order: PageOrder::OverThenDown, ..Default::default() };
let mut saved = serde_json::to_value(&p).unwrap();
assert_eq!(saved["page_order"], "overThenDown");
assert!(saved.as_object_mut().unwrap().remove("page_order").is_some());
p = serde_json::from_value(saved).unwrap();
assert_eq!(p, PrintSettings::default());
}
}