1 use crate::{ 2 buffer::Buffer, 3 layout::{Constraint, Direction, Layout, Rect}, 4 style::Style, 5 text::Text, 6 widgets::{Block, StatefulWidget, Widget}, 7 }; 8 use unicode_width::UnicodeWidthStr; 9 10 /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. 11 /// 12 /// It can be created from anything that can be converted to a [`Text`]. 13 /// ```rust 14 /// # use tui::widgets::Cell; 15 /// # use tui::style::{Style, Modifier}; 16 /// # use tui::text::{Span, Spans, Text}; 17 /// # use std::borrow::Cow; 18 /// Cell::from("simple string"); 19 /// 20 /// Cell::from(Span::from("span")); 21 /// 22 /// Cell::from(Spans::from(vec![ 23 /// Span::raw("a vec of "), 24 /// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)) 25 /// ])); 26 /// 27 /// Cell::from(Text::from("a text")); 28 /// 29 /// Cell::from(Text::from(Cow::Borrowed("hello"))); 30 /// ``` 31 /// 32 /// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling 33 /// capabilities of [`Text`]. 34 #[derive(Debug, Clone, PartialEq, Default)] 35 pub struct Cell<'a> { 36 content: Text<'a>, 37 style: Style, 38 } 39 40 impl<'a> Cell<'a> { 41 /// Set the `Style` of this cell. 42 pub fn style(mut self, style: Style) -> Self { 43 self.style = style; 44 self 45 } 46 } 47 48 impl<'a, T> From<T> for Cell<'a> 49 where 50 T: Into<Text<'a>>, 51 { 52 fn from(content: T) -> Cell<'a> { 53 Cell { 54 content: content.into(), 55 style: Style::default(), 56 } 57 } 58 } 59 60 /// Holds data to be displayed in a [`Table`] widget. 61 /// 62 /// A [`Row`] is a collection of cells. It can be created from simple strings: 63 /// ```rust 64 /// # use tui::widgets::Row; 65 /// Row::new(vec!["Cell1", "Cell2", "Cell3"]); 66 /// ``` 67 /// 68 /// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s: 69 /// ```rust 70 /// # use tui::widgets::{Row, Cell}; 71 /// # use tui::style::{Style, Color}; 72 /// Row::new(vec![ 73 /// Cell::from("Cell1"), 74 /// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)), 75 /// ]); 76 /// ``` 77 /// 78 /// You can also construct a row from any type that can be converted into [`Text`]: 79 /// ```rust 80 /// # use std::borrow::Cow; 81 /// # use tui::widgets::Row; 82 /// Row::new(vec![ 83 /// Cow::Borrowed("hello"), 84 /// Cow::Owned("world".to_uppercase()), 85 /// ]); 86 /// ``` 87 /// 88 /// By default, a row has a height of 1 but you can change this using [`Row::height`]. 89 #[derive(Debug, Clone, PartialEq, Default)] 90 pub struct Row<'a> { 91 cells: Vec<Cell<'a>>, 92 height: u16, 93 style: Style, 94 bottom_margin: u16, 95 } 96 97 impl<'a> Row<'a> { 98 /// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`]. 99 pub fn new<T>(cells: T) -> Self 100 where 101 T: IntoIterator, 102 T::Item: Into<Cell<'a>>, 103 { 104 Self { 105 height: 1, 106 cells: cells.into_iter().map(|c| c.into()).collect(), 107 style: Style::default(), 108 bottom_margin: 0, 109 } 110 } 111 112 /// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this 113 /// height will see its content truncated. 114 pub fn height(mut self, height: u16) -> Self { 115 self.height = height; 116 self 117 } 118 119 /// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a 120 /// any individual [`Cell`] or event by their [`Text`] content. 121 pub fn style(mut self, style: Style) -> Self { 122 self.style = style; 123 self 124 } 125 126 /// Set the bottom margin. By default, the bottom margin is `0`. 127 pub fn bottom_margin(mut self, margin: u16) -> Self { 128 self.bottom_margin = margin; 129 self 130 } 131 132 /// Returns the total height of the row. 133 fn total_height(&self) -> u16 { 134 self.height.saturating_add(self.bottom_margin) 135 } 136 } 137 138 /// A widget to display data in formatted columns. 139 /// 140 /// It is a collection of [`Row`]s, themselves composed of [`Cell`]s: 141 /// ```rust 142 /// # use tui::widgets::{Block, Borders, Table, Row, Cell}; 143 /// # use tui::layout::Constraint; 144 /// # use tui::style::{Style, Color, Modifier}; 145 /// # use tui::text::{Text, Spans, Span}; 146 /// Table::new(vec![ 147 /// // Row can be created from simple strings. 148 /// Row::new(vec!["Row11", "Row12", "Row13"]), 149 /// // You can style the entire row. 150 /// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)), 151 /// // If you need more control over the styling you may need to create Cells directly 152 /// Row::new(vec![ 153 /// Cell::from("Row31"), 154 /// Cell::from("Row32").style(Style::default().fg(Color::Yellow)), 155 /// Cell::from(Spans::from(vec![ 156 /// Span::raw("Row"), 157 /// Span::styled("33", Style::default().fg(Color::Green)) 158 /// ])), 159 /// ]), 160 /// // If a Row need to display some content over multiple lines, you just have to change 161 /// // its height. 162 /// Row::new(vec![ 163 /// Cell::from("Row\n41"), 164 /// Cell::from("Row\n42"), 165 /// Cell::from("Row\n43"), 166 /// ]).height(2), 167 /// ]) 168 /// // You can set the style of the entire Table. 169 /// .style(Style::default().fg(Color::White)) 170 /// // It has an optional header, which is simply a Row always visible at the top. 171 /// .header( 172 /// Row::new(vec!["Col1", "Col2", "Col3"]) 173 /// .style(Style::default().fg(Color::Yellow)) 174 /// // If you want some space between the header and the rest of the rows, you can always 175 /// // specify some margin at the bottom. 176 /// .bottom_margin(1) 177 /// ) 178 /// // As any other widget, a Table can be wrapped in a Block. 179 /// .block(Block::default().title("Table")) 180 /// // Columns widths are constrained in the same way as Layout... 181 /// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)]) 182 /// // ...and they can be separated by a fixed spacing. 183 /// .column_spacing(1) 184 /// // If you wish to highlight a row in any specific way when it is selected... 185 /// .highlight_style(Style::default().add_modifier(Modifier::BOLD)) 186 /// // ...and potentially show a symbol in front of the selection. 187 /// .highlight_symbol(">>"); 188 /// ``` 189 #[derive(Debug, Clone, PartialEq)] 190 pub struct Table<'a> { 191 /// A block to wrap the widget in 192 block: Option<Block<'a>>, 193 /// Base style for the widget 194 style: Style, 195 /// Width constraints for each column 196 widths: &'a [Constraint], 197 /// Space between each column 198 column_spacing: u16, 199 /// Style used to render the selected row 200 highlight_style: Style, 201 /// Symbol in front of the selected rom 202 highlight_symbol: Option<&'a str>, 203 /// Optional header 204 header: Option<Row<'a>>, 205 /// Data to display in each row 206 rows: Vec<Row<'a>>, 207 } 208 209 impl<'a> Table<'a> { 210 pub fn new<T>(rows: T) -> Self 211 where 212 T: IntoIterator<Item = Row<'a>>, 213 { 214 Self { 215 block: None, 216 style: Style::default(), 217 widths: &[], 218 column_spacing: 1, 219 highlight_style: Style::default(), 220 highlight_symbol: None, 221 header: None, 222 rows: rows.into_iter().collect(), 223 } 224 } 225 226 pub fn block(mut self, block: Block<'a>) -> Self { 227 self.block = Some(block); 228 self 229 } 230 231 pub fn header(mut self, header: Row<'a>) -> Self { 232 self.header = Some(header); 233 self 234 } 235 236 pub fn widths(mut self, widths: &'a [Constraint]) -> Self { 237 let between_0_and_100 = |&w| match w { 238 Constraint::Percentage(p) => p <= 100, 239 _ => true, 240 }; 241 assert!( 242 widths.iter().all(between_0_and_100), 243 "Percentages should be between 0 and 100 inclusively." 244 ); 245 self.widths = widths; 246 self 247 } 248 249 pub fn style(mut self, style: Style) -> Self { 250 self.style = style; 251 self 252 } 253 254 pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self { 255 self.highlight_symbol = Some(highlight_symbol); 256 self 257 } 258 259 pub fn highlight_style(mut self, highlight_style: Style) -> Self { 260 self.highlight_style = highlight_style; 261 self 262 } 263 264 pub fn column_spacing(mut self, spacing: u16) -> Self { 265 self.column_spacing = spacing; 266 self 267 } 268 269 fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> { 270 let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1); 271 if has_selection { 272 let highlight_symbol_width = 273 self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0); 274 constraints.push(Constraint::Length(highlight_symbol_width)); 275 } 276 for constraint in self.widths { 277 constraints.push(*constraint); 278 constraints.push(Constraint::Length(self.column_spacing)); 279 } 280 if !self.widths.is_empty() { 281 constraints.pop(); 282 } 283 let mut chunks = Layout::default() 284 .direction(Direction::Horizontal) 285 .constraints(constraints) 286 .expand_to_fill(false) 287 .split(Rect { 288 x: 0, 289 y: 0, 290 width: max_width, 291 height: 1, 292 }); 293 if has_selection { 294 chunks.remove(0); 295 } 296 chunks.iter().step_by(2).map(|c| c.width).collect() 297 } 298 299 fn get_row_bounds( 300 &self, 301 selected: Option<usize>, 302 offset: usize, 303 max_height: u16, 304 ) -> (usize, usize) { 305 let offset = offset.min(self.rows.len().saturating_sub(1)); 306 let mut start = offset; 307 let mut end = offset; 308 let mut height = 0; 309 for item in self.rows.iter().skip(offset) { 310 if height + item.height > max_height { 311 break; 312 } 313 height += item.total_height(); 314 end += 1; 315 } 316 317 let selected = selected.unwrap_or(0).min(self.rows.len() - 1); 318 while selected >= end { 319 height = height.saturating_add(self.rows[end].total_height()); 320 end += 1; 321 while height > max_height { 322 height = height.saturating_sub(self.rows[start].total_height()); 323 start += 1; 324 } 325 } 326 while selected < start { 327 start -= 1; 328 height = height.saturating_add(self.rows[start].total_height()); 329 while height > max_height { 330 end -= 1; 331 height = height.saturating_sub(self.rows[end].total_height()); 332 } 333 } 334 (start, end) 335 } 336 } 337 338 #[derive(Debug, Clone)] 339 pub struct TableState { 340 offset: usize, 341 selected: Option<usize>, 342 } 343 344 impl Default for TableState { 345 fn default() -> TableState { 346 TableState { 347 offset: 0, 348 selected: None, 349 } 350 } 351 } 352 353 impl TableState { 354 pub fn selected(&self) -> Option<usize> { 355 self.selected 356 } 357 358 pub fn select(&mut self, index: Option<usize>) { 359 self.selected = index; 360 if index.is_none() { 361 self.offset = 0; 362 } 363 } 364 } 365 366 impl<'a> StatefulWidget for Table<'a> { 367 type State = TableState; 368 369 fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { 370 if area.area() == 0 { 371 return; 372 } 373 buf.set_style(area, self.style); 374 let table_area = match self.block.take() { 375 Some(b) => { 376 let inner_area = b.inner(area); 377 b.render(area, buf); 378 inner_area 379 } 380 None => area, 381 }; 382 383 let has_selection = state.selected.is_some(); 384 let columns_widths = self.get_columns_widths(table_area.width, has_selection); 385 let highlight_symbol = self.highlight_symbol.unwrap_or(""); 386 let blank_symbol = " ".repeat(highlight_symbol.width()); 387 let mut current_height = 0; 388 let mut rows_height = table_area.height; 389 390 // Draw header 391 if let Some(ref header) = self.header { 392 let max_header_height = table_area.height.min(header.total_height()); 393 buf.set_style( 394 Rect { 395 x: table_area.left(), 396 y: table_area.top(), 397 width: table_area.width, 398 height: table_area.height.min(header.height), 399 }, 400 header.style, 401 ); 402 let mut col = table_area.left(); 403 if has_selection { 404 col += (highlight_symbol.width() as u16).min(table_area.width); 405 } 406 for (width, cell) in columns_widths.iter().zip(header.cells.iter()) { 407 render_cell( 408 buf, 409 cell, 410 Rect { 411 x: col, 412 y: table_area.top(), 413 width: *width, 414 height: max_header_height, 415 }, 416 ); 417 col += *width + self.column_spacing; 418 } 419 current_height += max_header_height; 420 rows_height = rows_height.saturating_sub(max_header_height); 421 } 422 423 // Draw rows 424 if self.rows.is_empty() { 425 return; 426 } 427 let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height); 428 state.offset = start; 429 for (i, table_row) in self 430 .rows 431 .iter_mut() 432 .enumerate() 433 .skip(state.offset) 434 .take(end - start) 435 { 436 let (row, col) = (table_area.top() + current_height, table_area.left()); 437 current_height += table_row.total_height(); 438 let table_row_area = Rect { 439 x: col, 440 y: row, 441 width: table_area.width, 442 height: table_row.height, 443 }; 444 buf.set_style(table_row_area, table_row.style); 445 let is_selected = state.selected.map(|s| s == i).unwrap_or(false); 446 let table_row_start_col = if has_selection { 447 let symbol = if is_selected { 448 highlight_symbol 449 } else { 450 &blank_symbol 451 }; 452 let (col, _) = 453 buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style); 454 col 455 } else { 456 col 457 }; 458 let mut col = table_row_start_col; 459 for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) { 460 render_cell( 461 buf, 462 cell, 463 Rect { 464 x: col, 465 y: row, 466 width: *width, 467 height: table_row.height, 468 }, 469 ); 470 col += *width + self.column_spacing; 471 } 472 if is_selected { 473 buf.set_style(table_row_area, self.highlight_style); 474 } 475 } 476 } 477 } 478 479 fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) { 480 buf.set_style(area, cell.style); 481 for (i, spans) in cell.content.lines.iter().enumerate() { 482 if i as u16 >= area.height { 483 break; 484 } 485 buf.set_spans(area.x, area.y + i as u16, spans, area.width); 486 } 487 } 488 489 impl<'a> Widget for Table<'a> { 490 fn render(self, area: Rect, buf: &mut Buffer) { 491 let mut state = TableState::default(); 492 StatefulWidget::render(self, area, buf, &mut state); 493 } 494 } 495 496 #[cfg(test)] 497 mod tests { 498 use super::*; 499 500 #[test] 501 #[should_panic] 502 fn table_invalid_percentages() { 503 Table::new(vec![]).widths(&[Constraint::Percentage(110)]); 504 } 505 } 506