Skip to main content

guise/devtools/
probe.rs

1//! The element-tree recorder behind the Elements panel.
2//!
3//! gpui knows which element the pointer is over — that is what
4//! `Window::toggle_inspector` picking gives you — but it will not enumerate a
5//! tree: `inspector_hitboxes` is crate-private and only ever holds one frame of
6//! whatever happened to be under the cursor. A DOM-style outline has to be
7//! recorded by the thing being inspected.
8//!
9//! So `guise` records its own. [`Probed::probe`] wraps a component's root
10//! element in a pass-through [`Probe`] that pushes a node on the way into
11//! `prepaint` and pops it on the way out. gpui prepaints depth-first, so the
12//! push/pop pairs nest exactly like the element tree does, and the arena that
13//! falls out is the tree the panel renders.
14//!
15//! Two properties make this affordable to leave in every component:
16//!
17//! * It is off unless the inspector is open ([`set_enabled`]), and off means
18//!   two boolean checks per wrapped element per frame.
19//! * It never allocates while off — attributes are dropped at the setter.
20//!
21//! Every window on a thread shares one recorder, so an inspector claims the
22//! window it renders in ([`begin_frame`]) and elements prepainting in any
23//! other window are skipped. Without that, an app with a second window open
24//! records both trees into one and the inspector shows a tree its panels
25//! cannot explain.
26//!
27//! The recorder always runs one frame behind: an entity's `render` happens
28//! during `request_layout`, before anything has prepainted, so the panel reads
29//! the tree the *previous* frame built. [`begin_frame`] is what rotates them.
30
31use std::cell::RefCell;
32use std::panic::Location;
33
34use gpui::{
35    App, Bounds, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, Pixels,
36    SharedString, StyleRefinement, Styled, Window, WindowId,
37};
38
39use super::state::SourceRef;
40
41/// One recorded element: a row in the Elements tree.
42#[derive(Debug, Clone)]
43pub struct ProbeNode {
44    /// The component name, rendered as the tag: `Button` shows as `<Button>`.
45    pub name: SharedString,
46    /// Attributes shown inline after the tag, as a DOM node shows its
47    /// attributes. Ordered as the component declared them.
48    pub attrs: Vec<(SharedString, SharedString)>,
49    /// The gpui element id, when the element has one.
50    pub element_id: Option<SharedString>,
51    /// Where the component was constructed, for the Node panel and Sources.
52    pub source: Option<SourceRef>,
53    /// Laid-out bounds, captured during prepaint. This is what the highlight
54    /// overlay and the box model read.
55    pub bounds: Bounds<Pixels>,
56    /// The element's own style, snapshotted before it was laid out. Boxed
57    /// because a `StyleRefinement` is large and most of a tree's memory would
58    /// otherwise be styles nobody has selected.
59    pub style: Option<Box<StyleRefinement>>,
60    /// Index of the parent in the arena, or `None` for a root.
61    pub parent: Option<usize>,
62    /// Indices of the children, in paint order.
63    pub children: Vec<usize>,
64    /// Nesting depth, precomputed for the tree's indentation.
65    pub depth: usize,
66    /// A path key that survives across frames, so a selection outlives the
67    /// frame it was made in: parent path + tag + sibling ordinal.
68    pub key: SharedString,
69}
70
71impl ProbeNode {
72    /// Whether the node has no children — a leaf renders as `<Tag />`.
73    pub fn is_leaf(&self) -> bool {
74        self.children.is_empty()
75    }
76}
77
78/// A recorded tree: a flat arena plus its roots, in the order they prepainted.
79#[derive(Debug, Clone, Default)]
80pub struct ProbeTree {
81    pub nodes: Vec<ProbeNode>,
82    pub roots: Vec<usize>,
83}
84
85impl ProbeTree {
86    pub fn is_empty(&self) -> bool {
87        self.nodes.is_empty()
88    }
89
90    pub fn len(&self) -> usize {
91        self.nodes.len()
92    }
93
94    pub fn get(&self, index: usize) -> Option<&ProbeNode> {
95        self.nodes.get(index)
96    }
97
98    /// Find a node by the stable key a previous frame handed out.
99    pub fn find(&self, key: &str) -> Option<usize> {
100        self.nodes.iter().position(|n| n.key.as_ref() == key)
101    }
102
103    /// The chain from the root down to `index`, which is what the Elements
104    /// panel's breadcrumb bar shows.
105    pub fn ancestry(&self, index: usize) -> Vec<usize> {
106        let mut chain = Vec::new();
107        let mut cursor = Some(index);
108        while let Some(i) = cursor {
109            chain.push(i);
110            cursor = self.nodes.get(i).and_then(|n| n.parent);
111        }
112        chain.reverse();
113        chain
114    }
115
116    /// The deepest node whose bounds contain `point`, searched in reverse paint
117    /// order so the topmost element wins — the same rule as hit testing.
118    pub fn hit(&self, point: gpui::Point<Pixels>) -> Option<usize> {
119        let mut best: Option<(usize, usize)> = None;
120        for (index, node) in self.nodes.iter().enumerate() {
121            if node.bounds.contains(&point) {
122                let deeper = best.is_none_or(|(_, depth)| node.depth >= depth);
123                if deeper {
124                    best = Some((index, node.depth));
125                }
126            }
127        }
128        best.map(|(index, _)| index)
129    }
130}
131
132/// Recording state. Thread-local rather than a gpui `Global` because element
133/// methods run in the hot path of every frame and a thread-local read is a
134/// pointer deref, where `App::global` is a hash lookup.
135#[derive(Default)]
136struct Registry {
137    /// How many inspectors are alive.
138    ///
139    /// A count rather than a flag because instances overlap: replacing a
140    /// `DevTools` constructs the new one before dropping the old, so a boolean
141    /// would have the old one's `Drop` switch recording off underneath its
142    /// replacement.
143    recorders: usize,
144    /// The tree the current frame is prepainting into.
145    building: ProbeTree,
146    /// The last completed tree — what panels read.
147    current: ProbeTree,
148    /// Open ancestors, innermost last.
149    stack: Vec<usize>,
150    /// The window whose inspector claimed this frame, if one did. `None` means
151    /// record every window, which is what a host driving the recorder by hand
152    /// through [`set_enabled`] wants.
153    window: Option<WindowId>,
154}
155
156impl Registry {
157    fn is_recording(&self) -> bool {
158        self.recorders > 0
159    }
160
161    fn clear(&mut self) {
162        self.building = ProbeTree::default();
163        self.current = ProbeTree::default();
164        self.stack.clear();
165        self.window = None;
166    }
167}
168
169thread_local! {
170    static REGISTRY: RefCell<Registry> = RefCell::new(Registry::default());
171}
172
173/// Turn recording on or off outright, ignoring how many inspectors are alive.
174/// For a host driving the recorder by hand, and for tests; an inspector uses
175/// [`retain`] and [`release`] instead.
176pub fn set_enabled(enabled: bool) {
177    REGISTRY.with(|registry| {
178        let mut registry = registry.borrow_mut();
179        registry.recorders = usize::from(enabled);
180        if !enabled {
181            registry.clear();
182        }
183    });
184}
185
186/// Register an inspector. Recording starts on the first one.
187pub(crate) fn retain() {
188    REGISTRY.with(|registry| registry.borrow_mut().recorders += 1);
189}
190
191/// Drop an inspector. Recording stops, and the recorded tree is released, when
192/// the last one goes.
193pub(crate) fn release() {
194    REGISTRY.with(|registry| {
195        let mut registry = registry.borrow_mut();
196        registry.recorders = registry.recorders.saturating_sub(1);
197        if registry.recorders == 0 {
198            registry.clear();
199        }
200    });
201}
202
203pub fn is_enabled() -> bool {
204    REGISTRY.with(|registry| registry.borrow().recorders > 0)
205}
206
207/// Promote the tree the last frame recorded and start a fresh one, claiming
208/// this frame for `window`. Called from the inspector's `render`, which runs
209/// before any of this frame's prepaints, so elements in every other window
210/// this thread draws are skipped for the rest of the frame.
211///
212/// There is one tree per thread, so two inspectors in two windows take the
213/// claim from each other every frame and both come up empty. Showing each of
214/// them a tree of both windows would be worse.
215pub fn begin_frame(window: &Window) {
216    rotate(Some(window.window_handle().window_id()))
217}
218
219/// Rotate with no window claimed, so every window records. For the sibling
220/// tests, which drive the recorder without standing up a window.
221#[cfg(test)]
222pub(crate) fn begin_frame_unclaimed() {
223    rotate(None)
224}
225
226fn rotate(claim: Option<WindowId>) {
227    REGISTRY.with(|registry| {
228        let mut registry = registry.borrow_mut();
229        if !registry.is_recording() {
230            return;
231        }
232        let previous = std::mem::replace(&mut registry.window, claim);
233        // An unbalanced stack would mean an element was pushed and never
234        // popped; drop it rather than nesting the next frame under a ghost.
235        registry.stack.clear();
236        let built = std::mem::take(&mut registry.building);
237        // A tree recorded before this window held the claim belongs to some
238        // other window, or to no window in particular — the frame an inspector
239        // first opens on. Drop it rather than show a tree its panels cannot
240        // explain; the next frame records a real one.
241        if previous == claim && !built.is_empty() {
242            registry.current = built;
243        }
244    });
245}
246
247/// The most recently completed tree.
248pub fn tree() -> ProbeTree {
249    REGISTRY.with(|registry| registry.borrow().current.clone())
250}
251
252/// Run `f` against the current tree without cloning it — for a host that wants
253/// to inspect the tree without paying for a copy of it.
254pub fn with_tree<R>(f: impl FnOnce(&ProbeTree) -> R) -> R {
255    REGISTRY.with(|registry| f(&registry.borrow().current))
256}
257
258fn push(meta: &ProbeMeta, window: Option<WindowId>) -> Option<usize> {
259    REGISTRY.with(|registry| {
260        let mut registry = registry.borrow_mut();
261        if !registry.is_recording() {
262            return None;
263        }
264        // Another window prepainting into a tree this window's inspector
265        // claimed. Its elements are not what is being inspected.
266        if registry.window.is_some() && registry.window != window {
267            return None;
268        }
269
270        let parent = registry.stack.last().copied();
271        let depth = registry.stack.len();
272        let ordinal = match parent {
273            Some(parent) => registry.building.nodes[parent].children.len(),
274            None => registry.building.roots.len(),
275        };
276        let key = match parent {
277            Some(parent) => format!(
278                "{}/{}[{}]",
279                registry.building.nodes[parent].key, meta.name, ordinal
280            ),
281            None => format!("{}[{}]", meta.name, ordinal),
282        };
283
284        let index = registry.building.nodes.len();
285        registry.building.nodes.push(ProbeNode {
286            name: meta.name.clone(),
287            attrs: meta.attrs.clone(),
288            element_id: None,
289            source: meta.source.clone(),
290            bounds: Bounds::default(),
291            style: meta.style.clone(),
292            parent,
293            children: Vec::new(),
294            depth,
295            key: key.into(),
296        });
297
298        match parent {
299            Some(parent) => registry.building.nodes[parent].children.push(index),
300            None => registry.building.roots.push(index),
301        }
302        registry.stack.push(index);
303        Some(index)
304    })
305}
306
307fn pop(index: usize, bounds: Bounds<Pixels>, element_id: Option<ElementId>) {
308    REGISTRY.with(|registry| {
309        let mut registry = registry.borrow_mut();
310        if let Some(node) = registry.building.nodes.get_mut(index) {
311            node.bounds = bounds;
312            node.element_id = element_id.map(|id| SharedString::from(id.to_string()));
313        }
314        // Pop back to this node's own frame. A child that failed to pop would
315        // otherwise leave the stack permanently deeper.
316        while let Some(top) = registry.stack.pop() {
317            if top == index {
318                break;
319            }
320        }
321    });
322}
323
324/// Drive the recorder directly. Only for tests in sibling modules that need a
325/// tree without standing up a window and a full element pass.
326#[cfg(test)]
327pub(crate) fn test_record(name: &'static str, children: impl FnOnce()) {
328    let meta = ProbeMeta {
329        name: SharedString::new_static(name),
330        attrs: Vec::new(),
331        source: None,
332        style: None,
333    };
334    let index = push(&meta, None);
335    children();
336    if let Some(index) = index {
337        pop(index, Bounds::default(), None);
338    }
339}
340
341/// What a probe knows about the element it wraps, before it becomes one.
342#[derive(Debug, Clone)]
343struct ProbeMeta {
344    name: SharedString,
345    attrs: Vec<(SharedString, SharedString)>,
346    source: Option<SourceRef>,
347    style: Option<Box<StyleRefinement>>,
348}
349
350/// A component's root element, tagged for the Elements panel. Built by
351/// [`Probed::probe`].
352pub struct Probe<E> {
353    inner: E,
354    meta: ProbeMeta,
355    /// Recording state, sampled once at construction so the attribute setters
356    /// can skip their allocations entirely while the inspector is closed.
357    recording: bool,
358}
359
360impl<E> Probe<E> {
361    /// Add an attribute, shown inline after the tag name. Dropped without
362    /// allocating when the inspector is closed.
363    pub fn attr(mut self, name: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
364        if self.recording {
365            self.meta.attrs.push((name.into(), value.into()));
366        }
367        self
368    }
369
370    /// Add an attribute whose value costs something to build. The closure runs
371    /// only while the inspector is recording, so a `format!` in a component's
372    /// hot render path is not paid for by every release build.
373    pub fn attr_with<V: Into<SharedString>>(
374        mut self,
375        name: impl Into<SharedString>,
376        value: impl FnOnce() -> V,
377    ) -> Self {
378        if self.recording {
379            self.meta.attrs.push((name.into(), value().into()));
380        }
381        self
382    }
383
384    /// Add an attribute only when `value` is set — the usual shape for a
385    /// component's optional props.
386    pub fn attr_opt(
387        self,
388        name: impl Into<SharedString>,
389        value: Option<impl Into<SharedString>>,
390    ) -> Self {
391        match value {
392            Some(value) => self.attr(name, value),
393            None => self,
394        }
395    }
396
397    /// Add an attribute only when `present`, the way a boolean HTML attribute
398    /// either appears bare or not at all.
399    pub fn attr_if(self, name: impl Into<SharedString>, present: bool) -> Self {
400        if present {
401            self.attr(name, "")
402        } else {
403            self
404        }
405    }
406}
407
408/// Tag any element as a component for the Elements panel.
409///
410/// Call it last, on the element a component's `render` returns:
411///
412/// ```ignore
413/// impl RenderOnce for Button {
414///     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
415///         div()
416///             // ...
417///             .probe("Button")
418///             .attr("variant", self.variant.label())
419///     }
420/// }
421/// ```
422///
423/// The bound is [`Styled`] rather than plain [`IntoElement`] so the probe can
424/// snapshot the element's style on the way past — that snapshot is the whole
425/// Styles and Computed sidebar. Every component root is a styled element, so
426/// in practice this costs nothing.
427pub trait Probed: IntoElement + Styled + Sized {
428    /// Wrap this element in a probe named `name`.
429    #[track_caller]
430    fn probe(mut self, name: impl Into<SharedString>) -> Probe<Self> {
431        let recording = is_enabled();
432        let style = recording.then(|| Box::new(self.style().clone()));
433        // `Location::caller()` has to be read here rather than inside the
434        // `then` closure: a closure body is not `#[track_caller]`, so it would
435        // resolve to this file instead of to the component that called us.
436        let caller = Location::caller();
437        Probe {
438            inner: self,
439            meta: ProbeMeta {
440                name: name.into(),
441                attrs: Vec::new(),
442                source: recording.then(|| SourceRef::from(caller)),
443                style,
444            },
445            recording,
446        }
447    }
448}
449
450impl<E: IntoElement + Styled> Probed for E {}
451
452/// The same, for an element that has no style of its own.
453///
454/// A handful of components return something already composed — a `Field`, a
455/// `deferred(..)` overlay, another component — rather than a styled element.
456/// Those have no `StyleRefinement` to hand over; the style that matters belongs
457/// to whatever they wrapped, and that reports itself separately. They still
458/// belong in the tree, so they probe through here, and their Styles sidebar
459/// reads as empty — which is the truth.
460pub trait ProbedAny: IntoElement + Sized {
461    /// Wrap this element in a probe named `name`, without a style snapshot.
462    #[track_caller]
463    fn probe_any(self, name: impl Into<SharedString>) -> Probe<Self> {
464        let recording = is_enabled();
465        let caller = Location::caller();
466        Probe {
467            inner: self,
468            meta: ProbeMeta {
469                name: name.into(),
470                attrs: Vec::new(),
471                source: recording.then(|| SourceRef::from(caller)),
472                style: None,
473            },
474            recording,
475        }
476    }
477}
478
479impl<E: IntoElement> ProbedAny for E {}
480
481impl<E: IntoElement> IntoElement for Probe<E> {
482    type Element = ProbeElement<E::Element>;
483
484    fn into_element(self) -> Self::Element {
485        ProbeElement {
486            inner: self.inner.into_element(),
487            meta: self.meta,
488            index: None,
489        }
490    }
491}
492
493/// The element half of [`Probe`]: forwards every call to the wrapped element,
494/// and brackets `prepaint` with the push/pop that builds the tree.
495pub struct ProbeElement<E> {
496    inner: E,
497    meta: ProbeMeta,
498    /// Arena slot claimed during prepaint, carried to the pop.
499    index: Option<usize>,
500}
501
502impl<E: gpui::Element> IntoElement for ProbeElement<E> {
503    type Element = Self;
504
505    fn into_element(self) -> Self::Element {
506        self
507    }
508}
509
510impl<E: gpui::Element> gpui::Element for ProbeElement<E> {
511    type RequestLayoutState = E::RequestLayoutState;
512    type PrepaintState = E::PrepaintState;
513
514    fn id(&self) -> Option<ElementId> {
515        self.inner.id()
516    }
517
518    fn source_location(&self) -> Option<&'static Location<'static>> {
519        self.inner.source_location()
520    }
521
522    fn request_layout(
523        &mut self,
524        id: Option<&GlobalElementId>,
525        inspector_id: Option<&InspectorElementId>,
526        window: &mut Window,
527        cx: &mut App,
528    ) -> (LayoutId, Self::RequestLayoutState) {
529        self.inner.request_layout(id, inspector_id, window, cx)
530    }
531
532    fn prepaint(
533        &mut self,
534        id: Option<&GlobalElementId>,
535        inspector_id: Option<&InspectorElementId>,
536        bounds: Bounds<Pixels>,
537        request_layout: &mut Self::RequestLayoutState,
538        window: &mut Window,
539        cx: &mut App,
540    ) -> Self::PrepaintState {
541        self.index = push(&self.meta, Some(window.window_handle().window_id()));
542        let state = self
543            .inner
544            .prepaint(id, inspector_id, bounds, request_layout, window, cx);
545        if let Some(index) = self.index {
546            pop(index, bounds, self.inner.id());
547        }
548        state
549    }
550
551    fn paint(
552        &mut self,
553        id: Option<&GlobalElementId>,
554        inspector_id: Option<&InspectorElementId>,
555        bounds: Bounds<Pixels>,
556        request_layout: &mut Self::RequestLayoutState,
557        prepaint: &mut Self::PrepaintState,
558        window: &mut Window,
559        cx: &mut App,
560    ) {
561        self.inner.paint(
562            id,
563            inspector_id,
564            bounds,
565            request_layout,
566            prepaint,
567            window,
568            cx,
569        );
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576
577    /// The recorder is thread-local and these tests drive it directly, so each
578    /// one starts from a known state.
579    fn reset() {
580        set_enabled(false);
581        set_enabled(true);
582    }
583
584    fn meta(name: &str) -> ProbeMeta {
585        ProbeMeta {
586            name: SharedString::from(name.to_owned()),
587            attrs: Vec::new(),
588            source: None,
589            style: None,
590        }
591    }
592
593    fn record(name: &str, children: impl FnOnce()) {
594        let index = push(&meta(name), None);
595        children();
596        if let Some(index) = index {
597            pop(index, Bounds::default(), None);
598        }
599    }
600
601    #[test]
602    fn nesting_follows_the_push_pop_pairs() {
603        reset();
604        record("Stack", || {
605            record("Button", || {});
606            record("Badge", || {});
607        });
608        begin_frame_unclaimed();
609
610        let tree = tree();
611        assert_eq!(tree.roots, vec![0]);
612        assert_eq!(tree.nodes[0].name.as_ref(), "Stack");
613        assert_eq!(tree.nodes[0].children, vec![1, 2]);
614        assert_eq!(tree.nodes[1].parent, Some(0));
615        assert_eq!(tree.nodes[1].depth, 1);
616        assert_eq!(tree.nodes[2].name.as_ref(), "Badge");
617    }
618
619    #[test]
620    fn keys_are_path_plus_sibling_ordinal() {
621        reset();
622        record("Stack", || {
623            record("Button", || {});
624            record("Button", || {});
625        });
626        begin_frame_unclaimed();
627
628        let tree = tree();
629        assert_eq!(tree.nodes[0].key.as_ref(), "Stack[0]");
630        assert_eq!(tree.nodes[1].key.as_ref(), "Stack[0]/Button[0]");
631        assert_eq!(tree.nodes[2].key.as_ref(), "Stack[0]/Button[1]");
632        assert_eq!(tree.find("Stack[0]/Button[1]"), Some(2));
633    }
634
635    #[test]
636    fn a_key_survives_the_next_frame() {
637        reset();
638        record("Stack", || record("Button", || {}));
639        begin_frame_unclaimed();
640        let before = tree().find("Stack[0]/Button[0]");
641
642        record("Stack", || record("Button", || {}));
643        begin_frame_unclaimed();
644        let after = tree().find("Stack[0]/Button[0]");
645
646        assert_eq!(before, after);
647        assert!(after.is_some());
648    }
649
650    #[test]
651    fn ancestry_runs_root_first() {
652        reset();
653        record("AppShell", || record("Stack", || record("Button", || {})));
654        begin_frame_unclaimed();
655
656        let tree = tree();
657        let button = tree.find("AppShell[0]/Stack[0]/Button[0]").unwrap();
658        let names: Vec<_> = tree
659            .ancestry(button)
660            .into_iter()
661            .map(|i| tree.nodes[i].name.to_string())
662            .collect();
663        assert_eq!(names, vec!["AppShell", "Stack", "Button"]);
664    }
665
666    #[test]
667    fn multiple_roots_are_kept_in_order() {
668        reset();
669        record("AppShell", || {});
670        record("Modal", || {});
671        begin_frame_unclaimed();
672
673        let tree = tree();
674        assert_eq!(tree.roots, vec![0, 1]);
675        assert_eq!(tree.nodes[1].key.as_ref(), "Modal[1]");
676    }
677
678    #[test]
679    fn overlapping_inspectors_keep_recording_until_the_last_one_goes() {
680        set_enabled(false);
681        assert!(!is_enabled());
682
683        retain();
684        assert!(is_enabled());
685
686        // Replacing an inspector constructs the new one before dropping the
687        // old; a flag would have the old one's `Drop` switch recording off
688        // underneath its replacement.
689        retain();
690        release();
691        assert!(is_enabled());
692
693        release();
694        assert!(!is_enabled());
695    }
696
697    #[test]
698    fn an_unbalanced_release_cannot_underflow() {
699        set_enabled(false);
700        release();
701        release();
702        retain();
703        assert!(is_enabled());
704        release();
705        assert!(!is_enabled());
706    }
707
708    #[test]
709    fn the_recorded_tree_is_released_with_the_last_inspector() {
710        reset();
711        record("Stack", || {});
712        begin_frame_unclaimed();
713        assert!(!tree().is_empty());
714
715        release();
716        assert!(tree().is_empty());
717    }
718
719    #[test]
720    fn nothing_records_while_disabled() {
721        reset();
722        set_enabled(false);
723        record("Stack", || record("Button", || {}));
724        begin_frame_unclaimed();
725
726        assert!(tree().is_empty());
727    }
728
729    #[test]
730    fn hit_testing_picks_the_deepest_containing_node() {
731        reset();
732        let outer = push(&meta("Stack"), None).unwrap();
733        let inner = push(&meta("Button"), None).unwrap();
734        pop(
735            inner,
736            Bounds {
737                origin: gpui::point(gpui::px(10.0), gpui::px(10.0)),
738                size: gpui::size(gpui::px(50.0), gpui::px(20.0)),
739            },
740            None,
741        );
742        pop(
743            outer,
744            Bounds {
745                origin: gpui::point(gpui::px(0.0), gpui::px(0.0)),
746                size: gpui::size(gpui::px(200.0), gpui::px(100.0)),
747            },
748            None,
749        );
750        begin_frame_unclaimed();
751
752        let tree = tree();
753        let hit = tree
754            .hit(gpui::point(gpui::px(20.0), gpui::px(15.0)))
755            .unwrap();
756        assert_eq!(tree.nodes[hit].name.as_ref(), "Button");
757
758        let outside = tree
759            .hit(gpui::point(gpui::px(150.0), gpui::px(80.0)))
760            .unwrap();
761        assert_eq!(tree.nodes[outside].name.as_ref(), "Stack");
762
763        assert!(tree
764            .hit(gpui::point(gpui::px(400.0), gpui::px(400.0)))
765            .is_none());
766    }
767
768    #[test]
769    fn an_unbalanced_stack_does_not_leak_into_the_next_frame() {
770        reset();
771        // Push without popping, as a panicking prepaint would leave things.
772        push(&meta("Orphan"), None);
773        begin_frame_unclaimed();
774        record("Stack", || {});
775        begin_frame_unclaimed();
776
777        let tree = tree();
778        assert_eq!(tree.roots.len(), 1);
779        assert_eq!(tree.nodes[tree.roots[0]].name.as_ref(), "Stack");
780        assert_eq!(tree.nodes[tree.roots[0]].depth, 0);
781    }
782}