1use crate::{
2 buffer::Buffer,
3 layout::{Constraint, Direction, Layout, Rect},
4 style::Style,
5 text::Text,
6 widgets::{Block, StatefulWidget, Widget},
7};
8use unicode_width::UnicodeWidthStr;
9
10#[derive(Debug, Clone, PartialEq, Default)]
35pub struct Cell<'a> {
36 content: Text<'a>,
37 style: Style,
38}
39
40impl<'a> Cell<'a> {
41 pub fn style(mut self, style: Style) -> Self {
43 self.style = style;
44 self
45 }
46}
47
48impl<'a, T> From<T> for Cell<'a>
49where
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#[derive(Debug, Clone, PartialEq, Default)]
90pub struct Row<'a> {
91 cells: Vec<Cell<'a>>,
92 height: u16,
93 style: Style,
94 bottom_margin: u16,
95}
96
97impl<'a> Row<'a> {
98 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 pub fn height(mut self, height: u16) -> Self {
115 self.height = height;
116 self
117 }
118
119 pub fn style(mut self, style: Style) -> Self {
122 self.style = style;
123 self
124 }
125
126 pub fn bottom_margin(mut self, margin: u16) -> Self {
128 self.bottom_margin = margin;
129 self
130 }
131
132 fn total_height(&self) -> u16 {
134 self.height.saturating_add(self.bottom_margin)
135 }
136}
137
138#[derive(Debug, Clone, PartialEq)]
190pub struct Table<'a> {
191 block: Option<Block<'a>>,
193 style: Style,
195 widths: &'a [Constraint],
197 column_spacing: u16,
199 highlight_style: Style,
201 highlight_symbol: Option<&'a str>,
203 header: Option<Row<'a>>,
205 rows: Vec<Row<'a>>,
207}
208
209impl<'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)]
339pub struct TableState {
340 offset: usize,
341 selected: Option<usize>,
342}
343
344impl Default for TableState {
345 fn default() -> TableState {
346 TableState {
347 offset: 0,
348 selected: None,
349 }
350 }
351}
352
353impl 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
366impl<'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 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 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
479fn 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
489impl<'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)]
497mod 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}