// 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;
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".
///
/// - `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.
/// - `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::*;
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,
}
}
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())
}
/// 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]))
}
/// 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`.
/// 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 });
}
}
}
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 {
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
.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 {
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 {
}
#[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
/// 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!()
}
}
/// [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>,
// 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]
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();
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 {
}
/// 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
}
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 {