1use core::hash::Hash;
2
3use egui::WidgetText;
4
5use crate::{EguiProbe, Style};
6
7#[derive(Clone, Copy)]
8struct ProbeHeaderState {
9 has_inner: bool,
10 open: bool,
11 body_height: f32,
12}
13
14struct ProbeHeader {
15 id: egui::Id,
16 state: ProbeHeaderState,
17 dirty: bool,
18 openness: f32,
19}
20
21impl ProbeHeader {
22 fn load(cx: &egui::Context, id: egui::Id) -> ProbeHeader {
23 let state = cx.data_mut(|d| d.get_temp(id)).unwrap_or(ProbeHeaderState {
24 has_inner: false,
25 open: false,
26 body_height: 0.0,
27 });
28
29 let openness = cx.animate_bool(id, state.open);
30
31 ProbeHeader {
32 id,
33 state,
34 dirty: false,
35 openness,
36 }
37 }
38
39 fn store(self, cx: &egui::Context) {
40 if self.dirty {
41 cx.data_mut(|d| d.insert_temp(self.id, self.state));
42 cx.request_repaint();
43 }
44 }
45
46 pub const fn has_inner(&self) -> bool {
47 self.state.has_inner
48 }
49
50 pub const fn set_has_inner(&mut self, has_inner: bool) {
51 if self.state.has_inner != has_inner {
52 self.state.has_inner = has_inner;
53 self.dirty = true;
54 }
55 }
56
57 const fn toggle(&mut self) {
58 self.state.open = !self.state.open;
59 self.dirty = true;
60 }
61
62 fn set_body_height(&mut self, height: f32) {
63 if (self.state.body_height - height).abs() > 0.001 {
65 self.state.body_height = height;
66 self.dirty = true;
67 }
68 }
69
70 fn body_shift(&self) -> f32 {
71 (1.0 - self.openness) * self.state.body_height
72 }
73
74 fn collapse_button(&mut self, ui: &mut egui::Ui) -> egui::Response {
75 let desired_size = ui.spacing().icon_width_inner;
76 let response =
77 ui.allocate_response(egui::vec2(desired_size, desired_size), egui::Sense::click());
78
79 if response.clicked() {
80 self.toggle();
81 }
82
83 egui::collapsing_header::paint_default_icon(ui, self.openness, &response);
84 response
85 }
86}
87
88#[derive(Clone, Copy)]
89struct ProbeLayoutState {
90 labels_width: f32,
91}
92
93pub struct ProbeLayout {
94 id: egui::Id,
95 state: ProbeLayoutState,
96 min_labels_width: f32,
97}
98
99impl ProbeLayout {
100 fn load(cx: &egui::Context, id: egui::Id) -> ProbeLayout {
101 let state = cx.data_mut(|d| *d.get_temp_mut_or(id, ProbeLayoutState { labels_width: 0.0 }));
102 ProbeLayout {
103 id,
104 state,
105 min_labels_width: 0.0,
106 }
107 }
108
109 fn store(mut self, cx: &egui::Context) {
110 if self.state.labels_width != self.min_labels_width {
111 self.state.labels_width = self.min_labels_width;
112 cx.data_mut(|d| d.insert_temp(self.id, self.state));
113 cx.request_repaint();
114 }
115 }
116
117 fn bump_labels_width(&mut self, width: f32) {
118 if self.min_labels_width < width {
119 self.min_labels_width = width;
120 }
121 }
122
123 pub fn inner_label_ui(
124 &mut self,
125 indent: usize,
126 id_salt: impl Hash,
127 ui: &mut egui::Ui,
128 add_content: impl FnOnce(&mut egui::Ui) -> egui::Response,
129 ) -> egui::Response {
130 let labels_width = self.state.labels_width;
131 let cursor = ui.cursor();
132
133 let max = egui::pos2(cursor.max.x.min(cursor.min.x + labels_width), cursor.max.y);
134 let min = egui::pos2(cursor.min.x, cursor.min.y);
135 let rect = egui::Rect::from_min_max(min, max);
136
137 let mut label_ui = ui.new_child(
138 egui::UiBuilder::new()
139 .max_rect(rect.intersect(ui.max_rect()))
140 .layout(*ui.layout())
141 .id_salt(id_salt),
142 );
143 label_ui.set_clip_rect(
144 ui.clip_rect()
145 .intersect(egui::Rect::everything_left_of(max.x)),
146 );
147
148 for _ in 0..indent {
149 label_ui.separator();
150 }
151
152 let label_response = add_content(&mut label_ui);
153 let mut final_rect = label_ui.min_rect();
154
155 self.bump_labels_width(final_rect.width());
156
157 final_rect.max.x = final_rect.min.x + labels_width;
158
159 ui.advance_cursor_after_rect(final_rect);
160 label_response
161 }
162
163 pub fn inner_value_ui(
164 &mut self,
165 id_salt: impl Hash,
166 ui: &mut egui::Ui,
167 add_content: impl FnOnce(&mut egui::Ui),
168 ) {
169 let mut value_ui = ui.new_child(
170 egui::UiBuilder::new()
171 .max_rect(ui.cursor().intersect(ui.max_rect()))
172 .layout(*ui.layout())
173 .id_salt(id_salt),
174 );
175
176 add_content(&mut value_ui);
177 let final_rect = value_ui.min_rect();
178 ui.advance_cursor_after_rect(final_rect);
179 }
180}
181
182#[must_use = "You should call .show()"]
187pub struct Probe<'a, T> {
188 header: Option<egui::WidgetText>,
189 style: Style,
190 value: &'a mut T,
191}
192
193impl<'a, T> Probe<'a, T>
194where
195 T: EguiProbe,
196{
197 pub fn new(value: &'a mut T) -> Self {
199 Probe {
200 header: None,
202 style: Style::default(),
203 value,
204 }
205 }
206
207 pub fn with_header(mut self, label: impl Into<WidgetText>) -> Self {
208 self.header = Some(label.into());
209 self
210 }
211
212 pub fn show(self, ui: &mut egui::Ui) -> egui::Response {
214 let mut changed = false;
215
216 let mut r = ui
217 .allocate_ui(ui.available_size(), |ui| {
218 let child_ui = &mut ui.new_child(
219 egui::UiBuilder::new()
220 .max_rect(ui.max_rect())
221 .layout(egui::Layout::top_down(egui::Align::Min)),
222 );
223
224 let id = child_ui.next_auto_id();
225
226 let mut layout = ProbeLayout::load(child_ui.ctx(), id);
227
228 if let Some(label) = self.header {
229 let mut header = show_header(
230 label,
231 self.value,
232 &mut layout,
233 0,
234 child_ui,
235 &self.style,
236 id,
237 &mut changed,
238 );
239
240 if header.openness > 0.0 {
241 show_table(
242 self.value,
243 &mut header,
244 &mut layout,
245 0,
246 child_ui,
247 &self.style,
248 id,
249 &mut changed,
250 );
251 } else {
252 let mut got_inner = false;
253
254 self.value.iterate_inner(ui, &mut |_, _, _| {
255 got_inner = true;
256 });
257
258 header.set_has_inner(got_inner);
259 }
260
261 header.store(child_ui.ctx());
262 } else {
263 show_table_direct(
264 self.value,
265 &mut layout,
266 0,
267 child_ui,
268 &self.style,
269 id,
270 &mut changed,
271 );
272 }
273
274 layout.store(child_ui.ctx());
275
276 let final_rect = child_ui.min_rect();
277 ui.advance_cursor_after_rect(final_rect);
278
279 })
284 .response;
285
286 if changed {
287 r.mark_changed();
288 }
289
290 r
291 }
292}
293
294#[allow(clippy::too_many_arguments)]
295fn show_header(
296 label: impl Into<WidgetText>,
297 value: &mut dyn EguiProbe,
298 layout: &mut ProbeLayout,
299 indent: usize,
300 ui: &mut egui::Ui,
301 style: &Style,
302 id_salt: impl Hash,
303 changed: &mut bool,
304) -> ProbeHeader {
305 let id = ui.make_persistent_id(id_salt);
306
307 let mut header = ProbeHeader::load(ui.ctx(), id);
308
309 ui.horizontal(|ui| {
310 let label_response = layout.inner_label_ui(indent, id.with("label"), ui, |ui| {
311 if header.has_inner() {
312 header.collapse_button(ui);
313 }
314 ui.label(label)
315 });
316
317 layout.inner_value_ui(id.with("value"), ui, |ui| {
318 *changed |= value
319 .probe(ui, style)
320 .labelled_by(label_response.id)
321 .changed();
322 });
323 });
324
325 header
326}
327
328#[allow(clippy::too_many_arguments)]
329fn show_table(
330 value: &mut dyn EguiProbe,
331 header: &mut ProbeHeader,
332 layout: &mut ProbeLayout,
333 indent: usize,
334 ui: &mut egui::Ui,
335 style: &Style,
336 id_salt: impl Hash,
337 changed: &mut bool,
338) {
339 let cursor = ui.cursor();
340
341 let table_rect = egui::Rect::from_min_max(
342 egui::pos2(cursor.min.x, cursor.min.y - header.body_shift()),
343 ui.max_rect().max,
344 );
345
346 let mut table_ui = ui.new_child(
347 egui::UiBuilder::new()
348 .max_rect(table_rect)
349 .layout(egui::Layout::top_down(egui::Align::Min))
350 .id_salt(id_salt),
351 );
352 table_ui.set_clip_rect(
353 ui.clip_rect()
354 .intersect(egui::Rect::everything_below(ui.min_rect().max.y)),
355 );
356
357 let mut got_inner = false;
358 let mut idx = 0;
359 value.iterate_inner(&mut table_ui, &mut |label, table_ui, value| {
360 got_inner = true;
361
362 let mut header = show_header(
363 label,
364 value,
365 layout,
366 indent + 1,
367 table_ui,
368 style,
369 idx,
370 changed,
371 );
372
373 if header.openness > 0.0 {
374 show_table(
375 value,
376 &mut header,
377 layout,
378 indent + 1,
379 table_ui,
380 style,
381 idx,
382 changed,
383 );
384 } else {
385 let mut got_inner = false;
386
387 value.iterate_inner(ui, &mut |_, _, _| {
388 got_inner = true;
389 });
390
391 header.set_has_inner(got_inner);
392 }
393
394 header.store(table_ui.ctx());
395
396 idx += 1;
397 });
398
399 header.set_has_inner(got_inner);
400
401 let final_table_rect = table_ui.min_rect();
402
403 ui.advance_cursor_after_rect(final_table_rect);
404 let table_height = ui.cursor().min.y - table_rect.min.y;
405 header.set_body_height(table_height);
406}
407
408fn show_table_direct(
409 value: &mut dyn EguiProbe,
410 layout: &mut ProbeLayout,
411 indent: usize,
412 ui: &mut egui::Ui,
413 style: &Style,
414 id_salt: impl Hash,
415 changed: &mut bool,
416) {
417 let cursor = ui.cursor();
418
419 let table_rect =
420 egui::Rect::from_min_max(egui::pos2(cursor.min.x, cursor.min.y), ui.max_rect().max);
421
422 let mut table_ui = ui.new_child(
423 egui::UiBuilder::new()
424 .max_rect(table_rect)
425 .layout(egui::Layout::top_down(egui::Align::Min))
426 .id_salt(id_salt),
427 );
428 table_ui.set_clip_rect(
429 ui.clip_rect()
430 .intersect(egui::Rect::everything_below(ui.min_rect().max.y)),
431 );
432
433 let mut got_inner = false;
434 let mut idx = 0;
435 value.iterate_inner(&mut table_ui, &mut |label, table_ui, value| {
436 got_inner = true;
437
438 let mut header = show_header(
439 label,
440 value,
441 layout,
442 indent + 1,
443 table_ui,
444 style,
445 idx,
446 changed,
447 );
448
449 if header.openness > 0.0 {
450 show_table(
451 value,
452 &mut header,
453 layout,
454 indent + 1,
455 table_ui,
456 style,
457 idx,
458 changed,
459 );
460 } else {
461 let mut got_inner = false;
462
463 value.iterate_inner(ui, &mut |_, _, _| {
464 got_inner = true;
465 });
466
467 header.set_has_inner(got_inner);
468 }
469
470 header.store(table_ui.ctx());
471
472 idx += 1;
473 });
474
475 let final_table_rect = table_ui.min_rect();
476 ui.advance_cursor_after_rect(final_table_rect);
477}