1use crate::{
2 buffer::Buffer,
3 layout::{Constraint, Layout, Rect},
4 style::Style,
5 text::Text,
6 widgets::{Block, Widget},
7};
8use unicode_width::UnicodeWidthStr;
9
10#[derive(Debug, Clone, PartialEq, Eq, 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, Eq, 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, Eq)]
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 offset: usize,
208 selected: Option<usize>,
209}
210
211impl<'a> Table<'a> {
212 pub fn new<T>(rows: T) -> Self
213 where
214 T: IntoIterator<Item = Row<'a>>,
215 {
216 Self {
217 block: None,
218 style: Style::default(),
219 widths: &[],
220 column_spacing: 1,
221 highlight_style: Style::default(),
222 highlight_symbol: None,
223 header: None,
224 rows: rows.into_iter().collect(),
225 offset: 0,
226 selected: None,
227 }
228 }
229
230 pub fn selected(&self) -> Option<usize> {
231 self.selected
232 }
233
234 pub fn select(&mut self, index: Option<usize>) {
235 self.selected = index;
236 if index.is_none() {
237 self.offset = 0;
238 }
239 }
240
241 pub fn block(mut self, block: Block<'a>) -> Self {
242 self.block = Some(block);
243 self
244 }
245
246 pub fn header(mut self, header: Row<'a>) -> Self {
247 self.header = Some(header);
248 self
249 }
250
251 pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
252 let between_0_and_100 = |&w| match w {
253 Constraint::Percentage(p) => p <= 100,
254 _ => true,
255 };
256 assert!(
257 widths.iter().all(between_0_and_100),
258 "Percentages should be between 0 and 100 inclusively."
259 );
260 self.widths = widths;
261 self
262 }
263
264 pub fn style(mut self, style: Style) -> Self {
265 self.style = style;
266 self
267 }
268
269 pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
270 self.highlight_symbol = Some(highlight_symbol);
271 self
272 }
273
274 pub fn highlight_style(mut self, highlight_style: Style) -> Self {
275 self.highlight_style = highlight_style;
276 self
277 }
278
279 pub fn column_spacing(mut self, spacing: u16) -> Self {
280 self.column_spacing = spacing;
281 self
282 }
283
284 fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
285 let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
286 if has_selection {
287 let highlight_symbol_width =
288 self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
289 constraints.push(Constraint::Length(highlight_symbol_width));
290 }
291 for constraint in self.widths {
292 constraints.push(*constraint);
293 constraints.push(Constraint::Length(self.column_spacing));
294 }
295 if !self.widths.is_empty() {
296 constraints.pop();
297 }
298 let mut chunks = Layout::horizontal(constraints).split(Rect {
299 x: 0,
300 y: 0,
301 width: max_width,
302 height: 1,
303 });
304 if has_selection {
305 chunks.remove(0);
306 }
307 chunks.iter().step_by(2).map(|c| c.width).collect()
308 }
309
310 fn get_row_bounds(
311 &self,
312 selected: Option<usize>,
313 offset: usize,
314 max_height: u16,
315 ) -> (usize, usize) {
316 let offset = offset.min(self.rows.len().saturating_sub(1));
317 let mut start = offset;
318 let mut end = offset;
319 let mut height = 0;
320 for item in self.rows.iter().skip(offset) {
321 if height + item.height > max_height {
322 break;
323 }
324 height += item.total_height();
325 end += 1;
326 }
327
328 let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
329 while selected >= end {
330 height = height.saturating_add(self.rows[end].total_height());
331 end += 1;
332 while height > max_height {
333 height = height.saturating_sub(self.rows[start].total_height());
334 start += 1;
335 }
336 }
337 while selected < start {
338 start -= 1;
339 height = height.saturating_add(self.rows[start].total_height());
340 while height > max_height {
341 end -= 1;
342 height = height.saturating_sub(self.rows[end].total_height());
343 }
344 }
345 (start, end)
346 }
347}
348
349impl<'a> Widget for Table<'a> {
350 fn render(&mut self, area: Rect, buf: &mut Buffer) {
351 if area.area() == 0 {
352 return;
353 }
354 buf.set_style(area, self.style);
355 let table_area = match self.block.as_mut() {
356 Some(b) => {
357 let inner_area = b.inner(area);
358 b.render(area, buf);
359 inner_area
360 }
361 None => area,
362 };
363
364 let has_selection = self.selected.is_some();
365 let columns_widths = self.get_columns_widths(table_area.width, has_selection);
366 let highlight_symbol = self.highlight_symbol.unwrap_or("");
367 let blank_symbol = " ".repeat(highlight_symbol.width());
368 let mut current_height = 0;
369 let mut rows_height = table_area.height;
370
371 if let Some(ref header) = self.header {
373 let max_header_height = table_area.height.min(header.total_height());
374 buf.set_style(
375 Rect {
376 x: table_area.left(),
377 y: table_area.top(),
378 width: table_area.width,
379 height: table_area.height.min(header.height),
380 },
381 header.style,
382 );
383 let mut col = table_area.left();
384 if has_selection {
385 col += (highlight_symbol.width() as u16).min(table_area.width);
386 }
387 for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
388 render_cell(
389 buf,
390 cell,
391 Rect {
392 x: col,
393 y: table_area.top(),
394 width: *width,
395 height: max_header_height,
396 },
397 );
398 col += *width + self.column_spacing;
399 }
400 current_height += max_header_height;
401 rows_height = rows_height.saturating_sub(max_header_height);
402 }
403
404 if self.rows.is_empty() {
406 return;
407 }
408 let (start, end) = self.get_row_bounds(self.selected, self.offset, rows_height);
409 self.offset = start;
410 for (i, table_row) in self
411 .rows
412 .iter_mut()
413 .enumerate()
414 .skip(self.offset)
415 .take(end - start)
416 {
417 let (row, col) = (table_area.top() + current_height, table_area.left());
418 current_height += table_row.total_height();
419 let table_row_area = Rect {
420 x: col,
421 y: row,
422 width: table_area.width,
423 height: table_row.height,
424 };
425 buf.set_style(table_row_area, table_row.style);
426 let is_selected = self.selected.map(|s| s == i).unwrap_or(false);
427 let table_row_start_col = if has_selection {
428 let symbol = if is_selected {
429 highlight_symbol
430 } else {
431 &blank_symbol
432 };
433 let (col, _) =
434 buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
435 col
436 } else {
437 col
438 };
439 let mut col = table_row_start_col;
440 for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
441 render_cell(
442 buf,
443 cell,
444 Rect {
445 x: col,
446 y: row,
447 width: *width,
448 height: table_row.height,
449 },
450 );
451 col += *width + self.column_spacing;
452 }
453 if is_selected {
454 buf.set_style(table_row_area, self.highlight_style);
455 }
456 }
457 }
458}
459
460fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
461 buf.set_style(area, cell.style);
462 for (i, spans) in cell.content.lines.iter().enumerate() {
463 if i as u16 >= area.height {
464 break;
465 }
466 buf.set_spans(area.x, area.y + i as u16, spans, area.width);
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 #[should_panic]
476 fn table_invalid_percentages() {
477 Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
478 }
479}