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