From 8f31193cc0151e9f54ca03656bf5f9dd9c6545fa Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 10:39:52 -0700 Subject: [PATCH] Edit Points: Esc before releasing the mouse button cancels a point or handle drag Esc during a vertex or handle drag in Edit Points used to leave Edit Points while the drag went on: the point kept following the pointer and the release added an undo step. Esc now ends the drag and puts the shape back as it was at the press, with no undo step, and Edit Points stays on. Esc without a drag still leaves Edit Points. --- ROADMAP.md | 1 + crates/engine/src/keys.rs | 3 +++ crates/engine/src/tests.rs | 45 ++++++++++++++++++++++++++++++++++++++ crates/engine/src/tools.rs | 19 ++++++++++++++++ 4 files changed, 68 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 73b62ee6..a361f982 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -178,6 +178,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-16 | AutoFormat as you type: ordinals (1st, 22nd, 103rd) get superscript suffixes after a space or punctuation; Tools ▸ AutoCorrect and `text.autocorrect {ordinals}` turn it off | | 2026-10-16 | A PowerPoint Show (`.ppsx`) opened from the command line or the file association starts its slide show, as in PowerPoint; File › Open still opens it for editing (#862) | | 2026-10-15 | Canvas: the Format and Animation Painter override the cursor to show the tool is armed (#830) | +| 2026-10-24 | Edit Points: Esc pressed before the mouse button is released cancels a point or handle drag, as in PowerPoint, and leaves the shape as it was with no undo step. Edit Points stays on after the cancel | | 2026-10-15 | Edit Points: right-click a segment for Straight/Curved Segment, a point for Open/Close Path (`shape.segmentType`, `shape.openPath`, `shape.closePath`); Delete Segment and Add Point in the menu still to come | | 2026-10-15 | Design › Browse for Themes… with a `.pptx` or `.potx` brings its slide master and layouts, not just the theme: the master the current slide uses is replaced (a preserved one stays beside it), the template stretches to the deck's slide size, and each slide moves to the layout with the same name or kind and keeps its content. One undo step (#807) | | 2026-10-15 | Clicking the frame of a text box or placeholder selects the box whole, so it can be moved, duplicated or deleted; only a click inside puts the caret in the text (#787) | diff --git a/crates/engine/src/keys.rs b/crates/engine/src/keys.rs index ebc2eae2..e701d17a 100644 --- a/crates/engine/src/keys.rs +++ b/crates/engine/src/keys.rs @@ -54,6 +54,9 @@ impl Session { let has_shapes = self.active().is_some_and(|d| !d.selection.shapes.is_empty()); let k = key_token(key); let k = k.as_str(); + if matches!(k, "escape" | "esc") && self.cancel_point_drag() { + return Ok(json!({"handled": true})); + } if self.tool.drawing_freeform() && matches!(k, "enter" | "return" | "escape") { return self.finish_freeform(false); } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index fc5cd2c9..1e6636b4 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2111,6 +2111,51 @@ fn edit_points_with_the_pointer_and_curves() { assert!(s.tool.edit_points.is_none()); } +/// Esc before the button is released cancels a point or handle drag in Edit Points: the shape +/// is as it was at the press, no undo step is added, and Edit Points stays on. +#[test] +fn edit_points_escape_cancels_a_point_or_handle_drag() { + use crate::tools::{Mods, PointerEvent, PointerKind}; + let mut s = Session::with_new(); + s.execute("slide.new", &json!({"layout": "blank"})).unwrap(); + s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [100, 100, 200, 100]})).unwrap(); + s.execute("shape.editPoints", &json!({"on": true})).unwrap(); + let id = deckcraft_model::ShapeId(s.execute("shape.points", &json!({})).unwrap()["id"].as_u64().unwrap() as u32); + let path = |s: &Session| s.doc().unwrap().shape(id).map(|sh| format!("{:?}", sh.geom)).unwrap_or_default(); + let undo_len = |s: &Session| s.doc().unwrap().history.undo.len(); + let ev = |kind, x: f64, y: f64| PointerEvent { kind, x, y, mods: Mods::default(), ..Default::default() }; + let (before, steps) = (path(&s), undo_len(&s)); + // A vertex: drag it, press Esc, keep dragging, release. + let v: Vec<[f64; 2]> = serde_json::from_value(s.execute("shape.points", &json!({})).unwrap()["paths"][0].clone()).unwrap(); + let [x0, y0] = v[0]; + s.pointer(ev(PointerKind::Down, x0, y0)).unwrap(); + s.pointer(ev(PointerKind::Drag, x0 - 30.0, y0)).unwrap(); + assert_ne!(path(&s), before, "the drag moves the point"); + assert_eq!(s.key("Escape", Mods::default()).unwrap()["handled"], true); + assert_eq!(path(&s), before, "Esc puts the point back"); + assert!(!s.tool.dragging()); + s.pointer(ev(PointerKind::Drag, x0 - 50.0, y0)).unwrap(); + s.pointer(ev(PointerKind::Up, x0 - 50.0, y0)).unwrap(); + assert_eq!(path(&s), before, "the rest of the drag does nothing"); + assert_eq!(undo_len(&s), steps, "no undo step"); + assert_eq!(s.tool.editing_points(s.doc().unwrap()), Some(id), "Edit Points stays on"); + // A handle of the same point, which the press on the point shows. + let st = s.doc().unwrap(); + let (_, ho, _) = crate::cmd::points::slide_handles(&st.doc, &st.selection, st.shape(id).unwrap(), 0, 0).unwrap(); + let ho = ho.unwrap(); + s.pointer(ev(PointerKind::Down, ho.x, ho.y)).unwrap(); + s.pointer(ev(PointerKind::Drag, ho.x + 20.0, ho.y + 20.0)).unwrap(); + assert_ne!(path(&s), before, "the drag moves the handle"); + s.key("Escape", Mods::default()).unwrap(); + s.pointer(ev(PointerKind::Up, ho.x + 20.0, ho.y + 20.0)).unwrap(); + assert_eq!(path(&s), before, "Esc puts the handle back"); + assert_eq!(undo_len(&s), steps, "no undo step"); + assert_eq!(s.tool.editing_points(s.doc().unwrap()), Some(id), "Edit Points stays on"); + // Without a drag, Esc still leaves Edit Points. + s.key("Escape", Mods::default()).unwrap(); + assert!(s.tool.edit_points.is_none()); +} + #[test] fn edit_points_path_parse_and_write() { use crate::cmd::points::{Seg, parse, vertices, write}; diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index 50853838..7369a5d6 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -538,6 +538,25 @@ impl Session { } } + /// Cancels a point or handle drag in Edit Points when Esc is pressed: ends the drag, puts back the + /// document and selection saved at the press, and adds no undo step. Returns whether a drag was cancelled. + pub(crate) fn cancel_point_drag(&mut self) -> bool { + if !matches!(self.tool.drag, Some(Drag::Vertex { .. } | Drag::Handle { .. })) { + return false; + } + self.tool.drag = None; + if let Some(st) = self.active_mut() + && st.interaction.as_ref().is_some_and(|i| i.label == "Edit Points") + && let Some(i) = st.interaction.take() + && !Arc::ptr_eq(&i.doc, &st.doc) + { + st.doc = i.doc; + st.selection = i.selection; + st.revision += 1; + } + true + } + /// End the interaction only when it is the one `label` started. A twist must not commit a drag. pub(crate) fn commit_named(&mut self, label: &str) { let ours = self.active().and_then(|st| st.interaction.as_ref()).is_some_and(|i| i.label == label);