From: Ben Pfaff Date: Mon, 8 Dec 2025 01:13:47 +0000 (-0800) Subject: work on new approach to rendering X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9eea7c78a59d81953c944e2552cae403ebb616aa;p=pspp work on new approach to rendering --- diff --git a/rust/pspp/src/output/drivers/cairo/fsm.rs b/rust/pspp/src/output/drivers/cairo/fsm.rs index cb6651b0a6..60b006dcf7 100644 --- a/rust/pspp/src/output/drivers/cairo/fsm.rs +++ b/rust/pspp/src/output/drivers/cairo/fsm.rs @@ -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) diff --git a/rust/pspp/src/output/drivers/text.rs b/rust/pspp/src/output/drivers/text.rs index d930a0cc39..cc6cad0432 100644 --- a/rust/pspp/src/output/drivers/text.rs +++ b/rust/pspp/src/output/drivers/text.rs @@ -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}")?; diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index bca9556276..1714bc5cdb 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -15,13 +15,12 @@ // this program. If not, see . 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 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, +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>, - - /// [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>, - - /// 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, -} - -/// Returns the width of `extent` along `axis`. -fn axis_width(cp: &[isize], extent: Range) -> isize { - cp[extent.end] - cp[extent.start] } -/// Returns the width of cells within `extent` along `axis`. -fn joined_width(cp: &[isize], extent: Range) -> 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
, 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 { 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 { - 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, + ranges: EnumMap>, +} - /// 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, - a: Axis2, - extent: Range, - pixel0: isize, - pixel1: isize, - ) -> Arc { - 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) -> 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) -> 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, +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, 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> { + fn next(&mut self, device: &dyn Device, size: isize) -> Result, ()> { 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, 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; 5]>, + pages: SmallVec<[Page; 5]>, x_break: Option, y_break: Option, @@ -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.total_size(Axis2::Y)) + .map(|page: &Page| page.width(Axis2::Y)) .sum::() 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 { diff --git a/rust/pspp/src/output/table.rs b/rust/pspp/src/output/table.rs index 3b21e63567..ff7e440d8f 100644 --- a/rust/pspp/src/output/table.rs +++ b/rust/pspp/src/output/table.rs @@ -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, /// Styles for areas of the table.