From 7095a4eaf1cc893c901e427f637074488334c7c3 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Mon, 9 Nov 2026 23:25:23 +0100 Subject: [PATCH] Text boxes in a group: the topmost member takes the click Co-Authored-By: Claude Opus 5.5 --- crates/layout/src/hit.rs | 8 ++++++-- crates/layout/src/tests.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/crates/layout/src/hit.rs b/crates/layout/src/hit.rs index 3469aff95c..18b9580f67 100644 --- a/crates/layout/src/hit.rs +++ b/crates/layout/src/hit.rs @@ -467,7 +467,7 @@ impl DocLayout { pub fn story_at(&self, page: usize, x: f32, y: f32) -> Option { let p = self.pages.get(page)?; // A group's text boxes count as text boxes too (#6027). - // Topmost first, a text box on its own or one in a group alike. + // Topmost first, a text box on its own or one in a group alike (a group's last member is painted on top). let text_box = |behind: bool| { p.items.iter().rev().find_map(|it| { let Placed::Object { rect, spin, text_box, wrap, group_boxes, .. } = it else { return None }; @@ -477,7 +477,11 @@ impl DocLayout { let centre = Point::new(rect.x + rect.w / 2.0, rect.y + rect.h / 2.0); let (ux, uy) = spin.unapply(centre.x, centre.y, x, y); text_box.filter(|_| rect.contains(Point::new(ux, uy))).or_else(|| { - group_boxes.iter().find(|(r, _)| Rect::new(rect.x + r.x, rect.y + r.y, r.w, r.h).contains(Point::new(x, y))).map(|(_, id)| *id) + group_boxes + .iter() + .rev() + .find(|(r, _)| Rect::new(rect.x + r.x, rect.y + r.y, r.w, r.h).contains(Point::new(x, y))) + .map(|(_, id)| *id) }) }) }; diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index cde16782c5..81bb9245f3 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -3992,6 +3992,36 @@ fn grouped_text_box_areas_turn_with_the_group_and_work_in_headers() { assert_eq!(l.header_footer_text_box_at(0, 150.0, 40.0), None, "the group's bar is not text"); } +/// #6027: where two text boxes of one group overlap, a click enters the one painted on top (the +/// last member), in the body and in a header alike. +#[test] +fn click_in_overlapping_grouped_text_boxes_enters_the_top_one() { + let (low, top) = (3, 4); + let group = |rect| Placed::Object { + rect, + spin: Default::default(), + story: StoryRef::Part(1), + path: Path::top(0), + off: 0, + text_box: None, + wrap: Default::default(), + origin: Default::default(), + group_boxes: vec![(wordcraft_geom::Rect::new(0.0, 0.0, 100.0, 100.0), low), (wordcraft_geom::Rect::new(50.0, 50.0, 100.0, 100.0), top)], + }; + let page = Page { + items: vec![group(wordcraft_geom::Rect::new(100.0, 100.0, 150.0, 150.0))], + header: vec![group(wordcraft_geom::Rect::new(100.0, 100.0, 150.0, 150.0))], + ..Default::default() + }; + let l = DocLayout { pages: vec![page], ..Default::default() }; + // Overlap (page 175, 175 is inside both boxes): the top box takes the click, and typing. + assert_eq!(l.story_at(0, 175.0, 175.0), Some(StoryRef::Part(top))); + assert_eq!(l.header_footer_text_box_at(0, 175.0, 175.0), Some(top)); + // Outside the overlap each box still answers for itself. + assert_eq!(l.story_at(0, 110.0, 110.0), Some(StoryRef::Part(low))); + assert_eq!(l.story_at(0, 240.0, 240.0), Some(StoryRef::Part(top))); +} + #[test] fn deleted_float_leaves_no_wrap_area_without_markup() { crate::test_fonts();