1use crate::console::RenderContext;
7use crate::panel::BorderStyle;
8use crate::renderable::{Renderable, Segment};
9use crate::style::Style;
10use crate::text::{Span, Text};
11use crate::box_drawing::Line;
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(
358 &self,
359 widths: &[usize],
360 line: &Line,
361 ) -> Segment {
362 let mut spans = vec![Span::styled(line.left.to_string(), self.style)];
363
364 for (i, &width) in widths.iter().enumerate() {
365 let cell_width = width + self.padding * 2;
366 spans.push(Span::styled(
367 line.mid.to_string().repeat(cell_width),
368 self.style,
369 ));
370 if i < widths.len() - 1 {
371 spans.push(Span::styled(line.cross.to_string(), self.style));
372 }
373 }
374
375 spans.push(Span::styled(line.right.to_string(), self.style));
376 Segment::line(spans)
377 }
378
379 fn render_row(
380 &self,
381 cells: &[Text],
382 widths: &[usize],
383 line: &Line,
384 cell_styles: &[Style],
385 ) -> Vec<Segment> {
386 let mut spans = Vec::new();
389
390 if self.show_border {
391 spans.push(Span::styled(line.left.to_string(), self.style));
392 }
393
394 for (i, width) in widths.iter().enumerate() {
395 let cell = cells.get(i);
396 let content = cell.map(|c| c.plain_text()).unwrap_or_default();
397 let _content_width = UnicodeWidthStr::width(content.as_str());
398 let cell_style = cell_styles.get(i).copied().unwrap_or_default();
399
400 let align = self.columns.get(i).map(|c| c.align).unwrap_or_default();
401 let padded = pad_string(&content, *width, align);
402
403 spans.push(Span::raw(" ".repeat(self.padding)));
405 spans.push(Span::styled(padded, cell_style));
406 spans.push(Span::raw(" ".repeat(self.padding)));
407
408 if i < widths.len() - 1 {
409 spans.push(Span::styled(line.cross.to_string(), self.style));
410 } else if self.show_border {
411 spans.push(Span::styled(line.right.to_string(), self.style));
412 }
413 }
414
415 vec![Segment::line(spans)]
416 }
417}
418
419fn pad_string(s: &str, width: usize, align: ColumnAlign) -> String {
420 let content_width = UnicodeWidthStr::width(s);
421 if content_width >= width {
422 return truncate_string(s, width);
423 }
424
425 let padding = width - content_width;
426 match align {
427 ColumnAlign::Left => format!("{}{}", s, " ".repeat(padding)),
428 ColumnAlign::Right => format!("{}{}", " ".repeat(padding), s),
429 ColumnAlign::Center => {
430 let left = padding / 2;
431 let right = padding - left;
432 format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
433 }
434 }
435}
436
437fn truncate_string(s: &str, width: usize) -> String {
438 use unicode_segmentation::UnicodeSegmentation;
439
440 let mut result = String::new();
441 let mut current_width = 0;
442
443 for grapheme in s.graphemes(true) {
444 let grapheme_width = UnicodeWidthStr::width(grapheme);
445 if current_width + grapheme_width > width {
446 if width > 1 && current_width < width {
447 result.push('…');
448 }
449 break;
450 }
451 result.push_str(grapheme);
452 current_width += grapheme_width;
453 }
454
455 while current_width < width {
457 result.push(' ');
458 current_width += 1;
459 }
460
461 result
462}
463
464impl From<&str> for Column {
465 fn from(s: &str) -> Self {
466 Column::new(s)
467 }
468}
469
470impl From<String> for Column {
471 fn from(s: String) -> Self {
472 Column::new(&s)
473 }
474}
475
476impl Renderable for Table {
477 fn render(&self, context: &RenderContext) -> Vec<Segment> {
478 if self.columns.is_empty() {
479 return vec![];
480 }
481
482 let box_chars = self.border_style.to_box();
483 let widths = self.calculate_widths(context.width);
484 let mut segments = Vec::new();
485
486 let content_width: usize = widths.iter().map(|w| w + self.padding * 2).sum();
488 let border_overhead = if self.show_border {
489 widths.len() + 1
490 } else {
491 widths.len() - 1
492 };
493 let table_width = content_width + border_overhead;
494
495 if let Some(title) = &self.title {
497 let title_width = UnicodeWidthStr::width(title.as_str());
498 if title_width <= table_width {
499 let padding = table_width - title_width;
500 let left_pad = padding / 2;
501 let right_pad = padding - left_pad;
502
503 let mut spans = Vec::new();
504 if left_pad > 0 {
505 spans.push(Span::raw(" ".repeat(left_pad)));
506 }
507 spans.push(Span::styled(title.clone(), Style::new().bold()));
508 if right_pad > 0 {
509 spans.push(Span::raw(" ".repeat(right_pad)));
510 }
511 segments.push(Segment::line(spans));
512 } else {
513 segments.push(Segment::line(vec![Span::styled(
515 title.clone(),
516 Style::new().bold(),
517 )]));
518 }
519 }
520
521 if self.show_border {
523 segments.push(self.render_horizontal_line(
524 &widths,
525 &box_chars.top,
526 ));
527 }
528
529 if self.show_header {
531 let header_cells: Vec<Text> = self
532 .columns
533 .iter()
534 .map(|c| Text::styled(c.header.clone(), c.header_style))
535 .collect();
536 let header_styles: Vec<Style> = self.columns.iter().map(|c| c.header_style).collect();
537 segments.extend(self.render_row(&header_cells, &widths, &box_chars.header, &header_styles));
539
540 if self.show_border || self.show_row_lines {
542 segments.push(self.render_horizontal_line(
543 &widths,
544 &box_chars.head,
545 ));
546 }
547 }
548
549 for (row_idx, row) in self.rows.iter().enumerate() {
551 let cell_styles: Vec<Style> = self.columns.iter().map(|c| c.style).collect();
552 segments.extend(self.render_row(&row.cells, &widths, &box_chars.cell, &cell_styles));
554
555 if self.show_row_lines && row_idx < self.rows.len() - 1 {
557 segments.push(self.render_horizontal_line(
558 &widths,
559 &box_chars.mid,
560 ));
561 }
562 }
563
564 if self.show_border {
566 segments.push(self.render_horizontal_line(
567 &widths,
568 &box_chars.bottom,
569 ));
570 }
571
572 segments
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579
580 #[test]
581 fn test_table_basic() {
582 let mut table = Table::new();
583 table.add_column("Name");
584 table.add_column("Age");
585 table.add_row_strs(&["Alice", "30"]);
586 table.add_row_strs(&["Bob", "25"]);
587
588 let context = RenderContext { width: 40, height: None };
589 let segments = table.render(&context);
590
591 assert!(!segments.is_empty());
592
593 let text: String = segments.iter().map(|s| s.plain_text()).collect();
595 assert!(text.contains("Name"));
596 assert!(text.contains("Alice"));
597 assert!(text.contains("Bob"));
598 }
599
600 #[test]
601 fn test_table_builder() {
602 let table = Table::new()
603 .columns(["A", "B", "C"])
604 .border_style(BorderStyle::Square);
605
606 assert_eq!(table.columns.len(), 3);
607 }
608
609 #[test]
610 fn test_pad_string() {
611 assert_eq!(pad_string("hi", 5, ColumnAlign::Left), "hi ");
612 assert_eq!(pad_string("hi", 5, ColumnAlign::Right), " hi");
613 assert_eq!(pad_string("hi", 5, ColumnAlign::Center), " hi ");
614 }
615}