sql_cli/ui/rendering/
table_renderer.rs

1// Pure table rendering function that depends only on TableRenderContext
2// This is completely decoupled from TUI internals
3
4use crate::app_state_container::SelectionMode;
5use crate::ui::rendering::table_render_context::TableRenderContext;
6use ratatui::{
7    layout::Constraint,
8    prelude::*,
9    style::{Color, Modifier, Style},
10    widgets::{Block, Borders, Cell, Paragraph, Row, Table},
11};
12
13/// Render a table using only the provided context
14/// This function has no dependencies on TUI internals
15pub fn render_table(f: &mut Frame, area: Rect, ctx: &TableRenderContext) {
16    // Handle empty results
17    if ctx.row_count == 0 {
18        let empty = Paragraph::new("No results found")
19            .block(Block::default().borders(Borders::ALL).title("Results"))
20            .style(Style::default().fg(Color::Yellow));
21        f.render_widget(empty, area);
22        return;
23    }
24
25    // Build header row
26    let header = build_header_row(ctx);
27
28    // Build data rows
29    let rows = build_data_rows(ctx);
30
31    // Calculate column widths for the table widget
32    let widths = calculate_column_widths(ctx);
33
34    // Create and render the table widget
35    let table = Table::new(rows, widths)
36        .header(header)
37        .block(
38            Block::default()
39                .borders(Borders::ALL)
40                .title(format!("Results ({} rows)", ctx.row_count)),
41        )
42        .column_spacing(1)
43        .row_highlight_style(
44            Style::default()
45                .bg(Color::DarkGray)
46                .add_modifier(Modifier::BOLD),
47        );
48
49    f.render_widget(table, area);
50}
51
52/// Build the header row with sort indicators and column selection
53fn build_header_row(ctx: &TableRenderContext) -> Row<'static> {
54    let mut header_cells: Vec<Cell> = Vec::new();
55
56    // Add row number header if enabled
57    if ctx.show_row_numbers {
58        header_cells.push(
59            Cell::from("#").style(
60                Style::default()
61                    .fg(Color::Magenta)
62                    .add_modifier(Modifier::BOLD),
63            ),
64        );
65    }
66
67    // Add data headers with separator between pinned and scrollable columns
68    let mut last_was_pinned = false;
69    for (visual_pos, header) in ctx.column_headers.iter().enumerate() {
70        let is_pinned = ctx.is_pinned_column(visual_pos);
71
72        // Add separator if transitioning from pinned to non-pinned
73        if last_was_pinned && !is_pinned && ctx.pinned_count > 0 {
74            // Add a visual separator column
75            header_cells.push(
76                Cell::from("│").style(
77                    Style::default()
78                        .fg(Color::DarkGray)
79                        .add_modifier(Modifier::BOLD),
80                ),
81            );
82        }
83
84        // Get sort indicator
85        let sort_indicator = ctx.get_sort_indicator(visual_pos);
86
87        // Check if this is the current column
88        let is_crosshair = ctx.is_selected_column(visual_pos);
89        let column_indicator = if is_crosshair { " [*]" } else { "" };
90
91        // Add pin indicator for pinned columns
92        let pin_indicator = if is_pinned { "📌 " } else { "" };
93
94        // Determine styling
95        let mut style = if is_pinned {
96            // Pinned columns get special styling
97            Style::default()
98                .bg(Color::Rgb(40, 40, 80)) // Darker blue background
99                .fg(Color::White)
100                .add_modifier(Modifier::BOLD)
101        } else {
102            // Regular columns
103            Style::default()
104                .fg(Color::Cyan)
105                .add_modifier(Modifier::BOLD)
106        };
107
108        if is_crosshair {
109            // Current column gets yellow text
110            style = style.fg(Color::Yellow).add_modifier(Modifier::UNDERLINED);
111        }
112
113        header_cells.push(
114            Cell::from(format!(
115                "{}{}{}{}",
116                pin_indicator, header, sort_indicator, column_indicator
117            ))
118            .style(style),
119        );
120
121        last_was_pinned = is_pinned;
122    }
123
124    Row::new(header_cells)
125}
126
127/// Build the data rows with appropriate styling
128fn build_data_rows(ctx: &TableRenderContext) -> Vec<Row<'static>> {
129    ctx.data_rows
130        .iter()
131        .enumerate()
132        .map(|(row_idx, row_data)| {
133            let mut cells: Vec<Cell> = Vec::new();
134
135            // Add row number if enabled
136            if ctx.show_row_numbers {
137                let row_num = ctx.row_viewport.start + row_idx + 1;
138                cells.push(
139                    Cell::from(row_num.to_string()).style(Style::default().fg(Color::DarkGray)),
140                );
141            }
142
143            // Check if this is the current row
144            let is_current_row = ctx.is_selected_row(row_idx);
145
146            // Add data cells with separator between pinned and scrollable
147            let mut last_was_pinned = false;
148            for (col_idx, val) in row_data.iter().enumerate() {
149                let is_pinned = ctx.is_pinned_column(col_idx);
150
151                // Add separator if transitioning from pinned to non-pinned
152                if last_was_pinned && !is_pinned && ctx.pinned_count > 0 {
153                    cells.push(Cell::from("│").style(Style::default().fg(Color::DarkGray)));
154                }
155
156                let is_selected_column = ctx.is_selected_column(col_idx);
157                let mut cell = Cell::from(val.clone());
158
159                // Apply fuzzy filter highlighting
160                if !is_current_row && ctx.cell_matches_filter(val) {
161                    cell = cell.style(Style::default().fg(Color::Magenta));
162                }
163
164                // Apply background for pinned columns
165                if is_pinned && !is_current_row {
166                    cell = cell.style(Style::default().bg(Color::Rgb(20, 20, 40)));
167                }
168
169                // Apply selection styling based on mode
170                cell = match ctx.selection_mode {
171                    SelectionMode::Cell if is_current_row && is_selected_column => {
172                        // Cell mode: Only highlight the specific cell
173                        cell.style(
174                            Style::default()
175                                .bg(Color::Yellow)
176                                .fg(Color::Black)
177                                .add_modifier(Modifier::BOLD),
178                        )
179                    }
180                    SelectionMode::Row if is_current_row => {
181                        // Row mode: Highlight entire row with special crosshair cell
182                        if is_selected_column {
183                            cell.style(
184                                Style::default()
185                                    .bg(Color::Yellow)
186                                    .fg(Color::Black)
187                                    .add_modifier(Modifier::BOLD),
188                            )
189                        } else if is_pinned {
190                            cell.style(Style::default().bg(Color::Rgb(60, 80, 120)))
191                        } else {
192                            cell.style(Style::default().bg(Color::Rgb(70, 70, 70)))
193                        }
194                    }
195                    _ if is_selected_column => {
196                        // Column highlight (not in current row)
197                        if is_pinned {
198                            cell.style(Style::default().bg(Color::Rgb(40, 60, 100)))
199                        } else {
200                            cell.style(Style::default().bg(Color::Rgb(50, 50, 50)))
201                        }
202                    }
203                    _ if is_pinned => {
204                        // Pinned column gets subtle blue tint
205                        cell.style(Style::default().bg(Color::Rgb(20, 30, 50)))
206                    }
207                    _ => cell,
208                };
209
210                cells.push(cell);
211                last_was_pinned = is_pinned;
212            }
213
214            // Apply row highlighting
215            let row_style = if is_current_row {
216                Style::default()
217                    .bg(Color::DarkGray)
218                    .add_modifier(Modifier::BOLD)
219            } else {
220                Style::default()
221            };
222
223            Row::new(cells).style(row_style)
224        })
225        .collect()
226}
227
228/// Calculate column widths for the table
229fn calculate_column_widths(ctx: &TableRenderContext) -> Vec<Constraint> {
230    let mut widths: Vec<Constraint> = Vec::new();
231
232    // Add row number column width if enabled
233    if ctx.show_row_numbers {
234        widths.push(Constraint::Length(8)); // Fixed width for row numbers
235    }
236
237    // Add widths for visible data columns with separator
238    let mut last_was_pinned = false;
239    for (idx, &width) in ctx.column_widths.iter().enumerate() {
240        let is_pinned = ctx.is_pinned_column(idx);
241
242        // Add separator width if transitioning from pinned to non-pinned
243        if last_was_pinned && !is_pinned && ctx.pinned_count > 0 {
244            widths.push(Constraint::Length(1)); // Separator column
245        }
246
247        widths.push(Constraint::Length(width));
248        last_was_pinned = is_pinned;
249    }
250
251    widths
252}