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 reset(&mut self) {}
401
402 fn pollable_modal(
403 &mut self,
404 ui: &mut egui::Ui,
405 centered: bool,
406 action: Cow<'_, str>,
407 action_progressive: Cow<'_, str>,
408 input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
409 ) -> Result<(), TableError>
410 where
411 Self: Sized,
412 {
413 if self.is_modal_open() {
414 egui::Modal::new(ui.id().with("pollable_modal"))
415 .show(ui.ctx(), |ui| {
416 ui.scope_builder(
417 egui::UiBuilder::new().layout(egui::Layout::top_down(if centered {
418 egui::Align::Center
419 } else {
420 egui::Align::Min
421 })),
422 |ui| {
423 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
424 ui.heading(
425 egui::RichText::new(format!("{} {}", self.name(), self.icon()))
426 .strong(),
427 );
428 ui.separator();
429 ui.spacing_mut().item_spacing.y = 5.0;
430
431 if self.just_completed() && self.error().is_none() {
432 self.reset();
433 return Ok(());
434 }
435
436 let is_pending = self.is_pending();
437 ui.add_enabled_ui(!is_pending, |ui| input_ui(ui, self))
438 .inner?;
439 ui.add_space(10.0);
440
441 if let Some(error) = self.error() {
442 ui.colored_label(egui::Color32::RED, t!("error"));
443 ui.colored_label(egui::Color32::RED, error);
444 }
445
446 if is_pending {
447 ui.label(action_progressive);
448 ui.add_space(5.0);
449 ui.spinner();
450 } else {
451 if self.is_last_page() {
452 let is_allowed = self.poll_allow_execution();
453 if ui
454 .add_enabled(is_allowed, egui::Button::new(action))
455 .clicked()
456 {
457 self.clear_error();
458 self.consume()?;
459 }
460 }
461 if self.is_first_page() && ui.button(t!("cancel")).clicked() {
462 self.reset();
463 }
464 }
465 Ok(())
466 },
467 )
468 .inner
469 })
470 .inner
471 } else {
472 Ok(())
473 }
474 }
475
476 fn polled_modal(
477 &mut self,
478 ui: &mut egui::Ui,
479 heading: Cow<'_, str>,
480 action_progressive: Cow<'_, str>,
481 input_ui: impl FnOnce(&mut egui::Ui, &mut Self) -> Result<(), TableError>,
482 ) -> Result<(), TableError>
483 where
484 Self: Sized,
485 {
486 if self.is_modal_open() {
487 egui::Modal::new(ui.id().with("polled_modal"))
488 .show(ui.ctx(), |ui| {
489 ui.vertical_centered(|ui| {
490 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
491 ui.heading(heading);
492 ui.separator();
493 ui.spacing_mut().item_spacing.y = 5.0;
494
495 if self.is_pending() {
496 ui.label(action_progressive);
497 ui.add_space(5.0);
498 ui.spinner();
499 } else if let Some(error) = self.error() {
500 ui.colored_label(egui::Color32::RED, t!("error"));
501 ui.colored_label(egui::Color32::RED, error);
502 } else {
503 input_ui(ui, self)?;
504 }
505
506 ui.add_space(10.0);
507 if ui.button(t!("close")).clicked() {
508 self.reset();
509 }
510 Ok::<_, TableError>(())
511 })
512 })
513 .inner
514 .inner?;
515 }
516 Ok(())
517 }
518
519 fn poll_allow_execution(&self) -> bool {
520 true
521 }
522}
523
524#[derive(Debug, Default)]
527pub struct CopyRows {
528 pub prioritize_hovers: bool,
529}
530
531impl TableOperation for CopyRows {
532 fn name(&self) -> Cow<'_, str> {
533 if self.prioritize_hovers {
534 t!("copy-hovered-rows")
535 } else {
536 t!("copy-rows")
537 }
538 }
539 fn icon(&self) -> &'static str {
540 if self.prioritize_hovers {
541 "📁"
542 } else {
543 "📋"
544 }
545 }
546 fn enabled(&self) -> TableOperationEnablement {
547 TableOperationEnablement::AtLeastOneSelected
548 }
549 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
550 let mut output = String::with_capacity(2048);
552
553 ctx.provider.for_selected_rows(ctx.data, &mut |row| {
554 if !output.is_empty() {
555 output.push('\n');
556 }
557 for (i, (val, hover)) in row.iter().enumerate() {
558 if i > 0 {
559 output.push(',');
560 }
561 let cell_text = if self.prioritize_hovers {
562 hover.as_deref().unwrap_or(val)
563 } else {
564 val
565 };
566 output.push_str(cell_text);
567 }
568 Ok(())
569 })?;
570
571 ctx.ui.ctx().copy_text(output);
572 Ok(())
573 }
574 fn just_completed(&mut self) -> bool {
575 true
576 }
577}
578
579#[derive(Debug, Default)]
580pub struct CopyHeadersRows {
581 pub prioritize_hovers: bool,
582}
583
584impl TableOperation for CopyHeadersRows {
585 fn name(&self) -> Cow<'_, str> {
586 if self.prioritize_hovers {
587 t!("copy-hovered-rows-with-headers")
588 } else {
589 t!("copy-rows-with-headers")
590 }
591 }
592 fn icon(&self) -> &'static str {
593 if self.prioritize_hovers {
594 "🗄"
595 } else {
596 "📜"
597 }
598 }
599 fn enabled(&self) -> TableOperationEnablement {
600 TableOperationEnablement::AtLeastOneSelected
601 }
602
603 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
604 let headers = ctx.provider.headers();
605
606 let mut output = String::with_capacity(2048);
608
609 for (i, header) in headers.iter().enumerate() {
611 if i > 0 {
612 output.push(',');
613 }
614 output.push_str(header);
615 }
616
617 ctx.provider.for_selected_rows(ctx.data, &mut |row| {
619 output.push('\n');
620 for (i, (val, hover)) in row.iter().enumerate() {
621 if i > 0 {
622 output.push(',');
623 }
624 let cell_text = if self.prioritize_hovers {
625 hover.as_deref().unwrap_or(val)
626 } else {
627 val
628 };
629 output.push_str(cell_text);
630 }
631 Ok(())
632 })?;
633
634 ctx.ui.ctx().copy_text(output);
636 Ok(())
637 }
638
639 fn just_completed(&mut self) -> bool {
640 true
641 }
642}
643
644#[derive(Debug, Default)]
645pub struct FilterSelectAll;
646
647impl TableOperation for FilterSelectAll {
648 fn name(&self) -> Cow<'_, str> {
649 t!("select-filtered")
650 }
651 fn icon(&self) -> &'static str {
652 "☑"
653 }
654 fn enabled(&self) -> TableOperationEnablement {
655 TableOperationEnablement::Always
656 }
657 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
658 let active_u32_iter = ctx.data.active_rows.iter().map(|&row| row as u32);
659 ctx.data.selected_rows.extend(active_u32_iter);
660 Ok(())
661 }
662 fn just_completed(&mut self) -> bool {
663 true
664 }
665}
666
667#[derive(Debug, Default)]
668pub struct FilterDeSelectAll;
669
670impl TableOperation for FilterDeSelectAll {
671 fn name(&self) -> Cow<'_, str> {
672 t!("deselect-filtered")
673 }
674 fn icon(&self) -> &'static str {
675 "❎"
676 }
677 fn enabled(&self) -> TableOperationEnablement {
678 TableOperationEnablement::Always
679 }
680 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
681 ctx.data.active_rows.iter().for_each(|row| {
682 ctx.data.selected_rows.remove(*row as u32);
683 });
684 Ok(())
685 }
686 fn just_completed(&mut self) -> bool {
687 true
688 }
689}
690
691#[derive(Debug, Default)]
692pub struct SelectAll;
693
694impl TableOperation for SelectAll {
695 fn name(&self) -> Cow<'_, str> {
696 t!("select-all")
697 }
698 fn icon(&self) -> &'static str {
699 "✔"
700 }
701 fn enabled(&self) -> TableOperationEnablement {
702 TableOperationEnablement::Always
703 }
704 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
705 ctx.data.selected_rows.clear();
706 ctx.data
707 .selected_rows
708 .insert_range(0..ctx.provider.row_count() as u32);
709 Ok(())
710 }
711 fn just_completed(&mut self) -> bool {
712 true
713 }
714}
715
716#[derive(Debug, Default)]
717pub struct DeSelectAll;
718
719impl TableOperation for DeSelectAll {
720 fn name(&self) -> Cow<'_, str> {
721 t!("deselect-all")
722 }
723 fn icon(&self) -> &'static str {
724 "❌"
725 }
726 fn enabled(&self) -> TableOperationEnablement {
727 TableOperationEnablement::Always
728 }
729 fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError> {
730 ctx.data.selected_rows.clear();
731 Ok(())
732 }
733 fn just_completed(&mut self) -> bool {
734 true
735 }
736}