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