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 - spacing) + 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 ui.add_space(16.0);
291 }
292
293 changed
294 }
295
296 pub fn handle_row_selection(&mut self, modifiers: egui::Modifiers, row_index: usize) {
297 let selected_rows = &mut self.selected_rows;
298 let active_rows = &self.active_rows;
299
300 let row_idx_u32 = row_index as u32;
301
302 if modifiers.command || modifiers.ctrl {
303 if selected_rows.contains(row_idx_u32) {
304 selected_rows.remove(row_idx_u32);
305 self.last_clicked_visible_index = None;
306 } else {
307 selected_rows.insert(row_idx_u32);
308 self.last_clicked_visible_index = active_rows.iter().position(|&r| r == row_index);
309 }
310 } else if modifiers.shift && self.last_clicked_visible_index.is_some() {
311 if let Some(anchor_visible_pos) = self.last_clicked_visible_index
312 && let Some(current_visible_pos) = active_rows.iter().position(|&r| r == row_index)
313 {
314 let start = anchor_visible_pos.min(current_visible_pos);
315 let end = anchor_visible_pos.max(current_visible_pos);
316 for visible_idx in start..=end {
317 if let Some(&actual_row_idx) = active_rows.get(visible_idx) {
318 selected_rows.insert(actual_row_idx as u32);
319 }
320 }
321 }
322 } else if selected_rows.len() == 1 && selected_rows.contains(row_idx_u32) {
323 selected_rows.clear();
324 self.last_clicked_visible_index = None;
325 } else {
326 selected_rows.clear();
327 selected_rows.insert(row_idx_u32);
328 self.last_clicked_visible_index = active_rows.iter().position(|&r| r == row_index);
329 }
330 }
331
332 pub fn rebuild_tree_filter_cache(&mut self, provider: &dyn TableProvider) {
334 let row_count = provider.row_count();
335
336 let is_empty = self.filter_matches.is_empty();
338 if self.filter_cache_dirty || is_empty {
339 self.sorted_children_cache.clear();
340 }
341
342 if !self.filter_cache_dirty && !is_empty {
343 return; }
345 self.filter_cache_dirty = false;
346 self.filter_matches.clear();
347
348 let active_filters = self.get_filter_state();
349 if active_filters.is_empty() {
350 self.filter_matches.insert_range(0..row_count as u32);
352 return;
353 }
354
355 for row_idx in (0..row_count).rev() {
357 let highlight = self.highlights.get_usize(row_idx);
358 let matches = provider.row_matches(self, row_idx, &active_filters, highlight);
359
360 if matches {
361 self.filter_matches.insert(row_idx as u32);
362 }
363
364 if self.filter_matches.contains(row_idx as u32)
366 && let Some(parent_idx) = provider.row_parent(row_idx)
367 {
368 self.filter_matches.insert(parent_idx as u32);
369 }
370 }
371 }
372
373 pub fn flatten_tree(&mut self, provider: &dyn TableProvider) {
375 self.rebuild_tree_filter_cache(provider);
376
377 let mut active = Vec::with_capacity(provider.row_count());
378 if provider.row_count() > 0 {
379 self.flatten_tree_impl(provider, 0, &mut active);
381 }
382 self.active_rows = active;
383 }
384
385 fn flatten_tree_impl(
386 &mut self,
387 provider: &dyn TableProvider,
388 row_idx: usize,
389 out: &mut Vec<usize>,
390 ) {
391 if !self.filter_matches.contains(row_idx as u32) {
393 return; }
395
396 out.push(row_idx);
397
398 let is_expanded = self.expanded_rows.contains(row_idx as u32);
399 if is_expanded {
400 let sorted_children = if let Some(cached) = self.sorted_children_cache.get(&row_idx) {
402 cached.clone()
403 } else {
404 let mut children = provider.row_children(row_idx);
405 let sort_state = self.get_sort_state();
406 if let Some((sort_col, sort_up)) = sort_state {
407 let _ = provider.sort_active_rows(&mut children, sort_col, sort_up);
408 }
409 self.sorted_children_cache.insert(row_idx, children.clone());
410 children
411 };
412
413 for child_idx in sorted_children {
414 self.flatten_tree_impl(provider, child_idx, out);
415 }
416 }
417 }
418}
419
420pub trait TableStateExt {
421 fn collect_responses(&mut self, responses: Vec<ColResponse>) -> TableChanges;
422}
423
424impl TableStateExt for TableState {
425 fn collect_responses(&mut self, responses: Vec<ColResponse>) -> TableChanges {
426 let mut filter_update = None;
427 let mut sort_update = None;
428 let mut filter_state = Vec::with_capacity(responses.len());
429
430 if self.columns.len() < responses.len() {
432 self.columns
433 .resize_with(responses.len(), ColumnState::default);
434 }
435
436 for (col_index, response) in responses.into_iter().enumerate() {
437 let col = &mut self.columns[col_index];
438 let old_active = !col.response.filtering.is_empty();
439 let new_active = !response.filtering.is_empty();
440
441 if !old_active && new_active {
442 filter_update = Some(FilterUpdate::Added(col_index, response.filtering.clone()));
443 self.filter_cache_dirty = true;
444 } else if old_active && !new_active {
445 filter_update = Some(FilterUpdate::Removed(col_index));
446 self.filter_cache_dirty = true;
447 } else if old_active && new_active {
448 let old = &col.response.filtering;
449 let new = &response.filtering;
450
451 if old.search.text() != new.search.text()
452 || old.search.options() != new.search.options()
453 || old.highlight != new.highlight
454 {
455 filter_update = Some(FilterUpdate::Modified(
456 col_index,
457 response.filtering.clone(),
458 ));
459 self.filter_cache_dirty = true;
460 }
461 }
462
463 if new_active {
464 filter_state.push((col_index, response.filtering.clone()));
465 }
466
467 col.response = response;
468 if col.response.to_sort {
469 col.response.to_sort = false;
470 sort_update = Some(col_index);
471 }
472 }
473
474 let sort_state = self.get_sort_state();
475
476 TableChanges {
477 filter_update,
478 sort_update,
479 filter_state,
480 sort_state,
481 }
482 }
483}