From: Ben Pfaff Date: Sun, 26 Jan 2025 01:33:30 +0000 (-0800) Subject: render page measuring now implemented X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=478c9971890b42be822c68e3324493bc1c026808;p=pspp render page measuring now implemented --- diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index 2853360680..c8317b4626 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -732,7 +732,7 @@ impl Not for Axis2 { } /// A 2-dimensional `(x,y)` pair. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] pub struct Coord2(pub EnumMap); impl Coord2 { diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index 7fdb5d53a4..318a757415 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -1,4 +1,4 @@ -use std::cmp::max; +use std::cmp::{max, min}; use std::collections::HashMap; use std::iter::once; use std::ops::Range; @@ -9,7 +9,7 @@ use itertools::interleave; use smallvec::SmallVec; use super::pivot::{Axis2, BorderStyle, Coord2, Look, PivotTable, Rect2, Stroke}; -use super::table::{Cell, CellInner, Table}; +use super::table::{Cell, CellInner, Content, Table}; /// Parameters for rendering a table_item to a device. /// @@ -79,17 +79,17 @@ pub trait Device { /// Returns the height required to render `cell` given a width of `width`. fn measure_cell_height(&self, cell: &CellInner, width: usize) -> usize; - /// Given that there is space measuring `width` by `height` to render - /// `cell`, where `height` is insufficient to render the entire height of - /// the cell, returns the largest height less than `height` at which it is + /// Given that there is space measuring `size` to render `cell`, where + /// `size.y()` is insufficient to render the entire height of the cell, + /// returns the largest height less than `size.y()` at which it is /// appropriate to break the cell. For example, if breaking at the - /// specified `height` would break in the middle of a line of text, the + /// specified `size.y()` would break in the middle of a line of text, the /// return value would be just sufficiently less that the breakpoint would /// be between lines of text. /// /// Optional. If [RenderParams::can_adjust_break] is false, the rendering /// engine assumes that all breakpoints are acceptable. - fn adjust_break(&self, cell: &Cell, size: Coord2) -> usize; + fn adjust_break(&self, cell: &Content, size: Coord2) -> usize; /// Draws a generalized intersection of lines in `bb`. /// @@ -309,6 +309,12 @@ fn cell_width(cp: &[usize], z: usize) -> usize { axis_width(cp, ofs..ofs + 1) } +#[derive(Clone)] +pub struct RenderCell<'a> { + rect: Rect2, + content: &'a Content, +} + impl Page { /// Creates and returns a new [Page] for rendering `table` with the given /// `look` on `device`. @@ -316,7 +322,7 @@ 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: &Arc, device: &Arc, min_width: usize, look: &Look) -> Self { + fn new(table: Arc
, device: Arc, min_width: usize, look: &Look) -> Self { use Axis2::*; let n = table.n.clone(); @@ -324,7 +330,7 @@ impl Page { // Figure out rule widths. let rules = EnumMap::from_fn(|axis| { (0..n[axis]) - .map(|z| measure_rule(table, &**device, axis, z)) + .map(|z| measure_rule(&*table, &*device, axis, z)) .collect::>() }); @@ -473,8 +479,8 @@ impl Page { let r = Rect2::new(h[X]..n[X], h[Y]..n[Y]); let maps = Self::new_mappings(h, &r); Self { - device: device.clone(), - table: table.clone(), + device, + table, n, h, r, @@ -578,48 +584,49 @@ impl Page { *self.cp[axis].last().unwrap() } - /// XXX This could return a - 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(), - } - } - } - fn new_mappings(h: Coord2, r: &Rect2) -> 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_cell(&self, coord: Coord2) -> RenderCell<'_> { + fn get_map(&self, axis: Axis2, ordinate: usize) -> &Map { + if ordinate < self.h[axis] { + &self.maps[axis][0] + } else { + &self.maps[axis][1] + } + } - }*/ + fn get_cell(&self, coord: Coord2) -> RenderCell<'_> { + let maps = EnumMap::from_fn(|axis| self.get_map(axis, coord[axis])); + let coord = Coord2(coord.0.map(|axis, ordinate| ordinate + maps[axis].ofs)); + let cell = self.table.get(coord); + RenderCell { + rect: Rect2(cell.rect().0.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, + } + } - /// Creates and returns a new [Page] whose contents are a subregion of - /// thispage's contents. The new page includes cells `extent` (exclusive) - /// along `axis`, plus any headers on `axis`. + /// 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`, plus any headers on `axis`. /// /// If `pixel0` is nonzero, then it is a number of pixels to exclude from /// the left or top (according to `axis`) of cell `extent.start`. @@ -628,46 +635,45 @@ impl Page { /// render cells that are too large to fit on a single page.) /// /// The whole of axis `!axis` is included. (The caller may follow up with - /// another call to select on `!axis` to select on that axis as well.) + /// another call to select on `!axis`.) fn select( self: &Arc, - axis: Axis2, + a: Axis2, extent: Range, pixel0: usize, pixel1: usize, ) -> Arc { + let b = !a; let z0 = extent.start; let z1 = extent.end; // If all of the page is selected, just make a copy. - if z0 == self.h[axis] && z1 == self.n[axis] && pixel0 == 0 && pixel1 == 0 { + if z0 == self.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[axis], self.n[axis] - z1]; + let trim = [z0 - self.h[a], self.n[a] - z1]; let mut n = self.n; - n[axis] -= trim[0] + trim[1]; - let mut h = self.h; + n[a] -= trim[0] + trim[1]; + let h = self.h; let mut r = self.r.clone(); - r[axis].start += trim[0]; - r[axis].end -= trim[1]; + 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.clone(); - is_edge_cutoff[axis][0] = - h[axis] == 0 && (pixel0 > 0 || (z0 == 0 && self.is_edge_cutoff[axis][0])); - is_edge_cutoff[axis][1] = - pixel1 > 0 || (z1 == self.n[axis] && self.is_edge_cutoff[axis][1]); + 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[axis].as_slice(); - let mut dcp = Vec::with_capacity(2 * n[axis] + 1); + 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[axis]) { - total += if z == 0 && is_edge_cutoff[axis][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] @@ -684,32 +690,127 @@ impl Page { } dcp.push(total); } - let z = self.rule_ofs_r(axis, 0); - if !is_edge_cutoff[axis][1] { + 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(), 2 * n[axis] + 1); + debug_assert_eq!(dcp.len(), 2 * n[a] + 1); let mut cp = EnumMap::default(); - cp[axis] = dcp; - cp[!axis] = self.cp[!axis].clone(); + cp[a] = dcp; + cp[!a] = self.cp[!a].clone(); + + let mut overflows = HashMap::new(); // Add new overflows. - let s = Selection; - if self.h[axis] == 0 || z0 > self.h[axis] || pixel0 > 0 { - let b = !axis; + let s = Selection { + a, + b, + h, + z0, + z1, + p0: pixel0, + p1: pixel1, + }; + if self.h[a] == 0 || z0 > self.h[a] || pixel0 > 0 { let mut z = 0; while z < self.n[b] { - let d = Coord2::for_axis((axis, z0), z); + let d = Coord2::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(cell.rect.top_left()), overflow) + .is_none()); + } + z += cell.rect[b].len(); } } - todo!() + let mut z = 0; + while z < self.n[b] { + let d = Coord2::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 + .get(&s.coord_to_subpage(cell.rect.top_left())) + .is_none() + { + 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.clone()); + } + } + + let maps = Self::new_mappings(h, &r); + Arc::new(Self { + device: self.device.clone(), + table: self.table.clone(), + n, + h, + r, + maps, + cp, + overflows, + is_edge_cutoff, + }) } + + fn table_width(&self, axis: Axis2) -> usize { + self.cp[axis].last().copied().unwrap() + } +} + +struct Selection { + a: Axis2, + b: Axis2, + z0: usize, + z1: usize, + p0: usize, + p1: usize, + h: Coord2, } -struct Selection; +impl Selection { + /// Returns the coordinates of `coord` as it will appear in this subpage. + /// + /// `coord` must be in the selected region or the results will not make + /// sense. + fn coord_to_subpage(&self, coord: Coord2) -> Coord2 { + let a = self.a; + let b = self.b; + let ha0 = self.h[a]; + Coord2::for_axis((a, max(coord[a] + ha0 - self.z0, ha0)), coord[b]) + } +} /// Maps a contiguous range of cells from a page to the underlying table along /// the horizontal or vertical dimension. @@ -721,6 +822,9 @@ struct Map { /// First ordinate in the table. t0: usize, + /// `t0 - p0`. + ofs: usize, + /// Number of ordinates in page and table. n: usize, } @@ -859,13 +963,15 @@ struct Break { } impl Break { - fn new(page: &Arc, axis: Axis2) -> Self { + fn new(page: Arc, axis: Axis2) -> Self { + let z = page.h[axis]; + let hw = page.headers_width(axis); Self { - page: page.clone(), + page, axis, - z: page.h[axis], + z, pixel: 0, - hw: page.headers_width(axis), + hw, } } @@ -918,71 +1024,119 @@ 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, size: usize) -> Option { + 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]; + self.find_breakpoint(size).map(|(z, pixel)| 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, + ), + }) + } - 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); - } + /// Returns a small but visible width. + fn em(&self) -> usize { + self.page.device.params().font_size[Axis2::X] + } - // 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) {} + fn break_cell(&self, z: usize, overflow: usize) -> usize { + 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 = 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 = self.em(); + 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 { + let mut x = 0; + while x < self.page.n[Axis2::X] { + let cell = self.page.get_cell(Coord2::new(x, z)); + let better_pixel = self.page.device.adjust_break( + cell.content, + Coord2::new( + self.page + .joined_width(Axis2::X, cell.rect[Axis2::X].clone()), + pixel, + ), + ); + x += cell.rect[Axis2::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; + } } } - break; } + + pixel + } else { + 0 } + } - todo!() + fn find_breakpoint(&mut self, size: usize) -> Option<(usize, usize)> { + 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(z, needed - size); + if z == self.z && pixel == 0 { + return None; + } else { + return Some((z, pixel)); + } + } + } + Some((self.page.n[self.axis], 0)) } /// Returns true if `cell` along this breaker's axis may be broken across a @@ -1002,11 +1156,10 @@ 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<[Box; 5]>, + pages: SmallVec<[Arc; 5]>, - cur_page: usize, - x_break: Break, - y_break: Break, + x_break: Option, + y_break: Option, } impl Pager { @@ -1022,10 +1175,89 @@ impl Pager { // 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_page = Page::new(Arc::new(output.body), device.clone(), 0, &pivot_table.look); let body_width = body_page.width(Axis2::X); + let mut scale = if body_width > device.params().size[Axis2::X] + && pivot_table.look.shrink_to_fit[Axis2::X] + && device.params().can_scale + { + device.params().size[Axis2::X] as f64 / body_width as f64 + } else { + 1.0 + }; - todo!() + let mut pages = SmallVec::new(); + for table in [output.title, output.layers].into_iter().flatten() { + pages.push(Arc::new(Page::new( + Arc::new(table), + device.clone(), + body_width, + &pivot_table.look, + ))); + } + pages.push(Arc::new(body_page)); + for table in [output.caption, output.footnotes].into_iter().flatten() { + pages.push(Arc::new(Page::new( + Arc::new(table), + device.clone(), + 0, + &pivot_table.look, + ))); + } + pages.reverse(); + + // If we're shrinking tables to fit the page length, then adjust the + // scale factor. + // + // XXX This will sometimes shrink more than needed, because adjusting + // the scale factor allows for cells to be "wider", which means that + // sometimes they won't break across as much vertical space, thus + // shrinking the table vertically more than the scale would imply. + // Shrinking only as much as necessary would require an iterative + // search. + if pivot_table.look.shrink_to_fit[Axis2::Y] && device.params().can_scale { + let total_height = pages + .iter() + .map(|page: &Arc| page.table_width(Axis2::Y)) + .sum::() as f64; + let max_height = device.params().size[Axis2::Y] as f64; + if total_height * scale >= max_height { + scale *= max_height / total_height; + } + } + + Self { + device, + scale, + pages, + x_break: None, + y_break: None, + } + } + + /// True if there's content left to rnder. + fn has_next(&mut self) -> bool { + while self + .y_break + .as_mut() + .is_none_or(|y_break| !y_break.has_next()) + { + self.y_break = self + .x_break + .as_mut() + .and_then(|x_break| { + x_break.next( + (self.device.params().size[Axis2::X] as f64 / self.scale as f64) as usize, + ) + }) + .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, + } + } + } + false } } diff --git a/rust/pspp/src/output/table.rs b/rust/pspp/src/output/table.rs index bf6a2f8f0c..4060f05b45 100644 --- a/rust/pspp/src/output/table.rs +++ b/rust/pspp/src/output/table.rs @@ -21,7 +21,7 @@ use super::pivot::{Area, AreaStyle, Axis2, Border, BorderStyle, HeadingRegion, R #[derive(Clone)] pub struct CellRef<'a> { pub coord: Coord2, - content: &'a Content, + pub content: &'a Content, } impl<'a> CellRef<'a> {