From: Ben Pfaff Date: Sun, 12 Jan 2025 17:46:29 +0000 (-0800) Subject: work on rendering code X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b79bbe76ca44627134436777cf87f9f45aeab25a;p=pspp work on rendering code --- diff --git a/Makefile.am b/Makefile.am index b82bb6bb6f..36d19c0df3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 diff --git a/rust/pspp/src/output/csv.rs b/rust/pspp/src/output/csv.rs index 93c45b5338..df0b158db9 100644 --- a/rust/pspp/src/output/csv.rs +++ b/rust/pspp/src/output/csv.rs @@ -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 { diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index fa55ae330f..606bb55177 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -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>, + pub heading_widths: EnumMap>, /// Kind of markers to use for footnotes. pub footnote_marker_type: FootnoteMarkerType, @@ -817,6 +817,12 @@ impl Index for Rect2 { } } +impl IndexMut 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, + pub look: Arc, - 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, + pub show_values: Option, - show_variables: Option, + pub show_variables: Option, - 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, /// Column and row sizing and page breaks. - sizing: EnumMap, + pub sizing: EnumMap, /// Format settings. - settings: FormatSettings, + pub settings: FormatSettings, /// Numeric grouping character (usually `.` or `,`). - grouping: Option, - - small: f64, - - command_local: Option, - command_c: Option, - language: Option, - locale: Option, - dataset: Option, - datafile: Option, - date: Option, - footnotes: Vec, - title: Option, - subtype: Option, - corner_text: Option, - caption: Option, - notes: Option, - dimensions: Vec>, - axes: EnumMap, - cells: HashMap, + pub grouping: Option, + + pub small: f64, + + pub command_local: Option, + pub command_c: Option, + pub language: Option, + pub locale: Option, + pub dataset: Option, + pub datafile: Option, + pub date: Option, + pub footnotes: Vec, + pub title: Option, + pub subtype: Option, + pub corner_text: Option, + pub caption: Option, + pub notes: Option, + pub dimensions: Vec>, + pub axes: EnumMap, + pub cells: HashMap, } impl PivotTable { diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index d69b5d9013..3a2e84e1ac 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -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, + device: Arc, table: Arc, - /// 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, - - /// 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, - - /// Minimum and maximum widths of columns based on their headings. - width_ranges: EnumMap>, } 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
, device: &Arc, 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::>() }); @@ -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::>() }); - 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::(); + let table_widths = columns + .iter() + .map(|row| row.iter().map(|row| row.width).sum::() + rule_widths) + .collect::>(); + + 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 { + 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, + table: &Arc
, + 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 { + 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 { + 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::() + + (&rules[1..n - 1]).iter().copied().sum::(); + 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, 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 { + 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, + device: Arc, 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, + device: Arc, 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!() } } diff --git a/rust/pspp/src/output/table.rs b/rust/pspp/src/output/table.rs index ac5c65307f..d7ae0505a6 100644 --- a/rust/pspp/src/output/table.rs +++ b/rust/pspp/src/output/table.rs @@ -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>, - cell_style: Option>, } 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, @@ -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 { - 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 { 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 { diff --git a/src/output/table-provider.h b/src/output/table-provider.h index a3d1fab78f..ecccbffe4e 100644 --- a/src/output/table-provider.h +++ b/src/output/table-provider.h @@ -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; };