1 | use crate::{ |
2 | grid::{ |
3 | config::{AlignmentHorizontal, ColoredConfig}, |
4 | dimension::{CompleteDimensionVecRecords, Dimension, Estimate}, |
5 | records::{ |
6 | vec_records::{CellInfo, VecRecords}, |
7 | ExactRecords, PeekableRecords, Records, Resizable, |
8 | }, |
9 | util::string::string_width, |
10 | }, |
11 | settings::{ |
12 | style::{BorderText, Offset}, |
13 | Color, TableOption, |
14 | }, |
15 | }; |
16 | |
17 | /// [`ColumnNames`] sets strings on horizontal lines for the columns. |
18 | /// |
19 | /// Notice that using a [`Default`] would reuse a names from the first row. |
20 | /// |
21 | /// # Examples |
22 | /// |
23 | /// ``` |
24 | /// use std::iter::FromIterator; |
25 | /// use tabled::{Table, settings::themes::ColumnNames}; |
26 | /// |
27 | /// let data = vec![ |
28 | /// vec!["Hello" , "World" ], |
29 | /// vec!["Hello" , "World" ], |
30 | /// ]; |
31 | /// |
32 | /// let mut table = Table::from_iter(data); |
33 | /// table.with(ColumnNames::new(["head1" , "head2" ]).set_offset(3).set_line(2)); |
34 | /// |
35 | /// assert_eq!( |
36 | /// table.to_string(), |
37 | /// "+--------+--------+ \n\ |
38 | /// | Hello | World | \n\ |
39 | /// +--------+--------+ \n\ |
40 | /// | Hello | World | \n\ |
41 | /// +---head1+---head2+" |
42 | /// ); |
43 | /// ``` |
44 | /// |
45 | /// [`Default`] usage. |
46 | /// |
47 | /// ``` |
48 | /// use std::iter::FromIterator; |
49 | /// use tabled::{Table, settings::themes::ColumnNames}; |
50 | /// |
51 | /// let data = vec![ |
52 | /// vec!["Hello" , "World" ], |
53 | /// vec!["Hello" , "World" ], |
54 | /// ]; |
55 | /// |
56 | /// let mut table = Table::from_iter(data); |
57 | /// table.with(ColumnNames::default()); |
58 | /// |
59 | /// assert_eq!( |
60 | /// table.to_string(), |
61 | /// "+Hello--+World--+ \n\ |
62 | /// | Hello | World | \n\ |
63 | /// +-------+-------+" |
64 | /// ); |
65 | /// ``` |
66 | #[derive (Debug, Clone)] |
67 | pub struct ColumnNames { |
68 | names: Option<Vec<String>>, |
69 | colors: Vec<Option<Color>>, |
70 | offset: usize, |
71 | line: usize, |
72 | alignment: AlignmentHorizontal, |
73 | } |
74 | |
75 | impl Default for ColumnNames { |
76 | fn default() -> Self { |
77 | Self { |
78 | names: Default::default(), |
79 | colors: Default::default(), |
80 | offset: Default::default(), |
81 | line: Default::default(), |
82 | alignment: AlignmentHorizontal::Left, |
83 | } |
84 | } |
85 | } |
86 | |
87 | impl ColumnNames { |
88 | /// Creates a [`ColumnNames`] with a given names. |
89 | /// |
90 | /// Using a [`Default`] would reuse a names from the first row. |
91 | /// |
92 | /// # Example |
93 | /// |
94 | /// ``` |
95 | /// use std::iter::FromIterator; |
96 | /// use tabled::{Table, settings::themes::ColumnNames}; |
97 | /// |
98 | /// let mut table = Table::from_iter(vec![vec!["Hello" , "World" ]]); |
99 | /// table.with(ColumnNames::new(["head1" , "head2" ])); |
100 | /// |
101 | /// assert_eq!( |
102 | /// table.to_string(), |
103 | /// "+head1--+head2--+ \n\ |
104 | /// | Hello | World | \n\ |
105 | /// +-------+-------+" |
106 | /// ); |
107 | /// ``` |
108 | pub fn new<I>(names: I) -> Self |
109 | where |
110 | I: IntoIterator, |
111 | I::Item: Into<String>, |
112 | { |
113 | let names = names.into_iter().map(Into::into).collect::<Vec<_>>(); |
114 | Self { |
115 | names: Some(names), |
116 | colors: Vec::new(), |
117 | offset: 0, |
118 | line: 0, |
119 | alignment: AlignmentHorizontal::Left, |
120 | } |
121 | } |
122 | |
123 | /// Set color for the column names. |
124 | /// |
125 | /// By default there's no colors. |
126 | /// |
127 | /// # Example |
128 | /// |
129 | /// ``` |
130 | /// use std::iter::FromIterator; |
131 | /// use tabled::Table; |
132 | /// use tabled::settings::{Color, themes::ColumnNames}; |
133 | /// |
134 | /// let mut table = Table::from_iter(vec![vec!["Hello" , "World" ]]); |
135 | /// table.with(ColumnNames::new(["head1" , "head2" ]).set_colors([Color::FG_RED])); |
136 | /// |
137 | /// assert_eq!( |
138 | /// table.to_string(), |
139 | /// "+ \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\ |
140 | /// | Hello | World | \n\ |
141 | /// +-------+-------+" |
142 | /// ); |
143 | /// ``` |
144 | pub fn set_colors<I>(self, colors: I) -> Self |
145 | where |
146 | I: IntoIterator, |
147 | I::Item: Into<Option<Color>>, |
148 | { |
149 | let colors = colors.into_iter().map(Into::into).collect::<Vec<_>>(); |
150 | Self { |
151 | names: self.names, |
152 | offset: self.offset, |
153 | line: self.line, |
154 | alignment: self.alignment, |
155 | colors, |
156 | } |
157 | } |
158 | |
159 | /// Set a left offset after which the names will be used. |
160 | /// |
161 | /// By default there's no offset. |
162 | /// |
163 | /// # Example |
164 | /// |
165 | /// ``` |
166 | /// use std::iter::FromIterator; |
167 | /// use tabled::{Table, settings::themes::ColumnNames}; |
168 | /// |
169 | /// let mut table = Table::from_iter(vec![vec!["Hello" , "World" ]]); |
170 | /// table.with(ColumnNames::new(["head1" , "head2" ]).set_offset(1)); |
171 | /// |
172 | /// assert_eq!( |
173 | /// table.to_string(), |
174 | /// "+-head1-+-head2-+ \n\ |
175 | /// | Hello | World | \n\ |
176 | /// +-------+-------+" |
177 | /// ); |
178 | /// ``` |
179 | pub fn set_offset(self, i: usize) -> Self { |
180 | Self { |
181 | names: self.names, |
182 | colors: self.colors, |
183 | line: self.line, |
184 | alignment: self.alignment, |
185 | offset: i, |
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" ]).set_line(1)); |
201 | /// |
202 | /// assert_eq!( |
203 | /// table.to_string(), |
204 | /// "+-------+-------+ \n\ |
205 | /// | Hello | World | \n\ |
206 | /// +head1--+head2--+" |
207 | /// ); |
208 | /// ``` |
209 | pub fn set_line(self, i: usize) -> Self { |
210 | Self { |
211 | names: self.names, |
212 | colors: self.colors, |
213 | offset: self.offset, |
214 | alignment: self.alignment, |
215 | line: i, |
216 | } |
217 | } |
218 | |
219 | /// Set an alignment for the names. |
220 | /// |
221 | /// By default it's left aligned. |
222 | /// |
223 | /// # Example |
224 | /// |
225 | /// ``` |
226 | /// use std::iter::FromIterator; |
227 | /// use tabled::{ |
228 | /// Table, |
229 | /// settings::themes::ColumnNames, |
230 | /// grid::config::AlignmentHorizontal, |
231 | /// }; |
232 | /// |
233 | /// let mut table = Table::from_iter(vec![vec!["Hello" , "World" ]]); |
234 | /// table.with(ColumnNames::new(["head1" , "head2" ]).set_alignment(AlignmentHorizontal::Right)); |
235 | /// |
236 | /// assert_eq!( |
237 | /// table.to_string(), |
238 | /// "+--head1+--head2+ \n\ |
239 | /// | Hello | World | \n\ |
240 | /// +-------+-------+" |
241 | /// ); |
242 | /// ``` |
243 | pub fn set_alignment(self, alignment: AlignmentHorizontal) -> Self { |
244 | Self { |
245 | names: self.names, |
246 | colors: self.colors, |
247 | offset: self.offset, |
248 | line: self.line, |
249 | alignment, |
250 | } |
251 | } |
252 | } |
253 | |
254 | impl TableOption<VecRecords<CellInfo<String>>, CompleteDimensionVecRecords<'static>, ColoredConfig> |
255 | for ColumnNames |
256 | { |
257 | fn change( |
258 | self, |
259 | records: &mut VecRecords<CellInfo<String>>, |
260 | cfg: &mut ColoredConfig, |
261 | dims: &mut CompleteDimensionVecRecords<'static>, |
262 | ) { |
263 | let names = match self.names { |
264 | Some(names) => names, |
265 | None => { |
266 | if records.count_rows() == 0 || records.count_columns() == 0 { |
267 | return; |
268 | } |
269 | |
270 | let names = (0..records.count_columns()) |
271 | .map(|column| records.get_text((0, column))) |
272 | .map(ToString::to_string) |
273 | .collect::<Vec<_>>(); |
274 | |
275 | records.remove_row(0); |
276 | |
277 | names |
278 | } |
279 | }; |
280 | |
281 | let names = names.iter().map(|name| name.lines().next().unwrap_or("" )); |
282 | |
283 | dims.estimate(&*records, cfg); |
284 | |
285 | let mut widths = (0..records.count_columns()) |
286 | .map(|column| dims.get_width(column)) |
287 | .collect::<Vec<_>>(); |
288 | |
289 | let names = names.take(widths.len()); |
290 | |
291 | let offset = self.offset; |
292 | widths |
293 | .iter_mut() |
294 | .zip(names.clone()) |
295 | .for_each(|(width, text)| { |
296 | let name_width = string_width(text) + offset; |
297 | *width = std::cmp::max(name_width, *width); |
298 | }); |
299 | |
300 | let _ = dims.set_widths(widths.clone()); |
301 | |
302 | let mut total_width = 0; |
303 | for (i, (width, name)) in widths.iter().zip(names).enumerate() { |
304 | let color = get_color(&self.colors, i); |
305 | let offset = total_width + 1; |
306 | let btext = get_border_text( |
307 | name, |
308 | offset, |
309 | *width, |
310 | self.alignment, |
311 | self.offset, |
312 | self.line, |
313 | color, |
314 | ); |
315 | btext.change(records, cfg, dims); |
316 | |
317 | total_width += width + 1; |
318 | } |
319 | } |
320 | } |
321 | |
322 | fn get_border_text( |
323 | text: &str, |
324 | offset: usize, |
325 | available: usize, |
326 | alignment: AlignmentHorizontal, |
327 | alignment_offset: usize, |
328 | line: usize, |
329 | color: Option<&Color>, |
330 | ) -> BorderText<usize> { |
331 | let name: String = text.to_string(); |
332 | let left_indent: usize = get_indent(text, align:alignment, alignment_offset, available); |
333 | let left_offset: Offset = Offset::Begin(offset + left_indent); |
334 | let mut btext: BorderText = BorderText::new(text:name).horizontal(line).offset(left_offset); |
335 | if let Some(color: &Color) = color { |
336 | btext = btext.color(color.clone()); |
337 | } |
338 | |
339 | btext |
340 | } |
341 | |
342 | fn get_color(colors: &[Option<Color>], i: usize) -> Option<&Color> { |
343 | colors.get(index:i).and_then(|color: &Option| match color { |
344 | Some(color: &Color) => Some(color), |
345 | None => None, |
346 | }) |
347 | } |
348 | |
349 | fn get_indent(text: &str, align: AlignmentHorizontal, offset: usize, available: usize) -> usize { |
350 | match align { |
351 | AlignmentHorizontal::Left => offset, |
352 | AlignmentHorizontal::Right => available - string_width(text) - offset, |
353 | AlignmentHorizontal::Center => (available - string_width(text)) / 2, |
354 | } |
355 | } |
356 | |