1use egui::{Color32, Margin, Response, Sense, Ui};
2
3use super::{
4 error::TableError,
5 filter::highlight::select_color,
6 header::{ColResponse, HeaderMenuAnchor, show_header_cell_contents},
7 layout::{CellInfo, HeaderCellInfo, PrefetchInfo, TableDelegate},
8 operations::{BorrowedRow, Row, TableProvider},
9 state::TableState,
10};
11
12pub type CustomCellCallback<'a> =
14 dyn FnMut(&mut Ui, &CellInfo, &dyn Row, Color32) -> Option<Response> + 'a;
15
16pub struct TableKitDelegate<'a> {
18 pub provider: &'a dyn TableProvider,
19 pub state: &'a mut TableState,
20 pub org_colors: &'a [[u8; 3]],
21 pub user_colors: &'a [[u8; 3]],
22 pub collected_responses: &'a mut Vec<ColResponse>,
23 pub halt_error: &'a mut Option<TableError>,
24 pub custom_cell_ui: Option<Box<CustomCellCallback<'a>>>,
25 pub item_clicked: &'a mut Option<usize>,
26 pub secondary_clicked: &'a mut Option<usize>,
27 pub is_new_pass: bool,
28
29 pub cell_padding: Margin,
31 pub highlight_entire_row: bool,
32 pub hovered_row: Option<u64>,
33 pub header_menu_anchor: HeaderMenuAnchor,
34 pub row_height: f32,
35 pub header_bg_color: Option<Color32>,
36
37 pub striped: bool,
38 pub striping_color: Option<Color32>,
39 pub hover_color: Option<Color32>,
40}
41
42impl<'a> TableKitDelegate<'a> {
43 pub fn new(
45 provider: &'a dyn TableProvider,
46 state: &'a mut TableState,
47 org_colors: &'a [[u8; 3]],
48 user_colors: &'a [[u8; 3]],
49 collected_responses: &'a mut Vec<ColResponse>,
50 halt_error: &'a mut Option<TableError>,
51 custom_cell_ui: Option<Box<CustomCellCallback<'a>>>,
52 item_clicked: &'a mut Option<usize>,
53 secondary_clicked: &'a mut Option<usize>,
54 ) -> Self {
55 Self {
56 provider,
57 state,
58 org_colors,
59 user_colors,
60 collected_responses,
61 halt_error,
62 custom_cell_ui,
63 item_clicked,
64 secondary_clicked,
65 is_new_pass: true,
66 cell_padding: Margin::symmetric(8, 0),
67 highlight_entire_row: true,
68 hovered_row: None,
69 header_menu_anchor: HeaderMenuAnchor::Cursor,
70 row_height: 16.0,
71 header_bg_color: None,
72 striped: false,
73 striping_color: None,
74 hover_color: None,
75 }
76 }
77}
78
79impl TableDelegate for TableKitDelegate<'_> {
80 fn default_row_height(&self) -> f32 {
81 self.row_height
82 }
83
84 fn prepare(&mut self, _info: &PrefetchInfo) {
85 if self.is_new_pass {
86 self.is_new_pass = false;
87 self.collected_responses.clear();
88 self.hovered_row = None;
89 }
90 }
91
92 fn header_cell_ui(&mut self, ui: &mut Ui, cell: &HeaderCellInfo) {
93 let col_idx = cell.col_range.start;
94 let title = self.provider.header(col_idx).unwrap_or_default();
95
96 let default_response = ColResponse::default();
97 let (previous_response, sort_up) = self
98 .state
99 .columns
100 .get(col_idx)
101 .map_or((&default_response, None), |col| {
102 (&col.response, col.sort_up)
103 });
104
105 let bg_color = self.header_bg_color.unwrap_or_else(|| {
107 if ui.visuals().dark_mode {
108 Color32::from_gray(38)
109 } else {
110 Color32::from_gray(230)
111 }
112 });
113 ui.visuals_mut().widgets.noninteractive.weak_bg_fill = bg_color;
114
115 ui.push_id(col_idx, |ui| {
117 match show_header_cell_contents(
118 ui,
119 title.as_ref(),
120 &sort_up,
121 previous_response,
122 self.org_colors,
123 self.user_colors,
124 self.header_menu_anchor,
125 ) {
126 Ok(response) => {
127 if col_idx >= self.collected_responses.len() {
128 self.collected_responses
129 .resize_with(col_idx + 1, ColResponse::default);
130 }
131 self.collected_responses[col_idx] = response;
132 }
133 Err(e) => {
134 *self.halt_error = Some(e);
135 }
136 }
137 });
138 }
139
140 fn cell_ui(&mut self, ui: &mut Ui, cell: &CellInfo) {
141 let current_visible_idx = cell.row_nr as usize;
142 let Some(&row_idx) = self.state.active_rows.get(current_visible_idx) else {
143 return;
144 };
145
146 let is_selected = self.state.selected_rows.contains(row_idx as u32);
147 let highlight = self.state.highlights.get_usize(row_idx);
148
149 let item_spacing = ui.spacing().item_spacing;
150 let cell_rect = ui.max_rect().expand2(0.5 * item_spacing);
151
152 let row_hovered = if self.highlight_entire_row {
154 cell.row_hovered
155 } else {
156 ui.rect_contains_pointer(cell_rect)
157 };
158
159 let highlight_rgb = if let Some(color_idx) = highlight {
161 select_color(color_idx, self.org_colors, self.user_colors).ok()
162 } else {
163 None
164 };
165
166 if is_selected {
168 ui.painter().rect_filled(
169 cell_rect,
170 egui::CornerRadius::ZERO,
171 ui.visuals().selection.bg_fill,
172 );
173 } else {
174 let base_color = if let Some(rgb) = highlight_rgb {
176 Some(Color32::from_rgb(rgb[0], rgb[1], rgb[2]))
177 } else if self.striped && cell.row_nr % 2 == 1 {
178 Some(
179 self.striping_color
180 .unwrap_or_else(|| ui.visuals().faint_bg_color),
181 )
182 } else {
183 None
184 };
185
186 if let Some(color) = base_color {
187 ui.painter()
188 .rect_filled(cell_rect, egui::CornerRadius::ZERO, color);
189 }
190
191 if row_hovered {
193 let hover_fill = self
194 .hover_color
195 .unwrap_or_else(|| ui.visuals().widgets.hovered.weak_bg_fill);
196
197 ui.painter()
198 .rect_filled(cell_rect, egui::CornerRadius::ZERO, hover_fill);
199 }
200 }
201
202 let is_tree_changed = if cell.col_nr == 0 && self.provider.is_tree() {
204 if let Some(hierarchy) = self.provider.row_hierarchy(self.state, row_idx) {
205 self.state.show_tree_cell(ui, row_idx, hierarchy)
206 } else {
207 false
208 }
209 } else {
210 false
211 };
212
213 if is_tree_changed {
214 ui.ctx().request_repaint();
215 }
216
217 let text_color = if is_selected {
219 ui.visuals().selection.stroke.color
220 } else {
221 ui.visuals().widgets.inactive.text_color()
222 };
223
224 let mut interact_rect = cell_rect;
230 let mut indent_strip_rect = None;
231 if cell.col_nr == 0
232 && self.provider.is_tree()
233 && let Some(hierarchy) = self.provider.row_hierarchy(self.state, row_idx)
234 && hierarchy.has_children
235 {
236 #[allow(clippy::cast_precision_loss)]
237 let indent_width = hierarchy.indent_level as f32 * 22.0;
238 let strip_end = (interact_rect.min.x + indent_width).min(interact_rect.max.x);
239 if strip_end > interact_rect.min.x {
240 indent_strip_rect = Some(egui::Rect::from_min_max(
241 interact_rect.min,
242 egui::pos2(strip_end, interact_rect.max.y),
243 ));
244 }
245 interact_rect.min.x = (strip_end + 26.0).min(interact_rect.max.x);
247 }
248
249 let mut handle_row_click = |response: &egui::Response| {
251 if response.clicked() {
252 *self.item_clicked = Some(row_idx);
253 self.state
254 .handle_row_selection(ui.input(|i| i.modifiers), row_idx);
255 }
256 if response.secondary_clicked() {
257 *self.secondary_clicked = Some(row_idx);
258 }
259 };
260
261 let response = ui.interact(
262 interact_rect,
263 ui.id().with((cell.row_nr, cell.col_nr)),
264 Sense::click(),
265 );
266 handle_row_click(&response);
267
268 if let Some(strip_rect) = indent_strip_rect {
269 let strip_response = ui.interact(
270 strip_rect,
271 ui.id().with((cell.row_nr, cell.col_nr, "indent_strip")),
272 Sense::click(),
273 );
274 handle_row_click(&strip_response);
275 }
276
277 let borrowed_row = BorrowedRow {
279 provider: self.provider,
280 row_index: row_idx,
281 };
282
283 let mut rendered = false;
284
285 let mut padding = self.cell_padding;
287 if cell.col_nr == 0 && self.provider.is_tree() {
288 padding.left = 0;
289 padding.right = 4;
290 }
291
292 egui::Frame::NONE.inner_margin(padding).show(ui, |ui| {
294 ui.push_id((cell.row_nr, cell.col_nr), |ui| {
295 let custom_renderer = self.custom_cell_ui.as_mut();
296
297 if let Some(renderer) = custom_renderer
298 && renderer(ui, cell, &borrowed_row as &dyn Row, text_color).is_some()
299 {
300 rendered = true;
301 }
302
303 if !rendered && let Some((val, _)) = borrowed_row.cell(cell.col_nr) {
304 ui.horizontal(|ui| {
305 ui.add(
306 egui::Label::new(egui::RichText::new(val.as_ref()).color(text_color))
307 .selectable(false)
308 .wrap_mode(if ui.is_sizing_pass() {
309 ui.wrap_mode()
310 } else {
311 egui::TextWrapMode::Truncate
312 }),
313 );
314 });
315 }
316 });
317 });
318 }
319}