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) + 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            // Render an invisible, disabled placeholder label to reserve the exact layout footprint
290            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    /// Rebuilds the O(N) reverse-propagation subtree filter cache from scratch if dirty.
336    pub fn rebuild_tree_filter_cache(&mut self, provider: &dyn TableProvider) {
337        let row_count = provider.row_count();
338
339        // Invalidate and clear sibling sort caches if the snapshot shifted (row count changed)
340        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; // Cache is warm: do nothing!
347        }
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            // Memory Optimization: populate the entire range in O(1) time
354            self.filter_matches.insert_range(0..row_count as u32);
355            return;
356        }
357
358        // Single-pass O(N) reverse propagation of matching subtrees
359        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            // Propagate match state upwards to parents
368            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    /// Recursively flattens the visible tree nodes matching the active filters into `active_rows`.
377    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            // Flatten from the root node (0) downwards
383            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        // Fast, O(log C) random access check over compressed roaring bitmap containers
395        if !self.filter_matches.contains(row_idx as u32) {
396            return; // Subtree does not match filters: discard early
397        }
398
399        out.push(row_idx);
400
401        let is_expanded = self.expanded_rows.contains(row_idx as u32);
402        if is_expanded {
403            // Retrieve stable sibling order from cache or sort once on cache-miss
404            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        // Ensure the vector capacity covers the received header layout size
434        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}