Skip to main content

ravenrs_sheet/model/
sheet.rs

1//! Worksheets.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use crate::base::{CellRef, RangeRef, Value};
8use serde::{Deserialize, Serialize};
9
10use crate::model::cell::{Cell, CellPicture};
11use crate::model::features::{
12    Anchor, AutoFilter, Chart, Comment, CondFormat, FormControl, Hyperlink, Image, LineInfo, ObjKind, ObjRef, PivotTable, PrintSettings, Shape,
13    ShapeKind, SheetProtection, Slicer, Sparkline, Table, Validation,
14};
15use crate::model::store::CellStore;
16use crate::model::style::{Color, StyleId};
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
19pub enum Visibility {
20    #[default]
21    Visible,
22    Hidden,
23    VeryHidden,
24}
25
26/// How a sheet is displayed (View › Workbook Views; `<sheetView view="…">` in XLSX).
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub enum SheetViewMode {
30    #[default]
31    Normal,
32    /// Pages on paper with rulers, margins and in-place headers and footers.
33    PageLayout,
34    PageBreakPreview,
35}
36
37impl SheetViewMode {
38    /// The XLSX / command spelling: `normal`, `pageLayout` or `pageBreakPreview`.
39    pub fn as_str(self) -> &'static str {
40        match self {
41            SheetViewMode::Normal => "normal",
42            SheetViewMode::PageLayout => "pageLayout",
43            SheetViewMode::PageBreakPreview => "pageBreakPreview",
44        }
45    }
46    pub fn parse(s: &str) -> Option<SheetViewMode> {
47        match s {
48            "normal" => Some(SheetViewMode::Normal),
49            "pageLayout" => Some(SheetViewMode::PageLayout),
50            "pageBreakPreview" => Some(SheetViewMode::PageBreakPreview),
51            _ => None,
52        }
53    }
54}
55
56/// A sheet's identity while the app runs, for what must follow one sheet (an open cell draft): a
57/// sheet made new (added, inserted, copied, read from a file or from JSON) gets a fresh one, and it
58/// stays with the sheet through renames, moves and the copies undo keeps. Never saved.
59/// It is not content: any two compare equal, so comparing sheets compares what they hold; compare
60/// identities with [`SheetUid::get`].
61#[derive(Clone, Copy, Debug)]
62pub struct SheetUid(u64);
63
64/// The next [`SheetUid`]; 0 is never given out.
65static NEXT_SHEET_UID: AtomicU64 = AtomicU64::new(1);
66
67impl SheetUid {
68    pub fn fresh() -> SheetUid {
69        SheetUid(NEXT_SHEET_UID.fetch_add(1, Ordering::Relaxed))
70    }
71    pub fn get(self) -> u64 {
72        self.0
73    }
74}
75
76impl Default for SheetUid {
77    fn default() -> SheetUid {
78        SheetUid::fresh()
79    }
80}
81
82impl PartialEq for SheetUid {
83    fn eq(&self, _: &SheetUid) -> bool {
84        true
85    }
86}
87
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
89pub struct Sheet {
90    pub name: String,
91    /// The sheet's identity while the app runs ([`SheetUid`]). Not in JSON or the file.
92    #[serde(skip)]
93    pub uid: SheetUid,
94    /// The `sheetId` the file gave the sheet, written back on save so that what refers to the
95    /// sheet by its id keeps finding it; 0 for a sheet the file did not have. Not in JSON.
96    #[serde(skip)]
97    pub file_id: u32,
98    pub cells: CellStore,
99    /// Static pictures are sparse rich content. Ordinary cells carry no picture pointer.
100    #[serde(default, skip_serializing_if = "BTreeMap::is_empty", with = "picture_map")]
101    pub cell_pictures: BTreeMap<CellRef, Arc<CellPicture>>,
102    /// Values spilled by dynamic-array formulas (maintained by calc), keyed by cell.
103    #[serde(skip)]
104    pub spill: BTreeMap<CellRef, Value>,
105    /// Anchor → spill range.
106    #[serde(skip)]
107    pub spill_ranges: BTreeMap<CellRef, RangeRef>,
108    pub cols: BTreeMap<u32, LineInfo>,
109    pub rows: BTreeMap<u32, LineInfo>,
110    /// Default column width and row height in pixels at 96 dpi (a file stores row heights in points).
111    pub default_col_width: f32,
112    pub default_row_height: f32,
113    pub merges: Vec<RangeRef>,
114    /// Frozen rows and columns (top/left).
115    pub freeze: Option<(u32, u32)>,
116    pub tab_color: Option<Color>,
117    pub visibility: Visibility,
118    pub show_gridlines: bool,
119    pub show_headings: bool,
120    pub show_formulas: bool,
121    pub show_zeros: bool,
122    /// The outline's level buttons and brackets (Excel's Show Outline Symbols, Ctrl+8); the
123    /// outline itself stays.
124    #[serde(default = "yes")]
125    pub show_outline_symbols: bool,
126    /// Outline summary rows sit below their detail, and summary columns right of it (Data ▸
127    /// Outline ▸ Settings; off puts them above / left). Saved as `outlinePr summaryBelow` and
128    /// `summaryRight`.
129    #[serde(default = "yes")]
130    pub summary_below: bool,
131    #[serde(default = "yes")]
132    pub summary_right: bool,
133    pub right_to_left: bool,
134    pub zoom: u16,
135    pub cond_formats: Vec<CondFormat>,
136    pub validations: Vec<Validation>,
137    pub tables: Vec<Table>,
138    pub autofilter: Option<AutoFilter>,
139    #[serde(with = "crate::model::cellmap")]
140    pub comments: BTreeMap<CellRef, Comment>,
141    #[serde(with = "crate::model::cellmap")]
142    pub hyperlinks: BTreeMap<CellRef, Hyperlink>,
143    pub charts: Vec<Chart>,
144    pub images: Vec<Image>,
145    pub shapes: Vec<Shape>,
146    pub sparklines: Vec<Sparkline>,
147    /// On-sheet slicer widgets (filter a table column by clicking value tiles).
148    #[serde(default)]
149    pub slicers: Vec<Slicer>,
150    /// Form controls (buttons, sliders, spin buttons). They paint above every other object and
151    /// take clicks first.
152    #[serde(default, skip_serializing_if = "Vec::is_empty")]
153    pub controls: Vec<FormControl>,
154    /// PivotTables whose output lives on this sheet.
155    #[serde(default)]
156    pub pivots: Vec<PivotTable>,
157    pub protection: Option<SheetProtection>,
158    /// Allow Edit Ranges (meaningful while the sheet is protected).
159    #[serde(default, skip_serializing_if = "Vec::is_empty")]
160    pub edit_ranges: Vec<crate::model::EditRange>,
161    pub print: PrintSettings,
162    /// Last selection and scroll position, saved with the file.
163    pub view_active: CellRef,
164    pub view_top_left: CellRef,
165    /// Normal, Page Layout or Page Break Preview.
166    #[serde(default)]
167    pub view: SheetViewMode,
168    /// Unmodelled file content of this sheet kept for saving (form controls, OLE objects, query
169    /// tables). Goes away with the sheet; not in JSON.
170    #[serde(skip)]
171    pub preserved: Option<Arc<crate::model::preserved::Preserved>>,
172}
173
174fn yes() -> bool {
175    true
176}
177
178/// Default width of a column and height of a row (pixels at 96 dpi) for the default font.
179pub const DEFAULT_COL_WIDTH: f32 = 64.0;
180pub const DEFAULT_ROW_HEIGHT: f32 = 20.0;
181
182/// Column width (points) → characters of the default font's digit width (7 px), without the
183/// cell padding: the value the Column Width dialog shows.
184pub fn points_to_chars(px: f64) -> f64 {
185    if px <= 12.0 { (px / 12.0 * 100.0).round() / 100.0 } else { ((px - 5.0) / 7.0 * 100.0 + 0.5).trunc() / 100.0 }
186}
187
188impl Sheet {
189    pub fn new(name: impl Into<String>) -> Sheet {
190        Sheet {
191            name: name.into(),
192            uid: SheetUid::fresh(),
193            file_id: 0,
194            cells: CellStore::new(),
195            cell_pictures: BTreeMap::new(),
196            spill: BTreeMap::new(),
197            spill_ranges: BTreeMap::new(),
198            cols: BTreeMap::new(),
199            rows: BTreeMap::new(),
200            default_col_width: DEFAULT_COL_WIDTH,
201            default_row_height: DEFAULT_ROW_HEIGHT,
202            merges: vec![],
203            freeze: None,
204            tab_color: None,
205            visibility: Visibility::Visible,
206            show_gridlines: true,
207            show_outline_symbols: true,
208            summary_below: true,
209            summary_right: true,
210            show_headings: true,
211            show_formulas: false,
212            show_zeros: true,
213            right_to_left: false,
214            zoom: 100,
215            cond_formats: vec![],
216            validations: vec![],
217            tables: vec![],
218            autofilter: None,
219            comments: BTreeMap::new(),
220            hyperlinks: BTreeMap::new(),
221            charts: vec![],
222            images: vec![],
223            shapes: vec![],
224            sparklines: vec![],
225            slicers: vec![],
226            controls: vec![],
227            pivots: vec![],
228            protection: None,
229            edit_ranges: vec![],
230            print: PrintSettings::default(),
231            view_active: CellRef::default(),
232            view_top_left: CellRef::default(),
233            view: SheetViewMode::Normal,
234            preserved: None,
235        }
236    }
237
238    pub fn cell(&self, c: CellRef) -> Option<&Cell> {
239        self.cells.get(c)
240    }
241    /// The source of a non-anchor dynamic-array spill cell, from the current calc state.
242    /// Anchors, ordinary cells and cells outside successfully generated spills return `None`.
243    pub fn spill_anchor(&self, c: CellRef) -> Option<CellRef> {
244        self.spill_ranges.iter().find(|(anchor, range)| **anchor != c && range.contains(c)).map(|(anchor, _)| *anchor)
245    }
246
247    /// Removes the spilled values inside `r`. The work is bounded by the values stored, not by
248    /// the size of `r`, so a range from a file that claims the whole sheet costs no more than the
249    /// values it holds.
250    pub fn clear_spill(&mut self, r: RangeRef) {
251        if r.count() <= self.spill.len() as u64 {
252            for c in r.iter() {
253                self.spill.remove(&c);
254            }
255        } else {
256            self.spill.retain(|c, _| !r.contains(*c));
257        }
258    }
259
260    /// Turns the spill anchored at `anchor` into plain values, for a Python cell that keeps only
261    /// its value: the range is forgotten, and each spilled value in it becomes its cell's value,
262    /// unless the cell holds content of its own (its format is kept either way). The work is
263    /// bounded by the spilled values stored, not by the size of the range.
264    pub fn settle_spill(&mut self, anchor: CellRef) {
265        let Some(r) = self.spill_ranges.remove(&anchor) else { return };
266        let inside: Vec<CellRef> = self.spill.range(r.start..=r.end).map(|(c, _)| *c).filter(|c| r.contains(*c)).collect();
267        for c in inside {
268            let Some(v) = self.spill.remove(&c) else { continue };
269            if v.is_empty() || c == anchor {
270                continue;
271            }
272            match self.cells.get_mut(c) {
273                Some(cell) if cell.has_content() => {}
274                Some(cell) => cell.value = v,
275                None => {
276                    let style = self.style_id(c);
277                    self.cells.set(c, Cell::styled(v, style));
278                }
279            }
280        }
281    }
282
283    /// The floating object with workbook-unique id `id` on this sheet.
284    pub fn object_ref(&self, id: u32) -> Option<ObjRef> {
285        let kind = if self.charts.iter().any(|o| o.id == id) {
286            ObjKind::Chart
287        } else if self.images.iter().any(|o| o.id == id) {
288            ObjKind::Image
289        } else if let Some(s) = self.shapes.iter().find(|o| o.id == id) {
290            if s.kind == ShapeKind::Equation { ObjKind::Equation } else { ObjKind::Shape }
291        } else if self.slicers.iter().any(|o| o.id == id) {
292            ObjKind::Slicer
293        } else if self.controls.iter().any(|o| o.id == id) {
294            ObjKind::Control
295        } else {
296            return None;
297        };
298        Some(ObjRef::new(kind, id))
299    }
300
301    /// Whether the floating object `o` exists on this sheet. `Shape` accepts any shape (an
302    /// equation included); `Equation` only an equation.
303    pub fn has_object(&self, o: ObjRef) -> bool {
304        self.object_anchor(o).is_some()
305    }
306
307    /// The anchor of the floating object `o`.
308    pub fn object_anchor(&self, o: ObjRef) -> Option<Anchor> {
309        match o.kind {
310            ObjKind::Chart => self.charts.iter().find(|c| c.id == o.id).map(|c| c.anchor),
311            ObjKind::Image => self.images.iter().find(|c| c.id == o.id).map(|c| c.anchor),
312            ObjKind::Shape => self.shapes.iter().find(|c| c.id == o.id).map(|c| c.anchor),
313            ObjKind::Slicer => self.slicers.iter().find(|c| c.id == o.id).map(|c| c.anchor),
314            ObjKind::Equation => self.shapes.iter().find(|c| c.id == o.id && c.kind == ShapeKind::Equation).map(|c| c.anchor),
315            ObjKind::Control => self.controls.iter().find(|c| c.id == o.id).map(|c| c.anchor),
316        }
317    }
318
319    pub fn object_anchor_mut(&mut self, o: ObjRef) -> Option<&mut Anchor> {
320        match o.kind {
321            ObjKind::Chart => self.charts.iter_mut().find(|c| c.id == o.id).map(|c| &mut c.anchor),
322            ObjKind::Image => self.images.iter_mut().find(|c| c.id == o.id).map(|c| &mut c.anchor),
323            ObjKind::Shape => self.shapes.iter_mut().find(|c| c.id == o.id).map(|c| &mut c.anchor),
324            ObjKind::Slicer => self.slicers.iter_mut().find(|c| c.id == o.id).map(|c| &mut c.anchor),
325            ObjKind::Equation => self.shapes.iter_mut().find(|c| c.id == o.id && c.kind == ShapeKind::Equation).map(|c| &mut c.anchor),
326            ObjKind::Control => self.controls.iter_mut().find(|c| c.id == o.id).map(|c| &mut c.anchor),
327        }
328    }
329
330    /// Removes the floating object `o`; false when there is no such object.
331    pub fn remove_object(&mut self, o: ObjRef) -> bool {
332        fn take<T>(v: &mut Vec<T>, f: impl Fn(&T) -> bool) -> bool {
333            let n = v.len();
334            v.retain(|x| !f(x));
335            v.len() != n
336        }
337        match o.kind {
338            ObjKind::Chart => take(&mut self.charts, |c| c.id == o.id),
339            ObjKind::Image => take(&mut self.images, |c| c.id == o.id),
340            ObjKind::Shape => take(&mut self.shapes, |c| c.id == o.id),
341            ObjKind::Slicer => take(&mut self.slicers, |c| c.id == o.id),
342            ObjKind::Equation => take(&mut self.shapes, |c| c.id == o.id && c.kind == ShapeKind::Equation),
343            ObjKind::Control => take(&mut self.controls, |c| c.id == o.id),
344        }
345    }
346
347    /// Every floating object's anchor in a fixed order (charts, images, shapes, slicers, controls)
348    /// that [`Sheet::object_anchors_mut`] repeats, so a pass can compute new anchors from the old
349    /// ones and write them back in step.
350    pub fn object_anchors(&self) -> impl Iterator<Item = &Anchor> {
351        self.charts
352            .iter()
353            .map(|o| &o.anchor)
354            .chain(self.images.iter().map(|o| &o.anchor))
355            .chain(self.shapes.iter().map(|o| &o.anchor))
356            .chain(self.slicers.iter().map(|o| &o.anchor))
357            .chain(self.controls.iter().map(|o| &o.anchor))
358    }
359
360    pub fn object_anchors_mut(&mut self) -> impl Iterator<Item = &mut Anchor> {
361        self.charts
362            .iter_mut()
363            .map(|o| &mut o.anchor)
364            .chain(self.images.iter_mut().map(|o| &mut o.anchor))
365            .chain(self.shapes.iter_mut().map(|o| &mut o.anchor))
366            .chain(self.slicers.iter_mut().map(|o| &mut o.anchor))
367            .chain(self.controls.iter_mut().map(|o| &mut o.anchor))
368    }
369    /// Displayed value of a cell: its own value, or a spilled value.
370    pub fn value(&self, c: CellRef) -> Value {
371        match self.cells.get(c) {
372            Some(cell) if !cell.value.is_empty() || cell.formula.is_some() => cell.value.clone(),
373            _ => self.spill.get(&c).cloned().unwrap_or(Value::Empty),
374        }
375    }
376    pub fn value_ref(&self, c: CellRef) -> Option<&Value> {
377        match self.cells.get(c) {
378            Some(cell) if !cell.value.is_empty() || cell.formula.is_some() => Some(&cell.value),
379            _ => self.spill.get(&c),
380        }
381    }
382    /// Last row (inclusive) of the maximal contiguous run of non-empty values in `col`, starting
383    /// at `from_row` and capped at `cap` (inclusive). `from_row - 1` when `from_row` itself is
384    /// empty (or `from_row > cap`), same as a cell-by-cell `value_ref` scan would find, but
385    /// without calling `value_ref` once per row: `CellStore::contiguous_value_run` walks each
386    /// band's row map directly for the common case (a run of stored cells), only falling back to
387    /// a per-row `value_ref` check where the stored run stops short of `cap`. Used by the fill-handle
388    /// double-click, where `col` can be densely populated for very long stretches.
389    pub fn last_contiguous_nonempty(&self, col: u32, from_row: u32, cap: u32) -> u32 {
390        if from_row > cap {
391            return from_row.saturating_sub(1);
392        }
393        let mut row = from_row;
394        loop {
395            if row > cap {
396                return cap;
397            }
398            let c = CellRef::new(row, col);
399            match self.cells.contiguous_value_run(c, cap) {
400                Some(last) => row = last + 1,
401                None => {
402                    // The run stopped short of `row`: ask `value_ref` for this one row, which also
403                    // covers a stored cell with no value and no formula (a formatted spill child)
404                    // falling through to `spill`.
405                    let qualifies = self.value_ref(c).is_some_and(|v| !v.is_empty());
406                    if qualifies {
407                        row += 1;
408                    } else {
409                        return row.saturating_sub(1);
410                    }
411                }
412            }
413        }
414    }
415    pub fn style_id(&self, c: CellRef) -> StyleId {
416        if let Some(cell) = self.cells.get(c) {
417            return cell.style;
418        }
419        if let Some(s) = self.rows.get(&c.row).and_then(|r| r.style) {
420            return s;
421        }
422        self.cols.get(&c.col).and_then(|r| r.style).unwrap_or_default()
423    }
424    pub fn set_value(&mut self, c: CellRef, v: Value) {
425        let style = self.style_id(c);
426        self.set_cell(c, Cell { value: v, formula: None, style, py: None });
427    }
428    /// Replaces content, removing any picture at this address. Style-only edits use set_style.
429    pub fn set_cell(&mut self, c: CellRef, cell: Cell) {
430        self.cell_pictures.remove(&c);
431        self.cells.set(c, cell);
432    }
433    pub fn remove_cell(&mut self, c: CellRef) -> Option<Cell> {
434        self.cell_pictures.remove(&c);
435        self.cells.remove(c)
436    }
437    /// Removes content in a range; returns scalar cells, with pictures removed from the side map.
438    pub fn take_cells(&mut self, range: RangeRef) -> Vec<(CellRef, Cell)> {
439        self.cell_pictures.retain(|c, _| !range.contains(*c));
440        self.cells.take_range(range)
441    }
442    /// Pictures retain a scalar fallback for calculation and unsupported consumers.
443    pub fn set_picture(&mut self, c: CellRef, picture: Arc<CellPicture>) {
444        let style = self.style_id(c);
445        self.cells.set(c, Cell { value: Value::Error(crate::base::CellError::Value), formula: None, style, py: None });
446        self.cell_pictures.insert(c, picture);
447    }
448    pub fn input_text(&self, c: CellRef) -> String {
449        if self.cell_pictures.contains_key(&c) { String::new() } else { self.cell(c).map(Cell::input_text).unwrap_or_default() }
450    }
451    pub fn set_style(&mut self, c: CellRef, style: StyleId) {
452        let mut cell = self.cells.get(c).cloned().unwrap_or_default();
453        cell.style = style;
454        self.cells.set(c, cell);
455    }
456
457    // ---- geometry
458
459    /// Whether this cell has a table or ordinary AutoFilter dropdown.
460    pub fn has_filter_button(&self, c: CellRef) -> bool {
461        self.autofilter.as_ref().is_some_and(|af| af.range.start.row == c.row && af.range.contains(c))
462            || self.tables.iter().any(|t| t.filter_button && t.header_row && t.range.start.row == c.row && t.range.contains(c))
463    }
464
465    /// Horizontal header space in unzoomed grid units (96 units per inch).
466    /// Observed in Windows Excel 16.0 build 20326: AutoFit reserves 12.5 pt at
467    /// 100-200% zoom, 25 pt at 50%, regardless of header row height. Below 100%
468    /// the dropdown keeps its screen width. This is geometry, not font padding.
469    pub fn filter_button_space(&self, c: CellRef) -> f32 {
470        if self.has_filter_button(c) { (12.5 * 96.0 / 72.0) / (self.zoom as f32 / 100.0).clamp(0.1, 1.0) } else { 0.0 }
471    }
472
473    pub fn col_width(&self, col: u32) -> f32 {
474        match self.cols.get(&col) {
475            Some(i) if i.hidden => 0.0,
476            Some(LineInfo { size: Some(w), .. }) => *w,
477            _ => self.default_col_width,
478        }
479    }
480    pub fn row_height(&self, row: u32) -> f32 {
481        match self.rows.get(&row) {
482            Some(i) if i.hidden => 0.0,
483            Some(LineInfo { size: Some(h), .. }) => *h,
484            _ => self.default_row_height,
485        }
486    }
487    pub fn is_row_hidden(&self, row: u32) -> bool {
488        self.rows.get(&row).is_some_and(|r| r.hidden)
489    }
490    /// Whether `row` is hidden by an applied filter: a hidden row below the header of the
491    /// sheet AutoFilter while it has criteria, or a hidden data row of a filtered table.
492    /// Other hidden rows were hidden by hand.
493    pub fn is_row_filtered(&self, row: u32) -> bool {
494        if !self.is_row_hidden(row) {
495            return false;
496        }
497        let in_sheet_filter =
498            self.autofilter.as_ref().is_some_and(|af| !af.criteria.is_empty() && row > af.range.start.row && row <= af.range.end.row);
499        in_sheet_filter || self.tables.iter().any(|t| !t.filter.is_empty() && t.data_range().is_some_and(|r| row >= r.start.row && row <= r.end.row))
500    }
501    pub fn is_col_hidden(&self, col: u32) -> bool {
502        self.cols.get(&col).is_some_and(|r| r.hidden)
503    }
504    /// Y offset (points) of the top of `row`.
505    pub fn row_top(&self, row: u32) -> f64 {
506        let mut y = row as f64 * self.default_row_height as f64;
507        for (r, info) in self.rows.range(..row) {
508            let _ = r;
509            let h = if info.hidden { 0.0 } else { info.size.unwrap_or(self.default_row_height) };
510            y += h as f64 - self.default_row_height as f64;
511        }
512        y
513    }
514    pub fn col_left(&self, col: u32) -> f64 {
515        let mut x = col as f64 * self.default_col_width as f64;
516        for (_, info) in self.cols.range(..col) {
517            let w = if info.hidden { 0.0 } else { info.size.unwrap_or(self.default_col_width) };
518            x += w as f64 - self.default_col_width as f64;
519        }
520        x
521    }
522    /// Row containing y (points from the top), clamped.
523    pub fn row_at(&self, y: f64) -> u32 {
524        line_at(y, self.default_row_height as f64, &self.rows, crate::base::MAX_ROWS)
525    }
526    pub fn col_at(&self, x: f64) -> u32 {
527        line_at(x, self.default_col_width as f64, &self.cols, crate::base::MAX_COLS)
528    }
529
530    /// The merged range containing `c`, if any.
531    pub fn merge_at(&self, c: CellRef) -> Option<RangeRef> {
532        self.merges.iter().find(|m| m.contains(c)).copied()
533    }
534    pub fn table_at(&self, c: CellRef) -> Option<&Table> {
535        self.tables.iter().find(|t| t.range.contains(c))
536    }
537    pub fn used_range(&self) -> Option<RangeRef> {
538        let mut r = self.cells.used_range();
539        for sr in self.spill_ranges.values() {
540            r = Some(r.map_or(*sr, |x| x.union(sr)));
541        }
542        r
543    }
544    pub fn is_protected(&self) -> bool {
545        self.protection.is_some()
546    }
547}
548
549/// Prefix sums over the custom-sized lines of an axis: O(log n) position ↔ line lookups even
550/// when every row has an explicit height (common in imported files).
551#[derive(Clone, Debug, Default)]
552pub struct LineIndex {
553    default: f64,
554    max: u32,
555    /// (line, start position of that line, its size), sorted by line.
556    lines: Vec<(u32, f64, f64)>,
557}
558
559impl LineIndex {
560    pub fn new(custom: &BTreeMap<u32, LineInfo>, default: f32, max: u32) -> LineIndex {
561        let default = default as f64;
562        let mut lines = Vec::with_capacity(custom.len());
563        let mut pos = 0.0;
564        let mut prev = 0u32;
565        for (&line, info) in custom {
566            pos += (line - prev) as f64 * default;
567            let size = if info.hidden { 0.0 } else { info.size.map_or(default, |s| s as f64) };
568            lines.push((line, pos, size));
569            pos += size;
570            prev = line + 1;
571        }
572        LineIndex { default, max, lines }
573    }
574    /// Start position of `line`.
575    pub fn start(&self, line: u32) -> f64 {
576        match self.lines.binary_search_by(|e| e.0.cmp(&line)) {
577            Ok(i) => self.lines.get(i).map_or(0.0, |e| e.1),
578            Err(0) => line as f64 * self.default,
579            Err(i) => match self.lines.get(i - 1) {
580                Some(&(l, p, s)) => p + s + (line - l - 1) as f64 * self.default,
581                None => line as f64 * self.default,
582            },
583        }
584    }
585    /// The line containing `pos`.
586    pub fn at(&self, pos: f64) -> u32 {
587        if pos <= 0.0 {
588            return 0;
589        }
590        // Last custom line starting at or before pos.
591        let i = self.lines.partition_point(|e| e.1 <= pos);
592        let (base_line, base_pos) = match i.checked_sub(1).and_then(|k| self.lines.get(k)) {
593            Some(&(l, p, s)) => {
594                if pos < p + s {
595                    return l.min(self.max - 1);
596                }
597                (l + 1, p + s)
598            }
599            None => (0, 0.0),
600        };
601        if self.default <= 0.0 {
602            return base_line.min(self.max - 1);
603        }
604        let k = ((pos - base_pos) / self.default).floor();
605        let r = base_line as f64 + k;
606        if r >= self.max as f64 { self.max - 1 } else { r as u32 }
607    }
608}
609
610/// Inverse of the cumulative size along an axis with sparse custom sizes.
611fn line_at(pos: f64, default: f64, custom: &BTreeMap<u32, LineInfo>, max: u32) -> u32 {
612    if pos <= 0.0 {
613        return 0;
614    }
615    let mut start_pos = 0.0; // position of line `idx`
616    let mut idx: u32 = 0;
617    for (&line, info) in custom.iter() {
618        // Lines idx..line have default size.
619        let span = (line - idx) as f64 * default;
620        if pos < start_pos + span {
621            let k = ((pos - start_pos) / default).floor() as u32;
622            return (idx + k).min(max - 1);
623        }
624        start_pos += span;
625        let size = if info.hidden { 0.0 } else { info.size.map_or(default, |s| s as f64) };
626        if pos < start_pos + size {
627            return line.min(max - 1);
628        }
629        start_pos += size;
630        idx = line + 1;
631    }
632    if default <= 0.0 {
633        return idx.min(max - 1);
634    }
635    let k = ((pos - start_pos) / default).floor();
636    let r = idx as f64 + k;
637    if r >= max as f64 { max - 1 } else { r as u32 }
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use crate::model::cell::Formula;
644
645    #[test]
646    fn a_sheet_keeps_its_identity_through_clones_and_a_new_sheet_gets_its_own() {
647        let a = Sheet::new("A");
648        let b = Sheet::new("A");
649        assert_ne!(a.uid.get(), b.uid.get(), "two new sheets");
650        assert_ne!(a.uid.get(), 0);
651        // A clone (an undo snapshot, a rename through Arc::make_mut) is the same sheet.
652        let mut renamed = a.clone();
653        renamed.name = "B".into();
654        assert_eq!(renamed.uid.get(), a.uid.get());
655        // Not content: equal sheets compare equal whatever their identity.
656        assert_eq!(a, b);
657        // Not in JSON: a sheet read back is a new one.
658        let json = serde_json::to_string(&a).unwrap();
659        assert!(!json.contains("uid"), "{json}");
660        let read: Sheet = serde_json::from_str(&json).unwrap();
661        assert_ne!(read.uid.get(), a.uid.get());
662        assert_eq!(read, a);
663    }
664
665    #[test]
666    fn line_index_matches_linear() {
667        let mut s = Sheet::new("S");
668        for r in (0..5000).step_by(3) {
669            s.rows.insert(r, LineInfo { size: Some(10.0 + (r % 7) as f32), hidden: r % 11 == 0, ..Default::default() });
670        }
671        let idx = LineIndex::new(&s.rows, s.default_row_height, crate::base::MAX_ROWS);
672        for r in [0u32, 1, 2, 3, 4, 100, 999, 4998, 5000, 6000] {
673            assert!((idx.start(r) - s.row_top(r)).abs() < 1e-6, "start {r}");
674        }
675        for y in [0.0, 5.0, 33.3, 1000.0, 47_000.0, 90_000.0] {
676            assert_eq!(idx.at(y), s.row_at(y), "at {y}");
677        }
678    }
679
680    /// Reference implementation: one `value_ref` call per row, exactly the loop
681    /// `last_contiguous_nonempty` replaces (from `grid.rs`'s fill-handle double-click).
682    fn last_contiguous_nonempty_naive(s: &Sheet, col: u32, from_row: u32, cap: u32) -> u32 {
683        let mut row = from_row;
684        while row <= cap && s.value_ref(CellRef::new(row, col)).is_some_and(|v| !v.is_empty()) {
685            row += 1;
686        }
687        row.saturating_sub(1).max(from_row.saturating_sub(1))
688    }
689
690    /// Deterministic xorshift32 so failures reproduce without pulling in a `rand` dependency
691    /// just for this test.
692    struct Xorshift32(u32);
693    impl Xorshift32 {
694        fn next(&mut self) -> u32 {
695            self.0 ^= self.0 << 13;
696            self.0 ^= self.0 >> 17;
697            self.0 ^= self.0 << 5;
698            self.0
699        }
700        fn below(&mut self, n: u32) -> u32 {
701            self.next() % n.max(1)
702        }
703    }
704
705    #[test]
706    fn last_contiguous_nonempty_matches_naive() {
707        let mut rng = Xorshift32(0x9e3779b9);
708        for trial in 0..300u32 {
709            let mut s = Sheet::new("S");
710            let rows = rng.below(400);
711            // A mix of directly-stored values, formulas (value empty but formula present),
712            // spill-only cells, blank-but-stored cells, and gaps, like a real sheet.
713            for r in 0..rows {
714                match rng.below(5) {
715                    0 => s.set_value(CellRef::new(r, 0), Value::Number(r as f64)),
716                    1 => s.set_cell(CellRef::new(r, 0), Cell { formula: Some(Arc::new(Formula::new("1"))), ..Default::default() }),
717                    2 => {
718                        s.spill.insert(CellRef::new(r, 0), Value::Number(r as f64));
719                    }
720                    3 => {
721                        // Stored but blank: does not count (matches `has_value`/`value_ref`).
722                        s.set_value(CellRef::new(r, 0), Value::Text("".into()));
723                    }
724                    4 if rng.below(2) == 0 => {
725                        // A style-only stored cell over a spill value: `value_ref` falls through
726                        // to the spill value, so the row counts.
727                        s.spill.insert(CellRef::new(r, 0), Value::Number(r as f64));
728                        s.set_cell(CellRef::new(r, 0), Cell { style: StyleId(1), ..Default::default() });
729                    }
730                    _ => {} // gap
731                }
732            }
733            for _ in 0..5 {
734                let from_row = rng.below(420);
735                let cap = from_row.saturating_add(rng.below(450));
736                let got = s.last_contiguous_nonempty(0, from_row, cap);
737                let want = last_contiguous_nonempty_naive(&s, 0, from_row, cap);
738                assert_eq!(got, want, "trial {trial}: col 0, from_row {from_row}, cap {cap}, rows up to {rows}");
739            }
740        }
741    }
742
743    #[test]
744    fn last_contiguous_nonempty_dense_run() {
745        let mut s = Sheet::new("S");
746        for r in 0..2000u32 {
747            s.set_value(CellRef::new(r, 2), Value::Number(r as f64));
748        }
749        assert_eq!(s.last_contiguous_nonempty(2, 0, 1_999), 1999);
750        assert_eq!(s.last_contiguous_nonempty(2, 0, 1_000), 1000); // capped
751        assert_eq!(s.last_contiguous_nonempty(2, 500, 1_999), 1999);
752        assert_eq!(s.last_contiguous_nonempty(2, 2000, 3000), 1999); // nothing at from_row
753        assert_eq!(s.last_contiguous_nonempty(2, 2001, 3000), 2000); // from_row > cap: from_row - 1
754    }
755
756    #[test]
757    fn last_contiguous_nonempty_formula_with_empty_value_ends_the_run() {
758        let mut s = Sheet::new("S");
759        s.set_value(CellRef::new(0, 1), Value::Number(1.0));
760        s.set_value(CellRef::new(1, 1), Value::Number(2.0));
761        // row 2: a formula (e.g. `=""`) whose cached value is empty. It does not count, same as
762        // `value_ref(...).is_some_and(|v| !v.is_empty())` says for it, even though it has a
763        // formula (unlike `CellStore::has`, used for Ctrl+arrow navigation, which counts it).
764        s.set_cell(CellRef::new(2, 1), Cell { formula: Some(Arc::new(Formula::new("\"\""))), ..Default::default() });
765        s.set_value(CellRef::new(3, 1), Value::Number(4.0)); // would extend the run if row 2 didn't stop it
766        assert_eq!(s.last_contiguous_nonempty(1, 0, 100), 1);
767        assert_eq!(s.last_contiguous_nonempty(1, 2, 100), 1); // from_row itself is the stop
768    }
769
770    #[test]
771    fn last_contiguous_nonempty_spill_extends_a_style_only_stored_row() {
772        let mut s = Sheet::new("S");
773        s.set_value(CellRef::new(0, 1), Value::Number(1.0));
774        // row 1: a formatted spill child: stored with a style only, value from `spill`.
775        s.spill.insert(CellRef::new(1, 1), Value::Number(2.0));
776        s.set_cell(CellRef::new(1, 1), Cell { style: StyleId(1), ..Default::default() });
777        s.set_value(CellRef::new(2, 1), Value::Number(3.0));
778        assert_eq!(s.last_contiguous_nonempty(1, 0, 100), 2);
779        assert_eq!(s.last_contiguous_nonempty(1, 1, 100), 2);
780    }
781
782    #[test]
783    fn last_contiguous_nonempty_spill_extends_an_unstored_row() {
784        let mut s = Sheet::new("S");
785        s.set_value(CellRef::new(0, 1), Value::Number(1.0));
786        // row 1: nothing stored, but a spill value from an array formula anchored elsewhere.
787        s.spill.insert(CellRef::new(1, 1), Value::Number(2.0));
788        s.set_value(CellRef::new(2, 1), Value::Number(3.0));
789        // row 3: a genuine gap (nothing stored, no spill).
790        assert_eq!(s.last_contiguous_nonempty(1, 0, 100), 2);
791        assert_eq!(s.last_contiguous_nonempty(1, 3, 100), 2); // from_row itself is the gap
792    }
793
794    #[test]
795    fn geometry() {
796        let mut s = Sheet::new("S");
797        assert_eq!(s.row_top(3), 60.0);
798        s.rows.insert(1, LineInfo { size: Some(40.0), ..Default::default() });
799        s.rows.insert(2, LineInfo { hidden: true, ..Default::default() });
800        assert_eq!(s.row_top(1), 20.0);
801        assert_eq!(s.row_top(2), 60.0);
802        assert_eq!(s.row_top(3), 60.0);
803        assert_eq!(s.row_top(4), 80.0);
804        assert_eq!(s.row_at(10.0), 0);
805        assert_eq!(s.row_at(30.0), 1);
806        assert_eq!(s.row_at(59.0), 1);
807        assert_eq!(s.row_at(61.0), 3);
808        assert_eq!(s.row_at(85.0), 4);
809        assert_eq!(s.col_at(1e12), crate::base::MAX_COLS - 1);
810        assert_eq!(s.col_at(-5.0), 0);
811        assert_eq!(s.col_left(2), 128.0);
812    }
813
814    use crate::model::features::{ChartKind, EquationCache, LegendPos, ShapeFormat, ShapeKind};
815
816    /// Adds a floating object of `kind` with id `id` at `anchor`. The match is exhaustive on
817    /// purpose: a new kind of floating object does not compile here until this test covers it,
818    /// and the counts in `object_anchor_accessors_visit_the_same_objects_in_the_same_order`
819    /// then catch an accessor that was not taught about it. An equation is a shape of
820    /// `ShapeKind::Equation`, so it lives in `shapes` and is visited among them.
821    fn push_object(s: &mut Sheet, kind: ObjKind, id: u32, anchor: Anchor) {
822        match kind {
823            ObjKind::Chart => s.charts.push(Chart {
824                id,
825                kind: ChartKind::default(),
826                anchor,
827                title: None,
828                series: vec![],
829                legend: LegendPos::default(),
830                data_labels: false,
831                data_table: None,
832                gridlines: false,
833                style: 0,
834                hole_size: 75,
835                first_slice_angle: 0,
836                x_title: None,
837                y_title: None,
838                source: None,
839                by_rows: false,
840                pivot: None,
841            }),
842            ObjKind::Image => s.images.push(Image { id, anchor, data: Arc::new(vec![]), mime: String::new(), alt: String::new() }),
843            ObjKind::Shape | ObjKind::Equation => s.shapes.push(Shape {
844                id,
845                kind: if kind == ObjKind::Equation { ShapeKind::Equation } else { ShapeKind::default() },
846                anchor,
847                fill: Color::default(),
848                line: Color::default(),
849                text: String::new(),
850                format: ShapeFormat::default(),
851                math: EquationCache::default(),
852            }),
853            ObjKind::Slicer => {
854                s.slicers.push(Slicer { id, table: String::new(), column: String::new(), caption: String::new(), anchor, selected: None, columns: 1 })
855            }
856            ObjKind::Control => s.controls.push(FormControl { id, anchor, ..Default::default() }),
857        }
858    }
859
860    /// Sorting, and inserting or deleting rows and columns, read every anchor through
861    /// `object_anchors`, compute the new ones and write them back through `object_anchors_mut`
862    /// in step. The two must visit the same objects in the same order, or an object receives
863    /// another object's anchor; a kind that reaches only one of them would break that silently.
864    #[test]
865    fn object_anchor_accessors_visit_the_same_objects_in_the_same_order() {
866        let mut s = Sheet::new("S");
867        // Two objects of every kind, inserted in a mixed order, with ids unrelated to the kind.
868        // Each anchor cell's row carries the id, so an anchor read back names its object.
869        let inserted = [
870            (ObjKind::Slicer, 7),
871            (ObjKind::Chart, 3),
872            (ObjKind::Image, 11),
873            (ObjKind::Shape, 2),
874            (ObjKind::Chart, 9),
875            (ObjKind::Slicer, 1),
876            (ObjKind::Image, 4),
877            (ObjKind::Equation, 6),
878            (ObjKind::Control, 13),
879            (ObjKind::Shape, 12),
880            (ObjKind::Equation, 8),
881            (ObjKind::Control, 15),
882        ];
883        for &(kind, id) in &inserted {
884            push_object(&mut s, kind, id, Anchor { cell: CellRef::new(id, 0), ..Default::default() });
885        }
886        let per_kind = s.charts.len() + s.images.len() + s.shapes.len() + s.slicers.len() + s.controls.len();
887        assert_eq!(per_kind, inserted.len());
888        assert_eq!(s.object_anchors().count(), per_kind, "object_anchors visits every collection");
889        assert_eq!(s.object_anchors_mut().count(), per_kind, "object_anchors_mut visits every collection");
890        // Mark each anchor with its position through the mutable accessor; the shared accessor
891        // reads the marks back in the same sequence.
892        for (i, a) in s.object_anchors_mut().enumerate() {
893            a.dx = i as f32;
894        }
895        let marks: Vec<f32> = s.object_anchors().map(|a| a.dx).collect();
896        assert_eq!(marks, (0..per_kind).map(|i| i as f32).collect::<Vec<f32>>());
897        // The sequence is the documented one (charts, images, shapes, slicers, controls, each in
898        // insertion order; equations are shapes) and every object appears in it once.
899        let collection = |k: ObjKind| if k == ObjKind::Equation { ObjKind::Shape } else { k };
900        let documented: Vec<u32> = [ObjKind::Chart, ObjKind::Image, ObjKind::Shape, ObjKind::Slicer, ObjKind::Control]
901            .iter()
902            .flat_map(|&k| inserted.iter().filter(move |(kind, _)| collection(*kind) == k).map(|&(_, id)| id))
903            .collect();
904        let visited: Vec<u32> = s.object_anchors().map(|a| a.cell.row).collect();
905        assert_eq!(visited, documented);
906        // The mark written through the bulk accessor landed on the object `object_anchor` finds
907        // by kind and id.
908        for &(kind, id) in &inserted {
909            let position = documented.iter().position(|&x| x == id).map(|p| p as f32);
910            assert_eq!(s.object_anchor(ObjRef::new(kind, id)).map(|a| a.dx), position, "{kind:?} {id}");
911        }
912    }
913}
914
915// CellRef is a struct, so encode the sparse map as entries for JSON as well as other formats.
916mod picture_map {
917    use crate::base::CellRef;
918    use crate::model::cell::CellPicture;
919    use serde::{Deserialize, Deserializer, Serialize, Serializer};
920    use std::collections::BTreeMap;
921    use std::sync::Arc;
922    pub fn serialize<S: Serializer>(map: &BTreeMap<CellRef, Arc<CellPicture>>, serializer: S) -> Result<S::Ok, S::Error> {
923        map.iter().collect::<Vec<_>>().serialize(serializer)
924    }
925    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<BTreeMap<CellRef, Arc<CellPicture>>, D::Error> {
926        Ok(Vec::<(CellRef, Arc<CellPicture>)>::deserialize(deserializer)?.into_iter().collect())
927    }
928}