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 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
31pub 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 fn sort_active_rows(
102 &self,
103 active_rows: &mut Vec<usize>,
104 col_index: usize,
105 ascending: bool,
106 ) -> Result<(), TableError> {
107 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 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 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 fn row_hierarchy(&self, _state: &TableState, _row_index: usize) -> Option<RowHierarchy> {
174 None
175 }
176
177 fn is_tree(&self) -> bool {
180 false
181 }
182
183 fn row_parent(&self, _row_index: usize) -> Option<usize> {
185 None
186 }
187
188 fn row_children(&self, _row_index: usize) -> Vec<usize> {
190 Vec::new()
191 }
192
193 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 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 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 fn get_primary(&self, col_index: usize) -> Result<Cow<'_, str>, TableError>;
249
250 fn get_hover(&self, col_index: usize) -> Result<Cow<'_, str>, TableError>;
252
253 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 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 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 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 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, &str, bool, ) -> 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 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 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 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, &str, ) -> 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 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, &str, ) -> 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 fn on_completed(&mut self, _success: bool) {}
607
608 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 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#[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 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 let mut output = String::with_capacity(2048);
842
843 for (i, header) in ctx.provider.headers().enumerate() {
845 if i > 0 {
846 output.push(',');
847 }
848 output.push_str(&header);
849 }
850
851 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 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}