Text boxes: a press inside one goes to its text, not to a picture under it (#4240)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Bryan MarkandClaude Opus 5.5 authored and GitHub committed 2026-11-04 11:31:58 +01:00
1 parent 08afab50b6
commit 6f62716f08
2 files changed
+73 -10

No files matched your search

+17 -10
View File
@@ -421,19 +421,20 @@ impl DocLayout {
}
/// The story whose text is under a point (body, footnotes, endnotes, text boxes; not
/// headers/footers). A text box wins inside its area, except one behind the text, which only
/// wins where no body text is.
/// headers/footers). A text box wins inside its area (its rotated area when it is turned, as
/// [`DocLayout::object_at`] takes it), except one behind the text, which only wins where no
/// body text is.
pub fn story_at(&self, page: usize, x: f32, y: f32) -> Option<StoryRef> {
let p = self.pages.get(page)?;
let text_box = |behind: bool| {
objects(p, page).find(|o| o.behind() == behind && o.text_box.is_some() && o.rect.contains(Point::new(x, y))).and_then(|o| o.text_box)
objects(p, page).find(|o| o.behind() == behind && o.text_box.is_some() && o.rect.contains(o.unspin(x, y))).and_then(|o| o.text_box)
};
text_box(false).map(StoryRef::Part).or_else(|| text_at(p, page, x, y)).or_else(|| text_box(true).map(StoryRef::Part))
}
/// The object a press at (x, y) grabs: a picture or shape anywhere on it, a text box on its
/// border (within `edge` points either side; inside is its text). Topmost first; objects
/// behind the text only where there's no text.
/// border (within `edge` points either side; inside is its text, whatever lies under the
/// box). Topmost first; objects behind the text only where there's no text.
pub fn object_at(&self, page: usize, x: f32, y: f32, edge: f32) -> Option<ObjectHit> {
self.object_at_in(page, x, y, edge, StoryRef::Body)
}
@@ -456,11 +457,17 @@ impl DocLayout {
StoryRef::Body => text_at(p, page, x, y).is_some(),
StoryRef::Part(id) => hf_text_at(p, id, x, y),
};
cands
.iter()
.find(|o| !o.behind() && grabs(o))
.or_else(|| if text_here { None } else { cands.iter().find(|o| o.behind() && grabs(o)) })
.cloned()
// Inside a text box is its text: a press there grabs nothing, not an object under the box
// (a form scanned as a picture, with text boxes laid over it to fill in). The same holds
// among the objects behind the text.
let inside_text_box = |o: &ObjectHit| o.text_box.is_some() && !grabs(o) && o.rect.contains(o.unspin(x, y));
let topmost = |behind: bool| cands.iter().find(|o| o.behind() == behind && (grabs(o) || inside_text_box(o)));
match topmost(false) {
Some(o) if inside_text_box(o) => None,
Some(o) => Some(o.clone()),
None if text_here => None,
None => topmost(true).filter(|o| !inside_text_box(o)).cloned(),
}
}
/// The text box in the header or footer at (x, y) on `page`.
+56
View File
@@ -4615,6 +4615,62 @@ fn presses_grab_pictures_anywhere_and_text_boxes_by_their_border() {
assert!(l.object_at(0, c.x, c.top + 2.0, 4.0).is_none());
}
#[test]
fn a_press_inside_a_text_box_over_a_picture_goes_to_its_text() {
// A form scanned as a picture, with text boxes laid over it to fill in: a press inside a box
// grabbed the picture under it, so the box's text could not be reached.
use wordcraft_doc::para::{Anchor, Float, Wrap};
let float =
|wrap: Wrap, z: u32| Float { wrap, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, x: 0.0, y: 0.0, dist: 0.0, z, ..Default::default() };
let form = |wrap: Wrap, picture_z: u32, rot: f32| {
let mut d = Document::from_text("Form");
let picture = InlineObject::Image {
media: "form".into(),
w: 300.0,
h: 400.0,
alt: String::new(),
float: float(wrap, picture_z),
crop: [0.0; 4],
ole: None,
};
d.insert_object(&Pos::body(0, 0), picture, &Default::default()).unwrap();
let id = text_box(&mut d, 0, "Name", 200.0, 100.0, Float { x: 50.0, y: 150.0, rot, ..float(wrap, 2) });
(lay(&d), id)
};
let (l, id) = form(Wrap::InFrontOfText, 1, 0.0);
let b = box_rect(&l, id);
let (cx, cy) = (b.x + b.w / 2.0, b.y + b.h / 2.0);
// Inside the box: its text, not the picture under it.
assert!(l.object_at(0, cx, cy, 4.0).is_none());
assert_eq!(l.story_at(0, cx, cy), Some(StoryRef::Part(id)));
// The box's border grabs the box; the picture around the box grabs the picture.
assert_eq!(l.object_at(0, b.x + 1.0, cy, 4.0).map(|o| o.text_box), Some(Some(id)));
assert_eq!(l.object_at(0, cx, b.y - 40.0, 4.0).map(|o| o.text_box), Some(None));
// A picture in front of the box still grabs where it covers it.
let (l, id) = form(Wrap::InFrontOfText, 3, 0.0);
let b = box_rect(&l, id);
assert_eq!(l.object_at(0, b.x + b.w / 2.0, b.y + b.h / 2.0, 4.0).map(|o| o.text_box), Some(None));
// A box turned a quarter covers a tall strip: a press in it, below its unturned frame, is its
// text for both the object and the story hit test.
let (l, id) = form(Wrap::InFrontOfText, 1, 90.0);
let b = box_rect(&l, id);
let (cx, y) = (b.x + b.w / 2.0, b.y + b.h / 2.0 + 80.0);
assert!(!b.contains(wordcraft_geom::Point::new(cx, y)), "{b:?}");
assert!(l.object_at(0, cx, y, 4.0).is_none());
assert_eq!(l.story_at(0, cx, y), Some(StoryRef::Part(id)));
assert_eq!(l.hit(0, cx, y, StoryRef::Part(id)).map(|p| p.story), Some(StoryRef::Part(id)));
// Both behind the text, away from the body text: the same, and a picture stacked above the
// box still grabs.
let (l, id) = form(Wrap::BehindText, 1, 0.0);
let b = box_rect(&l, id);
let (cx, cy) = (b.x + b.w / 2.0, b.y + b.h / 2.0);
assert!(l.object_at(0, cx, cy, 4.0).is_none());
assert_eq!(l.story_at(0, cx, cy), Some(StoryRef::Part(id)));
let (l, id) = form(Wrap::BehindText, 3, 0.0);
let b = box_rect(&l, id);
assert_eq!(l.object_at(0, b.x + b.w / 2.0, b.y + b.h / 2.0, 4.0).map(|o| o.text_box), Some(None));
}
/// (lines placed, lines laid out) of a text box's story, and its area.
fn box_lines(l: &DocLayout, id: u32) -> (usize, usize, wordcraft_geom::Rect) {
let mut shown = 0;