mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-30 13:36:52 -06:00
Merge pull request #943 from Zetenypapp/fix/click-in-front-931
# Conflicts: # crates/engine/src/tests.rs
This commit is contained in:
@@ -134,6 +134,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort |
|
||||
| 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape |
|
||||
| 2026-10-17 | Edit Points: turning a shape into a freeform is one undo step, so Undo brings the original shape back (it used to stay a freeform) |
|
||||
| 2026-10-17 | While a placeholder's text is being edited, a click on a shape in front of it selects that shape instead of moving the caret, as in PowerPoint (#931) |
|
||||
| 2026-10-15 | PPTX trigger sequences keep separate On Click steps, including paragraph builds; synthetic import/edit/save/reopen and headless show regressions |
|
||||
| 2026-10-16 | Export: PNG with a transparent background — the slide background is left out, only the slide's content is drawn (Export… dialog, File › Export, web download, `file.export`/`file.render` `transparentBackground`, `deckcraft-cli render --transparent`) (#784) |
|
||||
| 2026-10-15 | CLI: .txt and .md export includes table cell text, which it used to skip silently (#814) |
|
||||
|
||||
@@ -3985,3 +3985,59 @@ fn edit_points_conversion_is_one_undo_step() {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(preset(&s), "the only step was the conversion");
|
||||
}
|
||||
|
||||
/// #931: while the text of a placeholder is being edited, a shape in front of it still takes a
|
||||
/// click (as in PowerPoint); a click on the text where nothing is in front still moves the caret.
|
||||
#[test]
|
||||
fn a_shape_in_front_of_edited_text_takes_the_click() {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "titleAndContent"})).unwrap();
|
||||
let square = s.execute("shape.insert", &json!({"preset": "rect", "rect": [380, 260, 200, 120]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("edit.deselect", &json!({})).unwrap();
|
||||
let ev = |kind, x: f64, y: f64| tools::PointerEvent { kind, x, y, mods: Default::default(), tol: 4.0, pressure: None };
|
||||
let click = |s: &mut Session, x: f64, y: f64| {
|
||||
s.pointer(ev(tools::PointerKind::Down, x, y)).unwrap();
|
||||
s.pointer(ev(tools::PointerKind::Up, x, y)).unwrap();
|
||||
};
|
||||
// The content placeholder's edge: its (empty) text is now being edited.
|
||||
click(&mut s, 66.0, 300.0);
|
||||
let body = s.doc().unwrap().selection.text.as_ref().map(|t| t.shape).expect("editing the content placeholder");
|
||||
assert_ne!(body.0 as u64, square);
|
||||
// A click on the text away from the square keeps editing it.
|
||||
click(&mut s, 200.0, 300.0);
|
||||
assert_eq!(s.doc().unwrap().selection.text.as_ref().map(|t| t.shape), Some(body));
|
||||
// A click on the square, in front of the text, selects the square.
|
||||
click(&mut s, 480.0, 320.0);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -414,10 +414,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 });
|
||||
}
|
||||
@@ -1233,7 +1235,10 @@ 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).
|
||||
let in_front = shape_in_front(st, st.shapes(), t.shape, p, tol, 0);
|
||||
if !inside
|
||||
|| in_front
|
||||
|| matches!(
|
||||
self.hit_test(p, tol),
|
||||
Some(
|
||||
@@ -1655,6 +1660,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() {
|
||||
|
||||
Reference in New Issue
Block a user