diff --git a/crates/ui-egui/src/canvas/task_bar.rs b/crates/ui-egui/src/canvas/task_bar.rs index a81d8307f..d492ece08 100644 --- a/crates/ui-egui/src/canvas/task_bar.rs +++ b/crates/ui-egui/src/canvas/task_bar.rs @@ -142,13 +142,14 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { items.push((tl!("Duplicate"), "copy", "edit.duplicate", none())); } let fill = first.as_ref().map(|f| f.appearance.fill_paint()).unwrap_or_default(); + let stroke = first.as_ref().map(|f| f.appearance.stroke_paint()).unwrap_or_default(); let colored = has_colors(&st.doc, &st.selection.objects); let doc = st.uid; // Under the selection's lowest corner on screen, centered on the selection (the view may be // rotated). let lowest = xf.quad(b).iter().map(|c| c.y).fold(f32::NEG_INFINITY, f32::max); let anchor = pos2(xf.to_screen(b.center()).x, lowest); - let est_w = 118.0 + items.iter().map(|(l, _, _, _)| l.len() as f32 * 7.2 + 44.0).sum::(); + let est_w = 150.0 + items.iter().map(|(l, _, _, _)| l.len() as f32 * 7.2 + 44.0).sum::(); // Its place under the selection, then where the handle moved it, kept on the canvas (with the // bar's size last frame, the estimate before it first shows). let under = pos2(anchor.x - est_w / 2.0, anchor.y + 28.0); @@ -207,7 +208,7 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { run = Some((*cmd, params.clone())); } } - // The selected objects' Fill, Recolor Artwork and Lock, left out with the Artboard tool. + // The selected objects' Fill, Stroke, Recolor Artwork and Lock, left out with the Artboard tool. if !on_artboards { let (r, resp) = ui.allocate_exact_size(vec2(26.0, 30.0), Sense::click()); widgets::paint_chip(ui, egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0)), &fill); @@ -221,6 +222,15 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { app.session.fill_active = true; app.ui.open_panel = Some("swatches".into()); } + // The Stroke chip, drawn as a ring as the Control bar's is. + let (r, resp) = ui.allocate_exact_size(vec2(26.0, 30.0), Sense::click()); + let chip = egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0)); + widgets::proxy_chip(ui, chip, &stroke, false, Some(4.0)); + ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.button_border), StrokeKind::Outside); + if resp.on_hover_text(tl!("Stroke")).clicked() { + app.session.fill_active = false; + app.ui.open_panel = Some("swatches".into()); + } // Recolor Artwork, when the selection has colours to recolour. if colored && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, 30.0).clicked() { run = Some(("ui.recolorDialog", json!({}))); diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index bb76684dc..d3a0e4d1b 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -6067,6 +6067,56 @@ fn a_pen_tap_within_one_frame_clicks_where_it_tapped() { assert_eq!(doc.doc.art_bounds(), before, "the tap moved nothing"); } +/// The Contextual Task Bar shows a Stroke chip beside the Fill chip, drawn with the selection's +/// stroke paint. A click on it brings the Stroke proxy forward and opens Swatches, as a click on +/// the Fill chip does for the Fill proxy. +#[test] +fn the_task_bar_shows_a_stroke_chip_beside_fill() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let id = app.session.execute("shape.rectangle", &json!({"x": 180, "y": 130, "width": 40, "height": 40})).unwrap()["id"].clone(); + app.session.execute("paint.setFill", &json!({"ids": [id], "color": "#cc3322"})).unwrap(); + app.session.execute("paint.setStroke", &json!({"ids": [id], "color": "#2244cc"})).unwrap(); + app.select_tool("selection"); + let ctx = egui::Context::default(); + // Past the bar's fade-in, so its chips draw at full opacity. + for _ in 0..30 { + frame(&mut 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(&mut app, ui)); + out.textures_delta.clear(); + let bar = task_bar_rect(&ctx).expect("the task bar shows"); + // Right to left: the frame's margin, More Options, Lock, Recolor, then Stroke and Fill. + let stroke_x = bar.right() - 8.0 - 3.0 * (30.0 + 6.0) - 13.0; + let fill_x = stroke_x - 26.0 - 6.0; + let blue = Color32::from_rgb(0x22, 0x44, 0xcc); + let chip_with = |x: f32| { + out.shapes.iter().any(|c| match &c.shape { + egui::Shape::Rect(r) => r.fill == blue && bar.contains(r.rect.center()) && (r.rect.center().x - x).abs() < 4.0, + _ => false, + }) + }; + assert!(chip_with(stroke_x), "the Stroke chip is drawn with the stroke color"); + assert!(!chip_with(fill_x), "the Fill chip is not"); + + let click = |app: &mut VectorcraftApp, x: f32| { + let at = pos2(x, bar.center().y); + let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(app, &ctx, vec![egui::Event::PointerMoved(at)]); + frame(app, &ctx, vec![button(true)]); + frame(app, &ctx, vec![button(false)]); + }; + app.session.fill_active = true; + click(&mut app, stroke_x); + assert!(!app.session.fill_active, "the Stroke proxy is in front"); + assert_eq!(app.ui.open_panel.as_deref(), Some("swatches")); + app.ui.open_panel = None; + click(&mut app, fill_x); + assert!(app.session.fill_active, "the Fill proxy is in front"); + assert_eq!(app.ui.open_panel.as_deref(), Some("swatches")); +} + /// The Contextual Task Bar shows Recolor Artwork (a palette) when the selection has colours, /// and it opens the Recolor Artwork dialog; art painted with None has no button. #[test] @@ -6328,7 +6378,7 @@ fn the_task_bar_is_dragged_by_its_handle_pinned_and_reset() { app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); let mut square = |x: f64, y: f64| app.session.execute("shape.rectangle", &json!({"x": x, "y": y, "width": 40, "height": 40})).unwrap()["id"].clone(); - let (a, b) = (square(120.0, 40.0), square(160.0, 150.0)); + let (a, b) = (square(140.0, 40.0), square(160.0, 150.0)); app.select_tool("selection"); let ctx = egui::Context::default(); let bar = || task_bar_rect(&ctx).expect("the task bar shows"); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index ab2507744..2469d7967 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -560,6 +560,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - With one image selected, the contextual task bar offers Image Trace, which traces the image at once with the Default preset (the Image Trace panel is left as it was), and Crop Image; with an Image Trace object it offers Expand, which keeps only the traced paths. This follows Illustrator's bar: for a selected image it includes Image Trace, Retype and Crop Image, and traced artwork can be expanded from it ([Contextual Task Bar announcement](https://community.adobe.com/announcements-651/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580)). Offset Path and Simplify, which don't apply to an image, aren't offered for one. Not done: Retype, which the app doesn't have. - With the Artboard tool, the contextual task bar sits under the selected artboards and offers Resize (Artboard Options for the active artboard), Duplicate (each selected artboard, in one undo step, the copies then selected), Rearrange All (with two or more artboards) and Export (Export for Screens with the selected artboards checked). This follows Illustrator's bar: while the Artboard tool is in use, it has options for resizing, duplicating and exporting artboards (the [Contextual Task Bar announcement](https://community.adobe.com/questions-652/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580); Rearrange and Export on the bar: [Managing Artboards in Illustrator](https://creativepro.com/managing-artboards-in-illustrator/)). - The contextual task bar offers more next steps by selection (): an open path Join and Edit Path (the Direct Selection tool), a closed path Edit Path, a compound path Edit Path and Release Compound Path, a clipping group Edit Clipping Path and Release Clipping Mask instead of Ungroup and Isolate, an image selected with vector art Make Clipping Mask instead of Unite, and point or area type Convert To Area Type or Convert To Point Type beside Create Outlines. Evidence: `crates/ui-egui/src/tests/taskbar.rs`. + - The contextual task bar has a Stroke chip beside the Fill chip, drawn with the selected object's stroke paint; a click brings the Stroke proxy forward and opens the Swatches panel, as a click on the Fill chip does for the Fill proxy ("Fill and Stroke to change the 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` 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 ("Remove selected anchor points, Connect selected end points, Cut path at selected anchor points, Convert selected anchor points to corner, and Convert selected anchor points to smooth": [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). - 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.