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    pub fn is_selected_row(&self, viewport_row_index: usize) -> bool {
78        let absolute_row = self.row_viewport.start + viewport_row_index;
79        absolute_row == self.selected_row
80    }
81
82    /// Check if a given column is the currently selected column
83    pub fn is_selected_column(&self, visual_column_index: usize) -> bool {
84        visual_column_index == self.selected_column
85    }
86
87    /// Check if a column is pinned
88    pub fn is_pinned_column(&self, visual_column_index: usize) -> bool {
89        visual_column_index < self.pinned_count
90    }
91
92    /// Get the crosshair position (selected cell)
93    pub fn get_crosshair(&self) -> (usize, usize) {
94        (self.selected_row, self.selected_column)
95    }
96
97    /// Check if we're at a specific cell
98    pub fn is_crosshair_cell(&self, viewport_row_index: usize, visual_column_index: usize) -> bool {
99        self.is_selected_row(viewport_row_index) && self.is_selected_column(visual_column_index)
100    }
101
102    /// Get sort indicator for a column
103    pub fn get_sort_indicator(&self, visual_column_index: usize) -> &str {
104        if let Some(ref sort) = self.sort_state {
105            if sort.column == Some(visual_column_index) {
106                match sort.order {
107                    crate::data::data_view::SortOrder::Ascending => " ↑",
108                    crate::data::data_view::SortOrder::Descending => " ↓",
109                    crate::data::data_view::SortOrder::None => "",
110                }
111            } else {
112                ""
113            }
114        } else {
115            ""
116        }
117    }
118
119    /// Check if a cell value matches the fuzzy filter
120    pub fn cell_matches_filter(&self, cell_value: &str) -> bool {
121        if let Some(ref pattern) = self.fuzzy_filter_pattern {
122            if pattern.starts_with('\'') && pattern.len() > 1 {
123                // Exact match mode
124                let search_pattern = &pattern[1..];
125                if self.case_insensitive {
126                    cell_value
127                        .to_lowercase()
128                        .contains(&search_pattern.to_lowercase())
129                } else {
130                    cell_value.contains(search_pattern)
131                }
132            } else if !pattern.is_empty() {
133                // Fuzzy match mode
134                use fuzzy_matcher::skim::SkimMatcherV2;
135                use fuzzy_matcher::FuzzyMatcher;
136                let matcher = if self.case_insensitive {
137                    SkimMatcherV2::default().ignore_case()
138                } else {
139                    SkimMatcherV2::default().respect_case()
140                };
141                matcher
142                    .fuzzy_match(cell_value, pattern)
143                    .map(|score| score > 0)
144                    .unwrap_or(false)
145            } else {
146                false
147            }
148        } else {
149            false
150        }
151    }
152}
153
154/// Builder for TableRenderContext to make construction easier
155pub struct TableRenderContextBuilder {
156    context: TableRenderContext,
157}
158
159impl TableRenderContextBuilder {
160    pub fn new() -> Self {
161        Self {
162            context: TableRenderContext {
163                row_count: 0,
164                visible_row_indices: Vec::new(),
165                data_rows: Vec::new(),
166                column_headers: Vec::new(),
167                column_widths: Vec::new(),
168                pinned_column_indices: Vec::new(),
169                pinned_count: 0,
170                selected_row: 0,
171                selected_column: 0,
172                row_viewport: 0..0,
173                selection_mode: SelectionMode::Cell,
174                sort_state: None,
175                show_row_numbers: false,
176                app_mode: AppMode::Results,
177                fuzzy_filter_pattern: None,
178                case_insensitive: false,
179                available_width: 0,
180                available_height: 0,
181            },
182        }
183    }
184
185    pub fn row_count(mut self, count: usize) -> Self {
186        self.context.row_count = count;
187        self
188    }
189
190    pub fn visible_rows(mut self, indices: Vec<usize>, data: Vec<Vec<String>>) -> Self {
191        self.context.visible_row_indices = indices;
192        self.context.data_rows = data;
193        self
194    }
195
196    pub fn columns(mut self, headers: Vec<String>, widths: Vec<u16>) -> Self {
197        self.context.column_headers = headers;
198        self.context.column_widths = widths;
199        self
200    }
201
202    pub fn pinned_columns(mut self, indices: Vec<usize>) -> Self {
203        self.context.pinned_count = indices.len();
204        self.context.pinned_column_indices = indices;
205        self
206    }
207
208    pub fn selection(mut self, row: usize, column: usize, mode: SelectionMode) -> Self {
209        self.context.selected_row = row;
210        self.context.selected_column = column;
211        self.context.selection_mode = mode;
212        self
213    }
214
215    pub fn row_viewport(mut self, range: Range<usize>) -> Self {
216        self.context.row_viewport = range;
217        self
218    }
219
220    pub fn sort_state(mut self, state: Option<SortState>) -> Self {
221        self.context.sort_state = state;
222        self
223    }
224
225    pub fn display_options(mut self, show_row_numbers: bool, app_mode: AppMode) -> Self {
226        self.context.show_row_numbers = show_row_numbers;
227        self.context.app_mode = app_mode;
228        self
229    }
230
231    pub fn filter(mut self, pattern: Option<String>, case_insensitive: bool) -> Self {
232        self.context.fuzzy_filter_pattern = pattern;
233        self.context.case_insensitive = case_insensitive;
234        self
235    }
236
237    pub fn dimensions(mut self, width: u16, height: u16) -> Self {
238        self.context.available_width = width;
239        self.context.available_height = height;
240        self
241    }
242
243    pub fn build(self) -> TableRenderContext {
244        self.context
245    }
246}