Skip to main content

egui_table_kit/
operations.rs

1//! Table management actions and toolbar operations frameworks.
2
3use std::{borrow::Cow, collections::HashSet};
4
5use compact_str::{CompactString, ToCompactString as _};
6use fluent_zero::t;
7use roaring::RoaringBitmap;
8
9use super::{error::TableError, filter::Filter, state::TableState};
10
11/// Represents cell data, providing the primary text and an optional tooltip/hover override.
12pub type TableCell<'a> = (Cow<'a, str>, Option<Cow<'a, str>>);
13
14/// Owned counterpart of [`TableCell`] with a `'static` lifetime, used when a row's
15/// data must outlive the provider borrow (e.g. when caching visible rows for rendering).
16pub type TableCellOwned = (CompactString, Option<CompactString>);
17
18/// A fully-owned row snapshot, cacheable across frames and usable anywhere a
19/// [`Row`] is expected.
20#[derive(Default, Clone, Debug)]
21pub struct OwnedRow {
22    /// Owned cell values, indexed by column offset.
23    pub cells: Vec<TableCellOwned>,
24}
25
26impl Row for OwnedRow {
27    fn cell(&self, col_index: usize) -> Option<TableCell<'_>> {
28        self.cells.get(col_index).map(|(val, hover)| {
29            (
30                Cow::Borrowed(val.as_str()),
31                hover.as_ref().map(|h| Cow::Borrowed(h.as_str())),
32            )
33        })
34    }
35    fn column_count(&self) -> usize {
36        self.cells.len()
37    }
38}
39
40/// A row element that resolves display text properties at specific column offsets.
41pub trait Row {
42    fn cell(&self, col_index: usize) -> Option<TableCell<'_>>;
43    fn column_count(&self) -> usize;
44
45    /// Snapshots every column of this row into an [`OwnedRow`], detaching it from
46    /// the provider's borrow lifetime so it can be cached.
47    fn to_owned_row(&self) -> OwnedRow {
48        let mut cells = Vec::with_capacity(self.column_count());
49        for i in 0..self.column_count() {
50            if let Some((val, hover)) = self.cell(i) {
51                cells.push((
52                    val.to_compact_string(),
53                    hover.map(|h| h.to_compact_string()),
54                ));
55            }
56        }
57        OwnedRow { cells }
58    }
59}
60
61impl Row for [TableCell<'_>] {
62    fn cell(&self, col_index: usize) -> Option<TableCell<'_>> {
63        self.get(col_index).map(|(val, hover)| {
64            (
65                Cow::Borrowed(val.as_ref()),
66                hover.as_ref().map(|h| Cow::Borrowed(h.as_ref())),
67            )
68        })
69    }
70    fn column_count(&self) -> usize {
71        self.len()
72    }
73}
74
75/// The callback signature used to process streamed row data.
76/// - `'b` represents the lifetime of any local variables captured by the closure.
77pub type RowCallback<'b> = dyn FnMut(&dyn Row) -> Result<(), TableError> + 'b;
78
79/// Structural nesting parameters for tree hierarchy nodes.
80#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
81pub struct RowHierarchy {
82    pub indent_level: usize,
83    pub has_children: bool,
84    pub is_expanded: bool,
85}
86
87/// Zero-allocation lazy headers iterator.
88pub struct HeaderIter<'a> {
89    provider: &'a dyn TableProvider,
90    index: usize,
91    count: usize,
92}
93
94impl<'a> HeaderIter<'a> {
95    pub fn new(provider: &'a dyn TableProvider) -> Self {
96        Self {
97            provider,
98            index: 0,
99            count: provider.column_count(),
100        }
101    }
102}
103
104impl<'a> Iterator for HeaderIter<'a> {
105    type Item = Cow<'a, str>;
106
107    fn next(&mut self) -> Option<Self::Item> {
108        if self.index < self.count {
109            let res = self.provider.header(self.index);
110            self.index += 1;
111            res
112        } else {
113            None
114        }
115    }
116
117    fn size_hint(&self) -> (usize, Option<usize>) {
118        let remaining = self.count.saturating_sub(self.index);
119        (remaining, Some(remaining))
120    }
121}
122
123impl ExactSizeIterator for HeaderIter<'_> {
124    fn len(&self) -> usize {
125        self.count.saturating_sub(self.index)
126    }
127}
128
129/// A zero-allocation row wrapper that references cell data directly from a provider.
130#[derive(Copy, Clone)]
131pub struct BorrowedRow<'a> {
132    pub provider: &'a dyn TableProvider,
133    pub row_index: usize,
134}
135
136impl Row for BorrowedRow<'_> {
137    fn cell(&self, col_index: usize) -> Option<TableCell<'_>> {
138        self.provider
139            .cell_at(self.row_index, col_index)
140            .ok()
141            .flatten()
142    }
143
144    fn column_count(&self) -> usize {
145        self.provider.column_count()
146    }
147}
148
149/// Trait implemented by datasets to back the interactive table system.
150pub trait TableProvider {
151    fn column_count(&self) -> usize;
152    fn header(&self, index: usize) -> Option<Cow<'_, str>>;
153
154    fn headers(&self) -> HeaderIter<'_>;
155
156    fn row_count(&self) -> usize;
157
158    /// Direct cellular random-access method.
159    ///
160    /// Override this in your custom collections to achieve O(1) performance
161    /// and completely bypass heap-allocation pathways during rendering.
162    fn cell_at(
163        &self,
164        row_index: usize,
165        col_index: usize,
166    ) -> Result<Option<TableCell<'_>>, TableError> {
167        // Fallback default: Query row_at and copy values to satisfy lifetimes
168        if let Some(owned_row) = self.row_at(row_index)?
169            && let Some((val, hover)) = owned_row.cells.get(col_index)
170        {
171            return Ok(Some((
172                Cow::Owned(val.to_string()),
173                hover.as_ref().map(|h| Cow::Owned(h.to_string())),
174            )));
175        }
176        Ok(None)
177    }
178
179    /// Processes a single row by index. Override this for O(1) random access.
180    fn for_row_at(&self, index: usize, f: &mut RowCallback<'_>) -> Result<(), TableError> {
181        let mut idx = 0;
182        self.for_all_rows(&mut |row| {
183            if idx == index {
184                f(row)?;
185            }
186            idx += 1;
187            Ok(())
188        })
189    }
190
191    /// Randomly fetches a single row as an [`OwnedRow`]. This is the access path used
192    /// by the rendering delegate for visible rows.
193    ///
194    /// The default implementation walks the dataset sequentially (O(N)) and should be
195    /// overridden for true O(1) random access whenever the backing store supports it.
196    fn row_at(&self, index: usize) -> Result<Option<OwnedRow>, TableError> {
197        if index >= self.row_count() {
198            return Ok(None);
199        }
200        let mut out: Option<OwnedRow> = None;
201        let mut idx = 0usize;
202        self.for_all_rows(&mut |row| {
203            if idx == index {
204                out = Some(row.to_owned_row());
205            }
206            idx += 1;
207            Ok(())
208        })?;
209        Ok(out)
210    }
211
212    /// Sequentially processes each selected row with the provided callback.
213    fn for_selected_rows(
214        &self,
215        state: &TableState,
216        f: &mut RowCallback<'_>,
217    ) -> Result<(), TableError>;
218
219    /// Sequentially processes every row in the dataset.
220    fn for_all_rows(&self, f: &mut RowCallback<'_>) -> Result<(), TableError>;
221
222    /// Sorts the active row indices by the specified column.
223    /// Uses a generic string-based fallback sorting implementation, but can be overridden.
224    fn sort_active_rows(
225        &self,
226        active_rows: &mut Vec<usize>,
227        col_index: usize,
228        ascending: bool,
229    ) -> Result<(), TableError> {
230        if active_rows.len() <= 1 {
231            return Ok(());
232        }
233
234        let active_set: RoaringBitmap = active_rows.iter().map(|&i| i as u32).collect();
235        let mut sort_keys = Vec::with_capacity(active_rows.len());
236
237        let mut idx = 0usize;
238        self.for_all_rows(&mut |row| {
239            if active_set.contains(idx as u32) {
240                let val = row
241                    .cell(col_index)
242                    .map(|(v, _)| v.to_compact_string())
243                    .unwrap_or_default();
244                sort_keys.push((idx, val));
245            }
246            idx += 1;
247            Ok(())
248        })?;
249
250        if ascending {
251            sort_keys.sort_by(|a, b| a.1.cmp(&b.1));
252        } else {
253            sort_keys.sort_by(|a, b| b.1.cmp(&a.1));
254        }
255
256        *active_rows = sort_keys.into_iter().map(|(idx, _)| idx).collect();
257        Ok(())
258    }
259
260    /// Filters all rows sequentially. Override this to implement custom parallel filtering (e.g. Rayon).
261    fn filter_rows(
262        &self,
263        state: &TableState,
264        filters: &[(usize, Filter)],
265    ) -> Result<Vec<usize>, TableError> {
266        if filters.is_empty() {
267            return Ok((0..self.row_count()).collect());
268        }
269
270        let initial_capacity = self.row_count().min(2048);
271        let mut passing_indices = Vec::with_capacity(initial_capacity);
272        let mut row_idx = 0;
273
274        self.for_all_rows(&mut |row| {
275            let highlight = state.highlights.get_usize(row_idx);
276            let mut matches = true;
277
278            for &(col_idx, ref filter) in filters {
279                if let Some(cell) = row.cell(col_idx) {
280                    if !filter.matches(&cell.0, highlight) {
281                        matches = false;
282                        break;
283                    }
284                } else {
285                    matches = false;
286                    break;
287                }
288            }
289
290            if matches {
291                passing_indices.push(row_idx);
292            }
293            row_idx += 1;
294            Ok(())
295        })?;
296
297        Ok(passing_indices)
298    }
299
300    /// Returns tree nesting parameters for a given row.
301    /// Evaluates to `None` by default (representing traditional non-hierarchical flat tables).
302    fn row_hierarchy(&self, _state: &TableState, _row_index: usize) -> Option<RowHierarchy> {
303        None
304    }
305
306    /// Returns whether this provider represents a hierarchical tree table.
307    /// Returns `false` by default.
308    fn is_tree(&self) -> bool {
309        false
310    }
311
312    /// Returns the active parent row index for a given row (if any).
313    fn row_parent(&self, _row_index: usize) -> Option<usize> {
314        None
315    }
316
317    /// Returns the child row indices nested immediately under the specified row.
318    fn row_children(&self, _row_index: usize) -> Vec<usize> {
319        Vec::new()
320    }
321
322    /// Returns whether an individual row matches the currently active column filters.
323    fn row_matches(
324        &self,
325        _state: &TableState,
326        _row_index: usize,
327        _filters: &[(usize, Filter)],
328        _highlight: Option<u8>,
329    ) -> bool {
330        true
331    }
332}
333
334impl dyn TableProvider + '_ {
335    /// Maps over each selected row with a closure and collects the results into a flat Vector.
336    pub fn map_selected_rows<T, F>(
337        &self,
338        state: &TableState,
339        mut f: F,
340    ) -> Result<Vec<T>, TableError>
341    where
342        F: FnMut(&dyn Row) -> Result<T, TableError>,
343    {
344        let mut results = Vec::with_capacity(state.selected_rows.len() as usize);
345        self.for_selected_rows(state, &mut |row| {
346            results.push(f(row)?);
347            Ok(())
348        })?;
349        Ok(results)
350    }
351
352    /// Maps only the first selected row (if any) and returns the result, stopping iteration immediately.
353    pub fn map_first_selected_row<T, F>(
354        &self,
355        state: &TableState,
356        f: F,
357    ) -> Result<Option<T>, TableError>
358    where
359        F: FnOnce(&dyn Row) -> Result<T, TableError>,
360    {
361        let mut result = None;
362        let mut f_opt = Some(f);
363
364        self.for_selected_rows(state, &mut |row| {
365            if let Some(f_once) = f_opt.take() {
366                result = Some(f_once(row)?);
367            }
368            Ok(())
369        })?;
370
371        Ok(result)
372    }
373}
374
375/// Helper trait to parse, extract, or fall back between primary text and hover text in rows.
376pub trait RowSliceExt {
377    /// Extracts the primary text at the specified column index.
378    fn get_primary(&self, col_index: usize) -> Result<Cow<'_, str>, TableError>;
379
380    /// Extracts the hover/alternate text at the specified column index.
381    fn get_hover(&self, col_index: usize) -> Result<Cow<'_, str>, TableError>;
382
383    /// Parses the primary text at the specified column index into type `T`.
384    fn parse_primary<T>(&self, col_index: usize) -> Result<T, TableError>
385    where
386        T: std::str::FromStr,
387        <T as std::str::FromStr>::Err: std::fmt::Display;
388
389    /// Parses the hover text at the specified column index into type `T`.
390    fn parse_hover<T>(&self, col_index: usize) -> Result<T, TableError>
391    where
392        T: std::str::FromStr,
393        <T as std::str::FromStr>::Err: std::fmt::Display;
394}
395
396impl RowSliceExt for dyn Row + '_ {
397    fn get_primary(&self, col_index: usize) -> Result<Cow<'_, str>, TableError> {
398        self.cell(col_index)
399            .map(|(val, _)| val)
400            .ok_or(TableError::CorruptedState)
401    }
402
403    fn get_hover(&self, col_index: usize) -> Result<Cow<'_, str>, TableError> {
404        self.cell(col_index)
405            .and_then(|(_, hover)| hover)
406            .ok_or(TableError::CorruptedState)
407    }
408
409    fn parse_primary<T>(&self, col_index: usize) -> Result<T, TableError>
410    where
411        T: std::str::FromStr,
412        <T as std::str::FromStr>::Err: std::fmt::Display,
413    {
414        T::from_str(self.get_primary(col_index)?.as_ref())
415            .map_err(|e| TableError::Generic(e.to_string()))
416    }
417
418    fn parse_hover<T>(&self, col_index: usize) -> Result<T, TableError>
419    where
420        T: std::str::FromStr,
421        <T as std::str::FromStr>::Err: std::fmt::Display,
422    {
423        T::from_str(self.get_hover(col_index)?.as_ref())
424            .map_err(|e| TableError::Generic(e.to_string()))
425    }
426}
427
428/// Evaluation context supplied to active toolbar operations during executions.
429pub struct OperationContext<'a, 'b> {
430    pub ui: &'a mut egui::Ui,
431    pub data: &'a mut TableState,
432    pub provider: &'b dyn TableProvider,
433}
434
435/// Coordinates grouped sequences of toolbar actions, polling systems, and error dialogs.
436#[derive(Debug, Default)]
437pub struct TableOperations {
438    pub groups: Vec<Vec<Box<dyn TableOperation>>>,
439    pub pending_tracker: HashSet<(usize, usize), ahash::RandomState>,
440    pub last_tick: u64,
441}
442
443impl TableOperations {
444    #[must_use]
445    pub fn new() -> Self {
446        Self::default()
447    }
448
449    #[must_use]
450    pub fn with_group(mut self, group: Vec<Box<dyn TableOperation>>) -> Self {
451        self.groups.push(group);
452        self
453    }
454
455    #[must_use]
456    pub fn with_operation(mut self, op: impl TableOperation + 'static) -> Self {
457        if let Some(group) = self.groups.last_mut() {
458            group.push(Box::new(op));
459        } else {
460            self.groups.push(vec![Box::new(op)]);
461        }
462        self
463    }
464
465    /// Evaluates state transitions exactly once per unique frame tick.
466    /// Returns `true` if any completed operation requested a view refresh.
467    pub fn update(&mut self, ctx: &egui::Context) -> bool {
468        let mut refresh = false;
469        let current_tick = ctx.cumulative_frame_nr();
470        if self.last_tick != current_tick {
471            self.last_tick = current_tick;
472
473            for (g_idx, op_group) in self.groups.iter_mut().enumerate() {
474                for (op_idx, op) in op_group.iter_mut().enumerate() {
475                    let key = (g_idx, op_idx);
476                    let pending = op.is_pending();
477                    let was_pending = self.pending_tracker.contains(&key);
478
479                    if was_pending && !pending {
480                        self.pending_tracker.remove(&key);
481                        let success = op.error().is_none();
482                        op.on_completed(success);
483                        if op.refresh_on_completion() {
484                            refresh = true;
485                        }
486                    } else if !was_pending && pending {
487                        self.pending_tracker.insert(key);
488                    }
489                }
490            }
491        }
492        refresh
493    }
494
495    /// Renders standard table operation buttons with default look.
496    pub fn gui(
497        &mut self,
498        ui: &mut egui::Ui,
499        provider: &dyn TableProvider,
500        data: &mut TableState,
501        context_menu: bool,
502    ) -> Result<bool, TableError> {
503        self.gui_custom(
504            ui,
505            provider,
506            data,
507            context_menu,
508            |ui, op, enabled, reason, context_menu| {
509                ui.add_enabled_ui(enabled, |ui| {
510                    let mut button = ui
511                        .button(op.get_name(context_menu).as_ref())
512                        .on_hover_text(op.name());
513                    if !enabled {
514                        button = button.on_disabled_hover_text(format!("{}\n{reason}", op.name()));
515                    }
516                    button
517                })
518                .inner
519            },
520        )
521    }
522
523    /// Renders table operations using a custom button builder callback.
524    ///
525    /// This handles all the state machine details (polling, execution, pending modes, group separation)
526    /// but allows full control over the visual presentation of each button.
527    pub fn gui_custom<F>(
528        &mut self,
529        ui: &mut egui::Ui,
530        provider: &dyn TableProvider,
531        data: &mut TableState,
532        context_menu: bool,
533        mut button_renderer: F,
534    ) -> Result<bool, TableError>
535    where
536        F: FnMut(
537            &mut egui::Ui,
538            &mut Box<dyn TableOperation>,
539            bool, // enabled
540            &str, // localized disabled reason
541            bool, // context_menu
542        ) -> egui::Response,
543    {
544        let refresh = self.update(ui.ctx());
545        let mut any_clicked = false;
546        let num_groups = self.groups.len();
547
548        // Render operations and process interactions
549        for (g_idx, op_group) in self.groups.iter_mut().enumerate() {
550            for op in op_group {
551                let is_pending = op.is_pending();
552
553                if op.pollable() {
554                    op.poll(ui, data)?;
555                }
556                let (enabled, reason) = if is_pending {
557                    (false, t!("operation-pending"))
558                } else {
559                    op.evaluate_enablement(data)
560                };
561                if !context_menu {
562                    op.extra_ui(ui, data)?;
563                }
564                let response = button_renderer(ui, op, enabled, reason.as_ref(), context_menu);
565                if response.clicked() {
566                    any_clicked = true;
567                    let mut ctx = OperationContext { ui, data, provider };
568                    op.exec(&mut ctx)?;
569                }
570            }
571            // Draw group separators in standard layouts and menus alike
572            if g_idx + 1 < num_groups {
573                ui.separator();
574            }
575        }
576        if any_clicked && context_menu {
577            ui.close_kind(egui::UiKind::Menu);
578        }
579        Ok(refresh)
580    }
581
582    /// Renders all operations in a specific group.
583    /// This is useful for building custom caller layouts, submenus, and advanced structural separations.
584    pub fn show_group<F>(
585        &mut self,
586        ui: &mut egui::Ui,
587        provider: &dyn TableProvider,
588        data: &mut TableState,
589        group_idx: usize,
590        context_menu: bool,
591        mut button_renderer: F,
592    ) -> Result<bool, TableError>
593    where
594        F: FnMut(
595            &mut egui::Ui,
596            &mut Box<dyn TableOperation>,
597            bool, // enabled
598            &str, // localized disabled reason
599        ) -> egui::Response,
600    {
601        if group_idx >= self.groups.len() {
602            return Ok(false);
603        }
604
605        let refresh = self.update(ui.ctx());
606        let mut any_clicked = false;
607
608        let op_group = &mut self.groups[group_idx];
609        for op in op_group {
610            let is_pending = op.is_pending();
611
612            if op.pollable() {
613                op.poll(ui, data)?;
614            }
615            let (enabled, reason) = if is_pending {
616                (false, t!("operation-pending"))
617            } else {
618                op.evaluate_enablement(data)
619            };
620
621            if !context_menu {
622                op.extra_ui(ui, data)?;
623            }
624
625            let response = button_renderer(ui, op, enabled, reason.as_ref());
626            if response.clicked() {
627                any_clicked = true;
628                let mut ctx = OperationContext { ui, data, provider };
629                op.exec(&mut ctx)?;
630            }
631        }
632
633        if any_clicked && context_menu {
634            ui.close_kind(egui::UiKind::Menu);
635        }
636
637        Ok(refresh)
638    }
639
640    /// Renders a single operation directly at a specific group and operation index.
641    /// Gives the caller total control over fine-grained placement and visual arrangement.
642    pub fn show_operation<F>(
643        &mut self,
644        ui: &mut egui::Ui,
645        provider: &dyn TableProvider,
646        data: &mut TableState,
647        group_idx: usize,
648        op_idx: usize,
649        context_menu: bool,
650        button_renderer: F,
651    ) -> Result<bool, TableError>
652    where
653        F: FnOnce(
654            &mut egui::Ui,
655            &mut Box<dyn TableOperation>,
656            bool, // enabled
657            &str, // localized disabled reason
658        ) -> egui::Response,
659    {
660        if group_idx >= self.groups.len() || op_idx >= self.groups[group_idx].len() {
661            return Ok(false);
662        }
663
664        let refresh = self.update(ui.ctx());
665
666        let op = &mut self.groups[group_idx][op_idx];
667        let is_pending = op.is_pending();
668
669        if op.pollable() {
670            op.poll(ui, data)?;
671        }
672        let (enabled, reason) = if is_pending {
673            (false, t!("operation-pending"))
674        } else {
675            op.evaluate_enablement(data)
676        };
677
678        if !context_menu {
679            op.extra_ui(ui, data)?;
680        }
681
682        let response = button_renderer(ui, op, enabled, reason.as_ref());
683        if response.clicked() {
684            let mut ctx = OperationContext { ui, data, provider };
685            op.exec(&mut ctx)?;
686            if context_menu {
687                ui.close_kind(egui::UiKind::Menu);
688            }
689        }
690
691        Ok(refresh)
692    }
693}
694
695/// Triggers for when table actions can execute.
696#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
697pub enum TableOperationEnablement {
698    #[default]
699    Always,
700    AtLeastOneFiltered,
701    AtLeastOneSelected,
702    OneSelected,
703}
704
705/// Trait defining a single modular table operation.
706pub trait TableOperation: std::any::Any + std::fmt::Debug + Send + Sync {
707    fn name(&self) -> Cow<'_, str>;
708    fn icon(&self) -> &'static str {
709        "X"
710    }
711    fn get_name(&self, full: bool) -> Cow<'_, str> {
712        if full {
713            Cow::Owned(format!("{} {}", self.name(), self.icon()))
714        } else {
715            Cow::Borrowed(self.icon())
716        }
717    }
718    fn refresh_on_completion(&self) -> bool {
719        false
720    }
721    fn pollable(&self) -> bool {
722        false
723    }
724    fn is_first_page(&self) -> bool {
725        true
726    }
727    fn is_last_page(&self) -> bool {
728        true
729    }
730    fn enabled(&self) -> TableOperationEnablement;
731    fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError>;
732    fn extra_ui(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> {
733        Ok(())
734    }
735    fn is_pending(&mut self) -> bool {
736        false
737    }
738
739    /// Event hook called exactly once when the operation transitions from pending to completed.
740    fn on_completed(&mut self, _success: bool) {}
741
742    /// Routine tick loop, natively fired if `pollable()` evaluates to true.
743    fn poll(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> {
744        Ok(())
745    }
746    fn consume(&mut self) -> Result<(), TableError> {
747        Ok(())
748    }
749    fn error(&self) -> Option<&str> {
750        None
751    }
752    fn clear_error(&mut self) {}
753    fn is_modal_open(&self) -> bool {
754        false
755    }
756    fn set_modal_open(&mut self, _open: bool) {}
757    fn reset(&mut self) {}
758
759    /// Spawns an input form dialog, pausing interactions while polling.
760    fn pollable_modal(
761        &mut self,
762        ui: &mut egui::Ui,
763        centered: bool,
764        action: Cow<'_, str>,
765        action_progressive: Cow<'_, str>,
766        input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
767    ) -> Result<(), TableError>
768    where
769        Self: Sized,
770    {
771        if self.is_modal_open() {
772            egui::Modal::new(ui.id().with("pollable_modal"))
773                .show(ui.ctx(), |ui| {
774                    ui.scope_builder(
775                        egui::UiBuilder::new().layout(egui::Layout::top_down(if centered {
776                            egui::Align::Center
777                        } else {
778                            egui::Align::Min
779                        })),
780                        |ui| {
781                            ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
782                            ui.heading(
783                                egui::RichText::new(format!("{} {}", self.name(), self.icon()))
784                                    .strong(),
785                            );
786                            ui.separator();
787                            ui.spacing_mut().item_spacing.y = 5.0;
788
789                            let is_pending = self.is_pending();
790                            ui.add_enabled_ui(!is_pending, |ui| input_ui(ui, self))
791                                .inner?;
792                            ui.add_space(10.0);
793
794                            if let Some(error) = self.error() {
795                                ui.colored_label(egui::Color32::RED, t!("error"));
796                                ui.colored_label(egui::Color32::RED, error);
797                            }
798
799                            if is_pending {
800                                ui.label(action_progressive);
801                                ui.add_space(5.0);
802                                ui.spinner();
803                            } else {
804                                if self.is_last_page() {
805                                    let is_allowed = self.poll_allow_execution();
806                                    if ui
807                                        .add_enabled(is_allowed, egui::Button::new(action))
808                                        .clicked()
809                                    {
810                                        self.clear_error();
811                                        self.consume()?;
812                                    }
813                                }
814                                if self.is_first_page() && ui.button(t!("cancel")).clicked() {
815                                    self.reset();
816                                }
817                            }
818                            Ok(())
819                        },
820                    )
821                    .inner
822                })
823                .inner
824        } else {
825            Ok(())
826        }
827    }
828
829    /// Spawns a progress information dialog.
830    fn polled_modal(
831        &mut self,
832        ui: &mut egui::Ui,
833        heading: Cow<'_, str>,
834        action_progressive: Cow<'_, str>,
835        input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
836    ) -> Result<(), TableError>
837    where
838        Self: Sized,
839    {
840        if self.is_modal_open() {
841            egui::Modal::new(ui.id().with("polled_modal"))
842                .show(ui.ctx(), |ui| {
843                    ui.vertical_centered(|ui| {
844                        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
845                        ui.heading(heading);
846                        ui.separator();
847                        ui.spacing_mut().item_spacing.y = 5.0;
848
849                        if self.is_pending() {
850                            ui.label(action_progressive);
851                            ui.add_space(5.0);
852                            ui.spinner();
853                        } else if let Some(error) = self.error() {
854                            ui.colored_label(egui::Color32::RED, t!("error"));
855                            ui.colored_label(egui::Color32::RED, error);
856                        } else {
857                            input_ui(ui, self)?;
858                        }
859
860                        ui.add_space(10.0);
861                        if ui.button(t!("close")).clicked() {
862                            self.reset();
863                        }
864                        Ok::<_, TableError>(())
865                    })
866                })
867                .inner
868                .inner?;
869        }
870        Ok(())
871    }
872
873    fn poll_allow_execution(&self) -> bool {
874        true
875    }
876
877    /// Evaluates if the operation is enabled based on the current `TableState`,
878    /// returning a tuple of `(is_enabled, localized_disabled_reason)`.
879    fn evaluate_enablement(&self, state: &TableState) -> (bool, Cow<'static, str>) {
880        match self.enabled() {
881            TableOperationEnablement::Always => (true, Cow::Borrowed("")),
882            TableOperationEnablement::AtLeastOneSelected => (
883                !state.selected_rows.is_empty(),
884                t!("operation-at-least-one"),
885            ),
886            TableOperationEnablement::OneSelected => {
887                (state.selected_rows.len() == 1, t!("operation-one"))
888            }
889            TableOperationEnablement::AtLeastOneFiltered => (
890                !state.active_rows.is_empty(),
891                t!("operation-at-least-one-filtered"),
892            ),
893        }
894    }
895}
896
897// Default Operations
898
899#[derive(Debug, Default)]
900pub struct CopyRows {
901    pub prioritize_hovers: bool,
902}
903
904impl TableOperation for CopyRows {
905    fn name(&self) -> Cow<'_, str> {
906        if self.prioritize_hovers {
907            t!("copy-hovered-rows")
908        } else {
909            t!("copy-rows")
910        }
911    }
912    fn icon(&self) -> &'static str {
913        if self.prioritize_hovers {
914            "📁"
915        } else {
916            "📋"
917        }
918    }
919    fn enabled(&self) -> TableOperationEnablement {
920        TableOperationEnablement::AtLeastOneSelected
921    }
922    fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
923        // Pre-allocate a default chunk size to minimize system allocator pressure
924        let mut output = String::with_capacity(2048);
925
926        ctx.provider.for_selected_rows(ctx.data, &mut |row| {
927            if !output.is_empty() {
928                output.push('\n');
929            }
930            for i in 0..row.column_count() {
931                if i > 0 {
932                    output.push(',');
933                }
934                if let Some((val, hover)) = row.cell(i) {
935                    let cell_text = if self.prioritize_hovers {
936                        hover.as_ref().map_or_else(|| val.as_ref(), |h| h.as_ref())
937                    } else {
938                        val.as_ref()
939                    };
940                    output.push_str(cell_text);
941                }
942            }
943            Ok(())
944        })?;
945
946        ctx.ui.ctx().copy_text(output);
947        Ok(())
948    }
949}
950
951#[derive(Debug, Default)]
952pub struct CopyHeadersRows {
953    pub prioritize_hovers: bool,
954}
955
956impl TableOperation for CopyHeadersRows {
957    fn name(&self) -> Cow<'_, str> {
958        if self.prioritize_hovers {
959            t!("copy-hovered-rows-with-headers")
960        } else {
961            t!("copy-rows-with-headers")
962        }
963    }
964    fn icon(&self) -> &'static str {
965        if self.prioritize_hovers {
966            "🗄"
967        } else {
968            "📜"
969        }
970    }
971    fn enabled(&self) -> TableOperationEnablement {
972        TableOperationEnablement::AtLeastOneSelected
973    }
974
975    fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
976        // Pre-allocate a reasonable capacity for headers and initial rows
977        let mut output = String::with_capacity(2048);
978
979        // 1. Write the headers directly into the buffer (replacing headers.join(","))
980        for (i, header) in ctx.provider.headers().enumerate() {
981            if i > 0 {
982                output.push(',');
983            }
984            output.push_str(&header);
985        }
986
987        // 2. Stream the selected rows sequentially into the same buffer
988        ctx.provider.for_selected_rows(ctx.data, &mut |row| {
989            output.push('\n');
990            for i in 0..row.column_count() {
991                if i > 0 {
992                    output.push(',');
993                }
994                if let Some((val, hover)) = row.cell(i) {
995                    let cell_text = if self.prioritize_hovers {
996                        hover.as_ref().map_or_else(|| val.as_ref(), |h| h.as_ref())
997                    } else {
998                        val.as_ref()
999                    };
1000                    output.push_str(cell_text);
1001                }
1002            }
1003            Ok(())
1004        })?;
1005
1006        // 3. Send the single allocated string to the clipboard
1007        ctx.ui.ctx().copy_text(output);
1008        Ok(())
1009    }
1010}
1011
1012#[derive(Debug, Default)]
1013pub struct FilterSelectAll;
1014
1015impl TableOperation for FilterSelectAll {
1016    fn name(&self) -> Cow<'_, str> {
1017        t!("select-filtered")
1018    }
1019    fn icon(&self) -> &'static str {
1020        "☑"
1021    }
1022    fn enabled(&self) -> TableOperationEnablement {
1023        TableOperationEnablement::Always
1024    }
1025    fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
1026        let active_u32_iter = ctx.data.active_rows.iter().map(|&row| row as u32);
1027        ctx.data.selected_rows.extend(active_u32_iter);
1028        Ok(())
1029    }
1030}
1031
1032#[derive(Debug, Default)]
1033pub struct FilterDeSelectAll;
1034
1035impl TableOperation for FilterDeSelectAll {
1036    fn name(&self) -> Cow<'_, str> {
1037        t!("deselect-filtered")
1038    }
1039    fn icon(&self) -> &'static str {
1040        "❎"
1041    }
1042    fn enabled(&self) -> TableOperationEnablement {
1043        TableOperationEnablement::Always
1044    }
1045    fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
1046        ctx.data.active_rows.iter().for_each(|row| {
1047            ctx.data.selected_rows.remove(*row as u32);
1048        });
1049        Ok(())
1050    }
1051}
1052
1053#[derive(Debug, Default)]
1054pub struct SelectAll;
1055
1056impl TableOperation for SelectAll {
1057    fn name(&self) -> Cow<'_, str> {
1058        t!("select-all")
1059    }
1060    fn icon(&self) -> &'static str {
1061        "✔"
1062    }
1063    fn enabled(&self) -> TableOperationEnablement {
1064        TableOperationEnablement::Always
1065    }
1066    fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
1067        ctx.data.selected_rows.clear();
1068        ctx.data
1069            .selected_rows
1070            .insert_range(0..ctx.provider.row_count() as u32);
1071        Ok(())
1072    }
1073}
1074
1075#[derive(Debug, Default)]
1076pub struct DeSelectAll;
1077
1078impl TableOperation for DeSelectAll {
1079    fn name(&self) -> Cow<'_, str> {
1080        t!("deselect-all")
1081    }
1082    fn icon(&self) -> &'static str {
1083        "❌"
1084    }
1085    fn enabled(&self) -> TableOperationEnablement {
1086        TableOperationEnablement::Always
1087    }
1088    fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
1089        ctx.data.selected_rows.clear();
1090        Ok(())
1091    }
1092}