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                "{pin_indicator}{header}{sort_indicator}{column_indicator}"
116            ))
117            .style(style),
118        );
119
120        last_was_pinned = is_pinned;
121    }
122
123    Row::new(header_cells)
124}
125
126/// Build the data rows with appropriate styling
127fn build_data_rows(ctx: &TableRenderContext) -> Vec<Row<'static>> {
128    ctx.data_rows
129        .iter()
130        .enumerate()
131        .map(|(row_idx, row_data)| {
132            let mut cells: Vec<Cell> = Vec::new();
133
134            // Add row number if enabled
135            if ctx.show_row_numbers {
136                let row_num = ctx.row_viewport.start + row_idx + 1;
137                cells.push(
138                    Cell::from(row_num.to_string()).style(Style::default().fg(Color::DarkGray)),
139                );
140            }
141
142            // Check if this is the current row
143            let is_current_row = ctx.is_selected_row(row_idx);
144
145            // Add data cells with separator between pinned and scrollable
146            let mut last_was_pinned = false;
147            for (col_idx, val) in row_data.iter().enumerate() {
148                let is_pinned = ctx.is_pinned_column(col_idx);
149
150                // Add separator if transitioning from pinned to non-pinned
151                if last_was_pinned && !is_pinned && ctx.pinned_count > 0 {
152                    cells.push(Cell::from("│").style(Style::default().fg(Color::DarkGray)));
153                }
154
155                let is_selected_column = ctx.is_selected_column(col_idx);
156                let mut cell = Cell::from(val.clone());
157
158                // Apply fuzzy filter highlighting
159                if !is_current_row && ctx.cell_matches_filter(val) {
160                    cell = cell.style(Style::default().fg(Color::Magenta));
161                }
162
163                // Apply background for pinned columns
164                if is_pinned && !is_current_row {
165                    cell = cell.style(Style::default().bg(Color::Rgb(20, 20, 40)));
166                }
167
168                // Apply selection styling based on mode
169                cell = match ctx.selection_mode {
170                    SelectionMode::Cell if is_current_row && is_selected_column => {
171                        // Cell mode: Only highlight the specific cell
172                        cell.style(
173                            Style::default()
174                                .bg(Color::Yellow)
175                                .fg(Color::Black)
176                                .add_modifier(Modifier::BOLD),
177                        )
178                    }
179                    SelectionMode::Row if is_current_row => {
180                        // Row mode: Highlight entire row with special crosshair cell
181                        if is_selected_column {
182                            cell.style(
183                                Style::default()
184                                    .bg(Color::Yellow)
185                                    .fg(Color::Black)
186                                    .add_modifier(Modifier::BOLD),
187                            )
188                        } else if is_pinned {
189                            cell.style(Style::default().bg(Color::Rgb(60, 80, 120)))
190                        } else {
191                            cell.style(Style::default().bg(Color::Rgb(70, 70, 70)))
192                        }
193                    }
194                    _ if is_selected_column => {
195                        // Column highlight (not in current row)
196                        if is_pinned {
197                            cell.style(Style::default().bg(Color::Rgb(40, 60, 100)))
198                        } else {
199                            cell.style(Style::default().bg(Color::Rgb(50, 50, 50)))
200                        }
201                    }
202                    _ if is_pinned => {
203                        // Pinned column gets subtle blue tint
204                        cell.style(Style::default().bg(Color::Rgb(20, 30, 50)))
205                    }
206                    _ => cell,
207                };
208
209                cells.push(cell);
210                last_was_pinned = is_pinned;
211            }
212
213            // Apply row highlighting
214            let row_style = if is_current_row {
215                Style::default()
216                    .bg(Color::DarkGray)
217                    .add_modifier(Modifier::BOLD)
218            } else {
219                Style::default()
220            };
221
222            Row::new(cells).style(row_style)
223        })
224        .collect()
225}
226
227/// Calculate column widths for the table
228fn calculate_column_widths(ctx: &TableRenderContext) -> Vec<Constraint> {
229    let mut widths: Vec<Constraint> = Vec::new();
230
231    // Add row number column width if enabled
232    if ctx.show_row_numbers {
233        widths.push(Constraint::Length(8)); // Fixed width for row numbers
234    }
235
236    // Add widths for visible data columns with separator
237    let mut last_was_pinned = false;
238    for (idx, &width) in ctx.column_widths.iter().enumerate() {
239        let is_pinned = ctx.is_pinned_column(idx);
240
241        // Add separator width if transitioning from pinned to non-pinned
242        if last_was_pinned && !is_pinned && ctx.pinned_count > 0 {
243            widths.push(Constraint::Length(1)); // Separator column
244        }
245
246        widths.push(Constraint::Length(width));
247        last_was_pinned = is_pinned;
248    }
249
250    widths
251}