mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-13 22:02:31 +00:00
Thread the loaded text cursor into a text frame inside a group (gaps 27)
A click with the loaded text cursor on a text frame inside a group showed the not-allowed cursor and did nothing: `Document::hit_item` walks the spread's top-level items, so the hit was the group, and the group is not a text frame. `item_hit` already looks inside a group, but only to answer whether the group was hit at all -- the frame in it was never named. InDesign threads into it. `hit_descendant` names it: the frontmost descendant under a point that a predicate accepts, through nested groups, front to back and skipping hidden children. `ToolContext::hit_text_frame` asks for a text frame, and the thread tool resolves its target through it, falling back to the plain hit so a click on anything else means what it meant before. `hit` is untouched, so selection still picks the group. One fix covers the cursor and the click, since `click` resolves through `target`. The test asserts the cursor, that the click threads to the frame and not to the group, that `hit` still returns the group, and that a group holding nothing threadable is still refused.
This commit is contained in:
1 parent
a930276fd7
commit
39ef7799c0
5 files changed
+91
-3
No files matched your search
@@ -587,6 +587,30 @@ pub fn item_hit(it: &Item, p: Point, tol: f64) -> bool {
|
||||
designcraft_geom::hit::stroke_contains(&bp, it.stroke.weight.max(1.0), tol + reach, inner)
|
||||
}
|
||||
|
||||
/// The frontmost descendant of `it` under `p` that `want` accepts, looking inside nested groups;
|
||||
/// `it` itself when nothing inside it matches and `want` accepts it. `p` is in the space `it`
|
||||
/// sits in, as [`item_hit`] takes it.
|
||||
///
|
||||
/// Children are tried front to back and a hidden one is skipped, so of several that `want`
|
||||
/// accepts the answer is the one in front. Objects `want` rejects do not hide the ones behind
|
||||
/// them: a text frame under a rectangle in the same group is still found.
|
||||
pub fn hit_descendant(it: &Item, p: Point, tol: f64, want: &dyn Fn(&Item) -> bool) -> Option<ItemId> {
|
||||
if !item_hit(it, p, tol) {
|
||||
return None;
|
||||
}
|
||||
let inner = it.xf.inverse() * p;
|
||||
let children: Vec<&Arc<Item>> = it.shown_children().collect();
|
||||
for c in children.into_iter().rev() {
|
||||
if c.hidden {
|
||||
continue;
|
||||
}
|
||||
if let Some(id) = hit_descendant(c, inner, tol, want) {
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
want(it).then_some(it.id)
|
||||
}
|
||||
|
||||
/// How far from its path type on a path is hit (points): about a cap height of 12–18 pt type,
|
||||
/// either side (the type may sit on or under the path).
|
||||
const PATH_TYPE_REACH: f64 = 12.0;
|
||||
@@ -46,7 +46,7 @@ pub use datamerge::{
|
||||
};
|
||||
pub use designcraft_color as color;
|
||||
pub use designcraft_geom as geom;
|
||||
pub use edit::{ItemLoc, ItemPath, SpreadRef, item_hit as edit_hit};
|
||||
pub use edit::{ItemLoc, ItemPath, SpreadRef, hit_descendant, item_hit as edit_hit};
|
||||
pub use endnotes::{ENDNOTE_REF, EditorialNote, EndnoteOptions, NOTE_MARK};
|
||||
pub use flex::{FlexAlign, FlexDirection, FlexJustify, FlexLayout, FlexSize};
|
||||
pub use ids::*;
|
||||
|
||||
@@ -232,6 +232,19 @@ impl ToolContext<'_> {
|
||||
self.doc.hit_item(si, sp, self.tol(4.0)).map(|id| (sr, id))
|
||||
}
|
||||
|
||||
/// The frontmost text frame under `p`, looking inside groups. [`Self::hit`] stops at the
|
||||
/// group, so a loaded text cursor over a text frame in a group found nothing to thread into
|
||||
/// (gaps 27); InDesign threads into it.
|
||||
pub fn hit_text_frame(&self, p: Point) -> Option<(SpreadRef, ItemId)> {
|
||||
let (sr, id) = self.hit(p)?;
|
||||
let it = self.doc.item(id)?;
|
||||
if it.is_text_frame() {
|
||||
return Some((sr, id));
|
||||
}
|
||||
let (_, sp) = self.layout.spread_at(p)?;
|
||||
designcraft_doc::hit_descendant(it, sp, self.tol(4.0), &|c| c.is_text_frame()).map(|f| (sr, f))
|
||||
}
|
||||
|
||||
/// The parent item showing (not yet overridden) on the document page under `p`:
|
||||
/// (absolute page, parent item).
|
||||
pub fn hit_parent_on_page(&self, p: Point) -> Option<(usize, ItemId)> {
|
||||
|
||||
@@ -1252,7 +1252,8 @@ fn eyedropper_ignores_inactive_states_of_a_multi_state_object() {
|
||||
// Children [vector, raster] with the raster on top: it samples only while it is the active state.
|
||||
for (active, samples) in [(1, true), (0, false)] {
|
||||
let mut group = Item::new(ItemId(d.alloc()), lid, Shape::Rectangle, shapes::rectangle(area));
|
||||
group.content = Content::Group { items: vec![std::sync::Arc::new(vector.clone()), std::sync::Arc::new(raster.clone())] };
|
||||
group.content =
|
||||
designcraft_doc::item::Content::Group { items: vec![std::sync::Arc::new(vector.clone()), std::sync::Arc::new(raster.clone())] };
|
||||
group.states = vec!["a".into(), "b".into()];
|
||||
group.active_state = active;
|
||||
let mut d2 = d.clone();
|
||||
@@ -1314,3 +1315,52 @@ fn anchor_tools_show_pen_state_cursors() {
|
||||
assert_eq!(cursor("deleteAnchor"), Cursor::PenDelete);
|
||||
assert_eq!(cursor("convertDirection"), Cursor::PenConvert);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loaded_text_cursor_threads_into_a_frame_inside_a_group() {
|
||||
// `hit` stops at the group, so the loaded cursor showed NotAllowed over a text frame inside
|
||||
// one and a click did nothing. InDesign threads into it (gaps 27).
|
||||
let mut td = thread_doc();
|
||||
let lid = td.d.default_layer();
|
||||
let rect = Rect::new(600.0, 100.0, 700.0, 200.0);
|
||||
// The frame, then the same frame moved inside a group. Both transforms are the identity, so
|
||||
// the child's coordinates stay the spread's.
|
||||
let (inner, _) = td.d.add_text_frame(SpreadRef::Doc(0), rect, lid, "", ParaFormat::default()).unwrap();
|
||||
let frame_item = td.d.item(inner).cloned().unwrap();
|
||||
td.d.remove_item(inner).unwrap();
|
||||
let gid = ItemId(td.d.alloc());
|
||||
let mut group = Item::new(gid, lid, Shape::Group, shapes::rectangle(rect));
|
||||
group.content = designcraft_doc::item::Content::Group { items: vec![std::sync::Arc::new(frame_item)] };
|
||||
td.d.insert_item(SpreadRef::Doc(0), group, None).unwrap();
|
||||
td.l = CanvasLayout::new(&td.d, false);
|
||||
|
||||
let mut t = create("selection");
|
||||
let cx = td.cx();
|
||||
click(&mut t, &cx, td.port(td.a, true), Mods::default());
|
||||
let p = td.at(650.0, 150.0);
|
||||
|
||||
// Selection still reaches the group, which is what a plain click there should pick.
|
||||
assert_eq!(cx.hit(p).map(|(_, i)| i), Some(gid));
|
||||
// The loaded text cursor reaches the frame inside it.
|
||||
assert_eq!(cx.hit_text_frame(p).map(|(_, i)| i), Some(inner), "the frame in the group");
|
||||
assert_eq!(t.cursor(&cx, p, Mods::default()), Cursor::ThreadLink);
|
||||
// And the click threads to that frame, not to the group.
|
||||
assert_eq!(click(&mut t, &cx, p, Mods::default()), thread_exec(serde_json::json!({"from": td.a.0, "to": inner.0})));
|
||||
|
||||
// A group with nothing threadable in it still says no.
|
||||
let mut td2 = thread_doc();
|
||||
let r2 = Rect::new(600.0, 300.0, 700.0, 400.0);
|
||||
let plain = ItemId(td2.d.alloc());
|
||||
let child = Item::new(plain, lid, Shape::Rectangle, shapes::rectangle(r2));
|
||||
let g2 = ItemId(td2.d.alloc());
|
||||
let mut group2 = Item::new(g2, lid, Shape::Group, shapes::rectangle(r2));
|
||||
group2.content = designcraft_doc::item::Content::Group { items: vec![std::sync::Arc::new(child)] };
|
||||
td2.d.insert_item(SpreadRef::Doc(0), group2, None).unwrap();
|
||||
td2.l = CanvasLayout::new(&td2.d, false);
|
||||
let cx2 = td2.cx();
|
||||
let mut t2 = create("selection");
|
||||
click(&mut t2, &cx2, td2.port(td2.a, true), Mods::default());
|
||||
let q = td2.at(650.0, 350.0);
|
||||
assert_eq!(cx2.hit_text_frame(q).map(|(_, i)| i), None, "no text frame in that group");
|
||||
assert_eq!(t2.cursor(&cx2, q, Mods::default()), Cursor::NotAllowed);
|
||||
}
|
||||
@@ -194,7 +194,8 @@ impl Loaded {
|
||||
}
|
||||
|
||||
pub fn target(self, cx: &ToolContext, p: Point) -> Target {
|
||||
let Some((_, id)) = cx.hit(p) else { return Target::Empty };
|
||||
// A text frame inside a group is the frame the click means, not the group (gaps 27).
|
||||
let Some((_, id)) = cx.hit_text_frame(p).or_else(|| cx.hit(p)) else { return Target::Empty };
|
||||
if Some(id) == self.neighbour(cx.doc) {
|
||||
return Target::Unthread;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user