Width tool: Escape deselects the selected width points

With the Width tool, Escape did not deselect the selected width points:
they stayed selected, Delete still removed them, and in isolation mode
the same Escape left isolation mode. The reference app's shortcut list
gives Esc for deselecting a width point.

Escape now deselects the Width tool's selected width points. The path,
its widths and the undo history do not change, and no command is sent.
While width points are selected the tool takes Escape, so that Escape
does not also leave isolation mode. With no width point selected, and
during a drag, Escape does what it did before.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-29 10:02:18 +07:00
committed by bflatastic
parent aa503c2648
commit a4ba07dd1b
4 changed files with 77 additions and 5 deletions
+44 -3
View File
@@ -14,6 +14,8 @@
//! `stroke.widthPoint.copy {id, index, t}` or, for several points, `stroke.widthProfile.set {ids,
//! points}`, and commit on release; Delete runs `stroke.widthPoint.remove {id, indices}`.
//!
//! Escape deselects the selected width points and sends no command.
//!
//! A compound path's stroke is the compound's: its members are edited through it, and the width
//! points show on every subpath of the stroke (one profile runs along each). The cursor says what a
//! drag does: add a width point on a stroke, or move or widen the one under it.
@@ -391,14 +393,19 @@ impl Tool for WidthTool {
},
}
}
/// Delete/Backspace remove the selected width points; with none selected they are the
/// shortcut's (Clear deletes the selected objects).
/// Delete/Backspace remove the selected width points and Escape deselects them; with none
/// selected the keys do what they do with other tools (Clear deletes the selected objects, and
/// Escape leaves isolation mode, for one).
fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool {
matches!(key, ToolKey::Delete | ToolKey::Backspace)
matches!(key, ToolKey::Delete | ToolKey::Backspace | ToolKey::Escape)
&& self.drag.is_none()
&& self.selected.as_ref().is_some_and(|(id, _)| !self.selected_indices(cx, *id).is_empty())
}
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
if key == ToolKey::Escape && self.drag.is_none() {
self.selected = None;
return vec![];
}
if !matches!(key, ToolKey::Delete | ToolKey::Backspace) {
return vec![];
}
@@ -650,6 +657,40 @@ mod tests {
);
}
#[test]
fn escape_deselects_the_width_points() {
let (d, id) = doc_three_points();
let (s, p) = (Selection::default(), paint());
let c = cx(&d, &s, &p);
let mut t = WidthTool::default();
let (none, shift) = (Mods::default(), Mods { shift: true, ..Default::default() });
// With nothing selected the tool leaves Escape to the app.
assert!(!t.claims_key(&c, ToolKey::Escape));
// Select the points at t = 0.25 and 0.75: Escape is the tool's and deselects both, sending
// nothing to the document.
t.pointer(&c, &ev(PointerKind::Down, 150.0, 200.0, none));
t.pointer(&c, &ev(PointerKind::Up, 150.0, 200.0, none));
t.pointer(&c, &ev(PointerKind::Down, 250.0, 200.0, shift));
t.pointer(&c, &ev(PointerKind::Up, 250.0, 200.0, shift));
assert_eq!(t.selected_indices(&c, id), vec![1, 3]);
t.pointer(&c, &ev(PointerKind::Move, 200.0, 202.0, none));
let thick = |t: &WidthTool| t.overlays(&c).iter().filter(|o| matches!(o, Overlay::Path { width, .. } if *width > 2.0)).count();
assert_eq!(thick(&t), 2, "the two selected diamonds are drawn selected");
assert!(t.claims_key(&c, ToolKey::Escape));
assert!(t.key(&c, ToolKey::Escape, none).is_empty());
assert!(t.selected_indices(&c, id).is_empty());
assert_eq!(thick(&t), 0, "no diamond is drawn selected");
// Delete then removes nothing, and Escape is the app's again.
assert!(!t.claims_key(&c, ToolKey::Delete) && !t.claims_key(&c, ToolKey::Escape));
assert!(t.key(&c, ToolKey::Delete, none).is_empty());
// During a drag the dragged point stays selected.
t.pointer(&c, &ev(PointerKind::Down, 200.0, 200.0, none));
t.pointer(&c, &ev(PointerKind::Drag, 210.0, 200.0, none));
assert!(!t.claims_key(&c, ToolKey::Escape));
t.key(&c, ToolKey::Escape, none);
assert!(t.selected.is_some());
}
#[test]
fn width_points_show_on_every_subpath() {
let (d, c, _) = doc_compound();
+30 -1
View File
@@ -1,6 +1,7 @@
//! Delete and Backspace through shortcut routing: Artboard editing protects selected artwork,
//! including when no artboard can be deleted. Width points and Puppet Warp pins take the keys
//! while selected, then leave them to Clear.
//! while selected, then leave them to Clear. Escape deselects the selected width points before it
//! leaves isolation mode.
use egui::{Key, Modifiers};
use serde_json::{Value, json};
@@ -83,6 +84,34 @@ fn delete_and_backspace_remove_the_selected_width_point_then_clear() {
}
}
#[test]
fn escape_deselects_the_selected_width_point_then_leaves_isolation() {
let (mut app, id) = app_with_line();
app.run("stroke.widthPoint.set", json!({"id": id, "t": 0.5, "left": 12, "right": 12})).unwrap();
app.run("object.group", json!({})).unwrap();
app.run("object.isolate", json!({})).unwrap();
app.run("select.set", json!({"ids": [id]})).unwrap();
app.select_tool("width");
click(&mut app, 200.0, 200.0);
let st = app.session.active().unwrap();
let (doc, undo) = (st.doc.clone(), st.history.undo.len());
// Escape deselects the width point only: the line, its widths, the undo history and isolation
// mode stay as they were.
press(&mut app, Key::Escape);
let st = app.session.active().unwrap();
assert!(*st.doc == *doc && st.history.undo.len() == undo, "Escape changes nothing in the document");
assert!(st.isolation.is_some(), "the first Escape stays in isolation mode");
assert_eq!((app.session.tool_id(), width_points(&app, id)), ("width", json!(3)));
// With no width point selected, Escape leaves isolation mode as with other tools.
press(&mut app, Key::Escape);
assert!(app.session.active().unwrap().isolation.is_none(), "the next Escape leaves isolation mode");
// Delete after Escape clears the line instead of removing the width point.
click(&mut app, 200.0, 200.0);
press(&mut app, Key::Escape);
press(&mut app, Key::Delete);
assert!(!exists(&app, id), "Delete after Escape clears the selected line");
}
#[test]
fn delete_and_backspace_remove_the_selected_pin_then_clear() {
for key in [Key::Delete, Key::Backspace] {
+2 -1
View File
@@ -3003,7 +3003,8 @@ for a uniform stroke). `stroke.widthPoint.remove {id, index | indices}` deletes
points}` replaces them all, as dragging several Shift-selected points with the Width tool does in one undo step.
With the Width tool, `press_key` Delete or Backspace removes the selected width points (`handledBy: "tool"`); with none
selected the key clears the selected objects as usual (`edit.clear`). The Puppet Warp tool's selected pins go the
same way.
same way. With the Width tool, `press_key` Escape deselects the selected width points (`handledBy: "tool"`) and
leaves the document and its undo history as they were.
A compound path's stroke is the compound's: width point commands given one of its members edit the compound's
profile, which runs along every subpath (the Width tool shows each subpath's points).
+1
View File
@@ -498,6 +498,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Width-point finite-arithmetic follow-up (G1/M3):** Adjoining adjustments avoid intermediate overflow; non-finite normalized or resulting factors are rejected before committing. Engine regressions in `tests/widthpoints.rs` cover finite extremes, small nonzero results, atomic rejection, Undo/Redo and native save/reopen. Historical focused validation on the `a3357121` integration observed 2 expected RED failures with 11 passing controls, then all 13 tests passing with the repair; none were ignored. All three added regressions passed in current full CI. A full CI retry on the five-unit combined source at `a0671777` passed all seven stages with 6,613 test outcomes on 2026-10-14. An earlier full-CI run on the identical source panicked in the randomized `ai_mutated_save_never_panics` test; the cause remains unresolved. This five-fix batch does not claim to repair that failure. Scores and hours unchanged.
- **Width-point creation (G1):** double-click a stroked segment with the Width tool to open Width Point Edit using sampled side widths. OK creates the point and optionally adjusts adjoining widths in one undo step. Cancel or Delete discards a prospective point without changing the document or redo history. Command and pointer tests cover profiles, compound paths, adjoining adjustment, existing-point editing and Undo/Redo. Message-catalog checks and full CI passed; the original capture verifies opening and Cancel, and the dialog layout was reviewed (2026-10-13: 28 focused Width tests, all seven cargo xtask ci stages, and a headless opening/Cancel capture with original PNG review). [Variable-width strokes](https://helpx.adobe.com/illustrator/using/stroke-object.html).
- **Escape deselects width points:** with the Width tool, Escape deselects the selected width points, on the key the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists for deselecting a width point. The path, its widths and the undo history do not change. With no width point selected, Escape does what it does with the other tools, such as leaving isolation mode.
- **Rulers (#1212):** drag the intersection to preview and set a document zero point, or double-click it to reset to the active artboard's top-left. Transform, Properties and Control X/Y fields measure from it and convert entered positions back to absolute command coordinates. The origin survives every native format version and compression; geometry, guides and patterns stay in place. Public reference: [Use rulers](https://helpx.adobe.com/ca/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html).
- **Global and artboard rulers:** View › Rulers › Change to Global / Artboard Rulers (Cmd+Alt+R, `view.globalRulers`). A new document has artboard rulers: the rulers and the Transform, Properties, Control and Info X/Y count from the active artboard's top-left corner, or the zero point dragged out for that artboard, which moves with it; with global rulers one document zero point serves every artboard. Guide readouts and Shift ruler-tick snapping follow the same zero point. The mode is saved with the document; files from before it open with global rulers, as they measured. Regression tests: `engine/tests/it/ruler_origin.rs`, `ui-egui/src/tests/ruler_origin.rs`, `tools/src/rulerguide.rs`. Public reference: [About rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/about-rulers.html).
- **Video display options (#2849):** Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio, and View › Rulers › Show Video Rulers ([Set video display options for artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/set-video-display-options-for-artboards.html)). Each artboard has its own (`artboard.setProps`/`artboard.new` `showVideoSafeAreas`, `videoRulerPixelAspectRatio` 0.1–10, default 1), set in one undo step and written to the native format only when on or not 1, so other documents save byte for byte as before. The help page names the safe areas without numbers; our choice is [EBU R 95](https://tech.ebu.ch/docs/r/r095.pdf)'s (note 5): the canvas outlines the action-safe area 3.5% and the title-safe (graphics-safe) area 5% in from each edge, 1 point wide and turned with the view; exports and print leave them out. Show Video Rulers (`view.videoRulers`) shows the rulers counting the active artboard in pixels from its top-left corner, whatever the ruler origin: a pixel is as many points wide as the ratio and one point tall, so a 720 × 480 pt artboard at 0.9 reads 800 × 480. Hide Video Rulers returns them to the General unit; Shift ruler-tick snapping of new guides keeps the General unit. Regression tests: `tests/panelcmds.rs` (`artboard_video_display_options_*`, `artboard_display_marks_stay_out_of_exports`), `tests/docsetup.rs` (`an_artboard_shows_its_video_safe_areas`), `canvas.rs` (`video_rulers_count_the_artboard_in_pixels_of_its_aspect_ratio`), `tests/ruler_origin.rs` (`show_video_rulers_counts_the_active_artboard_in_pixels`), `dialogs/tests.rs` (`artboard_options_show_video_display_options`).