pub can_scale: bool,
}
+impl Params {
+ /// Returns a small but visible width.
+ fn em(&self) -> usize {
+ self.font_size[Axis2::X]
+ }
+}
+
pub trait Device {
fn params(&self) -> &Params;
/// single word across multiple lines (normally, this is the width of the
/// longest word in the cell) and `max_width` is the minimum width required
/// to avoid line breaks other than at new-lines.
- fn measure_cell_width(&self, cell: &CellInner) -> [usize; 2];
+ fn measure_cell_width(&self, cell: &DrawCell) -> [usize; 2];
/// Returns the height required to render `cell` given a width of `width`.
- fn measure_cell_height(&self, cell: &CellInner, width: usize) -> usize;
+ fn measure_cell_height(&self, cell: &DrawCell, width: usize) -> usize;
/// Given that there is space measuring `size` to render `cell`, where
/// `size.y()` is insufficient to render the entire height of the cell,
/// Optional. If [RenderParams::can_adjust_break] is false, the rendering
/// engine assumes that all breakpoints are acceptable.
fn adjust_break(&self, cell: &Content, size: Coord2) -> usize;
-}
-pub trait Draw {
/// Draws a generalized intersection of lines in `bb`.
///
/// `styles` is interpreted this way:
pub footnotes: &'a [Arc<Footnote>],
}
+impl<'a> DrawCell<'a> {
+ fn new(inner: &'a CellInner, table: &'a Table) -> Self {
+ let (style, subscripts, footnotes) = if let Some(styling) = inner.value.styling.as_ref() {
+ (
+ &styling.style,
+ styling.subscripts.as_slice(),
+ styling.footnotes.as_slice(),
+ )
+ } else {
+ (&table.areas[inner.area], [].as_slice(), [].as_slice())
+ };
+ Self {
+ rotate: inner.rotate,
+ inner: &inner.value.inner,
+ style,
+ subscripts,
+ footnotes,
+ }
+ }
+}
+
/// 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
/// 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 {
- device: Arc<dyn Device>,
table: Arc<Table>,
/// Size of the table in cells.
/// 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: Arc<dyn Device>, min_width: usize, look: &Look) -> Self {
+ fn new(table: Arc<Table>, device: &dyn Device, min_width: usize, look: &Look) -> Self {
use Axis2::*;
let n = table.n.clone();
// 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(&*device, &*table, axis, z))
.collect::<Vec<_>>()
});
// multiple columns.
let mut unspanned_columns = [vec![0; n.x()], vec![0; n.x()]];
for cell in table.cells().filter(|cell| cell.col_span() == 1) {
- let mut w = device.measure_cell_width(cell.inner());
+ let mut w = device.measure_cell_width(&DrawCell::new(cell.inner(), &*table));
if device.params().px_size.is_some() {
if let Some(region) = table.heading_region(cell.coord) {
let wr = &heading_widths[region];
for cell in table.cells().filter(|cell| cell.col_span() > 1) {
let rect = cell.rect();
- let w = device.measure_cell_width(cell.inner());
+ let w = device.measure_cell_width(&DrawCell::new(cell.inner(), &*table));
for i in 0..2 {
distribute_spanned_width(
w[i],
let rect = cell.rect();
let w = joined_width(&cp_x, rect[X].clone());
- let h = device.measure_cell_height(cell.inner(), w);
+ let h = device.measure_cell_height(&DrawCell::new(cell.inner(), &*table), w);
let row = &mut unspanned_rows[cell.coord.y()];
if h > *row {
for cell in table.cells().filter(|cell| cell.row_span() > 1) {
let rect = cell.rect();
let w = joined_width(&cp_x, rect[X].clone());
- let h = device.measure_cell_height(cell.inner(), w);
+ let h = device.measure_cell_height(&DrawCell::new(cell.inner(), &*table), w);
distribute_spanned_width(
h,
&unspanned_rows[rect[Y].clone()],
let r = Rect2::new(h[X]..n[X], h[Y]..n[Y]);
let maps = Self::new_mappings(h, &r);
Self {
- device,
table,
n,
h,
let maps = Self::new_mappings(h, &r);
Arc::new(Self {
- device: self.device.clone(),
table: self.table.clone(),
n,
h,
self.cp[axis].last().copied().unwrap()
}
- fn draw(&self, draw: &mut dyn Draw, ofs: Coord2) {
+ fn draw(&self, device: &mut dyn Device, ofs: Coord2) {
use Axis2::*;
self.draw_cells(
+ device,
ofs,
- draw,
Rect2::new(0..self.n[X] * 2 + 1, 0..self.n[Y] * 2 + 1),
);
}
- fn draw_cells(&self, ofs: Coord2, draw: &mut dyn Draw, cells: Rect2) {
+ fn draw_cells(&self, device: &mut dyn Device, ofs: Coord2, cells: Rect2) {
use Axis2::*;
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(Coord2::new(x, y));
- self.draw_cell(ofs, draw, &cell);
+ self.draw_cell(device, ofs, &cell);
x = rule_ofs(cell.rect[X].end);
} else {
x += 1;
}
}
- /*
- for y in cells[Y] {
- for x in cells[X] {
+ for y in cells[Y].clone() {
+ for x in cells[X].clone() {
if is_rule(x) && is_rule(y) {
- self.draw_rule(ofs, draw, Coord2::new(x, y));
+ self.draw_rule(device, ofs, Coord2::new(x, y));
}
}
- }*/
+ }
}
- fn draw_rule(&self, ofs: Coord2, draw: &mut dyn Draw, coord: Coord2) {
+ fn draw_rule(&self, device: &mut dyn Device, ofs: Coord2, coord: Coord2) {
const NO_BORDER: BorderStyle = BorderStyle::none();
let styles = EnumMap::from_fn(|a: Axis2| {
let b = !a;
{
let bb =
Rect2::from_fn(|a| self.cp[a][coord[a]]..self.cp[a][coord[a] + 1]).translate(ofs);
- draw.draw_line(bb, styles);
+ device.draw_line(bb, styles);
}
}
}
}
- fn extra_height(&self, bb: &Rect2, inner: &CellInner) -> usize {
+ fn extra_height(&self, device: &dyn Device, bb: &Rect2, cell: &DrawCell) -> usize {
use Axis2::*;
- let height = self.device.measure_cell_height(inner, bb[X].len());
+ let height = device.measure_cell_height(cell, bb[X].len());
usize::saturating_sub(bb[Y].len(), height)
}
- fn draw_cell(&self, ofs: Coord2, draw: &mut dyn Draw, cell: &RenderCell) {
+ fn draw_cell(&self, device: &mut dyn Device, ofs: Coord2, cell: &RenderCell) {
use Axis2::*;
- let inner = cell.content.inner();
- let (style, subscripts, footnotes) = if let Some(styling) = inner.value.styling.as_ref() {
- (
- &styling.style,
- styling.subscripts.as_slice(),
- styling.footnotes.as_slice(),
- )
- } else {
- (&self.table.areas[inner.area], [].as_slice(), [].as_slice())
- };
- let draw_cell = DrawCell {
- rotate: inner.rotate,
- inner: &inner.value.inner,
- style,
- subscripts,
- footnotes,
- };
let mut bb = Rect2::from_fn(|a| {
self.cp[a][cell.rect[a].start * 2 + 1]..self.cp[a][cell.rect[a].end * 2]
})
bb.clone()
};
- let valign_offset = match style.cell_style.vert_align {
- VertAlign::Top => 0,
- VertAlign::Middle => self.extra_height(&bb, inner) / 2,
- VertAlign::Bottom => self.extra_height(&bb, inner),
- };
-
// Header rows are never alternate rows.
let alternate_row =
usize::checked_sub(cell.rect[Y].start, self.h[Y]).is_some_and(|row| row % 2 == 1);
- draw.draw_cell(&draw_cell, alternate_row, &bb, valign_offset, spill, &clip)
+ let draw_cell = DrawCell::new(cell.content.inner(), &*self.table);
+ let valign_offset = match draw_cell.style.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, alternate_row, &bb, valign_offset, spill, &clip)
}
}
/// Returns the width of the rule in `table` that is at offset `z` along axis
/// `a`, if rendered on `device`.
-fn measure_rule(table: &Table, device: &dyn Device, a: Axis2, z: usize) -> usize {
+fn measure_rule(device: &dyn Device, table: &Table, a: Axis2, z: usize) -> usize {
let b = !a;
// Determine the types of rules that are present.
/// 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<Arc<Page>> {
+ fn next(&mut self, device: &dyn Device, size: usize) -> Option<Arc<Page>> {
if !self.has_next() {
return None;
}
- 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,
- ),
- })
- }
-
- /// Returns a small but visible width.
- fn em(&self) -> usize {
- self.page.device.params().font_size[Axis2::X]
+ self.find_breakpoint(device, 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,
+ ),
+ })
}
- fn break_cell(&self, z: usize, overflow: usize) -> usize {
- if self.cell_is_breakable(z) {
+ fn break_cell(&self, device: &dyn Device, z: usize, overflow: usize) -> usize {
+ 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
// 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();
+ let em = device.params().em();
if pixel + em > cell_size {
pixel = pixel.saturating_sub(em);
}
// 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 {
+ 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(Coord2::new(x, z));
- let better_pixel = self.page.device.adjust_break(
+ let better_pixel = device.adjust_break(
cell.content,
Coord2::new(
self.page
}
}
- fn find_breakpoint(&mut self, size: usize) -> Option<(usize, usize)> {
+ fn find_breakpoint(&mut self, device: &dyn Device, 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);
+ let pixel = self.break_cell(device, z, needed - size);
if z == self.z && pixel == 0 {
return None;
} else {
///
/// 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]
+ fn cell_is_breakable(&self, device: &dyn Device, cell: usize) -> bool {
+ self.page.cell_width(self.axis, cell) >= device.params().min_break[self.axis]
}
}
pub struct Pager {
- device: Arc<dyn Device>,
scale: f64,
/// [Page]s to be rendered, in order, vertically. There may be up to 5
impl Pager {
pub fn new(
- device: Arc<dyn Device>,
+ device: &dyn Device,
pivot_table: &PivotTable,
layer_indexes: Option<&[usize]>,
) -> Self {
// 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.clone(), 0, &pivot_table.look);
+ let body_page = Page::new(Arc::new(output.body), device, 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]
for table in [output.title, output.layers].into_iter().flatten() {
pages.push(Arc::new(Page::new(
Arc::new(table),
- device.clone(),
+ device,
body_width,
&pivot_table.look,
)));
for table in [output.caption, output.footnotes].into_iter().flatten() {
pages.push(Arc::new(Page::new(
Arc::new(table),
- device.clone(),
+ device,
0,
&pivot_table.look,
)));
}
Self {
- device,
scale,
pages,
x_break: None,
}
/// True if there's content left to rnder.
- pub fn has_next(&mut self) -> bool {
+ pub fn has_next(&mut self, device: &dyn Device) -> bool {
while self
.y_break
.as_mut()
.as_mut()
.and_then(|x_break| {
x_break.next(
- (self.device.params().size[Axis2::X] as f64 / self.scale as f64) as usize,
+ device,
+ (device.params().size[Axis2::X] as f64 / self.scale as f64) as usize,
)
})
.map(|page| Break::new(page, Axis2::Y));
/// Returns the amount of vertical space actually used by the rendered
/// chunk, which will be 0 if `space` is too small to render anything or if
/// no content remains (use [Self::has_next] to distinguish these cases).
- pub fn draw_next(&mut self, mut space: usize, draw: &mut dyn Draw) -> usize {
+ pub fn draw_next(&mut self, device: &mut dyn Device, mut space: usize) -> usize {
use Axis2::*;
if self.scale != 1.0 {
- draw.scale(self.scale);
+ device.scale(self.scale);
space = (space as f64 / self.scale) as usize;
}
let mut ofs = Coord2::new(0, 0);
let mut n_pages = None;
- while self.has_next() && n_pages == Some(self.pages.len()) {
+ while self.has_next(device) && n_pages == Some(self.pages.len()) {
n_pages = Some(self.pages.len());
let Some(page) = self
.y_break
.as_mut()
- .and_then(|y_break| y_break.next(space - ofs[Y]))
+ .and_then(|y_break| y_break.next(device, space - ofs[Y]))
else {
break;
};
- page.draw(draw, ofs);
+ page.draw(device, ofs);
ofs[Y] += page.total_size(Y);
}
borrow::Cow,
fs::File,
io::{BufWriter, Write},
+ ops::Range,
sync::{Arc, LazyLock},
};
use enum_map::{Enum, EnumMap};
+use unicode_linebreak::{linebreaks, BreakOpportunity};
+use unicode_width::UnicodeWidthStr;
+
+use crate::output::pivot::DisplayValue;
use super::{
driver::Driver,
pivot::{Axis2, BorderStyle, Coord2, PivotTable, Rect2, Stroke},
- render::{Device, Draw, DrawCell, Pager, Params},
+ render::{Device, DrawCell, Pager, Params},
table::{CellInner, Content},
text_line::TextLine,
Details, Item,
fn output_table(&mut self, table: &PivotTable) {
for layer_indexes in table.layers(true) {
- let pager = Pager::new(todo!(), table, Some(layer_indexes.as_slice()));
- while pager.has_next() {
+ let mut pager = Pager::new(self, table, Some(layer_indexes.as_slice()));
+ while pager.has_next(self) {
if self.n_objects > 0 {
writeln!(&mut self.file).unwrap();
}
self.n_objects += 1;
- pager.draw_next(usize::MAX, self);
+ pager.draw_next(self, usize::MAX);
+ }
+ }
+ }
+
+ fn layout_cell(&self, cell: &DrawCell, mut text: &str, bb: Rect2) -> Coord2 {
+ if text.is_empty() {
+ return Coord2::default();
+ }
+
+ /*
+ let mut breaks = linebreaks(text);
+ let bb_w = bb[Axis2::X].len();
+ let bb_h = bb[Axis2::Y].len();
+ let mut pos = 0;
+ for _ in 0..bb_h {
+ let mut w = 0;
+ loop {
+ let (index, opportunity) = breaks.next().unwrap();
+ match opportunity {
+ BreakOpportunity::Mandatory => break index,
+ BreakOpportunity::Allowed => {
+ let segment_width = text[pos..index].width();
+ if w > 0 && w + segment_width > bb_w {
+ break index;
+ }
+ pos = index;
+ w += segment_width;
+ }
+ }
+ }
+ todo!()
+ }
+ */
+ todo!()
+ }
+}
+
+struct LineBreaks<'a, B>
+where
+ B: Iterator<Item = (usize, BreakOpportunity)> + Clone + 'a,
+{
+ text: &'a str,
+ max_width: usize,
+ indexes: Range<usize>,
+ width: usize,
+ saved: Option<(usize, BreakOpportunity)>,
+ breaks: B,
+}
+
+impl<'a, B> Iterator for LineBreaks<'a, B>
+where
+ B: Iterator<Item = (usize, BreakOpportunity)> + Clone + 'a,
+{
+ type Item = &'a str;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ while let Some((postindex, opportunity)) = self.saved.take().or_else(|| self.breaks.next())
+ {
+ let index = if postindex != self.text.len() {
+ self.text[..postindex].char_indices().next_back().unwrap().0
+ } else {
+ postindex
+ };
+ println!("index={index} {:?}", &self.text[index..]);
+ if index <= self.indexes.end {
+ dbg!();
+ continue;
+ }
+
+ let segment_width = self.text[self.indexes.end..index].width();
+ if self.width == 0 || self.width + segment_width <= self.max_width {
+ dbg!();
+ // Add this segment to the current line.
+ self.width += segment_width;
+ self.indexes.end = index;
+
+ // If this was a new-line, we're done.
+ if opportunity == BreakOpportunity::Mandatory {
+ dbg!();
+ let segment = self.text[self.indexes.clone()].trim_end_matches('\n');
+ self.indexes = postindex..postindex;
+ self.width = 0;
+ return Some(segment);
+ }
+ } else {
+ // Won't fit. Return what we've got and save this segment for next time.
+ //
+ // We trim trailing spaces from the line we return, and leading
+ // spaces from the position where we resume.
+ dbg!();
+ let segment = self.text[self.indexes.clone()].trim_end();
+
+ let start = self.text[self.indexes.end..].trim_start_matches([' ', '\t']);
+ let start_index = self.text.len() - start.len();
+ self.indexes = start_index..start_index;
+ self.width = 0;
+ self.saved = Some((postindex, opportunity));
+ return Some(segment);
}
}
+ None
+ }
+}
+
+fn new_line_breaks<'a>(
+ text: &'a str,
+ width: usize,
+) -> LineBreaks<'a, impl Iterator<Item = (usize, BreakOpportunity)> + Clone + 'a> {
+ LineBreaks {
+ text,
+ max_width: width,
+ indexes: 0..0,
+ width: 0,
+ saved: None,
+ breaks: linebreaks(text),
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
+
+ use crate::output::text::new_line_breaks;
+
+ #[test]
+ fn unicode_width() {
+ // `\n` is a control character, so [UnicodeWidthChar] considers it to
+ // have no width.
+ assert_eq!('\n'.width(), None);
+
+ // But [UnicodeWidthStr] has a different idea.
+ assert_eq!("\n".width(), 1);
+ assert_eq!("\r\n".width(), 1);
+ }
+
+ #[track_caller]
+ fn test_line_breaks(input: &str, width: usize, expected: Vec<&str>) {
+ let actual = new_line_breaks(input, width).collect::<Vec<_>>();
+ if expected != actual {
+ panic!("filling {input:?} to {width} columns:\nexpected: {expected:?}\nactual: {actual:?}");
+ }
+ }
+ #[test]
+ fn line_breaks() {
+ for width in 0..=6 {
+ test_line_breaks("abc def ghi", width, vec!["abc", "def", "ghi"]);
+ }
+ for width in 7..=10 {
+ test_line_breaks("abc def ghi", width, vec!["abc def", "ghi"]);
+ }
+ test_line_breaks("abc def ghi", 11, vec!["abc def ghi"]);
+
+ for width in 0..=6 {
+ test_line_breaks("abc def ghi", width, vec!["abc", "def", "ghi"]);
+ }
+ test_line_breaks("abc def ghi", 7, vec!["abc", "def ghi"]);
+ for width in 8..=11 {
+ test_line_breaks("abc def ghi", width, vec!["abc def", "ghi"]);
+ }
+ test_line_breaks("abc def ghi", 12, vec!["abc def ghi"]);
+
+ test_line_breaks("abc\ndef\nghi", 2, vec!["abc", "def", "ghi"]);
}
}
}
}
-impl Draw for TextDriver {
+impl Device for TextDriver {
+ fn params(&self) -> &Params {
+ &self.params
+ }
+
+ fn measure_cell_width(&self, cell: &DrawCell) -> [usize; 2] {
+ let text = cell
+ .inner
+ .display(() /* XXX */)
+ .with_font_style(&cell.style.font_style)
+ .with_subscripts(cell.subscripts)
+ .with_footnotes(cell.footnotes)
+ .to_string();
+ let max_width = self.layout_cell(cell, &text, Rect2::new(0..usize::MAX, 0..usize::MAX));
+ let min_width = self.layout_cell(cell, &text, Rect2::new(0..1, 0..usize::MAX));
+ [min_width.x(), max_width.x()]
+ }
+
+ fn measure_cell_height(&self, cell: &DrawCell, width: usize) -> usize {
+ todo!()
+ }
+
+ fn adjust_break(&self, cell: &Content, size: Coord2) -> usize {
+ unreachable!()
+ }
+
fn draw_line(&mut self, bb: Rect2, styles: EnumMap<Axis2, [BorderStyle; 2]>) {
todo!()
}
}
fn scale(&mut self, factor: f64) {
- unreachable!()
- }
-}
-
-impl Device for TextDriver {
- fn params(&self) -> &Params {
- todo!()
- }
-
- fn measure_cell_width(&self, cell: &CellInner) -> [usize; 2] {
- todo!()
- }
-
- fn measure_cell_height(&self, cell: &CellInner, width: usize) -> usize {
- todo!()
- }
-
- fn adjust_break(&self, cell: &Content, size: Coord2) -> usize {
todo!()
}
}