render done
authorBen Pfaff <blp@cs.stanford.edu>
Thu, 30 Jan 2025 00:23:41 +0000 (16:23 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 30 Jan 2025 00:23:41 +0000 (16:23 -0800)
rust/pspp/src/output/csv.rs
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/pivot/output.rs
rust/pspp/src/output/render.rs
rust/pspp/src/output/table.rs
rust/pspp/src/settings.rs

index a8edb849e6e35596ba671cbffa21a1dd501b0d8a..73149f8dd0036db2472f0d396f8085faa60b45f6 100644 (file)
@@ -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();
                     }
                 }
index c8317b462686b5b7ba86bd0a5739f01c5096511f..8d219e99f0f02f036c8f32b47887547116b80637 100644 (file)
@@ -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<Axis2, [i32; 2]>,
+    pub margins: EnumMap<Axis2, [i32; 2]>,
 }
 
 #[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: F) -> Self
+    where
+        F: FnMut(Axis2) -> Range<usize>,
+    {
+        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<EnumMap<Axis2, Range<usize>>> for Rect2 {
@@ -850,6 +859,51 @@ pub enum FootnoteMarkerPosition {
     Superscript,
 }
 
+#[derive(Copy, Clone, Debug)]
+pub struct ValueOptions {
+    pub show_values: Option<Show>,
+
+    pub show_variables: Option<Show>,
+
+    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<Look>,
@@ -895,7 +949,7 @@ pub struct PivotTable {
     pub dataset: Option<String>,
     pub datafile: Option<String>,
     pub date: Option<NaiveDateTime>,
-    pub footnotes: Vec<Footnote>,
+    pub footnotes: Vec<Arc<Footnote>>,
     pub title: Option<Box<Value>>,
     pub subtype: Option<Box<Value>>,
     pub corner_text: Option<Box<Value>>,
@@ -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<Box<ValueStyle>>,
+    pub inner: ValueInner,
+    pub styling: Option<Box<ValueStyle>>,
 }
 
 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<Footnote>],
+    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<Footnote>]) -> 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<Show>,
+    table_show: Option<Show>,
     value_show: Option<Show>,
-    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<Show>,
@@ -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<Show> {
+        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<String>,
-    footnote_indexes: Vec<usize>,
+    pub style: AreaStyle,
+    pub subscripts: Vec<String>,
+    pub footnotes: Vec<Arc<Footnote>>,
+}
+
+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(),
+        }
+    }
 }
index 28e62c85bf55ed3f3165cd86abf0fcaf81b207ea..eadf61c06c0ec337a977621f8dccc51743d16290 100644 (file)
@@ -116,13 +116,34 @@ impl PivotTable {
         }
     }
 
-    fn create_aux_table(&self, n: Coord2) -> Table {
-        Table::new(
-            n,
+    fn create_aux_table3<I>(&self, area: Area, rows: I) -> Table
+    where
+        I: Iterator<Item = Box<Value>> + 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<I>(&self, area: Area, rows: I) -> Option<Table>
+    where
+        I: Iterator<Item = Box<Value>> + ExactSizeIterator,
+    {
+        if rows.len() > 0 {
+            Some(self.create_aux_table3(area, rows))
+        } else {
+            None
+        }
     }
 
     fn borders(&self, printing: bool) -> EnumMap<Border, BorderStyle> {
@@ -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<Table> {
+        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<Table> {
+        self.create_aux_table_if_nonempty(
+            Area::Layers,
+            self.nonempty_layer_dimensions()
+                .collect::<Vec<_>>()
+                .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<Table> {
+        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<Footnote>]) -> Option<Table> {
+        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<Item = &'a Table>) -> Vec<Arc<Footnote>> {
         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()),
                 );
             }
 
index 318a7574155be3245d4f3f27fdff019594020776..7eed58f1307497908bb7b537814721b2ccbd9f30 100644 (file)
@@ -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<Axis2, [usize; 2]>,
@@ -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<Footnote>],
+}
+
 /// 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>| page.table_width(Axis2::Y))
+                .map(|page: &Arc<Page>| page.total_size(Axis2::Y))
                 .sum::<usize>() 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]
+    }
 }
index 4060f05b45d8c25b926c28262de2188bffdb7aa1..e58e35e6934540fcdc2aa032d5c3e929ca279c2c 100644 (file)
@@ -145,11 +145,11 @@ pub struct CellInner {
     /// The area that the cell belongs to.
     pub area: Area,
 
-    pub value: Option<Box<Value>>,
+    pub value: Box<Value>,
 }
 
 impl CellInner {
-    pub fn new(area: Area, value: Option<Box<Value>>) -> Self {
+    pub fn new(area: Area, value: Box<Value>) -> 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()
     }
 }
 
index 93b8df2e41373f069e8dfc4a544e684432c63e71..20933aba0535fa59ffadf6660013b9e8ad9fc469 100644 (file)
@@ -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,
 }