From 1d150415f7d5aa36f190900f11b37ad7b1a3e134 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Wed, 9 Apr 2025 14:04:04 -0700 Subject: [PATCH] pivot table text driver working better --- rust/pspp/src/output/pivot/mod.rs | 27 +- rust/pspp/src/output/pivot/output.rs | 475 +++++++++++++++------------ rust/pspp/src/output/pivot/test.rs | 23 +- rust/pspp/src/output/text.rs | 66 +++- rust/pspp/src/output/text_line.rs | 17 +- 5 files changed, 377 insertions(+), 231 deletions(-) diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index 7ce93dab3f..f582d18376 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -386,6 +386,12 @@ pub struct Group { pub show_label: Option, } +impl Group { + pub fn parent(&self) -> Option> { + self.parent.as_ref().map(|parent| parent.upgrade().unwrap()) + } +} + #[derive(Clone)] pub struct DimensionBuilder { axis: Axis3, @@ -693,6 +699,9 @@ impl Leaf { ..self } } + pub fn ancestors(&self) -> impl Iterator> { + std::iter::successors(self.parent(), |group| group.parent()) + } } /// Pivot result classes. @@ -787,7 +796,7 @@ impl CategoryTrait for Leaf { } fn parent(&self) -> Option> { - self.parent.upgrade() + Some(self.parent.upgrade().unwrap()) } } @@ -1067,16 +1076,16 @@ pub enum VertAlign { #[derive(Clone, Debug)] pub struct FontStyle { - bold: bool, - italic: bool, - underline: bool, - markup: bool, - font: String, - fg: [Color; 2], - bg: [Color; 2], + pub bold: bool, + pub italic: bool, + pub underline: bool, + pub markup: bool, + pub font: String, + pub fg: [Color; 2], + pub bg: [Color; 2], /// In 1/72" units. - size: i32, + pub size: i32, } #[derive(Copy, Clone, PartialEq, Eq)] diff --git a/rust/pspp/src/output/pivot/output.rs b/rust/pspp/src/output/pivot/output.rs index f094fcd3b6..706e7f46ac 100644 --- a/rust/pspp/src/output/pivot/output.rs +++ b/rust/pspp/src/output/pivot/output.rs @@ -2,10 +2,10 @@ use std::{ops::Range, sync::Arc}; use enum_map::{enum_map, EnumMap}; use itertools::Itertools; -use smallvec::{SmallVec, ToSmallVec}; +use smallvec::SmallVec; use crate::output::{ - pivot::LabelPosition, + pivot::{Group, LabelPosition, Leaf}, table::{CellInner, Table}, }; @@ -48,13 +48,12 @@ struct AxisEnumerationIter<'a> { } impl<'a> Iterator for AxisEnumerationIter<'a> { - type Item = SmallVec<[usize; 4]>; + type Item = &'a [usize]; fn next(&mut self) -> Option { if self.position < self.enumeration.indexes.len() { - let item = (&self.enumeration.indexes - [self.position..self.position + self.enumeration.stride]) - .to_smallvec(); + let item = + &self.enumeration.indexes[self.position..self.position + self.enumeration.stride]; self.position += self.enumeration.stride; Some(item) } else { @@ -159,12 +158,13 @@ impl PivotTable { 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 column_headings = Headings::new(self, Axis2::X, &column_enumeration); + let row_enumeration = self.enumerate_axis(Axis3::Y, layer_indexes, self.look.omit_empty); + let row_headings = Headings::new(self, Axis2::Y, &row_enumeration); + let data = Coord2::new(column_enumeration.len(), row_enumeration.len()); - let stub = Coord2::new( - self.axis_label_depth(Axis3::Y), - self.axis_label_depth(Axis3::X), - ); + let stub = Coord2::new(row_headings.height(), column_headings.height()); let n = EnumMap::from_fn(|axis| data[axis] + stub[axis]).into(); let mut body = Table::new( n, @@ -173,22 +173,19 @@ impl PivotTable { self.borders(printing), self.as_value_options(), ); - compose_headings( - self, + + column_headings.render( &mut body, - Axis2::X, - &column_enumeration, + row_headings.height(), RowColBorder::ColHorz, RowColBorder::ColVert, self.rotate_outer_row_labels, false, Area::ColumnLabels, ); - compose_headings( - self, + row_headings.render( &mut body, - Axis2::Y, - &row_enumeration, + column_headings.height(), RowColBorder::RowVert, RowColBorder::RowHorz, false, @@ -356,114 +353,87 @@ fn find_category<'a>( Some(c) } -/// Fills row or column headings into `table`. -/// -/// This function uses terminology and variable names for column headings, but -/// it also applies to row headings because it uses variables for the -/// differences, e.g. when for column headings it would use the H axis, it -/// instead uses 'h', which is set to H for column headings and V for row -/// headings. -fn compose_headings( - pt: &PivotTable, - table: &mut Table, - h: Axis2, - column_enumeration: &AxisEnumeration, - col_horz: RowColBorder, - col_vert: RowColBorder, - rotate_inner_labels: bool, - rotate_outer_labels: bool, - area: Area, -) { - let v = !h; - let h_axis = &pt.axes[h.into()]; - let v_size = pt.axis_label_depth(h.into()); - let h_ofs = pt.axis_label_depth(v.into()); - let n_columns = column_enumeration.len(); - - if h_axis.dimensions.is_empty() || n_columns == 0 || v_size == 0 { - return; +struct HeadingColumn<'a> { + leaf: &'a Leaf, + groups: SmallVec<[Arc; 4]>, +} + +impl<'a> HeadingColumn<'a> { + pub fn get(&self, y: usize, height: usize) -> Option<&Value> { + if y + 1 == height { + Some(&self.leaf.name) + } else { + self.groups.get(y).map(|group| &*group.name) + } } +} - // Below, we're going to iterate through the dimensions. Each dimension - // occupies one or more rows in the heading. `top_row` is the top row of - // these (and `top_row + d->label_depth - 1` is the bottom row). - let mut top_row = 0; - - // We're going to iterate through dimensions and the rows that label them - // from top to bottom (from outer to inner dimensions). As we move - // downward, we start drawing vertical rules to separate categories and - // groups. After we start drawing a vertical rule in a particular - // horizontal position, it continues until the bottom of the heading. - // vrules[pos] indicates whether, in our current row, we have already - // started drawing a vertical rule in horizontal position `pos`. (There are - // n_columns + 1 horizontal positions. We allocate all of them for - // convenience below but only the inner `n_columns - 1` of them really - // matter.) - // - // Here's an example that shows how vertical rules continue all the way - // downward: - // - // ```text - // +-----------------------------------------------------+ __ - // | bbbb | | - // +-----------------+-----------------+-----------------+ |dimension "bbbb" - // | bbbb1 | bbbb2 | bbbb3 | _| - // +-----------------+-----------------+-----------------+ __ - // | aaaa | aaaa | aaaa | | - // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ |dimension "aaaa" - // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| _| - // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ - // - // ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ - // | | | | | | | | | | - // 0 1 2 3 4 5 6 7 8 9 - // |___________________vrules[] indexes__________________| - // ``` - // - // Our data structures are more naturally iterated from bottom to top (inner - // to outer dimensions). A previous version of this code actually worked - // like that, but it didn't draw all of the vertical lines correctly as - // shown above. It ended up rendering the above heading much like shown - // below, which isn't what users expect. The "aaaa" label really needs to - // be shown three times for clarity: - // - // ```text - // +-----------------------------------------------------+ - // | bbbb | - // +-----------------+-----------------+-----------------+ - // | bbbb1 | bbbb2 | bbbb3 | - // +-----------------+-----------------+-----------------+ - // | | aaaa | | - // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ - // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| - // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ - // ``` - let mut vrules = vec![false; n_columns + 1]; - vrules[0] = true; - vrules[n_columns] = true; - - for (dim_index, d) in pt - .axis_dimensions(h.into()) - .enumerate() - .rev() - .filter(|(_, d)| !d.hide_all_labels) - { - for row_ofs in 0..d.label_depth() { +struct Heading<'a> { + dimension: &'a Dimension, + height: usize, + columns: Vec>, +} + +impl<'a> Heading<'a> { + fn new( + dimension: &'a Dimension, + dim_index: usize, + column_enumeration: &AxisEnumeration, + ) -> Option { + if dimension.hide_all_labels { + return None; + } + + let mut columns = Vec::new(); + let mut height = 0; + for indexes in column_enumeration.iter() { + let leaf = &*dimension.data_leaves[dimension.presentation_order[indexes[dim_index]]]; + let mut groups = leaf + .ancestors() + .filter(|group| group.show_label.is_some()) + .collect::>(); + groups.reverse(); + height = height.max(1 + groups.len()); + columns.push(HeadingColumn { leaf, groups }); + } + + Some(Self { + dimension, + height, + columns, + }) + } + + fn width(&self) -> usize { + self.columns.len() + } + + fn render( + &self, + table: &mut Table, + vrules: &mut [bool], + h: Axis2, + h_ofs: usize, + v_ofs: usize, + column_enumeration: &AxisEnumeration, + col_horz: RowColBorder, + col_vert: RowColBorder, + rotate_inner_labels: bool, + rotate_outer_labels: bool, + area: Area, + ) { + let v = !h; + + for row in 0..self.height { // Find all the categories, dropping columns without a category. - let categories = (0..n_columns).filter_map(|x| { - find_category( - d, - dim_index, - column_enumeration.get(x), - d.label_depth() - row_ofs - 1, - ) - .map(|c| (x..x + 1, c)) + let categories = self.columns.iter().enumerate().filter_map(|(x, column)| { + column.get(row, self.height).map(|name| (x..x + 1, name)) }); // Merge adjacent identical categories (but don't merge across a vertical rule). let categories = categories .coalesce(|(a_r, a), (b_r, b)| { - if !vrules[b_r.start] && a.ptr_eq(&b) { + if !vrules[b_r.start] && std::ptr::eq(a, b) { Ok((a_r.start..b_r.end, a)) } else { Err(((a_r, a), (b_r, b))) @@ -471,106 +441,191 @@ fn compose_headings( }) .collect::>(); - for (Range { start: x1, end: x2 }, c) in categories { - let y1 = top_row + row_ofs; - let y2 = y1 + c.extra_depth() + 1; - let is_outer_row = y1 == 0; - let is_inner_row = y2 == v_size; - if c.show_label() { - table.put( - Rect2::for_ranges((h, x1 + h_ofs..x2 + h_ofs), y1..y2), - CellInner { - rotate: (rotate_inner_labels && is_inner_row) - || (rotate_outer_labels && is_outer_row), - area, - value: Box::new(c.name().clone()), + for (Range { start: x1, end: x2 }, name) in categories { + let y1 = v_ofs + row; + let y2 = y1 + 1; + table.put( + Rect2::for_ranges((h, x1 + h_ofs..x2 + h_ofs), y1..y2), + CellInner { + rotate: { + let is_outer_row = y1 == 0; + let is_inner_row = y2 == self.height; + (rotate_inner_labels && is_inner_row) + || (rotate_outer_labels && is_outer_row) }, - ); - - // Draw all the vertical lines in our running example, other - // than the far left and far right ones. Only the ones that - // start in the last row of the heading are drawn with the - // "category" style, the rest with the "dimension" style, - // e.g. only the # below are category style: - // - // ```text - // +-----------------------------------------------------+ - // | bbbb | - // +-----------------+-----------------+-----------------+ - // | bbbb1 | bbbb2 | bbbb3 | - // +-----------------+-----------------+-----------------+ - // | aaaa | aaaa | aaaa | - // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ - // |aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3| - // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ - // ``` - let border = if y1 == v_size - 1 { - Border::Categories(col_vert) - } else { - Border::Dimensions(col_vert) - }; - if !vrules[x2] { - table.draw_line(border, (v, x2 + h_ofs), y1..table.n[v]); - vrules[x2] = true; - } - if !vrules[x1] { - table.draw_line(border, (v, x1 + h_ofs), y1..table.n[v]); - vrules[x1] = true; - } + area, + value: Box::new(name.clone()), + }, + ); - // Draws the horizontal lines within a dimension, that is, - // those that separate a category (or group) from its parent - // group or dimension's label. Our running example doesn't - // have groups but the `====` lines below show the - // separators between categories and their dimension label: - // - // ```text - // +-----------------------------------------------------+ - // | bbbb | - // +=================+=================+=================+ - // | bbbb1 | bbbb2 | bbbb3 | - // +-----------------+-----------------+-----------------+ - // | aaaa | aaaa | aaaa | - // +=====+=====+=====+=====+=====+=====+=====+=====+=====+ - // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| - // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ - // ``` - if c.parent().is_some_and(|parent| parent.show_label.is_some()) { - table.draw_line(Border::Categories(col_horz), (h, y1), h_ofs..table.n[h]); - } + // Draw all the vertical lines in our running example, other + // than the far left and far right ones. Only the ones that + // start in the last row of the heading are drawn with the + // "category" style, the rest with the "dimension" style, + // e.g. only the # below are category style: + // + // ```text + // +-----------------------------------------------------+ + // | bbbb | + // +-----------------+-----------------+-----------------+ + // | bbbb1 | bbbb2 | bbbb3 | + // +-----------------+-----------------+-----------------+ + // | aaaa | aaaa | aaaa | + // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ + // |aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3| + // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ + // ``` + let border = if y1 == self.height - 1 { + Border::Categories(col_vert) + } else { + Border::Dimensions(col_vert) + }; + if !vrules[x2] { + table.draw_line(border, (v, x2 + h_ofs), y1..table.n[v]); + vrules[x2] = true; + } + if !vrules[x1] { + table.draw_line(border, (v, x1 + h_ofs), y1..table.n[v]); + vrules[x1] = true; } - } - if d.root.show_label == Some(LabelPosition::Corner) && h_ofs > 0 { - table.put( - Rect2::for_ranges((h, 0..h_ofs), top_row..top_row + d.label_depth()), - CellInner::new(Area::Corner, d.root.name.clone()), - ); + // Draws the horizontal lines within a dimension, that is, + // those that separate a category (or group) from its parent + // group or dimension's label. Our running example doesn't + // have groups but the `====` lines below show the + // separators between categories and their dimension label: + // + // ```text + // +-----------------------------------------------------+ + // | bbbb | + // +=================+=================+=================+ + // | bbbb1 | bbbb2 | bbbb3 | + // +-----------------+-----------------+-----------------+ + // | aaaa | aaaa | aaaa | + // +=====+=====+=====+=====+=====+=====+=====+=====+=====+ + // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| + // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ + // ``` + if row > 0 { + table.draw_line(Border::Categories(col_horz), (h, y1), h_ofs..table.n[h]); + } } + } + } +} - // Draw the horizontal line between dimensions, e.g. the `=====` - // line here: - // - // ```text - // +-----------------------------------------------------+ __ - // | bbbb | | - // +-----------------+-----------------+-----------------+ |dim "bbbb" - // | bbbb1 | bbbb2 | bbbb3 | _| - // +=================+=================+=================+ __ - // | aaaa | aaaa | aaaa | | - // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ |dim "aaaa" - // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| _| - // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ - // ``` - if dim_index != h_axis.dimensions.len() - 1 { - table.draw_line( - Border::Dimensions(col_horz), - (h, top_row), - h_ofs..table.n[h], - ); +struct Headings<'a> { + headings: Vec>, + h: Axis2, + column_enumeration: &'a AxisEnumeration, +} + +impl<'a> Headings<'a> { + fn new(pt: &'a PivotTable, h: Axis2, column_enumeration: &'a AxisEnumeration) -> Self { + Self { + headings: pt.axes[h.into()] + .dimensions + .iter() + .copied() + .enumerate() + .rev() + .filter_map(|(axis_index, dim_index)| { + Heading::new(&pt.dimensions[dim_index], axis_index, column_enumeration) + }) + .collect(), + h, + column_enumeration, + } + } + + fn height(&self) -> usize { + self.headings.iter().map(|h| h.height).sum() + } + + fn width(&self) -> usize { + self.headings.first().map_or(0, |h| h.width()) + } + + fn render( + &self, + table: &mut Table, + h_ofs: usize, + col_horz: RowColBorder, + col_vert: RowColBorder, + rotate_inner_labels: bool, + rotate_outer_labels: bool, + area: Area, + ) { + if self.headings.is_empty() { + return; + } + + let h = self.h; + let n_columns = self.width(); + let n_rows = self.height(); + let mut vrules = vec![false; n_columns + 1]; + vrules[0] = true; + vrules[n_columns] = true; + + let mut v_ofs = 0; + for (index, heading) in self.headings.iter().enumerate() { + heading.render( + table, + &mut vrules, + h, + h_ofs, + v_ofs, + self.column_enumeration, + col_horz, + col_vert, + rotate_inner_labels, + rotate_outer_labels, + area, + ); + v_ofs += heading.height; + if index != self.headings.len() - 1 { + // Draw the horizontal line between dimensions, e.g. the `=====` + // line here: + // + // ```text + // +-----------------------------------------------------+ __ + // | bbbb | | + // +-----------------+-----------------+-----------------+ |dim "bbbb" + // | bbbb1 | bbbb2 | bbbb3 | _| + // +=================+=================+=================+ __ + // | aaaa | aaaa | aaaa | | + // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ |dim "aaaa" + // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| _| + // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ + // ``` + table.draw_line(Border::Dimensions(col_horz), (h, v_ofs), h_ofs..table.n[h]); } } - top_row += d.label_depth(); + } +} + +pub fn try_range(range: R, bounds: std::ops::RangeTo) -> Option> +where + R: std::ops::RangeBounds, +{ + let len = bounds.end; + + let start = match range.start_bound() { + std::ops::Bound::Included(&start) => start, + std::ops::Bound::Excluded(start) => start.checked_add(1)?, + std::ops::Bound::Unbounded => 0, + }; + + let end = match range.end_bound() { + std::ops::Bound::Included(end) => end.checked_add(1)?, + std::ops::Bound::Excluded(&end) => end, + std::ops::Bound::Unbounded => len, + }; + + if start > end || end > len { + None + } else { + Some(std::ops::Range { start, end }) } } diff --git a/rust/pspp/src/output/pivot/test.rs b/rust/pspp/src/output/pivot/test.rs index e6079ed015..5905433b94 100644 --- a/rust/pspp/src/output/pivot/test.rs +++ b/rust/pspp/src/output/pivot/test.rs @@ -1,6 +1,11 @@ use std::{fs::File, sync::Arc}; -use crate::output::{driver::Driver, pivot::Color, text::TextDriver, Details, Item}; +use crate::output::{ + driver::Driver, + pivot::{Area, Color, Look}, + text::TextDriver, + Details, Item, +}; use super::{Axis3, DimensionBuilder, GroupBuilder, PivotTableBuilder, Value}; @@ -42,13 +47,17 @@ fn pivot_table_1d() { #[test] fn pivot_table_2d() { - let mut a = GroupBuilder::new(Value::new_text("a")).with_label_shown(); + let mut look = Look::default(); + look.areas[Area::Title].cell_style.horz_align = Some(super::HorzAlign::Left); + look.areas[Area::Title].font_style.bold = false; + + let mut a = GroupBuilder::new(Value::new_text("a")); for name in ["a1", "a2", "a3"] { a.push(Value::new_text(name)); } let d1 = DimensionBuilder::new(Axis3::X, a); - let mut b = GroupBuilder::new(Value::new_text("b")).with_label_shown(); + let mut b = GroupBuilder::new(Value::new_text("b")); for name in ["b1", "b2", "b3"] { b.push(Value::new_text(name)); } @@ -56,16 +65,16 @@ fn pivot_table_2d() { let mut pt = PivotTableBuilder::new(Value::new_text("Columns"), &[d1, d2]); let mut i = 0; - for a in 0..3 { - for b in 0..3 { + for b in 0..3 { + for a in 0..3 { pt.insert(&[a, b], Value::new_integer(Some(i as f64))); i += 1; } } - let mut pt = pt.build(); + let mut pt = pt.with_look(Arc::new(look)).build(); let mut driver = TextDriver::new(File::create("/dev/stdout").unwrap()); driver.write(&Arc::new(Item::new(Details::Table(Box::new(pt.clone()))))); - + return; pt.transpose(); pt.title = Some(Box::new(Value::new_text("Rows"))); driver.write(&Arc::new(Item::new(Details::Table(Box::new(pt.clone()))))); diff --git a/rust/pspp/src/output/text.rs b/rust/pspp/src/output/text.rs index c290050a62..dd3781efb3 100644 --- a/rust/pspp/src/output/text.rs +++ b/rust/pspp/src/output/text.rs @@ -1,5 +1,6 @@ use std::{ borrow::Cow, + fmt::Display, fs::File, io::{BufWriter, Write}, ops::{Index, Range}, @@ -10,6 +11,8 @@ use enum_map::{Enum, EnumMap}; use unicode_linebreak::{linebreaks, BreakOpportunity}; use unicode_width::UnicodeWidthStr; +use crate::output::text_line::Emphasis; + use super::{ driver::Driver, pivot::{Axis2, BorderStyle, Coord2, DisplayValue, HorzAlign, PivotTable, Rect2, Stroke}, @@ -232,12 +235,66 @@ static UNICODE_BOX: LazyLock = LazyLock::new(|| { unicode_box }); +impl PivotTable { + pub fn display<'a>(&'a self) -> DisplayPivotTable<'a> { + DisplayPivotTable::new(self) + } +} + +pub struct DisplayPivotTable<'a> { + pt: &'a PivotTable, + + /// Enable bold and underline in output? + emphasis: bool, + + /// Page width. + width: usize, + + /// Minimum cell size to break across pages. + min_hbreak: usize, + + box_chars: &'static BoxChars, + + params: Params, +} + +impl<'a> DisplayPivotTable<'a> { + fn new(pt: &'a PivotTable) -> Self { + let width = 80; + Self { + pt, + emphasis: false, + width, + min_hbreak: 20, + box_chars: &*&UNICODE_BOX, + params: Params { + size: Coord2::new(width, usize::MAX), + font_size: EnumMap::from_fn(|_| 1), + line_widths: EnumMap::from_fn(|stroke| if stroke == Stroke::None { 0 } else { 1 }), + px_size: None, + min_break: EnumMap::default(), + supports_margins: false, + rtl: false, + printing: true, + can_adjust_break: false, + can_scale: false, + }, + } + } +} + +impl<'a> Display for DisplayPivotTable<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } +} + impl TextDriver { pub fn new(file: File) -> TextDriver { let width = 80; Self { file: BufWriter::new(file), - emphasis: false, + emphasis: true, width, min_hbreak: 20, box_chars: &*&UNICODE_BOX, @@ -528,7 +585,12 @@ impl Device for TextDriver { continue; }; - self.get_line(y).put(x, text); + let text = if self.emphasis { + Emphasis::from(&cell.style.font_style).apply(text) + } else { + Cow::from(text) + }; + self.get_line(y).put(x, &text); } } diff --git a/rust/pspp/src/output/text_line.rs b/rust/pspp/src/output/text_line.rs index 713b892b39..9a81402280 100644 --- a/rust/pspp/src/output/text_line.rs +++ b/rust/pspp/src/output/text_line.rs @@ -7,6 +7,8 @@ use std::{ use unicode_width::UnicodeWidthChar; +use crate::output::pivot::FontStyle; + /// A line of text, encoded in UTF-8, with support functions that properly /// handle double-width characters and backspaces. /// @@ -203,11 +205,20 @@ impl<'a> Iterator for Widths<'a> { } #[derive(Copy, Clone, PartialEq, Eq, Sequence)] -struct Emphasis { +pub struct Emphasis { pub bold: bool, pub underline: bool, } +impl From<&FontStyle> for Emphasis { + fn from(style: &FontStyle) -> Self { + Self { + bold: style.bold, + underline: style.underline, + } + } +} + impl Emphasis { const fn plain() -> Self { Self { @@ -215,10 +226,10 @@ impl Emphasis { underline: false, } } - fn is_plain(&self) -> bool { + pub fn is_plain(&self) -> bool { *self == Self::plain() } - fn apply<'a>(&self, s: &'a str) -> Cow<'a, str> { + pub fn apply<'a>(&self, s: &'a str) -> Cow<'a, str> { if self.is_plain() { Cow::from(s) } else { -- 2.30.2