Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions crates/moon-gpui/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pixels>,
/// 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<Pixels>,
}

/// A rectangular region that potentially blocks hitboxes inserted prior.
/// See [Window::insert_hitbox] for more details.
#[derive(Clone, Debug, Deref)]
Expand Down Expand Up @@ -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<InspectorHitbox> {
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() {
Expand Down
69 changes: 69 additions & 0 deletions crates/moon-gpui/src/window/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>) -> 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<Pixels>)> {
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"
);
}
Loading