Skip to main content

egui_table_kit/
state.rs

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, // Mark dirty initially to force first-frame population
52            ..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            // Only apply flat filters if this is a flat table
116            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            // Only apply flat filters if this is a flat table
130            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                    // Apply the sorted indices to active_rows in the new direction
157                    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    /// Renders the tree indentation guidelines and expand/collapse arrow inside a tree cell.
223    /// Returns `true` if the expansion state changed (allowing immediate-mode viewport updates).
224    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)); // Guidelines gray
240
241            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                // Draw dashed guideline segments
246                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            // Placeholder alignment spacing matching "⏵"
290            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    /// Rebuilds the O(N) reverse-propagation subtree filter cache from scratch if dirty.
333    pub fn rebuild_tree_filter_cache(&mut self, provider: &dyn TableProvider) {
334        let row_count = provider.row_count();
335
336        // Invalidate and clear sibling sort caches if the snapshot shifted (row count changed)
337        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; // Cache is warm: do nothing!
344        }
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            // Memory Optimization: populate the entire range in O(1) time
351            self.filter_matches.insert_range(0..row_count as u32);
352            return;
353        }
354
355        // Single-pass O(N) reverse propagation of matching subtrees
356        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            // Propagate match state upwards to parents
365            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    /// Recursively flattens the visible tree nodes matching the active filters into `active_rows`.
374    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            // Flatten from the root node (0) downwards
380            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        // Fast, O(log C) random access check over compressed roaring bitmap containers
392        if !self.filter_matches.contains(row_idx as u32) {
393            return; // Subtree does not match filters: discard early
394        }
395
396        out.push(row_idx);
397
398        let is_expanded = self.expanded_rows.contains(row_idx as u32);
399        if is_expanded {
400            // Retrieve stable sibling order from cache or sort once on cache-miss
401            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        // Ensure the vector capacity covers the received header layout size
431        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}