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