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
8pub type TableCell<'a> = (Cow<'a, str>, Option<Cow<'a, str>>);
10
11pub type RowSlice<'a, 'b> = &'b [TableCell<'a>];
13
14pub 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 fn sort_active_rows(
42 &self,
43 active_rows: &mut Vec<usize>,
44 col_index: usize,
45 ascending: bool,
46 ) -> Result<(), TableError> {
47 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 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 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 fn row_hierarchy(&self, _state: &TableState, _row_index: usize) -> Option<RowHierarchy> {
114 None
115 }
116
117 fn is_tree(&self) -> bool {
120 false
121 }
122
123 fn row_parent(&self, _row_index: usize) -> Option<usize> {
125 None
126 }
127
128 fn row_children(&self, _row_index: usize) -> Vec<usize> {
130 Vec::new()
131 }
132
133 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 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 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 fn get_primary(&self, col_index: usize) -> Result<&str, TableError>;
189
190 fn get_hover(&self, col_index: usize) -> Result<&str, TableError>;
192
193 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 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 pub fn update(&mut self, ctx: &egui::Context) -> bool {
274 let mut refresh = false;
275 let current_tick = ctx.cumulative_frame_nr();
276 if self.last_tick != current_tick {
277 self.last_tick = current_tick;
278
279 for (g_idx, op_group) in self.groups.iter_mut().enumerate() {
280 for (op_idx, op) in op_group.iter_mut().enumerate() {
281 let key = (g_idx, op_idx);
282 let pending = op.is_pending();
283 let was_pending = self.pending_tracker.contains(&key);
284
285 if was_pending && !pending {
286 self.pending_tracker.remove(&key);
287 let success = op.error().is_none();
288 op.on_completed(success);
289 if op.refresh_on_completion() {
290 refresh = true;
291 }
292 } else if !was_pending && pending {
293 self.pending_tracker.insert(key);
294 }
295 }
296 }
297 }
298 refresh
299 }
300
301 pub fn gui(
303 &mut self,
304 ui: &mut egui::Ui,
305 provider: &dyn TableProvider,
306 data: &mut TableState,
307 context_menu: bool,
308 ) -> Result<bool, TableError> {
309 self.gui_custom(
310 ui,
311 provider,
312 data,
313 context_menu,
314 |ui, op, enabled, reason, context_menu| {
315 ui.add_enabled_ui(enabled, |ui| {
316 let mut button = ui
317 .button(op.get_name(context_menu).as_ref())
318 .on_hover_text(op.name());
319 if !enabled {
320 button = button.on_disabled_hover_text(format!("{}\n{reason}", op.name()));
321 }
322 button
323 })
324 .inner
325 },
326 )
327 }
328
329 pub fn gui_custom<F>(
334 &mut self,
335 ui: &mut egui::Ui,
336 provider: &dyn TableProvider,
337 data: &mut TableState,
338 context_menu: bool,
339 mut button_renderer: F,
340 ) -> Result<bool, TableError>
341 where
342 F: FnMut(
343 &mut egui::Ui,
344 &mut Box<dyn TableOperation>,
345 bool, &str, bool, ) -> egui::Response,
349 {
350 let refresh = self.update(ui.ctx());
351 let mut any_clicked = false;
352 let num_groups = self.groups.len();
353
354 for (g_idx, op_group) in self.groups.iter_mut().enumerate() {
356 for op in op_group {
357 let is_pending = op.is_pending();
358
359 if op.pollable() {
360 op.poll(ui, data)?;
361 }
362 let (enabled, reason) = if is_pending {
363 (false, t!("operation-pending"))
364 } else {
365 op.evaluate_enablement(data)
366 };
367 if !context_menu {
368 op.extra_ui(ui, data)?;
369 }
370 let response = button_renderer(ui, op, enabled, reason.as_ref(), context_menu);
371 if response.clicked() {
372 any_clicked = true;
373 let mut ctx = OperationContext { ui, data, provider };
374 op.exec(&mut ctx)?;
375 }
376 }
377 if g_idx + 1 < num_groups {
379 ui.separator();
380 }
381 }
382 if any_clicked && context_menu {
383 ui.close_kind(egui::UiKind::Menu);
384 }
385 Ok(refresh)
386 }
387
388 pub fn show_group<F>(
391 &mut self,
392 ui: &mut egui::Ui,
393 provider: &dyn TableProvider,
394 data: &mut TableState,
395 group_idx: usize,
396 context_menu: bool,
397 mut button_renderer: F,
398 ) -> Result<bool, TableError>
399 where
400 F: FnMut(
401 &mut egui::Ui,
402 &mut Box<dyn TableOperation>,
403 bool, &str, ) -> egui::Response,
406 {
407 if group_idx >= self.groups.len() {
408 return Ok(false);
409 }
410
411 let refresh = self.update(ui.ctx());
412 let mut any_clicked = false;
413
414 let op_group = &mut self.groups[group_idx];
415 for op in op_group {
416 let is_pending = op.is_pending();
417
418 if op.pollable() {
419 op.poll(ui, data)?;
420 }
421 let (enabled, reason) = if is_pending {
422 (false, t!("operation-pending"))
423 } else {
424 op.evaluate_enablement(data)
425 };
426
427 if !context_menu {
428 op.extra_ui(ui, data)?;
429 }
430
431 let response = button_renderer(ui, op, enabled, reason.as_ref());
432 if response.clicked() {
433 any_clicked = true;
434 let mut ctx = OperationContext { ui, data, provider };
435 op.exec(&mut ctx)?;
436 }
437 }
438
439 if any_clicked && context_menu {
440 ui.close_kind(egui::UiKind::Menu);
441 }
442
443 Ok(refresh)
444 }
445
446 pub fn show_operation<F>(
449 &mut self,
450 ui: &mut egui::Ui,
451 provider: &dyn TableProvider,
452 data: &mut TableState,
453 group_idx: usize,
454 op_idx: usize,
455 context_menu: bool,
456 button_renderer: F,
457 ) -> Result<bool, TableError>
458 where
459 F: FnOnce(
460 &mut egui::Ui,
461 &mut Box<dyn TableOperation>,
462 bool, &str, ) -> egui::Response,
465 {
466 if group_idx >= self.groups.len() || op_idx >= self.groups[group_idx].len() {
467 return Ok(false);
468 }
469
470 let refresh = self.update(ui.ctx());
471
472 let op = &mut self.groups[group_idx][op_idx];
473 let is_pending = op.is_pending();
474
475 if op.pollable() {
476 op.poll(ui, data)?;
477 }
478 let (enabled, reason) = if is_pending {
479 (false, t!("operation-pending"))
480 } else {
481 op.evaluate_enablement(data)
482 };
483
484 if !context_menu {
485 op.extra_ui(ui, data)?;
486 }
487
488 let response = button_renderer(ui, op, enabled, reason.as_ref());
489 if response.clicked() {
490 let mut ctx = OperationContext { ui, data, provider };
491 op.exec(&mut ctx)?;
492 if context_menu {
493 ui.close_kind(egui::UiKind::Menu);
494 }
495 }
496
497 Ok(refresh)
498 }
499}
500
501#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
502pub enum TableOperationEnablement {
503 #[default]
504 Always,
505 AtLeastOneFiltered,
506 AtLeastOneSelected,
507 OneSelected,
508}
509
510pub trait TableOperation: std::any::Any + std::fmt::Debug + Send + Sync {
511 fn name(&self) -> Cow<'_, str>;
512 fn icon(&self) -> &'static str {
513 "X"
514 }
515 fn get_name(&self, full: bool) -> Cow<'_, str> {
516 if full {
517 Cow::Owned(format!("{} {}", self.name(), self.icon()))
518 } else {
519 Cow::Borrowed(self.icon())
520 }
521 }
522 fn refresh_on_completion(&self) -> bool {
523 false
524 }
525 fn pollable(&self) -> bool {
526 false
527 }
528 fn is_first_page(&self) -> bool {
529 true
530 }
531 fn is_last_page(&self) -> bool {
532 true
533 }
534 fn enabled(&self) -> TableOperationEnablement;
535 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError>;
536 fn extra_ui(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> {
537 Ok(())
538 }
539 fn is_pending(&mut self) -> bool {
540 false
541 }
542
543 fn on_completed(&mut self, _success: bool) {}
545
546 fn poll(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> {
548 Ok(())
549 }
550 fn consume(&mut self) -> Result<(), TableError> {
551 Ok(())
552 }
553 fn error(&self) -> Option<&str> {
554 None
555 }
556 fn clear_error(&mut self) {}
557 fn is_modal_open(&self) -> bool {
558 false
559 }
560 fn set_modal_open(&mut self, _open: bool) {}
561 fn reset(&mut self) {}
562
563 fn pollable_modal(
564 &mut self,
565 ui: &mut egui::Ui,
566 centered: bool,
567 action: Cow<'_, str>,
568 action_progressive: Cow<'_, str>,
569 input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
570 ) -> Result<(), TableError>
571 where
572 Self: Sized,
573 {
574 if self.is_modal_open() {
575 egui::Modal::new(ui.id().with("pollable_modal"))
576 .show(ui.ctx(), |ui| {
577 ui.scope_builder(
578 egui::UiBuilder::new().layout(egui::Layout::top_down(if centered {
579 egui::Align::Center
580 } else {
581 egui::Align::Min
582 })),
583 |ui| {
584 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
585 ui.heading(
586 egui::RichText::new(format!("{} {}", self.name(), self.icon()))
587 .strong(),
588 );
589 ui.separator();
590 ui.spacing_mut().item_spacing.y = 5.0;
591
592 let is_pending = self.is_pending();
593 ui.add_enabled_ui(!is_pending, |ui| input_ui(ui, self))
594 .inner?;
595 ui.add_space(10.0);
596
597 if let Some(error) = self.error() {
598 ui.colored_label(egui::Color32::RED, t!("error"));
599 ui.colored_label(egui::Color32::RED, error);
600 }
601
602 if is_pending {
603 ui.label(action_progressive);
604 ui.add_space(5.0);
605 ui.spinner();
606 } else {
607 if self.is_last_page() {
608 let is_allowed = self.poll_allow_execution();
609 if ui
610 .add_enabled(is_allowed, egui::Button::new(action))
611 .clicked()
612 {
613 self.clear_error();
614 self.consume()?;
615 }
616 }
617 if self.is_first_page() && ui.button(t!("cancel")).clicked() {
618 self.reset();
619 }
620 }
621 Ok(())
622 },
623 )
624 .inner
625 })
626 .inner
627 } else {
628 Ok(())
629 }
630 }
631
632 fn polled_modal(
633 &mut self,
634 ui: &mut egui::Ui,
635 heading: Cow<'_, str>,
636 action_progressive: Cow<'_, str>,
637 input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
638 ) -> Result<(), TableError>
639 where
640 Self: Sized,
641 {
642 if self.is_modal_open() {
643 egui::Modal::new(ui.id().with("polled_modal"))
644 .show(ui.ctx(), |ui| {
645 ui.vertical_centered(|ui| {
646 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
647 ui.heading(heading);
648 ui.separator();
649 ui.spacing_mut().item_spacing.y = 5.0;
650
651 if self.is_pending() {
652 ui.label(action_progressive);
653 ui.add_space(5.0);
654 ui.spinner();
655 } else if let Some(error) = self.error() {
656 ui.colored_label(egui::Color32::RED, t!("error"));
657 ui.colored_label(egui::Color32::RED, error);
658 } else {
659 input_ui(ui, self)?;
660 }
661
662 ui.add_space(10.0);
663 if ui.button(t!("close")).clicked() {
664 self.reset();
665 }
666 Ok::<_, TableError>(())
667 })
668 })
669 .inner
670 .inner?;
671 }
672 Ok(())
673 }
674
675 fn poll_allow_execution(&self) -> bool {
676 true
677 }
678
679 fn evaluate_enablement(&self, state: &TableState) -> (bool, Cow<'static, str>) {
682 match self.enabled() {
683 TableOperationEnablement::Always => (true, Cow::Borrowed("")),
684 TableOperationEnablement::AtLeastOneSelected => (
685 !state.selected_rows.is_empty(),
686 t!("operation-at-least-one"),
687 ),
688 TableOperationEnablement::OneSelected => {
689 (state.selected_rows.len() == 1, t!("operation-one"))
690 }
691 TableOperationEnablement::AtLeastOneFiltered => (
692 !state.active_rows.is_empty(),
693 t!("operation-at-least-one-filtered"),
694 ),
695 }
696 }
697}
698
699#[derive(Debug, Default)]
702pub struct CopyRows {
703 pub prioritize_hovers: bool,
704}
705
706impl TableOperation for CopyRows {
707 fn name(&self) -> Cow<'_, str> {
708 if self.prioritize_hovers {
709 t!("copy-hovered-rows")
710 } else {
711 t!("copy-rows")
712 }
713 }
714 fn icon(&self) -> &'static str {
715 if self.prioritize_hovers {
716 "📁"
717 } else {
718 "📋"
719 }
720 }
721 fn enabled(&self) -> TableOperationEnablement {
722 TableOperationEnablement::AtLeastOneSelected
723 }
724 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
725 let mut output = String::with_capacity(2048);
727
728 ctx.provider.for_selected_rows(ctx.data, &mut |row| {
729 if !output.is_empty() {
730 output.push('\n');
731 }
732 for (i, (val, hover)) in row.iter().enumerate() {
733 if i > 0 {
734 output.push(',');
735 }
736 let cell_text = if self.prioritize_hovers {
737 hover.as_deref().unwrap_or(val)
738 } else {
739 val
740 };
741 output.push_str(cell_text);
742 }
743 Ok(())
744 })?;
745
746 ctx.ui.ctx().copy_text(output);
747 Ok(())
748 }
749}
750
751#[derive(Debug, Default)]
752pub struct CopyHeadersRows {
753 pub prioritize_hovers: bool,
754}
755
756impl TableOperation for CopyHeadersRows {
757 fn name(&self) -> Cow<'_, str> {
758 if self.prioritize_hovers {
759 t!("copy-hovered-rows-with-headers")
760 } else {
761 t!("copy-rows-with-headers")
762 }
763 }
764 fn icon(&self) -> &'static str {
765 if self.prioritize_hovers {
766 "🗄"
767 } else {
768 "📜"
769 }
770 }
771 fn enabled(&self) -> TableOperationEnablement {
772 TableOperationEnablement::AtLeastOneSelected
773 }
774
775 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
776 let headers = ctx.provider.headers();
777
778 let mut output = String::with_capacity(2048);
780
781 for (i, header) in headers.iter().enumerate() {
783 if i > 0 {
784 output.push(',');
785 }
786 output.push_str(header);
787 }
788
789 ctx.provider.for_selected_rows(ctx.data, &mut |row| {
791 output.push('\n');
792 for (i, (val, hover)) in row.iter().enumerate() {
793 if i > 0 {
794 output.push(',');
795 }
796 let cell_text = if self.prioritize_hovers {
797 hover.as_deref().unwrap_or(val)
798 } else {
799 val
800 };
801 output.push_str(cell_text);
802 }
803 Ok(())
804 })?;
805
806 ctx.ui.ctx().copy_text(output);
808 Ok(())
809 }
810}
811
812#[derive(Debug, Default)]
813pub struct FilterSelectAll;
814
815impl TableOperation for FilterSelectAll {
816 fn name(&self) -> Cow<'_, str> {
817 t!("select-filtered")
818 }
819 fn icon(&self) -> &'static str {
820 "☑"
821 }
822 fn enabled(&self) -> TableOperationEnablement {
823 TableOperationEnablement::Always
824 }
825 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
826 let active_u32_iter = ctx.data.active_rows.iter().map(|&row| row as u32);
827 ctx.data.selected_rows.extend(active_u32_iter);
828 Ok(())
829 }
830}
831
832#[derive(Debug, Default)]
833pub struct FilterDeSelectAll;
834
835impl TableOperation for FilterDeSelectAll {
836 fn name(&self) -> Cow<'_, str> {
837 t!("deselect-filtered")
838 }
839 fn icon(&self) -> &'static str {
840 "❎"
841 }
842 fn enabled(&self) -> TableOperationEnablement {
843 TableOperationEnablement::Always
844 }
845 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
846 ctx.data.active_rows.iter().for_each(|row| {
847 ctx.data.selected_rows.remove(*row as u32);
848 });
849 Ok(())
850 }
851}
852
853#[derive(Debug, Default)]
854pub struct SelectAll;
855
856impl TableOperation for SelectAll {
857 fn name(&self) -> Cow<'_, str> {
858 t!("select-all")
859 }
860 fn icon(&self) -> &'static str {
861 "✔"
862 }
863 fn enabled(&self) -> TableOperationEnablement {
864 TableOperationEnablement::Always
865 }
866 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
867 ctx.data.selected_rows.clear();
868 ctx.data
869 .selected_rows
870 .insert_range(0..ctx.provider.row_count() as u32);
871 Ok(())
872 }
873}
874
875#[derive(Debug, Default)]
876pub struct DeSelectAll;
877
878impl TableOperation for DeSelectAll {
879 fn name(&self) -> Cow<'_, str> {
880 t!("deselect-all")
881 }
882 fn icon(&self) -> &'static str {
883 "❌"
884 }
885 fn enabled(&self) -> TableOperationEnablement {
886 TableOperationEnablement::Always
887 }
888 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
889 ctx.data.selected_rows.clear();
890 Ok(())
891 }
892}