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