Skip to main content

ravenrs_sheet/engine/
paper.rs

1//! Page Layout view geometry: a sheet shown as printed pages side by side.
2//!
3//! The view uses the print pagination ([`crate::engine::cmd::print::split_bands`]) over the sheet from
4//! A1, so its page edges are the printed ones whenever the print area starts at A1. Pages keep
5//! going right and down past the data ("Click to add data" pages), as far as the view needs.
6//!
7//! Coordinates are *layout units*: sheet pixels (96 dpi) at the print scale, so a cell keeps its
8//! sheet size and a page is `paper / scale` big. The UI multiplies by its zoom. Pages sit in a
9//! grid with a gap around each: page `(row band, column band)` starts at
10//! `(gap + j·(page_w + gap), gap + i·(page_h + gap))`, and its cells start at its top-left margin.
11
12use crate::base::{CellRef, MAX_COLS, MAX_ROWS, RangeRef};
13use crate::model::{LineIndex, PageOrder, Sheet, Workbook};
14
15use crate::engine::cmd::print::{self, PX_TO_PT, PrintOptions};
16
17/// Gap between pages on screen, in pixels at 100% zoom.
18pub const PAGE_GAP: f32 = 14.0;
19
20/// One sheet paginated for the Page Layout view.
21#[derive(Clone, Debug)]
22pub struct PageGrid {
23    /// Page size in layout units.
24    pub page_w: f64,
25    pub page_h: f64,
26    /// Left, right, top, bottom, header and footer margins in layout units.
27    pub margins: [f64; 6],
28    /// Gap between pages in layout units.
29    pub gap: f64,
30    /// Print scale (1.0 = 100%): screen sizes of cells are `zoom × scale`.
31    pub scale: f32,
32    /// Left and right margins of headers and footers in layout units (the side margins, or a
33    /// fixed 0.7 inch, at most a quarter of the page width, with Align with Page Margins off).
34    pub hf_sides: [f64; 2],
35    /// Layout units per sheet pixel of header and footer text: 1 with Scale with Document on,
36    /// otherwise `1 / scale`, so the text keeps its point size on paper.
37    pub hf_text_k: f32,
38    /// Paper size in points (after orientation), for the rulers.
39    pub paper_pt: (f32, f32),
40    pub row_bands: Vec<(u32, u32)>,
41    pub col_bands: Vec<(u32, u32)>,
42    /// The bands reach the sheet's last row / column.
43    pub rows_complete: bool,
44    pub cols_complete: bool,
45    /// Bands holding printed data: `[first, last]` row bands and column bands.
46    pub data_rows: (usize, usize),
47    pub data_cols: (usize, usize),
48    /// The sheet's page order, which numbers the printed pages.
49    pub page_order: PageOrder,
50    ri: LineIndex,
51    ci: LineIndex,
52}
53
54/// A header or footer zone of one page.
55#[derive(Clone, Copy, Debug, PartialEq)]
56pub struct HfZone {
57    pub footer: bool,
58    /// 0 left, 1 centre, 2 right.
59    pub section: usize,
60}
61
62impl PageGrid {
63    /// Paginates sheet `si` far enough to cover row `need_row` and column `need_col` (and at
64    /// least the printed area). `None` when the sheet doesn't exist.
65    pub fn build(wb: &Workbook, si: usize, need_row: u32, need_col: u32) -> Option<PageGrid> {
66        let sh = wb.sheet(si)?;
67        let printed = print::layout_sheet(wb, si, &PrintOptions::default(), None)?;
68        let mut lay = printed.clone();
69        let end_row = need_row.max(printed.area.end.row).min(MAX_ROWS - 1);
70        let end_col = need_col.max(printed.area.end.col).min(MAX_COLS - 1);
71        lay.area = RangeRef::new(CellRef::new(0, 0), CellRef::new(end_row, end_col));
72        let (row_bands, col_bands) = print::split_bands(sh, &lay, MAX_ROWS as usize);
73        let s = f64::from(lay.scale.clamp(0.1, 4.0));
74        let to_layout = |pt: f32| f64::from(pt) / f64::from(PX_TO_PT) / s;
75        let margins = lay.margins.map(to_layout);
76        let (hf_left, hf_right) = lay.hf_sides();
77        let hf_text_k = lay.hf_text_scale() / s as f32;
78        let band_range = |bands: &[(u32, u32)], lo: u32, hi: u32| {
79            let first = bands.iter().position(|b| b.1 >= lo).unwrap_or(0);
80            let last = bands.iter().rposition(|b| b.0 <= hi).unwrap_or(first).max(first);
81            (first, last)
82        };
83        let area = printed.area;
84        let data_rows = band_range(&row_bands, area.start.row, area.end.row);
85        let data_cols = band_range(&col_bands, area.start.col, area.end.col);
86        Some(PageGrid {
87            page_w: to_layout(lay.page_w),
88            page_h: to_layout(lay.page_h),
89            margins,
90            gap: f64::from(PAGE_GAP) / s,
91            scale: lay.scale,
92            hf_sides: [to_layout(hf_left), to_layout(hf_right)],
93            hf_text_k,
94            paper_pt: (lay.page_w, lay.page_h),
95            rows_complete: row_bands.last().is_some_and(|b| b.1 >= MAX_ROWS - 1),
96            cols_complete: col_bands.last().is_some_and(|b| b.1 >= MAX_COLS - 1),
97            row_bands,
98            col_bands,
99            data_rows,
100            data_cols,
101            page_order: printed.settings.page_order,
102            ri: LineIndex::new(&sh.rows, sh.default_row_height, MAX_ROWS),
103            ci: LineIndex::new(&sh.cols, sh.default_col_width, MAX_COLS),
104        })
105    }
106
107    /// The grid covers row `r` and column `c` (else it needs a rebuild with more pages).
108    pub fn covers(&self, r: u32, c: u32) -> bool {
109        (self.rows_complete || self.row_bands.last().is_some_and(|b| b.1 >= r))
110            && (self.cols_complete || self.col_bands.last().is_some_and(|b| b.1 >= c))
111    }
112
113    fn band_of(bands: &[(u32, u32)], line: u32) -> usize {
114        bands.partition_point(|b| b.1 < line).min(bands.len().saturating_sub(1))
115    }
116
117    /// Left edge of page column `j` / top edge of page row `i`.
118    pub fn page_x(&self, j: usize) -> f64 {
119        self.gap + j as f64 * (self.page_w + self.gap)
120    }
121    pub fn page_y(&self, i: usize) -> f64 {
122        self.gap + i as f64 * (self.page_h + self.gap)
123    }
124    /// The page rectangle `(x, y, w, h)` of page `(i, j)`.
125    pub fn page_rect(&self, i: usize, j: usize) -> (f64, f64, f64, f64) {
126        (self.page_x(j), self.page_y(i), self.page_w, self.page_h)
127    }
128    /// Where the cells of page `(i, j)` go: inside the margins.
129    pub fn content_rect(&self, i: usize, j: usize) -> (f64, f64, f64, f64) {
130        let [l, r, t, b, ..] = self.margins;
131        (self.page_x(j) + l, self.page_y(i) + t, (self.page_w - l - r).max(1.0), (self.page_h - t - b).max(1.0))
132    }
133
134    /// Left edge of column `col`.
135    pub fn col_x(&self, col: u32) -> f64 {
136        let j = Self::band_of(&self.col_bands, col);
137        let start = self.col_bands.get(j).map_or(0, |b| b.0);
138        self.page_x(j) + self.margins[0] + (self.ci.start(col) - self.ci.start(start))
139    }
140    /// Top edge of row `row`.
141    pub fn row_y(&self, row: u32) -> f64 {
142        let i = Self::band_of(&self.row_bands, row);
143        let start = self.row_bands.get(i).map_or(0, |b| b.0);
144        self.page_y(i) + self.margins[2] + (self.ri.start(row) - self.ri.start(start))
145    }
146
147    /// Page column under `x` (the nearest one in gaps).
148    pub fn page_col_at(&self, x: f64) -> usize {
149        let j = ((x - self.gap / 2.0) / (self.page_w + self.gap)).floor();
150        (j.max(0.0) as usize).min(self.col_bands.len().saturating_sub(1))
151    }
152    pub fn page_row_at(&self, y: f64) -> usize {
153        let i = ((y - self.gap / 2.0) / (self.page_h + self.gap)).floor();
154        (i.max(0.0) as usize).min(self.row_bands.len().saturating_sub(1))
155    }
156
157    /// Column under `x`, clamped to the page's columns (margins and gaps hit the nearest one).
158    pub fn col_at(&self, x: f64) -> u32 {
159        let j = self.page_col_at(x);
160        let Some(&(a, b)) = self.col_bands.get(j) else { return 0 };
161        let local = x - self.page_x(j) - self.margins[0];
162        if local <= 0.0 { a } else { self.ci.at(self.ci.start(a) + local).clamp(a, b) }
163    }
164    pub fn row_at(&self, y: f64) -> u32 {
165        let i = self.page_row_at(y);
166        let Some(&(a, b)) = self.row_bands.get(i) else { return 0 };
167        let local = y - self.page_y(i) - self.margins[2];
168        if local <= 0.0 { a } else { self.ri.at(self.ri.start(a) + local).clamp(a, b) }
169    }
170
171    /// The page `(i, j)` whose paper contains the point, if any.
172    pub fn page_at(&self, x: f64, y: f64) -> Option<(usize, usize)> {
173        let (i, j) = (self.page_row_at(y), self.page_col_at(x));
174        let (px, py, w, h) = self.page_rect(i, j);
175        (x >= px && x < px + w && y >= py && y < py + h).then_some((i, j))
176    }
177
178    /// The point lies on cells (inside a page's margins, on a cell of that page).
179    pub fn on_cells(&self, x: f64, y: f64) -> bool {
180        let Some((i, j)) = self.page_at(x, y) else { return false };
181        let (cx, cy, w, h) = self.content_rect(i, j);
182        let (Some(&(_, c1)), Some(&(_, r1))) = (self.col_bands.get(j), self.row_bands.get(i)) else { return false };
183        // Cells end where the band ends (the rest of the printable area stays blank).
184        let right = (self.col_x(c1) + self.ci.start(c1 + 1) - self.ci.start(c1)).min(cx + w);
185        let bottom = (self.row_y(r1) + self.ri.start(r1 + 1) - self.ri.start(r1)).min(cy + h);
186        x >= cx && x < right && y >= cy && y < bottom
187    }
188
189    /// The header or footer section under the point: the top or bottom margin of a page, split
190    /// into thirds between the header and footer side margins.
191    pub fn hf_zone_at(&self, x: f64, y: f64) -> Option<((usize, usize), HfZone)> {
192        let (i, j) = self.page_at(x, y)?;
193        let (px, py, _, _) = self.page_rect(i, j);
194        let [_, _, t, b, ..] = self.margins;
195        let [l, r] = self.hf_sides;
196        let footer = if y < py + t {
197            false
198        } else if y >= py + self.page_h - b {
199            true
200        } else {
201            return None;
202        };
203        let inner = (self.page_w - l - r).max(3.0);
204        let section = (((x - px - l) / (inner / 3.0)).floor().max(0.0) as usize).min(2);
205        Some(((i, j), HfZone { footer, section }))
206    }
207
208    /// Page `(i, j)` holds printed data (others are "Click to add data" pages).
209    pub fn is_data_page(&self, i: usize, j: usize) -> bool {
210        (self.data_rows.0..=self.data_rows.1).contains(&i) && (self.data_cols.0..=self.data_cols.1).contains(&j)
211    }
212    /// Printed page number of page `(i, j)` (in the sheet's page order) and the printed page count.
213    pub fn page_number(&self, i: usize, j: usize) -> Option<(usize, usize)> {
214        if !self.is_data_page(i, j) {
215            return None;
216        }
217        let rows = self.data_rows.1 - self.data_rows.0 + 1;
218        let cols = self.data_cols.1 - self.data_cols.0 + 1;
219        Some((self.page_order.index(i - self.data_rows.0, j - self.data_cols.0, rows, cols) + 1, rows * cols))
220    }
221
222    /// Page rows and columns overlapping the layout rectangle `[x0, x1] × [y0, y1]`.
223    pub fn pages_in(&self, x0: f64, y0: f64, x1: f64, y1: f64) -> ((usize, usize), (usize, usize)) {
224        ((self.page_row_at(y0), self.page_row_at(y1)), (self.page_col_at(x0), self.page_col_at(x1)))
225    }
226
227    /// Width / height of everything laid out so far.
228    pub fn extent(&self) -> (f64, f64) {
229        (self.page_x(self.col_bands.len()), self.page_y(self.row_bands.len()))
230    }
231
232    /// Rows `(first, last)` of a page row and columns of a page column.
233    pub fn row_band(&self, i: usize) -> Option<(u32, u32)> {
234        self.row_bands.get(i).copied()
235    }
236    pub fn col_band(&self, j: usize) -> Option<(u32, u32)> {
237        self.col_bands.get(j).copied()
238    }
239}
240
241/// Sheet-wide helper: the bottom-right cell a page grid for this sheet must reach so that the
242/// layout rectangle up to `(x, y)` is covered. Layout coordinates never run ahead of sheet
243/// coordinates (pages only add margins and gaps), so the sheet position is a safe bound.
244pub fn need_for(sh: &Sheet, x: f64, y: f64) -> (u32, u32) {
245    (sh.row_at(y.max(0.0)).saturating_add(1).min(MAX_ROWS - 1), sh.col_at(x.max(0.0)).saturating_add(1).min(MAX_COLS - 1))
246}
247
248#[cfg(test)]
249mod tests {
250    use crate::model::{Orientation, Sheet};
251
252    use super::*;
253
254    fn wb_with(rows: u32, cols: u32) -> Workbook {
255        let mut wb = Workbook::new();
256        if let Some(sh) = wb.sheet_mut(0) {
257            for r in 0..rows {
258                for c in 0..cols {
259                    sh.set_value(CellRef::new(r, c), crate::base::Value::Number(1.0));
260                }
261            }
262        }
263        wb
264    }
265
266    #[test]
267    fn pages_match_print_pagination_and_cells_map_both_ways() {
268        let wb = wb_with(120, 14);
269        let g = PageGrid::build(&wb, 0, 400, 40).unwrap();
270        let printed = print::layout_sheet(&wb, 0, &PrintOptions::default(), None).unwrap();
271        // The printed area starts at A1, so the view's page edges are the printed ones (the
272        // last printed page simply ends with the data).
273        let starts = |b: &[(u32, u32)], n: usize| b.iter().take(n).map(|x| x.0).collect::<Vec<_>>();
274        assert!(printed.row_bands.len() > 1 && printed.col_bands.len() > 1);
275        assert_eq!(starts(&g.row_bands, printed.row_bands.len()), starts(&printed.row_bands, usize::MAX));
276        assert_eq!(starts(&g.col_bands, printed.col_bands.len()), starts(&printed.col_bands, usize::MAX));
277        assert_eq!(g.data_rows, (0, printed.row_bands.len() - 1));
278        assert!(g.covers(400, 40));
279        // Every cell maps to a point inside its own page's margins and back.
280        for (r, c) in [(0, 0), (5, 3), (60, 9), (119, 13), (300, 30)] {
281            let (x, y) = (g.col_x(c) + 1.0, g.row_y(r) + 1.0);
282            assert_eq!((g.row_at(y), g.col_at(x)), (r, c), "cell ({r},{c})");
283            assert!(g.on_cells(x, y));
284            let (i, j) = g.page_at(x, y).unwrap();
285            let (cx, cy, _, _) = g.content_rect(i, j);
286            assert!(x >= cx && y >= cy);
287        }
288        // The first row of the second page starts below the first page plus a gap.
289        let (a, _) = g.row_bands[1];
290        assert!((g.row_y(a) - (g.page_y(1) + g.margins[2])).abs() < 1e-6);
291        assert!(g.row_y(a) > g.page_h + g.gap);
292    }
293
294    #[test]
295    fn margins_gaps_and_header_zones() {
296        let mut wb = wb_with(10, 3);
297        if let Some(sh) = wb.sheet_mut(0) {
298            sh.print.orientation = Orientation::Landscape;
299        }
300        let g = PageGrid::build(&wb, 0, 50, 20).unwrap();
301        assert!(g.page_w > g.page_h, "landscape pages are wider than tall");
302        // A point in the top margin hits the header, thirds pick the section.
303        let (px, py, w, h) = g.page_rect(0, 0);
304        let mid_top = (px + w / 2.0, py + g.margins[2] / 2.0);
305        assert_eq!(g.hf_zone_at(mid_top.0, mid_top.1), Some(((0, 0), HfZone { footer: false, section: 1 })));
306        assert_eq!(g.hf_zone_at(px + g.margins[0] + 2.0, py + h - 2.0).map(|z| z.1), Some(HfZone { footer: true, section: 0 }));
307        assert_eq!(g.hf_zone_at(px + w - g.margins[1] - 2.0, py + 3.0).map(|z| z.1.section), Some(2));
308        assert!(!g.on_cells(mid_top.0, mid_top.1), "margins aren't cells");
309        // The gap between pages is no page at all.
310        assert_eq!(g.page_at(px + w + g.gap / 2.0, py + 10.0), None);
311        assert_eq!(g.hf_zone_at(px - 1.0, py + 3.0), None);
312        // Page numbers count printed pages only.
313        assert_eq!(g.page_number(0, 0), Some((1, 1)));
314        assert_eq!(g.page_number(0, 1), None);
315        assert!(g.is_data_page(0, 0) && !g.is_data_page(1, 0));
316    }
317
318    #[test]
319    fn page_numbers_follow_the_page_order() {
320        // Two pages tall and two wide, as printed; the pages beyond are "Click to add data" pages.
321        let mut wb = wb_with(60, 15);
322        let numbers = |wb: &Workbook| {
323            let g = PageGrid::build(wb, 0, 200, 60).unwrap();
324            assert_eq!((g.data_rows, g.data_cols), ((0, 1), (0, 1)));
325            assert_eq!((g.page_number(2, 0), g.page_number(0, 2)), (None, None));
326            [(0, 0), (1, 0), (0, 1), (1, 1)].map(|(i, j)| g.page_number(i, j).unwrap())
327        };
328        assert_eq!(numbers(&wb), [(1, 4), (2, 4), (3, 4), (4, 4)]);
329        wb.sheet_mut(0).unwrap().print.page_order = PageOrder::OverThenDown;
330        assert_eq!(numbers(&wb), [(1, 4), (3, 4), (2, 4), (4, 4)]);
331    }
332
333    #[test]
334    fn header_zones_and_text_follow_scale_with_document_and_align_with_page_margins() {
335        let mut wb = wb_with(10, 3);
336        if let Some(sh) = wb.sheet_mut(0) {
337            sh.print.scale = 50;
338            sh.print.margins = [2.0, 2.0, 0.75, 0.75, 0.3, 0.3];
339        }
340        let g = PageGrid::build(&wb, 0, 50, 20).unwrap();
341        let (px, py, _, _) = g.page_rect(0, 0);
342        // Both on: the sections sit between the 2-inch side margins and the text scales with the cells.
343        assert_eq!(g.hf_sides, [g.margins[0], g.margins[1]]);
344        assert!((g.hf_text_k - 1.0).abs() < 1e-6);
345        assert_eq!(g.hf_zone_at(px + 1000.0, py + 3.0).map(|z| z.1.section), Some(2));
346        if let Some(sh) = wb.sheet_mut(0) {
347            sh.print.hf_scale_with_doc = false;
348            sh.print.hf_align_with_margins = false;
349        }
350        let g = PageGrid::build(&wb, 0, 50, 20).unwrap();
351        // Both off: 0.7 inch (67.2 sheet pixels, 134.4 layout units at 50%) on each side, and the
352        // text keeps its point size on paper.
353        assert!(g.hf_sides.iter().all(|s| (s - 134.4).abs() < 1e-3), "{:?}", g.hf_sides);
354        assert!((g.hf_text_k - 2.0).abs() < 1e-6);
355        assert_eq!(g.hf_zone_at(px + 1000.0, py + 3.0).map(|z| z.1.section), Some(1));
356    }
357
358    #[test]
359    fn print_scale_and_hostile_settings_stay_bounded() {
360        let mut wb = wb_with(3, 3);
361        if let Some(sh) = wb.sheet_mut(0) {
362            sh.print.scale = 50;
363            sh.print.margins = [f32::NAN, 1e9, -4.0, f32::INFINITY, 0.0, 0.0];
364            sh.print.row_breaks = vec![0, 1, 2, u32::MAX];
365        }
366        let g = PageGrid::build(&wb, 0, MAX_ROWS, MAX_COLS).unwrap();
367        assert!(g.rows_complete && g.cols_complete);
368        assert!(g.page_w.is_finite() && g.page_h.is_finite() && g.page_w > 0.0);
369        let (ex, ey) = g.extent();
370        assert!(ex.is_finite() && ey.is_finite());
371        assert_eq!(g.row_at(ey * 2.0), MAX_ROWS - 1);
372        assert_eq!(g.col_at(-5.0), 0);
373        assert_eq!(need_for(&Sheet::new("S"), f64::MAX, f64::MAX), (MAX_ROWS - 1, MAX_COLS - 1));
374        assert!(PageGrid::build(&wb, 7, 0, 0).is_none());
375    }
376}