From 68a77878664ae7c0a80938f995191ab8de4ee8d9 Mon Sep 17 00:00:00 2001 From: guyverino Date: Wed, 4 Nov 2026 11:20:17 +0100 Subject: [PATCH] feat(gpui): expose the inspector's element map of the last frame Add `Window::inspector_hitboxes()` and `InspectorHitbox`: every element the inspector could pick in the last rendered frame, in paint order, with its inspector id (global id path and source location), its bounds and the content mask it was drawn under. Picking mode already gives every non-transparent div a hitbox and records its inspector id, but only in `pub(crate)` frame fields. A tool that wants the whole screen therefore had to hover each element and wait for the inspector to name it, one frame per element. With the map public it reads the whole frame at once. Additive and behind `debug_assertions`, like the rest of the inspector API; outside picking mode the list is empty. --- crates/moon-gpui/src/window.rs | 43 +++++++++++++++++ crates/moon-gpui/src/window/tests.rs | 69 ++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) 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" + ); +}