| 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 | |