1use 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#[derive(Debug, Clone)]
43pub struct ProbeNode {
44 pub name: SharedString,
46 pub attrs: Vec<(SharedString, SharedString)>,
49 pub element_id: Option<SharedString>,
51 pub source: Option<SourceRef>,
53 pub bounds: Bounds<Pixels>,
56 pub style: Option<Box<StyleRefinement>>,
60 pub parent: Option<usize>,
62 pub children: Vec<usize>,
64 pub depth: usize,
66 pub key: SharedString,
69}
70
71impl ProbeNode {
72 pub fn is_leaf(&self) -> bool {
74 self.children.is_empty()
75 }
76}
77
78#[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 pub fn find(&self, key: &str) -> Option<usize> {
100 self.nodes.iter().position(|n| n.key.as_ref() == key)
101 }
102
103 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 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#[derive(Default)]
136struct Registry {
137 recorders: usize,
144 building: ProbeTree,
146 current: ProbeTree,
148 stack: Vec<usize>,
150 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
173pub 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
186pub(crate) fn retain() {
188 REGISTRY.with(|registry| registry.borrow_mut().recorders += 1);
189}
190
191pub(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
207pub fn begin_frame(window: &Window) {
216 rotate(Some(window.window_handle().window_id()))
217}
218
219#[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 registry.stack.clear();
236 let built = std::mem::take(&mut registry.building);
237 if previous == claim && !built.is_empty() {
242 registry.current = built;
243 }
244 });
245}
246
247pub fn tree() -> ProbeTree {
249 REGISTRY.with(|registry| registry.borrow().current.clone())
250}
251
252pub fn with_tree<R>(f: impl FnOnce(&ProbeTree) -> R) -> R {
255 REGISTRY.with(|registry| f(®istry.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 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 while let Some(top) = registry.stack.pop() {
317 if top == index {
318 break;
319 }
320 }
321 });
322}
323
324#[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#[derive(Debug, Clone)]
343struct ProbeMeta {
344 name: SharedString,
345 attrs: Vec<(SharedString, SharedString)>,
346 source: Option<SourceRef>,
347 style: Option<Box<StyleRefinement>>,
348}
349
350pub struct Probe<E> {
353 inner: E,
354 meta: ProbeMeta,
355 recording: bool,
358}
359
360impl<E> Probe<E> {
361 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 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 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 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
408pub trait Probed: IntoElement + Styled + Sized {
428 #[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 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
452pub trait ProbedAny: IntoElement + Sized {
461 #[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
493pub struct ProbeElement<E> {
496 inner: E,
497 meta: ProbeMeta,
498 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 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 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(&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}