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