1use crate::box_drawing::Line;
7use crate::console::RenderContext;
8use crate::panel::BorderStyle;
9use crate::renderable::{Renderable, Segment};
10use crate::style::Style;
11use crate::text::{Span, Text};
12use unicode_width::UnicodeWidthStr;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum ColumnAlign {
17 #[default]
18 Left,
19 Center,
20 Right,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum ColumnWidth {
26 #[default]
28 Auto,
29 Fixed(usize),
31 Min(usize),
33 Max(usize),
35}
36
37#[derive(Debug, Clone)]
39pub struct Column {
40 pub header: String,
42 pub align: ColumnAlign,
44 pub width: ColumnWidth,
46 pub header_style: Style,
48 pub style: Style,
50 pub wrap: bool,
52 #[allow(dead_code)]
54 min_width: usize,
55 #[allow(dead_code)]
57 max_width: usize,
58}
59
60impl Column {
61 pub fn new(header: &str) -> Self {
63 let header_width = UnicodeWidthStr::width(header);
64 Column {
65 header: header.to_string(),
66 align: ColumnAlign::Left,
67 width: ColumnWidth::Auto,
68 header_style: Style::new().bold(),
69 style: Style::new(),
70 wrap: true,
71 min_width: header_width,
72 max_width: header_width,
73 }
74 }
75
76 pub fn align(mut self, align: ColumnAlign) -> Self {
78 self.align = align;
79 self
80 }
81
82 pub fn width(mut self, width: ColumnWidth) -> Self {
84 self.width = width;
85 self
86 }
87
88 pub fn header_style(mut self, style: Style) -> Self {
90 self.header_style = style;
91 self
92 }
93
94 pub fn style(mut self, style: Style) -> Self {
96 self.style = style;
97 self
98 }
99
100 pub fn wrap(mut self, wrap: bool) -> Self {
102 self.wrap = wrap;
103 self
104 }
105
106 pub fn center(self) -> Self {
108 self.align(ColumnAlign::Center)
109 }
110
111 pub fn right(self) -> Self {
113 self.align(ColumnAlign::Right)
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct Row {
120 cells: Vec<Text>,
121 style: Option<Style>,
122}
123
124impl Row {
125 pub fn new<I, T>(cells: I) -> Self
127 where
128 I: IntoIterator<Item = T>,
129 T: Into<Text>,
130 {
131 Row {
132 cells: cells.into_iter().map(Into::into).collect(),
133 style: None,
134 }
135 }
136
137 pub fn style(mut self, style: Style) -> Self {
139 self.style = Some(style);
140 self
141 }
142}
143
144#[derive(Debug, Clone)]
148pub struct Table {
149 columns: Vec<Column>,
151 rows: Vec<Row>,
153 border_style: BorderStyle,
155 style: Style,
157 show_header: bool,
159 show_border: bool,
161 show_row_lines: bool,
163 padding: usize,
165 title: Option<String>,
167 expand: bool,
169}
170
171impl Default for Table {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177impl Table {
178 pub fn new() -> Self {
180 Table {
181 columns: Vec::new(),
182 rows: Vec::new(),
183 border_style: BorderStyle::Rounded,
184 style: Style::new(),
185 show_header: true,
186 show_border: true,
187 show_row_lines: false,
188 padding: 1,
189 title: None,
190 expand: false,
191 }
192 }
193
194 pub fn add_column<C: Into<Column>>(&mut self, column: C) -> &mut Self {
196 self.columns.push(column.into());
197 self
198 }
199
200 pub fn column(mut self, header: &str) -> Self {
202 self.columns.push(Column::new(header));
203 self
204 }
205
206 pub fn columns<I, S>(mut self, headers: I) -> Self
208 where
209 I: IntoIterator<Item = S>,
210 S: AsRef<str>,
211 {
212 for header in headers {
213 self.columns.push(Column::new(header.as_ref()));
214 }
215 self
216 }
217
218 pub fn add_row<I, T>(&mut self, cells: I) -> &mut Self
220 where
221 I: IntoIterator<Item = T>,
222 T: Into<Text>,
223 {
224 self.rows.push(Row::new(cells));
225 self
226 }
227
228 pub fn add_row_strs(&mut self, cells: &[&str]) -> &mut Self {
230 let text_cells: Vec<Text> = cells.iter().map(|s| Text::plain(s.to_string())).collect();
231 self.rows.push(Row {
232 cells: text_cells,
233 style: None,
234 });
235 self
236 }
237
238 pub fn add_row_obj(&mut self, row: Row) -> &mut Self {
240 self.rows.push(row);
241 self
242 }
243
244 pub fn border_style(mut self, style: BorderStyle) -> Self {
246 self.border_style = style;
247 self
248 }
249
250 pub fn style(mut self, style: Style) -> Self {
252 self.style = style;
253 self
254 }
255
256 pub fn set_title(mut self, title: &str) -> Self {
258 self.title = Some(title.to_string());
259 self
260 }
261
262 pub fn show_header(mut self, show: bool) -> Self {
264 self.show_header = show;
265 self
266 }
267
268 pub fn show_border(mut self, show: bool) -> Self {
270 self.show_border = show;
271 self
272 }
273
274 pub fn show_row_lines(mut self, show: bool) -> Self {
276 self.show_row_lines = show;
277 self
278 }
279
280 pub fn padding(mut self, padding: usize) -> Self {
282 self.padding = padding;
283 self
284 }
285
286 pub fn title(mut self, title: &str) -> Self {
288 self.title = Some(title.to_string());
289 self
290 }
291
292 pub fn expand(mut self, expand: bool) -> Self {
294 self.expand = expand;
295 self
296 }
297
298 fn calculate_widths(&self, available_width: usize) -> Vec<usize> {
300 let num_cols = self.columns.len();
301 if num_cols == 0 {
302 return vec![];
303 }
304
305 let mut max_widths: Vec<usize> = self
307 .columns
308 .iter()
309 .map(|c| UnicodeWidthStr::width(c.header.as_str()))
310 .collect();
311
312 for row in &self.rows {
313 for (i, cell) in row.cells.iter().enumerate() {
314 if i < max_widths.len() {
315 max_widths[i] = max_widths[i].max(cell.width());
316 }
317 }
318 }
319
320 let overhead = if self.show_border {
322 1 + num_cols + 1 + (self.padding * 2 * num_cols)
323 } else {
324 (num_cols - 1) + (self.padding * 2 * num_cols)
325 };
326
327 let content_width = available_width.saturating_sub(overhead);
328
329 let total_content: usize = max_widths.iter().sum();
331 if total_content == 0 {
332 return vec![content_width / num_cols.max(1); num_cols];
333 }
334
335 if total_content <= content_width {
336 if self.expand {
338 let extra = content_width - total_content;
340 let per_col = extra / num_cols;
341 max_widths.iter().map(|w| w + per_col).collect()
342 } else {
343 max_widths
344 }
345 } else {
346 max_widths
348 .iter()
349 .map(|w| {
350 let ratio = *w as f64 / total_content as f64;
351 ((content_width as f64 * ratio) as usize).max(1)
352 })
353 .collect()
354 }
355 }
356
357 fn render_horizontal_line(&self, widths: &[usize], line: &Line) -> Segment {
358 let mut spans = vec![Span::styled(line.left.to_string(), self.style)];
359
360 for (i, &width) in widths.iter().enumerate() {
361 let cell_width = width + self.padding * 2;
362 spans.push(Span::styled(
363 line.mid.to_string().repeat(cell_width),
364 self.style,
365 ));
366 if i < widths.len() - 1 {
367 spans.push(Span::styled(line.cross.to_string(), self.style));
368 }
369 }
370
371 spans.push(Span::styled(line.right.to_string(), self.style));
372 Segment::line(spans)
373 }
374
375 fn render_row(
376 &self,
377 cells: &[Text],
378 widths: &[usize],
379 line: &Line,
380 cell_styles: &[Style],
381 ) -> Vec<Segment> {
382 let mut spans = Vec::new();
385
386 if self.show_border {
387 spans.push(Span::styled(line.left.to_string(), self.style));
388 }
389
390 for (i, width) in widths.iter().enumerate() {
391 let cell = cells.get(i);
392 let content = cell.map(|c| c.plain_text()).unwrap_or_default();
393 let _content_width = UnicodeWidthStr::width(content.as_str());
394 let cell_style = cell_styles.get(i).copied().unwrap_or_default();
395
396 let align = self.columns.get(i).map(|c| c.align).unwrap_or_default();
397 let padded = pad_string(&content, *width, align);
398
399 spans.push(Span::raw(" ".repeat(self.padding)));
401 spans.push(Span::styled(padded, cell_style));
402 spans.push(Span::raw(" ".repeat(self.padding)));
403
404 if i < widths.len() - 1 {
405 spans.push(Span::styled(line.cross.to_string(), self.style));
406 } else if self.show_border {
407 spans.push(Span::styled(line.right.to_string(), self.style));
408 }
409 }
410
411 vec![Segment::line(spans)]
412 }
413}
414
415fn pad_string(s: &str, width: usize, align: ColumnAlign) -> String {
416 let content_width = UnicodeWidthStr::width(s);
417 if content_width >= width {
418 return truncate_string(s, width);
419 }
420
421 let padding = width - content_width;
422 match align {
423 ColumnAlign::Left => format!("{}{}", s, " ".repeat(padding)),
424 ColumnAlign::Right => format!("{}{}", " ".repeat(padding), s),
425 ColumnAlign::Center => {
426 let left = padding / 2;
427 let right = padding - left;
428 format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
429 }
430 }
431}
432
433fn truncate_string(s: &str, width: usize) -> String {
434 use unicode_segmentation::UnicodeSegmentation;
435
436 let mut result = String::new();
437 let mut current_width = 0;
438
439 for grapheme in s.graphemes(true) {
440 let grapheme_width = UnicodeWidthStr::width(grapheme);
441 if current_width + grapheme_width > width {
442 if width > 1 && current_width < width {
443 result.push('…');
444 }
445 break;
446 }
447 result.push_str(grapheme);
448 current_width += grapheme_width;
449 }
450
451 while current_width < width {
453 result.push(' ');
454 current_width += 1;
455 }
456
457 result
458}
459
460impl From<&str> for Column {
461 fn from(s: &str) -> Self {
462 Column::new(s)
463 }
464}
465
466impl From<String> for Column {
467 fn from(s: String) -> Self {
468 Column::new(&s)
469 }
470}
471
472impl Renderable for Table {
473 fn render(&self, context: &RenderContext) -> Vec<Segment> {
474 if self.columns.is_empty() {
475 return vec![];
476 }
477
478 let box_chars = self.border_style.to_box();
479 let widths = self.calculate_widths(context.width);
480 let mut segments = Vec::new();
481
482 let content_width: usize = widths.iter().map(|w| w + self.padding * 2).sum();
484 let border_overhead = if self.show_border {
485 widths.len() + 1
486 } else {
487 widths.len() - 1
488 };
489 let table_width = content_width + border_overhead;
490
491 if let Some(title) = &self.title {
493 let title_width = UnicodeWidthStr::width(title.as_str());
494 if title_width <= table_width {
495 let padding = table_width - title_width;
496 let left_pad = padding / 2;
497 let right_pad = padding - left_pad;
498
499 let mut spans = Vec::new();
500 if left_pad > 0 {
501 spans.push(Span::raw(" ".repeat(left_pad)));
502 }
503 spans.push(Span::styled(title.clone(), Style::new().bold()));
504 if right_pad > 0 {
505 spans.push(Span::raw(" ".repeat(right_pad)));
506 }
507 segments.push(Segment::line(spans));
508 } else {
509 segments.push(Segment::line(vec![Span::styled(
511 title.clone(),
512 Style::new().bold(),
513 )]));
514 }
515 }
516
517 if self.show_border {
519 segments.push(self.render_horizontal_line(&widths, &box_chars.top));
520 }
521
522 if self.show_header {
524 let header_cells: Vec<Text> = self
525 .columns
526 .iter()
527 .map(|c| Text::styled(c.header.clone(), c.header_style))
528 .collect();
529 let header_styles: Vec<Style> = self.columns.iter().map(|c| c.header_style).collect();
530 segments.extend(self.render_row(
532 &header_cells,
533 &widths,
534 &box_chars.header,
535 &header_styles,
536 ));
537
538 if self.show_border || self.show_row_lines {
540 segments.push(self.render_horizontal_line(&widths, &box_chars.head));
541 }
542 }
543
544 for (row_idx, row) in self.rows.iter().enumerate() {
546 let cell_styles: Vec<Style> = self.columns.iter().map(|c| c.style).collect();
547 segments.extend(self.render_row(&row.cells, &widths, &box_chars.cell, &cell_styles));
549
550 if self.show_row_lines && row_idx < self.rows.len() - 1 {
552 segments.push(self.render_horizontal_line(&widths, &box_chars.mid));
553 }
554 }
555
556 if self.show_border {
558 segments.push(self.render_horizontal_line(&widths, &box_chars.bottom));
559 }
560
561 segments
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
570 fn test_table_basic() {
571 let mut table = Table::new();
572 table.add_column("Name");
573 table.add_column("Age");
574 table.add_row_strs(&["Alice", "30"]);
575 table.add_row_strs(&["Bob", "25"]);
576
577 let context = RenderContext {
578 width: 40,
579 height: None,
580 };
581 let segments = table.render(&context);
582
583 assert!(!segments.is_empty());
584
585 let text: String = segments.iter().map(|s| s.plain_text()).collect();
587 assert!(text.contains("Name"));
588 assert!(text.contains("Alice"));
589 assert!(text.contains("Bob"));
590 }
591
592 #[test]
593 fn test_table_builder() {
594 let table = Table::new()
595 .columns(["A", "B", "C"])
596 .border_style(BorderStyle::Square);
597
598 assert_eq!(table.columns.len(), 3);
599 }
600
601 #[test]
602 fn test_pad_string() {
603 assert_eq!(pad_string("hi", 5, ColumnAlign::Left), "hi ");
604 assert_eq!(pad_string("hi", 5, ColumnAlign::Right), " hi");
605 assert_eq!(pad_string("hi", 5, ColumnAlign::Center), " hi ");
606 }
607}