mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-06 18:51:26 +00:00
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:
1 parent
08afab50b6
commit
6f62716f08
2 files changed
+73
-10
No files matched your search
+17
-10
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user