1use std::{collections::HashMap, fmt::Write as _};
2
3use compact_str::CompactString;
4use fluent_zero::t;
5use roaring::RoaringBitmap;
6
7use super::{
8 error::TableError,
9 filter::Filter,
10 header::{ColResponse, ColumnState},
11 highlights::Highlights,
12 operations::TableProvider,
13};
14
15#[derive(Debug)]
16pub enum FilterUpdate {
17 Added(usize, Filter),
18 Removed(usize),
19 Modified(usize, Filter),
20}
21
22#[derive(Debug)]
23pub struct TableChanges {
24 pub filter_update: Option<FilterUpdate>,
25 pub sort_update: Option<usize>,
26 pub filter_state: Vec<(usize, Filter)>,
27 pub sort_state: Option<(usize, bool)>,
28}
29
30#[derive(Debug, Default)]
31pub struct TableState {
32 pub id: CompactString,
33 pub columns: Vec<ColumnState>,
34 pub highlights: Highlights,
35 pub highlights_changed: bool,
36 pub active_rows: Vec<usize>,
37 pub selected_rows: RoaringBitmap,
38 pub expanded_rows: RoaringBitmap,
39 pub last_clicked_visible_index: Option<usize>,
40 pub filter_matches: RoaringBitmap,
41 pub filter_cache_dirty: bool,
42 pub sorted_children_cache: HashMap<usize, Vec<usize>, ahash::RandomState>,
43}
44
45impl TableState {
46 #[must_use]
47 pub fn new(id: impl Into<CompactString>, row_count: usize) -> Self {
48 Self {
49 id: id.into(),
50 active_rows: (0..row_count).collect(),
51 filter_cache_dirty: true, ..Default::default()
53 }
54 }
55
56 #[must_use]
57 pub fn get_filter_state(&self) -> Vec<(usize, Filter)> {
58 self.columns
59 .iter()
60 .enumerate()
61 .filter_map(|(col_index, col)| {
62 if col.response.filtering.is_empty() {
63 None
64 } else {
65 Some((col_index, col.response.filtering.clone()))
66 }
67 })
68 .collect()
69 }
70
71 #[must_use]
72 pub fn get_sort_state(&self) -> Option<(usize, bool)> {
73 self.columns
74 .iter()
75 .enumerate()
76 .find_map(|(col_index, col)| col.sort_up.map(|sort_up| (col_index, sort_up)))
77 }
78
79 #[must_use]
80 pub fn counts_header(&self, row_len: usize) -> String {
81 let mut counts = String::with_capacity(64);
82 counts.push_str(&t!("total-rows", { "count" => row_len }));
83
84 let active_rows_count = self.active_rows.len();
85 if active_rows_count != row_len {
86 counts.push_str(", ");
87 let _ = write!(counts, "{active_rows_count} {}", t!("passing-filter"));
88 }
89
90 let selected_rows_count = self.selected_rows.len();
91 if selected_rows_count != 0 {
92 counts.push_str(", ");
93 let _ = write!(counts, "{selected_rows_count} {}", t!("selected"));
94 }
95
96 counts
97 }
98
99 pub fn process_responses(
100 &mut self,
101 provider: &dyn TableProvider,
102 responses: Vec<ColResponse>,
103 ) -> Result<(), TableError> {
104 let changes = self.collect_responses(responses);
105 let filter_update = changes.filter_update;
106 let sort_update = changes.sort_update;
107 let filter_state = changes.filter_state;
108 let sort_state = changes.sort_state;
109
110 if self.highlights_changed {
111 self.highlights_changed = false;
112 self.filter_cache_dirty = true;
113 self.sorted_children_cache.clear();
114
115 if !provider.is_tree() {
117 self.apply_all_filters(provider, &filter_state)?;
118 }
119
120 if let Some((sort_col, sort_up)) = sort_state {
121 provider.sort_active_rows(&mut self.active_rows, sort_col, sort_up)?;
122 }
123 return Ok(());
124 }
125
126 if let Some(update) = filter_update {
127 self.sorted_children_cache.clear();
128
129 if !provider.is_tree() {
131 match update {
132 FilterUpdate::Added(col, filter) => {
133 self.apply_incremental_filter(provider, col, &filter)?;
134 }
135 FilterUpdate::Removed(_) | FilterUpdate::Modified(_, _) => {
136 self.apply_all_filters(provider, &filter_state)?;
137 }
138 }
139 }
140
141 if let Some((sort_col, sort_up)) = sort_state {
142 provider.sort_active_rows(&mut self.active_rows, sort_col, sort_up)?;
143 }
144 } else if let Some(sort_col) = sort_update {
145 self.sorted_children_cache.clear();
146 if let Some((already_sorted_col, sort_up)) = sort_state {
147 if already_sorted_col == sort_col {
148 let column = self
149 .columns
150 .get_mut(sort_col)
151 .ok_or(TableError::CorruptedState)?;
152
153 let new_sort_up = !sort_up;
154 column.sort_up = Some(new_sort_up);
155
156 provider.sort_active_rows(&mut self.active_rows, sort_col, new_sort_up)?;
158 } else {
159 self.apply_new_sort(provider, sort_col)?;
160 }
161 } else {
162 self.apply_new_sort(provider, sort_col)?;
163 }
164 }
165
166 Ok(())
167 }
168
169 pub fn apply_new_sort(
170 &mut self,
171 provider: &dyn TableProvider,
172 sort_col: usize,
173 ) -> Result<(), TableError> {
174 for (i, column) in self.columns.iter_mut().enumerate() {
175 column.sort_up = if i == sort_col { Some(true) } else { None };
176 }
177 provider.sort_active_rows(&mut self.active_rows, sort_col, true)
178 }
179
180 pub fn apply_all_filters(
181 &mut self,
182 provider: &dyn TableProvider,
183 filters: &[(usize, Filter)],
184 ) -> Result<(), TableError> {
185 self.active_rows = provider.filter_rows(self, filters)?;
186 Ok(())
187 }
188
189 pub fn apply_incremental_filter(
190 &mut self,
191 provider: &dyn TableProvider,
192 filter_col: usize,
193 filter: &Filter,
194 ) -> Result<(), TableError> {
195 let mut active_mask = vec![false; provider.row_count()];
196 for &idx in &self.active_rows {
197 if idx < active_mask.len() {
198 active_mask[idx] = true;
199 }
200 }
201
202 let mut new_active = Vec::with_capacity(self.active_rows.len());
203 let mut row_idx = 0;
204
205 provider.for_all_rows(&mut |row| {
206 if row_idx < active_mask.len() && active_mask[row_idx] {
207 let highlight = self.highlights.get_usize(row_idx);
208 if let Some(cell) = row.get(filter_col)
209 && filter.matches(&cell.0, highlight)
210 {
211 new_active.push(row_idx);
212 }
213 }
214 row_idx += 1;
215 Ok(())
216 })?;
217
218 self.active_rows = new_active;
219 Ok(())
220 }
221
222 pub fn show_tree_cell(
225 &mut self,
226 ui: &mut egui::Ui,
227 row_index: usize,
228 hierarchy: crate::operations::RowHierarchy,
229 ) -> bool {
230 let mut changed = false;
231
232 #[allow(clippy::cast_precision_loss)]
233 let spacing = hierarchy.indent_level as f32 * 16.0;
234 if spacing > 0.0 {
235 ui.add_space(spacing);
236
237 let rect = ui.max_rect();
238 let painter = ui.painter();
239 let stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(65, 65, 65)); for i in 0..hierarchy.indent_level {
242 #[allow(clippy::cast_precision_loss)]
243 let x = (i as f32).mul_add(16.0, rect.min.x) + 8.0;
244
245 let dash_length = 2.0;
247 let gap_length = 2.0;
248 let step = dash_length + gap_length;
249 let total_height = rect.max.y - rect.min.y;
250 if total_height > 0.0 {
251 let num_steps = (total_height / step).ceil() as usize;
252 for step_idx in 0..num_steps {
253 #[allow(clippy::cast_precision_loss)]
254 let segment_y = (step_idx as f32).mul_add(step, rect.min.y);
255 let next_y = (segment_y + dash_length).min(rect.max.y);
256 painter.line_segment(
257 [egui::pos2(x, segment_y), egui::pos2(x, next_y)],
258 stroke,
259 );
260 }
261 }
262 }
263 }
264
265 if hierarchy.has_children {
266 let arrow = if hierarchy.is_expanded { "⏷" } else { "⏵" };
267
268 let arrow_color = if hierarchy.is_expanded {
269 ui.visuals().widgets.active.text_color()
270 } else {
271 ui.visuals().widgets.inactive.text_color()
272 };
273
274 let rich_arrow = egui::RichText::new(arrow).color(arrow_color);
275 if ui
276 .selectable_label(hierarchy.is_expanded, rich_arrow)
277 .clicked()
278 {
279 if hierarchy.is_expanded {
280 self.expanded_rows.remove(row_index as u32);
281 } else {
282 self.expanded_rows.insert(row_index as u32);
283 }
284
285 self.sorted_children_cache.remove(&row_index);
286 changed = true;
287 }
288 } else {
289 let dummy_arrow = egui::RichText::new("⏵").color(egui::Color32::TRANSPARENT);
291 ui.add_enabled_ui(false, |ui| {
292 let _ = ui.selectable_label(false, dummy_arrow);
293 });
294 }
295
296 changed
297 }
298
299 pub fn handle_row_selection(&mut self, modifiers: egui::Modifiers, row_index: usize) {
300 let selected_rows = &mut self.selected_rows;
301 let active_rows = &self.active_rows;
302
303 let row_idx_u32 = row_index as u32;
304
305 if modifiers.command || modifiers.ctrl {
306 if selected_rows.contains(row_idx_u32) {
307 selected_rows.remove(row_idx_u32);
308 self.last_clicked_visible_index = None;
309 } else {
310 selected_rows.insert(row_idx_u32);
311 self.last_clicked_visible_index = active_rows.iter().position(|&r| r == row_index);
312 }
313 } else if modifiers.shift && self.last_clicked_visible_index.is_some() {
314 if let Some(anchor_visible_pos) = self.last_clicked_visible_index
315 && let Some(current_visible_pos) = active_rows.iter().position(|&r| r == row_index)
316 {
317 let start = anchor_visible_pos.min(current_visible_pos);
318 let end = anchor_visible_pos.max(current_visible_pos);
319 for visible_idx in start..=end {
320 if let Some(&actual_row_idx) = active_rows.get(visible_idx) {
321 selected_rows.insert(actual_row_idx as u32);
322 }
323 }
324 }
325 } else if selected_rows.len() == 1 && selected_rows.contains(row_idx_u32) {
326 selected_rows.clear();
327 self.last_clicked_visible_index = None;
328 } else {
329 selected_rows.clear();
330 selected_rows.insert(row_idx_u32);
331 self.last_clicked_visible_index = active_rows.iter().position(|&r| r == row_index);
332 }
333 }
334
335 pub fn rebuild_tree_filter_cache(&mut self, provider: &dyn TableProvider) {
337 let row_count = provider.row_count();
338
339 let is_empty = self.filter_matches.is_empty();
341 if self.filter_cache_dirty || is_empty {
342 self.sorted_children_cache.clear();
343 }
344
345 if !self.filter_cache_dirty && !is_empty {
346 return; }
348 self.filter_cache_dirty = false;
349 self.filter_matches.clear();
350
351 let active_filters = self.get_filter_state();
352 if active_filters.is_empty() {
353 self.filter_matches.insert_range(0..row_count as u32);
355 return;
356 }
357
358 for row_idx in (0..row_count).rev() {
360 let highlight = self.highlights.get_usize(row_idx);
361 let matches = provider.row_matches(self, row_idx, &active_filters, highlight);
362
363 if matches {
364 self.filter_matches.insert(row_idx as u32);
365 }
366
367 if self.filter_matches.contains(row_idx as u32)
369 && let Some(parent_idx) = provider.row_parent(row_idx)
370 {
371 self.filter_matches.insert(parent_idx as u32);
372 }
373 }
374 }
375
376 pub fn flatten_tree(&mut self, provider: &dyn TableProvider) {
378 self.rebuild_tree_filter_cache(provider);
379
380 let mut active = Vec::with_capacity(provider.row_count());
381 if provider.row_count() > 0 {
382 self.flatten_tree_impl(provider, 0, &mut active);
384 }
385 self.active_rows = active;
386 }
387
388 fn flatten_tree_impl(
389 &mut self,
390 provider: &dyn TableProvider,
391 row_idx: usize,
392 out: &mut Vec<usize>,
393 ) {
394 if !self.filter_matches.contains(row_idx as u32) {
396 return; }
398
399 out.push(row_idx);
400
401 let is_expanded = self.expanded_rows.contains(row_idx as u32);
402 if is_expanded {
403 let sorted_children = if let Some(cached) = self.sorted_children_cache.get(&row_idx) {
405 cached.clone()
406 } else {
407 let mut children = provider.row_children(row_idx);
408 let sort_state = self.get_sort_state();
409 if let Some((sort_col, sort_up)) = sort_state {
410 let _ = provider.sort_active_rows(&mut children, sort_col, sort_up);
411 }
412 self.sorted_children_cache.insert(row_idx, children.clone());
413 children
414 };
415
416 for child_idx in sorted_children {
417 self.flatten_tree_impl(provider, child_idx, out);
418 }
419 }
420 }
421}
422
423pub trait TableStateExt {
424 fn collect_responses(&mut self, responses: Vec<ColResponse>) -> TableChanges;
425}
426
427impl TableStateExt for TableState {
428 fn collect_responses(&mut self, responses: Vec<ColResponse>) -> TableChanges {
429 let mut filter_update = None;
430 let mut sort_update = None;
431 let mut filter_state = Vec::with_capacity(responses.len());
432
433 if self.columns.len() < responses.len() {
435 self.columns
436 .resize_with(responses.len(), ColumnState::default);
437 }
438
439 for (col_index, response) in responses.into_iter().enumerate() {
440 let col = &mut self.columns[col_index];
441 let old_active = !col.response.filtering.is_empty();
442 let new_active = !response.filtering.is_empty();
443
444 if !old_active && new_active {
445 filter_update = Some(FilterUpdate::Added(col_index, response.filtering.clone()));
446 self.filter_cache_dirty = true;
447 } else if old_active && !new_active {
448 filter_update = Some(FilterUpdate::Removed(col_index));
449 self.filter_cache_dirty = true;
450 } else if old_active && new_active {
451 let old = &col.response.filtering;
452 let new = &response.filtering;
453
454 if old.search.text() != new.search.text()
455 || old.search.options() != new.search.options()
456 || old.highlight != new.highlight
457 {
458 filter_update = Some(FilterUpdate::Modified(
459 col_index,
460 response.filtering.clone(),
461 ));
462 self.filter_cache_dirty = true;
463 }
464 }
465
466 if new_active {
467 filter_state.push((col_index, response.filtering.clone()));
468 }
469
470 col.response = response;
471 if col.response.to_sort {
472 col.response.to_sort = false;
473 sort_update = Some(col_index);
474 }
475 }
476
477 let sort_state = self.get_sort_state();
478
479 TableChanges {
480 filter_update,
481 sort_update,
482 filter_state,
483 sort_state,
484 }
485 }
486}