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 gui(
273 &mut self,
274 ui: &mut egui::Ui,
275 provider: &dyn TableProvider,
276 data: &mut TableState,
277 context_menu: bool,
278 ) -> Result<bool, TableError> {
279 self.gui_custom(
280 ui,
281 provider,
282 data,
283 context_menu,
284 |ui, op, enabled, reason, context_menu| {
285 ui.add_enabled_ui(enabled, |ui| {
286 let mut button = ui
287 .button(op.get_name(context_menu).as_ref())
288 .on_hover_text(op.name());
289 if !enabled {
290 button = button.on_disabled_hover_text(format!("{}\n{reason}", op.name()));
291 }
292 button
293 })
294 .inner
295 },
296 )
297 }
298
299 pub fn gui_custom<F>(
304 &mut self,
305 ui: &mut egui::Ui,
306 provider: &dyn TableProvider,
307 data: &mut TableState,
308 context_menu: bool,
309 mut button_renderer: F,
310 ) -> Result<bool, TableError>
311 where
312 F: FnMut(
313 &mut egui::Ui,
314 &mut Box<dyn TableOperation>,
315 bool, &str, bool, ) -> egui::Response,
319 {
320 let mut refresh = false;
321 let mut any_clicked = false;
322 let num_groups = self.groups.len();
323
324 let current_tick = ui.ctx().cumulative_frame_nr();
326 if self.last_tick != current_tick {
327 self.last_tick = current_tick;
328
329 for (g_idx, op_group) in self.groups.iter_mut().enumerate() {
330 for (op_idx, op) in op_group.iter_mut().enumerate() {
331 let key = (g_idx, op_idx);
332 let pending = op.is_pending();
333 let was_pending = self.pending_tracker.contains(&key);
334
335 if was_pending && !pending {
336 self.pending_tracker.remove(&key);
337 let success = op.error().is_none();
338 op.on_completed(success);
339 if op.refresh_on_completion() {
340 refresh = true;
341 }
342 } else if !was_pending && pending {
343 self.pending_tracker.insert(key);
344 }
345 }
346 }
347 }
348
349 for (g_idx, op_group) in self.groups.iter_mut().enumerate() {
351 for op in op_group {
352 let is_pending = op.is_pending();
353
354 if op.pollable() {
355 op.poll(ui, data)?;
356 }
357 let (enabled, reason) = if is_pending {
358 (false, t!("operation-pending"))
359 } else {
360 op.evaluate_enablement(data)
361 };
362 if !context_menu {
363 op.extra_ui(ui, data)?;
364 }
365 let response = button_renderer(ui, op, enabled, reason.as_ref(), context_menu);
366 if response.clicked() {
367 any_clicked = true;
368 let mut ctx = OperationContext { ui, data, provider };
369 op.exec(&mut ctx)?;
370 }
371 }
372 if !context_menu && g_idx + 1 < num_groups {
374 ui.separator();
375 }
376 }
377 if any_clicked && context_menu {
378 ui.close_kind(egui::UiKind::Menu);
379 }
380 Ok(refresh)
381 }
382}
383
384#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
385pub enum TableOperationEnablement {
386 #[default]
387 Always,
388 AtLeastOneFiltered,
389 AtLeastOneSelected,
390 OneSelected,
391}
392
393pub trait TableOperation: std::any::Any + std::fmt::Debug + Send + Sync {
394 fn name(&self) -> Cow<'_, str>;
395 fn icon(&self) -> &'static str {
396 "X"
397 }
398 fn get_name(&self, full: bool) -> Cow<'_, str> {
399 if full {
400 Cow::Owned(format!("{} {}", self.name(), self.icon()))
401 } else {
402 Cow::Borrowed(self.icon())
403 }
404 }
405 fn refresh_on_completion(&self) -> bool {
406 false
407 }
408 fn pollable(&self) -> bool {
409 false
410 }
411 fn is_first_page(&self) -> bool {
412 true
413 }
414 fn is_last_page(&self) -> bool {
415 true
416 }
417 fn enabled(&self) -> TableOperationEnablement;
418 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError>;
419 fn extra_ui(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> {
420 Ok(())
421 }
422 fn is_pending(&mut self) -> bool {
423 false
424 }
425
426 fn on_completed(&mut self, _success: bool) {}
428
429 fn poll(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> {
431 Ok(())
432 }
433 fn consume(&mut self) -> Result<(), TableError> {
434 Ok(())
435 }
436 fn error(&self) -> Option<&str> {
437 None
438 }
439 fn clear_error(&mut self) {}
440 fn is_modal_open(&self) -> bool {
441 false
442 }
443 fn set_modal_open(&mut self, _open: bool) {}
444 fn reset(&mut self) {}
445
446 fn pollable_modal(
447 &mut self,
448 ui: &mut egui::Ui,
449 centered: bool,
450 action: Cow<'_, str>,
451 action_progressive: Cow<'_, str>,
452 input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
453 ) -> Result<(), TableError>
454 where
455 Self: Sized,
456 {
457 if self.is_modal_open() {
458 egui::Modal::new(ui.id().with("pollable_modal"))
459 .show(ui.ctx(), |ui| {
460 ui.scope_builder(
461 egui::UiBuilder::new().layout(egui::Layout::top_down(if centered {
462 egui::Align::Center
463 } else {
464 egui::Align::Min
465 })),
466 |ui| {
467 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
468 ui.heading(
469 egui::RichText::new(format!("{} {}", self.name(), self.icon()))
470 .strong(),
471 );
472 ui.separator();
473 ui.spacing_mut().item_spacing.y = 5.0;
474
475 let is_pending = self.is_pending();
476 ui.add_enabled_ui(!is_pending, |ui| input_ui(ui, self))
477 .inner?;
478 ui.add_space(10.0);
479
480 if let Some(error) = self.error() {
481 ui.colored_label(egui::Color32::RED, t!("error"));
482 ui.colored_label(egui::Color32::RED, error);
483 }
484
485 if is_pending {
486 ui.label(action_progressive);
487 ui.add_space(5.0);
488 ui.spinner();
489 } else {
490 if self.is_last_page() {
491 let is_allowed = self.poll_allow_execution();
492 if ui
493 .add_enabled(is_allowed, egui::Button::new(action))
494 .clicked()
495 {
496 self.clear_error();
497 self.consume()?;
498 }
499 }
500 if self.is_first_page() && ui.button(t!("cancel")).clicked() {
501 self.reset();
502 }
503 }
504 Ok(())
505 },
506 )
507 .inner
508 })
509 .inner
510 } else {
511 Ok(())
512 }
513 }
514
515 fn polled_modal(
516 &mut self,
517 ui: &mut egui::Ui,
518 heading: Cow<'_, str>,
519 action_progressive: Cow<'_, str>,
520 input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
521 ) -> Result<(), TableError>
522 where
523 Self: Sized,
524 {
525 if self.is_modal_open() {
526 egui::Modal::new(ui.id().with("polled_modal"))
527 .show(ui.ctx(), |ui| {
528 ui.vertical_centered(|ui| {
529 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
530 ui.heading(heading);
531 ui.separator();
532 ui.spacing_mut().item_spacing.y = 5.0;
533
534 if self.is_pending() {
535 ui.label(action_progressive);
536 ui.add_space(5.0);
537 ui.spinner();
538 } else if let Some(error) = self.error() {
539 ui.colored_label(egui::Color32::RED, t!("error"));
540 ui.colored_label(egui::Color32::RED, error);
541 } else {
542 input_ui(ui, self)?;
543 }
544
545 ui.add_space(10.0);
546 if ui.button(t!("close")).clicked() {
547 self.reset();
548 }
549 Ok::<_, TableError>(())
550 })
551 })
552 .inner
553 .inner?;
554 }
555 Ok(())
556 }
557
558 fn poll_allow_execution(&self) -> bool {
559 true
560 }
561
562 fn evaluate_enablement(&self, state: &TableState) -> (bool, Cow<'static, str>) {
565 match self.enabled() {
566 TableOperationEnablement::Always => (true, Cow::Borrowed("")),
567 TableOperationEnablement::AtLeastOneSelected => (
568 !state.selected_rows.is_empty(),
569 t!("operation-at-least-one"),
570 ),
571 TableOperationEnablement::OneSelected => {
572 (state.selected_rows.len() == 1, t!("operation-one"))
573 }
574 TableOperationEnablement::AtLeastOneFiltered => (
575 !state.active_rows.is_empty(),
576 t!("operation-at-least-one-filtered"),
577 ),
578 }
579 }
580}
581
582#[derive(Debug, Default)]
585pub struct CopyRows {
586 pub prioritize_hovers: bool,
587}
588
589impl TableOperation for CopyRows {
590 fn name(&self) -> Cow<'_, str> {
591 if self.prioritize_hovers {
592 t!("copy-hovered-rows")
593 } else {
594 t!("copy-rows")
595 }
596 }
597 fn icon(&self) -> &'static str {
598 if self.prioritize_hovers {
599 "📁"
600 } else {
601 "📋"
602 }
603 }
604 fn enabled(&self) -> TableOperationEnablement {
605 TableOperationEnablement::AtLeastOneSelected
606 }
607 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
608 let mut output = String::with_capacity(2048);
610
611 ctx.provider.for_selected_rows(ctx.data, &mut |row| {
612 if !output.is_empty() {
613 output.push('\n');
614 }
615 for (i, (val, hover)) in row.iter().enumerate() {
616 if i > 0 {
617 output.push(',');
618 }
619 let cell_text = if self.prioritize_hovers {
620 hover.as_deref().unwrap_or(val)
621 } else {
622 val
623 };
624 output.push_str(cell_text);
625 }
626 Ok(())
627 })?;
628
629 ctx.ui.ctx().copy_text(output);
630 Ok(())
631 }
632}
633
634#[derive(Debug, Default)]
635pub struct CopyHeadersRows {
636 pub prioritize_hovers: bool,
637}
638
639impl TableOperation for CopyHeadersRows {
640 fn name(&self) -> Cow<'_, str> {
641 if self.prioritize_hovers {
642 t!("copy-hovered-rows-with-headers")
643 } else {
644 t!("copy-rows-with-headers")
645 }
646 }
647 fn icon(&self) -> &'static str {
648 if self.prioritize_hovers {
649 "🗄"
650 } else {
651 "📜"
652 }
653 }
654 fn enabled(&self) -> TableOperationEnablement {
655 TableOperationEnablement::AtLeastOneSelected
656 }
657
658 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
659 let headers = ctx.provider.headers();
660
661 let mut output = String::with_capacity(2048);
663
664 for (i, header) in headers.iter().enumerate() {
666 if i > 0 {
667 output.push(',');
668 }
669 output.push_str(header);
670 }
671
672 ctx.provider.for_selected_rows(ctx.data, &mut |row| {
674 output.push('\n');
675 for (i, (val, hover)) in row.iter().enumerate() {
676 if i > 0 {
677 output.push(',');
678 }
679 let cell_text = if self.prioritize_hovers {
680 hover.as_deref().unwrap_or(val)
681 } else {
682 val
683 };
684 output.push_str(cell_text);
685 }
686 Ok(())
687 })?;
688
689 ctx.ui.ctx().copy_text(output);
691 Ok(())
692 }
693}
694
695#[derive(Debug, Default)]
696pub struct FilterSelectAll;
697
698impl TableOperation for FilterSelectAll {
699 fn name(&self) -> Cow<'_, str> {
700 t!("select-filtered")
701 }
702 fn icon(&self) -> &'static str {
703 "☑"
704 }
705 fn enabled(&self) -> TableOperationEnablement {
706 TableOperationEnablement::Always
707 }
708 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
709 let active_u32_iter = ctx.data.active_rows.iter().map(|&row| row as u32);
710 ctx.data.selected_rows.extend(active_u32_iter);
711 Ok(())
712 }
713}
714
715#[derive(Debug, Default)]
716pub struct FilterDeSelectAll;
717
718impl TableOperation for FilterDeSelectAll {
719 fn name(&self) -> Cow<'_, str> {
720 t!("deselect-filtered")
721 }
722 fn icon(&self) -> &'static str {
723 "❎"
724 }
725 fn enabled(&self) -> TableOperationEnablement {
726 TableOperationEnablement::Always
727 }
728 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
729 ctx.data.active_rows.iter().for_each(|row| {
730 ctx.data.selected_rows.remove(*row as u32);
731 });
732 Ok(())
733 }
734}
735
736#[derive(Debug, Default)]
737pub struct SelectAll;
738
739impl TableOperation for SelectAll {
740 fn name(&self) -> Cow<'_, str> {
741 t!("select-all")
742 }
743 fn icon(&self) -> &'static str {
744 "✔"
745 }
746 fn enabled(&self) -> TableOperationEnablement {
747 TableOperationEnablement::Always
748 }
749 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
750 ctx.data.selected_rows.clear();
751 ctx.data
752 .selected_rows
753 .insert_range(0..ctx.provider.row_count() as u32);
754 Ok(())
755 }
756}
757
758#[derive(Debug, Default)]
759pub struct DeSelectAll;
760
761impl TableOperation for DeSelectAll {
762 fn name(&self) -> Cow<'_, str> {
763 t!("deselect-all")
764 }
765 fn icon(&self) -> &'static str {
766 "❌"
767 }
768 fn enabled(&self) -> TableOperationEnablement {
769 TableOperationEnablement::Always
770 }
771 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
772 ctx.data.selected_rows.clear();
773 Ok(())
774 }
775}