diff --git a/crates/moon-gpui/src/text_system/line.rs b/crates/moon-gpui/src/text_system/line.rs index cb66359..432604d 100644 --- a/crates/moon-gpui/src/text_system/line.rs +++ b/crates/moon-gpui/src/text_system/line.rs @@ -89,6 +89,19 @@ impl ShapedLine { window: &mut Window, cx: &mut App, ) -> Result<()> { + #[cfg(debug_assertions)] + note_drawn_text( + &self.text, + origin, + &self.layout, + line_height, + &align, + align_width, + 0, + window, + cx, + ); + paint_line( origin, &self.layout, @@ -285,6 +298,19 @@ impl WrappedLine { None => self.layout.wrap_width, }; + #[cfg(debug_assertions)] + note_drawn_text( + &self.text, + origin, + &self.layout.unwrapped_layout, + line_height, + &align, + align_width, + self.wrap_boundaries.len(), + window, + cx, + ); + paint_line( origin, &self.layout.unwrapped_layout, @@ -749,6 +775,43 @@ fn aligned_origin_x( } } +/// Tell the inspector what a line of text says and where it lands, while it is picking. +/// +/// Both ways of painting text end here, because `paint_line` no longer has the string. A wrapped +/// line is one entry for its whole block: its lines start at different places when aligned, and +/// what a reader of the map wants is the text, not its line breaks. +#[cfg(debug_assertions)] +#[allow(clippy::too_many_arguments)] +fn note_drawn_text( + text: &SharedString, + origin: Point, + layout: &LineLayout, + line_height: Pixels, + align: &TextAlign, + align_width: Option, + wraps: usize, + window: &mut Window, + cx: &App, +) { + if !window.is_inspector_picking(cx) { + return; + } + let (x, width) = if wraps == 0 { + let box_width = align_width.unwrap_or(layout.width); + ( + aligned_origin_x(origin, box_width, px(0.0), align, layout, None), + layout.width, + ) + } else { + (origin.x, align_width.unwrap_or(layout.width)) + }; + let bounds = Bounds::new( + point(x, origin.y), + size(width, line_height * (wraps as f32 + 1.0)), + ); + window.note_inspector_text(text.clone(), bounds); +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/moon-gpui/src/window.rs b/crates/moon-gpui/src/window.rs index 854921a..4a0d5d0 100644 --- a/crates/moon-gpui/src/window.rs +++ b/crates/moon-gpui/src/window.rs @@ -740,6 +740,25 @@ pub struct InspectorHitbox { pub content_mask: Bounds, } +/// One run of text drawn in a frame while the inspector was picking. +/// +/// Returned by [`Window::inspector_texts`]. +#[cfg(debug_assertions)] +#[derive(Clone, Debug)] +pub struct InspectorText { + /// The string as it was shaped, in the language the window is drawn in. + pub text: SharedString, + /// The ids of the elements it was painted inside, outermost first. Text is no element a + /// hitbox knows about, so this is what says whose text it is: a label's path starts with the + /// path of the control it labels, whatever the two rectangles happen to do. + pub path: GlobalElementId, + /// Where it landed, in window-relative logical pixels. + pub bounds: Bounds, + /// The content mask in force when it was painted. As with [`InspectorHitbox::content_mask`], + /// what is visible of it is `bounds` intersected with this. + pub content_mask: Bounds, +} + /// A rectangular region that potentially blocks hitboxes inserted prior. /// See [Window::insert_hitbox] for more details. #[derive(Clone, Debug, Deref)] @@ -908,6 +927,8 @@ pub(crate) struct Frame { pub(crate) next_inspector_instance_ids: FxHashMap, usize>, #[cfg(debug_assertions)] pub(crate) inspector_hitboxes: FxHashMap, + #[cfg(debug_assertions)] + pub(crate) inspector_texts: Vec, pub(crate) tab_stops: TabStopMap, } @@ -957,6 +978,8 @@ impl Frame { #[cfg(debug_assertions)] inspector_hitboxes: FxHashMap::default(), + #[cfg(debug_assertions)] + inspector_texts: Vec::new(), tab_stops: TabStopMap::default(), } } @@ -985,6 +1008,7 @@ impl Frame { { self.next_inspector_instance_ids.clear(); self.inspector_hitboxes.clear(); + self.inspector_texts.clear(); } } @@ -6279,6 +6303,32 @@ impl Window { .collect() } + /// Keep one painted run of text for [`Window::inspector_texts`]. The caller has checked that the + /// inspector is picking, so that a frame outside picking pays for nothing. + #[cfg(debug_assertions)] + pub(crate) fn note_inspector_text(&mut self, text: SharedString, bounds: Bounds) { + let path = GlobalElementId(Arc::from(&*self.element_id_stack)); + let content_mask = self.content_mask().bounds; + self.next_frame.inspector_texts.push(InspectorText { + text, + path, + bounds, + content_mask, + }); + } + + /// Every run of text painted in the last rendered frame, in paint order. + /// + /// The other half of [`Window::inspector_hitboxes`]: that map says what is on screen and where, + /// this says what it reads, so a tool can name a control by the words a person sees in every + /// language the window is drawn in. Filled under the same condition, so outside picking mode + /// the list is empty; picking also turns view caching off, so a frame never reuses text it + /// did not paint. + #[cfg(debug_assertions)] + pub fn inspector_texts(&self) -> &[InspectorText] { + &self.rendered_frame.inspector_texts + } + #[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 476d116..bd9a328 100644 --- a/crates/moon-gpui/src/window/tests.rs +++ b/crates/moon-gpui/src/window/tests.rs @@ -771,3 +771,58 @@ fn the_inspector_map_lists_every_div_of_a_picking_frame_in_paint_order() { "the child's bounds are where the layout put it" ); } + +/// A label inside a control, for the inspector's list of painted text. +struct LabelledView; + +impl Render for LabelledView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + .id("panel") + .size(px(200.0)) + .child(div().id("save").ml(px(10.0)).mt(px(20.0)).child("Save")) + } +} + +/// Catches `inspector_texts` reading `next_frame`, which a draw clears, and the list filling +/// outside picking, which would make every debug frame pay for it. The path is what ties a label to +/// its control: read off the wrong stack, or taken after the element popped, it names the panel +/// instead, and the bounds without the alignment step put centred text in the wrong column. +#[test] +fn the_inspector_lists_painted_text_with_the_elements_around_it() { + let (mut app, any) = open_window(|_, _| LabelledView); + let texts = |app: &mut TestAppContext| { + app.update_window(any, |_, window, _| window.inspector_texts().to_vec()) + .unwrap() + }; + assert!( + texts(&mut app).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 texts = texts(&mut app); + let save = texts + .iter() + .find(|text| text.text.as_ref() == "Save") + .unwrap_or_else(|| panic!("the label must be listed, got {texts:?}")); + let path: Vec = save.path.iter().map(ToString::to_string).collect(); + assert_eq!( + path.iter().rev().take(2).rev().cloned().collect::>(), + ["panel", "save"], + "the text sits inside the control that draws it" + ); + assert_eq!( + save.bounds.origin, + point(px(10.0), px(20.0)), + "the text starts where the layout put its control" + ); +}