use std::cell::RefCell;
use std::panic::Location;
use gpui::{
App, Bounds, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, Pixels,
SharedString, StyleRefinement, Styled, Window, WindowId,
};
use super::state::SourceRef;
#[derive(Debug, Clone)]
pub struct ProbeNode {
pub name: SharedString,
pub attrs: Vec<(SharedString, SharedString)>,
pub element_id: Option<SharedString>,
pub source: Option<SourceRef>,
pub bounds: Bounds<Pixels>,
pub style: Option<Box<StyleRefinement>>,
pub parent: Option<usize>,
pub children: Vec<usize>,
pub depth: usize,
pub key: SharedString,
}
impl ProbeNode {
pub fn is_leaf(&self) -> bool {
self.children.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct ProbeTree {
pub nodes: Vec<ProbeNode>,
pub roots: Vec<usize>,
}
impl ProbeTree {
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn get(&self, index: usize) -> Option<&ProbeNode> {
self.nodes.get(index)
}
pub fn find(&self, key: &str) -> Option<usize> {
self.nodes.iter().position(|n| n.key.as_ref() == key)
}
pub fn ancestry(&self, index: usize) -> Vec<usize> {
let mut chain = Vec::new();
let mut cursor = Some(index);
while let Some(i) = cursor {
chain.push(i);
cursor = self.nodes.get(i).and_then(|n| n.parent);
}
chain.reverse();
chain
}
pub fn hit(&self, point: gpui::Point<Pixels>) -> Option<usize> {
let mut best: Option<(usize, usize)> = None;
for (index, node) in self.nodes.iter().enumerate() {
if node.bounds.contains(&point) {
let deeper = best.is_none_or(|(_, depth)| node.depth >= depth);
if deeper {
best = Some((index, node.depth));
}
}
}
best.map(|(index, _)| index)
}
}
#[derive(Default)]
struct Registry {
recorders: usize,
building: ProbeTree,
current: ProbeTree,
stack: Vec<usize>,
window: Option<WindowId>,
}
impl Registry {
fn is_recording(&self) -> bool {
self.recorders > 0
}
fn clear(&mut self) {
self.building = ProbeTree::default();
self.current = ProbeTree::default();
self.stack.clear();
self.window = None;
}
}
thread_local! {
static REGISTRY: RefCell<Registry> = RefCell::new(Registry::default());
}
pub fn set_enabled(enabled: bool) {
REGISTRY.with(|registry| {
let mut registry = registry.borrow_mut();
registry.recorders = usize::from(enabled);
if !enabled {
registry.clear();
}
});
}
pub(crate) fn retain() {
REGISTRY.with(|registry| registry.borrow_mut().recorders += 1);
}
pub(crate) fn release() {
REGISTRY.with(|registry| {
let mut registry = registry.borrow_mut();
registry.recorders = registry.recorders.saturating_sub(1);
if registry.recorders == 0 {
registry.clear();
}
});
}
pub fn is_enabled() -> bool {
REGISTRY.with(|registry| registry.borrow().recorders > 0)
}
pub fn begin_frame(window: &Window) {
rotate(Some(window.window_handle().window_id()))
}
#[cfg(test)]
pub(crate) fn begin_frame_unclaimed() {
rotate(None)
}
fn rotate(claim: Option<WindowId>) {
REGISTRY.with(|registry| {
let mut registry = registry.borrow_mut();
if !registry.is_recording() {
return;
}
let previous = std::mem::replace(&mut registry.window, claim);
registry.stack.clear();
let built = std::mem::take(&mut registry.building);
if previous == claim && !built.is_empty() {
registry.current = built;
}
});
}
pub fn tree() -> ProbeTree {
REGISTRY.with(|registry| registry.borrow().current.clone())
}
pub fn with_tree<R>(f: impl FnOnce(&ProbeTree) -> R) -> R {
REGISTRY.with(|registry| f(®istry.borrow().current))
}
fn push(meta: &ProbeMeta, window: Option<WindowId>) -> Option<usize> {
REGISTRY.with(|registry| {
let mut registry = registry.borrow_mut();
if !registry.is_recording() {
return None;
}
if registry.window.is_some() && registry.window != window {
return None;
}
let parent = registry.stack.last().copied();
let depth = registry.stack.len();
let ordinal = match parent {
Some(parent) => registry.building.nodes[parent].children.len(),
None => registry.building.roots.len(),
};
let key = match parent {
Some(parent) => format!(
"{}/{}[{}]",
registry.building.nodes[parent].key, meta.name, ordinal
),
None => format!("{}[{}]", meta.name, ordinal),
};
let index = registry.building.nodes.len();
registry.building.nodes.push(ProbeNode {
name: meta.name.clone(),
attrs: meta.attrs.clone(),
element_id: None,
source: meta.source.clone(),
bounds: Bounds::default(),
style: meta.style.clone(),
parent,
children: Vec::new(),
depth,
key: key.into(),
});
match parent {
Some(parent) => registry.building.nodes[parent].children.push(index),
None => registry.building.roots.push(index),
}
registry.stack.push(index);
Some(index)
})
}
fn pop(index: usize, bounds: Bounds<Pixels>, element_id: Option<ElementId>) {
REGISTRY.with(|registry| {
let mut registry = registry.borrow_mut();
if let Some(node) = registry.building.nodes.get_mut(index) {
node.bounds = bounds;
node.element_id = element_id.map(|id| SharedString::from(id.to_string()));
}
while let Some(top) = registry.stack.pop() {
if top == index {
break;
}
}
});
}
#[cfg(test)]
pub(crate) fn test_record(name: &'static str, children: impl FnOnce()) {
let meta = ProbeMeta {
name: SharedString::new_static(name),
attrs: Vec::new(),
source: None,
style: None,
};
let index = push(&meta, None);
children();
if let Some(index) = index {
pop(index, Bounds::default(), None);
}
}
#[derive(Debug, Clone)]
struct ProbeMeta {
name: SharedString,
attrs: Vec<(SharedString, SharedString)>,
source: Option<SourceRef>,
style: Option<Box<StyleRefinement>>,
}
pub struct Probe<E> {
inner: E,
meta: ProbeMeta,
recording: bool,
}
impl<E> Probe<E> {
pub fn attr(mut self, name: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
if self.recording {
self.meta.attrs.push((name.into(), value.into()));
}
self
}
pub fn attr_with<V: Into<SharedString>>(
mut self,
name: impl Into<SharedString>,
value: impl FnOnce() -> V,
) -> Self {
if self.recording {
self.meta.attrs.push((name.into(), value().into()));
}
self
}
pub fn attr_opt(
self,
name: impl Into<SharedString>,
value: Option<impl Into<SharedString>>,
) -> Self {
match value {
Some(value) => self.attr(name, value),
None => self,
}
}
pub fn attr_if(self, name: impl Into<SharedString>, present: bool) -> Self {
if present {
self.attr(name, "")
} else {
self
}
}
}
pub trait Probed: IntoElement + Styled + Sized {
#[track_caller]
fn probe(mut self, name: impl Into<SharedString>) -> Probe<Self> {
let recording = is_enabled();
let style = recording.then(|| Box::new(self.style().clone()));
let caller = Location::caller();
Probe {
inner: self,
meta: ProbeMeta {
name: name.into(),
attrs: Vec::new(),
source: recording.then(|| SourceRef::from(caller)),
style,
},
recording,
}
}
}
impl<E: IntoElement + Styled> Probed for E {}
pub trait ProbedAny: IntoElement + Sized {
#[track_caller]
fn probe_any(self, name: impl Into<SharedString>) -> Probe<Self> {
let recording = is_enabled();
let caller = Location::caller();
Probe {
inner: self,
meta: ProbeMeta {
name: name.into(),
attrs: Vec::new(),
source: recording.then(|| SourceRef::from(caller)),
style: None,
},
recording,
}
}
}
impl<E: IntoElement> ProbedAny for E {}
impl<E: IntoElement> IntoElement for Probe<E> {
type Element = ProbeElement<E::Element>;
fn into_element(self) -> Self::Element {
ProbeElement {
inner: self.inner.into_element(),
meta: self.meta,
index: None,
}
}
}
pub struct ProbeElement<E> {
inner: E,
meta: ProbeMeta,
index: Option<usize>,
}
impl<E: gpui::Element> IntoElement for ProbeElement<E> {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl<E: gpui::Element> gpui::Element for ProbeElement<E> {
type RequestLayoutState = E::RequestLayoutState;
type PrepaintState = E::PrepaintState;
fn id(&self) -> Option<ElementId> {
self.inner.id()
}
fn source_location(&self) -> Option<&'static Location<'static>> {
self.inner.source_location()
}
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
self.inner.request_layout(id, inspector_id, window, cx)
}
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
self.index = push(&self.meta, Some(window.window_handle().window_id()));
let state = self
.inner
.prepaint(id, inspector_id, bounds, request_layout, window, cx);
if let Some(index) = self.index {
pop(index, bounds, self.inner.id());
}
state
}
fn paint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.inner.paint(
id,
inspector_id,
bounds,
request_layout,
prepaint,
window,
cx,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reset() {
set_enabled(false);
set_enabled(true);
}
fn meta(name: &str) -> ProbeMeta {
ProbeMeta {
name: SharedString::from(name.to_owned()),
attrs: Vec::new(),
source: None,
style: None,
}
}
fn record(name: &str, children: impl FnOnce()) {
let index = push(&meta(name), None);
children();
if let Some(index) = index {
pop(index, Bounds::default(), None);
}
}
#[test]
fn nesting_follows_the_push_pop_pairs() {
reset();
record("Stack", || {
record("Button", || {});
record("Badge", || {});
});
begin_frame_unclaimed();
let tree = tree();
assert_eq!(tree.roots, vec![0]);
assert_eq!(tree.nodes[0].name.as_ref(), "Stack");
assert_eq!(tree.nodes[0].children, vec![1, 2]);
assert_eq!(tree.nodes[1].parent, Some(0));
assert_eq!(tree.nodes[1].depth, 1);
assert_eq!(tree.nodes[2].name.as_ref(), "Badge");
}
#[test]
fn keys_are_path_plus_sibling_ordinal() {
reset();
record("Stack", || {
record("Button", || {});
record("Button", || {});
});
begin_frame_unclaimed();
let tree = tree();
assert_eq!(tree.nodes[0].key.as_ref(), "Stack[0]");
assert_eq!(tree.nodes[1].key.as_ref(), "Stack[0]/Button[0]");
assert_eq!(tree.nodes[2].key.as_ref(), "Stack[0]/Button[1]");
assert_eq!(tree.find("Stack[0]/Button[1]"), Some(2));
}
#[test]
fn a_key_survives_the_next_frame() {
reset();
record("Stack", || record("Button", || {}));
begin_frame_unclaimed();
let before = tree().find("Stack[0]/Button[0]");
record("Stack", || record("Button", || {}));
begin_frame_unclaimed();
let after = tree().find("Stack[0]/Button[0]");
assert_eq!(before, after);
assert!(after.is_some());
}
#[test]
fn ancestry_runs_root_first() {
reset();
record("AppShell", || record("Stack", || record("Button", || {})));
begin_frame_unclaimed();
let tree = tree();
let button = tree.find("AppShell[0]/Stack[0]/Button[0]").unwrap();
let names: Vec<_> = tree
.ancestry(button)
.into_iter()
.map(|i| tree.nodes[i].name.to_string())
.collect();
assert_eq!(names, vec!["AppShell", "Stack", "Button"]);
}
#[test]
fn multiple_roots_are_kept_in_order() {
reset();
record("AppShell", || {});
record("Modal", || {});
begin_frame_unclaimed();
let tree = tree();
assert_eq!(tree.roots, vec![0, 1]);
assert_eq!(tree.nodes[1].key.as_ref(), "Modal[1]");
}
#[test]
fn overlapping_inspectors_keep_recording_until_the_last_one_goes() {
set_enabled(false);
assert!(!is_enabled());
retain();
assert!(is_enabled());
retain();
release();
assert!(is_enabled());
release();
assert!(!is_enabled());
}
#[test]
fn an_unbalanced_release_cannot_underflow() {
set_enabled(false);
release();
release();
retain();
assert!(is_enabled());
release();
assert!(!is_enabled());
}
#[test]
fn the_recorded_tree_is_released_with_the_last_inspector() {
reset();
record("Stack", || {});
begin_frame_unclaimed();
assert!(!tree().is_empty());
release();
assert!(tree().is_empty());
}
#[test]
fn nothing_records_while_disabled() {
reset();
set_enabled(false);
record("Stack", || record("Button", || {}));
begin_frame_unclaimed();
assert!(tree().is_empty());
}
#[test]
fn hit_testing_picks_the_deepest_containing_node() {
reset();
let outer = push(&meta("Stack"), None).unwrap();
let inner = push(&meta("Button"), None).unwrap();
pop(
inner,
Bounds {
origin: gpui::point(gpui::px(10.0), gpui::px(10.0)),
size: gpui::size(gpui::px(50.0), gpui::px(20.0)),
},
None,
);
pop(
outer,
Bounds {
origin: gpui::point(gpui::px(0.0), gpui::px(0.0)),
size: gpui::size(gpui::px(200.0), gpui::px(100.0)),
},
None,
);
begin_frame_unclaimed();
let tree = tree();
let hit = tree
.hit(gpui::point(gpui::px(20.0), gpui::px(15.0)))
.unwrap();
assert_eq!(tree.nodes[hit].name.as_ref(), "Button");
let outside = tree
.hit(gpui::point(gpui::px(150.0), gpui::px(80.0)))
.unwrap();
assert_eq!(tree.nodes[outside].name.as_ref(), "Stack");
assert!(tree
.hit(gpui::point(gpui::px(400.0), gpui::px(400.0)))
.is_none());
}
#[test]
fn an_unbalanced_stack_does_not_leak_into_the_next_frame() {
reset();
push(&meta("Orphan"), None);
begin_frame_unclaimed();
record("Stack", || {});
begin_frame_unclaimed();
let tree = tree();
assert_eq!(tree.roots.len(), 1);
assert_eq!(tree.nodes[tree.roots[0]].name.as_ref(), "Stack");
assert_eq!(tree.nodes[tree.roots[0]].depth, 0);
}
}