1use std::{borrow::Cow, collections::HashSet};
4
5use compact_str::{CompactString, ToCompactString as _};
6use fluent_zero::t;
7use roaring::RoaringBitmap;
8
9use super::{error::TableError, filter::Filter, state::TableState};
10
11pub type TableCell<'a> = (Cow<'a, str>, Option<Cow<'a, str>>);
13
14pub type TableCellOwned = (CompactString, Option<CompactString>);
17
18#[derive(Default, Clone, Debug)]
21pub struct OwnedRow {
22 pub cells: Vec<TableCellOwned>,
24}
25
26impl Row for OwnedRow {
27 fn cell(&self, col_index: usize) -> Option<TableCell<'_>> {
28 self.cells.get(col_index).map(|(val, hover)| {
29 (
30 Cow::Borrowed(val.as_str()),
31 hover.as_ref().map(|h| Cow::Borrowed(h.as_str())),
32 )
33 })
34 }
35 fn column_count(&self) -> usize {
36 self.cells.len()
37 }
38}
39
40pub trait Row {
42 fn cell(&self, col_index: usize) -> Option<TableCell<'_>>;
43 fn column_count(&self) -> usize;
44
45 fn row_index(&self) -> Option<usize> {
47 None
48 }
49
50 fn to_owned_row(&self) -> OwnedRow {
53 let mut cells = Vec::with_capacity(self.column_count());
54 for i in 0..self.column_count() {
55 if let Some((val, hover)) = self.cell(i) {
56 cells.push((
57 val.to_compact_string(),
58 hover.map(|h| h.to_compact_string()),
59 ));
60 }
61 }
62 OwnedRow { cells }
63 }
64}
65
66impl Row for [TableCell<'_>] {
67 fn cell(&self, col_index: usize) -> Option<TableCell<'_>> {
68 self.get(col_index).map(|(val, hover)| {
69 (
70 Cow::Borrowed(val.as_ref()),
71 hover.as_ref().map(|h| Cow::Borrowed(h.as_ref())),
72 )
73 })
74 }
75 fn column_count(&self) -> usize {
76 self.len()
77 }
78}
79
80pub type RowCallback<'b> = dyn FnMut(&dyn Row) -> Result<(), TableError> + 'b;
83
84#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
86pub struct RowHierarchy {
87 pub indent_level: usize,
88 pub has_children: bool,
89 pub is_expanded: bool,
90}
91
92pub struct HeaderIter<'a> {
94 provider: &'a dyn TableProvider,
95 index: usize,
96 count: usize,
97}
98
99impl<'a> HeaderIter<'a> {
100 pub fn new(provider: &'a dyn TableProvider) -> Self {
101 Self {
102 provider,
103 index: 0,
104 count: provider.column_count(),
105 }
106 }
107}
108
109impl<'a> Iterator for HeaderIter<'a> {
110 type Item = Cow<'a, str>;
111
112 fn next(&mut self) -> Option<Self::Item> {
113 if self.index < self.count {
114 let res = self.provider.header(self.index);
115 self.index += 1;
116 res
117 } else {
118 None
119 }
120 }
121
122 fn size_hint(&self) -> (usize, Option<usize>) {
123 let remaining = self.count.saturating_sub(self.index);
124 (remaining, Some(remaining))
125 }
126}
127
128impl ExactSizeIterator for HeaderIter<'_> {
129 fn len(&self) -> usize {
130 self.count.saturating_sub(self.index)
131 }
132}
133
134#[derive(Copy, Clone)]
136pub struct BorrowedRow<'a> {
137 pub provider: &'a dyn TableProvider,
138 pub row_index: usize,
139}
140
141impl Row for BorrowedRow<'_> {
142 fn cell(&self, col_index: usize) -> Option<TableCell<'_>> {
143 self.provider
144 .cell_at(self.row_index, col_index)
145 .ok()
146 .flatten()
147 }
148
149 fn row_index(&self) -> Option<usize> {
150 Some(self.row_index)
151 }
152
153 fn column_count(&self) -> usize {
154 self.provider.column_count()
155 }
156}
157
158pub trait TableProvider {
160 fn column_count(&self) -> usize;
161 fn header(&self, index: usize) -> Option<Cow<'_, str>>;
162
163 fn headers(&self) -> HeaderIter<'_>;
164
165 fn row_count(&self) -> usize;
166
167 fn cell_at(
172 &self,
173 row_index: usize,
174 col_index: usize,
175 ) -> Result<Option<TableCell<'_>>, TableError> {
176 if let Some(owned_row) = self.row_at(row_index)?
178 && let Some((val, hover)) = owned_row.cells.get(col_index)
179 {
180 return Ok(Some((
181 Cow::Owned(val.to_string()),
182 hover.as_ref().map(|h| Cow::Owned(h.to_string())),
183 )));
184 }
185 Ok(None)
186 }
187
188 fn for_row_at(&self, index: usize, f: &mut RowCallback<'_>) -> Result<(), TableError> {
190 let mut idx = 0;
191 self.for_all_rows(&mut |row| {
192 if idx == index {
193 f(row)?;
194 }
195 idx += 1;
196 Ok(())
197 })
198 }
199
200 fn row_at(&self, index: usize) -> Result<Option<OwnedRow>, TableError> {
206 if index >= self.row_count() {
207 return Ok(None);
208 }
209 let mut out: Option<OwnedRow> = None;
210 let mut idx = 0usize;
211 self.for_all_rows(&mut |row| {
212 if idx == index {
213 out = Some(row.to_owned_row());
214 }
215 idx += 1;
216 Ok(())
217 })?;
218 Ok(out)
219 }
220
221 fn for_selected_rows(
223 &self,
224 state: &TableState,
225 f: &mut RowCallback<'_>,
226 ) -> Result<(), TableError>;
227
228 fn for_all_rows(&self, f: &mut RowCallback<'_>) -> Result<(), TableError>;
230
231 fn sort_active_rows(
234 &self,
235 active_rows: &mut Vec<usize>,
236 col_index: usize,
237 ascending: bool,
238 ) -> Result<(), TableError> {
239 if active_rows.len() <= 1 {
240 return Ok(());
241 }
242
243 let active_set: RoaringBitmap = active_rows.iter().map(|&i| i as u32).collect();
244 let mut sort_keys = Vec::with_capacity(active_rows.len());
245
246 let mut idx = 0usize;
247 self.for_all_rows(&mut |row| {
248 if active_set.contains(idx as u32) {
249 let val = row
250 .cell(col_index)
251 .map(|(v, _)| v.to_compact_string())
252 .unwrap_or_default();
253 sort_keys.push((idx, val));
254 }
255 idx += 1;
256 Ok(())
257 })?;
258
259 if ascending {
260 sort_keys.sort_by(|a, b| a.1.cmp(&b.1));
261 } else {
262 sort_keys.sort_by(|a, b| b.1.cmp(&a.1));
263 }
264
265 *active_rows = sort_keys.into_iter().map(|(idx, _)| idx).collect();
266 Ok(())
267 }
268
269 fn filter_rows(
271 &self,
272 state: &TableState,
273 filters: &[(usize, Filter)],
274 ) -> Result<Vec<usize>, TableError> {
275 if filters.is_empty() {
276 return Ok((0..self.row_count()).collect());
277 }
278
279 let initial_capacity = self.row_count().min(2048);
280 let mut passing_indices = Vec::with_capacity(initial_capacity);
281 let mut row_idx = 0;
282
283 self.for_all_rows(&mut |row| {
284 let highlight = state.highlights.get_usize(row_idx);
285 let mut matches = true;
286
287 for &(col_idx, ref filter) in filters {
288 if let Some(cell) = row.cell(col_idx) {
289 if !filter.matches(&cell.0, highlight) {
290 matches = false;
291 break;
292 }
293 } else {
294 matches = false;
295 break;
296 }
297 }
298
299 if matches {
300 passing_indices.push(row_idx);
301 }
302 row_idx += 1;
303 Ok(())
304 })?;
305
306 Ok(passing_indices)
307 }
308
309 fn row_hierarchy(&self, _state: &TableState, _row_index: usize) -> Option<RowHierarchy> {
312 None
313 }
314
315 fn is_tree(&self) -> bool {
318 false
319 }
320
321 fn row_parent(&self, _row_index: usize) -> Option<usize> {
323 None
324 }
325
326 fn row_children(&self, _row_index: usize) -> Vec<usize> {
328 Vec::new()
329 }
330
331 fn row_matches(
333 &self,
334 _state: &TableState,
335 _row_index: usize,
336 _filters: &[(usize, Filter)],
337 _highlight: Option<u8>,
338 ) -> bool {
339 true
340 }
341}
342
343impl dyn TableProvider + '_ {
344 pub fn map_selected_rows<T, F>(
346 &self,
347 state: &TableState,
348 mut f: F,
349 ) -> Result<Vec<T>, TableError>
350 where
351 F: FnMut(&dyn Row) -> Result<T, TableError>,
352 {
353 let mut results = Vec::with_capacity(state.selected_rows.len() as usize);
354 self.for_selected_rows(state, &mut |row| {
355 results.push(f(row)?);
356 Ok(())
357 })?;
358 Ok(results)
359 }
360
361 pub fn map_first_selected_row<T, F>(
363 &self,
364 state: &TableState,
365 f: F,
366 ) -> Result<Option<T>, TableError>
367 where
368 F: FnOnce(&dyn Row) -> Result<T, TableError>,
369 {
370 let mut result = None;
371 let mut f_opt = Some(f);
372
373 self.for_selected_rows(state, &mut |row| {
374 if let Some(f_once) = f_opt.take() {
375 result = Some(f_once(row)?);
376 }
377 Ok(())
378 })?;
379
380 Ok(result)
381 }
382}
383
384pub trait RowSliceExt {
386 fn get_primary(&self, col_index: usize) -> Result<Cow<'_, str>, TableError>;
388
389 fn get_hover(&self, col_index: usize) -> Result<Cow<'_, str>, TableError>;
391
392 fn parse_primary<T>(&self, col_index: usize) -> Result<T, TableError>
394 where
395 T: std::str::FromStr,
396 <T as std::str::FromStr>::Err: std::fmt::Display;
397
398 fn parse_hover<T>(&self, col_index: usize) -> Result<T, TableError>
400 where
401 T: std::str::FromStr,
402 <T as std::str::FromStr>::Err: std::fmt::Display;
403}
404
405impl RowSliceExt for dyn Row + '_ {
406 fn get_primary(&self, col_index: usize) -> Result<Cow<'_, str>, TableError> {
407 self.cell(col_index)
408 .map(|(val, _)| val)
409 .ok_or(TableError::CorruptedState)
410 }
411
412 fn get_hover(&self, col_index: usize) -> Result<Cow<'_, str>, TableError> {
413 self.cell(col_index)
414 .and_then(|(_, hover)| hover)
415 .ok_or(TableError::CorruptedState)
416 }
417
418 fn parse_primary<T>(&self, col_index: usize) -> Result<T, TableError>
419 where
420 T: std::str::FromStr,
421 <T as std::str::FromStr>::Err: std::fmt::Display,
422 {
423 T::from_str(self.get_primary(col_index)?.as_ref())
424 .map_err(|e| TableError::Generic(e.to_string()))
425 }
426
427 fn parse_hover<T>(&self, col_index: usize) -> Result<T, TableError>
428 where
429 T: std::str::FromStr,
430 <T as std::str::FromStr>::Err: std::fmt::Display,
431 {
432 T::from_str(self.get_hover(col_index)?.as_ref())
433 .map_err(|e| TableError::Generic(e.to_string()))
434 }
435}
436
437pub struct OperationContext<'a, 'b> {
439 pub ui: &'a mut egui::Ui,
440 pub data: &'a mut TableState,
441 pub provider: &'b dyn TableProvider,
442}
443
444#[derive(Debug, Default)]
446pub struct TableOperations {
447 pub groups: Vec<Vec<Box<dyn TableOperation>>>,
448 pub pending_tracker: HashSet<(usize, usize), ahash::RandomState>,
449 pub last_tick: u64,
450}
451
452impl TableOperations {
453 #[must_use]
454 pub fn new() -> Self {
455 Self::default()
456 }
457
458 #[must_use]
459 pub fn with_group(mut self, group: Vec<Box<dyn TableOperation>>) -> Self {
460 self.groups.push(group);
461 self
462 }
463
464 #[must_use]
465 pub fn with_operation(mut self, op: impl TableOperation + 'static) -> Self {
466 if let Some(group) = self.groups.last_mut() {
467 group.push(Box::new(op));
468 } else {
469 self.groups.push(vec![Box::new(op)]);
470 }
471 self
472 }
473
474 pub fn update(&mut self, ctx: &egui::Context) -> bool {
477 let mut refresh = false;
478 let current_tick = ctx.cumulative_frame_nr();
479 if self.last_tick != current_tick {
480 self.last_tick = current_tick;
481
482 for (g_idx, op_group) in self.groups.iter_mut().enumerate() {
483 for (op_idx, op) in op_group.iter_mut().enumerate() {
484 let key = (g_idx, op_idx);
485 let pending = op.is_pending();
486 let was_pending = self.pending_tracker.contains(&key);
487
488 if was_pending && !pending {
489 self.pending_tracker.remove(&key);
490 let success = op.error().is_none();
491 op.on_completed(success);
492 if op.refresh_on_completion() {
493 refresh = true;
494 }
495 } else if !was_pending && pending {
496 self.pending_tracker.insert(key);
497 }
498 }
499 }
500 }
501 refresh
502 }
503
504 pub fn gui(
506 &mut self,
507 ui: &mut egui::Ui,
508 provider: &dyn TableProvider,
509 data: &mut TableState,
510 context_menu: bool,
511 ) -> Result<bool, TableError> {
512 self.gui_custom(
513 ui,
514 provider,
515 data,
516 context_menu,
517 |ui, op, enabled, reason, context_menu| {
518 ui.add_enabled_ui(enabled, |ui| {
519 let mut button = ui
520 .button(op.get_name(context_menu).as_ref())
521 .on_hover_text(op.name());
522 if !enabled {
523 button = button.on_disabled_hover_text(format!("{}\n{reason}", op.name()));
524 }
525 button
526 })
527 .inner
528 },
529 )
530 }
531
532 pub fn gui_custom<F>(
537 &mut self,
538 ui: &mut egui::Ui,
539 provider: &dyn TableProvider,
540 data: &mut TableState,
541 context_menu: bool,
542 mut button_renderer: F,
543 ) -> Result<bool, TableError>
544 where
545 F: FnMut(
546 &mut egui::Ui,
547 &mut Box<dyn TableOperation>,
548 bool, &str, bool, ) -> egui::Response,
552 {
553 let refresh = self.update(ui.ctx());
554 let mut any_clicked = false;
555 let num_groups = self.groups.len();
556
557 for (g_idx, op_group) in self.groups.iter_mut().enumerate() {
559 for op in op_group {
560 let is_pending = op.is_pending();
561
562 if op.pollable() {
563 op.poll(ui, data)?;
564 }
565 let (enabled, reason) = if is_pending {
566 (false, t!("operation-pending"))
567 } else {
568 op.evaluate_enablement(data)
569 };
570 if !context_menu {
571 op.extra_ui(ui, data)?;
572 }
573 let response = button_renderer(ui, op, enabled, reason.as_ref(), context_menu);
574 if response.clicked() {
575 any_clicked = true;
576 let mut ctx = OperationContext { ui, data, provider };
577 op.exec(&mut ctx)?;
578 }
579 }
580 if g_idx + 1 < num_groups {
582 ui.separator();
583 }
584 }
585 if any_clicked && context_menu {
586 ui.close_kind(egui::UiKind::Menu);
587 }
588 Ok(refresh)
589 }
590
591 pub fn show_group<F>(
594 &mut self,
595 ui: &mut egui::Ui,
596 provider: &dyn TableProvider,
597 data: &mut TableState,
598 group_idx: usize,
599 context_menu: bool,
600 mut button_renderer: F,
601 ) -> Result<bool, TableError>
602 where
603 F: FnMut(
604 &mut egui::Ui,
605 &mut Box<dyn TableOperation>,
606 bool, &str, ) -> egui::Response,
609 {
610 if group_idx >= self.groups.len() {
611 return Ok(false);
612 }
613
614 let refresh = self.update(ui.ctx());
615 let mut any_clicked = false;
616
617 let op_group = &mut self.groups[group_idx];
618 for op in op_group {
619 let is_pending = op.is_pending();
620
621 if op.pollable() {
622 op.poll(ui, data)?;
623 }
624 let (enabled, reason) = if is_pending {
625 (false, t!("operation-pending"))
626 } else {
627 op.evaluate_enablement(data)
628 };
629
630 if !context_menu {
631 op.extra_ui(ui, data)?;
632 }
633
634 let response = button_renderer(ui, op, enabled, reason.as_ref());
635 if response.clicked() {
636 any_clicked = true;
637 let mut ctx = OperationContext { ui, data, provider };
638 op.exec(&mut ctx)?;
639 }
640 }
641
642 if any_clicked && context_menu {
643 ui.close_kind(egui::UiKind::Menu);
644 }
645
646 Ok(refresh)
647 }
648
649 pub fn show_operation<F>(
652 &mut self,
653 ui: &mut egui::Ui,
654 provider: &dyn TableProvider,
655 data: &mut TableState,
656 group_idx: usize,
657 op_idx: usize,
658 context_menu: bool,
659 button_renderer: F,
660 ) -> Result<bool, TableError>
661 where
662 F: FnOnce(
663 &mut egui::Ui,
664 &mut Box<dyn TableOperation>,
665 bool, &str, ) -> egui::Response,
668 {
669 if group_idx >= self.groups.len() || op_idx >= self.groups[group_idx].len() {
670 return Ok(false);
671 }
672
673 let refresh = self.update(ui.ctx());
674
675 let op = &mut self.groups[group_idx][op_idx];
676 let is_pending = op.is_pending();
677
678 if op.pollable() {
679 op.poll(ui, data)?;
680 }
681 let (enabled, reason) = if is_pending {
682 (false, t!("operation-pending"))
683 } else {
684 op.evaluate_enablement(data)
685 };
686
687 if !context_menu {
688 op.extra_ui(ui, data)?;
689 }
690
691 let response = button_renderer(ui, op, enabled, reason.as_ref());
692 if response.clicked() {
693 let mut ctx = OperationContext { ui, data, provider };
694 op.exec(&mut ctx)?;
695 if context_menu {
696 ui.close_kind(egui::UiKind::Menu);
697 }
698 }
699
700 Ok(refresh)
701 }
702}
703
704#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
706pub enum TableOperationEnablement {
707 #[default]
708 Always,
709 AtLeastOneFiltered,
710 AtLeastOneSelected,
711 OneSelected,
712}
713
714pub trait TableOperation: std::any::Any + std::fmt::Debug + Send + Sync {
716 fn name(&self) -> Cow<'_, str>;
717 fn icon(&self) -> &'static str {
718 "X"
719 }
720 fn get_name(&self, full: bool) -> Cow<'_, str> {
721 if full {
722 Cow::Owned(format!("{} {}", self.name(), self.icon()))
723 } else {
724 Cow::Borrowed(self.icon())
725 }
726 }
727 fn refresh_on_completion(&self) -> bool {
728 false
729 }
730 fn pollable(&self) -> bool {
731 false
732 }
733 fn is_first_page(&self) -> bool {
734 true
735 }
736 fn is_last_page(&self) -> bool {
737 true
738 }
739 fn enabled(&self) -> TableOperationEnablement;
740 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError>;
741 fn extra_ui(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> {
742 Ok(())
743 }
744 fn is_pending(&mut self) -> bool {
745 false
746 }
747
748 fn on_completed(&mut self, _success: bool) {}
750
751 fn poll(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> {
753 Ok(())
754 }
755 fn consume(&mut self) -> Result<(), TableError> {
756 Ok(())
757 }
758 fn error(&self) -> Option<&str> {
759 None
760 }
761 fn clear_error(&mut self) {}
762 fn is_modal_open(&self) -> bool {
763 false
764 }
765 fn set_modal_open(&mut self, _open: bool) {}
766 fn reset(&mut self) {}
767
768 fn pollable_modal(
770 &mut self,
771 ui: &mut egui::Ui,
772 centered: bool,
773 action: Cow<'_, str>,
774 action_progressive: Cow<'_, str>,
775 input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
776 ) -> Result<(), TableError>
777 where
778 Self: Sized,
779 {
780 if self.is_modal_open() {
781 egui::Modal::new(ui.id().with("pollable_modal"))
782 .show(ui.ctx(), |ui| {
783 ui.scope_builder(
784 egui::UiBuilder::new().layout(egui::Layout::top_down(if centered {
785 egui::Align::Center
786 } else {
787 egui::Align::Min
788 })),
789 |ui| {
790 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
791 ui.heading(
792 egui::RichText::new(format!("{} {}", self.name(), self.icon()))
793 .strong(),
794 );
795 ui.separator();
796 ui.spacing_mut().item_spacing.y = 5.0;
797
798 let is_pending = self.is_pending();
799 ui.add_enabled_ui(!is_pending, |ui| input_ui(ui, self))
800 .inner?;
801 ui.add_space(10.0);
802
803 if let Some(error) = self.error() {
804 ui.colored_label(egui::Color32::RED, t!("error"));
805 ui.colored_label(egui::Color32::RED, error);
806 }
807
808 if is_pending {
809 ui.label(action_progressive);
810 ui.add_space(5.0);
811 ui.spinner();
812 } else {
813 if self.is_last_page() {
814 let is_allowed = self.poll_allow_execution();
815 if ui
816 .add_enabled(is_allowed, egui::Button::new(action))
817 .clicked()
818 {
819 self.clear_error();
820 self.consume()?;
821 }
822 }
823 if self.is_first_page() && ui.button(t!("cancel")).clicked() {
824 self.reset();
825 }
826 }
827 Ok(())
828 },
829 )
830 .inner
831 })
832 .inner
833 } else {
834 Ok(())
835 }
836 }
837
838 fn polled_modal(
840 &mut self,
841 ui: &mut egui::Ui,
842 heading: Cow<'_, str>,
843 action_progressive: Cow<'_, str>,
844 input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
845 ) -> Result<(), TableError>
846 where
847 Self: Sized,
848 {
849 if self.is_modal_open() {
850 egui::Modal::new(ui.id().with("polled_modal"))
851 .show(ui.ctx(), |ui| {
852 ui.vertical_centered(|ui| {
853 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
854 ui.heading(heading);
855 ui.separator();
856 ui.spacing_mut().item_spacing.y = 5.0;
857
858 if self.is_pending() {
859 ui.label(action_progressive);
860 ui.add_space(5.0);
861 ui.spinner();
862 } else if let Some(error) = self.error() {
863 ui.colored_label(egui::Color32::RED, t!("error"));
864 ui.colored_label(egui::Color32::RED, error);
865 } else {
866 input_ui(ui, self)?;
867 }
868
869 ui.add_space(10.0);
870 if ui.button(t!("close")).clicked() {
871 self.reset();
872 }
873 Ok::<_, TableError>(())
874 })
875 })
876 .inner
877 .inner?;
878 }
879 Ok(())
880 }
881
882 fn poll_allow_execution(&self) -> bool {
883 true
884 }
885
886 fn evaluate_enablement(&self, state: &TableState) -> (bool, Cow<'static, str>) {
889 match self.enabled() {
890 TableOperationEnablement::Always => (true, Cow::Borrowed("")),
891 TableOperationEnablement::AtLeastOneSelected => (
892 !state.selected_rows.is_empty(),
893 t!("operation-at-least-one"),
894 ),
895 TableOperationEnablement::OneSelected => {
896 (state.selected_rows.len() == 1, t!("operation-one"))
897 }
898 TableOperationEnablement::AtLeastOneFiltered => (
899 !state.active_rows.is_empty(),
900 t!("operation-at-least-one-filtered"),
901 ),
902 }
903 }
904}
905
906#[derive(Debug, Default)]
909pub struct CopyRows {
910 pub prioritize_hovers: bool,
911}
912
913impl TableOperation for CopyRows {
914 fn name(&self) -> Cow<'_, str> {
915 if self.prioritize_hovers {
916 t!("copy-hovered-rows")
917 } else {
918 t!("copy-rows")
919 }
920 }
921 fn icon(&self) -> &'static str {
922 if self.prioritize_hovers {
923 "📁"
924 } else {
925 "📋"
926 }
927 }
928 fn enabled(&self) -> TableOperationEnablement {
929 TableOperationEnablement::AtLeastOneSelected
930 }
931 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
932 let mut output = String::with_capacity(2048);
934
935 ctx.provider.for_selected_rows(ctx.data, &mut |row| {
936 if !output.is_empty() {
937 output.push('\n');
938 }
939 for i in 0..row.column_count() {
940 if i > 0 {
941 output.push(',');
942 }
943 if let Some((val, hover)) = row.cell(i) {
944 let cell_text = if self.prioritize_hovers {
945 hover.as_ref().map_or_else(|| val.as_ref(), |h| h.as_ref())
946 } else {
947 val.as_ref()
948 };
949 output.push_str(cell_text);
950 }
951 }
952 Ok(())
953 })?;
954
955 ctx.ui.ctx().copy_text(output);
956 Ok(())
957 }
958}
959
960#[derive(Debug, Default)]
961pub struct CopyHeadersRows {
962 pub prioritize_hovers: bool,
963}
964
965impl TableOperation for CopyHeadersRows {
966 fn name(&self) -> Cow<'_, str> {
967 if self.prioritize_hovers {
968 t!("copy-hovered-rows-with-headers")
969 } else {
970 t!("copy-rows-with-headers")
971 }
972 }
973 fn icon(&self) -> &'static str {
974 if self.prioritize_hovers {
975 "🗄"
976 } else {
977 "📜"
978 }
979 }
980 fn enabled(&self) -> TableOperationEnablement {
981 TableOperationEnablement::AtLeastOneSelected
982 }
983
984 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
985 let mut output = String::with_capacity(2048);
987
988 for (i, header) in ctx.provider.headers().enumerate() {
990 if i > 0 {
991 output.push(',');
992 }
993 output.push_str(&header);
994 }
995
996 ctx.provider.for_selected_rows(ctx.data, &mut |row| {
998 output.push('\n');
999 for i in 0..row.column_count() {
1000 if i > 0 {
1001 output.push(',');
1002 }
1003 if let Some((val, hover)) = row.cell(i) {
1004 let cell_text = if self.prioritize_hovers {
1005 hover.as_ref().map_or_else(|| val.as_ref(), |h| h.as_ref())
1006 } else {
1007 val.as_ref()
1008 };
1009 output.push_str(cell_text);
1010 }
1011 }
1012 Ok(())
1013 })?;
1014
1015 ctx.ui.ctx().copy_text(output);
1017 Ok(())
1018 }
1019}
1020
1021#[derive(Debug, Default)]
1022pub struct FilterSelectAll;
1023
1024impl TableOperation for FilterSelectAll {
1025 fn name(&self) -> Cow<'_, str> {
1026 t!("select-filtered")
1027 }
1028 fn icon(&self) -> &'static str {
1029 "☑"
1030 }
1031 fn enabled(&self) -> TableOperationEnablement {
1032 TableOperationEnablement::Always
1033 }
1034 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
1035 let active_u32_iter = ctx.data.active_rows.iter().map(|&row| row as u32);
1036 ctx.data.selected_rows.extend(active_u32_iter);
1037 Ok(())
1038 }
1039}
1040
1041#[derive(Debug, Default)]
1042pub struct FilterDeSelectAll;
1043
1044impl TableOperation for FilterDeSelectAll {
1045 fn name(&self) -> Cow<'_, str> {
1046 t!("deselect-filtered")
1047 }
1048 fn icon(&self) -> &'static str {
1049 "❎"
1050 }
1051 fn enabled(&self) -> TableOperationEnablement {
1052 TableOperationEnablement::Always
1053 }
1054 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
1055 ctx.data.active_rows.iter().for_each(|row| {
1056 ctx.data.selected_rows.remove(*row as u32);
1057 });
1058 Ok(())
1059 }
1060}
1061
1062#[derive(Debug, Default)]
1063pub struct SelectAll;
1064
1065impl TableOperation for SelectAll {
1066 fn name(&self) -> Cow<'_, str> {
1067 t!("select-all")
1068 }
1069 fn icon(&self) -> &'static str {
1070 "✔"
1071 }
1072 fn enabled(&self) -> TableOperationEnablement {
1073 TableOperationEnablement::Always
1074 }
1075 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
1076 ctx.data.selected_rows.clear();
1077 ctx.data
1078 .selected_rows
1079 .insert_range(0..ctx.provider.row_count() as u32);
1080 Ok(())
1081 }
1082}
1083
1084#[derive(Debug, Default)]
1085pub struct DeSelectAll;
1086
1087impl TableOperation for DeSelectAll {
1088 fn name(&self) -> Cow<'_, str> {
1089 t!("deselect-all")
1090 }
1091 fn icon(&self) -> &'static str {
1092 "❌"
1093 }
1094 fn enabled(&self) -> TableOperationEnablement {
1095 TableOperationEnablement::Always
1096 }
1097 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
1098 ctx.data.selected_rows.clear();
1099 Ok(())
1100 }
1101}