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