Merge PR #306 from FabPei: fix: DeckCraft upstream issue #104

This commit is contained in:
Brandon Thomas
2026-10-14 12:57:23 -04:00
2 changed files with 61 additions and 1 deletions
+16
View File
@@ -419,6 +419,22 @@ fn double_click_in_table_uses_grown_row_heights() {
assert_eq!(s.doc().unwrap().selection.text.as_ref().and_then(|t| t.cell), Some((0, 0)));
}
#[test]
fn double_click_edits_text_inside_group() {
// A text member of a group is still editable: double-click reaches it through the group
// (regression: it used to be a no-op, so grouped text boxes could not be edited).
let mut s = session();
let a =
s.execute("shape.insert", &json!({"preset": "rect", "rect": [100.0, 100.0, 120.0, 50.0], "text": "Hello"})).unwrap()["id"].as_u64().unwrap();
let b = s.execute("shape.insert", &json!({"preset": "rect", "rect": [300.0, 200.0, 50.0, 50.0]})).unwrap()["id"].as_u64().unwrap();
s.execute("edit.select", &json!({"ids": [a, b]})).unwrap();
s.execute("arrange.group", &json!({})).unwrap();
let ev = |kind, x, y| PointerEvent { kind, x, y, mods: Mods::default(), tol: 3.0 };
// Double-click the grouped text box: it becomes editable and targets that member.
s.pointer(ev(PointerKind::DoubleClick, 160.0, 125.0)).unwrap();
assert_eq!(s.doc().unwrap().selection.text.as_ref().map(|t| t.shape.0 as u64), Some(a));
}
#[test]
fn grown_table_frame_covers_its_drawn_rows() {
// Rows stored 4 pt tall are drawn taller: the box, its handles and hit tests follow the drawn rows.
+45 -1
View File
@@ -815,7 +815,18 @@ impl Session {
self.execute("text.edit", &json!({"id": id, "at": [pos.0, pos.1]}))?;
self.execute("text.selectWord", &json!({}))
}
ShapeKind::Group { .. } => Ok(Value::Null),
ShapeKind::Group { .. } => {
// A group is a single selection unit, but its text stays editable:
// double-click reaches the text-holding member under the cursor.
match text_member_at(st, &sh, p, tol).and_then(|mid| st.shape(mid).cloned().map(|m| (mid, m))) {
Some((mid, msh)) if !msh.is_line() => {
let pos = text_pos(st, &msh, p);
self.execute("text.edit", &json!({"id": mid, "at": [pos.0, pos.1]}))?;
self.execute("text.selectWord", &json!({}))
}
_ => Ok(Value::Null),
}
}
_ => {
self.ui_requests.push(crate::UiRequest::Pane { id: "format".into() });
Ok(Value::Null)
@@ -1097,6 +1108,39 @@ fn is_text_first(st: &crate::DocState, id: ShapeId) -> bool {
st.shape(id).is_some_and(|s| s.ph.is_some() || s.text_box)
}
/// The topmost text-holding shape under `p` inside group `sh`, recursing into nested groups.
/// Group children live in the group's child space (`ShapeKind::Group::child`), so the point is
/// mapped through `group_child_affine`, exactly as `hit_shape` does for group hit-testing.
fn text_member_at(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<ShapeId> {
let ShapeKind::Group { children, child } = &sh.kind else { return None };
let x = xfrm_of(&st.doc, &st.selection, sh);
let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
member_at(children, deckcraft_geom::group_child_affine(&x, ch), p, tol)
}
/// Depth-first (topmost-first) search of `children` for a text-holding shape under `p`.
/// `to_abs` maps this level's child space to slide space.
fn member_at(children: &[Shape], to_abs: Affine, p: Point, tol: f64) -> Option<ShapeId> {
let q = to_abs.inverse() * p;
for c in children.iter().rev() {
let Some(cx) = c.xfrm else { continue };
let local = cx.affine().inverse() * q;
if local.x < -tol || local.y < -tol || local.x > cx.w + tol || local.y > cx.h + tol {
continue;
}
if let ShapeKind::Group { children: gc, child: gch } = &c.kind {
let gch = Rect::new(gch.x, gch.y, gch.x + gch.w, gch.y + gch.h);
let nested = to_abs * deckcraft_geom::group_child_affine(&cx, gch);
if let Some(id) = member_at(gc, nested, p, tol) {
return Some(id);
}
} else if matches!(c.kind, ShapeKind::Shape | ShapeKind::Connector { .. }) && !c.is_line() {
return Some(c.id);
}
}
None
}
fn contains_rect(outer: Rect, inner: Rect) -> bool {
inner.x0 >= outer.x0 && inner.y0 >= outer.y0 && inner.x1 <= outer.x1 && inner.y1 <= outer.y1
}