1use std::borrow::Cow;
2
3use compact_str::ToCompactString as _;
4
5use super::{error::TableError, filter::Filter, state::TableState};
6
7pub type TableCell<'a> = (Cow<'a, str>, Option<Cow<'a, str>>);
9
10pub type RowSlice<'a, 'b> = &'b [TableCell<'a>];
12
13pub type RowCallback<'b> = dyn for<'a, 'c> FnMut(RowSlice<'a, 'c>) -> Result<(), TableError> + 'b;
18
19#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
20pub struct RowHierarchy {
21 pub indent_level: usize,
22 pub has_children: bool,
23 pub is_expanded: bool,
24}
25
26pub trait TableProvider {
27 fn headers(&self) -> &[&str];
28 fn row_count(&self) -> usize;
29
30 fn for_selected_rows(
31 &self,
32 state: &TableState,
33 f: &mut RowCallback<'_>,
34 ) -> Result<(), TableError>;
35
36 fn for_all_rows(&self, f: &mut RowCallback<'_>) -> Result<(), TableError>;
37
38 fn sort_active_rows(
41 &self,
42 active_rows: &mut Vec<usize>,
43 col_index: usize,
44 ascending: bool,
45 ) -> Result<(), TableError> {
46 let mut values = Vec::with_capacity(self.row_count());
48 self.for_all_rows(&mut |row| {
49 let val = row
50 .get(col_index)
51 .map(|(v, _)| v.to_compact_string())
52 .unwrap_or_default();
53 values.push(val);
54 Ok(())
55 })?;
56
57 active_rows.sort_by(|&a, &b| {
59 let val_a = values.get(a);
60 let val_b = values.get(b);
61 if ascending {
62 val_a.cmp(&val_b)
63 } else {
64 val_b.cmp(&val_a)
65 }
66 });
67
68 Ok(())
69 }
70
71 fn filter_rows(
73 &self,
74 state: &TableState,
75 filters: &[(usize, Filter)],
76 ) -> Result<Vec<usize>, TableError> {
77 if filters.is_empty() {
78 return Ok((0..self.row_count()).collect());
79 }
80
81 let mut passing_indices = Vec::with_capacity(self.row_count());
82 let mut row_idx = 0;
83
84 self.for_all_rows(&mut |row| {
85 let highlight = state.highlights.get_usize(row_idx);
86 let mut matches = true;
87
88 for &(col_idx, ref filter) in filters {
89 if let Some(cell) = row.get(col_idx) {
90 if !filter.matches(&cell.0, highlight) {
91 matches = false;
92 break;
93 }
94 } else {
95 matches = false;
96 break;
97 }
98 }
99
100 if matches {
101 passing_indices.push(row_idx);
102 }
103 row_idx += 1;
104 Ok(())
105 })?;
106
107 Ok(passing_indices)
108 }
109
110 fn row_hierarchy(&self, _state: &TableState, _row_index: usize) -> Option<RowHierarchy> {
113 None
114 }
115
116 fn is_tree(&self) -> bool {
119 false
120 }
121
122 fn row_parent(&self, _row_index: usize) -> Option<usize> {
124 None
125 }
126
127 fn row_children(&self, _row_index: usize) -> Vec<usize> {
129 Vec::new()
130 }
131
132 fn row_matches(
134 &self,
135 _state: &TableState,
136 _row_index: usize,
137 _filters: &[(usize, Filter)],
138 _highlight: Option<u8>,
139 ) -> bool {
140 true
141 }
142}
143
144impl dyn TableProvider + '_ {
145 pub fn map_selected_rows<T, F>(
147 &self,
148 state: &TableState,
149 mut f: F,
150 ) -> Result<Vec<T>, TableError>
151 where
152 F: FnMut(RowSlice<'_, '_>) -> Result<T, TableError>,
153 {
154 let mut results = Vec::with_capacity(state.selected_rows.len() as usize);
155 self.for_selected_rows(state, &mut |row| {
156 results.push(f(row)?);
157 Ok(())
158 })?;
159 Ok(results)
160 }
161
162 pub fn map_first_selected_row<T, F>(
164 &self,
165 state: &TableState,
166 f: F,
167 ) -> Result<Option<T>, TableError>
168 where
169 F: FnOnce(RowSlice<'_, '_>) -> Result<T, TableError>,
170 {
171 let mut result = None;
172 let mut f_opt = Some(f);
173
174 self.for_selected_rows(state, &mut |row| {
175 if let Some(f_once) = f_opt.take() {
176 result = Some(f_once(row)?);
177 }
178 Ok(())
179 })?;
180
181 Ok(result)
182 }
183}
184
185pub trait RowSliceExt {
186 fn get_primary(&self, col_index: usize) -> Result<&str, TableError>;
188
189 fn get_hover(&self, col_index: usize) -> Result<&str, TableError>;
191
192 fn parse_primary<T>(&self, col_index: usize) -> Result<T, TableError>
194 where
195 T: std::str::FromStr,
196 <T as std::str::FromStr>::Err: std::fmt::Display;
197
198 fn parse_hover<T>(&self, col_index: usize) -> Result<T, TableError>
200 where
201 T: std::str::FromStr,
202 <T as std::str::FromStr>::Err: std::fmt::Display;
203}
204
205impl RowSliceExt for RowSlice<'_, '_> {
206 fn get_primary(&self, col_index: usize) -> Result<&str, TableError> {
207 self.get(col_index)
208 .map(|(val, _)| val.as_ref())
209 .ok_or(TableError::CorruptedState)
210 }
211
212 fn get_hover(&self, col_index: usize) -> Result<&str, TableError> {
213 self.get(col_index)
214 .and_then(|(_, hover)| hover.as_ref().map(AsRef::as_ref))
215 .ok_or(TableError::CorruptedState)
216 }
217
218 fn parse_primary<T>(&self, col_index: usize) -> Result<T, TableError>
219 where
220 T: std::str::FromStr,
221 <T as std::str::FromStr>::Err: std::fmt::Display,
222 {
223 T::from_str(self.get_primary(col_index)?).map_err(|e| TableError::Generic(e.to_string()))
224 }
225
226 fn parse_hover<T>(&self, col_index: usize) -> Result<T, TableError>
227 where
228 T: std::str::FromStr,
229 <T as std::str::FromStr>::Err: std::fmt::Display,
230 {
231 T::from_str(self.get_hover(col_index)?).map_err(|e| TableError::Generic(e.to_string()))
232 }
233}
234
235pub struct OperationContext<'a, 'b> {
236 pub ui: &'a mut egui::Ui,
237 pub data: &'a mut TableState,
238 pub provider: &'b dyn TableProvider,
239}
240
241#[derive(Debug, Default)]
242pub struct TableOperations(pub Vec<Vec<Box<dyn TableOperation>>>);
243
244impl TableOperations {
245 #[must_use]
246 pub fn new() -> Self {
247 Self::default()
248 }
249
250 #[must_use]
251 pub fn with_group(mut self, group: Vec<Box<dyn TableOperation>>) -> Self {
252 self.0.push(group);
253 self
254 }
255
256 #[must_use]
257 pub fn with_operation(mut self, op: impl TableOperation + 'static) -> Self {
258 if let Some(group) = self.0.last_mut() {
259 group.push(Box::new(op));
260 } else {
261 self.0.push(vec![Box::new(op)]);
262 }
263 self
264 }
265
266 pub fn gui(
267 &mut self,
268 ui: &mut egui::Ui,
269 provider: &dyn TableProvider,
270 data: &mut TableState,
271 context_menu: bool,
272 ) -> Result<bool, TableError> {
273 let mut refresh = false;
274 let mut any_clicked = false;
275
276 for op_group in &mut self.0 {
277 for op in op_group {
278 let is_pending = op.is_pending();
279 if op.just_completed() && op.refresh_on_completion() {
280 refresh = true;
281 }
282
283 if op.pollable() {
284 op.poll(ui, data)?;
285 }
286
287 let (enabled, reason): (bool, &'static str) = if is_pending {
288 (false, "Operation pending...")
289 } else {
290 match op.enabled() {
291 TableOperationEnablement::Always => (true, ""),
292 TableOperationEnablement::AtLeastOneSelected => {
293 (!data.selected_rows.is_empty(), "At least one row required")
294 }
295 TableOperationEnablement::OneSelected => {
296 (data.selected_rows.len() == 1, "Exactly one row required")
297 }
298 TableOperationEnablement::AtLeastOneFiltered => (
299 !data.active_rows.is_empty(),
300 "At least one filtered row required",
301 ),
302 }
303 };
304
305 if !context_menu {
306 op.extra_ui(ui, data)?;
307 }
308
309 ui.add_enabled_ui(enabled, |ui| {
310 let mut button = ui
311 .button(op.get_name(context_menu).as_ref())
312 .on_hover_text(op.name());
313
314 if !enabled {
316 button = button.on_disabled_hover_text(format!("{}\n{reason}", op.name()));
317 }
318
319 if button.clicked() {
320 any_clicked = true;
321 let mut ctx = OperationContext { ui, data, provider };
322 op.exec(&mut ctx)
323 } else {
324 Ok(())
325 }
326 })
327 .inner?;
328 }
329 ui.separator();
330 }
331
332 if any_clicked && context_menu {
333 ui.close_kind(egui::UiKind::Menu);
334 }
335
336 Ok(refresh)
337 }
338}
339
340#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
341pub enum TableOperationEnablement {
342 #[default]
343 Always,
344 AtLeastOneFiltered,
345 AtLeastOneSelected,
346 OneSelected,
347}
348
349pub trait TableOperation: std::any::Any + std::fmt::Debug + Send + Sync {
350 fn name(&self) -> Cow<'_, str>;
351 fn icon(&self) -> &'static str {
352 "X"
353 }
354 fn get_name(&self, full: bool) -> Cow<'_, str> {
355 if full {
356 Cow::Owned(format!("{} {}", self.name(), self.icon()))
357 } else {
358 Cow::Borrowed(self.icon())
359 }
360 }
361 fn refresh_on_completion(&self) -> bool {
362 false
363 }
364 fn pollable(&self) -> bool {
365 false
366 }
367 fn is_first_page(&self) -> bool {
368 true
369 }
370 fn is_last_page(&self) -> bool {
371 true
372 }
373 fn enabled(&self) -> TableOperationEnablement;
374 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError>;
375 fn extra_ui(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> {
376 Ok(())
377 }
378 fn is_pending(&mut self) -> bool {
379 false
380 }
381 fn just_completed(&mut self) -> bool {
382 false
383 }
384 fn poll(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> {
386 Ok(())
387 }
388 fn consume(&mut self) -> Result<(), TableError> {
389 Ok(())
390 }
391 fn error(&self) -> Option<&str> {
392 None
393 }
394 fn clear_error(&mut self) {}
395 fn is_modal_open(&self) -> bool {
396 false
397 }
398 fn set_modal_open(&mut self, _open: bool) {}
399 fn new() -> Self
400 where
401 Self: Sized;
402 fn reset(&mut self)
403 where
404 Self: Sized,
405 {
406 *self = Self::new();
407 }
408
409 fn pollable_modal(
410 &mut self,
411 ui: &mut egui::Ui,
412 centered: bool,
413 action: Cow<'_, str>,
414 action_progressive: Cow<'_, str>,
415 input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
416 ) -> Result<(), TableError>
417 where
418 Self: Sized,
419 {
420 if self.is_modal_open() {
421 egui::Modal::new(ui.id().with("pollable_modal"))
422 .show(ui.ctx(), |ui| {
423 ui.scope_builder(
424 egui::UiBuilder::new().layout(egui::Layout::top_down(if centered {
425 egui::Align::Center
426 } else {
427 egui::Align::Min
428 })),
429 |ui| {
430 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
431 ui.heading(
432 egui::RichText::new(format!("{} {}", self.name(), self.icon()))
433 .strong(),
434 );
435 ui.separator();
436 ui.spacing_mut().item_spacing.y = 5.0;
437
438 if self.just_completed() && self.error().is_none() {
439 self.reset();
440 return Ok(());
441 }
442
443 let is_pending = self.is_pending();
444 ui.add_enabled_ui(!is_pending, |ui| input_ui(ui, self))
445 .inner?;
446 ui.add_space(10.0);
447
448 if let Some(error) = self.error() {
449 ui.colored_label(egui::Color32::RED, "Error");
450 ui.colored_label(egui::Color32::RED, error);
451 }
452
453 if is_pending {
454 ui.label(action_progressive);
455 ui.add_space(5.0);
456 ui.spinner();
457 } else {
458 if self.is_last_page() {
459 let is_allowed = self.poll_allow_execution();
460 if ui
461 .add_enabled(is_allowed, egui::Button::new(action))
462 .clicked()
463 {
464 self.clear_error();
465 self.consume()?;
466 }
467 }
468 if self.is_first_page() && ui.button("Cancel").clicked() {
469 self.reset();
470 }
471 }
472 Ok(())
473 },
474 )
475 .inner
476 })
477 .inner
478 } else {
479 Ok(())
480 }
481 }
482
483 fn polled_modal(
484 &mut self,
485 ui: &mut egui::Ui,
486 heading: Cow<'_, str>,
487 action_progressive: Cow<'_, str>,
488 input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
489 ) -> Result<(), TableError>
490 where
491 Self: Sized,
492 {
493 if self.is_modal_open() {
494 egui::Modal::new(ui.id().with("polled_modal"))
495 .show(ui.ctx(), |ui| {
496 ui.vertical_centered(|ui| {
497 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
498 ui.heading(heading);
499 ui.separator();
500 ui.spacing_mut().item_spacing.y = 5.0;
501
502 if self.is_pending() {
503 ui.label(action_progressive);
504 ui.add_space(5.0);
505 ui.spinner();
506 } else if let Some(error) = self.error() {
507 ui.colored_label(egui::Color32::RED, "Error");
508 ui.colored_label(egui::Color32::RED, error);
509 } else {
510 input_ui(ui, self)?;
511 }
512
513 ui.add_space(10.0);
514 if ui.button("Close").clicked() {
515 self.reset();
516 }
517 Ok::<_, TableError>(())
518 })
519 })
520 .inner
521 .inner?;
522 }
523 Ok(())
524 }
525
526 fn poll_allow_execution(&self) -> bool {
527 true
528 }
529}
530
531#[derive(Debug, Default)]
534pub struct CopyRows {
535 pub prioritize_hovers: bool,
536}
537
538impl TableOperation for CopyRows {
539 fn new() -> Self
540 where
541 Self: Sized,
542 {
543 Self::default()
544 }
545 fn name(&self) -> Cow<'_, str> {
546 if self.prioritize_hovers {
547 Cow::Borrowed("Copy hovered rows")
548 } else {
549 Cow::Borrowed("Copy rows")
550 }
551 }
552 fn icon(&self) -> &'static str {
553 if self.prioritize_hovers {
554 "📁"
555 } else {
556 "📋"
557 }
558 }
559 fn enabled(&self) -> TableOperationEnablement {
560 TableOperationEnablement::AtLeastOneSelected
561 }
562 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
563 let mut output = String::with_capacity(2048);
565
566 ctx.provider.for_selected_rows(ctx.data, &mut |row| {
567 if !output.is_empty() {
568 output.push('\n');
569 }
570 for (i, (val, hover)) in row.iter().enumerate() {
571 if i > 0 {
572 output.push(',');
573 }
574 let cell_text = if self.prioritize_hovers {
575 hover.as_deref().unwrap_or(val)
576 } else {
577 val
578 };
579 output.push_str(cell_text);
580 }
581 Ok(())
582 })?;
583
584 ctx.ui.ctx().copy_text(output);
585 Ok(())
586 }
587 fn just_completed(&mut self) -> bool {
588 true
589 }
590}
591
592#[derive(Debug, Default)]
593pub struct CopyHeadersRows {
594 pub prioritize_hovers: bool,
595}
596
597impl TableOperation for CopyHeadersRows {
598 fn new() -> Self
599 where
600 Self: Sized,
601 {
602 Self::default()
603 }
604 fn name(&self) -> Cow<'_, str> {
605 if self.prioritize_hovers {
606 Cow::Borrowed("Copy hovered rows with headers")
607 } else {
608 Cow::Borrowed("Copy rows with headers")
609 }
610 }
611 fn icon(&self) -> &'static str {
612 if self.prioritize_hovers {
613 "🗄"
614 } else {
615 "📜"
616 }
617 }
618 fn enabled(&self) -> TableOperationEnablement {
619 TableOperationEnablement::AtLeastOneSelected
620 }
621
622 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
623 let headers = ctx.provider.headers();
624
625 let mut output = String::with_capacity(2048);
627
628 for (i, header) in headers.iter().enumerate() {
630 if i > 0 {
631 output.push(',');
632 }
633 output.push_str(header);
634 }
635
636 ctx.provider.for_selected_rows(ctx.data, &mut |row| {
638 output.push('\n');
639 for (i, (val, hover)) in row.iter().enumerate() {
640 if i > 0 {
641 output.push(',');
642 }
643 let cell_text = if self.prioritize_hovers {
644 hover.as_deref().unwrap_or(val)
645 } else {
646 val
647 };
648 output.push_str(cell_text);
649 }
650 Ok(())
651 })?;
652
653 ctx.ui.ctx().copy_text(output);
655 Ok(())
656 }
657
658 fn just_completed(&mut self) -> bool {
659 true
660 }
661}
662
663#[derive(Debug, Default)]
664pub struct FilterSelectAll;
665
666impl TableOperation for FilterSelectAll {
667 fn new() -> Self
668 where
669 Self: Sized,
670 {
671 Self
672 }
673 fn name(&self) -> Cow<'_, str> {
674 Cow::Borrowed("Select filtered")
675 }
676 fn icon(&self) -> &'static str {
677 "☑"
678 }
679 fn enabled(&self) -> TableOperationEnablement {
680 TableOperationEnablement::Always
681 }
682 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
683 let active_u32_iter = ctx.data.active_rows.iter().map(|&row| row as u32);
684 ctx.data.selected_rows.extend(active_u32_iter);
685 Ok(())
686 }
687 fn just_completed(&mut self) -> bool {
688 true
689 }
690}
691
692#[derive(Debug, Default)]
693pub struct FilterDeSelectAll;
694
695impl TableOperation for FilterDeSelectAll {
696 fn new() -> Self
697 where
698 Self: Sized,
699 {
700 Self
701 }
702 fn name(&self) -> Cow<'_, str> {
703 Cow::Borrowed("Deselect filtered")
704 }
705 fn icon(&self) -> &'static str {
706 "❎"
707 }
708 fn enabled(&self) -> TableOperationEnablement {
709 TableOperationEnablement::Always
710 }
711 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
712 ctx.data.active_rows.iter().for_each(|row| {
713 ctx.data.selected_rows.remove(*row as u32);
714 });
715 Ok(())
716 }
717 fn just_completed(&mut self) -> bool {
718 true
719 }
720}
721
722#[derive(Debug, Default)]
723pub struct SelectAll;
724
725impl TableOperation for SelectAll {
726 fn new() -> Self
727 where
728 Self: Sized,
729 {
730 Self
731 }
732 fn name(&self) -> Cow<'_, str> {
733 Cow::Borrowed("Select all")
734 }
735 fn icon(&self) -> &'static str {
736 "✔"
737 }
738 fn enabled(&self) -> TableOperationEnablement {
739 TableOperationEnablement::Always
740 }
741 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
742 ctx.data.selected_rows.clear();
743 ctx.data
744 .selected_rows
745 .insert_range(0..ctx.provider.row_count() as u32);
746 Ok(())
747 }
748 fn just_completed(&mut self) -> bool {
749 true
750 }
751}
752
753#[derive(Debug, Default)]
754pub struct DeSelectAll;
755
756impl TableOperation for DeSelectAll {
757 fn new() -> Self
758 where
759 Self: Sized,
760 {
761 Self
762 }
763 fn name(&self) -> Cow<'_, str> {
764 Cow::Borrowed("Deselect all")
765 }
766 fn icon(&self) -> &'static str {
767 "❌"
768 }
769 fn enabled(&self) -> TableOperationEnablement {
770 TableOperationEnablement::Always
771 }
772 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
773 ctx.data.selected_rows.clear();
774 Ok(())
775 }
776 fn just_completed(&mut self) -> bool {
777 true
778 }
779}