1use std::{collections::HashMap, fmt::Write as _, sync::Arc};
4
5use compact_str::CompactString;
6use fluent_zero::t;
7use roaring::RoaringBitmap;
8
9use crate::operations::Row as _;
10
11use super::{
12 error::TableError,
13 filter::Filter,
14 header::{ColResponse, ColumnState},
15 highlights::Highlights,
16 operations::{RowHierarchy, TableProvider},
17};
18
19#[derive(Debug)]
20pub enum FilterUpdate {
21 Added(usize, Filter),
22 Removed(usize),
23 Modified(usize, Filter),
24}
25
26#[derive(Debug)]
27pub struct TableChanges {
28 pub filter_update: Option<FilterUpdate>,
29 pub sort_update: Option<usize>,
30 pub filter_state: Vec<(usize, Filter)>,
31 pub sort_state: Option<(usize, bool)>,
32}
33
34#[derive(Debug, Default)]
36pub struct TableState {
37 pub id: CompactString,
38 pub columns: Vec<ColumnState>,
39 pub highlights: Highlights,
40 pub highlights_changed: bool,
41 pub active_rows: Vec<usize>,
42 pub selected_rows: RoaringBitmap,
43 pub expanded_rows: RoaringBitmap,
44 pub last_clicked_visible_index: Option<usize>,
45 pub filter_matches: RoaringBitmap,
46 pub filter_cache_dirty: bool,
47 pub sorted_children_cache: HashMap<usize, Arc<Vec<usize>>, ahash::RandomState>,
48}
49
50impl TableState {
51 #[must_use]
53 pub fn new(id: impl Into<CompactString>, row_count: usize) -> Self {
54 Self {
55 id: id.into(),
56 active_rows: (0..row_count).collect(),
57 filter_cache_dirty: true, ..Default::default()
59 }
60 }
61
62 #[must_use]
64 pub fn get_filter_state(&self) -> Vec<(usize, Filter)> {
65 self.columns
66 .iter()
67 .enumerate()
68 .filter_map(|(col_index, col)| {
69 if col.response.filtering.is_empty() {
70 None
71 } else {
72 Some((col_index, col.response.filtering.clone()))
73 }
74 })
75 .collect()
76 }
77
78 #[must_use]
80 pub fn get_sort_state(&self) -> Option<(usize, bool)> {
81 self.columns
82 .iter()
83 .enumerate()
84 .find_map(|(col_index, col)| col.sort_up.map(|sort_up| (col_index, sort_up)))
85 }
86
87 #[must_use]
89 pub fn counts_header(&self, row_len: usize) -> String {
90 let mut counts = String::with_capacity(64);
91 counts.push_str(&t!("total-rows", { "count" => row_len }));
92
93 let active_rows_count = self.active_rows.len();
94 if active_rows_count != row_len {
95 counts.push_str(", ");
96 let _ = write!(counts, "{active_rows_count} {}", t!("passing-filter"));
97 }
98
99 let selected_rows_count = self.selected_rows.len();
100 if selected_rows_count != 0 {
101 counts.push_str(", ");
102 let _ = write!(counts, "{selected_rows_count} {}", t!("selected"));
103 }
104
105 counts
106 }
107
108 pub fn process_responses(
110 &mut self,
111 provider: &dyn TableProvider,
112 responses: Vec<ColResponse>,
113 ) -> Result<(), TableError> {
114 let changes = self.collect_responses(responses);
115 let filter_update = changes.filter_update;
116 let sort_update = changes.sort_update;
117 let filter_state = changes.filter_state;
118 let sort_state = changes.sort_state;
119
120 if self.highlights_changed {
121 self.highlights_changed = false;
122 self.filter_cache_dirty = true;
123 self.sorted_children_cache.clear();
124
125 if !provider.is_tree() {
127 self.apply_all_filters(provider, &filter_state)?;
128 }
129
130 if let Some((sort_col, sort_up)) = sort_state {
131 provider.sort_active_rows(&mut self.active_rows, sort_col, sort_up)?;
132 }
133 return Ok(());
134 }
135
136 if let Some(update) = filter_update {
137 self.sorted_children_cache.clear();
138
139 if !provider.is_tree() {
141 match update {
142 FilterUpdate::Added(col, filter) => {
143 self.apply_incremental_filter(provider, col, &filter)?;
144 }
145 FilterUpdate::Removed(_) | FilterUpdate::Modified(_, _) => {
146 self.apply_all_filters(provider, &filter_state)?;
147 }
148 }
149 }
150
151 if let Some((sort_col, sort_up)) = sort_state {
152 provider.sort_active_rows(&mut self.active_rows, sort_col, sort_up)?;
153 }
154 } else if let Some(sort_col) = sort_update {
155 self.sorted_children_cache.clear();
156 if let Some((already_sorted_col, sort_up)) = sort_state {
157 if already_sorted_col == sort_col {
158 let column = self
159 .columns
160 .get_mut(sort_col)
161 .ok_or(TableError::CorruptedState)?;
162
163 let new_sort_up = !sort_up;
164 column.sort_up = Some(new_sort_up);
165
166 provider.sort_active_rows(&mut self.active_rows, sort_col, new_sort_up)?;
168 } else {
169 self.apply_new_sort(provider, sort_col)?;
170 }
171 } else {
172 self.apply_new_sort(provider, sort_col)?;
173 }
174 }
175
176 Ok(())
177 }
178
179 pub fn apply_new_sort(
181 &mut self,
182 provider: &dyn TableProvider,
183 sort_col: usize,
184 ) -> Result<(), TableError> {
185 for (i, column) in self.columns.iter_mut().enumerate() {
186 column.sort_up = if i == sort_col { Some(true) } else { None };
187 }
188 provider.sort_active_rows(&mut self.active_rows, sort_col, true)
189 }
190
191 pub fn apply_all_filters(
193 &mut self,
194 provider: &dyn TableProvider,
195 filters: &[(usize, Filter)],
196 ) -> Result<(), TableError> {
197 self.active_rows = provider.filter_rows(self, filters)?;
198 Ok(())
199 }
200
201 pub fn refresh_view(&mut self, provider: &dyn TableProvider) -> Result<bool, TableError> {
209 if !self.filter_cache_dirty {
210 return Ok(false);
211 }
212
213 if provider.is_tree() {
214 self.flatten_tree(provider);
215 } else {
216 self.filter_cache_dirty = false;
217 let filter_state = self.get_filter_state();
218 self.apply_all_filters(provider, &filter_state)?;
219 if let Some((sort_col, sort_up)) = self.get_sort_state() {
220 provider.sort_active_rows(&mut self.active_rows, sort_col, sort_up)?;
221 }
222 }
223
224 Ok(true)
225 }
226
227 pub fn apply_incremental_filter(
229 &mut self,
230 provider: &dyn TableProvider,
231 filter_col: usize,
232 filter: &Filter,
233 ) -> Result<(), TableError> {
234 let mut new_active = Vec::with_capacity(self.active_rows.len());
235
236 if self.active_rows.len() < 1000 {
238 for &row_idx in &self.active_rows {
239 if let Some(row) = provider.row_at(row_idx)? {
240 let highlight = self.highlights.get_usize(row_idx);
241 if let Some(cell) = row.cell(filter_col)
242 && filter.matches(&cell.0, highlight)
243 {
244 new_active.push(row_idx);
245 }
246 }
247 }
248 } else {
249 let active_set: RoaringBitmap = self.active_rows.iter().map(|&i| i as u32).collect();
251 let mut row_idx = 0;
252 provider.for_all_rows(&mut |row| {
253 if active_set.contains(row_idx as u32) {
254 let highlight = self.highlights.get_usize(row_idx);
255 if let Some(cell) = row.cell(filter_col)
256 && filter.matches(&cell.0, highlight)
257 {
258 new_active.push(row_idx);
259 }
260 }
261 row_idx += 1;
262 Ok(())
263 })?;
264 }
265
266 self.active_rows = new_active;
267 Ok(())
268 }
269
270 pub fn show_tree_cell(
273 &mut self,
274 ui: &mut egui::Ui,
275 row_index: usize,
276 hierarchy: RowHierarchy,
277 ) -> bool {
278 let mut changed = false;
279
280 #[allow(clippy::cast_precision_loss)]
281 let spacing = hierarchy.indent_level as f32 * 22.0; if spacing > 0.0 {
283 ui.add_space(spacing);
284
285 let rect = ui.max_rect();
286 let painter = ui.painter();
287 let stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(65, 65, 65));
288
289 let dash_length = 2.0;
290 let gap_length = 2.0;
291 let step = dash_length + gap_length;
292 let total_height = rect.max.y - rect.min.y;
293
294 if total_height > 0.0 {
295 let num_steps = (total_height / step).ceil() as usize;
296
297 let segments = (0..hierarchy.indent_level).flat_map(|i| {
298 #[allow(clippy::cast_precision_loss)]
299 let x = (i as f32).mul_add(22.0, rect.min.x) + 6.8;
300 (0..num_steps).map(move |step_idx| {
301 #[allow(clippy::cast_precision_loss)]
302 let segment_y = (step_idx as f32).mul_add(step, rect.min.y);
303 let next_y = (segment_y + dash_length).min(rect.max.y);
304 egui::Shape::line_segment(
305 [egui::pos2(x, segment_y), egui::pos2(x, next_y)],
306 stroke,
307 )
308 })
309 });
310
311 painter.extend(segments);
312 }
313 }
314
315 ui.scope(|ui| {
316 ui.spacing_mut().interact_size.x = 0.0;
318 ui.spacing_mut().button_padding = egui::vec2(2.0, 2.0);
319 ui.spacing_mut().item_spacing.x = 4.0;
320
321 if hierarchy.has_children {
322 let arrow = if hierarchy.is_expanded { "⏷" } else { "⏵" };
323
324 let (rect, response) =
326 ui.allocate_exact_size(egui::vec2(14.0, 14.0), egui::Sense::click());
327
328 let arrow_color = if response.hovered() {
330 if hierarchy.is_expanded {
331 ui.visuals().warn_fg_color.linear_multiply(0.9)
332 } else {
333 ui.visuals().widgets.hovered.text_color()
334 }
335 } else if hierarchy.is_expanded {
336 ui.visuals().widgets.active.text_color()
337 } else {
338 ui.visuals()
339 .widgets
340 .inactive
341 .text_color()
342 .linear_multiply(0.5)
343 };
344
345 ui.painter().text(
347 rect.center(),
348 egui::Align2::CENTER_CENTER,
349 arrow,
350 egui::FontId::proportional(11.0),
351 arrow_color,
352 );
353
354 if response.clicked() {
355 if hierarchy.is_expanded {
356 self.expanded_rows.remove(row_index as u32);
357 } else {
358 self.expanded_rows.insert(row_index as u32);
359 }
360
361 self.sorted_children_cache.remove(&row_index);
362 changed = true;
363 }
364 } else {
365 let dummy_arrow = egui::RichText::new("⏵").color(egui::Color32::TRANSPARENT);
366 ui.add_enabled_ui(false, |ui| {
367 let _ = ui.selectable_label(false, dummy_arrow);
368 });
369 }
370 });
371
372 changed
373 }
374
375 pub fn handle_row_selection(&mut self, modifiers: egui::Modifiers, row_index: usize) {
377 let selected_rows = &mut self.selected_rows;
378 let active_rows = &self.active_rows;
379 let row_idx_u32 = row_index as u32;
380
381 if modifiers.command || modifiers.ctrl {
382 if selected_rows.contains(row_idx_u32) {
383 selected_rows.remove(row_idx_u32);
384 self.last_clicked_visible_index = None;
385 } else {
386 selected_rows.insert(row_idx_u32);
387 self.last_clicked_visible_index = active_rows.iter().position(|&r| r == row_index);
388 }
389 } else if modifiers.shift && self.last_clicked_visible_index.is_some() {
390 if let Some(anchor_visible_pos) = self.last_clicked_visible_index
391 && let Some(current_visible_pos) = active_rows.iter().position(|&r| r == row_index)
392 {
393 let start = anchor_visible_pos.min(current_visible_pos);
394 let end = anchor_visible_pos.max(current_visible_pos);
395 for visible_idx in start..=end {
396 if let Some(&actual_row_idx) = active_rows.get(visible_idx) {
397 selected_rows.insert(actual_row_idx as u32);
398 }
399 }
400 }
401 } else if selected_rows.len() == 1 && selected_rows.contains(row_idx_u32) {
402 selected_rows.clear();
403 self.last_clicked_visible_index = None;
404 } else {
405 selected_rows.clear();
406 selected_rows.insert(row_idx_u32);
407 self.last_clicked_visible_index = active_rows.iter().position(|&r| r == row_index);
408 }
409 }
410
411 pub fn rebuild_tree_filter_cache(&mut self, provider: &dyn TableProvider) {
413 let row_count = provider.row_count();
414
415 let is_empty = self.filter_matches.is_empty();
417 if self.filter_cache_dirty || is_empty {
418 self.sorted_children_cache.clear();
419 }
420
421 if !self.filter_cache_dirty && !is_empty {
422 return; }
424 self.filter_cache_dirty = false;
425 self.filter_matches.clear();
426
427 let active_filters = self.get_filter_state();
428 if active_filters.is_empty() {
429 self.filter_matches.insert_range(0..row_count as u32);
431 return;
432 }
433
434 for row_idx in (0..row_count).rev() {
436 let highlight = self.highlights.get_usize(row_idx);
437 let matches = provider.row_matches(self, row_idx, &active_filters, highlight);
438
439 if matches {
440 self.filter_matches.insert(row_idx as u32);
441 }
442
443 if self.filter_matches.contains(row_idx as u32)
445 && let Some(parent_idx) = provider.row_parent(row_idx)
446 {
447 self.filter_matches.insert(parent_idx as u32);
448 }
449 }
450 }
451
452 pub fn flatten_tree(&mut self, provider: &dyn TableProvider) {
454 self.rebuild_tree_filter_cache(provider);
455
456 let mut active = Vec::with_capacity(provider.row_count());
457 if provider.row_count() > 0 {
458 self.flatten_tree_impl(provider, 0, &mut active);
460 }
461 self.active_rows = active;
462 }
463
464 fn flatten_tree_impl(
465 &mut self,
466 provider: &dyn TableProvider,
467 row_idx: usize,
468 out: &mut Vec<usize>,
469 ) {
470 if !self.filter_matches.contains(row_idx as u32) {
472 return; }
474
475 out.push(row_idx);
476
477 let is_expanded = self.expanded_rows.contains(row_idx as u32);
478 if is_expanded {
479 let sorted_children = if let Some(cached) = self.sorted_children_cache.get(&row_idx) {
482 Arc::clone(cached)
483 } else {
484 let mut children = provider.row_children(row_idx);
485 let sort_state = self.get_sort_state();
486 if let Some((sort_col, sort_up)) = sort_state {
487 let _ = provider.sort_active_rows(&mut children, sort_col, sort_up);
488 }
489 let children = Arc::new(children);
490 self.sorted_children_cache
491 .insert(row_idx, Arc::clone(&children));
492 children
493 };
494
495 for &child_idx in sorted_children.iter() {
496 self.flatten_tree_impl(provider, child_idx, out);
497 }
498 }
499 }
500}
501
502pub trait TableStateExt {
503 fn collect_responses(&mut self, responses: Vec<ColResponse>) -> TableChanges;
504}
505
506impl TableStateExt for TableState {
507 fn collect_responses(&mut self, responses: Vec<ColResponse>) -> TableChanges {
508 let mut filter_update = None;
509 let mut sort_update = None;
510 let mut filter_state = Vec::with_capacity(responses.len());
511
512 if self.columns.len() < responses.len() {
514 self.columns
515 .resize_with(responses.len(), ColumnState::default);
516 }
517
518 for (col_index, response) in responses.into_iter().enumerate() {
519 let col = &mut self.columns[col_index];
520 let old_active = !col.response.filtering.is_empty();
521 let new_active = !response.filtering.is_empty();
522
523 if !old_active && new_active {
524 filter_update = Some(FilterUpdate::Added(col_index, response.filtering.clone()));
525 self.filter_cache_dirty = true;
526 } else if old_active && !new_active {
527 filter_update = Some(FilterUpdate::Removed(col_index));
528 self.filter_cache_dirty = true;
529 } else if old_active && new_active {
530 let old = &col.response.filtering;
531 let new = &response.filtering;
532
533 if old.search.text() != new.search.text()
534 || old.search.options() != new.search.options()
535 || old.highlight != new.highlight
536 {
537 filter_update = Some(FilterUpdate::Modified(
538 col_index,
539 response.filtering.clone(),
540 ));
541 self.filter_cache_dirty = true;
542 }
543 }
544
545 if new_active {
546 filter_state.push((col_index, response.filtering.clone()));
547 }
548
549 col.response = response;
550 if col.response.to_sort {
551 col.response.to_sort = false;
552 sort_update = Some(col_index);
553 }
554 }
555
556 let sort_state = self.get_sort_state();
557
558 TableChanges {
559 filter_update,
560 sort_update,
561 filter_state,
562 sort_state,
563 }
564 }
565}