Skip to main content

boxmux_lib/components/
table_component.rs

1//! Table Component - Unified table rendering component
2//!
3//! This component provides comprehensive table rendering capabilities with borders,
4//! pagination, sorting, filtering, and styling. Extracts table rendering logic
5//! into a reusable component following the established component architecture.
6
7use crate::components::ComponentDimensions;
8use crate::model::common::Bounds;
9use crate::table::{render_table, TableConfig, TableData, TablePagination};
10use crossterm::style::Color;
11
12/// Configuration for table component styling
13#[derive(Debug, Clone, PartialEq)]
14pub struct TableComponentConfig {
15    /// Table border color
16    pub border_color: Color,
17    /// Header background color
18    pub header_bg_color: Option<Color>,
19    /// Header text color
20    pub header_text_color: Color,
21    /// Data row text color
22    pub row_text_color: Color,
23    /// Highlighted row background color
24    pub highlight_bg_color: Option<Color>,
25    /// Zebra striping background color
26    pub zebra_bg_color: Option<Color>,
27    /// Page info display format
28    pub page_info_format: String,
29    /// Show page navigation indicators
30    pub show_navigation: bool,
31    /// Padding inside table cells
32    pub cell_padding: usize,
33}
34
35impl Default for TableComponentConfig {
36    fn default() -> Self {
37        Self {
38            border_color: Color::White,
39            header_bg_color: Some(Color::DarkBlue),
40            header_text_color: Color::White,
41            row_text_color: Color::White,
42            highlight_bg_color: Some(Color::DarkYellow),
43            zebra_bg_color: Some(Color::DarkGrey),
44            page_info_format: "Page {current} of {total} ({count} rows)".to_string(),
45            show_navigation: true,
46            cell_padding: 1,
47        }
48    }
49}
50
51/// Table Component for unified table rendering
52pub struct TableComponent {
53    config: TableComponentConfig,
54}
55
56impl TableComponent {
57    /// Create new table component with default configuration
58    pub fn new() -> Self {
59        Self {
60            config: TableComponentConfig::default(),
61        }
62    }
63
64    /// Create table component with custom configuration
65    pub fn with_config(config: TableComponentConfig) -> Self {
66        Self { config }
67    }
68
69    /// Create table component with custom styling
70    pub fn with_colors(border_color: Color, header_color: Color, row_color: Color) -> Self {
71        Self {
72            config: TableComponentConfig {
73                border_color,
74                header_text_color: header_color,
75                row_text_color: row_color,
76                ..Default::default()
77            },
78        }
79    }
80
81    /// Render table within specified bounds using table data and configuration
82    pub fn render(
83        &self,
84        table_data: &TableData,
85        table_config: &TableConfig,
86        bounds: &Bounds,
87    ) -> Vec<String> {
88        // Adjust table config to fit within bounds
89        let mut adjusted_config = table_config.clone();
90        adjusted_config.width = ComponentDimensions::new(*bounds).content_bounds().width(); // Account for borders
91        adjusted_config.height = ComponentDimensions::new(*bounds).content_bounds().height(); // Account for borders
92
93        // Generate base table content using existing table system
94        let table_content = render_table(table_data, &adjusted_config);
95
96        // Split content into lines and apply component styling
97        let mut lines: Vec<String> = table_content.lines().map(|s| s.to_string()).collect();
98
99        // Apply component-specific styling and formatting
100        self.apply_component_styling(&mut lines, &adjusted_config);
101
102        // Ensure lines fit within bounds
103        self.fit_to_bounds(&mut lines, bounds);
104
105        lines
106    }
107
108    /// Render table with pagination controls
109    pub fn render_with_pagination(
110        &self,
111        table_data: &TableData,
112        table_config: &TableConfig,
113        bounds: &Bounds,
114    ) -> Vec<String> {
115        let mut lines = self.render(table_data, table_config, bounds);
116
117        // Add pagination info if pagination is enabled
118        if let Some(pagination) = &table_config.pagination {
119            if pagination.show_page_info {
120                let page_info = self.generate_page_info(table_data, pagination);
121                lines.push(page_info);
122
123                // Add navigation indicators if enabled
124                if self.config.show_navigation {
125                    let nav_line = self.generate_navigation_line(pagination);
126                    lines.push(nav_line);
127                }
128            }
129        }
130
131        lines
132    }
133
134    /// Apply component-specific styling to table lines
135    fn apply_component_styling(&self, lines: &mut [String], config: &TableConfig) {
136        // Apply zebra striping background colors
137        if config.zebra_striping {
138            self.apply_zebra_styling(lines);
139        }
140
141        // Apply row highlighting
142        if let Some(highlight_row) = config.highlight_row {
143            self.apply_row_highlighting(lines, highlight_row);
144        }
145
146        // Apply header styling
147        if config.show_headers && !lines.is_empty() {
148            self.apply_header_styling(lines);
149        }
150    }
151
152    /// Apply zebra striping background colors
153    fn apply_zebra_styling(&self, lines: &mut [String]) {
154        // Skip border and header lines when applying zebra striping
155        let data_start = self.find_data_row_start(lines);
156
157        for (index, line) in lines.iter_mut().enumerate().skip(data_start) {
158            if self.is_data_row(line) && (index - data_start) % 2 == 1 {
159                // Apply zebra background to every other data row
160                *line = format!("\x1b[48;5;8m{}\x1b[0m", line); // Dark grey background
161            }
162        }
163    }
164
165    /// Apply highlighting to specific row
166    fn apply_row_highlighting(&self, lines: &mut [String], highlight_row: usize) {
167        let data_start = self.find_data_row_start(lines);
168        let target_index = data_start + highlight_row;
169
170        if let Some(line) = lines.get_mut(target_index) {
171            if self.is_data_row(line) {
172                // Apply highlight background
173                *line = format!("\x1b[48;5;11m{}\x1b[0m", line); // Bright yellow background
174            }
175        }
176    }
177
178    /// Apply header styling
179    fn apply_header_styling(&self, lines: &mut [String]) {
180        // Find header row (typically after top border)
181        for (index, line) in lines.iter_mut().enumerate() {
182            if self.is_header_row(line, index) {
183                // Apply header background and text color
184                *line = format!("\x1b[48;5;4m\x1b[97m{}\x1b[0m", line); // Blue background, white text
185                break;
186            }
187        }
188    }
189
190    /// Find the starting index of data rows
191    fn find_data_row_start(&self, lines: &[String]) -> usize {
192        for (index, line) in lines.iter().enumerate() {
193            if self.is_data_row(line) {
194                return index;
195            }
196        }
197        lines.len()
198    }
199
200    /// Check if line is a data row (not border or header)
201    fn is_data_row(&self, line: &str) -> bool {
202        !line.chars().all(|c| matches!(c, '┌' | '┐' | '└' | '┘' | '├' | '┤' | '┬' | '┴' | '┼' | '─' | '│' | '+' | '-' | '|' | ' '))
203            && line.contains('│') // Contains vertical border character
204            && !line.trim().is_empty()
205    }
206
207    /// Check if line is a header row
208    fn is_header_row(&self, line: &str, index: usize) -> bool {
209        // Header row is typically the first data-like row after borders
210        index > 0 && self.is_data_row(line)
211    }
212
213    /// Generate pagination info string
214    fn generate_page_info(&self, table_data: &TableData, pagination: &TablePagination) -> String {
215        let total_rows = table_data.rows.len();
216        let total_pages = total_rows.div_ceil(pagination.page_size);
217
218        self.config
219            .page_info_format
220            .replace("{current}", &pagination.current_page.to_string())
221            .replace("{total}", &total_pages.to_string())
222            .replace("{count}", &total_rows.to_string())
223    }
224
225    /// Generate navigation line with indicators
226    fn generate_navigation_line(&self, pagination: &TablePagination) -> String {
227        let mut nav_line = String::new();
228
229        // Previous page indicator
230        if pagination.current_page > 1 {
231            nav_line.push_str("◄ Prev  ");
232        } else {
233            nav_line.push_str("       ");
234        }
235
236        // Page numbers (show current ± 2 pages)
237        let start_page = pagination.current_page.saturating_sub(2).max(1);
238        let end_page = (pagination.current_page + 2).min(10); // Assume reasonable page limit
239
240        for page in start_page..=end_page {
241            if page == pagination.current_page {
242                nav_line.push_str(&format!("[{}] ", page));
243            } else {
244                nav_line.push_str(&format!("{} ", page));
245            }
246        }
247
248        // Next page indicator
249        nav_line.push_str("  Next ►");
250
251        nav_line
252    }
253
254    /// Fit lines to bounds by truncating or padding
255    fn fit_to_bounds(&self, lines: &mut Vec<String>, bounds: &Bounds) {
256        // Truncate lines that are too long
257        for line in lines.iter_mut() {
258            if line.chars().count() > bounds.width() {
259                let truncated: String = line.chars().take(bounds.width()).collect();
260                *line = truncated;
261                if bounds.width() > 3 {
262                    let ellipsis_pos = bounds.width().saturating_sub(3);
263                    let mut chars: Vec<char> = line.chars().collect();
264                    if chars.len() > ellipsis_pos {
265                        chars.truncate(ellipsis_pos);
266                        chars.extend("...".chars());
267                        *line = chars.into_iter().collect();
268                    }
269                }
270            }
271        }
272
273        // Limit number of lines to bounds height
274        if lines.len() > bounds.height() {
275            lines.truncate(bounds.height());
276        }
277
278        // Pad with empty lines if needed
279        while lines.len() < bounds.height() {
280            lines.push(" ".repeat(bounds.width()));
281        }
282    }
283
284    /// Get current configuration
285    pub fn get_config(&self) -> &TableComponentConfig {
286        &self.config
287    }
288
289    /// Update configuration
290    pub fn update_config(&mut self, config: TableComponentConfig) {
291        self.config = config;
292    }
293}
294
295impl Default for TableComponent {
296    fn default() -> Self {
297        Self::new()
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use std::collections::HashMap;
305
306    fn create_test_table_data() -> TableData {
307        TableData {
308            headers: vec!["Name".to_string(), "Age".to_string(), "Status".to_string()],
309            rows: vec![
310                vec!["Alice".to_string(), "25".to_string(), "Active".to_string()],
311                vec!["Bob".to_string(), "30".to_string(), "Inactive".to_string()],
312                vec![
313                    "Charlie".to_string(),
314                    "35".to_string(),
315                    "Active".to_string(),
316                ],
317            ],
318            metadata: HashMap::new(),
319        }
320    }
321
322    fn create_test_bounds() -> Bounds {
323        Bounds::new(0, 0, 50, 10)
324    }
325
326    #[test]
327    fn test_table_component_creation() {
328        let component = TableComponent::new();
329        assert_eq!(component.config.border_color, Color::White);
330        assert_eq!(component.config.header_text_color, Color::White);
331    }
332
333    #[test]
334    fn test_table_component_with_config() {
335        let config = TableComponentConfig {
336            border_color: Color::Red,
337            header_text_color: Color::Yellow,
338            row_text_color: Color::Green,
339            ..Default::default()
340        };
341
342        let component = TableComponent::with_config(config.clone());
343        assert_eq!(component.config.border_color, Color::Red);
344        assert_eq!(component.config.header_text_color, Color::Yellow);
345        assert_eq!(component.config.row_text_color, Color::Green);
346    }
347
348    #[test]
349    fn test_table_component_with_colors() {
350        let component = TableComponent::with_colors(Color::Blue, Color::Cyan, Color::Magenta);
351        assert_eq!(component.config.border_color, Color::Blue);
352        assert_eq!(component.config.header_text_color, Color::Cyan);
353        assert_eq!(component.config.row_text_color, Color::Magenta);
354    }
355
356    #[test]
357    fn test_basic_table_rendering() {
358        let component = TableComponent::new();
359        let table_data = create_test_table_data();
360        let table_config = TableConfig::default();
361        let bounds = create_test_bounds();
362
363        let lines = component.render(&table_data, &table_config, &bounds);
364
365        assert!(!lines.is_empty());
366        assert!(lines.len() <= bounds.height());
367
368        // Check that all lines fit within width bounds
369        for line in &lines {
370            assert!(line.chars().count() <= bounds.width());
371        }
372    }
373
374    #[test]
375    fn test_table_rendering_with_pagination() {
376        let component = TableComponent::new();
377        let table_data = create_test_table_data();
378        let mut table_config = TableConfig::default();
379        table_config.pagination = Some(TablePagination {
380            page_size: 2,
381            current_page: 1,
382            show_page_info: true,
383        });
384        let bounds = create_test_bounds();
385
386        let lines = component.render_with_pagination(&table_data, &table_config, &bounds);
387
388        assert!(!lines.is_empty());
389
390        // Should have pagination info
391        let has_page_info = lines.iter().any(|line| line.contains("Page"));
392        assert!(has_page_info);
393    }
394
395    #[test]
396    fn test_zebra_striping_detection() {
397        let component = TableComponent::new();
398
399        // Test data row detection
400        assert!(component.is_data_row("│ Alice   │ 25  │ Active   │"));
401        assert!(!component.is_data_row("├─────────┼─────┼──────────┤"));
402        assert!(!component.is_data_row("┌─────────┬─────┬──────────┐"));
403    }
404
405    #[test]
406    fn test_page_info_generation() {
407        let component = TableComponent::new();
408        let table_data = create_test_table_data();
409        let pagination = TablePagination {
410            page_size: 2,
411            current_page: 2,
412            show_page_info: true,
413        };
414
415        let page_info = component.generate_page_info(&table_data, &pagination);
416
417        assert!(page_info.contains("Page 2"));
418        assert!(page_info.contains("3 rows"));
419    }
420
421    #[test]
422    fn test_navigation_line_generation() {
423        let component = TableComponent::new();
424        let pagination = TablePagination {
425            page_size: 2,
426            current_page: 2,
427            show_page_info: true,
428        };
429
430        let nav_line = component.generate_navigation_line(&pagination);
431
432        assert!(nav_line.contains("◄ Prev"));
433        assert!(nav_line.contains("[2]")); // Current page in brackets
434        assert!(nav_line.contains("Next ►"));
435    }
436
437    #[test]
438    fn test_bounds_fitting() {
439        let component = TableComponent::new();
440        let table_data = create_test_table_data();
441        let table_config = TableConfig::default();
442        let small_bounds = Bounds::new(0, 0, 20, 5);
443
444        let lines = component.render(&table_data, &table_config, &small_bounds);
445
446        assert_eq!(lines.len(), small_bounds.height());
447        for line in &lines {
448            assert!(line.chars().count() <= small_bounds.width());
449        }
450    }
451
452    #[test]
453    fn test_config_update() {
454        let mut component = TableComponent::new();
455        let original_color = component.config.border_color;
456
457        let mut new_config = component.config.clone();
458        new_config.border_color = Color::Red;
459
460        component.update_config(new_config);
461
462        assert_ne!(component.config.border_color, original_color);
463        assert_eq!(component.config.border_color, Color::Red);
464    }
465}