diff --git a/crates/moon-gpui/src/window.rs b/crates/moon-gpui/src/window.rs index d87ca9c..854921a 100644 --- a/crates/moon-gpui/src/window.rs +++ b/crates/moon-gpui/src/window.rs @@ -724,6 +724,22 @@ impl HitboxId { } } +/// One element the inspector could pick, as the last rendered frame laid it out. +/// +/// Returned by [`Window::inspector_hitboxes`]. +#[cfg(debug_assertions)] +#[derive(Clone, Debug)] +pub struct InspectorHitbox { + /// The element's inspector id: its global id path and the source location that built it. + pub id: crate::InspectorElementId, + /// The element's bounds, in window-relative logical pixels. + pub bounds: Bounds, + /// The content mask in force when the element registered. What is visible of the element is + /// `bounds` intersected with this; an element scrolled out of its container intersects it in + /// nothing. + pub content_mask: Bounds, +} + /// A rectangular region that potentially blocks hitboxes inserted prior. /// See [Window::insert_hitbox] for more details. #[derive(Clone, Debug, Deref)] @@ -6236,6 +6252,33 @@ impl Window { } } + /// Every element the inspector could pick in the last rendered frame, in paint order: an entry + /// later in the list is drawn above the ones before it, the order hit testing walks backwards. + /// + /// Picking mode gives every non-transparent div a hitbox, so while picking is on this is the + /// whole frame's element map — id, source location and bounds for each element at once. A tool + /// that needs the map otherwise has to hover each element in turn and wait for the inspector to + /// name it, one frame per element. Outside picking mode nothing registers and the list is + /// empty. + #[cfg(debug_assertions)] + pub fn inspector_hitboxes(&self) -> Vec { + let frame = &self.rendered_frame; + frame + .hitboxes + .iter() + .filter_map(|hitbox| { + frame + .inspector_hitboxes + .get(&hitbox.id) + .map(|id| InspectorHitbox { + id: id.clone(), + bounds: hitbox.bounds, + content_mask: hitbox.content_mask.bounds, + }) + }) + .collect() + } + #[cfg(debug_assertions)] fn paint_inspector_hitbox(&mut self, cx: &App) { if let Some(inspector) = self.inspector.as_ref() { diff --git a/crates/moon-gpui/src/window/tests.rs b/crates/moon-gpui/src/window/tests.rs index ab1b1bc..476d116 100644 --- a/crates/moon-gpui/src/window/tests.rs +++ b/crates/moon-gpui/src/window/tests.rs @@ -702,3 +702,72 @@ fn explicit_present_prepares_text_even_if_canvases_skip() { fn any_canvas_present_request_prepares_the_window_text_frame() { assert!(gpu_canvas_prepare_text(false, true)); } + +/// A container with one smaller child at a known offset, for the inspector's element map. +struct PickableView; + +impl Render for PickableView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div().id("outer").size(px(200.0)).child( + div() + .id("inner") + .ml(px(10.0)) + .mt(px(20.0)) + .w(px(40.0)) + .h(px(30.0)), + ) + } +} + +/// The last named segment of every element in `any`'s inspector map, with its bounds, in the +/// order the map lists them. +fn inspector_map(app: &mut TestAppContext, any: AnyWindowHandle) -> Vec<(String, Bounds)> { + app.update_window(any, |_, window, _| { + window + .inspector_hitboxes() + .into_iter() + .filter_map(|hitbox| { + let name = hitbox.id.path.global_id.0.last()?.to_string(); + Some((name, hitbox.bounds)) + }) + .collect() + }) + .unwrap() +} + +/// Catches `inspector_hitboxes` reading `next_frame` instead of the frame just drawn: `next_frame` +/// is cleared when a draw begins, so every tool would read an empty map. Also catches the list +/// losing paint order, which makes a tool take the container for what sits on top of it, and the +/// map filling outside picking mode, which tools rely on to know picking must be turned on first. +#[test] +fn the_inspector_map_lists_every_div_of_a_picking_frame_in_paint_order() { + let (mut app, any) = open_window(|_, _| PickableView); + assert!( + inspector_map(&mut app, any).is_empty(), + "nothing registers while the inspector is not picking" + ); + + app.update_window(any, |_, window, cx| { + window.toggle_inspector(cx); + if let Some(inspector) = window.inspector.clone() { + inspector.update(cx, |inspector, _| inspector.start_picking()); + } + }) + .unwrap(); + draw(&mut app, any); + + let map = inspector_map(&mut app, any); + let position = |name: &str| map.iter().position(|(seen, _)| seen == name); + let (Some(outer), Some(inner)) = (position("outer"), position("inner")) else { + panic!("both divs must be in the map, got {map:?}"); + }; + assert!( + outer < inner, + "the child is drawn above its parent: {map:?}" + ); + assert_eq!( + map[inner].1, + Bounds::new(point(px(10.0), px(20.0)), size(px(40.0), px(30.0))), + "the child's bounds are where the layout put it" + ); +}