tabled/settings/themes/
column_names.rs

1use std::cmp;
2
3use crate::{
4    grid::{
5        config::{AlignmentHorizontal, AlignmentVertical, ColoredConfig, Entity, Offset, Position},
6        dimension::{CompleteDimension, Dimension, Estimate},
7        records::{
8            vec_records::{Text, VecRecords},
9            ExactRecords, PeekableRecords, Records, Resizable,
10        },
11        util::string::{get_char_width, get_line_width},
12    },
13    settings::{
14        object::{Column, Row},
15        style::LineText,
16        Alignment, Color, TableOption,
17    },
18};
19
20/// [`ColumnNames`] sets strings on horizontal lines for the columns.
21///
22/// Notice that using a [`Default`] would reuse a names from the first row.
23///
24/// # Examples
25///
26/// ```
27/// use std::iter::FromIterator;
28/// use tabled::{
29///     Table,
30///     settings::{themes::ColumnNames, Alignment},
31/// };
32///
33/// let data = vec![
34///     vec!["Hello", "World"],
35///     vec!["Hello", "World"],
36/// ];
37///
38/// let mut table = Table::from_iter(data);
39/// table.with(
40///     ColumnNames::new(["head1", "head2"])
41///         .line(2)
42///         .alignment(Alignment::right())
43/// );
44///
45/// assert_eq!(
46///     table.to_string(),
47///     "+-------+-------+\n\
48///      | Hello | World |\n\
49///      +-------+-------+\n\
50///      | Hello | World |\n\
51///      +--head1+--head2+"
52/// );
53/// ```
54///
55/// [`Default`] usage.
56///
57/// ```
58/// use std::iter::FromIterator;
59/// use tabled::{Table, settings::themes::ColumnNames};
60///
61/// let data = vec![
62///     vec!["Hello", "World"],
63///     vec!["Hello", "World"],
64/// ];
65///
66/// let mut table = Table::from_iter(data);
67/// table.with(ColumnNames::head());
68///
69/// assert_eq!(
70///     table.to_string(),
71///     "+Hello--+World--+\n\
72///      | Hello | World |\n\
73///      +-------+-------+"
74/// );
75/// ```
76#[derive(Debug, Clone)]
77pub struct ColumnNames {
78    names: Option<Vec<String>>,
79    colors: Option<ListValue<Color>>,
80    alignments: ListValue<Alignment>,
81    line: usize,
82}
83
84impl ColumnNames {
85    /// Creates a [`ColumnNames`]
86    /// which will be removing the head row and putting it right on the given border.
87    ///
88    /// # Example
89    ///
90    /// ```
91    /// use std::iter::FromIterator;
92    /// use tabled::{Table, settings::themes::ColumnNames, assert::assert_table};
93    ///
94    /// let data = vec![
95    ///     vec!["head1", "head2"],
96    ///     vec!["Hello", "World"],
97    /// ];
98    ///
99    /// let mut table = Table::from_iter(data);
100    /// table.with(ColumnNames::head());
101    ///
102    /// assert_table!(
103    ///     table,
104    ///     "+head1--+head2--+"
105    ///     "| Hello | World |"
106    ///     "+-------+-------+"
107    /// );
108    /// ```
109    pub fn head() -> Self {
110        Self {
111            names: Default::default(),
112            colors: Default::default(),
113            line: Default::default(),
114            alignments: ListValue::Static(Alignment::left()),
115        }
116    }
117
118    /// Creates a [`ColumnNames`] with a given names.
119    ///
120    /// Using a [`Default`] would reuse a names from the first row.
121    ///
122    /// # Example
123    ///
124    /// ```
125    /// use std::iter::FromIterator;
126    /// use tabled::{Table, settings::themes::ColumnNames, assert::assert_table};
127    ///
128    /// let data = vec![vec!["Hello", "World"]];
129    /// let mut table = Table::from_iter(data);
130    /// table.with(ColumnNames::new(["head1", "head2"]));
131    ///
132    /// assert_table!(
133    ///     table,
134    ///     "+head1--+head2--+"
135    ///     "| Hello | World |"
136    ///     "+-------+-------+"
137    /// );
138    /// ```
139    pub fn new<I>(names: I) -> Self
140    where
141        I: IntoIterator,
142        I::Item: Into<String>,
143    {
144        let names = names.into_iter().map(Into::into).collect::<Vec<_>>();
145
146        Self {
147            names: Some(names),
148            alignments: ListValue::Static(Alignment::left()),
149            colors: Default::default(),
150            line: 0,
151        }
152    }
153
154    /// Set color for the column names.
155    ///
156    /// By default there's no colors.
157    ///
158    /// # Example
159    ///
160    /// ```
161    /// use std::iter::FromIterator;
162    /// use tabled::Table;
163    /// use tabled::settings::{Color, themes::ColumnNames};
164    /// use tabled::assert::assert_table;
165    ///
166    /// let data = vec![vec!["Hello", "World"]];
167    /// let mut table = Table::from_iter(data);
168    /// table.with(ColumnNames::new(["head1", "head2"]).color(vec![Color::FG_RED]));
169    ///
170    /// assert_table!(
171    ///     table,
172    ///     "+\u{1b}[31mh\u{1b}[39m\u{1b}[31me\u{1b}[39m\u{1b}[31ma\u{1b}[39m\u{1b}[31md\u{1b}[39m\u{1b}[31m1\u{1b}[39m--+head2--+"
173    ///     "| Hello | World |"
174    ///     "+-------+-------+"
175    /// );
176    /// ```
177    pub fn color<T>(self, color: T) -> Self
178    where
179        T: Into<ListValue<Color>>,
180    {
181        Self {
182            names: self.names,
183            line: self.line,
184            alignments: self.alignments,
185            colors: Some(color.into()),
186        }
187    }
188
189    /// Set a horizontal line the names will be applied to.
190    ///
191    /// The default value is 0 (the top horizontal line).
192    ///
193    /// # Example
194    ///
195    /// ```
196    /// use std::iter::FromIterator;
197    /// use tabled::{Table, settings::themes::ColumnNames};
198    ///
199    /// let mut table = Table::from_iter(vec![vec!["Hello", "World"]]);
200    /// table.with(ColumnNames::new(["head1", "head2"]).line(1));
201    ///
202    /// assert_eq!(
203    ///     table.to_string(),
204    ///     "+-------+-------+\n\
205    ///      | Hello | World |\n\
206    ///      +head1--+head2--+"
207    /// );
208    /// ```
209    pub fn line(self, i: usize) -> Self {
210        Self {
211            names: self.names,
212            line: i,
213            alignments: self.alignments,
214            colors: self.colors,
215        }
216    }
217
218    /// Set an alignment for the names.
219    ///
220    /// By default it's left aligned.
221    ///
222    /// # Example
223    ///
224    /// ```
225    /// use std::iter::FromIterator;
226    /// use tabled::{
227    ///     Table,
228    ///     settings::{themes::ColumnNames, Alignment},
229    /// };
230    ///
231    /// let mut table = Table::from_iter(vec![vec!["Hello", "World"]]);
232    /// table.with(ColumnNames::new(["head1", "head2"]).alignment(Alignment::right()));
233    ///
234    /// assert_eq!(
235    ///     table.to_string(),
236    ///     "+--head1+--head2+\n\
237    ///      | Hello | World |\n\
238    ///      +-------+-------+"
239    /// );
240    /// ```
241    pub fn alignment<T>(self, alignment: T) -> Self
242    where
243        T: Into<ListValue<Alignment>>,
244    {
245        Self {
246            names: self.names,
247            line: self.line,
248            alignments: alignment.into(),
249            colors: self.colors,
250        }
251    }
252}
253
254// TODO: Split into ColumnNames and RowNames
255
256impl TableOption<VecRecords<Text<String>>, ColoredConfig, CompleteDimension> for ColumnNames {
257    fn change(
258        self,
259        records: &mut VecRecords<Text<String>>,
260        cfg: &mut ColoredConfig,
261        dims: &mut CompleteDimension,
262    ) {
263        let count_rows = records.count_rows();
264        let count_columns = records.count_columns();
265
266        if count_columns == 0 || count_rows == 0 || self.line > count_rows {
267            return;
268        }
269
270        let alignment_horizontal = convert_alignment_value(self.alignments.clone());
271        let alignment_vertical = convert_alignment_value(self.alignments.clone());
272
273        if let Some(alignment) = alignment_horizontal {
274            let names = get_column_names(records, self.names);
275            let names = vec_set_size(names, records.count_columns());
276            set_column_text(names, self.line, alignment, self.colors, records, dims, cfg);
277            return;
278        }
279
280        if let Some(alignment) = alignment_vertical {
281            let names = get_column_names(records, self.names);
282            let names = vec_set_size(names, records.count_rows());
283            set_row_text(names, self.line, alignment, self.colors, records, dims, cfg);
284            return;
285        }
286
287        let names = get_column_names(records, self.names);
288        let names = vec_set_size(names, records.count_columns());
289        let alignment = ListValue::Static(AlignmentHorizontal::Left);
290        set_column_text(names, self.line, alignment, self.colors, records, dims, cfg);
291    }
292
293    fn hint_change(&self) -> Option<Entity> {
294        let alignment_vertical: Option<ListValue<AlignmentVertical>> =
295            convert_alignment_value(self.alignments.clone());
296        if alignment_vertical.is_some() {
297            Some(Entity::Column(0))
298        } else {
299            Some(Entity::Row(0))
300        }
301    }
302}
303
304fn set_column_text(
305    names: Vec<String>,
306    target_line: usize,
307    alignments: ListValue<AlignmentHorizontal>,
308    colors: Option<ListValue<Color>>,
309    records: &mut VecRecords<Text<String>>,
310    dims: &mut CompleteDimension,
311    cfg: &mut ColoredConfig,
312) {
313    dims.estimate(&*records, cfg);
314
315    let count_columns = names.len();
316    let widths = names
317        .iter()
318        .enumerate()
319        .map(|(col, name)| (cmp::max(get_line_width(name), dims.get_width(col))))
320        .collect::<Vec<_>>();
321
322    dims.set_widths(widths.clone());
323
324    let mut total_width = 0;
325    for (column, (width, name)) in widths.into_iter().zip(names).enumerate() {
326        let color = get_color(&colors, column);
327        let alignment = alignments.get(column).unwrap_or(AlignmentHorizontal::Left);
328        let left_vertical = get_vertical_width(cfg, (target_line, column).into(), count_columns);
329        let grid_offset =
330            total_width + left_vertical + get_horizontal_indent(&name, alignment, width);
331        let line = Row::from(target_line);
332
333        let linetext = create_line_text(&name, grid_offset, color, line);
334        linetext.change(records, cfg, dims);
335
336        total_width += width + left_vertical;
337    }
338}
339
340fn set_row_text(
341    names: Vec<String>,
342    target_line: usize,
343    alignments: ListValue<AlignmentVertical>,
344    colors: Option<ListValue<Color>>,
345    records: &mut VecRecords<Text<String>>,
346    dims: &mut CompleteDimension,
347    cfg: &mut ColoredConfig,
348) {
349    dims.estimate(&*records, cfg);
350
351    let count_rows = names.len();
352    let heights = names
353        .iter()
354        .enumerate()
355        .map(|(row, name)| (cmp::max(get_line_width(name), dims.get_height(row))))
356        .collect::<Vec<_>>();
357
358    dims.set_heights(heights.clone());
359
360    let mut total_height = 0;
361    for (row, (row_height, name)) in heights.into_iter().zip(names).enumerate() {
362        let color = get_color(&colors, row);
363        let alignment = alignments.get(row).unwrap_or(AlignmentVertical::Top);
364        let top_horizontal = get_horizontal_width(cfg, (row, target_line).into(), count_rows);
365        let cell_indent = get_vertical_indent(&name, alignment, row_height);
366        let grid_offset = total_height + top_horizontal + cell_indent;
367        let line = Column::from(target_line);
368
369        let linetext = create_line_text(&name, grid_offset, color, line);
370        linetext.change(records, cfg, dims);
371
372        total_height += row_height + top_horizontal;
373    }
374}
375
376fn get_column_names(
377    records: &mut VecRecords<Text<String>>,
378    opt: Option<Vec<String>>,
379) -> Vec<String> {
380    match opt {
381        Some(names) => names
382            .into_iter()
383            .map(|name| name.lines().next().unwrap_or("").to_string())
384            .collect::<Vec<_>>(),
385        None => collect_head(records),
386    }
387}
388
389fn vec_set_size(mut data: Vec<String>, size: usize) -> Vec<String> {
390    match data.len().cmp(&size) {
391        cmp::Ordering::Equal => {}
392        cmp::Ordering::Less => {
393            let additional_size = size - data.len();
394            data.extend(std::iter::repeat_n(String::new(), additional_size));
395        }
396        cmp::Ordering::Greater => {
397            data.truncate(size);
398        }
399    }
400
401    data
402}
403
404fn collect_head(records: &mut VecRecords<Text<String>>) -> Vec<String> {
405    if records.count_rows() == 0 || records.count_columns() == 0 {
406        return Vec::new();
407    }
408
409    let names = (0..records.count_columns())
410        .map(|column| records.get_line((0, column).into(), 0))
411        .map(ToString::to_string)
412        .collect();
413
414    records.remove_row(0);
415
416    names
417}
418
419fn create_line_text<T>(text: &str, offset: usize, color: Option<&Color>, line: T) -> LineText<T> {
420    let offset = Offset::Start(offset);
421    let mut btext = LineText::new(text, line).offset(offset);
422    if let Some(color) = color {
423        btext = btext.color(color.clone());
424    }
425
426    btext
427}
428
429fn get_color(colors: &Option<ListValue<Color>>, i: usize) -> Option<&Color> {
430    match colors {
431        Some(ListValue::List(list)) => list.get(i),
432        Some(ListValue::Static(color)) => Some(color),
433        None => None,
434    }
435}
436
437fn get_horizontal_indent(text: &str, align: AlignmentHorizontal, available: usize) -> usize {
438    match align {
439        AlignmentHorizontal::Left => 0,
440        AlignmentHorizontal::Right => available - get_line_width(text),
441        AlignmentHorizontal::Center => (available - get_line_width(text)) / 2,
442    }
443}
444
445fn get_vertical_indent(text: &str, align: AlignmentVertical, available: usize) -> usize {
446    match align {
447        AlignmentVertical::Top => 0,
448        AlignmentVertical::Bottom => available - get_line_width(text),
449        AlignmentVertical::Center => (available - get_line_width(text)) / 2,
450    }
451}
452
453fn get_vertical_width(cfg: &mut ColoredConfig, pos: Position, count_columns: usize) -> usize {
454    cfg.get_vertical(pos, count_columns)
455        .map(get_char_width)
456        .unwrap_or(0)
457}
458
459fn get_horizontal_width(cfg: &mut ColoredConfig, pos: Position, count_rows: usize) -> usize {
460    cfg.get_horizontal(pos, count_rows)
461        .map(get_char_width)
462        .unwrap_or(0)
463}
464
465fn convert_alignment_value<T>(value: ListValue<Alignment>) -> Option<ListValue<T>>
466where
467    Option<T>: From<Alignment>,
468{
469    match value {
470        ListValue::List(list) => {
471            let new = list
472                .iter()
473                .flat_map(|value| Option::from(*value))
474                .collect::<Vec<_>>();
475            if new.len() == list.len() {
476                Some(ListValue::List(new))
477            } else {
478                None
479            }
480        }
481        ListValue::Static(value) => Option::from(value).map(ListValue::Static),
482    }
483}
484
485#[derive(Debug, Clone)]
486pub enum ListValue<T> {
487    List(Vec<T>),
488    Static(T),
489}
490
491impl<T> ListValue<T> {
492    fn get(&self, i: usize) -> Option<T>
493    where
494        T: Copy,
495    {
496        match self {
497            ListValue::List(list) => list.get(i).copied(),
498            ListValue::Static(alignment) => Some(*alignment),
499        }
500    }
501}
502
503impl<T> From<T> for ListValue<T> {
504    fn from(value: T) -> Self {
505        Self::Static(value)
506    }
507}
508
509impl<T> From<Vec<T>> for ListValue<T> {
510    fn from(value: Vec<T>) -> Self {
511        Self::List(value)
512    }
513}
514
515impl<T> Default for ListValue<T>
516where
517    T: Default,
518{
519    fn default() -> Self {
520        Self::Static(T::default())
521    }
522}