Skip to main content

egui_probe/
widget.rs

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