work on new approach to rendering
authorBen Pfaff <blp@cs.stanford.edu>
Mon, 8 Dec 2025 01:13:47 +0000 (17:13 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Mon, 8 Dec 2025 01:14:05 +0000 (17:14 -0800)
rust/pspp/src/output/drivers/cairo/fsm.rs
rust/pspp/src/output/drivers/text.rs
rust/pspp/src/output/render.rs
rust/pspp/src/output/table.rs

index cb6651b0a65874e56708640d2ce850e5ea7cfe4e..60b006dcf762c0240730757460835321751f586e 100644 (file)
@@ -178,23 +178,20 @@ impl CairoFsm {
             context,
         };
         let mut used = pager.draw_next(&mut device, space);
-        if !pager.has_next(&device) {
-            match self.layer_iterator.as_mut().unwrap().next() {
-                Some(layer_indexes) => {
-                    self.pager = Some(Pager::new(
-                        &device,
-                        pivot_table,
-                        Some(layer_indexes.as_slice()),
-                    ));
-                    if pivot_table.style.look.paginate_layers {
-                        used = space;
-                    } else {
-                        used += self.style.object_spacing;
-                    }
-                }
-                _ => {
-                    self.pager = None;
+        if pager.has_next(&device).is_none() {
+            if let Some(layer_indexes) = self.layer_iterator.as_mut().unwrap().next() {
+                self.pager = Some(Pager::new(
+                    &device,
+                    pivot_table,
+                    Some(layer_indexes.as_slice()),
+                ));
+                if pivot_table.style.look.paginate_layers {
+                    used = space;
+                } else {
+                    used += self.style.object_spacing;
                 }
+            } else {
+                self.pager = None;
             }
         }
         used.min(space)
index d930a0cc39916ba0bea0e50add4aaaaad65190b9..cc6cad04326e95508874607a5fe4d3ca2a6bf2d3 100644 (file)
@@ -415,7 +415,7 @@ impl TextRenderer {
             }
 
             let mut pager = Pager::new(self, table, Some(layer_indexes.as_slice()));
-            while pager.has_next(self) {
+            while pager.has_next(self).is_some() {
                 pager.draw_next(self, isize::MAX);
                 for line in self.lines.drain(..) {
                     writeln!(writer, "{line}")?;
index bca95562767cefbd09a9bd39b67165b4eb28eb72..1714bc5cdb6f70a90d5dbdf2c884d2268e9d0708 100644 (file)
 // this program.  If not, see <http://www.gnu.org/licenses/>.
 
 use std::cmp::{max, min};
-use std::collections::HashMap;
 use std::iter::{once, zip};
 use std::ops::Range;
 use std::sync::Arc;
 
 use enum_map::{Enum, EnumMap, enum_map};
-use itertools::interleave;
+use itertools::{Itertools, interleave};
 use num::Integer;
 use smallvec::SmallVec;
 
@@ -166,36 +165,9 @@ pub trait Device {
     fn scale(&mut self, factor: f64);
 }
 
-/// A layout for rendering a specific table on a specific device.
-///
-/// May represent the layout of an entire table presented to [Pager::new], or a
-/// rectangular subregion of a table broken out using [Break::next] to allow a
-/// table to be broken across multiple pages.
-///
-/// A page's size is not limited to the size passed in as part of [Params].
-/// [Pager] breaks a [Page] into smaller [page]s that will fit in the available
-/// space.
-///
-/// A [Page] always has the same headers as its [Table].
-///
-/// # Rendered cells
-///
-/// - The columns rendered are the leftmost `self.table.h[X]`, then `r[X]`.
-/// - The rows rendered are the topmost `self.table.h[Y]`, then `r[Y]`.
 #[derive(Debug)]
-struct Page {
-    table: Arc<Table>,
-
-    /// Table size in cells.
-    ///
-    /// This is the sum of `self.table.h` and `self.r`.
-    n: CellPos,
-
-    /// The region of cells in `self.table` to render.
-    r: CellRect,
-
-    /// Mappings from [Page] positions to those in the underlying [Table].
-    maps: EnumMap<Axis2, [Map; 2]>,
+struct RenderedTable {
+    table: Table,
 
     /// "Cell positions".
     ///
@@ -209,6 +181,12 @@ struct Page {
     /// - `cp[X][2 * n[X]]` = `x` position of the rightmost vertical rule.
     /// - `cp[X][2 * n[X] + 1]` = total table width including all rules.
     ///
+    /// So, for 0-based column `i`:
+    ///
+    /// * The rule to its left covers `cp[X][i * 2]..cp[X][i * 2 + 1]`.
+    /// * The column covers `cp[X][i * 2 + 1]..cp[X][i * 2 + 2]`.
+    /// * The rule to its right covers `cp[X][i * 2 + 2]..cp[X][i * 2 + 3]`.
+    ///
     /// Similarly, `cp[Y]` represents `y` positions within the table:
     ///
     /// - `cp[Y][0]` = 0.
@@ -219,152 +197,25 @@ struct Page {
     /// - `cp[Y][2 * n[Y]]` = `y` position of the bottommost horizontal rule.
     /// - `cp[Y][2 * n[Y] + 1]` = total table height including all rules.
     ///
+    /// So, for 0-based row `i`:
+    ///
+    /// * The rule above it covers `cp[Y][i * 2]..cp[Y][i * 2 + 1]`.
+    /// * The row covers `cp[Y][i * 2 + 1]..cp[Y][i * 2 + 2]`.
+    /// * The rule below it covers `cp[Y][i * 2 + 2]..cp[Y][i * 2 + 3]`.
+    ///
     /// Rules and columns can have width or height 0, in which case consecutive
     /// values in this array are equal.
     cp: EnumMap<Axis2, Vec<isize>>,
-
-    /// [Break::next] can break a [Page] in the middle of a cell, if a cell is
-    /// too wide or two tall to fit on a single page, or if a cell spans
-    /// multiple rows or columns and the page only includes some of those rows
-    /// or columns.
-    ///
-    /// This hash table represents each such cell that doesn't completely fit on
-    /// this page.
-    ///
-    /// Each overflow cell borders at least one header edge of the table and may
-    /// border more.  (A single table cell that is so large that it fills the
-    /// entire page can overflow on all four sides!)
-    ///
-    /// # Interpretation
-    ///
-    /// Given `overflow` as a value in the [HashMap]:
-    ///
-    /// - `overflow[Axis2::X][0]`: space trimmed off the cell's left side.
-    /// - `overflow[Axis2::X][1]`: space trimmed off the cell's right side.
-    /// - `overflow[Axis2::Y][0]`: space trimmed off the cell's top.
-    /// - `overflow[Axis2::Y][1]`: space trimmed off the cell's bottom.
-    ///
-    /// During rendering, this information is used to position the rendered
-    /// portion of the cell within the available space.
-    ///
-    /// When a cell is rendered, sometimes it is permitted to spill over into
-    /// space that is ordinarily reserved for rules.  Either way, this space is
-    /// still included in overflow values.
-    ///
-    /// Suppose, for example, that a cell that joins 2 columns has a width of 60
-    /// pixels and content `abcdef`, that the 2 columns that it joins have
-    /// widths of 20 and 30 pixels, respectively, and that therefore the rule
-    /// between the two joined columns has a width of 10 (20 + 10 + 30 = 60).
-    /// It might render like this, if each character is 10x10, and showing a few
-    /// extra table cells for context:
-    ///
-    /// ```text
-    /// ┌──────┐
-    /// │abcdef│
-    /// ├──┬───┤
-    /// │gh│ijk│
-    /// └──┴───┘
-    /// ```
-    ///
-    /// If this [Page] is broken at the rule that separates `gh` from
-    /// `ijk`, then the page that contains the left side of the `abcdef` cell
-    /// will have `overflow[Axis2::X][1]` of 10 + 30 = 40 for its portion of the cell,
-    /// and the page that contains the right side of the cell will have
-    /// `overflow[Axis2::X][0]` of 20 + 10 = 30.  The two resulting pages would look like
-    /// this:
-    ///
-    /// ```text
-    /// ┌───
-    /// │abc
-    /// ├──┬
-    /// │gh│
-    /// └──┴
-    /// ```
-    ///
-    /// and:
-    ///
-    /// ```text
-    /// ────┐
-    /// cdef│
-    /// ┬───┤
-    /// │ijk│
-    /// ┴───┘
-    /// ```
-    /// Each entry maps from a cell that overflows to the space that has been
-    /// trimmed off the cell.
-    overflows: HashMap<CellPos, EnumMap<Axis2, [isize; 2]>>,
-
-    /// If a single column (or row) is too wide (or tall) to fit on a page
-    /// reasonably, then [Break::next] will split a single row or column across
-    /// multiple [Page]s.  This member indicates when this has happened:
-    ///
-    /// - `is_edge_cutoff[Axis2::X][0]` is true if pixels have been cut off the
-    ///   left side of the leftmost column in this page, and false otherwise.
-    ///
-    /// - `is_edge_cutoff[Axis2::X][1]` is true if pixels have been cut off the
-    ///   right side of the rightmost column in this page, and false otherwise.
-    ///
-    /// - `is_edge_cutoff[Axis2::Y][0]` and `is_edge_cutoff[Axis2::Y][1]` are
-    ///   similar for the top and bottom of the table.
-    ///
-    /// The effect is to prevent rules along the edge in question from being
-    /// rendered.
-    ///
-    /// When `is_edge_cutoff` is true for a given edge, 'overflows' will contain
-    /// a node for each cell along that edge.
-    is_edge_cutoff: EnumMap<Axis2, [bool; 2]>,
-}
-
-/// Returns the width of `extent` along `axis`.
-fn axis_width(cp: &[isize], extent: Range<usize>) -> isize {
-    cp[extent.end] - cp[extent.start]
 }
 
-/// Returns the width of cells within `extent` along `axis`.
-fn joined_width(cp: &[isize], extent: Range<usize>) -> isize {
-    axis_width(cp, cell_ofs(extent.start)..cell_ofs(extent.end) - 1)
-}
-/// Returns the offset in [Self::cp] of the cell with index `cell_index`.
-/// That is, if `cell_index` is 0, then the offset is 1, that of the leftmost
-/// or topmost cell; if `cell_index` is 1, then the offset is 3, that of the
-/// next cell to the right (or below); and so on. */
-fn cell_ofs(cell_index: usize) -> usize {
-    cell_index * 2 + 1
-}
-
-/// Returns the offset in [Self::cp] of the rule with index `rule_index`.
-/// That is, if `rule_index` is 0, then the offset is that of the leftmost
-/// or topmost rule; if `rule_index` is 1, then the offset is that of the
-/// next rule to the right (or below); and so on.
-fn rule_ofs(rule_index: usize) -> usize {
-    rule_index * 2
-}
-
-/// Returns the width of cell `z` along `axis`.
-fn cell_width(cp: &[isize], z: usize) -> isize {
-    let ofs = cell_ofs(z);
-    axis_width(cp, ofs..ofs + 1)
-}
-
-/// Is `ofs` the offset of a rule in `cp`?
-fn is_rule(z: usize) -> bool {
-    z.is_even()
-}
-
-#[derive(Clone)]
-pub struct RenderCell<'a> {
-    rect: CellRect,
-    content: &'a Content,
-}
-
-impl Page {
-    /// Creates and returns a new [Page] for rendering `table` with the given
+impl RenderedTable {
+    /// Creates and returns a new [RenderedTable] for rendering `table` with the given
     /// `look` on `device`.
     ///
     /// The new [Page] will be suitable for rendering on a device whose page
     /// size is `params.size`, but the caller is responsible for actually
     /// breaking it up to fit on such a device, using the [Break] abstraction.
-    fn new(table: Arc<Table>, device: &dyn Device, min_width: isize, look: &Look) -> Self {
+    fn new(table: Table, device: &dyn Device, min_width: isize, look: &Look) -> Self {
         use Axis2::*;
         use Extreme::*;
 
@@ -518,16 +369,9 @@ impl Page {
                 h[axis] = 0;
             }
         }
-        let r = CellRect::new(h[X]..n[X], h[Y]..n[Y]);
-        let maps = Self::new_mappings(h, &r);
         Self {
             table,
-            n,
-            r,
             cp: Axis2::new_enum(cp_x, cp_y),
-            overflows: HashMap::new(),
-            is_edge_cutoff: EnumMap::default(),
-            maps,
         }
     }
 
@@ -536,6 +380,10 @@ impl Page {
         self.table.h
     }
 
+    fn n(&self) -> CellPos {
+        self.table.n
+    }
+
     fn use_row_widths(rows: &[isize], rules: &[isize]) -> Vec<isize> {
         let mut vec = once(0)
             .chain(interleave(rules, rows).copied())
@@ -579,6 +427,10 @@ impl Page {
     }
 
     /// Returns the width of the headers along `axis`.
+    ///
+    /// The headers do not include the rule along the right or bottom edge of
+    /// the headers; that rule is considered to be part of the top or left body
+    /// cell.
     fn headers_width(&self, axis: Axis2) -> isize {
         self.axis_width(axis, rule_ofs(0)..cell_ofs(self.h()[axis]))
     }
@@ -602,7 +454,7 @@ impl Page {
     /// rule; if `rule_index_r` is 1, then the offset is that of the next rule to the left
     /// (or above); and so on.
     fn rule_ofs_r(&self, axis: Axis2, rule_index_r: usize) -> usize {
-        (self.n[axis] - rule_index_r) * 2
+        (self.table.n[axis] - rule_index_r) * 2
     }
 
     /// Returns the width of cell `z` along `axis`.
@@ -613,253 +465,136 @@ impl Page {
 
     /// Returns the width of the widest cell, excluding headers, along `axis`.
     fn max_cell_width(&self, axis: Axis2) -> isize {
-        (self.h()[axis]..self.n[axis])
+        (self.h()[axis]..self.n()[axis])
             .map(|z| self.cell_width(axis, z))
             .max()
             .unwrap_or(0)
     }
+}
 
-    fn width(&self, axis: Axis2) -> isize {
-        *self.cp[axis].last().unwrap()
-    }
-
-    fn new_mappings(h: CellPos, r: &CellRect) -> EnumMap<Axis2, [Map; 2]> {
-        EnumMap::from_fn(|axis| {
-            [
-                Map {
-                    p0: 0,
-                    t0: 0,
-                    ofs: 0,
-                    n: h[axis],
-                },
-                Map {
-                    p0: h[axis],
-                    t0: r[axis].start,
-                    ofs: r[axis].start - h[axis],
-                    n: r[axis].len(),
-                },
-            ]
-        })
-    }
-
-    fn get_map(&self, axis: Axis2, z: usize) -> &Map {
-        if z < self.h()[axis] {
-            &self.maps[axis][0]
-        } else {
-            &self.maps[axis][1]
-        }
-    }
-
-    fn map_z(&self, axis: Axis2, z: usize) -> usize {
-        z + self.get_map(axis, z).ofs
-    }
-
-    fn map_coord(&self, coord: CellPos) -> CellPos {
-        CellPos::from_fn(|a| self.map_z(a, coord[a]))
-    }
-
-    fn get_cell(&self, coord: CellPos) -> RenderCell<'_> {
-        let maps = EnumMap::from_fn(|axis| self.get_map(axis, coord[axis]));
-        let cell = self.table.get(self.map_coord(coord));
-        RenderCell {
-            rect: cell.rect().map(|axis, Range { start, end }| {
-                let m = maps[axis];
-                max(m.p0, start - m.ofs)..min(m.p0 + m.n, end - m.ofs)
-            }),
-            content: cell.content,
-        }
-    }
+/// A layout for rendering a specific table on a specific device.
+///
+/// May represent the layout of an entire table presented to [Pager::new], or a
+/// rectangular subregion of a table broken out using [Break::next] to allow a
+/// table to be broken across multiple pages.
+///
+/// A page's size is not limited to the size passed in as part of [Params].
+/// [Pager] breaks a [Page] into smaller [page]s that will fit in the available
+/// space.
+///
+/// A [Page] always has the same headers as its [Table].
+///
+/// # Rendered cells
+///
+/// - The columns rendered are the leftmost `self.table.h[X]`, then `r[X]`.
+/// - The rows rendered are the topmost `self.table.h[Y]`, then `r[Y]`.
+#[derive(Clone, Debug)]
+struct Page {
+    /// Rendered table.
+    table: Arc<RenderedTable>,
+    ranges: EnumMap<Axis2, Range<isize>>,
+}
 
-    /// Creates and returns a new [Page] whose contents are a subregion of this
-    /// page's contents.  The new page includes cells `extent` (exclusive) along
-    /// axis `a`, plus any headers on `a`.
-    ///
-    /// If `pixel0` is nonzero, then it is a number of pixels to exclude from
-    /// the left or top (according to `a`) of cell `extent.start`.  Similarly,
-    /// `pixel1` is a number of pixels to exclude from the right or bottom of
-    /// cell `extent.end - 1`.  (`pixel0` and `pixel1` are used to render cells
-    /// that are too large to fit on a single page.)
+impl Page {
+    /// Creates and returns a new [RenderedTable] for rendering `table` with the given
+    /// `look` on `device`.
     ///
-    /// The whole of axis `!a` is included.  (The caller may follow up with
-    /// another call to select on `!a`.)
-    fn select(
-        self: &Arc<Self>,
-        a: Axis2,
-        extent: Range<usize>,
-        pixel0: isize,
-        pixel1: isize,
-    ) -> Arc<Self> {
-        let b = !a;
-        let z0 = extent.start;
-        let z1 = extent.end;
-        let h = self.h();
-
-        // If all of the page is selected, just make a copy.
-        if z0 == h[a] && z1 == self.n[a] && pixel0 == 0 && pixel1 == 0 {
-            return self.clone();
-        }
-
-        // Figure out `n`, `h`, `r` for the subpage.
-        let trim = [z0 - self.h()[a], self.n[a] - z1];
-        let mut n = self.n;
-        n[a] -= trim[0] + trim[1];
-        let mut r = self.r.clone();
-        r[a].start += trim[0];
-        r[a].end -= trim[1];
-
-        // An edge is cut off if it was cut off in `self` or if we're trimming
-        // pixels off that side of the page and there are no headers.
-        let mut is_edge_cutoff = self.is_edge_cutoff;
-        is_edge_cutoff[a][0] = h[a] == 0 && (pixel0 > 0 || (z0 == 0 && self.is_edge_cutoff[a][0]));
-        is_edge_cutoff[a][1] = pixel1 > 0 || (z1 == self.n[a] && self.is_edge_cutoff[a][1]);
-
-        // Select widths from `self` into subpage.
-        let scp = self.cp[a].as_slice();
-        let mut dcp = Vec::with_capacity(2 * n[a] + 1);
-        dcp.push(0);
-        let mut total = 0;
-        for z in 0..=rule_ofs(h[a]) {
-            total += if z == 0 && is_edge_cutoff[a][0] {
-                0
-            } else {
-                scp[z + 1] - scp[z]
-            };
-            dcp.push(total);
-        }
-        for z in cell_ofs(z0)..=cell_ofs(z1 - 1) {
-            total += scp[z + 1] - scp[z];
-            if z == cell_ofs(z0) {
-                total -= pixel0;
-            }
-            if z == cell_ofs(z1 - 1) {
-                total -= pixel1;
-            }
-            dcp.push(total);
-        }
-        let z = self.rule_ofs_r(a, 0);
-        if !is_edge_cutoff[a][1] {
-            total += scp[z + 1] - scp[z];
-        }
-        dcp.push(total);
-        debug_assert_eq!(dcp.len(), 1 + 2 * n[a] + 1);
-
-        let mut cp = EnumMap::default();
-        cp[a] = dcp;
-        cp[!a] = self.cp[!a].clone();
-
-        let mut overflows = HashMap::new();
-
-        // Add new overflows.
-        let s = Selection {
-            a,
-            b,
-            h,
-            z0,
-            z1,
-            p0: pixel0,
-            p1: pixel1,
-        };
-        // Add overflows along the left side...
-        if h[a] == 0 || z0 > h[a] || pixel0 > 0 {
-            let mut z = 0;
-            while z < self.n[b] {
-                let d = CellPos::for_axis((a, z0), z);
-                let cell = self.get_cell(d);
-                let overflow0 = pixel0 > 0 || cell.rect[a].start < z0;
-                let overflow1 = cell.rect[a].end > z1 || (cell.rect[a].end == z1 && pixel1 > 0);
-                if overflow0 || overflow1 {
-                    let mut overflow = self.overflows.get(&d).cloned().unwrap_or_default();
-                    if overflow0 {
-                        overflow[a][0] +=
-                            pixel0 + self.axis_width(a, cell_ofs(cell.rect[a].start)..cell_ofs(z0));
-                    }
-                    if overflow1 {
-                        overflow[a][1] +=
-                            pixel1 + self.axis_width(a, cell_ofs(z1)..cell_ofs(cell.rect[a].end));
-                    }
-                    assert!(overflows.insert(s.coord_to_subpage(d), overflow).is_none());
-                }
-                z += cell.rect[b].len();
-            }
-        }
-
-        // Add overflows along the right side.
-        let mut z = 0;
-        while z < self.n[b] {
-            let d = CellPos::for_axis((a, z1 - 1), z);
-            let cell = self.get_cell(d);
-            if cell.rect[a].end > z1
-                || (cell.rect[a].end == z1 && pixel1 > 0)
-                    && !overflows.contains_key(&s.coord_to_subpage(cell.rect.top_left()))
-            {
-                let mut overflow = self.overflows.get(&d).cloned().unwrap_or_default();
-                overflow[a][1] +=
-                    pixel1 + self.axis_width(a, cell_ofs(z1)..cell_ofs(cell.rect[a].end));
-                assert!(
-                    overflows
-                        .insert(s.coord_to_subpage(cell.rect.top_left()), overflow)
-                        .is_none()
-                );
-            }
-            z += cell.rect[b].len();
-        }
-
-        // Copy overflows from `self` into the subpage.
-        // XXX this could be done at the start, which would simplify the while loops above
-        for (coord, overflow) in self.overflows.iter() {
-            let cell = self.table.get(*coord);
-            let rect = cell.rect();
-            if rect[a].end > z0 && rect[a].start < z1 {
-                overflows
-                    .entry(s.coord_to_subpage(rect.top_left()))
-                    .or_insert(*overflow);
-            }
-        }
+    /// The new [Page] will be suitable for rendering on a device whose page
+    /// size is `params.size`, but the caller is responsible for actually
+    /// breaking it up to fit on such a device, using the [Break] abstraction.
+    pub fn new(table: Table, device: &dyn Device, min_width: isize, look: &Look) -> Self {
+        let table = Arc::new(RenderedTable::new(table, device, min_width, look));
+        let ranges = EnumMap::from_fn(|axis| {
+            table.cp[axis][1 + table.h()[axis] * 2]..table.cp[axis].last().copied().unwrap()
+        });
+        Self { table, ranges }
+    }
 
-        let maps = Self::new_mappings(h, &r);
-        Arc::new(Self {
-            table: self.table.clone(),
-            n,
-            r,
-            maps,
-            cp,
-            overflows,
-            is_edge_cutoff,
-        })
+    pub fn split(&self, axis: Axis2) -> Break {
+        Break::new(self.clone(), axis)
     }
 
-    fn total_size(&self, axis: Axis2) -> isize {
-        self.cp[axis].last().copied().unwrap()
+    fn width(&self, axis: Axis2) -> isize {
+        self.table.cp[axis].last().copied().unwrap()
     }
 
     fn draw(&self, device: &mut dyn Device, ofs: Coord2) {
         use Axis2::*;
-        self.draw_cells(
-            device,
-            ofs,
-            CellRect::new(0..self.n[X] * 2 + 1, 0..self.n[Y] * 2 + 1),
-        );
-    }
-
-    fn draw_cells(&self, device: &mut dyn Device, ofs: Coord2, cells: CellRect) {
-        for y in cells.y.clone() {
-            let mut x = cells.x.start;
-            while x < cells.x.end {
-                if !is_rule(x) && !is_rule(y) {
-                    let cell = self.get_cell(CellPos::new(x / 2, y / 2));
-                    self.draw_cell(device, ofs, &cell);
-                    x = rule_ofs(cell.rect.x.end);
+        let cp = &self.table.cp;
+        for (y, yr) in self.table.cp[Y]
+            .iter()
+            .copied()
+            .tuple_windows()
+            .map(|(y0, y1)| y0..y1)
+            .enumerate()
+            .filter_map(|(y, yr)| if y % 2 == 1 { Some((y / 2, yr)) } else { None })
+        {
+            for (x, xr) in self.table.cp[X]
+                .iter()
+                .copied()
+                .tuple_windows()
+                .map(|(x0, x1)| x0..x1)
+                .enumerate()
+                .filter_map(|(x, xr)| if x % 2 == 1 { Some((x / 2, xr)) } else { None })
+            {
+                let cell = self.table.table.get(CellPos { x, y });
+                // XXX skip if not top-left cell
+                let rect = cell.rect();
+                let bb = Rect2::from_fn(|a| {
+                    cp[a][rect[a].start * 2 + 1]..cp[a][(rect[a].end - 1) * 2 + 2]
+                });
+                let clip = if y < self.table.h().y {
+                    if x < self.table.h().x {
+                        bb.clone()
+                    } else {
+                        Rect2::new(
+                            max(bb[X].start, self.ranges[X].start)
+                                ..min(bb[X].end, self.ranges[X].end),
+                            bb[Y].clone(),
+                        )
+                    }
+                } else if x < self.table.h().x {
+                    Rect2::new(
+                        bb[X].clone(),
+                        max(bb[Y].start, self.ranges[Y].start)..min(bb[Y].end, self.ranges[Y].end),
+                    )
                 } else {
-                    x += 1;
-                }
+                    Rect2::from_fn(|a| {
+                        max(bb[a].start, self.ranges[a].start)..min(bb[a].end, self.ranges[a].end)
+                    })
+                };
+                let draw_cell = DrawCell::new(cell.content.inner(), &self.table.table);
+                let valign_offset = match draw_cell.cell_style.vert_align {
+                    VertAlign::Top => 0,
+                    VertAlign::Middle => self.extra_height(device, &bb, &draw_cell) / 2,
+                    VertAlign::Bottom => self.extra_height(device, &bb, &draw_cell),
+                };
+                device.draw_cell(
+                    &draw_cell,
+                    bb.translate(ofs),
+                    valign_offset,
+                    EnumMap::from_fn(|_| [0, 0]),
+                    &clip.translate(ofs),
+                )
             }
         }
 
-        for y in cells.y.clone() {
-            for x in cells.x.clone() {
-                if is_rule(x) || is_rule(y) {
-                    self.draw_rule(device, ofs, CellPos { x, y });
-                }
+        for (y, yr) in self.table.cp[Y]
+            .iter()
+            .copied()
+            .tuple_windows()
+            .map(|(y0, y1)| y0..y1)
+            .enumerate()
+        {
+            for (x, xr) in self.table.cp[X]
+                .iter()
+                .copied()
+                .tuple_windows()
+                .map(|(x0, x1)| x0..x1)
+                .enumerate()
+                .filter(|(x, _)| *x % 2 == 0 || y % 2 == 0)
+            {
+                self.draw_rule(device, ofs, CellPos { x, y });
             }
         }
     }
@@ -868,10 +603,7 @@ impl Page {
         const NO_BORDER: BorderStyle = BorderStyle::none();
         let styles = EnumMap::from_fn(|a: Axis2| {
             let b = !a;
-            if !is_rule(coord[a])
-                || (self.is_edge_cutoff[a][0] && coord[a] == 0)
-                || (self.is_edge_cutoff[a][1] && coord[a] == self.n[a] * 2)
-            {
+            if !is_rule(coord[a]) {
                 [NO_BORDER, NO_BORDER]
             } else if is_rule(coord[b]) {
                 let first = if coord[b] > 0 {
@@ -882,7 +614,7 @@ impl Page {
                     NO_BORDER
                 };
 
-                let second = if coord[b] / 2 < self.n[b] {
+                let second = if coord[b] / 2 < self.table.n()[b] {
                     self.get_rule(a, coord)
                 } else {
                     NO_BORDER
@@ -899,26 +631,15 @@ impl Page {
             .values()
             .all(|border| border.iter().all(BorderStyle::is_none))
         {
-            let bb =
-                Rect2::from_fn(|a| self.cp[a][coord[a]]..self.cp[a][coord[a] + 1]).translate(ofs);
+            let bb = Rect2::from_fn(|a| self.table.cp[a][coord[a]]..self.table.cp[a][coord[a] + 1])
+                .translate(ofs);
             device.draw_line(bb, styles);
         }
     }
 
     fn get_rule(&self, a: Axis2, coord: CellPos) -> BorderStyle {
         let coord = CellPos::from_fn(|a| coord[a] / 2);
-        let coord = self.map_coord(coord);
-
-        let border = self.table.get_rule(a, coord);
-        let h = self.h();
-        if h[a] > 0 && coord[a] == h[a] {
-            let border2 = self
-                .table
-                .get_rule(a, CellPos::for_axis((a, h[a]), coord[!a]));
-            border.combine(border2)
-        } else {
-            border
-        }
+        self.table.table.get_rule(a, coord)
     }
 
     fn extra_height(&self, device: &dyn Device, bb: &Rect2, cell: &DrawCell) -> isize {
@@ -926,51 +647,48 @@ impl Page {
         let height = device.measure_cell_height(cell, bb[X].len() as isize);
         bb[Y].len() as isize - height
     }
-    fn draw_cell(&self, device: &mut dyn Device, ofs: Coord2, cell: &RenderCell) {
-        let mut bb = Rect2::from_fn(|a| {
-            self.cp[a][cell.rect[a].start * 2 + 1]..self.cp[a][cell.rect[a].end * 2]
-        })
-        .translate(ofs);
-        /*
-            let spill = EnumMap::from_fn(|a| {
-                [
-                    self.rule_width(a, cell.rect[a].start) / 2,
-                    self.rule_width(a, cell.rect[a].end) / 2,
-                ]
-        });*/
-        let spill = EnumMap::from_fn(|_| [0, 0]);
-
-        let clip = if let Some(overflow) = self.overflows.get(&cell.rect.top_left()) {
-            Rect2::from_fn(|a| {
-                let mut clip = bb[a].clone();
-                if overflow[a][0] > 0 {
-                    bb[a].start -= overflow[a][0];
-                    if cell.rect[a].start == 0 && !self.is_edge_cutoff[a][0] {
-                        clip.start = ofs[a] + self.cp[a][cell.rect[a].start * 2];
-                    }
-                }
+}
 
-                if overflow[a][1] > 0 {
-                    bb[a].end += overflow[a][1];
-                    if cell.rect[a].end == self.n[a] && !self.is_edge_cutoff[a][1] {
-                        clip.end = ofs[a] + self.cp[a][cell.rect[a].end * 2 + 1];
-                    }
-                }
+/// Returns the width of `extent` along `axis`.
+fn axis_width(cp: &[isize], extent: Range<usize>) -> isize {
+    cp[extent.end] - cp[extent.start]
+}
 
-                clip
-            })
-        } else {
-            bb.clone()
-        };
+/// Returns the width of cells within `extent` along `axis`.
+fn joined_width(cp: &[isize], extent: Range<usize>) -> isize {
+    axis_width(cp, cell_ofs(extent.start)..cell_ofs(extent.end) - 1)
+}
+/// Returns the offset in [Self::cp] of the cell with index `cell_index`.
+/// That is, if `cell_index` is 0, then the offset is 1, that of the leftmost
+/// or topmost cell; if `cell_index` is 1, then the offset is 3, that of the
+/// next cell to the right (or below); and so on. */
+fn cell_ofs(cell_index: usize) -> usize {
+    cell_index * 2 + 1
+}
 
-        let draw_cell = DrawCell::new(cell.content.inner(), &self.table);
-        let valign_offset = match draw_cell.cell_style.vert_align {
-            VertAlign::Top => 0,
-            VertAlign::Middle => self.extra_height(device, &bb, &draw_cell) / 2,
-            VertAlign::Bottom => self.extra_height(device, &bb, &draw_cell),
-        };
-        device.draw_cell(&draw_cell, bb, valign_offset, spill, &clip)
-    }
+/// Returns the offset in [Self::cp] of the rule with index `rule_index`.
+/// That is, if `rule_index` is 0, then the offset is that of the leftmost
+/// or topmost rule; if `rule_index` is 1, then the offset is that of the
+/// next rule to the right (or below); and so on.
+fn rule_ofs(rule_index: usize) -> usize {
+    rule_index * 2
+}
+
+/// Returns the width of cell `z` along `axis`.
+fn cell_width(cp: &[isize], z: usize) -> isize {
+    let ofs = cell_ofs(z);
+    axis_width(cp, ofs..ofs + 1)
+}
+
+/// Is `ofs` the offset of a rule in `cp`?
+fn is_rule(z: usize) -> bool {
+    z.is_even()
+}
+
+#[derive(Clone)]
+pub struct RenderCell<'a> {
+    rect: CellRect,
+    content: &'a Content,
 }
 
 struct Selection {
@@ -1140,77 +858,20 @@ fn measure_rule(device: &dyn Device, table: &Table, a: Axis2, z: usize) -> isize
 }
 
 #[derive(Debug)]
-struct Break {
-    page: Arc<Page>,
+pub struct Break {
+    page: Page,
 
     /// Axis along which `page` is being broken.
     axis: Axis2,
-
-    /// Next cell along `axis`.
-    z: usize,
-
-    /// Pixel offset within cell `z` (usually 0).
-    pixel: isize,
-
-    /// Width of headers of `page` along `axis`.
-    hw: isize,
 }
 
 impl Break {
-    fn new(page: Arc<Page>, axis: Axis2) -> Self {
-        let z = page.h()[axis];
-        let hw = page.headers_width(axis);
-        Self {
-            page,
-            axis,
-            z,
-            pixel: 0,
-            hw,
-        }
+    fn new(page: Page, axis: Axis2) -> Self {
+        Self { page, axis }
     }
 
     fn has_next(&self) -> bool {
-        self.z < self.page.n[self.axis]
-    }
-
-    /// Returns the width that would be required along this breaker's axis to
-    /// render a page from the current position up to but not including `cell`.
-    fn needed_size(&self, cell: usize) -> isize {
-        // Width of header not including its rightmost rule.
-        let mut size = self
-            .page
-            .axis_width(self.axis, 0..rule_ofs(self.page.h()[self.axis]));
-
-        // If we have a pixel offset and there is no header, then we omit
-        // the leftmost rule of the body.  Otherwise the rendering is deceptive
-        // because it looks like the whole cell is present instead of a partial
-        // cell.
-        //
-        // Otherwise (if there is a header) we will be merging two rules: the
-        // rightmost rule in the header and the leftmost rule in the body.  We
-        // assume that the width of a merged rule is the larger of the widths of
-        // either rule individually.
-        if self.pixel == 0 || self.page.h()[self.axis] > 0 {
-            size += max(
-                self.page.rule_width(self.axis, self.page.h()[self.axis]),
-                self.page.rule_width(self.axis, self.z),
-            );
-        }
-
-        // Width of body, minus any pixel offset in the leftmost cell.
-        size += self
-            .page
-            .joined_width(self.axis, self.z..cell)
-            .checked_sub(self.pixel)
-            .unwrap();
-
-        // Width of rightmost rule in body merged with leftmost rule in headers.
-        size += max(
-            self.page.rule_width_r(self.axis, 0),
-            self.page.rule_width(self.axis, cell),
-        );
-
-        size
+        !self.page.ranges[self.axis].is_empty()
     }
 
     /// Returns a new [Page] that is up to `size` pixels wide along the axis
@@ -1218,124 +879,52 @@ impl Break {
     /// completely broken up, or if `size` is too small to reasonably render any
     /// cells.  The latter will never happen if `size` is at least as large as
     /// the page size passed to [Page::new] along the axis using for breaking.
-    fn next(&mut self, device: &dyn Device, size: isize) -> Option<Arc<Page>> {
+    fn next(&mut self, device: &dyn Device, size: isize) -> Result<Option<Page>, ()> {
         if !self.has_next() {
-            return None;
+            return Ok(None);
         }
-
-        self.find_breakpoint(device, size).map(|(z, pixel)| {
-            let page = match pixel {
-                0 => self.page.select(self.axis, self.z..z, self.pixel, 0),
-                pixel => self.page.select(
-                    self.axis,
-                    self.z..z + 1,
-                    pixel,
-                    self.page.cell_width(self.axis, z) - pixel,
-                ),
-            };
-            self.z = z;
-            self.pixel = pixel;
-            page
-        })
+        let target = size
+            .checked_sub(self.page.table.headers_width(self.axis))
+            .ok_or(())?;
+        let start = self.page.ranges[self.axis].start;
+        let (end, next_start) = self.find_breakpoint(start..start + target, device);
+        let result = Page {
+            table: self.page.table.clone(),
+            ranges: EnumMap::from_fn(|axis| {
+                if axis == self.axis {
+                    start..end
+                } else {
+                    self.page.ranges[axis].clone()
+                }
+            }),
+        };
+        self.page.ranges[self.axis].start = next_start;
+        Ok(Some(result))
     }
 
-    fn break_cell(&self, device: &dyn Device, z: usize, overflow: isize) -> isize {
-        if self.cell_is_breakable(device, z) {
-            // If there is no right header and we render a partial cell
-            // on the right side of the body, then we omit the rightmost
-            // rule of the body.  Otherwise the rendering is deceptive
-            // because it looks like the whole cell is present instead
-            // of a partial cell.
-            //
-            // This is similar to code for the left side in
-            // [Self::needed_size].
-            let rule_allowance = self.page.rule_width(self.axis, z);
-
-            // The amount that, if we added cell `z`, the rendering
-            // would overfill the allocated `size`.
-            let overhang = overflow - rule_allowance; // XXX could go negative
-
-            // The width of cell `z`.
-            let cell_size = self.page.cell_width(self.axis, z);
-
-            // The amount trimmed off the left side of `z`, and the
-            // amount left to render.
-            let cell_ofs = if z == self.z { self.pixel } else { 0 };
-            let cell_left = cell_size - cell_ofs;
-
-            // If some of the cell remains to render, and there would
-            // still be some of the cell left afterward, then partially
-            // render that much of the cell.
-            let mut pixel = if cell_left > 0 && cell_left > overhang {
-                cell_left - overhang + cell_ofs
-            } else {
-                0
-            };
-
-            // If there would be only a tiny amount of the cell left
-            // after rendering it partially, reduce the amount rendered
-            // slightly to make the output look a little better.
-            let em = device.params().em();
-            if pixel + em > cell_size {
-                pixel = pixel.saturating_sub(em);
-            }
+    fn find_breakpoint(&self, range: Range<isize>, device: &dyn Device) -> (isize, isize) {
+        let cp = &self.page.table.cp[self.axis];
 
-            // If we're breaking vertically, then consider whether the
-            // cells being broken have a better internal breakpoint than
-            // the exact number of pixels available, which might look
-            // bad e.g. because it breaks in the middle of a line of
-            // text.
-            if self.axis == Axis2::Y && device.params().can_adjust_break {
-                let mut x = 0;
-                while x < self.page.n[Axis2::X] {
-                    let cell = self.page.get_cell(CellPos::new(x, z));
-                    let better_pixel = device.adjust_break(
-                        cell.content,
-                        Coord2::new(self.page.joined_width(Axis2::X, cell.rect.x.clone()), pixel),
-                    );
-                    x += cell.rect.x.len();
-
-                    if better_pixel < pixel {
-                        let start_pixel = if z > self.z { self.pixel } else { 0 };
-                        if better_pixel > start_pixel {
-                            pixel = better_pixel;
-                            break;
-                        } else if better_pixel == 0 && z != self.z {
-                            pixel = 0;
-                            break;
-                        }
-                    }
-                }
-            }
-
-            pixel
-        } else {
-            0
+        // If everything remaining fits, then take it all.
+        let max = cp.last().copied().unwrap();
+        if range.end >= max {
+            return (max, max);
         }
-    }
 
-    fn find_breakpoint(&mut self, device: &dyn Device, size: isize) -> Option<(usize, isize)> {
-        for z in self.z..self.page.n[self.axis] {
-            let needed = self.needed_size(z + 1);
-            if needed > size {
-                let pixel = self.break_cell(device, z, needed - size);
-                if z == self.z && pixel == 0 {
-                    return None;
+        // Otherwise, take as much as fits.
+        for c in 0..self.page.table.n()[self.axis] {
+            let position = cp[c * 2 + 3];
+            if position > range.end {
+                if self.page.table.cell_width(self.axis, c) >= device.params().min_break[self.axis]
+                {
+                    // XXX various way to choose a better breakpoint
+                    return (range.end, range.end);
                 } else {
-                    return Some((z, pixel));
+                    return (cp[(c - 1) * 2 + 3], cp[(c - 1) * 2 + 2]);
                 }
             }
         }
-        Some((self.page.n[self.axis], 0))
-    }
-
-    /// Returns true if `cell` along this breaker's axis may be broken across a
-    /// page boundary.
-    ///
-    /// This is just a heuristic.  Breaking cells across page boundaries can
-    /// save space, but it looks ugly.
-    fn cell_is_breakable(&self, device: &dyn Device, cell: usize) -> bool {
-        self.page.cell_width(self.axis, cell) >= device.params().min_break[self.axis]
+        unreachable!()
     }
 }
 
@@ -1345,7 +934,7 @@ pub struct Pager {
     /// [Page]s to be rendered, in order, vertically.  There may be up to 5
     /// pages, for the pivot table's title, layers, body, captions, and
     /// footnotes.
-    pages: SmallVec<[Arc<Page>; 5]>,
+    pages: SmallVec<[Page; 5]>,
 
     x_break: Option<Break>,
     y_break: Option<Break>,
@@ -1364,7 +953,7 @@ impl Pager {
 
         // Figure out the width of the body of the table. Use this to determine
         // the base scale.
-        let body_page = Page::new(Arc::new(output.body), device, 0, &pivot_table.style.look);
+        let body_page = Page::new(output.body, device, 0, &pivot_table.style.look);
         let body_width = body_page.width(Axis2::X);
         let mut scale = if body_width > device.params().size[Axis2::X]
             && pivot_table.style.look.shrink_to_fit[Axis2::X]
@@ -1377,21 +966,16 @@ impl Pager {
 
         let mut pages = SmallVec::new();
         for table in [output.title, output.layers].into_iter().flatten() {
-            pages.push(Arc::new(Page::new(
-                Arc::new(table),
+            pages.push(Page::new(
+                table,
                 device,
                 body_width,
                 &pivot_table.style.look,
-            )));
+            ));
         }
-        pages.push(Arc::new(body_page));
+        pages.push(body_page);
         for table in [output.caption, output.footnotes].into_iter().flatten() {
-            pages.push(Arc::new(Page::new(
-                Arc::new(table),
-                device,
-                0,
-                &pivot_table.style.look,
-            )));
+            pages.push(Page::new(table, device, 0, &pivot_table.style.look));
         }
         pages.reverse();
 
@@ -1407,7 +991,7 @@ impl Pager {
         if pivot_table.style.look.shrink_to_fit[Axis2::Y] && device.params().can_scale {
             let total_height = pages
                 .iter()
-                .map(|page: &Arc<Page>| page.total_size(Axis2::Y))
+                .map(|page: &Page| page.width(Axis2::Y))
                 .sum::<isize>() as f64;
             let max_height = device.params().size[Axis2::Y] as f64;
             if total_height * scale >= max_height {
@@ -1424,30 +1008,30 @@ impl Pager {
     }
 
     /// True if there's content left to render.
-    pub fn has_next(&mut self, device: &dyn Device) -> bool {
-        while self
-            .y_break
-            .as_mut()
-            .is_none_or(|y_break| !y_break.has_next())
+    pub fn has_next(&mut self, device: &dyn Device) -> Option<&mut Break> {
+        // If there's a nonempty y_break, return it.
+        if let Some(y_break) = self.y_break.as_mut()
+            && y_break.has_next()
         {
-            self.y_break = self
-                .x_break
-                .as_mut()
-                .and_then(|x_break| {
-                    x_break.next(
+            return self.y_break.as_mut();
+        }
+
+        loop {
+            // Get a new y_break from the x_break.
+            if let Some(x_break) = &mut self.x_break
+                && let Some(page) = x_break
+                    .next(
                         device,
                         (device.params().size[Axis2::X] as f64 / self.scale) as isize,
                     )
-                })
-                .map(|page| Break::new(page, Axis2::Y));
-            if self.y_break.is_none() {
-                match self.pages.pop() {
-                    Some(page) => self.x_break = Some(Break::new(page, Axis2::X)),
-                    _ => return false,
-                }
+                    .unwrap()
+            {
+                self.y_break = Some(page.split(Axis2::Y));
+                return self.y_break.as_mut();
             }
+
+            self.x_break = Some(self.pages.pop()?.split(Axis2::X));
         }
-        true
     }
 
     /// Draws a chunk of content to fit in a space that has vertical size
@@ -1464,16 +1048,12 @@ impl Pager {
         }
 
         let mut ofs = Coord2::new(0, 0);
-        while self.has_next(device) {
-            let Some(page) = self
-                .y_break
-                .as_mut()
-                .and_then(|y_break| y_break.next(device, space - ofs[Y]))
-            else {
+        while let Some(y_break) = self.has_next(device) {
+            let Some(page) = y_break.next(device, space - ofs[Y]).unwrap_or_default() else {
                 break;
             };
             page.draw(device, ofs);
-            ofs[Y] += page.total_size(Y);
+            ofs[Y] += page.width(Y);
         }
 
         if self.scale != 1.0 {
index 3b21e63567fc206a49932f4f229c4ef9d7cb5775..ff7e440d8f837587a0cc0ea7de781ecb581e63fa 100644 (file)
@@ -305,8 +305,14 @@ pub struct Table {
     pub n: CellPos,
 
     /// Table header rows and columns.
+    ///
+    /// This is a subset of `n`, so `h.x <= n.x` and
+    /// `h.y <= n.y`.
     pub h: CellPos,
 
+    /// Table contents.
+    ///
+    /// The array has `n.x` columns and `n.y` rows.
     pub contents: Array2<Content>,
 
     /// Styles for areas of the table.