1use std::mem::{replace, take};
2
3use egui::{
4 Align, Color32, Event, Layout, PointerButton, Rect, Response, RichText, Sense, Stroke,
5 StrokeKind, Widget,
6};
7use egui_extras::Column;
8use tap::prelude::{Pipe, Tap};
9
10use crate::{
11 viewer::{EmptyRowCreateContext, RowViewer},
12 DataTable, UiAction,
13};
14
15use self::state::*;
16
17use format as f;
18
19pub(crate) mod state;
20mod tsv;
21
22#[derive(Default, Debug, Clone, Copy)]
27#[non_exhaustive]
28pub struct Style {
29 pub bg_selected_cell: Option<egui::Color32>,
31
32 pub bg_selected_highlight_cell: Option<egui::Color32>,
34
35 pub fg_selected_highlight_cell: Option<egui::Color32>,
37
38 pub fg_drag_selection: Option<egui::Color32>,
40
41 pub max_undo_history: usize,
46
47 pub table_row_height: Option<f32>,
49
50 pub single_click_edit_mode: bool,
53
54 pub cell_align: egui::Align,
56
57 pub focused_row_stroke: Option<egui::Color32>,
60}
61
62pub struct Renderer<'a, R, V: RowViewer<R>> {
65 table: &'a mut DataTable<R>,
66 viewer: &'a mut V,
67 state: Option<Box<UiState<R>>>,
68 style: Style,
69}
70
71impl<R, V: RowViewer<R>> egui::Widget for Renderer<'_, R, V> {
72 fn ui(self, ui: &mut egui::Ui) -> Response {
73 self.show(ui)
74 }
75}
76
77impl<'a, R, V: RowViewer<R>> Renderer<'a, R, V> {
78 pub fn new(table: &'a mut DataTable<R>, viewer: &'a mut V) -> Self {
79 if table.rows.is_empty() {
80 table.push(viewer.new_empty_row_for(EmptyRowCreateContext::InsertNewLine));
81 }
82
83 Self {
84 state: Some(table.ui.take().unwrap_or_default().tap_mut(|state| {
85 state.validate_identity(viewer);
86 })),
87 table,
88 viewer,
89 style: Default::default(),
90 }
91 }
92
93 pub fn with_style(mut self, style: Style) -> Self {
94 self.style = style;
95 self
96 }
97
98 pub fn with_style_modify(mut self, f: impl FnOnce(&mut Style)) -> Self {
99 f(&mut self.style);
100 self
101 }
102
103 pub fn with_table_row_height(mut self, height: f32) -> Self {
104 self.style.table_row_height = Some(height);
105 self
106 }
107
108 pub fn with_max_undo_history(mut self, max_undo_history: usize) -> Self {
109 self.style.max_undo_history = max_undo_history;
110 self
111 }
112
113 pub fn show(self, ui: &mut egui::Ui) -> Response {
114 egui::ScrollArea::horizontal()
115 .show(ui, |ui| self.impl_show(ui))
116 .inner
117 }
118
119 fn impl_show(mut self, ui: &mut egui::Ui) -> Response {
120 let ctx = &ui.ctx().clone();
121 let ui_id = ui.id();
122 let style = ui.style().clone();
123 let painter = ui.painter().clone();
124 let visual = &style.visuals;
125 let viewer = &mut *self.viewer;
126 let s = self.state.as_mut().unwrap();
127 let mut resp_total = None::<Response>;
128 let mut resp_ret = None::<Response>;
129 let mut commands = Vec::<Command<R>>::new();
130 let ui_layer_id = ui.layer_id();
131
132 let green = if visual.window_fill.g() > 128 {
136 Color32::DARK_GREEN
137 } else {
138 Color32::GREEN
139 };
140
141 let mut builder = egui_extras::TableBuilder::new(ui).column(Column::auto());
142
143 let iter_vis_cols_with_flag = s
144 .vis_cols()
145 .iter()
146 .enumerate()
147 .map(|(index, column)| (column, index + 1 == s.vis_cols().len()));
148
149 for (column, flag) in iter_vis_cols_with_flag {
150 builder = builder.column(viewer.column_render_config(column.0, flag));
151 }
152
153 if replace(&mut s.cci_want_move_scroll, false) {
154 let interact_row = s.interactive_cell().0;
155 builder = builder.scroll_to_row(interact_row.0, None);
156 }
157
158 builder
159 .columns(Column::auto(), s.num_columns() - s.vis_cols().len())
160 .drag_to_scroll(false) .striped(true)
162 .cell_layout(egui::Layout::default().with_cross_align(self.style.cell_align))
163 .max_scroll_height(f32::MAX)
164 .sense(Sense::click_and_drag().tap_mut(|s| s.set(Sense::FOCUSABLE, true)))
165 .header(20., |mut h| {
166 h.col(|_ui| {
167 });
169
170 let has_any_hidden_col = s.vis_cols().len() != s.num_columns();
171
172 for (vis_col, &col) in s.vis_cols().iter().enumerate() {
173 let vis_col = VisColumnPos(vis_col);
174 let mut painter = None;
175 let (col_rect, resp) = h.col(|ui| {
176 ui.horizontal_centered(|ui| {
177 if let Some(pos) = s.sort().iter().position(|(c, ..)| c == &col) {
178 let is_asc = s.sort()[pos].1 .0 as usize;
179
180 ui.colored_label(
181 [green, Color32::RED][is_asc],
182 RichText::new(format!("{}{}", ["↘", "↗"][is_asc], pos + 1,))
183 .monospace(),
184 );
185 } else {
186 ui.monospace(" ");
187 }
188
189 egui::Label::new(viewer.column_name(col.0))
190 .selectable(false)
191 .ui(ui);
192 });
193
194 painter = Some(ui.painter().clone());
195 });
196
197 resp.dnd_set_drag_payload(vis_col);
199
200 if resp.dragged() {
201 egui::popup::show_tooltip_text(
202 ctx,
203 ui_layer_id,
204 "_EGUI_DATATABLE__COLUMN_MOVE__".into(),
205 viewer.column_name(col.0),
206 );
207 }
208
209 if resp.hovered() && viewer.is_sortable_column(col.0) {
210 if let Some(p) = &painter {
211 p.rect_filled(
212 col_rect,
213 egui::CornerRadius::ZERO,
214 visual.selection.bg_fill.gamma_multiply(0.2),
215 );
216 }
217 }
218
219 if viewer.is_sortable_column(col.0) && resp.clicked_by(PointerButton::Primary) {
220 let mut sort = s.sort().to_owned();
221 match sort.iter_mut().find(|(c, ..)| c == &col) {
222 Some((_, asc)) => match asc.0 {
223 true => asc.0 = false,
224 false => sort.retain(|(c, ..)| c != &col),
225 },
226 None => {
227 sort.push((col, IsAscending(true)));
228 }
229 }
230
231 commands.push(Command::SetColumnSort(sort));
232 }
233
234 if resp.dnd_hover_payload::<VisColumnPos>().is_some() {
235 if let Some(p) = &painter {
236 p.rect_filled(
237 col_rect,
238 egui::CornerRadius::ZERO,
239 visual.selection.bg_fill.gamma_multiply(0.5),
240 );
241 }
242 }
243
244 if let Some(payload) = resp.dnd_release_payload::<VisColumnPos>() {
245 commands.push(Command::CcReorderColumn {
246 from: *payload,
247 to: vis_col
248 .0
249 .pipe(|v| v + (payload.0 < v) as usize)
250 .pipe(VisColumnPos),
251 })
252 }
253
254 resp.context_menu(|ui| {
255 if ui.button("Hide").clicked() {
256 commands.push(Command::CcHideColumn(col));
257 ui.close_menu();
258 }
259
260 if !s.sort().is_empty() && ui.button("Clear Sort").clicked() {
261 commands.push(Command::SetColumnSort(Vec::new()));
262 ui.close_menu();
263 }
264
265 if has_any_hidden_col {
266 ui.separator();
267 ui.label("Hidden");
268
269 for col in (0..s.num_columns()).map(ColumnIdx) {
270 if !s.vis_cols().contains(&col)
271 && ui.button(viewer.column_name(col.0)).clicked()
272 {
273 commands.push(Command::CcShowColumn {
274 what: col,
275 at: vis_col,
276 });
277 ui.close_menu();
278 }
279 }
280 }
281 });
282 }
283
284 resp_total = Some(h.response());
286 })
287 .tap_mut(|table| {
288 table.ui_mut().separator();
289 })
290 .body(|body: egui_extras::TableBody<'_>| {
291 resp_ret = Some(
292 self.impl_show_body(body, painter, commands, ctx, &style, ui_id, resp_total),
293 );
294 });
295
296 resp_ret.unwrap_or_else(|| ui.label("??"))
297 }
298
299 #[allow(clippy::too_many_arguments)]
300 fn impl_show_body(
301 &mut self,
302 body: egui_extras::TableBody<'_>,
303 mut _painter: egui::Painter,
304 mut commands: Vec<Command<R>>,
305 ctx: &egui::Context,
306 style: &egui::Style,
307 ui_id: egui::Id,
308 mut resp_total: Option<Response>,
309 ) -> Response {
310 let viewer = &mut *self.viewer;
311 let s = self.state.as_mut().unwrap();
312 let table = &mut *self.table;
313 let visual = &style.visuals;
314 let visible_cols = s.vis_cols().clone();
315 let no_rounding = egui::CornerRadius::ZERO;
316
317 let mut actions = Vec::<UiAction>::new();
318 let mut edit_started = false;
319 let hotkeys = viewer.hotkeys(&s.ui_action_context());
320
321 'detect_hotkey: {
323 if !s.cci_has_focus {
326 break 'detect_hotkey;
327 }
328
329 if !s.is_editing() {
330 ctx.input_mut(|i| {
331 i.events.retain(|x| {
332 match x {
333 Event::Copy => actions.push(UiAction::CopySelection),
334 Event::Cut => actions.push(UiAction::CutSelection),
335
336 Event::Paste(clipboard) => {
339 if !clipboard.is_empty() {
340 s.try_update_clipboard_from_string(viewer, clipboard);
344 }
345
346 if i.modifiers.shift {
347 actions.push(UiAction::PasteInsert)
348 } else {
349 actions.push(UiAction::PasteInPlace)
350 }
351 }
352
353 _ => return true,
354 }
355 false
356 })
357 });
358 }
359
360 for (hotkey, action) in &hotkeys {
361 ctx.input_mut(|inp| {
362 if inp.consume_shortcut(hotkey) {
363 actions.push(*action);
364 }
365 })
366 }
367 }
368
369 #[cfg(feature = "persistency")]
371 if viewer.persist_ui_state() {
372 s.validate_persistency(ctx, ui_id, viewer);
373 }
374
375 s.validate_cc(&mut table.rows, viewer);
378
379 let cc_row_heights = take(&mut s.cc_row_heights);
383
384 let mut row_height_updates = Vec::new();
385 let vis_row_digits = s.cc_rows.len().max(1).ilog10();
386 let row_id_digits = table.len().max(1).ilog10();
387
388 let body_max_rect = body.max_rect();
389 let has_any_sort = !s.sort().is_empty();
390
391 let pointer_interact_pos = ctx.input(|i| i.pointer.latest_pos().unwrap_or_default());
392 let pointer_primary_down = ctx.input(|i| i.pointer.button_down(PointerButton::Primary));
393
394 s.cci_page_row_count = 0;
395
396 let render_fn = |mut row: egui_extras::TableRow| {
401 s.cci_page_row_count += 1;
402
403 let vis_row = VisRowPos(row.index());
404 let row_id = s.cc_rows[vis_row.0];
405 let prev_row_height = cc_row_heights[vis_row.0];
406
407 let mut row_elem_start = Default::default();
408
409 let edit_state = s.row_editing_cell(row_id);
411 let mut editing_cell_rect = Rect::NOTHING;
412 let interactive_row = s.is_interactive_row(vis_row);
413
414 let check_mouse_dragging_selection = {
415 let s_cci_has_focus = s.cci_has_focus;
416 let s_cci_has_selection = s.has_cci_selection();
417
418 move |rect: &Rect, resp: &egui::Response| {
419 let cci_hovered: bool = s_cci_has_focus
420 && s_cci_has_selection
421 && rect
422 .with_max_x(resp.rect.right())
423 .contains(pointer_interact_pos);
424 let sel_drag = cci_hovered && pointer_primary_down;
425 let sel_click = !s_cci_has_selection && resp.hovered() && pointer_primary_down;
426
427 sel_drag || sel_click
428 }
429 };
430
431 row.set_selected(edit_state.is_some());
435
436 let (head_rect, head_resp) = row.col(|ui| {
438 row_elem_start = ui.max_rect().right_top();
440
441 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
442 ui.separator();
443
444 if has_any_sort {
445 ui.monospace(
446 RichText::from(f!(
447 "{:·>width$}",
448 row_id.0,
449 width = row_id_digits as usize
450 ))
451 .strong(),
452 );
453 } else {
454 ui.monospace(
455 RichText::from(f!("{:>width$}", "", width = row_id_digits as usize))
456 .strong(),
457 );
458 }
459
460 ui.monospace(
461 RichText::from(f!(
462 "{:·>width$}",
463 vis_row.0 + 1,
464 width = vis_row_digits as usize
465 ))
466 .weak(),
467 );
468 });
469 });
470
471 if check_mouse_dragging_selection(&head_rect, &head_resp) {
472 s.cci_sel_update_row(vis_row);
473 }
474
475 let mut new_maximum_height = 0.;
479
480 for (vis_col, col) in visible_cols.iter().enumerate() {
482 let vis_col = VisColumnPos(vis_col);
483 let linear_index = vis_row.linear_index(visible_cols.len(), vis_col);
484 let selected = s.is_selected(vis_row, vis_col);
485 let cci_selected = s.is_selected_cci(vis_row, vis_col);
486 let is_editing = edit_state.is_some();
487 let is_interactive_cell = interactive_row.is_some_and(|x| x == vis_col);
488 let mut response_consumed = s.is_editing();
489
490 let (rect, resp) = row.col(|ui| {
491 let ui_max_rect = ui.max_rect();
492
493 if cci_selected {
494 ui.painter().rect_stroke(
495 ui_max_rect,
496 no_rounding,
497 Stroke {
498 width: 2.,
499 color: self
500 .style
501 .fg_drag_selection
502 .unwrap_or(visual.selection.bg_fill),
503 },
504 StrokeKind::Inside,
505 );
506 }
507
508 if is_interactive_cell {
509 ui.painter().rect_filled(
510 ui_max_rect.expand(2.),
511 no_rounding,
512 self.style
513 .bg_selected_highlight_cell
514 .unwrap_or(visual.selection.bg_fill),
515 );
516 } else if selected {
517 ui.painter().rect_filled(
518 ui_max_rect.expand(1.),
519 no_rounding,
520 self.style
521 .bg_selected_cell
522 .unwrap_or(visual.selection.bg_fill.gamma_multiply(0.5)),
523 );
524 }
525
526 ui.style_mut()
530 .visuals
531 .widgets
532 .noninteractive
533 .fg_stroke
534 .color = if is_interactive_cell {
535 self.style
536 .fg_selected_highlight_cell
537 .unwrap_or(visual.strong_text_color())
538 } else {
539 visual.strong_text_color()
540 };
541
542 ui.add_enabled_ui(false, |ui| {
547 viewer.show_cell_view(ui, &table.rows[row_id.0], col.0);
548 });
549
550 #[cfg(any())]
551 if selected {
552 ui.painter().rect_stroke(
553 ui_max_rect,
554 no_rounding,
555 Stroke {
556 width: 1.,
557 color: visual.weak_text_color(),
558 },
559 );
560 }
561
562 if interactive_row.is_some() && !is_editing {
563 let st = Stroke {
564 width: 1.,
565 color: self
566 .style
567 .focused_row_stroke
568 .unwrap_or(visual.warn_fg_color.gamma_multiply(0.5)),
569 };
570
571 let xr = ui_max_rect.x_range();
572 let yr = ui_max_rect.y_range();
573 ui.painter().hline(xr, yr.min, st);
574 ui.painter().hline(xr, yr.max, st);
575 }
576
577 if edit_state.is_some_and(|(_, vis)| vis == vis_col) {
578 editing_cell_rect = ui_max_rect;
579 }
580 });
581
582 new_maximum_height = rect.height().max(new_maximum_height);
583
584 if check_mouse_dragging_selection(&rect, &resp) {
586 response_consumed = true;
588 s.cci_sel_update(linear_index);
589 }
590
591 if resp.clicked_by(PointerButton::Primary)
592 && (self.style.single_click_edit_mode || is_interactive_cell)
593 {
594 response_consumed = true;
595 commands.push(Command::CcEditStart(
596 row_id,
597 vis_col,
598 viewer.clone_row(&table.rows[row_id.0]).into(),
599 ));
600 edit_started = true;
601 }
602
603 (resp.clone() | head_resp.clone()).context_menu(|ui| {
606 response_consumed = true;
607 ui.set_min_size(egui::vec2(250., 10.));
608
609 if !selected {
610 commands.push(Command::CcSetSelection(vec![VisSelection(
611 linear_index,
612 linear_index,
613 )]));
614 } else if !is_interactive_cell {
615 s.set_interactive_cell(vis_row, vis_col);
616 }
617
618 let sel_multi_row = s.cursor_as_selection().is_some_and(|sel| {
619 let mut min = usize::MAX;
620 let mut max = usize::MIN;
621
622 for sel in sel {
623 min = min.min(sel.0 .0);
624 max = max.max(sel.1 .0);
625 }
626
627 let (r_min, _) = VisLinearIdx(min).row_col(s.vis_cols().len());
628 let (r_max, _) = VisLinearIdx(max).row_col(s.vis_cols().len());
629
630 r_min != r_max
631 });
632
633 let cursor_x = ui.cursor().min.x;
634 let clip = s.has_clipboard_contents();
635 let b_undo = s.has_undo();
636 let b_redo = s.has_redo();
637 let mut n_sep_menu = 0;
638 let mut draw_sep = false;
639
640 [
641 Some((selected, "🖻", "Selection: Copy", UiAction::CopySelection)),
642 Some((selected, "🖻", "Selection: Cut", UiAction::CutSelection)),
643 Some((selected, "🗙", "Selection: Clear", UiAction::DeleteSelection)),
644 Some((
645 sel_multi_row,
646 "🗐",
647 "Selection: Fill",
648 UiAction::SelectionDuplicateValues,
649 )),
650 None,
651 Some((clip, "➿", "Clipboard: Paste", UiAction::PasteInPlace)),
652 Some((clip, "🛠", "Clipboard: Insert", UiAction::PasteInsert)),
653 None,
654 Some((true, "🗐", "Row: Duplicate", UiAction::DuplicateRow)),
655 Some((true, "🗙", "Row: Delete", UiAction::DeleteRow)),
656 None,
657 Some((b_undo, "⎗", "Undo", UiAction::Undo)),
658 Some((b_redo, "⎘", "Redo", UiAction::Redo)),
659 ]
660 .map(|opt| {
661 if let Some((icon, label, action)) =
662 opt.filter(|x| x.0).map(|x| (x.1, x.2, x.3))
663 {
664 if draw_sep {
665 draw_sep = false;
666 ui.separator();
667 }
668
669 let hotkey = hotkeys
670 .iter()
671 .find_map(|(k, a)| (a == &action).then(|| ctx.format_shortcut(k)));
672
673 ui.horizontal(|ui| {
674 ui.monospace(icon);
675 ui.add_space(cursor_x + 20. - ui.cursor().min.x);
676
677 let btn = egui::Button::new(label)
678 .shortcut_text(hotkey.unwrap_or_else(|| "🗙".into()));
679 let r = ui.centered_and_justified(|ui| ui.add(btn)).inner;
680
681 if r.clicked() {
682 actions.push(action);
683 ui.close_menu();
684 }
685 });
686
687 n_sep_menu += 1;
688 } else if n_sep_menu > 0 {
689 n_sep_menu = 0;
690 draw_sep = true;
691 }
692 });
693 });
694
695 let drop_area_rect = rect.with_max_x(resp.rect.max.x);
703 let contains_pointer = ctx
704 .pointer_hover_pos()
705 .is_some_and(|pos| drop_area_rect.contains(pos));
706
707 if !response_consumed && contains_pointer {
708 if let Some(new_value) =
709 viewer.on_cell_view_response(&table.rows[row_id.0], col.0, &resp)
710 {
711 commands.push(Command::SetCells {
712 slab: vec![*new_value].into_boxed_slice(),
713 values: vec![(row_id, *col, RowSlabIndex(0))].into_boxed_slice(),
714 });
715 }
716 }
717 }
718
719 if let Some((should_focus, vis_column)) = edit_state {
721 let column = s.vis_cols()[vis_column.0];
722
723 egui::Window::new("")
724 .id(ui_id.with(row_id).with(column))
725 .constrain_to(body_max_rect)
726 .fixed_pos(editing_cell_rect.min)
727 .auto_sized()
728 .min_size(editing_cell_rect.size())
729 .max_width(editing_cell_rect.width())
730 .title_bar(false)
731 .frame(egui::Frame::NONE.corner_radius(egui::CornerRadius::same(3)))
732 .show(ctx, |ui| {
733 ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
734 if let Some(resp) =
735 viewer.show_cell_editor(ui, s.unwrap_editing_row_data(), column.0)
736 {
737 if should_focus {
738 resp.request_focus()
739 }
740
741 new_maximum_height = resp.rect.height().max(new_maximum_height);
742 } else {
743 commands.push(Command::CcCommitEdit);
744 }
745 });
746 });
747 }
748
749 if let Some(resp) = &mut resp_total {
751 *resp = resp.union(row.response());
752 } else {
753 resp_total = Some(row.response());
754 }
755
756 if self.style.table_row_height.is_none() && prev_row_height != new_maximum_height {
758 row_height_updates.push((vis_row, new_maximum_height));
759 }
760 }; if let Some(height) = self.style.table_row_height {
764 body.rows(height, cc_row_heights.len(), render_fn);
765 } else {
766 body.heterogeneous_rows(cc_row_heights.iter().cloned(), render_fn);
767 }
768
769 if ctx.input(|i| i.pointer.button_released(PointerButton::Primary)) {
772 let mods = ctx.input(|i| i.modifiers);
773 if let Some(sel) = s.cci_take_selection(mods).filter(|_| !edit_started) {
774 commands.push(Command::CcSetSelection(sel));
775 }
776 }
777
778 if let Some(resp) = resp_total.clone() {
780 if resp.clicked() | resp.dragged() {
781 s.cci_has_focus = true;
782 } else if resp.clicked_elsewhere() {
783 s.cci_has_focus = false;
784 }
785 }
786
787 s.cc_row_heights = cc_row_heights.tap_mut(|values| {
789 if !row_height_updates.is_empty() {
790 ctx.request_repaint();
791 }
792
793 for (row_index, row_height) in row_height_updates {
794 values[row_index.0] = row_height;
795 }
796 });
797
798 commands.extend(
800 actions
801 .into_iter()
802 .flat_map(|action| s.try_apply_ui_action(table, viewer, action)),
803 );
804
805 for cmd in commands {
807 match cmd {
808 Command::CcUpdateSystemClipboard(new_content) => {
809 ctx.copy_text(new_content);
810 }
811 cmd => {
812 if matches!(cmd, Command::CcCommitEdit) {
813 ctx.memory_mut(|x| {
815 if let Some(fc) = x.focused() {
816 x.surrender_focus(fc)
817 }
818 });
819 }
820
821 s.push_new_command(
822 table,
823 viewer,
824 cmd,
825 if self.style.max_undo_history == 0 {
826 100
827 } else {
828 self.style.max_undo_history
829 },
830 );
831 }
832 }
833 }
834
835 resp_total.unwrap()
837 }
838}
839
840impl<R, V: RowViewer<R>> Drop for Renderer<'_, R, V> {
841 fn drop(&mut self) {
842 self.table.ui = self.state.take();
843 }
844}