sql_cli/ui/rendering/
table_render_context.rs

1// Table rendering context that encapsulates all data needed for rendering
2// This decouples the table renderer from TUI internals
3
4use crate::app_state_container::SelectionMode;
5use crate::buffer::AppMode;
6use crate::data::data_view::SortState;
7use std::ops::Range;
8
9/// All the data needed to render a table, collected in one place
10/// This allows the table renderer to be independent of TUI internals
11#[derive(Debug, Clone)]
12pub struct TableRenderContext {
13    // ========== Data Source ==========
14    /// Total number of rows in the dataset
15    pub row_count: usize,
16
17    /// Row indices to display (the visible viewport)
18    pub visible_row_indices: Vec<usize>,
19
20    /// The actual data to display (already formatted as strings)
21    /// Outer vec is rows, inner vec is columns
22    pub data_rows: Vec<Vec<String>>,
23
24    // ========== Column Information ==========
25    /// Column headers in visual order
26    pub column_headers: Vec<String>,
27
28    /// Column widths in visual order (matching `column_headers`)
29    pub column_widths: Vec<u16>,
30
31    /// Indices of pinned columns (in visual space)
32    pub pinned_column_indices: Vec<usize>,
33
34    /// Number of pinned columns (convenience field)
35    pub pinned_count: usize,
36
37    // ========== Selection & Navigation ==========
38    /// Currently selected row (absolute index, not viewport-relative)
39    pub selected_row: usize,
40
41    /// Currently selected column (visual index)
42    pub selected_column: usize,
43
44    /// Row viewport range (start..end absolute indices)
45    pub row_viewport: Range<usize>,
46
47    /// Selection mode (Cell or Row)
48    pub selection_mode: SelectionMode,
49
50    // ========== Visual Indicators ==========
51    /// Sort state (which column is sorted and how)
52    pub sort_state: Option<SortState>,
53
54    /// Whether to show row numbers
55    pub show_row_numbers: bool,
56
57    /// Current application mode (for title bar)
58    pub app_mode: AppMode,
59
60    // ========== Search & Filter ==========
61    /// Fuzzy filter pattern if active
62    pub fuzzy_filter_pattern: Option<String>,
63
64    /// Whether filter is case insensitive
65    pub case_insensitive: bool,
66
67    // ========== Layout Information ==========
68    /// Available width for the table (excluding borders)
69    pub available_width: u16,
70
71    /// Available height for the table (excluding borders)
72    pub available_height: u16,
73}
74
75impl TableRenderContext {
76    /// Check if a given row is the currently selected row
77    #[must_use]
78    pub fn is_selected_row(&self, viewport_row_index: usize) -> bool {
79        let absolute_row = self.row_viewport.start + viewport_row_index;
80        absolute_row == self.selected_row
81    }
82
83    /// Check if a given column is the currently selected column
84    #[must_use]
85    pub fn is_selected_column(&self, visual_column_index: usize) -> bool {
86        visual_column_index == self.selected_column
87    }
88
89    /// Check if a column is pinned
90    #[must_use]
91    pub fn is_pinned_column(&self, visual_column_index: usize) -> bool {
92        visual_column_index < self.pinned_count
93    }
94
95    /// Get the crosshair position (selected cell)
96    #[must_use]
97    pub fn get_crosshair(&self) -> (usize, usize) {
98        (self.selected_row, self.selected_column)
99    }
100
101    /// Check if we're at a specific cell
102    #[must_use]
103    pub fn is_crosshair_cell(&self, viewport_row_index: usize, visual_column_index: usize) -> bool {
104        self.is_selected_row(viewport_row_index) && self.is_selected_column(visual_column_index)
105    }
106
107    /// Get sort indicator for a column
108    #[must_use]
109    pub fn get_sort_indicator(&self, visual_column_index: usize) -> &str {
110        if let Some(ref sort) = self.sort_state {
111            if sort.column == Some(visual_column_index) {
112                match sort.order {
113                    crate::data::data_view::SortOrder::Ascending => " ↑",
114                    crate::data::data_view::SortOrder::Descending => " ↓",
115                    crate::data::data_view::SortOrder::None => "",
116                }
117            } else {
118                ""
119            }
120        } else {
121            ""
122        }
123    }
124
125    /// Check if a cell value matches the fuzzy filter
126    #[must_use]
127    pub fn cell_matches_filter(&self, cell_value: &str) -> bool {
128        if let Some(ref pattern) = self.fuzzy_filter_pattern {
129            if pattern.starts_with('\'') && pattern.len() > 1 {
130                // Exact match mode
131                let search_pattern = &pattern[1..];
132                if self.case_insensitive {
133                    cell_value
134                        .to_lowercase()
135                        .contains(&search_pattern.to_lowercase())
136                } else {
137                    cell_value.contains(search_pattern)
138                }
139            } else if !pattern.is_empty() {
140                // Fuzzy match mode
141                use fuzzy_matcher::skim::SkimMatcherV2;
142                use fuzzy_matcher::FuzzyMatcher;
143                let matcher = if self.case_insensitive {
144                    SkimMatcherV2::default().ignore_case()
145                } else {
146                    SkimMatcherV2::default().respect_case()
147                };
148                matcher
149                    .fuzzy_match(cell_value, pattern)
150                    .is_some_and(|score| score > 0)
151            } else {
152                false
153            }
154        } else {
155            false
156        }
157    }
158}
159
160/// Builder for `TableRenderContext` to make construction easier
161pub struct TableRenderContextBuilder {
162    context: TableRenderContext,
163}
164
165impl Default for TableRenderContextBuilder {
166    fn default() -> Self {
167        Self::new()
168    }
169}
170
171impl TableRenderContextBuilder {
172    #[must_use]
173    pub fn new() -> Self {
174        Self {
175            context: TableRenderContext {
176                row_count: 0,
177                visible_row_indices: Vec::new(),
178                data_rows: Vec::new(),
179                column_headers: Vec::new(),
180                column_widths: Vec::new(),
181                pinned_column_indices: Vec::new(),
182                pinned_count: 0,
183                selected_row: 0,
184                selected_column: 0,
185                row_viewport: 0..0,
186                selection_mode: SelectionMode::Cell,
187                sort_state: None,
188                show_row_numbers: false,
189                app_mode: AppMode::Results,
190                fuzzy_filter_pattern: None,
191                case_insensitive: false,
192                available_width: 0,
193                available_height: 0,
194            },
195        }
196    }
197
198    #[must_use]
199    pub fn row_count(mut self, count: usize) -> Self {
200        self.context.row_count = count;
201        self
202    }
203
204    #[must_use]
205    pub fn visible_rows(mut self, indices: Vec<usize>, data: Vec<Vec<String>>) -> Self {
206        self.context.visible_row_indices = indices;
207        self.context.data_rows = data;
208        self
209    }
210
211    #[must_use]
212    pub fn columns(mut self, headers: Vec<String>, widths: Vec<u16>) -> Self {
213        self.context.column_headers = headers;
214        self.context.column_widths = widths;
215        self
216    }
217
218    #[must_use]
219    pub fn pinned_columns(mut self, indices: Vec<usize>) -> Self {
220        self.context.pinned_count = indices.len();
221        self.context.pinned_column_indices = indices;
222        self
223    }
224
225    #[must_use]
226    pub fn selection(mut self, row: usize, column: usize, mode: SelectionMode) -> Self {
227        self.context.selected_row = row;
228        self.context.selected_column = column;
229        self.context.selection_mode = mode;
230        self
231    }
232
233    #[must_use]
234    pub fn row_viewport(mut self, range: Range<usize>) -> Self {
235        self.context.row_viewport = range;
236        self
237    }
238
239    #[must_use]
240    pub fn sort_state(mut self, state: Option<SortState>) -> Self {
241        self.context.sort_state = state;
242        self
243    }
244
245    #[must_use]
246    pub fn display_options(mut self, show_row_numbers: bool, app_mode: AppMode) -> Self {
247        self.context.show_row_numbers = show_row_numbers;
248        self.context.app_mode = app_mode;
249        self
250    }
251
252    #[must_use]
253    pub fn filter(mut self, pattern: Option<String>, case_insensitive: bool) -> Self {
254        self.context.fuzzy_filter_pattern = pattern;
255        self.context.case_insensitive = case_insensitive;
256        self
257    }
258
259    #[must_use]
260    pub fn dimensions(mut self, width: u16, height: u16) -> Self {
261        self.context.available_width = width;
262        self.context.available_height = height;
263        self
264    }
265
266    #[must_use]
267    pub fn build(self) -> TableRenderContext {
268        self.context
269    }
270}