egui_macview/controls/table_view/
mod.rs1pub mod header;
8
9pub use header::{MacSortDirection, MacTableColumn};
10
11use crate::layout::subpixel_snap_rect;
12use crate::theme::MacTheme;
13use egui::{
14 CornerRadius, Id, Key, Layout, Modifiers, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui,
15 UiBuilder, Vec2,
16};
17use std::collections::BTreeSet;
18use std::fmt::Debug;
19use std::hash::Hash;
20
21#[derive(Debug, Clone)]
23pub struct MacTableViewResponse {
24 pub response: Response,
26 pub clicked_header: Option<usize>,
28 pub toggled_sort: Option<(usize, MacSortDirection)>,
30 pub clicked_row: Option<usize>,
32 pub double_clicked_row: Option<usize>,
34 pub selection_changed: bool,
36}
37
38pub type MacTableViewOutput = MacTableViewResponse;
40
41pub struct MacTableView<'a> {
68 id: Id,
69 columns: &'a [MacTableColumn<'a>],
70 row_count: usize,
71 selection: &'a mut BTreeSet<usize>,
72 uses_alternating_row_backgrounds: bool,
73 row_height: Option<f32>,
74 enabled: bool,
75 theme: Option<&'a MacTheme>,
76}
77
78impl<'a> MacTableView<'a> {
79 pub fn new(
81 id_source: impl Hash + Debug,
82 columns: &'a [MacTableColumn<'a>],
83 row_count: usize,
84 selection: &'a mut BTreeSet<usize>,
85 ) -> Self {
86 #[allow(deprecated)]
87 let id = Id::new(id_source);
88 Self {
89 id,
90 columns,
91 row_count,
92 selection,
93 uses_alternating_row_backgrounds: true,
94 row_height: None,
95 enabled: true,
96 theme: None,
97 }
98 }
99
100 pub fn uses_alternating_row_backgrounds(mut self, alternating: bool) -> Self {
102 self.uses_alternating_row_backgrounds = alternating;
103 self
104 }
105
106 pub fn row_height(mut self, height: f32) -> Self {
108 self.row_height = Some(height);
109 self
110 }
111
112 pub fn enabled(mut self, enabled: bool) -> Self {
114 self.enabled = enabled;
115 self
116 }
117
118 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
120 self.theme = Some(theme);
121 self
122 }
123
124 pub fn show(
126 self,
127 ui: &mut Ui,
128 mut cell_ui: impl FnMut(&mut Ui, usize, usize),
129 ) -> MacTableViewResponse {
130 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
131 let theme: &MacTheme = &theme_cow;
132
133 let tm = theme.table_view();
134 let hdr_h = tm.header_height.points();
135 let row_h = self.row_height.unwrap_or_else(|| tm.row_height.points());
136 let pad_h = tm.cell_padding_horizontal.points();
137 let pad_v = tm.cell_padding_vertical.points();
138 let sep_w = tm.separator_width.points();
139 let outer_r = CornerRadius::same(tm.corner_radius.points() as u8);
140 let row_r = CornerRadius::same(tm.row_corner_radius.points() as u8);
141
142 let cols_w: f32 = self.columns.iter().map(|c| c.width).sum();
143 let total_w = cols_w.max(ui.available_width());
144 let total_h = hdr_h + (self.row_count as f32) * row_h;
145
146 let (raw_rect, _) = ui.allocate_exact_size(Vec2::new(total_w, total_h), Sense::hover());
147 let mut response = ui.interact(raw_rect, self.id, Sense::focusable_noninteractive());
148 let rect = subpixel_snap_rect(raw_rect, ui.ctx().pixels_per_point());
149
150 let painter = ui.painter_at(rect);
151 painter.rect_filled(rect, outer_r, theme.control_background());
152
153 let header_rect = Rect::from_min_size(rect.min, Vec2::new(rect.width(), hdr_h));
154 let (clicked_header, toggled_sort) = header::render_table_header(
155 ui,
156 self.id,
157 theme,
158 header_rect,
159 self.columns,
160 self.enabled,
161 );
162
163 let mut clicked_row = None;
164 let mut double_clicked_row = None;
165 let mut selection_changed = false;
166 let prev_sel_len = self.selection.len();
167 self.selection.retain(|&idx| idx < self.row_count);
168 if self.selection.len() != prev_sel_len {
169 selection_changed = true;
170 }
171
172 let anchor_id = self.id.with("selection_anchor");
173 let mut anchor = ui
174 .data(|d| d.get_temp::<usize>(anchor_id))
175 .filter(|&a| a < self.row_count)
176 .or_else(|| self.selection.iter().next().copied())
177 .unwrap_or(0);
178
179 let is_focused_or_hovered = response.has_focus()
182 || ui.memory(|m| m.has_focus(self.id))
183 || (ui.memory(|m| m.focused().is_none()) && ui.rect_contains_pointer(rect));
184 if self.enabled && self.row_count > 0 {
185 if is_focused_or_hovered
186 && ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowDown))
187 {
188 let cur = self.selection.iter().next_back().copied().unwrap_or(0);
189 let next = (cur + 1).min(self.row_count - 1);
190 self.selection.clear();
191 self.selection.insert(next);
192 anchor = next;
193 selection_changed = true;
194 } else if is_focused_or_hovered
195 && ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowUp))
196 {
197 let cur = self.selection.iter().next().copied().unwrap_or(0);
198 let prev = cur.saturating_sub(1);
199 self.selection.clear();
200 self.selection.insert(prev);
201 anchor = prev;
202 selection_changed = true;
203 } else if (is_focused_or_hovered || !self.selection.is_empty())
204 && (ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter))
205 || ui.input_mut(|i| i.consume_key(Modifiers::COMMAND, Key::ArrowDown))
206 || ui.input_mut(|i| i.consume_key(Modifiers::CTRL, Key::ArrowDown)))
207 {
208 if let Some(&first_sel) = self.selection.iter().next() {
209 double_clicked_row = Some(first_sel);
210 }
211 }
212 }
213
214 let body_top = header_rect.bottom();
215 let clip_rect = ui.clip_rect();
216
217 let (first_row, last_row) = if row_h > 0.0 {
218 let start = ((clip_rect.top() - body_top) / row_h).floor().max(0.0) as usize;
219 let end = (((clip_rect.bottom() - body_top) / row_h).ceil().max(0.0) as usize)
220 .min(self.row_count);
221 let start = start.saturating_sub(1).min(self.row_count);
223 let end = (end + 1).clamp(start, self.row_count);
224 (start, end)
225 } else {
226 (0, self.row_count)
227 };
228
229 for row_idx in first_row..last_row {
230 let row_y = body_top + (row_idx as f32) * row_h;
231 let row_rect = Rect::from_min_size(
232 Pos2::new(rect.left(), row_y),
233 Vec2::new(rect.width(), row_h),
234 );
235 let row_id = self.id.with(("row", row_idx));
236 let row_sense = if self.enabled {
237 Sense::click()
238 } else {
239 Sense::hover()
240 };
241 let row_resp = ui.interact(row_rect, row_id, row_sense);
242
243 if row_resp.double_clicked() && self.enabled {
244 double_clicked_row = Some(row_idx);
245 }
246
247 if row_resp.clicked() && self.enabled {
248 clicked_row = Some(row_idx);
249 ui.memory_mut(|m| m.request_focus(response.id));
250 let mods = ui.input(|i| {
251 let mut m = i.modifiers;
252 for ev in &i.events {
253 if let egui::Event::PointerButton { modifiers, .. } = ev {
254 m |= *modifiers;
255 }
256 }
257 m
258 });
259 if mods.shift {
260 let lo = anchor.min(row_idx);
261 let hi = anchor.max(row_idx);
262 self.selection.clear();
263 for idx in lo..=hi {
264 self.selection.insert(idx);
265 }
266 } else if mods.command || mods.ctrl {
267 if !self.selection.remove(&row_idx) {
268 self.selection.insert(row_idx);
269 }
270 anchor = row_idx;
271 } else {
272 self.selection.clear();
273 self.selection.insert(row_idx);
274 anchor = row_idx;
275 }
276 selection_changed = true;
277 }
278
279 let is_selected = self.selection.contains(&row_idx);
280 let row_painter = ui.painter_at(row_rect);
281 if is_selected {
282 let sel_bg = if self.enabled {
283 theme.selected_control()
284 } else {
285 theme.selected_control_inactive()
286 };
287 row_painter.rect_filled(row_rect, row_r, sel_bg);
288 } else if row_resp.hovered() && self.enabled {
289 row_painter.rect_filled(row_rect, row_r, theme.control());
290 } else if self.uses_alternating_row_backgrounds {
291 let alt_bg = theme.alternating_content_background(row_idx);
292 row_painter.rect_filled(row_rect, 0, alt_bg);
293 }
294
295 let mut col_x = row_rect.left();
296 for (col_idx, col) in self.columns.iter().enumerate() {
297 let cell_rect = Rect::from_min_max(
298 Pos2::new(col_x + pad_h, row_rect.top() + pad_v),
299 Pos2::new(
300 (col_x + col.width - pad_h).max(col_x + pad_h),
301 row_rect.bottom() - pad_v,
302 ),
303 );
304 let layout =
305 Layout::left_to_right(egui::Align::Center).with_main_align(col.alignment);
306 let mut child_ui = ui.new_child(
307 UiBuilder::new()
308 .max_rect(cell_rect)
309 .layout(layout)
310 .id_salt(("cell", row_idx, col_idx)),
311 );
312 child_ui.set_clip_rect(cell_rect.intersect(ui.clip_rect()));
313 if is_selected {
314 let fg = theme
315 .selected_control_background_style()
316 .primary_text_color(theme);
317 child_ui.visuals_mut().override_text_color = Some(fg);
318 }
319 cell_ui(&mut child_ui, row_idx, col_idx);
320 col_x += col.width;
321 }
322 }
323
324 let prev_anchor = ui.data(|d| d.get_temp::<usize>(anchor_id));
325 if prev_anchor != Some(anchor) {
326 ui.data_mut(|d| d.insert_temp(anchor_id, anchor));
327 }
328
329 ui.painter_at(rect).rect_stroke(
330 rect,
331 outer_r,
332 Stroke::new(sep_w, theme.separator()),
333 StrokeKind::Inside,
334 );
335
336 if selection_changed || clicked_header.is_some() {
337 response.mark_changed();
338 }
339
340 MacTableViewResponse {
341 response,
342 clicked_header,
343 toggled_sort,
344 clicked_row,
345 double_clicked_row,
346 selection_changed,
347 }
348 }
349}