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