From 8703859c66f15db85542b5c33b02784c10890176 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 12:10:11 -0800 Subject: [PATCH] Contextual Task Bar: Smooth opens the Smooth slider with Direct Selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a path selected whole by the Direct Selection tool, the bar's "Smooth" button converted every anchor point to smooth at once. It was the anchor convert, labeled like the path Smooth that the overview lists beside Simplify as adjusting the path's curvature, and the bar had no button for Object › Path › Smooth…. - A path selected whole with Direct Selection now offers Smooth after Simplify. It runs object.path.smooth the way the menu item does, so it opens the Smooth slider and changes nothing until it is confirmed. - The two anchor convert buttons, with a whole path or with anchors selected, show their icons alone, named by the Control bar's tool tips "Convert Selected Anchor Points to Corner" and "to Smooth". The keyboard shortcut part of #6784 is fixed on main by #6810. Docs: docs/mcp.md, docs/target-app-parity.md, the ui.smoothAuto params. Fixes #6784 --- crates/mcp/src/ui_catalog.rs | 2 +- crates/ui-egui/src/canvas/task_bar.rs | 40 ++++++--- crates/ui-egui/src/canvas/tests.rs | 48 +++++++++-- crates/ui-egui/src/menus/ui_commands.rs | 2 +- crates/ui-egui/src/tests/taskbar.rs | 107 ++++++++++++++++++++---- docs/mcp.md | 7 +- docs/target-app-parity.md | 2 +- 7 files changed, 171 insertions(+), 37 deletions(-) diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index 8836b72ac2..9557bf6bf8 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -1258,7 +1258,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "ui.smoothAuto", "Auto-Smooth", "", - "{} the open Smooth slider (Object › Path › Smooth and the canvas context menu's Smooth… open it: dialog `smoothSlider` with field `level` 0..100, object.path.smooth's level, opening at the level object.path.smooth {auto: true} picks, previewed on the canvas until ui.dialog.confirm keeps it as one undo step) moves to the level object.path.smooth {auto: true} picks and previews it → {level}; an error when the slider isn't open", + "{} the open Smooth slider (Object › Path › Smooth, the canvas context menu's Smooth… and the Contextual Task Bar's Smooth open it: dialog `smoothSlider` with field `level` 0..100, object.path.smooth's level, opening at the level object.path.smooth {auto: true} picks, previewed on the canvas until ui.dialog.confirm keeps it as one undo step) moves to the level object.path.smooth {auto: true} picks and previews it → {level}; an error when the slider isn't open", ), ( "ui.simplifyMoreOptions", diff --git a/crates/ui-egui/src/canvas/task_bar.rs b/crates/ui-egui/src/canvas/task_bar.rs index e908640c25..f89a921371 100644 --- a/crates/ui-egui/src/canvas/task_bar.rs +++ b/crates/ui-egui/src/canvas/task_bar.rs @@ -163,8 +163,8 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { items.push((tl!("Remove Anchor Points"), "pen-tool-delete", "path.removeAnchors", none())); items.push((tl!("Connect"), "dc-join", "path.join", none())); items.push((tl!("Cut Path"), "scissors", "path.cutAtAnchors", 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!("Convert Selected Anchor Points to Corner"), "dc-anchor", TASK_BAR_TO_CORNER, none())); + items.push((tl!("Convert Selected Anchor Points to Smooth"), "dc-anchor-smooth", TASK_BAR_TO_SMOOTH, none())); } // As in the reference app, open paths offer Join and Edit Path, compound paths and clipping // masks their Release, images selected with vector art Make Clipping Mask, type converts @@ -213,14 +213,17 @@ 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. + // The two convert buttons convert every anchor of a path selected whole; Remove and Cut + // need anchors picked out, so they wait for them. Simplify and Smooth open their + // sliders, as Object › Path › Simplify… and Smooth… do; the page above names Smooth as + // adjusting the path's curvature, apart from converting its anchor points. 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!("Convert Selected Anchor Points to Corner"), "dc-anchor", TASK_BAR_TO_CORNER, none())); + items.push((tl!("Convert Selected Anchor Points to Smooth"), "dc-anchor-smooth", TASK_BAR_TO_SMOOTH, none())); items.push((tl!("Simplify"), "spline", "object.path.simplify", none())); + items.push((tl!("Smooth"), "dc-smooth", "object.path.smooth", none())); } else if !anchors { if open_path { items.push((tl!("Join"), "dc-join", "path.join", none())); @@ -297,6 +300,14 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { ui.ctx().set_cursor_icon(egui::CursorIcon::Grab); } for (label, icon, cmd, params) in &items { + // The convert buttons show their icon alone, named by their hover text, as + // the Control bar's do. + if icon_only(cmd) { + if widgets::icon_button(ui, icon, label, false, 30.0).clicked() { + run = Some((*cmd, params.clone())); + } + continue; + } let galley = ui.painter().layout_no_wrap(label.to_string(), egui::FontId::proportional(13.0), t.text_strong); let (r, resp) = ui.allocate_exact_size(vec2(galley.size().x + 38.0, 30.0), Sense::click()); resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, *label)); @@ -405,7 +416,7 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { Some((TASK_BAR_ARTBOARD_OPTIONS, _)) => crate::menus::invoke(app, "tool.options", json!({"tool": "artboard"})), Some((TASK_BAR_DUPLICATE_ARTBOARDS, _)) => crate::panels::artboards::duplicate_picked(app), Some((TASK_BAR_REARRANGE_ARTBOARDS, _)) => crate::panels::artboards::open_rearrange(app), - // Corner and Smooth convert the direct-selected anchors, as the Control bar's buttons do. + // The convert buttons convert the direct-selected anchors, as the Control bar's buttons do. Some((TASK_BAR_TO_CORNER, _)) => crate::menus::invoke(app, "path.convertAnchors", json!({"to": "corner"})), Some((TASK_BAR_TO_SMOOTH, _)) => crate::menus::invoke(app, "path.convertAnchors", json!({"to": "smooth"})), Some((c, p)) => crate::menus::invoke(app, c, p), @@ -419,10 +430,16 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { /// `room` (and narrow to fit it there). fn size_estimate(items: &[(&str, &str, &str, serde_json::Value)], fonts: bool, fixed: f32, room: f32) -> (bool, Vec2) { let font_w: f32 = crate::panels::character::TASK_BAR_WIDTHS.iter().filter(|_| fonts).map(|w| w + 6.0).sum(); - let items_w = if fonts { fixed } else { 150.0 } + items.iter().map(|(l, _, _, _)| l.len() as f32 * 7.2 + 44.0).sum::(); + let width = |(l, _, cmd, _): &(&str, &str, &str, serde_json::Value)| if icon_only(cmd) { 36.0 } else { l.len() as f32 * 7.2 + 44.0 }; + let items_w = if fonts { fixed } else { 150.0 } + items.iter().map(width).sum::(); if fonts && items_w + font_w > room { (true, vec2(items_w.max((font_w + 16.0).min(room)), 80.0)) } else { (false, vec2(items_w + font_w, 44.0)) } } +/// Whether the bar's item that runs `cmd` shows its icon alone: the anchor convert buttons. +fn icon_only(cmd: &str) -> bool { + matches!(cmd, TASK_BAR_TO_CORNER | TASK_BAR_TO_SMOOTH) +} + /// The paints the Fill and Stroke chips show, and whether each is mixed ("?"): the proxies', or /// while a Type tool edits type, the edited characters' fill, which a swatch picked then paints. pub(crate) fn chip_paints(app: &VectorcraftApp, ctx: &egui::Context) -> (vectorcraft_color::Paint, vectorcraft_color::Paint, bool, bool) { @@ -473,8 +490,11 @@ const TASK_BAR_DUPLICATE_ARTBOARDS: &str = "taskBar.duplicateArtboards"; /// The Contextual Task Bar's Rearrange All button with the Artboard tool. const TASK_BAR_REARRANGE_ARTBOARDS: &str = "taskBar.rearrangeArtboards"; -/// The Contextual Task Bar's Corner button for direct-selected anchors. +/// The Contextual Task Bar's Convert Selected Anchor Points to Corner button, for direct-selected +/// anchors or a path selected whole with Direct Selection. const TASK_BAR_TO_CORNER: &str = "taskBar.toCorner"; -/// The Contextual Task Bar's Smooth button for direct-selected anchors. +/// The Contextual Task Bar's Convert Selected Anchor Points to Smooth button, for direct-selected +/// anchors or a path selected whole with Direct Selection. The bar's Smooth button runs +/// `object.path.smooth` instead. const TASK_BAR_TO_SMOOTH: &str = "taskBar.toSmooth"; diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index d6df378c87..dc95453b98 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -10350,9 +10350,34 @@ fn the_artboard_tools_task_bar_buttons_act_on_the_selected_artboards() { assert_eq!(boards(&app), before, "one undo step"); } +/// The names and places of the buttons the Contextual Task Bar draws, from the accessibility tree +/// (`ctx` has it enabled): an icon-only button is named by its hover text. +fn task_bar_buttons(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec<(String, egui::Rect)> { + frame(app, ctx, vec![]); + let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| show(app, ui)); + out.textures_delta.clear(); + let Some(bar) = task_bar_rect(ctx) else { return vec![] }; + let Some(update) = out.platform_output.accesskit_update.as_ref() else { return vec![] }; + let mut v: Vec<(String, egui::Rect)> = update + .nodes + .iter() + .filter(|(_, n)| n.role() == egui::accesskit::Role::Button) + .filter_map(|(_, n)| { + let b = n.bounds()?; + let r = egui::Rect::from_min_max(Pos2::new(b.x0 as f32, b.y0 as f32), Pos2::new(b.x1 as f32, b.y1 as f32)); + let label = n.label()?; + bar.contains(r.center()).then(|| (label.to_string(), r)) + }) + .collect(); + v.sort_by(|a, b| a.1.left().total_cmp(&b.1.left())); + v +} + /// With anchors direct-selected, the Contextual Task Bar offers Remove Anchor Points, Connect, -/// Cut Path, Corner and Smooth. Smooth and Corner convert the selected anchor, Cut Path opens -/// the path there and Connect closes it again, as the Control bar's anchor buttons do. +/// Cut Path and the two convert buttons, which show their icons alone. Converting to smooth and to +/// corner changes the selected anchor, Cut Path opens the path there and Connect closes it again, +/// as the Control bar's anchor buttons do. #[test] fn the_task_bar_connects_cuts_and_converts_direct_selected_anchors() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); @@ -10361,10 +10386,21 @@ fn the_task_bar_connects_cuts_and_converts_direct_selected_anchors() { app.select_tool("directSelection"); app.run("select.anchors", json!({"id": id, "anchors": [[0, 1]]})).unwrap(); let ctx = egui::Context::default(); + ctx.enable_accesskit(); let names = |app: &mut VectorcraftApp| task_bar_labels(app, &ctx).into_iter().map(|(t, _)| t).collect::>(); - assert_eq!(names(&mut app), ["Remove Anchor Points", "Connect", "Cut Path", "Corner", "Smooth", "Duplicate"]); + assert_eq!(names(&mut app), ["Remove Anchor Points", "Connect", "Cut Path", "Duplicate"]); + let buttons = task_bar_buttons(&mut app, &ctx).into_iter().map(|(t, _)| t).collect::>(); + let expected = [ + "Remove Anchor Points", + "Connect", + "Cut Path", + "Convert Selected Anchor Points to Corner", + "Convert Selected Anchor Points to Smooth", + "Duplicate", + ]; + assert_eq!(buttons[..expected.len()], expected, "{buttons:?}"); let press = |app: &mut VectorcraftApp, label: &str| { - let labels = task_bar_labels(app, &ctx); + let labels = task_bar_buttons(app, &ctx); let at = labels.iter().find(|(t, _)| t == label).unwrap_or_else(|| panic!("{label} in {labels:?}")).1.center(); let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; frame(app, &ctx, vec![egui::Event::PointerMoved(at)]); @@ -10374,11 +10410,11 @@ fn the_task_bar_connects_cuts_and_converts_direct_selected_anchors() { let subpath = |app: &VectorcraftApp| app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().path_data().unwrap().subpaths[0].clone(); let last_undo = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.last().unwrap().label.clone(); - press(&mut app, "Smooth"); + press(&mut app, "Convert Selected Anchor Points to Smooth"); let a = subpath(&app).anchors[1]; assert!(a.has_in() && a.has_out(), "smooth: {a:?}"); assert_eq!(last_undo(&app), "Convert Anchor Points"); - press(&mut app, "Corner"); + press(&mut app, "Convert Selected Anchor Points to Corner"); let a = subpath(&app).anchors[1]; assert!(!a.has_in() && !a.has_out(), "corner: {a:?}"); press(&mut app, "Cut Path"); diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 37cdcb2d56..b21c28006c 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -1259,7 +1259,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "ui.smoothAuto", "Auto-Smooth", "", - "{} the open Smooth slider (Object › Path › Smooth and the canvas context menu's Smooth… open it: dialog `smoothSlider` with field `level` 0..100, object.path.smooth's level, opening at the level object.path.smooth {auto: true} picks, previewed on the canvas until ui.dialog.confirm keeps it as one undo step) moves to the level object.path.smooth {auto: true} picks and previews it → {level}; an error when the slider isn't open", + "{} the open Smooth slider (Object › Path › Smooth, the canvas context menu's Smooth… and the Contextual Task Bar's Smooth open it: dialog `smoothSlider` with field `level` 0..100, object.path.smooth's level, opening at the level object.path.smooth {auto: true} picks, previewed on the canvas until ui.dialog.confirm keeps it as one undo step) moves to the level object.path.smooth {auto: true} picks and previews it → {level}; an error when the slider isn't open", ), ( "ui.simplifyMoreOptions", diff --git a/crates/ui-egui/src/tests/taskbar.rs b/crates/ui-egui/src/tests/taskbar.rs index 68ef8a3f15..1d29663473 100644 --- a/crates/ui-egui/src/tests/taskbar.rs +++ b/crates/ui-egui/src/tests/taskbar.rs @@ -54,6 +54,42 @@ fn click(app: &mut VectorcraftApp, label: &str) { frame(app, &ctx, vec![]); } +/// The names and places of the buttons the task bar draws, from the accessibility tree: an +/// icon-only button is named by its hover text. +fn buttons(app: &mut VectorcraftApp) -> (egui::Context, Vec<(String, Rect)>) { + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + frame(app, &ctx, vec![]); + let screen = Rect::from_min_size(Pos2::ZERO, vec2(1000.0, 700.0)); + let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| canvas::show(app, ui)); + out.textures_delta.clear(); + let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes; + let named = nodes + .iter() + .filter(|(_, n)| n.role() == egui::accesskit::Role::Button) + .filter_map(|(_, n)| { + let b = n.bounds()?; + Some((n.label()?.to_string(), Rect::from_min_max(Pos2::new(b.x0 as f32, b.y0 as f32), Pos2::new(b.x1 as f32, b.y1 as f32)))) + }) + .collect(); + (ctx, named) +} + +/// Click the task bar's button named `name` (its label, or an icon-only button's hover text). +fn click_button(app: &mut VectorcraftApp, name: &str) { + let (ctx, named) = buttons(app); + let p = named.iter().find(|(n, _)| n == name).unwrap_or_else(|| panic!("no button named {name:?} in {named:?}")).1.center(); + let press = |pressed| Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(app, &ctx, vec![Event::PointerMoved(p)]); + frame(app, &ctx, vec![press(true)]); + frame(app, &ctx, vec![press(false)]); + frame(app, &ctx, vec![]); +} + +const TO_CORNER: &str = "Convert Selected Anchor Points to Corner"; +const TO_SMOOTH: &str = "Convert Selected Anchor Points to Smooth"; + fn select(app: &mut VectorcraftApp, ids: &[u64]) { app.run("select.set", json!({"ids": ids})).unwrap(); } @@ -293,27 +329,67 @@ fn direct_selection_of_a_whole_path_offers_its_anchor_actions() { 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 (_, named) = buttons(&mut app); + let names: Vec<&str> = named.iter().map(|(n, _)| n.as_str()).collect(); + for name in ["Connect", TO_CORNER, TO_SMOOTH, "Simplify", "Smooth", "Duplicate"] { + assert!(names.contains(&name), "{name}: {names:?}"); + } + for name in ["Edit Path", "Offset Path", "Join", "Remove Anchor Points", "Cut Path"] { + assert!(!names.contains(&name), "{name}: {names:?}"); + } + // The convert buttons show their icons alone: the bar draws no convert text, and one "Smooth". 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"); + assert!(!has(&texts, "Corner") && !texts.iter().any(|(t, _)| t.starts_with("Convert")), "{texts:?}"); + assert_eq!(texts.iter().filter(|(t, _)| t == "Smooth").count(), 1, "{texts:?}"); + // The convert buttons convert every anchor of the path, one undo step each. + click_button(&mut app, TO_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"); + click_button(&mut app, TO_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:?}"); + let (_, named) = buttons(&mut app); + assert!(!named.iter().any(|(n, _)| n == "Connect") && named.iter().any(|(n, _)| n == TO_CORNER), "{named:?}"); +} + +/// With the Direct Selection tool, the bar's Smooth smooths the path's curvature: it opens the +/// Smooth slider, as Object › Path › Smooth… does, and changes nothing until the slider is +/// confirmed (#6784). Simplify opens its slider in the same way. +#[test] +fn direct_selection_smooth_opens_the_smooth_slider() { + for (name, kind) in [("Smooth", crate::dialogs::smooth_slider::KIND), ("Simplify", crate::dialogs::simplify_slider::KIND)] { + let mut app = app(); + // A jagged line of 40 anchor points, which Smooth changes at its opening level. + let anchors_in: Vec<_> = (0..40).map(|i| json!({"x": 50 + i * 5, "y": 150 + (i % 2) * 6})).collect(); + let line = id(app.run("path.create", json!({"anchors": anchors_in})).unwrap()); + app.run("tool.select", json!({"tool": "directSelection"})).unwrap(); + select(&mut app, &[line]); + let before = anchors(&app, line); + let undo = app.session.active().unwrap().history.undo.len(); + click_button(&mut app, name); + let d = app.ui.dialog.as_ref().unwrap_or_else(|| panic!("{name} opens its slider: {}", app.ui.status)); + assert_eq!(d.kind, kind, "{name}"); + assert_eq!(app.session.active().unwrap().history.undo.len(), undo, "{name} adds no undo step before it is confirmed"); + crate::dialogs::cancel(&mut app); + assert_eq!(anchors(&app, line), before, "{name}: canceling leaves the path as it was"); + if name != "Smooth" { + continue; + } + // Confirmed, the slider smooths the path as one undo step, which is not the anchor convert. + click_button(&mut app, name); + crate::dialogs::confirm(&mut app).unwrap(); + let history = &app.session.active().unwrap().history.undo; + assert_eq!(history.len(), undo + 1, "one undo step"); + assert_ne!(history.last().unwrap().label, "Convert Anchor Points"); + assert_ne!(anchors(&app, line), before, "Smooth changes the path's curvature"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(anchors(&app, line), before, "undo restores the path"); + } } /// Anchors picked out with the Direct Selection tool keep the full set: Remove and Cut need them. @@ -323,11 +399,12 @@ fn direct_selected_anchors_still_offer_remove_and_cut() { 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:?}"); + let (_, named) = buttons(&mut app); + let names: Vec<&str> = named.iter().map(|(n, _)| n.as_str()).collect(); + for name in ["Remove Anchor Points", "Connect", "Cut Path", TO_CORNER, TO_SMOOTH] { + assert!(names.contains(&name), "{name}: {names:?}"); } - assert!(!has(&texts, "Edit Path"), "{texts:?}"); + assert!(!names.contains(&"Edit Path"), "{names:?}"); } /// Click the task bar's button left of More Options (Lock, or a basic shape's Shape Properties), diff --git a/docs/mcp.md b/docs/mcp.md index 6bebc86bab..2d2e9cc6bc 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5156,9 +5156,10 @@ round, such as a rectangle, stays as it is. Closed paths stay closed and open pa {"name":"run_command","arguments":{"command":"object.path.smooth","params":{"level":80}}} ``` -In the desktop app, Object › Path › Smooth and the canvas context menu's Smooth… (listed while the selection holds a -path) open the slider (`ui.menuDialog {command: "object.path.smooth"}`, dialog `smoothSlider` with field `level`) at -the Auto-Smooth level, and the slider previews the result until `ui.dialog.confirm`; `ui.smoothAuto` moves the open +In the desktop app, Object › Path › Smooth, the canvas context menu's Smooth… (listed while the selection holds a +path) and the Contextual Task Bar's Smooth (with a path selected whole with the Direct Selection tool) open the +slider (`ui.menuDialog {command: "object.path.smooth"}`, dialog `smoothSlider` with field `level`) at the +Auto-Smooth level, and the slider previews the result until `ui.dialog.confirm`; `ui.smoothAuto` moves the open slider back to the Auto-Smooth level. ## Editing a path with the Curvature tool diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7fdc7ef81f..0efc5b77ce 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -845,7 +845,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - While a Type tool (Type, Area Type, Type on a Path and their vertical versions) adds or edits type, the contextual task bar shows the font family menu, the font style menu, the Font Size combo and the Fill chip (); the Touch Type tool shows no bar, as the same page describes. Type selected with a selection tool shows the same three font controls beside its Create Outlines, conversion and Type on a Path items (). The controls act as the Character panel's do: on the characters a Type tool selects, else on every selected type object, with the size left blank where the sizes differ. VectorCraft's own rules: a Type tool's bar leaves out the object actions, the Stroke chip, Recolor Artwork and Lock; a Type tool shows no bar before it adds type or with other art selected; type selected with other art shows no font controls; the font controls go on a second row when one row would be wider than the canvas, and narrow to fit a canvas narrower than they are; while a Type tool edits type, the Fill chip shows the edited characters' fill ("?" where it differs). Evidence: `the_type_tools_show_font_and_fill_controls`, `the_type_tools_show_no_bar_without_type`, `selected_type_shows_font_controls_beside_its_actions`, `the_task_bars_font_size_sets_what_the_character_panel_would`, `the_type_tools_fill_chip_activates_the_fill`, `the_type_tools_bar_is_named_and_stays_on_the_canvas`, `selected_types_font_controls_take_a_second_row_on_a_narrow_canvas` and `the_type_tools_fill_chip_shows_the_edited_characters_fill` in `crates/ui-egui/src/tests/taskbar.rs`. - With a live rectangle, rounded rectangle, ellipse or polygon selected with the Selection tool, the contextual task bar shows a Shape Properties button in place of Lock. It opens a popup with the shape's rows from the Transform panel (corner radius, pie angles, or sides, angle, corner and radius), which edit it through `object.setLiveShape`. The overview describes this: when a shape drawn with a shape tool (Rectangle, Star) is selected, the bar offers to change its properties where Lock Object would be ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Object › Lock and ⌘2 still lock the shape. Not done: stars, which the app draws as plain paths. Evidence: `a_basic_shape_shows_its_properties_in_place_of_lock`, `the_shape_properties_popup_edits_the_shape` and `other_selections_keep_lock` in `crates/ui-egui/src/tests/taskbar.rs`. - The contextual task bar has a Stroke chip beside the Fill chip, drawn with the paint the Stroke proxy shows (a "?" when the selected objects' strokes differ, as on the Fill chip); a click brings the Stroke proxy forward and shows the Swatches panel wherever it is docked, floating or closed, as a click on the Fill chip does for the Fill proxy. The reference app's bar has Fill and Stroke controls for changing an object's colors ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Evidence: `the_task_bar_shows_a_stroke_chip_beside_fill` and `the_task_bar_chips_follow_the_proxies_and_show_swatches_anywhere` in `crates/ui-egui/src/canvas/tests.rs`. - - With anchor points selected with Direct Selection, the contextual task bar offers Remove Anchor Points, Connect (`path.join`), Cut Path, Corner and Smooth (`path.convertAnchors` with `to` set to `corner` or `smooth`), the commands of the Control bar's anchor buttons. As in Illustrator, whose bar has items that delete the selected anchors, join selected end points, split the path at the selected anchors, and make the selected anchors corner or smooth points ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Not done: Simplify, which the bar offers only while no anchors are selected, and the bar's Smooth item that adjusts a path's curvature, a command the app doesn't have (Object › Path › Smooth). + - With anchor points selected with Direct Selection, the contextual task bar offers Remove Anchor Points, Connect (`path.join`), Cut Path, and Convert Selected Anchor Points to Corner and to Smooth (`path.convertAnchors` with `to` set to `corner` or `smooth`), the commands of the Control bar's anchor buttons; the two convert buttons show their icons alone, named by their tool tips as on the Control bar (VectorCraft's own choice). The overview lists items that delete the selected anchors, join selected end points, split the path at the selected anchors, and make the selected anchors corner or smooth points ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). With a path selected whole with Direct Selection, the bar also offers Simplify and Smooth, which open the Simplify and Smooth sliders as Object › Path › Simplify… and Smooth… do; the same overview names Smooth as adjusting the path's curvature (#6784). VectorCraft's own rule: while anchors are picked out, the bar offers the anchor items without Simplify and Smooth. Evidence: `direct_selection_smooth_opens_the_smooth_slider` in `crates/ui-egui/src/tests/taskbar.rs`. - Tool flyouts tear off: dragging or clicking the tear-off bar on a flyout's right side (or releasing the long press over it) floats its group as a strip of tool buttons, moved by the same bar and put back in the toolbar by the × at its top (`window.floatTools`). While a group floats, the presses that would open its flyout raise the strip instead. Floating strips are kept with the preferences and in user workspaces. Not done: turning a strip vertical, docking it. - Screen readers get a name for the Tools panel's tool buttons, flyout rows, floating strips and tear-off bars, and for the icon buttons, text buttons, toggles, checkboxes, radio buttons, tabs, chips, sliders, angle dials and combo boxes that panels and dialogs draw with `crates/ui-egui/src/widgets.rs`: the text, label or tool tip each one shows, or a name of its own where it shows none (the angle dial, the spinner, the preset arrow, the color wheel and the Reference Point's squares). The tool buttons, toggles, checkboxes, radio buttons, tabs and the Reference Point's nine squares also report whether they are on, an icon button that is lit, such as the chosen Align To button, reports that it is pressed, and the angle dial reports its angle. The status bar's First, Previous, Next and Last Artboard buttons have tool tips. - Tab does not stop on the regions that only the pointer uses: the custom title bar's background and the window's resize zones, the band at the top of a dialog that moves it, the Tools panel's grip, a flyout's and a floating strip's tear-off bar, the isolation bar's background, and the rulers and the box where they meet.