From 68f442079f48544c5786e09773d99eabaaae1fd3 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Wed, 29 Jan 2025 16:23:41 -0800 Subject: [PATCH] render done --- rust/pspp/src/output/csv.rs | 28 ++- rust/pspp/src/output/pivot/mod.rs | 335 +++++++++++++++++---------- rust/pspp/src/output/pivot/output.rs | 164 +++++++------ rust/pspp/src/output/render.rs | 183 ++++++++++++++- rust/pspp/src/output/table.rs | 6 +- rust/pspp/src/settings.rs | 6 +- 6 files changed, 499 insertions(+), 223 deletions(-) diff --git a/rust/pspp/src/output/csv.rs b/rust/pspp/src/output/csv.rs index a8edb849e6..73149f8dd0 100644 --- a/rust/pspp/src/output/csv.rs +++ b/rust/pspp/src/output/csv.rs @@ -8,7 +8,12 @@ use std::{ use crate::output::pivot::Coord2; -use super::{driver::Driver, pivot::PivotTable, table::Table, Details, Item, TextType}; +use super::{ + driver::Driver, + pivot::{PivotTable, ValueOptions}, + table::Table, + Details, Item, TextType, +}; struct CsvDriver { file: File, @@ -119,14 +124,12 @@ impl CsvDriver { let coord = Coord2::new(x, y); let content = table.get(coord); if content.is_top_left() { - if let Some(value) = &content.inner().value { - let display = value.display(Some(pivot_table)); - let s = match leader { - Some(leader) if x == 0 && y == 0 => format!("{leader}: {display}"), - _ => display.to_string(), - }; - write!(&mut self.file, "{}", CsvField::new(&s, self.options))?; - } + let display = content.inner().value.display(pivot_table); + let s = match leader { + Some(leader) if x == 0 && y == 0 => format!("{leader}: {display}"), + _ => display.to_string(), + }; + write!(&mut self.file, "{}", CsvField::new(&s, self.options))?; } } writeln!(&mut self.file)?; @@ -163,7 +166,12 @@ impl Driver for CsvDriver { TextType::Syntax | TextType::PageTitle => (), TextType::Title | TextType::Log => { self.start_item(); - for line in text.content.display(None).to_string().lines() { + for line in text + .content + .display(ValueOptions::default()) + .to_string() + .lines() + { writeln!(&self.file, "{}", CsvField::new(line, self.options)).unwrap(); } } diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index c8317b4626..8d219e99f0 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -592,14 +592,14 @@ pub enum HeadingRegion { #[derive(Clone, Debug)] pub struct AreaStyle { - cell_style: CellStyle, - font_style: FontStyle, + pub cell_style: CellStyle, + pub font_style: FontStyle, } #[derive(Clone, Debug)] pub struct CellStyle { - horz_align: HorzAlign, - vert_align: VertAlign, + pub horz_align: HorzAlign, + pub vert_align: VertAlign, /// Margins in 1/96" units. /// @@ -607,7 +607,7 @@ pub struct CellStyle { /// `margins[Axis2::X][1]` is the right margin. /// `margins[Axis2::Y][0]` is the top margin. /// `margins[Axis2::Y][1]` is the bottom margin. - margins: EnumMap, + pub margins: EnumMap, } #[derive(Copy, Clone, Debug, PartialEq)] @@ -808,6 +808,15 @@ impl Rect2 { use Axis2::*; Coord2::new(self[X].start, self[Y].start) } + pub fn from_fn(f: F) -> Self + where + F: FnMut(Axis2) -> Range, + { + Self(EnumMap::from_fn(f)) + } + pub fn offset(self, offset: Coord2) -> Rect2 { + Self::from_fn(|axis| self[axis].start + offset[axis]..self[axis].end + offset[axis]) + } } impl From>> for Rect2 { @@ -850,6 +859,51 @@ pub enum FootnoteMarkerPosition { Superscript, } +#[derive(Copy, Clone, Debug)] +pub struct ValueOptions { + pub show_values: Option, + + pub show_variables: Option, + + pub small: f64, + + /// Where to put the footnote markers. + pub footnote_marker_type: FootnoteMarkerType, +} + +impl Default for ValueOptions { + fn default() -> Self { + Self { + show_values: None, + show_variables: None, + small: 0.0001, + footnote_marker_type: FootnoteMarkerType::default(), + } + } +} + +pub trait AsValueOptions { + fn as_value_options(self) -> ValueOptions; +} + +impl AsValueOptions for () { + fn as_value_options(self) -> ValueOptions { + ValueOptions::default() + } +} + +impl AsValueOptions for &PivotTable { + fn as_value_options(self) -> ValueOptions { + self.value_options() + } +} + +impl AsValueOptions for ValueOptions { + fn as_value_options(self) -> ValueOptions { + self + } +} + #[derive(Clone, Debug)] pub struct PivotTable { pub look: Arc, @@ -895,7 +949,7 @@ pub struct PivotTable { pub dataset: Option, pub datafile: Option, pub date: Option, - pub footnotes: Vec, + pub footnotes: Vec>, pub title: Option>, pub subtype: Option>, pub corner_text: Option>, @@ -991,6 +1045,15 @@ impl PivotTable { Box::new(once(SmallVec::from_slice(&self.current_layer))) } } + + pub fn value_options(&self) -> ValueOptions { + ValueOptions { + show_values: self.show_values, + show_variables: self.show_variables, + small: self.small, + footnote_marker_type: self.look.footnote_marker_type, + } + } } pub struct Layers {} @@ -1004,46 +1067,30 @@ pub struct Footnote { } impl Footnote { - pub fn display_marker<'a, 'b>( - &'a self, - table: Option<&'b PivotTable>, - ) -> DisplayMarker<'a, 'b> { + pub fn display_marker<'a, 'b>(&'a self, options: impl AsValueOptions) -> DisplayMarker<'a> { DisplayMarker { footnote: self, - table, + options: options.as_value_options(), } } - pub fn display_content<'a, 'b>( - &'a self, - table: Option<&'b PivotTable>, - ) -> DisplayValue<'a, 'b> { - self.content.display(table) + pub fn display_content<'a, 'b>(&'a self, options: impl AsValueOptions) -> DisplayValue<'a> { + self.content.display(options) } } -pub struct DisplayMarker<'a, 'b> { +pub struct DisplayMarker<'a> { footnote: &'a Footnote, - table: Option<&'b PivotTable>, + options: ValueOptions, } -impl<'a, 'b> DisplayMarker<'a, 'b> { - fn marker_type(&self) -> FootnoteMarkerType { - if let Some(table) = self.table { - table.look.footnote_marker_type - } else { - FootnoteMarkerType::default() - } - } -} - -impl<'a, 'b> Display for DisplayMarker<'a, 'b> { +impl<'a> Display for DisplayMarker<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(marker) = &self.footnote.marker { - write!(f, "{}", marker.display(self.table).without_suffixes()) + write!(f, "{}", marker.display(self.options).without_suffixes()) } else { let i = self.footnote.index + 1; - match self.marker_type() { + match self.options.footnote_marker_type { FootnoteMarkerType::Alphabetic => write!(f, "{}", Display26Adic(i)), FootnoteMarkerType::Numeric => write!(f, "{i}"), } @@ -1107,10 +1154,10 @@ impl Display for Display26Adic { /// /// 5. A template. PSPP doesn't create these itself yet, but it can read and /// interpret those created by SPSS. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct Value { - inner: ValueInner, - styling: Option>, + pub inner: ValueInner, + pub styling: Option>, } impl Value { @@ -1128,68 +1175,69 @@ impl Value { } } -pub struct DisplayValue<'a, 'b> { - value: &'a Value, - table: Option<&'b PivotTable>, - - /// Whether to show subscripts and footnotes (which follow the body). - show_suffixes: bool, +pub struct DisplayValue<'a> { + inner: &'a ValueInner, + markup: bool, + subscripts: &'a [String], + footnotes: &'a [Arc], + options: ValueOptions, } -impl<'a, 'b> DisplayValue<'a, 'b> { +impl<'a> DisplayValue<'a> { pub fn without_suffixes(self) -> Self { Self { - show_suffixes: false, + subscripts: &[], + footnotes: &[], ..self } } - fn show(&self) -> (bool, Option<&String>) { - match &self.value.inner { - ValueInner::Number { - value_label: None, .. - } - | ValueInner::String { - value_label: None, .. - } - | ValueInner::Variable { - variable_label: None, - .. - } - | ValueInner::Text { .. } - | ValueInner::Template { .. } => (true, None), + pub fn with_styling(self, styling: &'a ValueStyle) -> Self { + Self { + markup: styling.style.font_style.markup, + subscripts: styling.subscripts.as_slice(), + footnotes: styling.footnotes.as_slice(), + ..self + } + } - ValueInner::Number { - show, - value_label: Some(label), - .. - } - | ValueInner::String { - show, - value_label: Some(label), - .. - } => interpret_show( + pub fn with_font_style(self, font_style: &FontStyle) -> Self { + Self { + markup: font_style.markup, + ..self + } + } + + pub fn with_subscripts(self, subscripts: &'a [String]) -> Self { + Self { subscripts, ..self } + } + + pub fn with_footnotes(self, footnotes: &'a [Arc]) -> Self { + Self { footnotes, ..self } + } + + fn show(&self) -> (bool, Option<&str>) { + if let Some(value_label) = self.inner.value_label() { + interpret_show( || Settings::global().show_values, - || self.table.map_or(None, |table| table.show_values), - *show, - label, - ), - - ValueInner::Variable { - show, - variable_label: Some(label), - .. - } => interpret_show( + self.options.show_values, + self.inner.show(), + value_label, + ) + } else if let Some(variable_label) = self.inner.variable_label() { + interpret_show( || Settings::global().show_variables, - || self.table.map_or(None, |table| table.show_variables), - *show, - label, - ), + self.options.show_variables, + self.inner.show(), + variable_label, + ) + } else { + (true, None) } } fn small(&self) -> f64 { - self.table.map_or(0.0, |table| table.small) + self.options.small } fn template( @@ -1213,7 +1261,7 @@ impl<'a, 'b> DisplayValue<'a, 'b> { continue; }; if let Some(arg) = arg.get(0) { - write!(f, "{}", arg.display(self.table))?; + write!(f, "{}", arg.display(self.options))?; } } b'[' => { @@ -1269,7 +1317,7 @@ impl<'a, 'b> DisplayValue<'a, 'b> { continue; }; args_consumed = args_consumed.max(index); - write!(f, "{}", arg.display(self.table))?; + write!(f, "{}", arg.display(self.options))?; } c => write!(f, "{c}")?, } @@ -1300,21 +1348,21 @@ fn extract_inner_template(input: &[u8]) -> (&[u8], &[u8]) { fn interpret_show( global_show: impl Fn() -> Show, - table_show: impl Fn() -> Option, + table_show: Option, value_show: Option, - label: &String, -) -> (bool, Option<&String>) { - match value_show.or_else(table_show).unwrap_or_else(global_show) { + label: &str, +) -> (bool, Option<&str>) { + match value_show.or(table_show).unwrap_or_else(global_show) { Show::Value => (true, None), Show::Label => (false, Some(label)), Show::Both => (true, Some(label)), } } -impl<'a, 'b> Display for DisplayValue<'a, 'b> { +impl<'a, 'b> Display for DisplayValue<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let (show_value, label) = self.show(); - match &self.value.inner { + match self.inner { ValueInner::Number { format, honor_small, @@ -1358,36 +1406,29 @@ impl<'a, 'b> Display for DisplayValue<'a, 'b> { } ValueInner::Text { local, .. } => { + /* if self - .value + .inner .styling .as_ref() - .is_some_and(|styling| styling.font_style.markup) + .is_some_and(|styling| styling.style.font_style.markup) { todo!(); - } + }*/ f.write_str(&local) } ValueInner::Template { args, local, .. } => self.template(f, &local, args), + + ValueInner::Empty => Ok(()), }?; - if self.show_suffixes { - if let Some(styling) = &self.value.styling { - for (subscript, delimiter) in - styling.subscripts.iter().zip(once('_').chain(repeat(','))) - { - write!(f, "{delimiter}{subscript}")?; - } + for (subscript, delimiter) in self.subscripts.iter().zip(once('_').chain(repeat(','))) { + write!(f, "{delimiter}{subscript}")?; + } - for footnote_index in styling.footnote_indexes.iter().copied() { - if let Some(table) = self.table { - if let Some(footnote) = table.footnotes.get(footnote_index) { - write!(f, "[{}]", footnote.display_marker(self.table))?; - } - } - } - } + for footnote in self.footnotes { + write!(f, "[{}]", footnote.display_marker(self.options))?; } Ok(()) @@ -1396,19 +1437,18 @@ impl<'a, 'b> Display for DisplayValue<'a, 'b> { impl Value { // Returns an object that will format this value, including subscripts and - // superscripts and footnotes. Settings on `table` control whether variable - // and value labels are included; if `table` is not provided, then defaults - // are used. `table` is also needed to display footnote markers. - pub fn display<'a, 'b>(&'a self, table: Option<&'b PivotTable>) -> DisplayValue<'a, 'b> { - DisplayValue { - value: self, - table, - show_suffixes: true, + // superscripts and footnotes. `options` controls whether variable and + // value labels are included. + pub fn display<'a, 'b>(&'a self, options: impl AsValueOptions) -> DisplayValue<'a> { + let display = self.inner.display(options.as_value_options()); + match &self.styling { + Some(styling) => display.with_styling(&*styling), + None => display, } } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub enum ValueInner { Number { show: Option, @@ -1447,12 +1487,67 @@ pub enum ValueInner { local: String, id: String, }, + + #[default] + Empty, +} + +impl ValueInner { + pub fn is_empty(&self) -> bool { + match self { + Self::Empty => true, + _ => false, + } + } + fn show(&self) -> Option { + match self { + ValueInner::Number { show, .. } + | ValueInner::String { show, .. } + | ValueInner::Variable { show, .. } => *show, + _ => None, + } + } + + fn label(&self) -> Option<&str> { + self.value_label().or_else(|| self.variable_label()) + } + + fn value_label(&self) -> Option<&str> { + match self { + ValueInner::Number { value_label, .. } | ValueInner::String { value_label, .. } => { + value_label.as_ref().map(String::as_str) + } + _ => None, + } + } + + fn variable_label(&self) -> Option<&str> { + match self { + ValueInner::Variable { variable_label, .. } => { + variable_label.as_ref().map(String::as_str) + } + _ => None, + } + } } #[derive(Clone, Debug)] pub struct ValueStyle { - font_style: FontStyle, - cell_style: CellStyle, - subscripts: Vec, - footnote_indexes: Vec, + pub style: AreaStyle, + pub subscripts: Vec, + pub footnotes: Vec>, +} + +impl ValueInner { + // Returns an object that will format this value. Settings on `options` + // control whether variable and value labels are included. + pub fn display<'a>(&'a self, options: impl AsValueOptions) -> DisplayValue<'a> { + DisplayValue { + inner: &self, + markup: false, + subscripts: &[], + footnotes: &[], + options: options.as_value_options(), + } + } } diff --git a/rust/pspp/src/output/pivot/output.rs b/rust/pspp/src/output/pivot/output.rs index 28e62c85bf..eadf61c06c 100644 --- a/rust/pspp/src/output/pivot/output.rs +++ b/rust/pspp/src/output/pivot/output.rs @@ -116,13 +116,34 @@ impl PivotTable { } } - fn create_aux_table(&self, n: Coord2) -> Table { - Table::new( - n, + fn create_aux_table3(&self, area: Area, rows: I) -> Table + where + I: Iterator> + ExactSizeIterator, + { + let mut table = Table::new( + Coord2::new(0, rows.len()), Coord2::new(0, 0), self.look.areas.clone(), self.borders(false), - ) + ); + for (y, row) in rows.enumerate() { + table.put( + Rect2::for_cell(Coord2::new(0, y)), + CellInner::new(area, row), + ); + } + table + } + + fn create_aux_table_if_nonempty(&self, area: Area, rows: I) -> Option + where + I: Iterator> + ExactSizeIterator, + { + if rows.len() > 0 { + Some(self.create_aux_table3(area, rows)) + } else { + None + } } fn borders(&self, printing: bool) -> EnumMap { @@ -131,7 +152,7 @@ impl PivotTable { }) } - pub fn output(&self, layer_indexes: &[usize], printing: bool) -> OutputTables { + pub fn output_body(&self, layer_indexes: &[usize], printing: bool) -> Table { let column_enumeration = self.enumerate_axis(Axis3::X, layer_indexes, self.look.omit_empty); let row_enumeration = self.enumerate_axis(Axis3::Y, layer_indexes, self.look.omit_empty); let data = Coord2::new(column_enumeration.len(), row_enumeration.len()); @@ -182,19 +203,18 @@ impl PivotTable { CellInner { rotate: false, area: Area::Data, - value: value.map(|value| Box::new(value.clone())), + value: Box::new(value.cloned().unwrap_or_default()), }, ); } } - if (self.corner_text.is_some() || self.look.row_labels_in_corner) && stub.x() > 0 && stub.y() > 0 { body.put( Rect2::new(0..stub.x(), 0..stub.y()), - CellInner::new(Area::Corner, self.corner_text.clone()), + CellInner::new(Area::Corner, self.corner_text.clone().unwrap_or_default()), ); } @@ -217,74 +237,57 @@ impl PivotTable { body.v_line(Border::DataLeft, stub.x(), 0..body.n.y()); } } + body + } - // Title. - let title = if self.title.is_some() && self.show_title { - let mut title = self.create_aux_table(Coord2::new(0, 0)); - title.put( - Rect2::new(0..1, 0..1), - CellInner::new(Area::Title, self.title.clone()), - ); - Some(title) - } else { - None - }; + pub fn output_title(&self) -> Option
{ + Some(self.create_aux_table3(Area::Title, [self.title.as_ref()?.clone()].into_iter())) + } - // Layers. - let n_layers: usize = self.nonempty_layer_dimensions().count(); - let layers = if n_layers > 0 { - let mut layers = self.create_aux_table(Coord2::new(1, n_layers)); - for (y, dimension) in self.nonempty_layer_dimensions().enumerate() { - layers.put( - Rect2::for_cell(Coord2::new(0, y)), - CellInner::new( - Area::Layers, - Some(dimension.data_leaves[layer_indexes[y]].name.clone()), - ), - ); - } - Some(layers) - } else { - None - }; + pub fn output_layers(&self, layer_indexes: &[usize]) -> Option
{ + self.create_aux_table_if_nonempty( + Area::Layers, + self.nonempty_layer_dimensions() + .collect::>() + .into_iter() + .enumerate() + .map(|(i, dimension)| dimension.data_leaves[layer_indexes[i]].name.clone()), + ) + } - // Caption. - let caption = if self.caption.is_some() && self.show_caption { - let mut caption = self.create_aux_table(Coord2::new(1, 1)); - caption.put( - Rect2::for_cell(Coord2::new(0, 0)), - CellInner::new(Area::Caption, self.caption.clone()), - ); - Some(caption) - } else { - None - }; + pub fn output_caption(&self) -> Option
{ + Some(self.create_aux_table3(Area::Caption, [self.caption.as_ref()?.clone()].into_iter())) + } - // Footnotes. - let f = self.collect_footnotes(&[ + pub fn output_footnotes<'a>(&self, footnotes: &[Arc]) -> Option
{ + self.create_aux_table_if_nonempty( + Area::Footer, + footnotes.into_iter().map(|f| { + Box::new(Value::new_user_text(format!( + "{}. {}", + f.display_marker(self), + f.display_content(self) + ))) + }), + ) + } + + pub fn output(&self, layer_indexes: &[usize], printing: bool) -> OutputTables { + // Produce most of the tables. + let title = self.show_title.then(|| self.output_title()).flatten(); + let layers = self.output_layers(layer_indexes); + let body = self.output_body(layer_indexes, printing); + let caption = self.show_caption.then(|| self.output_caption()).flatten(); + + // Then collect the footnotes from those tables. + let tables = [ title.as_ref(), layers.as_ref(), Some(&body), caption.as_ref(), - ]); - let footnotes = if !f.is_empty() { - let mut footnotes = self.create_aux_table(Coord2::new(1, f.len())); - for (y, f) in f.into_iter().enumerate() { - let s = format!( - "{}. {}", - f.display_marker(Some(self)), - f.display_content(Some(self)) - ); - let value = Some(Box::new(Value::new_user_text(s))); - footnotes.put( - Rect2::for_cell(Coord2::new(0, y)), - CellInner::new(Area::Footer, value), - ); - } - Some(footnotes) - } else { - None - }; + ]; + let footnotes = + self.output_footnotes(&self.collect_footnotes(tables.into_iter().flatten())); OutputTables { title, @@ -303,27 +306,22 @@ impl PivotTable { .filter(|d| !d.data_leaves.is_empty()) } - fn collect_footnotes<'a>(&'a self, tables: &[Option<&Table>]) -> Vec<&'a Footnote> { + fn collect_footnotes<'a>(&self, tables: impl Iterator) -> Vec> { if self.footnotes.is_empty() { return Vec::new(); } - let mut refs = vec![false; self.footnotes.len()]; - for table in tables.into_iter().flatten() { + let mut refs = Vec::with_capacity(self.footnotes.len()); + for table in tables { for cell in table.cells() { - if let Some(value) = &cell.inner().value { - if let Some(styling) = &value.styling { - for index in &styling.footnote_indexes { - refs[*index] = true; - } - } + if let Some(styling) = &cell.inner().value.styling { + refs.extend(styling.footnotes.iter().cloned()); } } } - refs.iter() - .enumerate() - .filter_map(|(index, r)| (*r).then_some(&self.footnotes[index])) - .collect() + refs.sort_by(|a, b| a.index.cmp(&b.index)); + refs.dedup_by(|a, b| a.index == b.index); + refs } } @@ -480,7 +478,7 @@ fn compose_headings( rotate: (rotate_inner_labels && is_inner_row) || (rotate_outer_labels && is_outer_row), area, - value: Some(Box::new(c.name().clone())), + value: Box::new(c.name().clone()), }, ); @@ -546,7 +544,7 @@ fn compose_headings( if d.root.show_label_in_corner && h_ofs > 0 { table.put( Rect2::for_ranges((h, 0..h_ofs), top_row..top_row + d.label_depth), - CellInner::new(Area::Corner, Some(d.root.name.clone())), + CellInner::new(Area::Corner, d.root.name.clone()), ); } diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index 318a757415..7eed58f130 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -6,10 +6,15 @@ use std::sync::Arc; use enum_map::EnumMap; use itertools::interleave; +use num::Integer; use smallvec::SmallVec; -use super::pivot::{Axis2, BorderStyle, Coord2, Look, PivotTable, Rect2, Stroke}; -use super::table::{Cell, CellInner, Content, Table}; +use crate::output::pivot::VertAlign; + +use super::pivot::{ + AreaStyle, Axis2, BorderStyle, Coord2, Footnote, Look, PivotTable, Rect2, Stroke, ValueInner, +}; +use super::table::{CellInner, Content, Table}; /// Parameters for rendering a table_item to a device. /// @@ -66,6 +71,16 @@ pub struct Params { can_scale: bool, } +/* +pub struct DeviceCell { + /// Rotate cell contents 90 degrees? + rotate: bool, + + /// Value to render. + value: &ValueInner, + + +}*/ pub trait Device { fn params(&self) -> &Params; @@ -90,7 +105,9 @@ pub trait Device { /// 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; +} +trait Draw { /// Draws a generalized intersection of lines in `bb`. /// /// `styles` is interpreted this way: @@ -114,8 +131,8 @@ pub trait Device { /// alignment itself. fn draw_cell( &mut self, - cell: &Cell, - color_idx: usize, + draw_cell: &DrawCell, + alternate_row: bool, bb: &Rect2, valign_offset: usize, spill: EnumMap, @@ -131,6 +148,14 @@ pub trait Device { fn scale(&mut self, factor: f64); } +struct DrawCell<'a> { + pub rotate: bool, + pub inner: &'a ValueInner, + pub style: &'a AreaStyle, + pub subscripts: &'a [String], + pub footnotes: &'a [Arc], +} + /// 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 @@ -309,6 +334,11 @@ fn cell_width(cp: &[usize], z: usize) -> usize { 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: Rect2, @@ -784,9 +814,115 @@ impl Page { }) } - fn table_width(&self, axis: Axis2) -> usize { + fn total_size(&self, axis: Axis2) -> usize { self.cp[axis].last().copied().unwrap() } + + fn draw(&self, draw: &mut dyn Draw, ofs: Coord2) { + use Axis2::*; + self.draw_cells( + 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) { + 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); + x = rule_ofs(cell.rect[X].end); + } else { + x += 1; + } + } + } + + /* + for y in cells[Y] { + for x in cells[X] { + if is_rule(x) && is_rule(y) { + self.draw_rule(ofs, draw, Coord2::new(x, y)); + } + } + }*/ + } + + fn extra_height(&self, bb: &Rect2, inner: &CellInner) -> usize { + use Axis2::*; + let height = self.device.measure_cell_height(inner, bb[X].len()); + usize::saturating_sub(bb[Y].len(), height) + } + fn draw_cell(&self, ofs: Coord2, draw: &mut dyn Draw, 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] + }) + .offset(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 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]; + } + } + + clip + }) + } else { + 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) + } } struct Selection { @@ -1218,7 +1354,7 @@ impl Pager { 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)) + .map(|page: &Arc| page.total_size(Axis2::Y)) .sum::() as f64; let max_height = device.params().size[Axis2::Y] as f64; if total_height * scale >= max_height { @@ -1260,4 +1396,39 @@ impl Pager { } false } + + /// Draws a chunk of content to fit in a space that has vertical size + /// `space` and the horizontal size specified in the device parameters. + /// 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). + fn draw_next(&mut self, mut space: usize, draw: &mut dyn Draw) -> usize { + use Axis2::*; + + if self.scale != 1.0 { + draw.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()) { + n_pages = Some(self.pages.len()); + + let Some(page) = self + .y_break + .as_mut() + .and_then(|y_break| y_break.next(space - ofs[Y])) + else { + break; + }; + page.draw(draw, ofs); + ofs[Y] += page.total_size(Y); + } + + if self.scale != 1.0 { + ofs[Y] = (ofs[Y] as f64 * self.scale) as usize; + } + ofs[Y] + } } diff --git a/rust/pspp/src/output/table.rs b/rust/pspp/src/output/table.rs index 4060f05b45..e58e35e693 100644 --- a/rust/pspp/src/output/table.rs +++ b/rust/pspp/src/output/table.rs @@ -145,11 +145,11 @@ pub struct CellInner { /// The area that the cell belongs to. pub area: Area, - pub value: Option>, + pub value: Box, } impl CellInner { - pub fn new(area: Area, value: Option>) -> Self { + pub fn new(area: Area, value: Box) -> Self { Self { rotate: false, area, @@ -158,7 +158,7 @@ impl CellInner { } pub fn is_empty(&self) -> bool { - self.value.is_none() + self.value.inner.is_empty() } } diff --git a/rust/pspp/src/settings.rs b/rust/pspp/src/settings.rs index 93b8df2e41..20933aba05 100644 --- a/rust/pspp/src/settings.rs +++ b/rust/pspp/src/settings.rs @@ -12,14 +12,18 @@ use crate::{ /// name. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum Show { - /// Value or variable name only. + /// Value (or variable name) only. Value, /// Label only. + /// + /// The value will be shown if no label is available. #[default] Label, /// Value (or variable name) and label. + /// + /// Just the value will be shown, if no label is available. Both, } -- 2.30.2