work on rendering code
authorBen Pfaff <blp@cs.stanford.edu>
Sun, 12 Jan 2025 17:46:29 +0000 (09:46 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Sun, 12 Jan 2025 17:46:29 +0000 (09:46 -0800)
Makefile.am
rust/pspp/src/output/csv.rs
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/render.rs
rust/pspp/src/output/table.rs
src/output/table-provider.h

index b82bb6bb6f5c417e2af2eb7f10d4fb73e52b4593..36d19c0df3ad89392ce2c4d55c0b757b6139c958 100644 (file)
@@ -159,7 +159,3 @@ $(bin_PROGRAMS) $(RECURSIVE_TARGETS) dist: $(BUILT_SOURCES) config.h
 mimedir = $(datadir)/mime/packages
 mime_DATA = org.gnu.pspp.xml
 EXTRA_DIST += org.gnu.pspp.xml
-
-EXTRA_DIST += rust/Cargo.lock rust/Cargo.toml rust/src/main.rs \
-rust/src/lib.rs rust/src/endian.rs rust/src/hexfloat.rs                \
-rust/src/raw.rs rust/src/sack.rs rust/tests/sack.rs
index 93c45b53382728e60ac708b926794ad20a53effb..df0b158db910222f849a5d6b982faa4c6a61b58d 100644 (file)
@@ -112,7 +112,7 @@ impl CsvDriver {
 
                 let coord = Coord2::new(x, y);
                 let content = table.get(coord);
-                if content.is_top_left(coord) {
+                if content.is_top_left() {
                     if let Some(value) = &content.inner().value {
                         let display = value.display(Some(pivot_table));
                         let s = match leader {
index fa55ae330f5e5a724311bd45ed9446656a2fb050..606bb55177da4b88344be4c6a7bff810fff231f9 100644 (file)
@@ -509,7 +509,7 @@ pub struct Look {
     pub row_labels_in_corner: bool,
 
     /// Ranges of column widths in the two heading regions, in 1/96" units.
-     pub heading_widths: EnumMap<HeadingRegion, RangeInclusive<usize>>,
+    pub heading_widths: EnumMap<HeadingRegion, RangeInclusive<usize>>,
 
     /// Kind of markers to use for footnotes.
     pub footnote_marker_type: FootnoteMarkerType,
@@ -817,6 +817,12 @@ impl Index<Axis2> for Rect2 {
     }
 }
 
+impl IndexMut<Axis2> for Rect2 {
+    fn index_mut(&mut self, index: Axis2) -> &mut Self::Output {
+        &mut self.0[index]
+    }
+}
+
 #[derive(Copy, Clone, Debug, Default)]
 pub enum FootnoteMarkerType {
     /// a, b, c, ...
@@ -839,23 +845,23 @@ pub enum FootnoteMarkerPosition {
 
 #[derive(Clone, Debug)]
 pub struct PivotTable {
-    look: Arc<Look>,
+    pub look: Arc<Look>,
 
-    rotate_inner_column_labels: bool,
+    pub rotate_inner_column_labels: bool,
 
-    rotate_outer_row_labels: bool,
+    pub rotate_outer_row_labels: bool,
 
-    show_grid_lines: bool,
+    pub show_grid_lines: bool,
 
-    show_title: bool,
+    pub show_title: bool,
 
-    show_caption: bool,
+    pub show_caption: bool,
 
-    show_values: Option<Show>,
+    pub show_values: Option<Show>,
 
-    show_variables: Option<Show>,
+    pub show_variables: Option<Show>,
 
-    weight_format: Format,
+    pub weight_format: Format,
 
     /// Current layer indexes, with `axes[Axis3::Z].dimensions.len()` elements.
     /// `current_layer[i]` is an offset into
@@ -865,32 +871,32 @@ pub struct PivotTable {
     pub current_layer: Vec<usize>,
 
     /// Column and row sizing and page breaks.
-    sizing: EnumMap<Axis2, Sizing>,
+    pub sizing: EnumMap<Axis2, Sizing>,
 
     /// Format settings.
-    settings: FormatSettings,
+    pub settings: FormatSettings,
 
     /// Numeric grouping character (usually `.` or `,`).
-    grouping: Option<char>,
-
-    small: f64,
-
-    command_local: Option<String>,
-    command_c: Option<String>,
-    language: Option<String>,
-    locale: Option<String>,
-    dataset: Option<String>,
-    datafile: Option<String>,
-    date: Option<NaiveDateTime>,
-    footnotes: Vec<Footnote>,
-    title: Option<Value>,
-    subtype: Option<Value>,
-    corner_text: Option<Value>,
-    caption: Option<Value>,
-    notes: Option<String>,
-    dimensions: Vec<Arc<Dimension>>,
-    axes: EnumMap<Axis3, Axis>,
-    cells: HashMap<usize, Value>,
+    pub grouping: Option<char>,
+
+    pub small: f64,
+
+    pub command_local: Option<String>,
+    pub command_c: Option<String>,
+    pub language: Option<String>,
+    pub locale: Option<String>,
+    pub dataset: Option<String>,
+    pub datafile: Option<String>,
+    pub date: Option<NaiveDateTime>,
+    pub footnotes: Vec<Footnote>,
+    pub title: Option<Value>,
+    pub subtype: Option<Value>,
+    pub corner_text: Option<Value>,
+    pub caption: Option<Value>,
+    pub notes: Option<String>,
+    pub dimensions: Vec<Arc<Dimension>>,
+    pub axes: EnumMap<Axis3, Axis>,
+    pub cells: HashMap<usize, Value>,
 }
 
 impl PivotTable {
index d69b5d901396ae20c00609e359450c012588a5dc..3a2e84e1acf1694cfc00433686a117ec76bd29a0 100644 (file)
@@ -1,11 +1,12 @@
+use std::cmp::max;
 use std::collections::HashMap;
-use std::ops::RangeInclusive;
+use std::ops::Range;
 use std::sync::Arc;
 
-use enum_map::EnumMap;
+use enum_map::{enum_map, EnumMap};
 use smallvec::SmallVec;
 
-use super::pivot::{Axis2, BorderStyle, Coord2, HeadingRegion, Look, PivotTable, Rect2, Stroke};
+use super::pivot::{Axis2, BorderStyle, Coord2, Look, PivotTable, Rect2, Stroke};
 use super::table::{Cell, CellInner, Table};
 
 /// Parameters for rendering a table_item to a device.
@@ -137,25 +138,26 @@ pub trait Device {
 /// 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.
+///
+/// # Rendered cells
+///
+/// The horizontal cells rendered are the leftmost `h[X]`, then `r[X]`.
+/// The vertical cells rendered are the topmost `h[Y]`, then `r[Y]`.
+/// `n[i]` is the sum of `h[i]` and `r[i].len()`.
 struct Page {
-    params: Arc<Params>,
+    device: Arc<dyn Device>,
     table: Arc<Table>,
 
-    /// Region of `table` to render.
-    ///
-    /// The horizontal cells rendered are the leftmost h[H][0], then
-    /// r[H][0] through r[H][1], exclusive, then the rightmost h[H][1].
-    ///
-    /// The vertical cells rendered are the topmost h[V][0], then r[V][0]
-    /// through r[V][1], exclusive, then the bottommost h[V][1].
+    /// Size of the table in cells.
     ///
-    /// n[H] = h[H][0] + (r[H][1] - r[H][0]) + h[H][1]
-    /// n[V] = h[V][0] + (r[V][1] - r[V][0]) + h[V][1]
     n: Coord2,
-    /*
-    int h[TABLE_N_AXES][2];
-    int r[TABLE_N_AXES][2];
-    int n[TABLE_N_AXES];*/
+
+    /// Header size.  Cells `0..h[X]` are rendered horizontally, and `0..h[Y]` vertically.
+    h: Coord2,
+
+    /// Main region of cells to render.
+    r: Rect2,
+
     /// "Cell positions".
     ///
     /// cp[H] represents x positions within the table.
@@ -269,23 +271,6 @@ struct Page {
     /// When is_edge_cutoff is true for a given edge, the 'overflows' hmap will
     /// contain a node for each cell along that edge.
     is_edge_cutoff: EnumMap<Axis2, [bool; 2]>,
-
-    /// If part of a joined cell would be cut off by breaking a table along
-    /// 'axis' at the rule with offset 'z' (where 0 <= z <= n[axis]), then
-    /// join_crossing[axis][z] is the thickness of the rule that would be cut
-    /// off.
-    ///
-    /// This is used to know to allocate extra space for breaking at such a
-    /// position, so that part of the cell's content is not lost.
-    ///
-    /// This affects breaking a table only when headers are present.  When
-    /// headers are not present, the rule's thickness is used for cell content,
-    /// so no part of the cell's content is lost (and in fact it is duplicated
-    /// across both pages).
-    join_crossing: EnumMap<Axis2, usize>,
-
-    /// Minimum and maximum widths of columns based on their headings.
-    width_ranges: EnumMap<HeadingRegion, RangeInclusive<usize>>,
 }
 
 impl Page {
@@ -295,12 +280,14 @@ impl Page {
     /// 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: &Table, device: &dyn Device, min_width: usize, look: &Look) -> Self {
+    fn new(table: &Arc<Table>, device: &Arc<dyn Device>, min_width: usize, look: &Look) -> Self {
+        use Axis2::*;
+
         // Figure out rule widths.
         let rules = EnumMap::from_fn(|axis| {
             let n = table.n[axis];
             (0..n)
-                .map(|z| measure_rule(table, device, axis, z))
+                .map(|z| measure_rule(table, &**device, axis, z))
                 .collect::<Vec<_>>()
         });
 
@@ -308,21 +295,21 @@ impl Page {
         let heading_widths = look
             .heading_widths
             .clone()
-            .map(|region, range| [*range.start() * px_size, *range.end() * px_size]);
+            .map(|_region, range| [*range.start() * px_size, *range.end() * px_size]);
 
         // Calculate minimum and maximum widths of cells that do not span
         // multiple columns.
         let mut columns = [vec![0; table.n.x()], vec![0; table.n.x()]];
-        for y in 0..table.n[Axis2::Y] {
+        for y in 0..table.n[Y] {
             for x in table.iter_x(y) {
                 let coord = Coord2::new(x, y);
                 let contents = table.get(coord);
-                if !contents.is_top_left(coord) || contents.col_span() != 1 {
+                if !contents.is_top_left() || contents.col_span() != 1 {
                     continue;
                 }
 
                 let mut w = device.measure_cell_width(contents.inner());
-                if let Some(px_size) = device.params().px_size {
+                if device.params().px_size.is_some() {
                     if let Some(region) = table.heading_region(coord) {
                         let wr = &heading_widths[region];
                         if w[0] < wr[0] {
@@ -348,7 +335,7 @@ impl Page {
         }
 
         // Distribute widths of spanned columns.
-        let columns = columns.map(|widths| {
+        let mut columns = columns.map(|widths| {
             widths
                 .into_iter()
                 .map(|width| Row {
@@ -357,22 +344,327 @@ impl Page {
                 })
                 .collect::<Vec<_>>()
         });
-        for y in 0..table.n[Axis2::Y] {
+        for y in 0..table.n[Y] {
             for x in table.iter_x(y) {
                 let coord = Coord2::new(x, y);
                 let contents = table.get(coord);
-                if !contents.is_top_left(coord) || contents.col_span() == 1 {
+                if !contents.is_top_left() || contents.col_span() == 1 {
                     continue;
                 }
+                let rect = contents.rect();
 
                 let w = device.measure_cell_width(contents.inner());
                 for i in 0..2 {
-                    //distribute_spanned_width(w[i],
+                    distribute_spanned_width(
+                        w[i],
+                        &mut columns[i][rect[X].clone()],
+                        &rules[X][rect[X].start..rect[X].end + 1],
+                    );
                 }
             }
         }
+        if min_width > 0 {
+            for i in 0..2 {
+                distribute_spanned_width(min_width, columns[i].as_mut_slice(), &rules[X]);
+            }
+        }
 
-        todo!()
+        // In pathological cases, spans can cause the minimum width of a column
+        // to exceed the maximum width.  This bollixes our interpolation
+        // algorithm later, so fix it up.
+        for i in 0..table.n.x() {
+            if columns[0][i].width > columns[1][i].width {
+                columns[1][i].width = columns[0][i].width;
+            }
+        }
+
+        // Decide final column widths.
+        let rule_widths = rules[X].iter().copied().sum::<usize>();
+        let table_widths = columns
+            .iter()
+            .map(|row| row.iter().map(|row| row.width).sum::<usize>() + rule_widths)
+            .collect::<SmallVec<[usize; 2]>>();
+
+        let mut page = if table_widths[1] <= device.params().size[X] {
+            // Fits even with maximum widths.  Use them.
+            Self::new_with_exact_widths(device, table, &columns[1], &rules[X])
+        } else if table_widths[0] <= device.params().size[X] {
+            // Fits with minimum widths, so distribute the leftover space.
+            //Self::new_with_interpolated_widths()
+            todo!()
+        } else {
+            // Doesn't fit even with minimum widths.  Assign minimums for now, and
+            // later we can break it horizontally into multiple pages.
+            Self::new_with_exact_widths(device, table, &columns[0], &rules[X])
+        };
+
+        // Calculate heights of cells that do not span multiple rows.
+        let mut rows = vec![Row::default(); table.n[Y]];
+        for y in 0..table.n[Y] {
+            let row = &mut rows[y];
+            for x in table.iter_x(y) {
+                let coord = Coord2::new(x, y);
+                let contents = table.get(coord);
+                if !contents.is_top_left() {
+                    continue;
+                }
+                let rect = contents.rect();
+
+                if contents.row_span() == 1 {
+                    let w = page.joined_width(X, rect[X].clone());
+                    let h = device.measure_cell_height(contents.inner(), w);
+                    if h > row.unspanned {
+                        row.unspanned = h;
+                        row.width = h;
+                    }
+                }
+            }
+        }
+
+        // Distribute heights of spanned rows.
+        for y in 0..table.n[Y] {
+            for x in table.iter_x(y) {
+                let coord = Coord2::new(x, y);
+                let contents = table.get(coord);
+                if contents.is_top_left() && contents.row_span() > 1 {
+                    let rect = contents.rect();
+                    let w = page.joined_width(X, rect[X].clone());
+                    let h = device.measure_cell_height(contents.inner(), w);
+                    distribute_spanned_width(
+                        h,
+                        &mut rows[rect[Y].clone()],
+                        &rules[Y][rect[Y].start..rect[Y].end + 1],
+                    );
+                }
+            }
+        }
+
+        // Decide final row heights.
+        page.cp[Y] = Self::accumulate_row_widths(&rows, &rules[Y]);
+
+        // Measure headers.  If they are "too big", get rid of them.
+        for axis in [X, Y] {
+            let hw = page.headers_width(axis);
+            let threshold = device.params().size[axis];
+            if hw * 2 >= threshold || hw + page.max_cell_width(axis) > threshold {
+                page.h[axis] = 0;
+                page.r[axis] = 0..page.n[axis];
+            }
+        }
+        page
+    }
+
+    fn accumulate_row_widths(rows: &[Row], rules: &[usize]) -> Vec<usize> {
+        debug_assert_eq!(rows.len() + 1, rules.len());
+        let n = 2 * (rows.len()) + 1;
+        let mut cp = Vec::with_capacity(n);
+        let mut total = 0;
+        cp.push(total);
+        for (rule, row) in rules.iter().zip(rows.iter()) {
+            total += *rule;
+            cp.push(total);
+            total += row.width;
+            cp.push(total);
+        }
+        total += rules.last().unwrap();
+        cp.push(total);
+        debug_assert_eq!(cp.len(), n);
+        cp
+    }
+
+    fn new_with_exact_widths(
+        device: &Arc<dyn Device>,
+        table: &Arc<Table>,
+        rows: &[Row],
+        rules: &[usize],
+    ) -> Self {
+        use Axis2::*;
+        Self {
+            device: device.clone(),
+            table: table.clone(),
+            n: table.n,
+            h: table.h,
+            r: Rect2::new(table.h[X]..table.n[X], table.h[Y]..table.n[Y]),
+            cp: enum_map! {
+                X => Self::accumulate_row_widths(rows, rules),
+                Y => Vec::new(),
+            },
+            overflows: HashMap::new(),
+            is_edge_cutoff: EnumMap::default(),
+        }
+    }
+
+    /// Returns the width of `extent` along `axis`.
+    fn axis_width(&self, axis: Axis2, extent: Range<usize>) -> usize {
+        self.cp[axis][extent.end] - self.cp[axis][extent.start]
+    }
+
+    /// Returns the width of cells within `extent` along `axis`.
+    fn joined_width(&self, axis: Axis2, extent: Range<usize>) -> usize {
+        self.axis_width(
+            axis,
+            Self::cell_ofs(extent.start)..Self::cell_ofs(extent.end) - 1,
+        )
+    }
+
+    /// Returns the width of the headers along `axis`.
+    fn headers_width(&self, axis: Axis2) -> usize {
+        self.axis_width(axis, Self::rule_ofs(0)..Self::cell_ofs(self.h[axis]))
+    }
+
+    /// Returns the width of rule `z` along `axis`.
+    fn rule_width(&self, axis: Axis2, z: usize) -> usize {
+        self.axis_width(axis, Self::rule_ofs(z + 1)..Self::rule_ofs(z + 1))
+    }
+
+    /// Returns the width of rule `z` along `axis`, counting in reverse order.
+    fn rule_width_r(&self, axis: Axis2, z: usize) -> usize {
+        let ofs = self.rule_ofs_r(axis, z);
+        self.axis_width(axis, ofs..ofs + 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 offset in [Self::cp] of the rule with
+    /// index `rule_index_r`, which counts from the right side (or bottom) of the page
+    /// left (or up), according to `axis`, respectively.  That is,
+    /// if `rule_index_r` is 0, then the offset is that of the rightmost or bottommost
+    /// 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
+    }
+
+    /// Returns the width of cell `z` along `axis`.
+    fn cell_width(&self, axis: Axis2, z: usize) -> usize {
+        let ofs = Self::cell_ofs(z);
+        self.axis_width(axis, ofs..ofs + 1)
+    }
+
+    /// Returns the width of the widest cell, excluding headers, along `axis`.
+    fn max_cell_width(&self, axis: Axis2) -> usize {
+        (self.h[axis]..self.n[axis])
+            .map(|z| self.cell_width(axis, z))
+            .max()
+            .unwrap_or(0)
+    }
+
+    fn width(&self, axis: Axis2) -> usize {
+        *self.cp[axis].last().unwrap()
+    }
+
+    fn get_map(&self, a: Axis2, z: usize) -> Map {
+        if z < self.h[a] {
+            Map {
+                p0: 0,
+                t0: 0,
+                n: self.h[a],
+            }
+        } else {
+            Map {
+                p0: self.h[a],
+                t0: self.r[a].start,
+                n: self.r[a].len(),
+            }
+        }
+    }
+}
+
+/// Maps a contiguous range of cells from a page to the underlying table along
+/// the horizontal or vertical dimension.
+struct Map {
+    /// First ordinate in the page.
+    p0: usize,
+
+    /// First ordinate in the table.
+    t0: usize,
+
+    /// Number of ordinates in page and table.
+    n: usize,
+}
+
+/// Modifies the 'width' members of `rows` so that their sum, when added to rule
+/// widths `rules[1..n - 1]`, where n is rows.len(), is at least `width`.
+///
+/// # Implementation
+///
+/// The algorithm used here is based on the following description from HTML 4:
+///
+/// > For cells that span multiple columns, a simple approach consists of
+/// > apportioning the min/max widths evenly to each of the constituent
+/// > columns.  A slightly more complex approach is to use the min/max
+/// > widths of unspanned cells to weight how spanned widths are
+/// > apportioned.  Experiments suggest that a blend of the two approaches
+/// > gives good results for a wide range of tables.
+///
+/// We blend the two approaches half-and-half, except that we cannot use the
+/// unspanned weights when 'total_unspanned' is 0 (because that would cause a
+/// division by zero).
+///
+/// The calculation we want to do is this:
+///
+/// ```text
+/// w0 = width / n
+/// w1 = width * (column's unspanned width) / (total unspanned width)
+/// (column's width) = (w0 + w1) / 2
+/// ```
+///
+/// We implement it as a precise calculation in integers by multiplying `w0` and
+/// `w1` by the common denominator of all three calculations (`d`), dividing
+/// that out in the column width calculation, and then keeping the remainder for
+/// the next iteration.
+///
+/// (We actually compute the unspanned width of a column as twice the unspanned
+/// width, plus the width of the rule on the left, plus the width of the rule on
+/// the right.  That way each rule contributes to both the cell on its left and
+/// on its right.)
+fn distribute_spanned_width(width: usize, rows: &mut [Row], rules: &[usize]) {
+    debug_assert!(rows.len() >= 2);
+    debug_assert!(rules.len() == rows.len() + 1);
+
+    let n = rows.len();
+    let total_unspanned = rows.iter().map(|row| row.unspanned).sum::<usize>()
+        + (&rules[1..n - 1]).iter().copied().sum::<usize>();
+    if total_unspanned >= width {
+        return;
+    }
+
+    let d0 = rows.len();
+    let d1 = 2 * total_unspanned.max(1);
+    let d = if total_unspanned > 0 {
+        d0 * d1 * 2
+    } else {
+        d0 * d1
+    };
+    let mut w = d / 2;
+    for x in 0..n {
+        w += width * d1;
+        if total_unspanned > 0 {
+            let mut unspanned = rows[x].unspanned * 2;
+            if x < n - 1 {
+                unspanned += rules[x + 1];
+            }
+            if x > 0 {
+                unspanned += rules[x];
+            }
+            w += width * unspanned * d0;
+        }
+        rows[x].width = max(rows[x].width, w / d);
+        w = w.checked_sub(rows[x].width * d).unwrap();
     }
 }
 
@@ -443,8 +735,145 @@ struct Break {
     hw: usize,
 }
 
+impl Break {
+    fn new(page: &Arc<Page>, axis: Axis2) -> Self {
+        Self {
+            page: page.clone(),
+            axis,
+            z: page.h[axis],
+            pixel: 0,
+            hw: page.headers_width(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) -> usize {
+        // Width of header not including its rightmost rule.
+        let mut size = self
+            .page
+            .axis_width(self.axis, 0..Page::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
+    }
+
+    /// Returns a new [Page] that is up to `size` pixels wide along the axis
+    /// used for breaking.  Returns `None` if the page has already been
+    /// 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, size: usize) -> Option<Page> {
+        if !self.has_next() {
+            return None;
+        }
+
+        // A small but visible width.
+        let em = self.page.device.params().font_size[Axis2::X];
+
+        let mut pixel = 0;
+        for z in self.z..self.page.n[self.axis] {
+            let needed = self.needed_size(z + 1);
+            if needed > size {
+                if self.cell_is_breakable(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 = needed - size - 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.
+                    if pixel + em > cell_size {
+                        pixel = pixel.saturating_sub(em);
+                    }
+
+                    // 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 && self.page.device.params().can_adjust_break {
+                        for x in self.page.table.iter_x(z) {}
+                    }
+                }
+                break;
+            }
+        }
+
+        todo!()
+    }
+
+    /// 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, cell: usize) -> bool {
+        self.page.cell_width(self.axis, cell) >= self.page.device.params().min_break[self.axis]
+    }
+}
+
 pub struct Pager {
-    device: Box<dyn Device>,
+    device: Arc<dyn Device>,
     scale: f64,
 
     /// [Page]s to be rendered, in order, vertically.  There may be up to 5
@@ -459,7 +888,7 @@ pub struct Pager {
 
 impl Pager {
     pub fn new(
-        device: Box<dyn Device>,
+        device: Arc<dyn Device>,
         pivot_table: &PivotTable,
         layer_indexes: Option<&[usize]>,
     ) -> Self {
@@ -468,6 +897,12 @@ impl Pager {
             device.params().printing,
         );
 
+        // Figure out the width of the body of the table. Use this to determine
+        // the base scale.
+        let body = Arc::new(output.body);
+        let body_page = Page::new(&body, &device, 0, &pivot_table.look);
+        let body_width = body_page.width(Axis2::X);
+
         todo!()
     }
 }
index ac5c65307f27121a8dbe8577b0ac1c5e4ce8421a..d7ae0505a61acd290cb55bda9be216c05e56a547 100644 (file)
@@ -16,9 +16,44 @@ use enum_map::{enum_map, EnumMap};
 
 use crate::output::pivot::Coord2;
 
-use super::pivot::{
-    Area, AreaStyle, Axis2, Border, BorderStyle, CellStyle, FontStyle, HeadingRegion, Rect2, Value,
-};
+use super::pivot::{Area, AreaStyle, Axis2, Border, BorderStyle, HeadingRegion, Rect2, Value};
+
+pub struct CellRef<'a> {
+    coord: Coord2,
+    content: &'a Content,
+}
+
+impl<'a> CellRef<'a> {
+    pub fn inner(&self) -> &CellInner {
+        self.content.inner()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.content.is_empty()
+    }
+
+    pub fn rect(&self) -> Rect2 {
+        self.content.rect(self.coord)
+    }
+
+    pub fn next_x(&self) -> usize {
+        self.content.next_x(self.coord.x())
+    }
+
+    pub fn is_top_left(&self) -> bool {
+        self.content.is_top_left(self.coord)
+    }
+
+    pub fn span(&self, axis: Axis2) -> usize {
+        self.content.span(axis)
+    }
+    pub fn col_span(&self) -> usize {
+        self.span(Axis2::X)
+    }
+    pub fn row_span(&self) -> usize {
+        self.span(Axis2::Y)
+    }
+}
 
 #[derive(Clone)]
 pub enum Content {
@@ -51,28 +86,47 @@ impl Content {
         }
     }
 
-    pub fn region(&self) -> Option<&Rect2> {
-        if let Content::Join(cell) = self {
-            Some(&cell.region)
-        } else {
-            None
+    /// Returns the rectangle that this cell covers, only if the cell contains
+    /// that information. (Joined cells always do, and other cells usually
+    /// don't.)
+    pub fn joined_rect(&self) -> Option<&Rect2> {
+        match self {
+            Content::Join(cell) => Some(&cell.region),
+            _ => None,
+        }
+    }
+
+    /// Returns the rectangle that this cell covers. If the cell doesn't contain
+    /// that information, returns a rectangle containing `coord`.
+    pub fn rect(&self, coord: Coord2) -> Rect2 {
+        match self {
+            Content::Join(cell) => cell.region.clone(),
+            _ => Rect2::for_cell(coord),
         }
     }
 
     pub fn next_x(&self, x: usize) -> usize {
-        self.region().map_or(x + 1, |region| region[Axis2::X].end)
+        self.joined_rect()
+            .map_or(x + 1, |region| region[Axis2::X].end)
     }
 
     pub fn is_top_left(&self, coord: Coord2) -> bool {
-        self.region().map_or(true, |r| coord == r.top_left())
+        self.joined_rect().map_or(true, |r| coord == r.top_left())
     }
 
-    pub fn col_span(&self) -> usize {
-        self.region().map_or(1, |r| {
-            let range = &r.0[Axis2::X];
+    pub fn span(&self, axis: Axis2) -> usize {
+        self.joined_rect().map_or(1, |r| {
+            let range = &r.0[axis];
             range.end - range.start
         })
     }
+
+    pub fn col_span(&self) -> usize {
+        self.span(Axis2::X)
+    }
+    pub fn row_span(&self) -> usize {
+        self.span(Axis2::Y)
+    }
 }
 
 #[derive(Clone)]
@@ -81,18 +135,11 @@ pub struct Cell {
 
     /// Occupied table region.
     region: Rect2,
-    font_style: Option<Box<FontStyle>>,
-    cell_style: Option<Box<CellStyle>>,
 }
 
 impl Cell {
     fn new(inner: CellInner, region: Rect2) -> Self {
-        Self {
-            inner,
-            region,
-            font_style: None,
-            cell_style: None,
-        }
+        Self { inner, region }
     }
 }
 
@@ -123,7 +170,7 @@ pub struct Table {
     pub n: Coord2,
 
     /// Table header rows and columns.
-    pub headers: Coord2,
+    pub h: Coord2,
 
     pub contents: Vec<Content>,
 
@@ -146,7 +193,7 @@ impl Table {
     ) -> Self {
         Self {
             n,
-            headers,
+            h: headers,
             contents: vec![Content::Empty; n.y() * n.x()],
             areas,
             borders,
@@ -161,8 +208,11 @@ impl Table {
         pos.x() + self.n.x() * pos.y()
     }
 
-    pub fn get(&self, pos: Coord2) -> &Content {
-        &self.contents[self.offset(pos)]
+    pub fn get(&self, coord: Coord2) -> CellRef<'_> {
+        CellRef {
+            coord,
+            content: &self.contents[self.offset(coord)],
+        }
     }
 
     pub fn get_rule(&self, axis: Axis2, pos: Coord2) -> BorderStyle {
@@ -237,7 +287,7 @@ impl Table {
             for x in self.iter_x(y) {
                 let coord = Coord2::new(x, y);
                 let content = self.get(coord);
-                if !content.is_empty() && content.is_top_left(coord) {
+                if !content.is_empty() && content.is_top_left() {
                     f(content.inner());
                 }
             }
@@ -246,9 +296,9 @@ impl Table {
 
     /// The heading region that `pos` is part of, if any.
     pub fn heading_region(&self, pos: Coord2) -> Option<HeadingRegion> {
-        if pos.x() < self.headers.x() {
+        if pos.x() < self.h.x() {
             Some(HeadingRegion::RowHeadings)
-        } else if pos.y() < self.headers.y() {
+        } else if pos.y() < self.h.y() {
             Some(HeadingRegion::ColumnHeadings)
         } else {
             None
@@ -268,7 +318,7 @@ impl<'a> Iterator for XIter<'a> {
     fn next(&mut self) -> Option<Self::Item> {
         let next_x = self
             .x
-            .map_or(0, |x| self.table.get(Coord2::new(x, self.y)).next_x(x));
+            .map_or(0, |x| self.table.get(Coord2::new(x, self.y)).next_x());
         if next_x >= self.table.n.x() {
             None
         } else {
index a3d1fab78f0381ac0996fcc489d91e624f6adea3..ecccbffe4e6366a11d0225e38e5e1397419775dc 100644 (file)
@@ -47,7 +47,6 @@ struct table_cell
 
     unsigned char options;       /* TABLE_CELL_*. */
     const struct pivot_value *value;
-    const struct font_style *font_style;
     const struct cell_style *cell_style;
   };