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
63 changes: 63 additions & 0 deletions crates/moon-gpui/src/text_system/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Pixels>,
layout: &LineLayout,
line_height: Pixels,
align: &TextAlign,
align_width: Option<Pixels>,
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::*;
Expand Down
50 changes: 50 additions & 0 deletions crates/moon-gpui/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,25 @@ pub struct InspectorHitbox {
pub content_mask: Bounds<Pixels>,
}

/// 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<Pixels>,
/// 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<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 @@ -908,6 +927,8 @@ pub(crate) struct Frame {
pub(crate) next_inspector_instance_ids: FxHashMap<Rc<crate::InspectorElementPath>, usize>,
#[cfg(debug_assertions)]
pub(crate) inspector_hitboxes: FxHashMap<HitboxId, crate::InspectorElementId>,
#[cfg(debug_assertions)]
pub(crate) inspector_texts: Vec<InspectorText>,
pub(crate) tab_stops: TabStopMap,
}

Expand Down Expand Up @@ -957,6 +978,8 @@ impl Frame {

#[cfg(debug_assertions)]
inspector_hitboxes: FxHashMap::default(),
#[cfg(debug_assertions)]
inspector_texts: Vec::new(),
tab_stops: TabStopMap::default(),
}
}
Expand Down Expand Up @@ -985,6 +1008,7 @@ impl Frame {
{
self.next_inspector_instance_ids.clear();
self.inspector_hitboxes.clear();
self.inspector_texts.clear();
}
}

Expand Down Expand Up @@ -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<Pixels>) {
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() {
Expand Down
55 changes: 55 additions & 0 deletions crates/moon-gpui/src/window/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>) -> 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<String> = save.path.iter().map(ToString::to_string).collect();
assert_eq!(
path.iter().rev().take(2).rev().cloned().collect::<Vec<_>>(),
["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"
);
}
Loading