egui_data_table/
draw.rs

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/* -------------------------------------------- Style ------------------------------------------- */
23
24/// Style configuration for the table.
25// TODO: Implement more style configurations.
26#[derive(Default, Debug, Clone, Copy)]
27#[non_exhaustive]
28pub struct Style {
29    /// Background color override for selection. Default uses `visuals.selection.bg_fill`.
30    pub bg_selected_cell: Option<egui::Color32>,
31
32    /// Background color override for selected cell. Default uses `visuals.selection.bg_fill`.
33    pub bg_selected_highlight_cell: Option<egui::Color32>,
34
35    /// Foreground color override for selected cell. Default uses `visuals.strong_text_colors`.
36    pub fg_selected_highlight_cell: Option<egui::Color32>,
37
38    /// Foreground color for cells that are going to be selected when mouse is dropped.
39    pub fg_drag_selection: Option<egui::Color32>,
40
41    /* ·························································································· */
42    /// Maximum number of undo history. This is applied when actual action is performed.
43    ///
44    /// Setting value '0' results in kinda appropriate default value.
45    pub max_undo_history: usize,
46
47    /// If specify this as [`None`], the heterogeneous row height will be used.
48    pub table_row_height: Option<f32>,
49
50    /// When enabled, single click on a cell will start editing mode. Default is `false` where
51    /// double action(click 1: select, click 2: edit) is required.
52    pub single_click_edit_mode: bool,
53
54    /// How to align cell contents. Default is left-aligned.
55    pub cell_align: egui::Align,
56
57    /// Color to use for the stroke above/below focused row.
58    /// If `None`, defaults to a darkened `warn_fg_color`.
59    pub focused_row_stroke: Option<egui::Color32>,
60}
61
62/* ------------------------------------------ Rendering ----------------------------------------- */
63
64pub 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        // NOTE: unlike RED and YELLOW which can be acquirable through 'error_bg_color' and
133        // 'warn_bg_color', there's no 'green' color which can be acquired from inherent theme.
134        // Following logic simply gets 'green' color from current background's brightness.
135        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) // Drag is used for selection;
161            .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                    // TODO: Add `Configure Sorting` button
168                });
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                    // Set drag payload for column reordering.
198                    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                // Account for header response to calculate total response.
285                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        // Preemptively consume all hotkeys.
322        'detect_hotkey: {
323            // Detect hotkey inputs only when the table has focus. While editing, let the
324            // editor consume input.
325            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                            // Try to parse clipboard contents and detect if it's compatible
337                            // with cells being pasted.
338                            Event::Paste(clipboard) => {
339                                if !clipboard.is_empty() {
340                                    // If system clipboard is not empty, try to update the internal
341                                    // clipboard with system clipboard content before applying
342                                    // paste operation.
343                                    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        // Validate persistency state.
370        #[cfg(feature = "persistency")]
371        if viewer.persist_ui_state() {
372            s.validate_persistency(ctx, ui_id, viewer);
373        }
374
375        // Validate ui state. Defer this as late as possible; since it may not be
376        // called if the table area is out of the visible space.
377        s.validate_cc(&mut table.rows, viewer);
378
379        // Checkout `cc_rows` to satisfy borrow checker. We need to access to
380        // state mutably within row rendering; therefore, we can't simply borrow
381        // `cc_rows` during the whole logic!
382        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        /* ----------------------------- Primary Rendering Function ----------------------------- */
397        // - Extracted as a closure to differentiate behavior based on row height
398        //   configuration. (heterogeneous or homogeneous row heights)
399
400        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            // Check if current row is edition target
410            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            /* -------------------------------- Header Rendering -------------------------------- */
432
433            // Mark row background filled if being edited.
434            row.set_selected(edit_state.is_some());
435
436            // Render row header button
437            let (head_rect, head_resp) = row.col(|ui| {
438                // Calculate the position where values start.
439                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            /* -------------------------------- Columns Rendering ------------------------------- */
476
477            // Overridable maximum height
478            let mut new_maximum_height = 0.;
479
480            // Render cell contents regardless of the edition state.
481            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                    // Actual widget rendering happens within this line.
527
528                    // ui.set_enabled(false);
529                    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                    // FIXME: After egui 0.27, now the widgets spawned inside this closure
543                    // intercepts interactions, which is basically natural behavior(Upper layer
544                    // widgets). However, this change breaks current implementation which relies on
545                    // the previous table behavior.
546                    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                // -- Mouse Actions --
585                if check_mouse_dragging_selection(&rect, &resp) {
586                    // Expand cci selection
587                    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                /* --------------------------- Context Menu Rendering --------------------------- */
604
605                (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                // Forward DnD event if not any event was consumed by the response.
696
697                // FIXME: Upgrading egui 0.29 make interaction rectangle of response object
698                // larger(in y axis) than actually visible column cell size. To deal with this,
699                // I've used returned content area rectangle instead, expanding its width to
700                // response size.
701
702                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            /* -------------------------------- Editor Rendering -------------------------------- */
720            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            // Accumulate response
750            if let Some(resp) = &mut resp_total {
751                *resp = resp.union(row.response());
752            } else {
753                resp_total = Some(row.response());
754            }
755
756            // Update row height cache if necessary.
757            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        }; // ~ render_fn
761
762        // Actual rendering
763        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        /* ----------------------------------- Event Handling ----------------------------------- */
770
771        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        // Control overall focus status.
779        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        // Check in borrowed `cc_rows` back to state.
788        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        // Handle queued actions
799        commands.extend(
800            actions
801                .into_iter()
802                .flat_map(|action| s.try_apply_ui_action(table, viewer, action)),
803        );
804
805        // Handle queued commands
806        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                        // If any commit action is detected, release any remaining focus.
814                        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        // Total response
836        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}