sql_cli/ui/rendering/
table_renderer.rs1use 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
13pub fn render_table(f: &mut Frame, area: Rect, ctx: &TableRenderContext) {
16 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 let header = build_header_row(ctx);
27
28 let rows = build_data_rows(ctx);
30
31 let widths = calculate_column_widths(ctx);
33
34 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
52fn build_header_row(ctx: &TableRenderContext) -> Row<'static> {
54 let mut header_cells: Vec<Cell> = Vec::new();
55
56 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 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 if last_was_pinned && !is_pinned && ctx.pinned_count > 0 {
74 header_cells.push(
76 Cell::from("│").style(
77 Style::default()
78 .fg(Color::DarkGray)
79 .add_modifier(Modifier::BOLD),
80 ),
81 );
82 }
83
84 let sort_indicator = ctx.get_sort_indicator(visual_pos);
86
87 let is_crosshair = ctx.is_selected_column(visual_pos);
89 let column_indicator = if is_crosshair { " [*]" } else { "" };
90
91 let pin_indicator = if is_pinned { "📌 " } else { "" };
93
94 let mut style = if is_pinned {
96 Style::default()
98 .bg(Color::Rgb(40, 40, 80)) .fg(Color::White)
100 .add_modifier(Modifier::BOLD)
101 } else {
102 Style::default()
104 .fg(Color::Cyan)
105 .add_modifier(Modifier::BOLD)
106 };
107
108 if is_crosshair {
109 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
126fn 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 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 let is_current_row = ctx.is_selected_row(row_idx);
144
145 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 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 if !is_current_row && ctx.cell_matches_filter(val) {
160 cell = cell.style(Style::default().fg(Color::Magenta));
161 }
162
163 if is_pinned && !is_current_row {
165 cell = cell.style(Style::default().bg(Color::Rgb(20, 20, 40)));
166 }
167
168 cell = match ctx.selection_mode {
170 SelectionMode::Cell if is_current_row && is_selected_column => {
171 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 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 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 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 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
227fn calculate_column_widths(ctx: &TableRenderContext) -> Vec<Constraint> {
229 let mut widths: Vec<Constraint> = Vec::new();
230
231 if ctx.show_row_numbers {
233 widths.push(Constraint::Length(8)); }
235
236 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 if last_was_pinned && !is_pinned && ctx.pinned_count > 0 {
243 widths.push(Constraint::Length(1)); }
245
246 widths.push(Constraint::Length(width));
247 last_was_pinned = is_pinned;
248 }
249
250 widths
251}