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 selection_changed: bool,
34}
35
36pub struct MacTableView<'a> {
63 id: Id,
64 columns: &'a [MacTableColumn<'a>],
65 row_count: usize,
66 selection: &'a mut BTreeSet<usize>,
67 uses_alternating_row_backgrounds: bool,
68 row_height: Option<f32>,
69 enabled: bool,
70 theme: Option<&'a MacTheme>,
71}
72
73impl<'a> MacTableView<'a> {
74 pub fn new(
76 id_source: impl Hash + Debug,
77 columns: &'a [MacTableColumn<'a>],
78 row_count: usize,
79 selection: &'a mut BTreeSet<usize>,
80 ) -> Self {
81 #[allow(deprecated)]
82 let id = Id::new(id_source);
83 Self {
84 id,
85 columns,
86 row_count,
87 selection,
88 uses_alternating_row_backgrounds: true,
89 row_height: None,
90 enabled: true,
91 theme: None,
92 }
93 }
94
95 pub fn uses_alternating_row_backgrounds(mut self, alternating: bool) -> Self {
97 self.uses_alternating_row_backgrounds = alternating;
98 self
99 }
100
101 pub fn row_height(mut self, height: f32) -> Self {
103 self.row_height = Some(height);
104 self
105 }
106
107 pub fn enabled(mut self, enabled: bool) -> Self {
109 self.enabled = enabled;
110 self
111 }
112
113 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
115 self.theme = Some(theme);
116 self
117 }
118
119 pub fn show(
121 self,
122 ui: &mut Ui,
123 mut cell_ui: impl FnMut(&mut Ui, usize, usize),
124 ) -> MacTableViewResponse {
125 let fallback_theme;
126 let theme = match self.theme {
127 Some(t) => t,
128 None => {
129 fallback_theme = MacTheme::from_ctx(ui.ctx());
130 &fallback_theme
131 }
132 };
133
134 let tm = theme.table_view();
135 let hdr_h = tm.header_height.points();
136 let row_h = self.row_height.unwrap_or_else(|| tm.row_height.points());
137 let pad_h = tm.cell_padding_horizontal.points();
138 let pad_v = tm.cell_padding_vertical.points();
139 let sep_w = tm.separator_width.points();
140 let outer_r = CornerRadius::same(tm.corner_radius.points() as u8);
141 let row_r = CornerRadius::same(tm.row_corner_radius.points() as u8);
142
143 let cols_w: f32 = self.columns.iter().map(|c| c.width).sum();
144 let total_w = cols_w.max(ui.available_width());
145 let total_h = hdr_h + (self.row_count as f32) * row_h;
146
147 let (raw_rect, mut response) =
148 ui.allocate_exact_size(Vec2::new(total_w, total_h), Sense::hover());
149 let rect = subpixel_snap_rect(raw_rect, ui.ctx().pixels_per_point());
150
151 let painter = ui.painter_at(rect);
152 painter.rect_filled(rect, outer_r, theme.control_background());
153
154 let header_rect = Rect::from_min_size(rect.min, Vec2::new(rect.width(), hdr_h));
155 let (clicked_header, toggled_sort) = header::render_table_header(
156 ui,
157 self.id,
158 theme,
159 header_rect,
160 self.columns,
161 self.enabled,
162 );
163
164 let anchor_id = self.id.with("selection_anchor");
165 let mut anchor = ui
166 .data(|d| d.get_temp::<usize>(anchor_id))
167 .or_else(|| self.selection.iter().next().copied())
168 .unwrap_or(0);
169
170 let mut clicked_row = None;
171 let mut selection_changed = false;
172
173 if self.enabled
175 && self.row_count > 0
176 && (!self.selection.is_empty() || response.has_focus())
177 {
178 if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowDown)) {
179 let cur = self.selection.iter().next_back().copied().unwrap_or(0);
180 let next = (cur + 1).min(self.row_count - 1);
181 self.selection.clear();
182 self.selection.insert(next);
183 anchor = next;
184 selection_changed = true;
185 } else if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowUp)) {
186 let cur = self.selection.iter().next().copied().unwrap_or(0);
187 let prev = cur.saturating_sub(1);
188 self.selection.clear();
189 self.selection.insert(prev);
190 anchor = prev;
191 selection_changed = true;
192 }
193 }
194
195 let body_top = header_rect.bottom();
196
197 for row_idx in 0..self.row_count {
198 let row_y = body_top + (row_idx as f32) * row_h;
199 let row_rect = Rect::from_min_size(
200 Pos2::new(rect.left(), row_y),
201 Vec2::new(rect.width(), row_h),
202 );
203 let row_id = self.id.with(("row", row_idx));
204 let row_sense = if self.enabled {
205 Sense::click()
206 } else {
207 Sense::hover()
208 };
209 let row_resp = ui.interact(row_rect, row_id, row_sense);
210
211 if row_resp.clicked() && self.enabled {
212 clicked_row = Some(row_idx);
213 ui.memory_mut(|m| m.request_focus(response.id));
214 let mods = ui.input(|i| {
215 let mut m = i.modifiers;
216 for ev in &i.events {
217 if let egui::Event::PointerButton { modifiers, .. } = ev {
218 m |= *modifiers;
219 }
220 }
221 m
222 });
223 if mods.shift {
224 let lo = anchor.min(row_idx);
225 let hi = anchor.max(row_idx);
226 self.selection.clear();
227 for idx in lo..=hi {
228 self.selection.insert(idx);
229 }
230 } else if mods.command || mods.ctrl {
231 if !self.selection.remove(&row_idx) {
232 self.selection.insert(row_idx);
233 }
234 anchor = row_idx;
235 } else {
236 self.selection.clear();
237 self.selection.insert(row_idx);
238 anchor = row_idx;
239 }
240 selection_changed = true;
241 }
242
243 let is_selected = self.selection.contains(&row_idx);
244 let row_painter = ui.painter_at(row_rect);
245 if is_selected {
246 let sel_bg = if self.enabled {
247 theme.selected_control()
248 } else {
249 theme.selected_control_inactive()
250 };
251 row_painter.rect_filled(row_rect, row_r, sel_bg);
252 } else if row_resp.hovered() && self.enabled {
253 row_painter.rect_filled(row_rect, row_r, theme.control());
254 } else if self.uses_alternating_row_backgrounds {
255 let alt_bg = theme.alternating_content_background(row_idx);
256 row_painter.rect_filled(row_rect, 0, alt_bg);
257 }
258
259 let mut col_x = row_rect.left();
260 for (col_idx, col) in self.columns.iter().enumerate() {
261 let cell_rect = Rect::from_min_max(
262 Pos2::new(col_x + pad_h, row_rect.top() + pad_v),
263 Pos2::new(
264 (col_x + col.width - pad_h).max(col_x + pad_h),
265 row_rect.bottom() - pad_v,
266 ),
267 );
268 let layout =
269 Layout::left_to_right(egui::Align::Center).with_main_align(col.alignment);
270 let mut child_ui = ui.new_child(
271 UiBuilder::new()
272 .max_rect(cell_rect)
273 .layout(layout)
274 .id_salt(("cell", row_idx, col_idx)),
275 );
276 child_ui.set_clip_rect(cell_rect.intersect(ui.clip_rect()));
277 if is_selected {
278 let fg = theme
279 .selected_control_background_style()
280 .primary_text_color(theme);
281 child_ui.visuals_mut().override_text_color = Some(fg);
282 }
283 cell_ui(&mut child_ui, row_idx, col_idx);
284 col_x += col.width;
285 }
286 }
287
288 ui.data_mut(|d| d.insert_temp(anchor_id, anchor));
289
290 ui.painter_at(rect).rect_stroke(
291 rect,
292 outer_r,
293 Stroke::new(sep_w, theme.separator()),
294 StrokeKind::Inside,
295 );
296
297 if selection_changed || clicked_header.is_some() {
298 response.mark_changed();
299 }
300
301 MacTableViewResponse {
302 response,
303 clicked_header,
304 toggled_sort,
305 clicked_row,
306 selection_changed,
307 }
308 }
309}