Skip to main content

egui_table_kit/
state.rs

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