Skip to main content

egui_table_kit/
operations.rs

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