Skip to main content

facet_egui/
probe.rs

1#![allow(clippy::too_many_arguments)]
2
3use alloc::{
4    borrow::{Cow, ToOwned},
5    string::{String, ToString},
6};
7use core::ops::DerefMut;
8
9use derive_more::{Deref, DerefMut as DeriveDerefMut, From};
10use egui::{Align, Checkbox, Color32, Id, Layout, Response, TextEdit, Ui, UiBuilder, WidgetText};
11use facet::{Def, Facet, ListDef, MapDef, OptionDef, ScalarType, SetDef, Type, UserType};
12use facet_reflect::{
13    HasFields, Partial, Peek, PeekEnum, PeekListLike, PeekMap, PeekOption, PeekPointer, PeekSet,
14    PeekStruct, PeekTuple, Poke, PokeEnum, PokeList, PokeStruct,
15};
16
17use crate::{
18    MaybeMut,
19    layout::{ProbeHeader, ProbeLayout, swap_probe_header_state},
20    maybe_mut::{Guard, MakeLockErrorKind},
21};
22
23/// Returns `true` if the given attributes slice contains `Attr::Skip`.
24fn has_egui_skip(attributes: &[facet::Attr]) -> bool {
25    attributes
26        .iter()
27        .any(|a| matches!((a.ns, a.key), (Some("egui"), "skip")))
28}
29
30/// Returns `true` if the given attributes slice contains `Attr::AsDisplay`.
31fn has_egui_as_display(attributes: &[facet::Attr]) -> bool {
32    attributes
33        .iter()
34        .any(|a| matches!((a.ns, a.key), (Some("egui"), "as_display")))
35}
36
37/// Returns the `Attr::Rename` value from the attributes, if present.
38fn egui_rename(attributes: &[facet::Attr]) -> Option<&'static str> {
39    attributes.iter().find_map(|a| {
40        if matches!((a.ns, a.key), (Some("egui"), "rename")) {
41            a.get_as::<&'static str>().copied()
42        } else {
43            None
44        }
45    })
46}
47
48/// Returns the display name for a field: uses `egui::rename` if present,
49/// otherwise falls back to `effective_name()`.
50fn field_display_name(field: &facet::Field) -> String {
51    egui_rename(field.attributes)
52        .unwrap_or_else(|| field.effective_name())
53        .to_owned()
54}
55
56/// Returns the display name for a shape: uses `egui::rename` if present,
57/// otherwise falls back to `effective_name()`.
58fn shape_display_name(shape: &facet::Shape) -> &str {
59    egui_rename(shape.attributes).unwrap_or_else(|| shape.effective_name())
60}
61
62fn text_line_count(s: &str) -> usize {
63    1 + s.chars().filter(|&c| c == '\n').count()
64}
65
66fn shift_enter_pressed(ui: &Ui) -> bool {
67    ui.input(|i| {
68        i.events.iter().any(|event| {
69            matches!(
70                event,
71                egui::Event::Key {
72                    key: egui::Key::Enter,
73                    pressed: true,
74                    modifiers,
75                    ..
76                } if modifiers.shift
77            )
78        })
79    })
80}
81
82fn should_render_as_display(shape: &facet::Shape, attributes: &[facet::Attr]) -> bool {
83    has_egui_as_display(attributes) || has_egui_as_display(shape.attributes)
84}
85
86/// The container that stores a [`MaybeMut`] of the type `T` that should be shown
87/// in the [`Ui`](egui::Ui)
88#[must_use = "use [`FacetProbe::show`] to display the probe in the [`Ui`]"]
89#[derive(Deref, DeriveDerefMut)]
90pub struct FacetProbe<'mem, 'facet> {
91    header: Option<WidgetText>,
92    id: Option<Id>,
93    read_only: bool,
94    expand_all: bool,
95    /// SAFETY: if used, there is a high chance what you do is unsound.
96    ///
97    /// If you use this, you will have to manually ensure your variances are
98    /// okay for use with reborrowing. Normally, this is determined by facet but
99    /// there may be cases where a type does not implement Facet or has opaque
100    /// parts that would (if used with facet) be ok.
101    force_reborrow: bool,
102    #[deref]
103    #[deref_mut]
104    inner: MaybeMut<'mem, 'facet>,
105}
106
107#[derive(Debug, From)]
108pub enum MaybeMutT<'mem, T> {
109    Not(&'mem T),
110    Mut(&'mem mut T),
111}
112
113impl<'mem, 'facet> FacetProbe<'mem, 'facet> {
114    pub fn readonly(self, readonly: bool) -> Self {
115        Self {
116            read_only: readonly,
117            ..self
118        }
119    }
120
121    pub fn expand_all(self, expand_all: bool) -> Self {
122        Self {
123            expand_all,
124            ..self
125        }
126    }
127
128    pub fn with_header(mut self, label: impl Into<WidgetText>) -> Self {
129        self.header = Some(label.into());
130        self
131    }
132
133    /// Set a stable egui id source for this probe.
134    ///
135    /// Use this when the probe can move in the UI hierarchy (e.g. draggable tabs),
136    /// so collapse/expand state remains stable.
137    pub fn with_id_source(mut self, id_source: impl core::hash::Hash) -> Self {
138        self.id = Some(Id::new(id_source));
139        self
140    }
141
142    /// # Safety
143    ///
144    /// If used, there is a high chance what you do is unsound.
145    ///
146    /// If you use this, you will have to manually ensure your variances are
147    /// okay for use with reborrowing. Normally, this is determined by facet but
148    /// there may be cases where a type does not implement Facet or has opaque
149    /// parts that would (if used with facet) be ok.
150    pub unsafe fn force_reborrow(self) -> Self {
151        Self {
152            force_reborrow: true,
153            ..self
154        }
155    }
156
157    pub fn new_peek(value: Peek<'mem, 'facet>) -> Self {
158        Self {
159            header: None,
160            id: None,
161            read_only: true,
162            expand_all: false,
163            force_reborrow: false,
164            inner: MaybeMut::Not(value),
165        }
166    }
167
168    pub fn new_poke(value: Poke<'mem, 'facet>) -> Self {
169        Self {
170            header: None,
171            id: None,
172            read_only: false,
173            expand_all: false,
174            force_reborrow: false,
175            inner: MaybeMut::Mut(value),
176        }
177    }
178
179    pub fn new<T>(value: impl Into<MaybeMutT<'mem, T>>) -> Self
180    where
181        T: Facet<'facet> + 'mem,
182    {
183        let v: MaybeMutT<'mem, T> = value.into();
184        let inner: MaybeMut = match v {
185            MaybeMutT::Mut(v) => Poke::new(v).into(),
186            MaybeMutT::Not(v) => Peek::new(v).into(),
187        };
188        Self {
189            header: None,
190            id: None,
191            read_only: false,
192            expand_all: false,
193            force_reborrow: false,
194            inner,
195        }
196    }
197
198    pub fn show<'lock>(self, ui: &mut Ui) -> Response
199    where
200        'mem: 'lock,
201    {
202        // Container-level skip: hide the entire probe
203        if has_egui_skip(self.shape().attributes) {
204            return ui.label("");
205        }
206
207        let mut changed = false;
208
209        let shape_attrs = self.shape().attributes;
210        let readonly = shape_attrs
211            .iter()
212            .any(|a| matches!((a.ns, a.key), (Some("egui"), "readonly")))
213            || self.read_only;
214
215        // Check for expand_all attribute (or use self.expand_all)
216        let expand_all = self.expand_all
217            || shape_attrs
218                .iter()
219                .any(|a| matches!((a.ns, a.key), (Some("egui"), "expand_all")));
220
221        let mut guard: Guard<'lock, 'facet> = if readonly {
222            let Ok(read) = self.inner.read() else {
223                return ui.colored_label(Color32::RED, "Read Failure");
224            };
225            read
226        } else {
227            match self.inner.write() {
228                Ok(write) => write,
229                // fallback to readonly
230                Err(e) if matches!(e.kind, MakeLockErrorKind::NotLockable) => {
231                    let Ok(read) = MaybeMut::Not(e.unchanged).read() else {
232                        return ui.colored_label(Color32::RED, "Fallback Read Failure");
233                    };
234                    read
235                }
236                Err(e) if matches!(e.kind, MakeLockErrorKind::LockFailure) => {
237                    return ui.colored_label(Color32::RED, "Lock Failure");
238                }
239                Err(e) => {
240                    return ui.colored_label(Color32::RED, alloc::format!("Error: {e}"));
241                }
242            }
243        };
244
245        let maybe_mut = guard.deref_mut();
246        // Anchor persistent widget state to a probe root id that does not depend
247        // on current Ui ancestry, so tab moves don't reset collapsed sections.
248        let ptr_salt = maybe_mut.as_peek().data().as_byte_ptr() as usize;
249        let probe_id = self
250            .id
251            .unwrap_or_else(|| Id::new(("facet_egui::probe", ptr_salt)));
252        let mut r = ui
253            .push_id(probe_id, |ui| {
254                let child_ui = &mut ui.new_child(
255                    UiBuilder::new()
256                        .max_rect(ui.max_rect())
257                        .layout(Layout::top_down(Align::Min)),
258                );
259
260                let mut layout = ProbeLayout::load(child_ui.ctx(), probe_id.with("layout"));
261                let root_as_display = should_render_as_display(maybe_mut.shape(), &[]);
262
263                if let Some(label) = self.header {
264                    // Show with a top-level header (like Probe::new(x).with_header("name"))
265                    let mut header = show_header(
266                        label,
267                        maybe_mut,
268                        &mut layout,
269                        0,
270                        child_ui,
271                        probe_id.with("root"),
272                        &mut changed,
273                        self.force_reborrow,
274                        expand_all,
275                        root_as_display,
276                    );
277
278                    if header.openness > 0.0 && !root_as_display {
279                        show_body(
280                            maybe_mut,
281                            &mut header,
282                            &mut layout,
283                            0,
284                            child_ui,
285                            probe_id.with("root"),
286                            &mut changed,
287                            self.force_reborrow,
288                            expand_all,
289                            root_as_display,
290                        );
291                    }
292
293                    header.store(child_ui.ctx());
294                } else {
295                    // Show directly without a top-level header (table of fields)
296                    show_body_direct(
297                        maybe_mut,
298                        &mut layout,
299                        0,
300                        child_ui,
301                        probe_id.with("root"),
302                        &mut changed,
303                        self.force_reborrow,
304                        expand_all,
305                        root_as_display,
306                    );
307                }
308
309                layout.store(child_ui.ctx());
310
311                let final_rect = child_ui.min_rect();
312                ui.advance_cursor_after_rect(final_rect);
313            })
314            .response;
315
316        drop(guard);
317
318        if changed {
319            r.mark_changed();
320            ui.ctx().request_repaint();
321        }
322
323        r
324    }
325}
326
327// ---------------------------------------------------------------------------
328// Core layout functions (egui-probe style)
329// ---------------------------------------------------------------------------
330
331/// Returns true if the given `MaybeMut` has inner fields/items to display
332/// (i.e. it should get a collapse arrow).
333fn has_inner(value: &MaybeMut<'_, '_>) -> bool {
334    let peek = value.as_peek();
335    // Structs with fields, enums with variant fields, lists, maps, options
336    // with inner, tuples, sets, pointers to inner — all have inner content.
337    // Option/Result must be checked before enum, since they also match into_enum().
338    if let Ok(opt) = peek.into_option()
339        && let Some(inner) = opt.value()
340    {
341        return has_inner(&MaybeMut::Not(inner));
342    }
343    if let Ok(s) = peek.into_struct() {
344        return s.field_count() > 0;
345    }
346    if let Ok(e) = peek.into_enum()
347        && let Ok(v) = e.active_variant()
348    {
349        return !v.data.fields.is_empty();
350    }
351    if let Ok(l) = peek.into_list_like() {
352        return !l.is_empty();
353    }
354    if let Ok(m) = peek.into_map() {
355        return !m.is_empty();
356    }
357    if let Ok(t) = peek.into_tuple() {
358        return !t.is_empty();
359    }
360    if let Ok(p) = peek.into_pointer()
361        && let Some(inner) = p.borrow_inner()
362    {
363        return has_inner(&MaybeMut::Not(inner));
364    }
365    false
366}
367
368/// Show a single row: label on the left, inline value widget on the right.
369/// Returns the ProbeHeader for the row (which tracks collapse state).
370#[expect(clippy::too_many_arguments)]
371fn show_header(
372    label: impl Into<WidgetText>,
373    value: &mut MaybeMut<'_, '_>,
374    layout: &mut ProbeLayout,
375    indent: usize,
376    ui: &mut Ui,
377    id: Id,
378    changed: &mut bool,
379    force_reborrow: bool,
380    expand_all: bool,
381    as_display: bool,
382) -> ProbeHeader {
383    show_header_with_prefix(
384        |_| {},
385        true,
386        label,
387        value,
388        layout,
389        indent,
390        ui,
391        id,
392        changed,
393        force_reborrow,
394        expand_all,
395        as_display,
396    )
397}
398
399/// Like `show_header` but renders `prefix` widgets in the label column before
400/// the collapse button. Used by the list row to inject ▲/▼ swap buttons.
401#[expect(clippy::too_many_arguments)]
402fn show_header_with_prefix(
403    prefix: impl FnOnce(&mut Ui),
404    animate: bool,
405    label: impl Into<WidgetText>,
406    value: &mut MaybeMut<'_, '_>,
407    layout: &mut ProbeLayout,
408    indent: usize,
409    ui: &mut Ui,
410    id: Id,
411    changed: &mut bool,
412    force_reborrow: bool,
413    expand_all: bool,
414    as_display: bool,
415) -> ProbeHeader {
416    let mut header = if animate {
417        ProbeHeader::load(ui.ctx(), id)
418    } else {
419        ProbeHeader::load_no_animation(ui.ctx(), id)
420    };
421    let row_has_inner = !as_display && has_inner(value);
422    header.set_has_inner(row_has_inner);
423    if !row_has_inner {
424        header.set_open(false);
425    }
426
427    if expand_all && row_has_inner {
428        header.set_open(true);
429    }
430
431    ui.horizontal(|ui| {
432        let label_response = layout.inner_label_ui(indent, id.with("label"), ui, |ui| {
433            prefix(ui);
434            if header.has_inner() {
435                header.collapse_button(ui);
436            }
437            ui.label(label)
438        });
439
440        layout.inner_value_ui(id.with("value"), ui, |ui| {
441            *changed |= show_inline_value(value, ui, id, force_reborrow, as_display)
442                .labelled_by(label_response.id)
443                .changed();
444        });
445    });
446
447    header
448}
449
450/// Show the collapsible body (the inner fields/items) below a header row.
451#[expect(clippy::too_many_arguments)]
452fn show_body(
453    value: &mut MaybeMut<'_, '_>,
454    header: &mut ProbeHeader,
455    layout: &mut ProbeLayout,
456    indent: usize,
457    ui: &mut Ui,
458    id: Id,
459    changed: &mut bool,
460    force_reborrow: bool,
461    expand_all: bool,
462    as_display: bool,
463) {
464    if as_display {
465        header.set_has_inner(false);
466        header.set_open(false);
467        return;
468    }
469
470    let cursor = ui.cursor();
471    let table_rect = egui::Rect::from_min_max(
472        egui::pos2(cursor.min.x, cursor.min.y - header.body_shift()),
473        ui.max_rect().max,
474    );
475
476    let mut table_ui = ui.new_child(
477        UiBuilder::new()
478            .max_rect(table_rect)
479            .layout(Layout::top_down(Align::Min))
480            .id_salt(id.with("body")),
481    );
482    table_ui.set_clip_rect(
483        ui.clip_rect()
484            .intersect(egui::Rect::everything_below(ui.min_rect().max.y)),
485    );
486
487    let got_inner = show_inner_rows(
488        value,
489        layout,
490        indent + 1,
491        id,
492        &mut table_ui,
493        changed,
494        force_reborrow,
495        expand_all,
496    );
497    header.set_has_inner(got_inner);
498
499    let final_table_rect = table_ui.min_rect();
500    ui.advance_cursor_after_rect(final_table_rect);
501    let table_height = ui.cursor().min.y - table_rect.min.y;
502    header.set_body_height(table_height);
503}
504
505/// Show the body directly (no collapse header wrapper). Used when there is no
506/// top-level header.
507fn show_body_direct(
508    value: &mut MaybeMut<'_, '_>,
509    layout: &mut ProbeLayout,
510    indent: usize,
511    ui: &mut Ui,
512    id: Id,
513    changed: &mut bool,
514    force_reborrow: bool,
515    expand_all: bool,
516    as_display: bool,
517) {
518    if as_display {
519        *changed |= show_inline_value(value, ui, id, force_reborrow, true).changed();
520        return;
521    }
522
523    let cursor = ui.cursor();
524    let table_rect =
525        egui::Rect::from_min_max(egui::pos2(cursor.min.x, cursor.min.y), ui.max_rect().max);
526
527    let mut table_ui = ui.new_child(
528        UiBuilder::new()
529            .max_rect(table_rect)
530            .layout(Layout::top_down(Align::Min))
531            .id_salt(id.with("body")),
532    );
533    table_ui.set_clip_rect(
534        ui.clip_rect()
535            .intersect(egui::Rect::everything_below(ui.min_rect().max.y)),
536    );
537
538    show_inner_rows(
539        value,
540        layout,
541        indent + 1,
542        id,
543        &mut table_ui,
544        changed,
545        force_reborrow,
546        expand_all,
547    );
548
549    let final_table_rect = table_ui.min_rect();
550    ui.advance_cursor_after_rect(final_table_rect);
551}
552
553/// Iterate over the "inner" rows of a value and render each as a header+body pair.
554/// Returns `true` if any inner rows were emitted.
555fn show_inner_rows(
556    value: &mut MaybeMut<'_, '_>,
557    layout: &mut ProbeLayout,
558    indent: usize,
559    id: Id,
560    ui: &mut Ui,
561    changed: &mut bool,
562    force_reborrow: bool,
563    expand_all: bool,
564) -> bool {
565    match value {
566        MaybeMut::Mut(poke) => show_inner_rows_poke(
567            poke,
568            layout,
569            indent,
570            id,
571            ui,
572            changed,
573            force_reborrow,
574            expand_all,
575        ),
576        MaybeMut::Not(peek) => show_inner_rows_peek(
577            *peek,
578            layout,
579            indent,
580            id,
581            ui,
582            changed,
583            force_reborrow,
584            expand_all,
585        ),
586    }
587}
588
589/// Attempt to write-lock a child [`MaybeMut`]. If the child is already
590/// [`MaybeMut::Mut`] or wraps a lockable pointer (e.g. `RwLock`), this
591/// returns a [`Guard`] with mutable access. Otherwise it falls back to
592/// a read lock.
593fn lock_child<'mem, 'facet>(child: MaybeMut<'mem, 'facet>) -> Option<Guard<'mem, 'facet>> {
594    match child.write() {
595        Ok(guard) => Some(guard),
596        Err(e) if matches!(e.kind, MakeLockErrorKind::NotLockable) => {
597            MaybeMut::Not(e.unchanged).read().ok()
598        }
599        Err(_) => None,
600    }
601}
602
603fn show_inner_rows_poke(
604    poke: &mut Poke<'_, '_>,
605    layout: &mut ProbeLayout,
606    indent: usize,
607    id: Id,
608    ui: &mut Ui,
609    changed: &mut bool,
610    force_reborrow: bool,
611    expand_all: bool,
612) -> bool {
613    // Option/Result have Def::Option/Def::Result but Type::User(UserType::Enum),
614    // so check Def before is_enum() to avoid misrouting.
615    if let Def::Option(option_def) = poke.shape().def {
616        return show_inner_rows_poke_option(
617            poke,
618            option_def,
619            layout,
620            indent,
621            id,
622            ui,
623            changed,
624            force_reborrow,
625            expand_all,
626        );
627    }
628
629    // For enums, we can use into_enum directly without reborrowing,
630    // since PokeEnum.field() takes &mut self.
631    if poke.is_enum() {
632        let enu_poke = match poke.try_reborrow() {
633            Some(rb) => rb,
634            None if force_reborrow => unsafe {
635                Poke::from_raw_parts(poke.data_mut(), poke.shape())
636            },
637            None => {
638                return show_inner_rows_peek(
639                    poke.as_peek(),
640                    layout,
641                    indent,
642                    id,
643                    ui,
644                    changed,
645                    force_reborrow,
646                    expand_all,
647                );
648            }
649        };
650        if let Ok(enu) = enu_poke.into_enum() {
651            return show_inner_rows_poke_enum(
652                enu,
653                layout,
654                indent,
655                id,
656                ui,
657                changed,
658                force_reborrow,
659                expand_all,
660            );
661        }
662        return show_inner_rows_peek(
663            poke.as_peek(),
664            layout,
665            indent,
666            id,
667            ui,
668            changed,
669            force_reborrow,
670            expand_all,
671        );
672    }
673
674    // For structs, reborrow to get mutable field access
675    if poke.is_struct() {
676        let reborrow = match poke.try_reborrow() {
677            Some(rb) => rb,
678            None if force_reborrow => unsafe {
679                Poke::from_raw_parts(poke.data_mut(), poke.shape())
680            },
681            None => {
682                return show_inner_rows_peek(
683                    poke.as_peek(),
684                    layout,
685                    indent,
686                    id,
687                    ui,
688                    changed,
689                    force_reborrow,
690                    expand_all,
691                );
692            }
693        };
694        if let Ok(struc) = reborrow.into_struct() {
695            return show_inner_rows_poke_struct(
696                struc,
697                layout,
698                indent,
699                id,
700                ui,
701                changed,
702                force_reborrow,
703                expand_all,
704            );
705        }
706    }
707
708    // Maps and Sets: PokeMap/PokeSet don't expose mutable values, so render
709    // entries via the read-only peek path (the inline `+` button is shown by
710    // `show_inline_poke_map`/`show_inline_poke_set`).
711    if matches!(poke.shape().def, Def::Map(_) | Def::Set(_)) {
712        return show_inner_rows_peek(
713            poke.as_peek(),
714            layout,
715            indent,
716            id,
717            ui,
718            changed,
719            force_reborrow,
720            expand_all,
721        );
722    }
723
724    let data_mut = poke.data_mut();
725    let shape = poke.shape();
726    let poke = match poke.try_reborrow() {
727        Some(rb) => rb,
728        None if force_reborrow => unsafe { Poke::from_raw_parts(poke.data_mut(), poke.shape()) },
729        None => {
730            return show_inner_rows_peek(
731                poke.as_peek(),
732                layout,
733                indent,
734                id,
735                ui,
736                changed,
737                force_reborrow,
738                expand_all,
739            );
740        }
741    };
742    if let Ok(poke_list) = poke.into_list() {
743        show_inner_rows_poke_list(
744            poke_list,
745            layout,
746            indent,
747            id,
748            ui,
749            changed,
750            force_reborrow,
751            expand_all,
752        )
753    } else {
754        // restore old poke
755        // SAFETY: this is ok because there still is only one access to poke due to the if
756        // branch not being reached
757        let poke = unsafe { Poke::from_raw_parts(data_mut, shape) };
758        // For tuple, option, pointer — fall through to peek
759        show_inner_rows_peek(
760            poke.as_peek(),
761            layout,
762            indent,
763            id,
764            ui,
765            changed,
766            force_reborrow,
767            expand_all,
768        )
769    }
770}
771
772fn show_inner_rows_poke_list(
773    mut list: PokeList<'_, '_>,
774    layout: &mut ProbeLayout,
775    indent: usize,
776    id: Id,
777    ui: &mut Ui,
778    changed: &mut bool,
779    force_reborrow: bool,
780    expand_all: bool,
781) -> bool {
782    let len = list.len();
783    if len == 0 {
784        return false;
785    }
786    let can_swap = list.def().vtable.swap.is_some();
787    // Swaps can't be performed while we hold a `get_mut` borrow on a row, so
788    // collect the requested swap and apply it after the iteration.
789    let mut pending_swap: Option<(usize, usize)> = None;
790    for idx in 0..len {
791        let label = alloc::format!("[{idx}]");
792        if let Some(field_poke) = list.get_mut(idx) {
793            let row_id = id.with(("list", idx));
794            let Some(mut guard) = lock_child(MaybeMut::Mut(field_poke)) else {
795                continue;
796            };
797            let child = &mut *guard;
798            let as_display = should_render_as_display(child.shape(), &[]);
799            let prefix = |ui: &mut Ui| {
800                if !can_swap {
801                    return;
802                }
803                ui.scope(|ui| {
804                    ui.spacing_mut().button_padding = egui::vec2(2.0, 0.0);
805                    let up = ui
806                        .add_enabled(idx > 0, egui::Button::new("⬆").small())
807                        .on_hover_text("move up");
808                    if up.clicked() {
809                        pending_swap = Some((idx, idx - 1));
810                    }
811                    let down = ui
812                        .add_enabled(idx + 1 < len, egui::Button::new("⬇").small())
813                        .on_hover_text("move down");
814                    if down.clicked() {
815                        pending_swap = Some((idx, idx + 1));
816                    }
817                });
818            };
819            let mut header = show_header_with_prefix(
820                prefix,
821                false,
822                &label,
823                child,
824                layout,
825                indent,
826                ui,
827                row_id,
828                changed,
829                force_reborrow,
830                expand_all,
831                as_display,
832            );
833            if header.openness > 0.0 && !as_display {
834                show_body(
835                    child,
836                    &mut header,
837                    layout,
838                    indent,
839                    ui,
840                    row_id,
841                    changed,
842                    force_reborrow,
843                    expand_all,
844                    as_display,
845                );
846            }
847            header.store(ui.ctx());
848        }
849    }
850    if let Some((a, b)) = pending_swap
851        && list.swap(a, b).is_ok()
852    {
853        // Move the persisted collapse state along with the item, otherwise the
854        // (now-different) item that ends up at the original index would inherit
855        // the open/closed state of the moved row.
856        swap_probe_header_state(ui.ctx(), id.with(("list", a)), id.with(("list", b)));
857        *changed = true;
858    }
859    true
860}
861
862fn show_inner_rows_poke_struct(
863    mut struc: PokeStruct<'_, '_>,
864    layout: &mut ProbeLayout,
865    indent: usize,
866    id: Id,
867    ui: &mut Ui,
868    changed: &mut bool,
869    force_reborrow: bool,
870    expand_all: bool,
871) -> bool {
872    let count = struc.field_count();
873    if count == 0 {
874        return false;
875    }
876    let mut got_inner = false;
877    for idx in 0..count {
878        let field = &struc.ty().fields[idx];
879        if has_egui_skip(field.attributes) {
880            continue;
881        }
882        if let Ok(field_poke) = struc.field(idx) {
883            let row_id = id.with(("struct", idx));
884            let Some(mut guard) = lock_child(MaybeMut::Mut(field_poke)) else {
885                continue;
886            };
887            let child = &mut *guard;
888            if field.is_flattened() {
889                ui.push_id(row_id, |ui| {
890                    got_inner |= show_inner_rows(
891                        child,
892                        layout,
893                        indent,
894                        row_id,
895                        ui,
896                        changed,
897                        force_reborrow,
898                        expand_all,
899                    );
900                });
901                continue;
902            }
903            got_inner = true;
904            let field_name = field_display_name(field);
905            let as_display = should_render_as_display(child.shape(), field.attributes);
906            let mut header = show_header(
907                &field_name,
908                child,
909                layout,
910                indent,
911                ui,
912                row_id,
913                changed,
914                force_reborrow,
915                expand_all,
916                as_display,
917            );
918            if header.openness > 0.0 && !as_display {
919                show_body(
920                    child,
921                    &mut header,
922                    layout,
923                    indent,
924                    ui,
925                    row_id,
926                    changed,
927                    force_reborrow,
928                    expand_all,
929                    as_display,
930                );
931            }
932            header.store(ui.ctx());
933        }
934    }
935    got_inner
936}
937
938fn show_inner_rows_poke_enum(
939    mut enu: PokeEnum<'_, '_>,
940    layout: &mut ProbeLayout,
941    indent: usize,
942    id: Id,
943    ui: &mut Ui,
944    changed: &mut bool,
945    force_reborrow: bool,
946    expand_all: bool,
947) -> bool {
948    let variant = match enu.active_variant() {
949        Ok(v) => v,
950        Err(_) => return false,
951    };
952    let field_count = variant.data.fields.len();
953    if field_count == 0 {
954        return false;
955    }
956    let mut got_inner = false;
957    for idx in 0..field_count {
958        let field = &variant.data.fields[idx];
959        if has_egui_skip(field.attributes) {
960            continue;
961        }
962        if let Ok(Some(field_poke)) = enu.field(idx) {
963            let row_id = id.with(("enum", idx));
964            let Some(mut guard) = lock_child(MaybeMut::Mut(field_poke)) else {
965                continue;
966            };
967            let child = &mut *guard;
968            if field.is_flattened() {
969                ui.push_id(row_id, |ui| {
970                    got_inner |= show_inner_rows(
971                        child,
972                        layout,
973                        indent,
974                        row_id,
975                        ui,
976                        changed,
977                        force_reborrow,
978                        expand_all,
979                    );
980                });
981                continue;
982            }
983            let field_name = field_display_name(field);
984            let as_display = should_render_as_display(child.shape(), field.attributes);
985            let mut header = show_header(
986                &field_name,
987                child,
988                layout,
989                indent,
990                ui,
991                row_id,
992                changed,
993                force_reborrow,
994                expand_all,
995                as_display,
996            );
997            if header.openness > 0.0 && !as_display {
998                show_body(
999                    child,
1000                    &mut header,
1001                    layout,
1002                    indent,
1003                    ui,
1004                    row_id,
1005                    changed,
1006                    force_reborrow,
1007                    expand_all,
1008                    as_display,
1009                );
1010            }
1011            header.store(ui.ctx());
1012        }
1013    }
1014    got_inner
1015}
1016
1017fn show_inner_rows_peek(
1018    peek: Peek<'_, '_>,
1019    layout: &mut ProbeLayout,
1020    indent: usize,
1021    id: Id,
1022    ui: &mut Ui,
1023    changed: &mut bool,
1024    force_reborrow: bool,
1025    expand_all: bool,
1026) -> bool {
1027    if let Ok(opt) = peek.into_option() {
1028        show_inner_rows_peek_option(
1029            opt,
1030            layout,
1031            indent,
1032            id,
1033            ui,
1034            changed,
1035            force_reborrow,
1036            expand_all,
1037        )
1038    } else if let Ok(struc) = peek.into_struct() {
1039        show_inner_rows_peek_struct(
1040            struc,
1041            layout,
1042            indent,
1043            id,
1044            ui,
1045            changed,
1046            force_reborrow,
1047            expand_all,
1048        )
1049    } else if let Ok(enu) = peek.into_enum() {
1050        show_inner_rows_peek_enum(
1051            enu,
1052            layout,
1053            indent,
1054            id,
1055            ui,
1056            changed,
1057            force_reborrow,
1058            expand_all,
1059        )
1060    } else if let Ok(list) = peek.into_list_like() {
1061        show_inner_rows_peek_list(
1062            list,
1063            layout,
1064            indent,
1065            id,
1066            ui,
1067            changed,
1068            force_reborrow,
1069            expand_all,
1070        )
1071    } else if let Ok(map) = peek.into_map() {
1072        show_inner_rows_peek_map(
1073            map,
1074            layout,
1075            indent,
1076            id,
1077            ui,
1078            changed,
1079            force_reborrow,
1080            expand_all,
1081        )
1082    } else if let Ok(set) = peek.into_set() {
1083        show_inner_rows_peek_set(
1084            set,
1085            layout,
1086            indent,
1087            id,
1088            ui,
1089            changed,
1090            force_reborrow,
1091            expand_all,
1092        )
1093    } else if let Ok(tuple) = peek.into_tuple() {
1094        show_inner_rows_peek_tuple(
1095            tuple,
1096            layout,
1097            indent,
1098            id,
1099            ui,
1100            changed,
1101            force_reborrow,
1102            expand_all,
1103        )
1104    } else if let Ok(ptr) = peek.into_pointer() {
1105        show_inner_rows_peek_pointer(
1106            ptr,
1107            layout,
1108            indent,
1109            id,
1110            ui,
1111            changed,
1112            force_reborrow,
1113            expand_all,
1114        )
1115    } else {
1116        false
1117    }
1118}
1119
1120fn show_inner_rows_peek_struct(
1121    struc: PeekStruct<'_, '_>,
1122    layout: &mut ProbeLayout,
1123    indent: usize,
1124    id: Id,
1125    ui: &mut Ui,
1126    changed: &mut bool,
1127    force_reborrow: bool,
1128    expand_all: bool,
1129) -> bool {
1130    let mut got_inner = false;
1131    for (idx, (field, value)) in struc.fields().enumerate() {
1132        let row_id = id.with(("struct", idx));
1133        if has_egui_skip(field.attributes) {
1134            continue;
1135        }
1136        let Some(mut guard) = lock_child(MaybeMut::Not(value)) else {
1137            continue;
1138        };
1139        let child = &mut *guard;
1140        if field.is_flattened() {
1141            ui.push_id(row_id, |ui| {
1142                got_inner |= show_inner_rows(
1143                    child,
1144                    layout,
1145                    indent,
1146                    row_id,
1147                    ui,
1148                    changed,
1149                    force_reborrow,
1150                    expand_all,
1151                );
1152            });
1153            continue;
1154        }
1155        got_inner = true;
1156        let field_name = field_display_name(&field);
1157        let as_display = should_render_as_display(child.shape(), field.attributes);
1158        let mut header = show_header(
1159            &field_name,
1160            child,
1161            layout,
1162            indent,
1163            ui,
1164            row_id,
1165            changed,
1166            force_reborrow,
1167            expand_all,
1168            as_display,
1169        );
1170        if header.openness > 0.0 && !as_display {
1171            show_body(
1172                child,
1173                &mut header,
1174                layout,
1175                indent,
1176                ui,
1177                row_id,
1178                changed,
1179                force_reborrow,
1180                expand_all,
1181                as_display,
1182            );
1183        }
1184        header.store(ui.ctx());
1185    }
1186    got_inner
1187}
1188
1189fn show_inner_rows_peek_enum(
1190    enu: PeekEnum<'_, '_>,
1191    layout: &mut ProbeLayout,
1192    indent: usize,
1193    id: Id,
1194    ui: &mut Ui,
1195    changed: &mut bool,
1196    force_reborrow: bool,
1197    expand_all: bool,
1198) -> bool {
1199    let mut got_inner = false;
1200    for (idx, (field, value)) in enu.fields().enumerate() {
1201        let row_id = id.with(("enum", idx));
1202        if has_egui_skip(field.attributes) {
1203            continue;
1204        }
1205        let Some(mut guard) = lock_child(MaybeMut::Not(value)) else {
1206            continue;
1207        };
1208        let child = &mut *guard;
1209        if field.is_flattened() {
1210            ui.push_id(row_id, |ui| {
1211                got_inner |= show_inner_rows(
1212                    child,
1213                    layout,
1214                    indent,
1215                    row_id,
1216                    ui,
1217                    changed,
1218                    force_reborrow,
1219                    expand_all,
1220                );
1221            });
1222            continue;
1223        }
1224        got_inner = true;
1225        let field_name = field_display_name(&field);
1226        let as_display = should_render_as_display(child.shape(), field.attributes);
1227        let mut header = show_header(
1228            &field_name,
1229            child,
1230            layout,
1231            indent,
1232            ui,
1233            row_id,
1234            changed,
1235            force_reborrow,
1236            expand_all,
1237            as_display,
1238        );
1239        if header.openness > 0.0 && !as_display {
1240            show_body(
1241                child,
1242                &mut header,
1243                layout,
1244                indent,
1245                ui,
1246                row_id,
1247                changed,
1248                force_reborrow,
1249                expand_all,
1250                as_display,
1251            );
1252        }
1253        header.store(ui.ctx());
1254    }
1255    got_inner
1256}
1257
1258fn show_inner_rows_peek_list(
1259    list: PeekListLike<'_, '_>,
1260    layout: &mut ProbeLayout,
1261    indent: usize,
1262    id: Id,
1263    ui: &mut Ui,
1264    changed: &mut bool,
1265    force_reborrow: bool,
1266    expand_all: bool,
1267) -> bool {
1268    let mut got_inner = false;
1269    for (idx, item) in list.iter().enumerate() {
1270        let row_id = id.with(("list", idx));
1271        got_inner = true;
1272        let label = alloc::format!("[{idx}]");
1273        let Some(mut guard) = lock_child(MaybeMut::Not(item)) else {
1274            continue;
1275        };
1276        let child = &mut *guard;
1277        let as_display = should_render_as_display(child.shape(), &[]);
1278        let mut header = show_header(
1279            &label,
1280            child,
1281            layout,
1282            indent,
1283            ui,
1284            row_id,
1285            changed,
1286            force_reborrow,
1287            expand_all,
1288            as_display,
1289        );
1290        if header.openness > 0.0 && !as_display {
1291            show_body(
1292                child,
1293                &mut header,
1294                layout,
1295                indent,
1296                ui,
1297                row_id,
1298                changed,
1299                force_reborrow,
1300                expand_all,
1301                as_display,
1302            );
1303        }
1304        header.store(ui.ctx());
1305    }
1306    got_inner
1307}
1308
1309fn show_inner_rows_peek_map(
1310    map: PeekMap<'_, '_>,
1311    layout: &mut ProbeLayout,
1312    indent: usize,
1313    id: Id,
1314    ui: &mut Ui,
1315    changed: &mut bool,
1316    force_reborrow: bool,
1317    expand_all: bool,
1318) -> bool {
1319    let mut got_inner = false;
1320    for (idx, (key, value)) in map.iter().enumerate() {
1321        let row_id = id.with(("map", idx));
1322        got_inner = true;
1323        let label = alloc::format!("{}", key);
1324        let Some(mut guard) = lock_child(MaybeMut::Not(value)) else {
1325            continue;
1326        };
1327        let child = &mut *guard;
1328        let as_display = should_render_as_display(child.shape(), &[]);
1329        let mut header = show_header(
1330            &label,
1331            child,
1332            layout,
1333            indent,
1334            ui,
1335            row_id,
1336            changed,
1337            force_reborrow,
1338            expand_all,
1339            as_display,
1340        );
1341        if header.openness > 0.0 && !as_display {
1342            show_body(
1343                child,
1344                &mut header,
1345                layout,
1346                indent,
1347                ui,
1348                row_id,
1349                changed,
1350                force_reborrow,
1351                expand_all,
1352                as_display,
1353            );
1354        }
1355        header.store(ui.ctx());
1356    }
1357    got_inner
1358}
1359
1360fn show_inner_rows_peek_set(
1361    set: PeekSet<'_, '_>,
1362    layout: &mut ProbeLayout,
1363    indent: usize,
1364    id: Id,
1365    ui: &mut Ui,
1366    changed: &mut bool,
1367    force_reborrow: bool,
1368    expand_all: bool,
1369) -> bool {
1370    let mut got_inner = false;
1371    for (idx, value) in set.iter().enumerate() {
1372        let row_id = id.with(("set", idx));
1373        got_inner = true;
1374        let label = alloc::format!("[{idx}]");
1375        let Some(mut guard) = lock_child(MaybeMut::Not(value)) else {
1376            continue;
1377        };
1378        let child = &mut *guard;
1379        let as_display = should_render_as_display(child.shape(), &[]);
1380        let mut header = show_header(
1381            &label,
1382            child,
1383            layout,
1384            indent,
1385            ui,
1386            row_id,
1387            changed,
1388            force_reborrow,
1389            expand_all,
1390            as_display,
1391        );
1392        if header.openness > 0.0 && !as_display {
1393            show_body(
1394                child,
1395                &mut header,
1396                layout,
1397                indent,
1398                ui,
1399                row_id,
1400                changed,
1401                force_reborrow,
1402                expand_all,
1403                as_display,
1404            );
1405        }
1406        header.store(ui.ctx());
1407    }
1408    got_inner
1409}
1410
1411fn show_inner_rows_peek_option(
1412    opt: PeekOption<'_, '_>,
1413    layout: &mut ProbeLayout,
1414    indent: usize,
1415    id: Id,
1416    ui: &mut Ui,
1417    changed: &mut bool,
1418    force_reborrow: bool,
1419    expand_all: bool,
1420) -> bool {
1421    if let Some(inner) = opt.value() {
1422        let Some(mut guard) = lock_child(MaybeMut::Not(inner)) else {
1423            return false;
1424        };
1425        let child = &mut *guard;
1426        return show_inner_rows(
1427            child,
1428            layout,
1429            indent,
1430            id.with("option"),
1431            ui,
1432            changed,
1433            force_reborrow,
1434            expand_all,
1435        );
1436    }
1437    false
1438}
1439
1440fn show_inner_rows_peek_tuple(
1441    tuple: PeekTuple<'_, '_>,
1442    layout: &mut ProbeLayout,
1443    indent: usize,
1444    id: Id,
1445    ui: &mut Ui,
1446    changed: &mut bool,
1447    force_reborrow: bool,
1448    expand_all: bool,
1449) -> bool {
1450    let mut got_inner = false;
1451    for (idx, (_field, value)) in tuple.fields().enumerate() {
1452        let row_id = id.with(("tuple", idx));
1453        got_inner = true;
1454        let label = alloc::format!("[{idx}]");
1455        let Some(mut guard) = lock_child(MaybeMut::Not(value)) else {
1456            continue;
1457        };
1458        let child = &mut *guard;
1459        let as_display = should_render_as_display(child.shape(), &[]);
1460        let mut header = show_header(
1461            &label,
1462            child,
1463            layout,
1464            indent,
1465            ui,
1466            row_id,
1467            changed,
1468            force_reborrow,
1469            expand_all,
1470            as_display,
1471        );
1472        if header.openness > 0.0 && !as_display {
1473            show_body(
1474                child,
1475                &mut header,
1476                layout,
1477                indent,
1478                ui,
1479                row_id,
1480                changed,
1481                force_reborrow,
1482                expand_all,
1483                as_display,
1484            );
1485        }
1486        header.store(ui.ctx());
1487    }
1488    got_inner
1489}
1490
1491fn show_inner_rows_peek_pointer(
1492    ptr: PeekPointer<'_, '_>,
1493    layout: &mut ProbeLayout,
1494    indent: usize,
1495    id: Id,
1496    ui: &mut Ui,
1497    changed: &mut bool,
1498    force_reborrow: bool,
1499    expand_all: bool,
1500) -> bool {
1501    if let Some(inner) = ptr.borrow_inner() {
1502        let Some(mut guard) = lock_child(MaybeMut::Not(inner)) else {
1503            return false;
1504        };
1505        let child = &mut *guard;
1506        return show_inner_rows(
1507            child,
1508            layout,
1509            indent,
1510            id.with("ptr"),
1511            ui,
1512            changed,
1513            force_reborrow,
1514            expand_all,
1515        );
1516    }
1517    false
1518}
1519
1520// ---------------------------------------------------------------------------
1521// Inline value rendering (the right-side widget for a row)
1522// ---------------------------------------------------------------------------
1523
1524/// Show the inline (right-side) widget for a value. Returns the Response.
1525fn show_inline_value(
1526    value: &mut MaybeMut<'_, '_>,
1527    ui: &mut Ui,
1528    id: Id,
1529    force_reborrow: bool,
1530    as_display: bool,
1531) -> Response {
1532    match value {
1533        MaybeMut::Mut(poke) => show_inline_poke(poke, ui, id, force_reborrow, as_display),
1534        MaybeMut::Not(peek) => show_inline_peek(*peek, ui, id, as_display),
1535    }
1536}
1537
1538/// Show inline widget for a mutable value.
1539fn show_inline_poke(
1540    poke: &mut Poke<'_, '_>,
1541    ui: &mut Ui,
1542    id: Id,
1543    force_reborrow: bool,
1544    as_display: bool,
1545) -> Response {
1546    if as_display || should_render_as_display(poke.shape(), &[]) {
1547        return ui.label(alloc::format!("{}", poke.as_peek()));
1548    }
1549
1550    if let Some(scalar_type) = poke.as_peek().scalar_type() {
1551        return show_inline_poke_scalar(poke, scalar_type, ui);
1552    }
1553
1554    // For non-scalar types, show a type summary label
1555    // Option/Result have Def::Option/Def::Result but Type::User(UserType::Enum),
1556    // so check Def before is_enum() to avoid misrouting.
1557    if let Def::Option(option_def) = poke.shape().def {
1558        return show_inline_poke_option(poke, option_def, ui, id, force_reborrow);
1559    }
1560    if poke.is_enum() {
1561        return show_inline_poke_enum(poke, ui, id);
1562    }
1563    if poke.is_struct() {
1564        return ui.weak(shape_display_name(poke.shape()));
1565    }
1566    if let Def::List(list_def) = poke.shape().def {
1567        return show_inline_poke_list(poke, list_def, ui);
1568    }
1569    if let Def::Map(map_def) = poke.shape().def {
1570        return show_inline_poke_map(poke, map_def, ui);
1571    }
1572    if let Def::Set(set_def) = poke.shape().def {
1573        return show_inline_poke_set(poke, set_def, ui);
1574    }
1575    if let Ok(tuple) = poke.as_peek().into_tuple() {
1576        return ui.weak(alloc::format!("({})", tuple.len()));
1577    }
1578    if let Ok(ptr) = poke.as_peek().into_pointer()
1579        && let Some(inner) = ptr.borrow_inner()
1580    {
1581        return show_inline_peek(inner, ui, id, false);
1582    }
1583
1584    ui.weak(shape_display_name(poke.shape()))
1585}
1586
1587/// Show inline widget for a mutable list: `[len]` with +/- buttons.
1588fn show_inline_poke_list(poke: &mut Poke<'_, '_>, list_def: ListDef, ui: &mut Ui) -> Response {
1589    let len = poke
1590        .as_peek()
1591        .into_list_like()
1592        .map(|l| l.len())
1593        .unwrap_or(0);
1594    let item_shape = list_def.t();
1595    let has_default = item_shape.is_default();
1596    let has_push = list_def.push().is_some();
1597    let has_pop = list_def.pop().is_some();
1598
1599    let mut changed = false;
1600    let r = ui.horizontal(|ui| {
1601        ui.weak(alloc::format!("[{len}]"));
1602
1603        if has_push && has_default && ui.small_button("+").clicked() {
1604            changed |= try_push_default_to_list(poke, list_def);
1605        }
1606
1607        if has_pop && len > 0 && ui.small_button("-").clicked() {
1608            changed |= try_pop_from_list(poke);
1609        }
1610    });
1611
1612    let mut r = r.response;
1613    if changed {
1614        r.mark_changed();
1615    }
1616    r
1617}
1618
1619/// Push a default-constructed element to the list via `PokeList::push_from_heap`.
1620fn try_push_default_to_list(poke: &mut Poke<'_, '_>, list_def: ListDef) -> bool {
1621    let item_shape = list_def.t();
1622
1623    // SAFETY: item_shape comes from the ListDef of this poke's shape.
1624    let partial = match unsafe { Partial::alloc_shape(item_shape) } {
1625        Ok(p) => p,
1626        Err(_) => return false,
1627    };
1628    let partial = match partial.set_default() {
1629        Ok(p) => p,
1630        Err(_) => return false,
1631    };
1632    let heap_value = match partial.build() {
1633        Ok(v) => v,
1634        Err(_) => return false,
1635    };
1636
1637    let Some(reborrow) = poke.try_reborrow() else {
1638        return false;
1639    };
1640    let Ok(mut list) = reborrow.into_list() else {
1641        return false;
1642    };
1643    list.push_from_heap(heap_value).is_ok()
1644}
1645
1646/// Pop the last element from the list via `PokeList::pop`. The returned
1647/// `HeapValue` drops the popped element when it goes out of scope.
1648fn try_pop_from_list(poke: &mut Poke<'_, '_>) -> bool {
1649    let Some(reborrow) = poke.try_reborrow() else {
1650        return false;
1651    };
1652    let Ok(mut list) = reborrow.into_list() else {
1653        return false;
1654    };
1655    matches!(list.pop(), Ok(Some(_)))
1656}
1657
1658/// Show inline widget for a mutable map: `[len]` with a `+` button to insert a
1659/// default-built (key, value) entry. Map removal is not exposed by `PokeMap`,
1660/// so there is no `-` button.
1661fn show_inline_poke_map(poke: &mut Poke<'_, '_>, map_def: MapDef, ui: &mut Ui) -> Response {
1662    let len = poke.as_peek().into_map().map(|m| m.len()).unwrap_or(0);
1663    let key_shape = map_def.k();
1664    let value_shape = map_def.v();
1665    let can_insert = key_shape.is_default() && value_shape.is_default();
1666
1667    let mut changed = false;
1668    let r = ui.horizontal(|ui| {
1669        ui.weak(alloc::format!("[{len}]"));
1670
1671        if can_insert
1672            && ui
1673                .small_button("+")
1674                .on_hover_text("insert default key/value")
1675                .clicked()
1676        {
1677            changed |= try_insert_default_into_map(poke, map_def);
1678        }
1679    });
1680
1681    let mut r = r.response;
1682    if changed {
1683        r.mark_changed();
1684    }
1685    r
1686}
1687
1688/// Insert a (default key, default value) entry into the map via
1689/// `PokeMap::insert_from_heap`.
1690fn try_insert_default_into_map(poke: &mut Poke<'_, '_>, map_def: MapDef) -> bool {
1691    let key = match build_default_heap_value(map_def.k()) {
1692        Some(v) => v,
1693        None => return false,
1694    };
1695    let value = match build_default_heap_value(map_def.v()) {
1696        Some(v) => v,
1697        None => return false,
1698    };
1699
1700    let Some(reborrow) = poke.try_reborrow() else {
1701        return false;
1702    };
1703    let Ok(mut map) = reborrow.into_map() else {
1704        return false;
1705    };
1706    map.insert_from_heap(key, value).is_ok()
1707}
1708
1709/// Show inline widget for a mutable set: `[len]` with a `+` button to insert a
1710/// default-built element. Set removal is not exposed by `PokeSet`, so there is
1711/// no `-` button.
1712fn show_inline_poke_set(poke: &mut Poke<'_, '_>, set_def: SetDef, ui: &mut Ui) -> Response {
1713    let len = poke.as_peek().into_set().map(|s| s.len()).unwrap_or(0);
1714    let elem_shape = set_def.t();
1715    let can_insert = elem_shape.is_default();
1716
1717    let mut changed = false;
1718    let r = ui.horizontal(|ui| {
1719        ui.weak(alloc::format!("[{len}]"));
1720
1721        if can_insert
1722            && ui
1723                .small_button("+")
1724                .on_hover_text("insert default value")
1725                .clicked()
1726        {
1727            changed |= try_insert_default_into_set(poke, set_def);
1728        }
1729    });
1730
1731    let mut r = r.response;
1732    if changed {
1733        r.mark_changed();
1734    }
1735    r
1736}
1737
1738/// Insert a default-constructed element into the set via
1739/// `PokeSet::insert_from_heap`.
1740fn try_insert_default_into_set(poke: &mut Poke<'_, '_>, set_def: SetDef) -> bool {
1741    let value = match build_default_heap_value(set_def.t()) {
1742        Some(v) => v,
1743        None => return false,
1744    };
1745
1746    let Some(reborrow) = poke.try_reborrow() else {
1747        return false;
1748    };
1749    let Ok(mut set) = reborrow.into_set() else {
1750        return false;
1751    };
1752    set.insert_from_heap(value).is_ok()
1753}
1754
1755/// Build a default-constructed `HeapValue` for the given shape, or return
1756/// `None` if allocation, defaulting, or building fails.
1757fn build_default_heap_value(
1758    shape: &'static facet::Shape,
1759) -> Option<facet_reflect::HeapValue<'static, true>> {
1760    // SAFETY: `shape` is provided by the caller and assumed to be a real, registered shape.
1761    let partial = unsafe { Partial::alloc_shape(shape) }.ok()?;
1762    let partial = partial.set_default().ok()?;
1763    partial.build().ok()
1764}
1765
1766/// Show inline widget for a mutable option: toggle between None and Some.
1767fn show_inline_poke_option(
1768    poke: &mut Poke<'_, '_>,
1769    option_def: OptionDef,
1770    ui: &mut Ui,
1771    id: Id,
1772    force_reborrow: bool,
1773) -> Response {
1774    let is_some = unsafe { (option_def.vtable.is_some)(poke.data_mut().as_const()) };
1775
1776    let mut changed = false;
1777    let r = ui.horizontal(|ui| {
1778        if ui.selectable_label(!is_some, "None").clicked()
1779            && is_some
1780            && let Some(reborrow) = poke.try_reborrow()
1781            && let Ok(mut opt) = reborrow.into_option()
1782        {
1783            opt.set_none();
1784            changed = true;
1785        }
1786        if ui.selectable_label(is_some, "Some").clicked() && !is_some {
1787            // Switch from None to Some(default)
1788            changed = try_set_option_to_some_default(poke, option_def);
1789        }
1790        if is_some {
1791            let inner_ptr = unsafe { (option_def.vtable.get_value)(poke.data_mut().as_const()) };
1792            if !inner_ptr.is_null() {
1793                // SAFETY: We have unique mutable access through poke, and the inner value
1794                // is stored within the Option's memory. The shape matches the inner type.
1795                let mut inner_poke = unsafe {
1796                    Poke::from_raw_parts(facet::PtrMut::new(inner_ptr as *mut u8), option_def.t())
1797                };
1798                show_inline_poke(&mut inner_poke, ui, id.with("some"), force_reborrow, false);
1799            }
1800        }
1801    });
1802
1803    let mut r = r.response;
1804    if changed {
1805        r.mark_changed();
1806    }
1807    r
1808}
1809
1810/// Set an Option from None to Some(T::default()) via `PokeOption::set_some_from_heap`.
1811fn try_set_option_to_some_default(poke: &mut Poke<'_, '_>, option_def: OptionDef) -> bool {
1812    let inner_shape = option_def.t();
1813
1814    // Allocate and default-construct the inner value
1815    // SAFETY: inner_shape comes from the OptionDef of this poke's shape.
1816    let partial = match unsafe { Partial::alloc_shape(inner_shape) } {
1817        Ok(p) => p,
1818        Err(_) => return false,
1819    };
1820    let partial = match partial.set_default() {
1821        Ok(p) => p,
1822        Err(_) => return false,
1823    };
1824    let heap_value = match partial.build() {
1825        Ok(v) => v,
1826        Err(_) => return false,
1827    };
1828
1829    let Some(reborrow) = poke.try_reborrow() else {
1830        return false;
1831    };
1832    let Ok(mut opt) = reborrow.into_option() else {
1833        return false;
1834    };
1835    opt.set_some_from_heap(heap_value).is_ok()
1836}
1837
1838/// Show inner rows for a mutable Option. When Some, shows the inner value mutably.
1839fn show_inner_rows_poke_option(
1840    poke: &mut Poke<'_, '_>,
1841    option_def: OptionDef,
1842    layout: &mut ProbeLayout,
1843    indent: usize,
1844    id: Id,
1845    ui: &mut Ui,
1846    changed: &mut bool,
1847    force_reborrow: bool,
1848    expand_all: bool,
1849) -> bool {
1850    let is_some = unsafe { (option_def.vtable.is_some)(poke.data_mut().as_const()) };
1851    if !is_some {
1852        return false;
1853    }
1854
1855    let inner_ptr = unsafe { (option_def.vtable.get_value)(poke.data_mut().as_const()) };
1856    if inner_ptr.is_null() {
1857        return false;
1858    }
1859
1860    // SAFETY: We have unique mutable access through poke, and the inner value
1861    // is stored within the Option's memory. The shape matches the inner type.
1862    let inner_poke =
1863        unsafe { Poke::from_raw_parts(facet::PtrMut::new(inner_ptr as *mut u8), option_def.t()) };
1864
1865    let mut child = MaybeMut::Mut(inner_poke);
1866    show_inner_rows(
1867        &mut child,
1868        layout,
1869        indent,
1870        id.with("option"),
1871        ui,
1872        changed,
1873        force_reborrow,
1874        expand_all,
1875    )
1876}
1877
1878/// Show inline widget for a mutable enum: ComboBox to select variant.
1879fn show_inline_poke_enum(poke: &mut Poke<'_, '_>, ui: &mut Ui, id: Id) -> Response {
1880    let shape = poke.shape();
1881    let Type::User(UserType::Enum(enum_type)) = shape.ty else {
1882        return ui.weak("enum");
1883    };
1884
1885    // Get the active variant name (Peek is Copy, variant names are 'static)
1886    let active_name = poke
1887        .as_peek()
1888        .into_enum()
1889        .ok()
1890        .and_then(|e| e.active_variant().ok())
1891        .map(|v| v.effective_name())
1892        .unwrap_or("?");
1893
1894    let mut changed = false;
1895    let r = egui::ComboBox::from_id_salt(id)
1896        .selected_text(active_name)
1897        .show_ui(ui, |ui| {
1898            for (idx, variant) in enum_type.variants.iter().enumerate() {
1899                let variant_name: &str = variant.effective_name();
1900                let is_active = variant_name == active_name;
1901                if ui.selectable_label(is_active, variant_name).clicked()
1902                    && !is_active
1903                    && try_change_variant(poke, idx)
1904                {
1905                    changed = true;
1906                }
1907            }
1908        });
1909
1910    let mut r = r.response;
1911    if changed {
1912        r.mark_changed();
1913    }
1914    r
1915}
1916
1917/// Try to change the enum variant by constructing a new value via `Partial`.
1918///
1919/// Returns `true` if the variant was successfully changed.
1920fn try_change_variant(poke: &mut Poke<'_, '_>, variant_idx: usize) -> bool {
1921    let shape = poke.shape();
1922    // Build a new enum value with the selected variant using Partial.
1923    // SAFETY: The shape used is from the provided Poke
1924    let partial = match unsafe { Partial::alloc_shape(shape) } {
1925        Ok(p) => p,
1926        Err(e) => {
1927            log::debug!("alloc_shape failed: {e}");
1928            return false;
1929        }
1930    };
1931    // this is the partial of the to be active variant
1932    let mut partial = match partial.select_nth_variant(variant_idx) {
1933        Ok(p) => p,
1934        Err(e) => {
1935            log::debug!("select_nth_variant failed: {e}");
1936            return false;
1937        }
1938    };
1939
1940    // Explicitly default each field of the variant.
1941    // The variant's fields are available from the shape's enum type.
1942    let Type::User(UserType::Enum(enum_type)) = shape.ty else {
1943        return false;
1944    };
1945    let variant = &enum_type.variants[variant_idx];
1946    for field_idx in 0..variant.data.fields.len() {
1947        partial = match partial.set_nth_field_to_default(field_idx) {
1948            Ok(p) => p,
1949            Err(e) => {
1950                log::debug!(
1951                    "set_nth_field_to_default({field_idx}) failed for variant '{}': {e}",
1952                    variant.effective_name()
1953                );
1954                return false;
1955            }
1956        };
1957    }
1958
1959    let heap_value = match partial.build() {
1960        Ok(v) => v,
1961        Err(e) => {
1962            log::debug!("build failed: {e}");
1963            return false;
1964        }
1965    };
1966
1967    let size = shape
1968        .layout
1969        .sized_layout()
1970        .expect("enum must be sized")
1971        .size();
1972
1973    // FIXME: replace once <https://github.com/facet-rs/facet/issues/2152> is implemented
1974    assert_eq!(poke.shape(), heap_value.shape());
1975    // SAFETY: the Shape is the same and this is the same as core::mem::replace
1976    // if we had T
1977    unsafe {
1978        // Swap the old enum value (in poke) with the new one (in heap_value).
1979        // After the swap, heap_value holds the old value — its Drop impl will
1980        // call drop_in_place on it and then free the allocation.
1981        let dst = poke.data_mut().as_mut_byte_ptr();
1982        let src = heap_value.peek().data().as_byte_ptr() as *mut u8;
1983        core::ptr::swap_nonoverlapping(dst, src, size);
1984    }
1985    drop(heap_value);
1986
1987    true
1988}
1989
1990fn show_inline_poke_scalar(
1991    poke: &mut Poke<'_, '_>,
1992    scalar_type: ScalarType,
1993    ui: &mut Ui,
1994) -> Response {
1995    match scalar_type {
1996        ScalarType::Bool => {
1997            if let Ok(v) = poke.get_mut::<bool>() {
1998                return ui.add(Checkbox::without_text(v));
1999            }
2000        }
2001        ScalarType::U8 => {
2002            if let Ok(v) = poke.get_mut::<u8>() {
2003                return ui.add(egui::DragValue::new(v));
2004            }
2005        }
2006        ScalarType::U16 => {
2007            if let Ok(v) = poke.get_mut::<u16>() {
2008                return ui.add(egui::DragValue::new(v));
2009            }
2010        }
2011        ScalarType::U32 => {
2012            if let Ok(v) = poke.get_mut::<u32>() {
2013                return ui.add(egui::DragValue::new(v));
2014            }
2015        }
2016        ScalarType::U64 => {
2017            if let Ok(v) = poke.get_mut::<u64>() {
2018                return ui.add(egui::DragValue::new(v));
2019            }
2020        }
2021        ScalarType::U128 => {
2022            // DragValue doesn't support u128, show as label
2023            if let Ok(v) = poke.get::<u128>() {
2024                return ui.label(alloc::format!("{v}"));
2025            }
2026        }
2027        ScalarType::USize => {
2028            if let Ok(v) = poke.get_mut::<usize>() {
2029                return ui.add(egui::DragValue::new(v));
2030            }
2031        }
2032        ScalarType::I8 => {
2033            if let Ok(v) = poke.get_mut::<i8>() {
2034                return ui.add(egui::DragValue::new(v));
2035            }
2036        }
2037        ScalarType::I16 => {
2038            if let Ok(v) = poke.get_mut::<i16>() {
2039                return ui.add(egui::DragValue::new(v));
2040            }
2041        }
2042        ScalarType::I32 => {
2043            if let Ok(v) = poke.get_mut::<i32>() {
2044                return ui.add(egui::DragValue::new(v));
2045            }
2046        }
2047        ScalarType::I64 => {
2048            if let Ok(v) = poke.get_mut::<i64>() {
2049                return ui.add(egui::DragValue::new(v));
2050            }
2051        }
2052        ScalarType::I128 => {
2053            if let Ok(v) = poke.get::<i128>() {
2054                return ui.label(alloc::format!("{v}"));
2055            }
2056        }
2057        ScalarType::ISize => {
2058            if let Ok(v) = poke.get_mut::<isize>() {
2059                return ui.add(egui::DragValue::new(v));
2060            }
2061        }
2062        ScalarType::F32 => {
2063            if let Ok(v) = poke.get_mut::<f32>() {
2064                return ui.add(egui::DragValue::new(v));
2065            }
2066        }
2067        ScalarType::F64 => {
2068            if let Ok(v) = poke.get_mut::<f64>() {
2069                return ui.add(egui::DragValue::new(v));
2070            }
2071        }
2072        ScalarType::String => {
2073            if let Ok(v) = poke.get_mut::<String>() {
2074                if v.contains('\n') {
2075                    let rows = text_line_count(v);
2076                    return ui.add(TextEdit::multiline(v).desired_rows(rows));
2077                }
2078                let mut r = ui.add(TextEdit::singleline(v));
2079                if (r.has_focus() || r.lost_focus()) && shift_enter_pressed(ui) {
2080                    v.push('\n');
2081                    r.mark_changed();
2082                    r.request_focus();
2083                }
2084                return r;
2085            }
2086        }
2087        ScalarType::Char => {
2088            if let Ok(v) = poke.get::<char>() {
2089                let s = v.to_string();
2090                return ui.add_enabled(false, TextEdit::singleline(&mut s.as_str()));
2091            }
2092        }
2093        ScalarType::Str => {
2094            // str is unsized, fall through to display
2095            if poke.shape().is_display() {
2096                return ui.label(alloc::format!("{}", poke.as_peek()));
2097            }
2098        }
2099        ScalarType::CowStr => {
2100            if let Ok(v) = poke.get::<Cow<'_, str>>() {
2101                let mut s = v.clone();
2102                if s.contains('\n') {
2103                    let rows = text_line_count(&s);
2104                    return ui.add_enabled(false, TextEdit::multiline(&mut s).desired_rows(rows));
2105                }
2106                return ui.add_enabled(false, TextEdit::singleline(&mut s));
2107            }
2108        }
2109        _ if poke.shape().is_display() => {
2110            return ui.label(alloc::format!("{}", poke.as_peek()));
2111        }
2112        _ if poke.shape().is_debug() => {
2113            return ui.label(alloc::format!("{:?}", poke.as_peek()));
2114        }
2115        _ => {}
2116    }
2117    ui.colored_label(
2118        Color32::YELLOW,
2119        alloc::format!("unsupported scalar: {scalar_type:?}"),
2120    )
2121}
2122
2123fn show_inline_peek(peek: Peek<'_, '_>, ui: &mut Ui, id: Id, as_display: bool) -> Response {
2124    if as_display || should_render_as_display(peek.shape(), &[]) {
2125        return ui.label(alloc::format!("{}", peek));
2126    }
2127
2128    if let Some(scalar_type) = peek.scalar_type() {
2129        return show_inline_peek_scalar(peek, scalar_type, ui);
2130    }
2131
2132    // Option/Result have Def::Option/Def::Result but Type::User(UserType::Enum),
2133    // so check into_option before into_enum to avoid misrouting.
2134    if let Ok(opt) = peek.into_option() {
2135        return show_inline_peek_option(opt, ui, id);
2136    }
2137    if let Ok(enu) = peek.into_enum() {
2138        if let Ok(variant) = enu.active_variant() {
2139            return ui.weak(variant.effective_name());
2140        }
2141        return ui.weak("enum");
2142    }
2143    if let Ok(_struc) = peek.into_struct() {
2144        return ui.weak(shape_display_name(peek.shape()));
2145    }
2146    if let Ok(list) = peek.into_list_like() {
2147        return ui.weak(alloc::format!("[{}]", list.len()));
2148    }
2149    if let Ok(map) = peek.into_map() {
2150        return ui.weak(alloc::format!("[{}]", map.len()));
2151    }
2152    if let Ok(tuple) = peek.into_tuple() {
2153        return ui.weak(alloc::format!("({})", tuple.len()));
2154    }
2155    if let Ok(ptr) = peek.into_pointer()
2156        && let Some(inner) = ptr.borrow_inner()
2157    {
2158        return show_inline_peek(inner, ui, id.with("ptr"), false);
2159    }
2160
2161    ui.weak(shape_display_name(peek.shape()))
2162}
2163
2164fn show_inline_peek_scalar(peek: Peek<'_, '_>, scalar_type: ScalarType, ui: &mut Ui) -> Response {
2165    match scalar_type {
2166        ScalarType::Bool => {
2167            if let Ok(v) = peek.get::<bool>() {
2168                let mut value = *v;
2169                return ui.add_enabled(false, Checkbox::without_text(&mut value));
2170            }
2171        }
2172        ScalarType::Char => {
2173            if let Ok(c) = peek.get::<char>() {
2174                let s = c.to_string();
2175                return ui.add_enabled(false, TextEdit::singleline(&mut s.as_str()));
2176            }
2177        }
2178        ScalarType::Str => {
2179            if let Ok(v) = peek.get::<str>() {
2180                let mut s = v;
2181                if s.contains('\n') {
2182                    let rows = text_line_count(s);
2183                    return ui.add_enabled(false, TextEdit::multiline(&mut s).desired_rows(rows));
2184                }
2185                return ui.add_enabled(false, TextEdit::singleline(&mut s));
2186            }
2187        }
2188        ScalarType::CowStr => {
2189            if let Ok(v) = peek.get::<Cow<'_, str>>() {
2190                let mut s = v.clone();
2191                if s.contains('\n') {
2192                    let rows = text_line_count(&s);
2193                    return ui.add_enabled(false, TextEdit::multiline(&mut s).desired_rows(rows));
2194                }
2195                return ui.add_enabled(false, TextEdit::singleline(&mut s));
2196            }
2197        }
2198        ScalarType::String => {
2199            if let Ok(v) = peek.get::<String>() {
2200                let mut s: Cow<'_, str> = Cow::Borrowed(v.as_str());
2201                if s.contains('\n') {
2202                    let rows = text_line_count(&s);
2203                    return ui.add_enabled(false, TextEdit::multiline(&mut s).desired_rows(rows));
2204                }
2205                return ui.add_enabled(false, TextEdit::singleline(&mut s));
2206            }
2207        }
2208        _ if peek.shape().is_display() => {
2209            return ui.label(alloc::format!("{}", peek));
2210        }
2211        _ if peek.shape().is_debug() => {
2212            return ui.label(alloc::format!("{:?}", peek));
2213        }
2214        _ => {}
2215    }
2216    ui.colored_label(
2217        Color32::YELLOW,
2218        alloc::format!("unsupported scalar: {scalar_type:?}"),
2219    )
2220}
2221
2222fn show_inline_peek_option(opt: PeekOption<'_, '_>, ui: &mut Ui, id: Id) -> Response {
2223    ui.horizontal(|ui| {
2224        let is_some = opt.value().is_some();
2225        let _ = ui.selectable_label(!is_some, "None");
2226        let _ = ui.selectable_label(is_some, "Some");
2227        if let Some(inner) = opt.value() {
2228            show_inline_peek(inner, ui, id.with("some"), false);
2229        }
2230    })
2231    .response
2232}