Canvas: shapes in front of grouped text and of the edited box's edge take the click too

Review follow-up. The in-front check now also looks among the members of the group that holds
the edited text, and the edited box's frame (which drags it) yields to a shape in front of the
text at that spot; the box's own handles stay on top, as in PowerPoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZetenypappandClaude Opus 5.5 committed 2026-10-18 10:56:38 +02:00
1 parent a773fcfce9
commit 051d3b4fd3
2 files changed
+55 -9

No files matched your search

+29
View File
@@ -3649,4 +3649,33 @@ fn a_shape_in_front_of_edited_text_takes_the_click() {
let st = s.doc().unwrap();
assert_eq!(st.selection.shapes.iter().map(|id| id.0 as u64).collect::<Vec<_>>(), vec![square]);
assert!(st.selection.text.as_ref().is_none_or(|t| t.shape.0 as u64 == square), "{:?}", st.selection.text);
// A shape over the edited box's edge takes the click there too: the edge would otherwise drag
// the box.
let edge = {
let st = s.doc().unwrap();
let x = cmd::xfrm_of(&st.doc, &st.selection, st.shape(body).unwrap());
(x.x, x.y + x.h / 2.0)
};
let corner =
s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [edge.0 - 10.0, edge.1 - 10.0, 20, 20]})).unwrap()["id"].as_u64().unwrap();
s.execute("edit.deselect", &json!({})).unwrap();
s.execute("text.edit", &json!({"id": body.0})).unwrap();
// On the box's left edge, inside the band that drags it, clear of its middle handle (handles
// stay on top, as in PowerPoint).
click(&mut s, edge.0 + 1.0, edge.1 - 9.0);
let st = s.doc().unwrap();
assert_eq!(st.selection.shapes.iter().map(|id| id.0 as u64).collect::<Vec<_>>(), vec![corner], "{:?}", st.selection);
// Text inside a group: a member in front of it takes the click instead of the caret.
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
let tb =
s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 300, 150], "text": "Grouped text"})).unwrap()["id"].as_u64().unwrap();
let cover = s.execute("shape.insert", &json!({"preset": "rect", "rect": [200, 150, 60, 40]})).unwrap()["id"].as_u64().unwrap();
s.execute("arrange.group", &json!({"ids": [tb, cover]})).unwrap();
s.execute("text.edit", &json!({"id": tb})).unwrap();
assert_eq!(s.doc().unwrap().selection.text.as_ref().map(|t| t.shape.0 as u64), Some(tb));
click(&mut s, 230.0, 170.0);
let st = s.doc().unwrap();
assert!(st.selection.text.as_ref().is_none_or(|t| t.shape.0 as u64 != tb), "the click on the member in front left the text: {:?}", st.selection);
}
+26 -9
View File
@@ -409,10 +409,12 @@ impl Session {
}
}
}
// The frame of the text being edited: grabbing it moves the box.
// The frame of the text being edited: grabbing it moves the box, unless a shape in front
// of the text covers that spot (#931).
if let Some(e) = edited_box(st)
&& e.movable
&& edit_zone(&e.xfrm, p, tol) == EditZone::Frame
&& !shape_in_front(st, st.shapes(), e.id, p, tol, 0)
{
return Some(Hit::Frame { id: e.id });
}
@@ -1228,14 +1230,8 @@ impl Session {
None => edited_box(st).is_some_and(|e| edit_zone(&e.xfrm, p, tol) != EditZone::Outside),
Some(_) => xfrm_of(&st.doc, &st.selection, sh).bounds().inflate(tol, tol).contains(p),
};
// A shape in front of the edited text takes the click, as in PowerPoint (#931): the shapes
// above the one holding the text, topmost first.
let in_front = st
.shapes()
.iter()
.rev()
.take_while(|s| deckcraft_model::find_shape(std::slice::from_ref(*s), t.shape).is_none())
.any(|s| !s.hidden && hit_shape(st, s, p, tol).is_some());
// A shape in front of the edited text takes the click, as in PowerPoint (#931).
let in_front = shape_in_front(st, st.shapes(), t.shape, p, tol, 0);
if !inside
|| in_front
|| matches!(
@@ -1659,6 +1655,27 @@ struct EditedBox {
movable: bool,
}
/// Whether a visible shape drawn in front of `target` is under `p`: the shapes above it in its
/// list, topmost first, and inside a group that holds it, the members above it.
fn shape_in_front(st: &crate::DocState, shapes: &[Shape], target: ShapeId, p: Point, tol: f64, depth: usize) -> bool {
if depth > 64 {
return false;
}
for s in shapes.iter().rev() {
if s.id == target {
return false;
}
if deckcraft_model::find_shape(std::slice::from_ref(s), target).is_some() {
// The group holding the text: look among its members.
return shape_in_front(st, s.children(), target, p, tol, depth + 1);
}
if !s.hidden && hit_shape(st, s, p, tol).is_some() {
return true;
}
}
false
}
fn edited_box(st: &crate::DocState) -> Option<EditedBox> {
let t = st.selection.text.as_ref()?;
if t.notes || t.cell.is_some() {