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