Skip to main content

sheets_diff/output/
view.rs

1//! Framework-neutral GUI view adapters over `WorkbookDiff` (RFC-029).
2//!
3//! These types borrow from `WorkbookDiff` and allocate display strings only
4//! on demand.  No GUI framework dependency is introduced.
5
6use crate::address::CellAddress;
7use crate::model::{
8    CellChangeKind, CellDiff, SheetChange, Severity, WorkbookDiff,
9};
10
11// ---------------------------------------------------------------------------
12// Filtering
13// ---------------------------------------------------------------------------
14
15/// Controls which change categories are visible in a `DiffView`.
16#[derive(Clone, Debug)]
17pub struct ViewFilter {
18    pub include_values: bool,
19    pub include_formulas: bool,
20    /// Formatting diffs (always false until RFC-022 is implemented).
21    pub include_formatting: bool,
22    pub include_info_diagnostics: bool,
23    /// If `Some`, only include changes from the listed sheet indices (0-based).
24    pub sheets: Option<Vec<usize>>,
25}
26
27impl Default for ViewFilter {
28    fn default() -> Self {
29        Self {
30            include_values: true,
31            include_formulas: true,
32            include_formatting: false,
33            include_info_diagnostics: false,
34            sheets: None,
35        }
36    }
37}
38
39// ---------------------------------------------------------------------------
40// Stable change anchor (for virtualized tables / navigation)
41// ---------------------------------------------------------------------------
42
43/// A stable, deterministic identifier for a single change row.
44///
45/// Ordering matches the canonical `(sheet_index, row, col)` sort.
46#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
47pub struct ChangeAnchor {
48    pub sheet_index: usize,
49    pub row: u32,
50    pub col: u32,
51}
52
53// ---------------------------------------------------------------------------
54// Flat change row (one entry per visible cell change)
55// ---------------------------------------------------------------------------
56
57/// A single row in the flat change list presented to a GUI table.
58pub struct CellChangeRow<'a> {
59    /// Stable anchor for navigation and virtualized table positioning.
60    pub anchor: ChangeAnchor,
61    pub sheet_name: &'a str,
62    pub address: &'a CellAddress,
63    /// Combined change kind derived from sub-fields.
64    pub change_kind: CellChangeKind,
65    /// Display string for the old value (empty if Added).
66    pub old_display: String,
67    /// Display string for the new value (empty if Removed).
68    pub new_display: String,
69    /// Whether a formula also changed on this cell.
70    pub formula_changed: bool,
71    /// Highest diagnostic severity attached to this cell.
72    pub max_severity: Option<Severity>,
73}
74
75// ---------------------------------------------------------------------------
76// Sheet summary row
77// ---------------------------------------------------------------------------
78
79/// Summary line for one sheet in the sheet-tree view.
80pub struct SheetSummaryRow<'a> {
81    pub sheet_index: usize,
82    pub name: &'a str,
83    pub change: &'a SheetChange,
84    pub cells_changed: usize,
85    pub has_diagnostics: bool,
86}
87
88// ---------------------------------------------------------------------------
89// DiffView — main adapter
90// ---------------------------------------------------------------------------
91
92/// Borrowed view over a `WorkbookDiff`, providing filtered iteration and
93/// deterministic navigation for GUI applications.
94pub struct DiffView<'a> {
95    pub workbook: &'a WorkbookDiff,
96}
97
98impl<'a> DiffView<'a> {
99    pub fn new(workbook: &'a WorkbookDiff) -> Self {
100        Self { workbook }
101    }
102
103    // ------------------------------------------------------------------
104    // Sheet tree
105    // ------------------------------------------------------------------
106
107    /// Iterate sheet summary rows in workbook display order.
108    pub fn sheets(&self) -> impl Iterator<Item = SheetSummaryRow<'a>> {
109        self.workbook.sheets.iter().enumerate().map(|(i, sd)| {
110            SheetSummaryRow {
111                sheet_index: i,
112                name: sd.new_sheet.as_ref().or(sd.old_sheet.as_ref())
113                    .map(|s| s.name.as_str()).unwrap_or("?"),
114                change: &sd.change,
115                cells_changed: sd.summary.cells_changed,
116                has_diagnostics: !sd.diagnostics.is_empty(),
117            }
118        })
119    }
120
121    // ------------------------------------------------------------------
122    // Flat change list
123    // ------------------------------------------------------------------
124
125    /// Collect all visible cell-change rows into a `Vec`, respecting the filter.
126    ///
127    /// Order is deterministic: sheet order → (row, col).
128    pub fn rows(&'a self, filter: &ViewFilter) -> Vec<CellChangeRow<'a>> {
129        let mut out = Vec::new();
130        for (sheet_idx, sd) in self.workbook.sheets.iter().enumerate() {
131            if let Some(ref allowed) = filter.sheets {
132                if !allowed.contains(&sheet_idx) { continue; }
133            }
134            let sheet_name = sd.new_sheet.as_ref().or(sd.old_sheet.as_ref())
135                .map(|s| s.name.as_str()).unwrap_or("?");
136            for cd in &sd.cell_diffs {
137                if let Some(row) = cell_to_row(cd, sheet_idx, sheet_name, filter) {
138                    out.push(row);
139                }
140            }
141        }
142        out
143    }
144
145    /// Total number of visible change rows (may iterate; not O(1)).
146    pub fn row_count(&self, filter: &ViewFilter) -> usize {
147        self.rows(filter).len()
148    }
149
150    // ------------------------------------------------------------------
151    // Navigation
152    // ------------------------------------------------------------------
153
154    /// Return the first change anchor in the view, or `None` if empty.
155    pub fn first(&self, filter: &ViewFilter) -> Option<ChangeAnchor> {
156        self.rows(filter).into_iter().next().map(|r| r.anchor)
157    }
158
159    /// Return the anchor immediately after `current`, or `None` if at end.
160    pub fn next_after(&self, current: &ChangeAnchor, filter: &ViewFilter) -> Option<ChangeAnchor> {
161        let mut past = false;
162        for row in self.rows(filter).into_iter() {
163            if past {
164                return Some(row.anchor);
165            }
166            if &row.anchor == current {
167                past = true;
168            }
169        }
170        None
171    }
172
173    /// Return the anchor immediately before `current`, or `None` if at start.
174    pub fn previous_before(&self, current: &ChangeAnchor, filter: &ViewFilter) -> Option<ChangeAnchor> {
175        let mut prev: Option<ChangeAnchor> = None;
176        for row in self.rows(filter).into_iter() {
177            if &row.anchor == current {
178                return prev;
179            }
180            prev = Some(row.anchor.clone());
181        }
182        None
183    }
184
185    // ------------------------------------------------------------------
186    // Per-sheet slice
187    // ------------------------------------------------------------------
188
189    /// All cell-change rows for one sheet (by 0-based sheet index).
190    pub fn sheet_rows(
191        &'a self,
192        sheet_index: usize,
193        filter: &ViewFilter,
194    ) -> Vec<CellChangeRow<'a>> {
195        let mut f = filter.clone();
196        f.sheets = Some(vec![sheet_index]);
197        self.rows(&f)
198    }
199}
200
201// ---------------------------------------------------------------------------
202// Helper: CellDiff → CellChangeRow
203// ---------------------------------------------------------------------------
204
205fn cell_to_row<'a>(
206    cd: &'a CellDiff,
207    sheet_index: usize,
208    sheet_name: &'a str,
209    filter: &ViewFilter,
210) -> Option<CellChangeRow<'a>> {
211    let has_value = cd.value.is_some() && filter.include_values;
212    let has_formula = cd.formula.is_some() && filter.include_formulas;
213
214    if !has_value && !has_formula {
215        return None;
216    }
217
218    let old_display = cd.value.as_ref()
219        .map(|vc| vc.old.display_string())
220        .unwrap_or_default();
221    let new_display = cd.value.as_ref()
222        .map(|vc| vc.new.display_string())
223        .unwrap_or_default();
224
225    let max_severity = cd.diagnostics.iter()
226        .map(|d| d.severity)
227        .max();
228
229    Some(CellChangeRow {
230        anchor: ChangeAnchor {
231            sheet_index,
232            row: cd.address.row,
233            col: cd.address.col,
234        },
235        sheet_name,
236        address: &cd.address,
237        change_kind: cd.change_kind(),
238        old_display,
239        new_display,
240        formula_changed: cd.formula.is_some(),
241        max_severity,
242    })
243}