sql_cli/ui/rendering/
table_render_context.rs1use crate::app_state_container::SelectionMode;
5use crate::buffer::AppMode;
6use crate::data::data_view::SortState;
7use std::ops::Range;
8
9#[derive(Debug, Clone)]
12pub struct TableRenderContext {
13 pub row_count: usize,
16
17 pub visible_row_indices: Vec<usize>,
19
20 pub data_rows: Vec<Vec<String>>,
23
24 pub column_headers: Vec<String>,
27
28 pub column_widths: Vec<u16>,
30
31 pub pinned_column_indices: Vec<usize>,
33
34 pub pinned_count: usize,
36
37 pub selected_row: usize,
40
41 pub selected_column: usize,
43
44 pub row_viewport: Range<usize>,
46
47 pub selection_mode: SelectionMode,
49
50 pub sort_state: Option<SortState>,
53
54 pub show_row_numbers: bool,
56
57 pub app_mode: AppMode,
59
60 pub fuzzy_filter_pattern: Option<String>,
63
64 pub case_insensitive: bool,
66
67 pub available_width: u16,
70
71 pub available_height: u16,
73}
74
75impl TableRenderContext {
76 #[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 #[must_use]
85 pub fn is_selected_column(&self, visual_column_index: usize) -> bool {
86 visual_column_index == self.selected_column
87 }
88
89 #[must_use]
91 pub fn is_pinned_column(&self, visual_column_index: usize) -> bool {
92 visual_column_index < self.pinned_count
93 }
94
95 #[must_use]
97 pub fn get_crosshair(&self) -> (usize, usize) {
98 (self.selected_row, self.selected_column)
99 }
100
101 #[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 #[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 #[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 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 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
160pub 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}