1use std::mem::{replace, take};
2
3use egui::{Align, Color32, CornerRadius, Event, Label, Layout, PointerButton, PopupAnchor, Rect, Response, RichText, Sense, Stroke, StrokeKind, Tooltip, Vec2b};
4use egui_extras::Column;
5use tap::prelude::{Pipe, Tap};
6
7use crate::{
8 viewer::{EmptyRowCreateContext, RowViewer},
9 DataTable, UiAction,
10};
11
12use self::state::*;
13
14use format as f;
15use std::sync::Arc;
16use egui::scroll_area::ScrollBarVisibility;
17
18pub(crate) mod state;
19mod tsv;
20
21#[derive(Default, Debug, Clone, Copy)]
26#[non_exhaustive]
27pub struct Style {
28 pub bg_selected_cell: Option<egui::Color32>,
30
31 pub bg_selected_highlight_cell: Option<egui::Color32>,
33
34 pub fg_selected_highlight_cell: Option<egui::Color32>,
36
37 pub fg_drag_selection: Option<egui::Color32>,
39
40 pub max_undo_history: usize,
45
46 pub table_row_height: Option<f32>,
48
49 pub single_click_edit_mode: bool,
52
53 pub cell_align: egui::Align,
55
56 pub focused_row_stroke: Option<egui::Color32>,
59
60 pub auto_shrink: Vec2b,
62
63 pub scroll_bar_visibility: ScrollBarVisibility,
65}
66
67pub struct Renderer<'a, R, V: RowViewer<R>> {
70 table: &'a mut DataTable<R>,
71 viewer: &'a mut V,
72 state: Option<Box<UiState<R>>>,
73 style: Style,
74 translator: Arc<dyn Translator>
75}
76
77impl<R, V: RowViewer<R>> egui::Widget for Renderer<'_, R, V> {
78 fn ui(self, ui: &mut egui::Ui) -> Response {
79 self.show(ui)
80 }
81}
82
83impl<'a, R, V: RowViewer<R>> Renderer<'a, R, V> {
84 pub fn new(table: &'a mut DataTable<R>, viewer: &'a mut V) -> Self {
85 if table.rows.is_empty() && viewer.allow_row_insertions() {
86 table.push(viewer.new_empty_row_for(EmptyRowCreateContext::InsertNewLine));
87 }
88
89 Self {
90 state: Some(table.ui.take().unwrap_or_default().tap_mut(|state| {
91 state.validate_identity(viewer);
92 })),
93 table,
94 viewer,
95 style: Default::default(),
96 translator: Arc::new(EnglishTranslator::default()),
97 }
98 }
99
100 pub fn with_style(mut self, style: Style) -> Self {
101 self.style = style;
102 self
103 }
104
105 pub fn with_style_modify(mut self, f: impl FnOnce(&mut Style)) -> Self {
106 f(&mut self.style);
107 self
108 }
109
110 pub fn with_table_row_height(mut self, height: f32) -> Self {
111 self.style.table_row_height = Some(height);
112 self
113 }
114
115 pub fn with_max_undo_history(mut self, max_undo_history: usize) -> Self {
116 self.style.max_undo_history = max_undo_history;
117 self
118 }
119
120 #[cfg(not(doctest))]
140 pub fn with_translator(mut self, translator: Arc<dyn Translator>) -> Self {
141 self.translator = translator;
142 self
143 }
144
145 pub fn show(self, ui: &mut egui::Ui) -> Response {
146 egui::ScrollArea::horizontal()
147 .show(ui, |ui| self.impl_show(ui))
148 .inner
149 }
150
151 fn impl_show(mut self, ui: &mut egui::Ui) -> Response {
152 let ctx = &ui.ctx().clone();
153 let ui_id = ui.id();
154 let style = ui.style().clone();
155 let painter = ui.painter().clone();
156 let visual = &style.visuals;
157 let viewer = &mut *self.viewer;
158 let s = self.state.as_mut().unwrap();
159 let mut resp_total = None::<Response>;
160 let mut resp_ret = None::<Response>;
161 let mut commands = Vec::<Command<R>>::new();
162 let ui_layer_id = ui.layer_id();
163
164 let green = if visual.window_fill.g() > 128 {
168 Color32::DARK_GREEN
169 } else {
170 Color32::GREEN
171 };
172
173 let mut builder = egui_extras::TableBuilder::new(ui).column(Column::auto());
174
175 let iter_vis_cols_with_flag = s
176 .vis_cols()
177 .iter()
178 .enumerate()
179 .map(|(index, column)| (column, index + 1 == s.vis_cols().len()));
180
181 for (column, flag) in iter_vis_cols_with_flag {
182 builder = builder.column(viewer.column_render_config(column.0, flag));
183 }
184
185 if replace(&mut s.cci_want_move_scroll, false) {
186 let interact_row = s.interactive_cell().0;
187 builder = builder.scroll_to_row(interact_row.0, None);
188 }
189
190 builder
191 .columns(Column::auto(), s.num_columns() - s.vis_cols().len())
192 .drag_to_scroll(false) .striped(true)
194 .cell_layout(egui::Layout::default().with_cross_align(self.style.cell_align))
195 .max_scroll_height(f32::MAX)
196 .auto_shrink(self.style.auto_shrink)
197 .scroll_bar_visibility(self.style.scroll_bar_visibility)
198 .sense(Sense::click_and_drag().tap_mut(|s| s.set(Sense::FOCUSABLE, true)))
199 .header(20., |mut h| {
200 h.col(|_ui| {
201 });
203
204 let has_any_hidden_col = s.vis_cols().len() != s.num_columns();
205
206 for (vis_col, &col) in s.vis_cols().iter().enumerate() {
207 let vis_col = VisColumnPos(vis_col);
208 let mut painter = None;
209 let (col_rect, resp) = h.col(|ui| {
210 egui::Sides::new().show(ui, |ui| {
211 ui.add(Label::new(viewer.column_name(col.0))
212 .selectable(false)
213 );
214 }, |ui|{
215 if let Some(pos) = s.sort().iter().position(|(c, ..)| c == &col) {
216 let is_asc = s.sort()[pos].1 .0 as usize;
217
218 ui.colored_label(
219 [green, Color32::RED][is_asc],
220 RichText::new(format!("{}{}", ["↘", "↗"][is_asc], pos + 1,))
221 .monospace(),
222 );
223 } else {
224 let max_sort_indicator_width = (s.num_columns() + 1).to_string().len() + 1;
226 ui.add(Label::new(RichText::new(" ".repeat(max_sort_indicator_width)).monospace()).selectable(false));
229 }
230 });
231
232 painter = Some(ui.painter().clone());
233 });
234
235 resp.dnd_set_drag_payload(vis_col);
237
238 if resp.dragged() {
239 Tooltip::always_open(ctx.clone(), ui_layer_id, "_EGUI_DATATABLE__COLUMN_MOVE__".into(), PopupAnchor::Pointer)
240 .gap(12.0)
241 .show(|ui|{
242 let colum_name = viewer.column_name(col.0);
243 ui.label(colum_name);
244 });
245 }
246
247 if resp.hovered() && viewer.is_sortable_column(col.0) {
248 if let Some(p) = &painter {
249 p.rect_filled(
250 col_rect,
251 egui::CornerRadius::ZERO,
252 visual.selection.bg_fill.gamma_multiply(0.2),
253 );
254 }
255 }
256
257 if viewer.is_sortable_column(col.0) && resp.clicked_by(PointerButton::Primary) {
258 let mut sort = s.sort().to_owned();
259 match sort.iter_mut().find(|(c, ..)| c == &col) {
260 Some((_, asc)) => match asc.0 {
261 true => asc.0 = false,
262 false => sort.retain(|(c, ..)| c != &col),
263 },
264 None => {
265 sort.push((col, IsAscending(true)));
266 }
267 }
268
269 commands.push(Command::SetColumnSort(sort));
270 }
271
272 if resp.dnd_hover_payload::<VisColumnPos>().is_some() {
273 if let Some(p) = &painter {
274 p.rect_filled(
275 col_rect,
276 egui::CornerRadius::ZERO,
277 visual.selection.bg_fill.gamma_multiply(0.5),
278 );
279 }
280 }
281
282 if let Some(payload) = resp.dnd_release_payload::<VisColumnPos>() {
283 commands.push(Command::CcReorderColumn {
284 from: *payload,
285 to: vis_col
286 .0
287 .pipe(|v| v + (payload.0 < v) as usize)
288 .pipe(VisColumnPos),
289 })
290 }
291
292 resp.context_menu(|ui| {
293 if ui.button(self.translator.translate("context-menu-hide")).clicked() {
294 commands.push(Command::CcHideColumn(col));
295 }
296
297 if !s.sort().is_empty() && ui.button(self.translator.translate("context-menu-clear-sort")).clicked() {
298 commands.push(Command::SetColumnSort(Vec::new()));
299 }
300
301 if has_any_hidden_col {
302 ui.separator();
303 ui.label(self.translator.translate("context-menu-hidden"));
304
305 for col in (0..s.num_columns()).map(ColumnIdx) {
306 if !s.vis_cols().contains(&col)
307 && ui.button(viewer.column_name(col.0)).clicked()
308 {
309 commands.push(Command::CcShowColumn {
310 what: col,
311 at: vis_col,
312 });
313 }
314 }
315 }
316 });
317 }
318
319 resp_total = Some(h.response());
321 })
322 .tap_mut(|table| {
323 table.ui_mut().separator();
324 })
325 .body(|body: egui_extras::TableBody<'_>| {
326 resp_ret = Some(
327 self.impl_show_body(body, painter, commands, ctx, &style, ui_id, resp_total),
328 );
329 });
330
331 resp_ret.unwrap_or_else(|| ui.label("??"))
332 }
333
334 #[allow(clippy::too_many_arguments)]
335 fn impl_show_body(
336 &mut self,
337 body: egui_extras::TableBody<'_>,
338 mut _painter: egui::Painter,
339 mut commands: Vec<Command<R>>,
340 ctx: &egui::Context,
341 style: &egui::Style,
342 ui_id: egui::Id,
343 mut resp_total: Option<Response>,
344 ) -> Response {
345 let viewer = &mut *self.viewer;
346 let s = self.state.as_mut().unwrap();
347 let table = &mut *self.table;
348 let visual = &style.visuals;
349 let visible_cols = s.vis_cols().clone();
350 let no_rounding = egui::CornerRadius::ZERO;
351
352 let mut actions = Vec::<UiAction>::new();
353 let mut edit_started = false;
354 let hotkeys = viewer.hotkeys(&s.ui_action_context());
355
356 'detect_hotkey: {
358 if !s.cci_has_focus {
361 break 'detect_hotkey;
362 }
363
364 if !s.is_editing() {
365 ctx.input_mut(|i| {
366 i.events.retain(|x| {
367 match x {
368 Event::Copy => actions.push(UiAction::CopySelection),
369 Event::Cut => actions.push(UiAction::CutSelection),
370
371 Event::Paste(clipboard) => {
374 if !clipboard.is_empty() {
375 s.try_update_clipboard_from_string(viewer, clipboard);
379 }
380
381 if i.modifiers.shift {
382 if viewer.allow_row_insertions() {
383 actions.push(UiAction::PasteInsert)
384 }
385 } else {
386 actions.push(UiAction::PasteInPlace)
387 }
388 }
389
390 _ => return true,
391 }
392 false
393 })
394 });
395 }
396
397 for (hotkey, action) in &hotkeys {
398 ctx.input_mut(|inp| {
399 if inp.consume_shortcut(hotkey) {
400 actions.push(*action);
401 }
402 })
403 }
404 }
405
406 #[cfg(feature = "persistency")]
408 if viewer.persist_ui_state() {
409 s.validate_persistency(ctx, ui_id, viewer);
410 }
411
412 s.validate_cc(&mut table.rows, viewer);
415
416 let cc_row_heights = take(&mut s.cc_row_heights);
420
421 let mut row_height_updates = Vec::new();
422 let vis_row_digits = s.cc_rows.len().max(1).ilog10();
423 let row_id_digits = table.len().max(1).ilog10();
424
425 let body_max_rect = body.max_rect();
426 let has_any_sort = !s.sort().is_empty();
427
428 let pointer_interact_pos = ctx.input(|i| i.pointer.latest_pos().unwrap_or_default());
429 let pointer_primary_down = ctx.input(|i| i.pointer.button_down(PointerButton::Primary));
430
431 s.cci_page_row_count = 0;
432
433 let render_fn = |mut row: egui_extras::TableRow| {
438 s.cci_page_row_count += 1;
439
440 let vis_row = VisRowPos(row.index());
441 let row_id = s.cc_rows[vis_row.0];
442 let prev_row_height = cc_row_heights[vis_row.0];
443
444 let mut row_elem_start = Default::default();
445
446 let edit_state = s.row_editing_cell(row_id);
448 let mut editing_cell_rect = Rect::NOTHING;
449 let interactive_row = s.is_interactive_row(vis_row);
450
451 let check_mouse_dragging_selection = {
452 let s_cci_has_focus = s.cci_has_focus;
453 let s_cci_has_selection = s.has_cci_selection();
454
455 move |rect: &Rect, resp: &egui::Response| {
456 let cci_hovered: bool = s_cci_has_focus
457 && s_cci_has_selection
458 && rect
459 .with_max_x(resp.rect.right())
460 .contains(pointer_interact_pos);
461 let sel_drag = cci_hovered && pointer_primary_down;
462 let sel_click = !s_cci_has_selection && resp.hovered() && pointer_primary_down;
463
464 sel_drag || sel_click
465 }
466 };
467
468 row.set_selected(edit_state.is_some());
472
473 let (head_rect, head_resp) = row.col(|ui| {
475 row_elem_start = ui.max_rect().right_top();
477
478 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
479 ui.separator();
480
481 if has_any_sort {
482 ui.monospace(
483 RichText::from(f!(
484 "{:·>width$}",
485 row_id.0,
486 width = row_id_digits as usize
487 ))
488 .strong(),
489 );
490 } else {
491 ui.monospace(
492 RichText::from(f!("{:>width$}", "", width = row_id_digits as usize))
493 .strong(),
494 );
495 }
496
497 ui.monospace(
498 RichText::from(f!(
499 "{:·>width$}",
500 vis_row.0 + 1,
501 width = vis_row_digits as usize
502 ))
503 .weak(),
504 );
505 });
506 });
507
508 if check_mouse_dragging_selection(&head_rect, &head_resp) {
509 s.cci_sel_update_row(vis_row);
510 }
511
512 let mut new_maximum_height = 0.;
516
517 for (vis_col, col) in visible_cols.iter().enumerate() {
519 let vis_col = VisColumnPos(vis_col);
520 let linear_index = vis_row.linear_index(visible_cols.len(), vis_col);
521 let selected = s.is_selected(vis_row, vis_col);
522 let cci_selected = s.is_selected_cci(vis_row, vis_col);
523 let is_editing = edit_state.is_some();
524 let is_interactive_cell = interactive_row.is_some_and(|x| x == vis_col);
525 let mut response_consumed = s.is_editing();
526
527 let (rect, resp) = row.col(|ui| {
528 let ui_max_rect = ui.max_rect();
529
530 if cci_selected {
531 ui.painter().rect_stroke(
532 ui_max_rect,
533 no_rounding,
534 Stroke {
535 width: 2.,
536 color: self
537 .style
538 .fg_drag_selection
539 .unwrap_or(visual.selection.bg_fill),
540 },
541 StrokeKind::Inside,
542 );
543 }
544
545 if is_interactive_cell {
546 ui.painter().rect_filled(
547 ui_max_rect.expand(2.),
548 no_rounding,
549 self.style
550 .bg_selected_highlight_cell
551 .unwrap_or(visual.selection.bg_fill),
552 );
553 } else if selected {
554 ui.painter().rect_filled(
555 ui_max_rect.expand(1.),
556 no_rounding,
557 self.style
558 .bg_selected_cell
559 .unwrap_or(visual.selection.bg_fill.gamma_multiply(0.5)),
560 );
561 }
562
563 ui.style_mut()
567 .visuals
568 .widgets
569 .noninteractive
570 .fg_stroke
571 .color = if is_interactive_cell {
572 self.style
573 .fg_selected_highlight_cell
574 .unwrap_or(visual.strong_text_color())
575 } else {
576 visual.strong_text_color()
577 };
578
579 ui.add_enabled_ui(false, |ui| {
584 if !(is_editing && is_interactive_cell) {
585 viewer.show_cell_view(ui, &table.rows[row_id.0], col.0);
586 }
587 });
588
589 #[cfg(any())]
590 if selected {
591 ui.painter().rect_stroke(
592 ui_max_rect,
593 no_rounding,
594 Stroke {
595 width: 1.,
596 color: visual.weak_text_color(),
597 },
598 );
599 }
600
601 if interactive_row.is_some() && !is_editing {
602 let st = Stroke {
603 width: 1.,
604 color: self
605 .style
606 .focused_row_stroke
607 .unwrap_or(visual.warn_fg_color.gamma_multiply(0.5)),
608 };
609
610 let xr = ui_max_rect.x_range();
611 let yr = ui_max_rect.y_range();
612 ui.painter().hline(xr, yr.min, st);
613 ui.painter().hline(xr, yr.max, st);
614 }
615
616 if edit_state.is_some_and(|(_, vis)| vis == vis_col) {
617 editing_cell_rect = ui_max_rect;
618 }
619 });
620
621 new_maximum_height = rect.height().max(new_maximum_height);
622
623 if check_mouse_dragging_selection(&rect, &resp) {
625 response_consumed = true;
627 s.cci_sel_update(linear_index);
628 }
629
630 let editable = viewer.is_editable_cell(vis_col.0, vis_row.0, &table.rows[row_id.0]);
631
632 if editable
633 && (resp.clicked_by(PointerButton::Primary)
634 && (self.style.single_click_edit_mode || is_interactive_cell))
635 {
636 response_consumed = true;
637 commands.push(Command::CcEditStart(
638 row_id,
639 vis_col,
640 viewer.clone_row(&table.rows[row_id.0]).into(),
641 ));
642 edit_started = true;
643 }
644
645 (resp.clone() | head_resp.clone()).context_menu(|ui| {
648 response_consumed = true;
649 ui.set_min_size(egui::vec2(250., 10.));
650
651 if !selected {
652 commands.push(Command::CcSetSelection(vec![VisSelection(
653 linear_index,
654 linear_index,
655 )]));
656 } else if !is_interactive_cell {
657 s.set_interactive_cell(vis_row, vis_col);
658 }
659
660 let sel_multi_row = s.cursor_as_selection().is_some_and(|sel| {
661 let mut min = usize::MAX;
662 let mut max = usize::MIN;
663
664 for sel in sel {
665 min = min.min(sel.0 .0);
666 max = max.max(sel.1 .0);
667 }
668
669 let (r_min, _) = VisLinearIdx(min).row_col(s.vis_cols().len());
670 let (r_max, _) = VisLinearIdx(max).row_col(s.vis_cols().len());
671
672 r_min != r_max
673 });
674
675 let cursor_x = ui.cursor().min.x;
676 let clip = s.has_clipboard_contents();
677 let b_undo = s.has_undo();
678 let b_redo = s.has_redo();
679 let mut n_sep_menu = 0;
680 let mut draw_sep = false;
681
682 let context_menu_items = [
683 Some((selected, "🖻", "context-menu-selection-copy", UiAction::CopySelection)),
684 Some((selected, "🖻", "context-menu-selection-cut", UiAction::CutSelection)),
685 Some((selected, "🗙", "context-menu-selection-clear", UiAction::DeleteSelection)),
686 Some((
687 sel_multi_row,
688 "🗐",
689 "context-menu-selection-fill",
690 UiAction::SelectionDuplicateValues,
691 )),
692 None,
693 Some((clip, "➿", "context-menu-clipboard-paste", UiAction::PasteInPlace)),
694 Some((
695 clip && viewer.allow_row_insertions(),
696 "🛠",
697 "context-menu-clipboard-insert",
698 UiAction::PasteInsert,
699 )),
700 None,
701 Some((
702 viewer.allow_row_insertions(),
703 "🗐",
704 "context-menu-row-duplicate",
705 UiAction::DuplicateRow,
706 )),
707 Some((
708 viewer.allow_row_deletions(),
709 "🗙",
710 "context-menu-row-delete",
711 UiAction::DeleteRow,
712 )),
713 None,
714 Some((b_undo, "⎗", "context-menu-undo", UiAction::Undo)),
715 Some((b_redo, "⎘", "context-menu-redo", UiAction::Redo)),
716 ];
717
718 context_menu_items.map(|opt| {
719 if let Some((icon, key, action)) =
720 opt.filter(|x| x.0).map(|x| (x.1, x.2, x.3))
721 {
722 if draw_sep {
723 draw_sep = false;
724 ui.separator();
725 }
726
727 let hotkey = hotkeys
728 .iter()
729 .find_map(|(k, a)| (a == &action).then(|| ctx.format_shortcut(k)));
730
731 ui.horizontal(|ui| {
732 ui.monospace(icon);
733 ui.add_space(cursor_x + 20. - ui.cursor().min.x);
734
735 let label = self.translator.translate(key);
736 let btn = egui::Button::new(label)
737 .shortcut_text(hotkey.unwrap_or_else(|| "🗙".into()));
738 let r = ui.centered_and_justified(|ui| ui.add(btn)).inner;
739
740 if r.clicked() {
741 actions.push(action);
742 }
743 });
744
745 n_sep_menu += 1;
746 } else if n_sep_menu > 0 {
747 n_sep_menu = 0;
748 draw_sep = true;
749 }
750 });
751 });
752
753 let drop_area_rect = rect.with_max_x(resp.rect.max.x);
761 let contains_pointer = ctx
762 .pointer_hover_pos()
763 .is_some_and(|pos| drop_area_rect.contains(pos));
764
765 if !response_consumed && contains_pointer {
766 if let Some(new_value) =
767 viewer.on_cell_view_response(&table.rows[row_id.0], col.0, &resp)
768 {
769 let mut values = vec![(row_id, *col, RowSlabIndex(0))];
770
771 values.retain(|(row, col, _slab_id)| {
772 viewer.is_editable_cell(col.0, row.0, &table.rows[row.0])
773 });
774
775 commands.push(Command::SetCells {
776 slab: vec![*new_value].into_boxed_slice(),
777 values: values.into_boxed_slice(),
778 });
779 }
780 }
781 }
782
783 if let Some((should_focus, vis_column)) = edit_state {
785 let column = s.vis_cols()[vis_column.0];
786
787 egui::Window::new("")
788 .id(ui_id.with(row_id).with(column))
789 .constrain_to(body_max_rect)
790 .fixed_pos(editing_cell_rect.min)
791 .auto_sized()
792 .min_size(editing_cell_rect.size())
793 .max_width(editing_cell_rect.width())
794 .title_bar(false)
795 .frame(egui::Frame::NONE.corner_radius(egui::CornerRadius::same(3)))
796 .show(ctx, |ui| {
797 ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
798 if let Some(resp) =
799 viewer.show_cell_editor(ui, s.unwrap_editing_row_data(), column.0)
800 {
801 if should_focus {
802 resp.request_focus()
803 }
804
805 new_maximum_height = resp.rect.height().max(new_maximum_height);
806 } else {
807 commands.push(Command::CcCommitEdit);
808 }
809 });
810 });
811 }
812
813 if let Some(resp) = &mut resp_total {
815 *resp = resp.union(row.response());
816 } else {
817 resp_total = Some(row.response());
818 }
819
820 if self.style.table_row_height.is_none() && prev_row_height != new_maximum_height {
822 row_height_updates.push((vis_row, new_maximum_height));
823 }
824 }; if let Some(height) = self.style.table_row_height {
828 body.rows(height, cc_row_heights.len(), render_fn);
829 } else {
830 body.heterogeneous_rows(cc_row_heights.iter().cloned(), render_fn);
831 }
832
833 if ctx.input(|i| i.pointer.button_released(PointerButton::Primary)) {
836 let mods = ctx.input(|i| i.modifiers);
837 if let Some(sel) = s.cci_take_selection(mods).filter(|_| !edit_started) {
838 commands.push(Command::CcSetSelection(sel));
839 }
840 }
841
842 if let Some(resp) = resp_total.clone() {
844
845 let clicked_elsewhere = resp.clicked_elsewhere();
846 let response_rect_contains_pointer = resp.rect.contains(pointer_interact_pos);
848
849 if resp.clicked() | resp.dragged() {
850 s.cci_has_focus = true;
851 } else if clicked_elsewhere && !response_rect_contains_pointer {
852 s.cci_has_focus = false;
853 if s.is_editing() {
854 commands.push(Command::CcCommitEdit)
855 }
856 }
857 }
858
859 s.cc_row_heights = cc_row_heights.tap_mut(|values| {
861 if !row_height_updates.is_empty() {
862 ctx.request_repaint();
863 }
864
865 for (row_index, row_height) in row_height_updates {
866 values[row_index.0] = row_height;
867 }
868 });
869
870 commands.extend(
872 actions
873 .into_iter()
874 .flat_map(|action| s.try_apply_ui_action(table, viewer, action)),
875 );
876
877 for cmd in commands {
879 match cmd {
880 Command::CcUpdateSystemClipboard(new_content) => {
881 ctx.copy_text(new_content);
882 }
883 cmd => {
884 if matches!(cmd, Command::CcCommitEdit) {
885 ctx.memory_mut(|x| {
887 if let Some(fc) = x.focused() {
888 x.surrender_focus(fc)
889 }
890 });
891 }
892
893 s.push_new_command(
894 table,
895 viewer,
896 cmd,
897 if self.style.max_undo_history == 0 {
898 100
899 } else {
900 self.style.max_undo_history
901 },
902 );
903 }
904 }
905 }
906
907 resp_total.unwrap()
909 }
910}
911
912impl<R, V: RowViewer<R>> Drop for Renderer<'_, R, V> {
913 fn drop(&mut self) {
914 self.table.ui = self.state.take();
915 }
916}
917
918pub trait Translator {
921
922 fn translate(&self, key: &str) -> String;
926}
927
928#[derive(Default)]
929pub struct EnglishTranslator {}
930
931impl Translator for EnglishTranslator {
932 fn translate(&self, key: &str) -> String {
933 match key {
934 "context-menu-selection-copy" => "Selection: Copy",
936 "context-menu-selection-cut" => "Selection: Cut",
937 "context-menu-selection-clear" => "Selection: Clear",
938 "context-menu-selection-fill" => "Selection: Fill",
939 "context-menu-clipboard-paste" => "Clipboard: Paste",
940 "context-menu-clipboard-insert" => "Clipboard: Insert",
941 "context-menu-row-duplicate" => "Row: Duplicate",
942 "context-menu-row-delete" => "Row: Delete",
943 "context-menu-undo" => "Undo",
944 "context-menu-redo" => "Redo",
945
946 "context-menu-hide" => "Hide",
948 "context-menu-hidden" => "Hidden",
949 "context-menu-clear-sort" => "Clear sort",
950 _ => key,
951 }.to_string()
952 }
953}