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 "{}{}{}{}",
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
127fn 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 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 let is_current_row = ctx.is_selected_row(row_idx);
145
146 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 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 if !is_current_row && ctx.cell_matches_filter(val) {
161 cell = cell.style(Style::default().fg(Color::Magenta));
162 }
163
164 if is_pinned && !is_current_row {
166 cell = cell.style(Style::default().bg(Color::Rgb(20, 20, 40)));
167 }
168
169 cell = match ctx.selection_mode {
171 SelectionMode::Cell if is_current_row && is_selected_column => {
172 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 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 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 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 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
228fn calculate_column_widths(ctx: &TableRenderContext) -> Vec<Constraint> {
230 let mut widths: Vec<Constraint> = Vec::new();
231
232 if ctx.show_row_numbers {
234 widths.push(Constraint::Length(8)); }
236
237 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 if last_was_pinned && !is_pinned && ctx.pinned_count > 0 {
244 widths.push(Constraint::Length(1)); }
246
247 widths.push(Constraint::Length(width));
248 last_was_pinned = is_pinned;
249 }
250
251 widths
252}