1use std::mem::{replace, take};
2
3use egui::{
4 Align, Color32, Event, Label, Layout, PointerButton, PopupAnchor, Rect, Response, RichText,
5 Sense, Stroke, StrokeKind, Tooltip, Vec2b,
6};
7use egui_extras::Column;
8use tap::prelude::{Pipe, Tap};
9
10use crate::{
11 DataTable, UiAction,
12 viewer::{EmptyRowCreateContext, RowViewer},
13};
14
15use self::state::*;
16
17use egui::scroll_area::ScrollBarVisibility;
18use format as f;
19use std::sync::Arc;
20
21mod body;
22pub(crate) mod state;
23mod tsv;
24
25#[derive(Default, Debug, Clone, Copy)]
30#[non_exhaustive]
31pub struct Style {
32 pub bg_selected_cell: Option<egui::Color32>,
34
35 pub bg_selected_highlight_cell: Option<egui::Color32>,
37
38 pub fg_selected_highlight_cell: Option<egui::Color32>,
40
41 pub fg_drag_selection: Option<egui::Color32>,
43
44 pub max_undo_history: usize,
49
50 pub table_row_height: Option<f32>,
52
53 pub single_click_edit_mode: bool,
56
57 pub cell_align: egui::Align,
59
60 pub focused_row_stroke: Option<egui::Color32>,
63
64 pub auto_shrink: Vec2b,
66
67 pub scroll_bar_visibility: ScrollBarVisibility,
69}
70
71pub struct Renderer<'a, R, V: RowViewer<R>> {
74 table: &'a mut DataTable<R>,
75 viewer: &'a mut V,
76 state: Option<Box<UiState<R>>>,
77 style: Style,
78 translator: Arc<dyn Translator>,
79}
80
81impl<R, V: RowViewer<R>> egui::Widget for Renderer<'_, R, V> {
82 fn ui(self, ui: &mut egui::Ui) -> Response {
83 self.show(ui)
84 }
85}
86
87impl<'a, R, V: RowViewer<R>> Renderer<'a, R, V> {
88 pub fn new(table: &'a mut DataTable<R>, viewer: &'a mut V) -> Self {
89 if table.rows.is_empty() && viewer.allow_row_insertions() {
90 table.push(viewer.new_empty_row_for(EmptyRowCreateContext::InsertNewLine));
91 }
92
93 Self {
94 state: Some(table.ui.take().unwrap_or_default().tap_mut(|state| {
95 state.validate_identity(viewer);
96 })),
97 table,
98 viewer,
99 style: Default::default(),
100 translator: Arc::new(EnglishTranslator::default()),
101 }
102 }
103
104 pub fn with_style(mut self, style: Style) -> Self {
105 self.style = style;
106 self
107 }
108
109 pub fn with_style_modify(mut self, f: impl FnOnce(&mut Style)) -> Self {
110 f(&mut self.style);
111 self
112 }
113
114 pub fn with_table_row_height(mut self, height: f32) -> Self {
115 self.style.table_row_height = Some(height);
116 self
117 }
118
119 pub fn with_max_undo_history(mut self, max_undo_history: usize) -> Self {
120 self.style.max_undo_history = max_undo_history;
121 self
122 }
123
124 #[cfg(not(doctest))]
144 pub fn with_translator(mut self, translator: Arc<dyn Translator>) -> Self {
145 self.translator = translator;
146 self
147 }
148
149 pub fn show(self, ui: &mut egui::Ui) -> Response {
150 egui::ScrollArea::horizontal()
151 .show(ui, |ui| self.impl_show(ui))
152 .inner
153 }
154
155 fn impl_show(mut self, ui: &mut egui::Ui) -> Response {
156 let ctx = &ui.ctx().clone();
157 let ui_id = ui.id();
158 let style = ui.style().clone();
159 let painter = ui.painter().clone();
160 let visual = &style.visuals;
161 let viewer = &mut *self.viewer;
162 let s = self.state.as_mut().unwrap();
163 let mut resp_total = None::<Response>;
164 let mut resp_ret = None::<Response>;
165 let mut commands = Vec::<Command<R>>::new();
166 let ui_layer_id = ui.layer_id();
167
168 let green = if visual.window_fill.g() > 128 {
172 Color32::DARK_GREEN
173 } else {
174 Color32::GREEN
175 };
176
177 let mut builder = egui_extras::TableBuilder::new(ui).column(Column::auto());
178
179 let iter_vis_cols_with_flag = s
180 .vis_cols()
181 .iter()
182 .enumerate()
183 .map(|(index, column)| (column, index + 1 == s.vis_cols().len()));
184
185 for (column, flag) in iter_vis_cols_with_flag {
186 builder = builder.column(viewer.column_render_config(column.0, flag));
187 }
188
189 if replace(&mut s.cci_want_move_scroll, false) {
190 let interact_row = s.interactive_cell().0;
191 builder = builder.scroll_to_row(interact_row.0, None);
192 }
193
194 builder
195 .columns(Column::auto(), s.num_columns() - s.vis_cols().len())
196 .drag_to_scroll(false) .striped(true)
198 .cell_layout(egui::Layout::default().with_cross_align(self.style.cell_align))
199 .max_scroll_height(f32::MAX)
200 .auto_shrink(self.style.auto_shrink)
201 .scroll_bar_visibility(self.style.scroll_bar_visibility)
202 .sense(Sense::click_and_drag().tap_mut(|s| s.set(Sense::FOCUSABLE, true)))
203 .header(20., |mut h| {
204 h.col(|_ui| {
205 });
207
208 let has_any_hidden_col = s.vis_cols().len() != s.num_columns();
209
210 for (vis_col, &col) in s.vis_cols().iter().enumerate() {
211 let vis_col = VisColumnPos(vis_col);
212 let mut painter = None;
213 let (col_rect, resp) = h.col(|ui| {
214 egui::Sides::new().show(
215 ui,
216 |ui| {
217 ui.add(Label::new(viewer.column_name(col.0)).selectable(false));
218 },
219 |ui| {
220 if let Some(pos) = s.sort().iter().position(|(c, ..)| c == &col) {
221 let is_asc = s.sort()[pos].1.0 as usize;
222
223 ui.colored_label(
224 [green, Color32::RED][is_asc],
225 RichText::new(
226 format!("{}{}", ["↘", "↗"][is_asc], pos + 1,),
227 )
228 .monospace(),
229 );
230 } else {
231 let max_sort_indicator_width =
233 (s.num_columns() + 1).to_string().len() + 1;
234 ui.add(
237 Label::new(
238 RichText::new(" ".repeat(max_sort_indicator_width))
239 .monospace(),
240 )
241 .selectable(false),
242 );
243 }
244 },
245 );
246
247 painter = Some(ui.painter().clone());
248 });
249
250 resp.dnd_set_drag_payload(vis_col);
252
253 if resp.dragged() {
254 Tooltip::always_open(
255 ctx.clone(),
256 ui_layer_id,
257 "_EGUI_DATATABLE__COLUMN_MOVE__".into(),
258 PopupAnchor::Pointer,
259 )
260 .gap(12.0)
261 .show(|ui| {
262 let colum_name = viewer.column_name(col.0);
263 ui.label(colum_name);
264 });
265 }
266
267 if resp.hovered() && viewer.is_sortable_column(col.0) {
268 if let Some(p) = &painter {
269 p.rect_filled(
270 col_rect,
271 egui::CornerRadius::ZERO,
272 visual.selection.bg_fill.gamma_multiply(0.2),
273 );
274 }
275 }
276
277 if viewer.is_sortable_column(col.0) && resp.clicked_by(PointerButton::Primary) {
278 let mut sort = s.sort().to_owned();
279 match sort.iter_mut().find(|(c, ..)| c == &col) {
280 Some((_, asc)) => match asc.0 {
281 true => asc.0 = false,
282 false => sort.retain(|(c, ..)| c != &col),
283 },
284 None => {
285 sort.push((col, IsAscending(true)));
286 }
287 }
288
289 commands.push(Command::SetColumnSort(sort));
290 }
291
292 if resp.dnd_hover_payload::<VisColumnPos>().is_some() {
293 if let Some(p) = &painter {
294 p.rect_filled(
295 col_rect,
296 egui::CornerRadius::ZERO,
297 visual.selection.bg_fill.gamma_multiply(0.5),
298 );
299 }
300 }
301
302 if let Some(payload) = resp.dnd_release_payload::<VisColumnPos>() {
303 commands.push(Command::CcReorderColumn {
304 from: *payload,
305 to: vis_col
306 .0
307 .pipe(|v| v + (payload.0 < v) as usize)
308 .pipe(VisColumnPos),
309 })
310 }
311
312 resp.context_menu(|ui| {
313 if ui
314 .button(self.translator.translate("context-menu-hide"))
315 .clicked()
316 {
317 commands.push(Command::CcHideColumn(col));
318 }
319
320 if !s.sort().is_empty()
321 && ui
322 .button(self.translator.translate("context-menu-clear-sort"))
323 .clicked()
324 {
325 commands.push(Command::SetColumnSort(Vec::new()));
326 }
327
328 if has_any_hidden_col {
329 ui.separator();
330 ui.label(self.translator.translate("context-menu-hidden"));
331
332 for col in (0..s.num_columns()).map(ColumnIdx) {
333 if !s.vis_cols().contains(&col)
334 && ui.button(viewer.column_name(col.0)).clicked()
335 {
336 commands.push(Command::CcShowColumn {
337 what: col,
338 at: vis_col,
339 });
340 }
341 }
342 }
343 });
344 }
345
346 resp_total = Some(h.response());
348 })
349 .tap_mut(|table| {
350 table.ui_mut().separator();
351 })
352 .body(|body: egui_extras::TableBody<'_>| {
353 resp_ret = Some(
354 self.impl_show_body(body, painter, commands, ctx, &style, ui_id, resp_total),
355 );
356 });
357
358 resp_ret.unwrap_or_else(|| ui.label("??"))
359 }
360}
361
362impl<R, V: RowViewer<R>> Drop for Renderer<'_, R, V> {
363 fn drop(&mut self) {
364 self.table.ui = self.state.take();
365 }
366}
367
368pub trait Translator {
371 fn translate(&self, key: &str) -> String;
375}
376
377#[derive(Default)]
378pub struct EnglishTranslator {}
379
380impl Translator for EnglishTranslator {
381 fn translate(&self, key: &str) -> String {
382 match key {
383 "context-menu-selection-copy" => "Selection: Copy",
385 "context-menu-selection-cut" => "Selection: Cut",
386 "context-menu-selection-clear" => "Selection: Clear",
387 "context-menu-selection-fill" => "Selection: Fill",
388 "context-menu-clipboard-paste" => "Clipboard: Paste",
389 "context-menu-clipboard-insert" => "Clipboard: Insert",
390 "context-menu-row-duplicate" => "Row: Duplicate",
391 "context-menu-row-delete" => "Row: Delete",
392 "context-menu-undo" => "Undo",
393 "context-menu-redo" => "Redo",
394
395 "context-menu-hide" => "Hide",
397 "context-menu-hidden" => "Hidden",
398 "context-menu-clear-sort" => "Clear sort",
399 _ => key,
400 }
401 .to_string()
402 }
403}