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}