egui_probe/
widget.rs

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        // TODO: Better approximation
64        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/// Widget for editing a value via `EguiProbe` trait.
183///
184/// For simple values it will show a probe UI for it.
185/// For complex values it will header with collapsible body.
186#[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    /// Creates a new `Probe` widget.
198    pub fn new(value: &'a mut T) -> Self {
199        Probe {
200            // id_salt: egui::Id::new(label.text()),
201            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    /// Show probbing UI to edit the value.
213    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                // let response = ui.interact(final_rect, child_ui.id(), egui::Sense::hover());
280                // response.widget_info(|| egui::WidgetInfo::new(egui::WidgetType::Other));
281
282                // response
283            })
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}