diff --git a/crates/ui-egui/src/canvas/task_bar.rs b/crates/ui-egui/src/canvas/task_bar.rs index 461730c6e..61c971987 100644 --- a/crates/ui-egui/src/canvas/task_bar.rs +++ b/crates/ui-egui/src/canvas/task_bar.rs @@ -93,6 +93,10 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { // (). // These run the same commands as the Control bar's anchor buttons. let anchors = !st.selection.anchors.is_empty(); + // With the Direct Selection tool, a path selected whole offers its anchor points' actions + // too, as the reference app's bar does for any path that tool selects (same page), and Edit + // Path, which only switches to that tool, is left out (#4469). + let direct = app.session.tool_id() == "directSelection"; if anchors { items.push((tl!("Remove Anchor Points"), "pen-tool-delete", "path.removeAnchors", none())); items.push((tl!("Connect"), "dc-join", "path.join", none())); @@ -122,7 +126,9 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { items.push((tl!("Isolate"), "square-dashed", "object.isolate", none())); } } else if is_compound { - items.push(edit_path()); + if !direct { + items.push(edit_path()); + } items.push((tl!("Release Compound Path"), "ungroup", "object.compoundPath.release", none())); } else if let Some(kind) = text { items.push((tl!("Create Outlines"), "type", "type.createOutlines", none())); @@ -134,6 +140,15 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { } } else if is_image { // Offset Path and Simplify don't apply to an image. + } else if !anchors && direct { + // Corner and Smooth convert every anchor of a path selected whole; Remove and Cut + // need anchors picked out, so they wait for them. + if open_path { + items.push((tl!("Connect"), "dc-join", "path.join", none())); + } + items.push((tl!("Corner"), "dc-anchor", TASK_BAR_TO_CORNER, none())); + items.push((tl!("Smooth"), "dc-anchor-smooth", TASK_BAR_TO_SMOOTH, none())); + items.push((tl!("Simplify"), "spline", "object.path.simplify", none())); } else if !anchors { if open_path { items.push((tl!("Join"), "dc-join", "path.join", none())); diff --git a/crates/ui-egui/src/tests/taskbar.rs b/crates/ui-egui/src/tests/taskbar.rs index e1563c339..27c49ac5e 100644 --- a/crates/ui-egui/src/tests/taskbar.rs +++ b/crates/ui-egui/src/tests/taskbar.rs @@ -163,3 +163,74 @@ fn type_converts_between_point_and_area_type() { click(&mut app, "Convert To Point Type"); assert!(matches!(text_kind(&app), TextKind::Point)); } + +/// The path's anchors (its first subpath). +fn anchors(app: &VectorcraftApp, id: u64) -> Vec { + match kind(app, id) { + NodeKind::Path { path, .. } => path.subpaths[0].anchors.clone(), + k => panic!("not a path: {k:?}"), + } +} + +/// With the Direct Selection tool, a path selected whole offers its anchor points' actions instead +/// of Edit Path and Offset Path (#4469), as after the bar's own Edit Path. +#[test] +fn direct_selection_of_a_whole_path_offers_its_anchor_actions() { + let mut app = app(); + let line = id(app.run("path.create", json!({"anchors": [{"x": 50, "y": 200}, {"x": 150, "y": 80}, {"x": 250, "y": 200}]})).unwrap()); + select(&mut app, &[line]); + click(&mut app, "Edit Path"); + assert_eq!(app.session.tool_id(), "directSelection"); + assert!(app.session.active().unwrap().selection.anchors.is_empty(), "the path is selected whole"); + let (_, texts) = bar(&mut app); + for label in ["Connect", "Corner", "Smooth", "Simplify", "Duplicate"] { + assert!(has(&texts, label), "{label}: {texts:?}"); + } + for label in ["Edit Path", "Offset Path", "Join", "Remove Anchor Points", "Cut Path"] { + assert!(!has(&texts, label), "{label}: {texts:?}"); + } + // Smooth and Corner convert every anchor of the path, one undo step each. + click(&mut app, "Smooth"); + let a = anchors(&app, line); + assert_eq!(a[1].kind, vectorcraft_geom::AnchorKind::Smooth, "{a:?}"); + assert!(a[1].has_in() && a[1].has_out(), "{a:?}"); + click(&mut app, "Corner"); + assert!(anchors(&app, line).iter().all(|a| !a.has_in() && !a.has_out()), "{:?}", anchors(&app, line)); + app.run("edit.undo", json!({})).unwrap(); + assert!(anchors(&app, line)[1].has_in(), "Corner undoes in one step"); + // Connect closes the open path; a closed one isn't offered it. + click(&mut app, "Connect"); + assert!(matches!(kind(&app, line), NodeKind::Path { path, .. } if path.is_closed())); + let (_, texts) = bar(&mut app); + assert!(!has(&texts, "Connect") && has(&texts, "Corner"), "{texts:?}"); +} + +/// Anchors picked out with the Direct Selection tool keep the full set: Remove and Cut need them. +#[test] +fn direct_selected_anchors_still_offer_remove_and_cut() { + let mut app = app(); + let line = id(app.run("path.create", json!({"anchors": [{"x": 50, "y": 200}, {"x": 150, "y": 80}, {"x": 250, "y": 200}]})).unwrap()); + app.run("tool.select", json!({"tool": "directSelection"})).unwrap(); + app.run("select.anchors", json!({"id": line, "anchors": [[0, 1]]})).unwrap(); + let (_, texts) = bar(&mut app); + for label in ["Remove Anchor Points", "Connect", "Cut Path", "Corner", "Smooth"] { + assert!(has(&texts, label), "{label}: {texts:?}"); + } + assert!(!has(&texts, "Edit Path"), "{texts:?}"); +} + +/// A compound path under the Direct Selection tool isn't offered Edit Path, which would only switch +/// to the tool already in use; the Selection tool still offers it. +#[test] +fn a_compound_path_under_direct_selection_drops_edit_path() { + let mut app = app(); + let a = id(app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 120, "height": 100})).unwrap()); + let b = id(app.run("shape.ellipse", json!({"x": 70, "y": 60, "width": 50, "height": 50})).unwrap()); + select(&mut app, &[a, b]); + let c = id(app.run("object.compoundPath.make", json!({})).unwrap()); + click(&mut app, "Edit Path"); + assert_eq!(app.session.tool_id(), "directSelection"); + select(&mut app, &[c]); + let (_, texts) = bar(&mut app); + assert!(has(&texts, "Release Compound Path") && !has(&texts, "Edit Path"), "{texts:?}"); +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 4c615393a..1e2bfa46b 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Character panel menu › Show Snap to Glyph Options shows the Snap to Glyph section (Baseline, x-height, Glyph Bounds, Proximity Guides, Angular Guides, Anchor Point; `view.snapToGlyph.options`) (#4232); scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Contextual Task Bar: with the Direct Selection tool, a path selected whole offers Corner, Smooth (every anchor), Connect (open paths) and Simplify instead of Edit Path and Offset Path, as the public overview lists for that tool (); a compound path under that tool no longer offers Edit Path (#4469); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -361,6 +361,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | Contextual Task Bar: with the Direct Selection tool, a path selected whole offers Corner, Smooth (every anchor), Connect (open paths) and Simplify instead of Edit Path and Offset Path, as the public overview lists for that tool (); a compound path under that tool no longer offers Edit Path (#4469). Tests in `crates/ui-egui/src/tests/taskbar.rs`; scores and estimates unchanged | | 2026-10-30 | minor | Character panel menu › Show Snap to Glyph Options shows the Snap to Glyph section (Baseline, x-height, Glyph Bounds, Proximity Guides, Angular Guides, Anchor Point; `view.snapToGlyph.options`) (#4232); scores and estimates unchanged | | 2026-10-30 | minor | Align panel menu › Align to Glyph Bounds › Point Text and Area Text align and distribute type by the ink of its glyphs (#4231); the Character and Properties panels' controls for it are not done; scores and estimates unchanged | | 2026-10-30 | minor | Art moved whole with the Direct Selection or Group Selection tool and dragged off the canvas goes to the panels (Asset Export, Graphic Styles, Swatches, Symbols) as with the Selection tool; anchor points and segments dragged off stay a reshape (#4233). Tests in `crates/ui-egui/src/canvas/tests.rs`; scores and estimates unchanged |