diff --git a/crates/ui-egui/src/dialogs/gradient_stop.rs b/crates/ui-egui/src/dialogs/gradient_stop.rs index b188ad265..42e66f258 100644 --- a/crates/ui-egui/src/dialogs/gradient_stop.rs +++ b/crates/ui-egui/src/dialogs/gradient_stop.rs @@ -102,6 +102,7 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { }); let swatches = d.str("tab") == "swatches"; let mut tab = None; + let mut eyedropper = false; let resp = egui::Area::new(area) .order(egui::Order::Foreground) .fixed_pos(pos) @@ -115,6 +116,12 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { tab = Some(if icon == "palette" { "color" } else { "swatches" }); } } + // A stop's colour can come from the art, as from the Gradient panel's eyedropper. + if matches!(target, Target::Stop(..)) + && widgets::icon_button(ui, "pipette", tl!("Eyedropper: click the art to sample the stop's colour"), false, 24.0).clicked() + { + eyedropper = true; + } }); ui.add_space(6.0); if swatches { @@ -141,6 +148,11 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { }); }) .response; + if eyedropper { + app.ui.dialog = None; + crate::panels::gradient::stop_eyedropper(app); + return; + } // The frame it opened in (its first drawn). let frame = ctx.cumulative_frame_nr(); let opened = d.fields.get(OPENED).and_then(Value::as_u64).unwrap_or(frame); diff --git a/crates/ui-egui/src/panels/gradient.rs b/crates/ui-egui/src/panels/gradient.rs index 91c3fa889..7fa3926b9 100644 --- a/crates/ui-egui/src/panels/gradient.rs +++ b/crates/ui-egui/src/panels/gradient.rs @@ -572,7 +572,7 @@ fn stop_fields(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: boo /// The stop's eyedropper: the Eyedropper tool samples the next colour clicked on the art into the /// selected stop, then hands back to the current tool. -fn stop_eyedropper(app: &mut VectorcraftApp) { +pub(crate) fn stop_eyedropper(app: &mut VectorcraftApp) { let back = app.session.tool_id(); app.select_tool("eyedropper"); app.session.set_tool_option("stop", &json!(back)); diff --git a/crates/ui-egui/src/panels/gradient/tests.rs b/crates/ui-egui/src/panels/gradient/tests.rs index 3f2581213..0a1f60114 100644 --- a/crates/ui-egui/src/panels/gradient/tests.rs +++ b/crates/ui-egui/src/panels/gradient/tests.rs @@ -540,3 +540,42 @@ fn angle_and_aspect_fields_are_named() { let _ = h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Angle"); let _ = h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Aspect Ratio"); } + +/// #4681: the stop popover the annotator opens has the eyedropper too: it closes the popover and +/// arms the Eyedropper for the stop. A freeform gradient's point has none. +#[test] +fn the_stop_popover_has_an_eyedropper_for_a_stop() { + use egui_kittest::kittest::Queryable; + const TIP: &str = "Eyedropper: click the art to sample the stop's colour"; + let harness = |app: VectorcraftApp| { + let mut fonts = false; + egui_kittest::Harness::builder().with_size(egui::vec2(800.0, 700.0)).with_accessibility_check(false).build_ui_state( + move |ui, app: &mut VectorcraftApp| { + if !std::mem::replace(&mut fonts, true) { + crate::theme::install_fonts(ui.ctx()); + return; + } + crate::dialogs::show(app, ui.ctx()); + }, + app, + ) + }; + let mut app = app(json!({"gradient": {}})); + app.select_tool("gradient"); + app.run("gradient.selectStop", json!({"index": 1})).unwrap(); + app.ui.dialog = Some(crate::state::Dialog::new("gradientStop", json!({"index": 1, "screen": [100.0, 100.0], "tab": "color"}))); + let mut h = harness(app); + h.run_steps(3); + h.get_by_label(TIP).click(); + h.run_steps(2); + let app = h.state(); + assert!(app.ui.dialog.is_none(), "the popover closed"); + assert_eq!((app.session.tool_id(), app.session.tool_options()["stop"].clone()), ("eyedropper", json!("gradient"))); + + let mut app = super::tests::app(json!({"gradient": {"kind": "freeform"}})); + app.select_tool("gradient"); + app.ui.dialog = Some(crate::state::Dialog::new("gradientStop", json!({"index": 0, "screen": [100.0, 100.0], "tab": "color"}))); + let mut h = harness(app); + h.run_steps(3); + assert!(h.query_by_label(TIP).is_none(), "no eyedropper for a freeform point"); +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 8eda7169d..3042edc3d 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Full Screen and Presentation Mode fill the screen (#4365); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Gradient stop popover: an eyedropper samples the stop's colour from the art (#4681); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -922,6 +922,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Sample Color from Screen:** an eyedropper button in the Color Picker and `ui.sampleScreenColor` (which can be given a key in Keyboard Shortcuts) pick a color anywhere on the screen with the system's sampler: `NSColorSampler` on macOS, the EyeDropper API in browsers that have it. The color becomes the fill or the stroke as one undo step, or the Color Picker's color, converted from display sRGB into the working RGB space. Windows, Linux and other browsers have no sampler yet; the button is gray there. - **Stroke visibility (#1253):** adding paint to a previously unpainted zero-width stroke restores the existing 1 pt default (`PaintDefaults::default`, `Appearance::set_stroke`), for the selected appearance item, the new-art defaults and Swap Fill/Stroke. Positive widths are kept; recoloring a painted zero-width stroke keeps its explicit zero. A real Eyedropper-to-new-shape sequence reproduces the zero-width inheritance. Five engine regressions cover creation, defaults, targeted items, preserved widths and one-step Undo/Redo (`crates/engine/tests/it/stroke_visibility.rs`). - **Gradients:** lossless, validated gradient params; gradients follow rotate, reflect, shear, non-uniform scale and distortions; the Gradient tool edits the fill or stroke of the active proxy, type included; an interactive on-canvas annotator (move, length/angle, rotate, add/move/delete/duplicate stops, midpoints, a stop popover, Delete and arrow keys). The Gradient panel has the Fill/Stroke proxy, a gradient-swatch dropdown with Save to Swatches, midpoint locations, Alt-drag to copy or swap stops, swatch drops on the ramp and a stop eyedropper; copied appearances and graphic styles place gradients on each target's own bounds; gradient handles snap and honour Constrain Angle. Gradient stops stay linked to global and spot swatches. Freeform gradients (points and lines) are edited on the canvas; radial gradients have an extent ellipse, an aspect handle and an off-centre focal point (SVG `fx`/`fy`, PDF two-point radials). A stroke's gradient runs within, along or across it. Object ▸ Expand turns gradients into N objects or a gradient mesh. + - The stop popover a double-click on a stop opens (the Gradient tool's annotator or the Gradient panel) has an eyedropper beside its Color and Swatches tabs: it closes the popover and samples the next colour clicked on the art into the stop, as the Gradient panel's eyedropper does (#4681). Cmd/Ctrl-click on the Gradient panel's Gradient Fill box or on the Tools panel's Gradient button resets the gradient of the active Fill or Stroke to the linear White, Black gradient, as one undo step ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). With a gradient stop or a freeform gradient point selected, Alt-clicking a color swatch in the Swatches panel recolors it instead of the inactive Fill or Stroke. - **Freeform gradient colors in their own model:** Object ▸ Expand and Object ▸ Create Gradient Mesh (`object.mesh.create`) mix a freeform gradient's points in the color model they share: points of C50 M20 Y0 K10 give mesh points of C50 M20 Y0 K10, and RGB points keep their values in a working RGB space other than sRGB. Points in different models mix in display RGB, and a CMYK document stores the result as CMYK through the color settings. Points colored along CMYK stops (`paint.editGradient {stops}`, or a CMYK gradient switched to freeform) are CMYK, and so are a point `paint.freeform.splitLine` adds between CMYK points and a point `paint.freeform.addPoint` adds without `color` to CMYK points. Public reference: [Gradients](https://web.archive.org/web/20250714042311/https://helpx.adobe.com/illustrator/using/gradients.html) and [Meshes](https://web.archive.org/web/20250812201019/https://helpx.adobe.com/illustrator/using/meshes.html), which do not state how colors in different models mix; the rule is VectorCraft's own. @@ -1263,6 +1264,7 @@ undo step, with any adjustment and its params; bad requests refused) and `addons | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | Gradient stop popover: an eyedropper samples the stop's colour from the art (#4681); scores and estimates unchanged | | 2026-10-30 | minor | Full Screen and Presentation Mode fill the screen (#4365); scores and estimates unchanged | | 2026-10-30 | minor | Place: a photo's EXIF orientation turns it upright (#4284); scores and estimates unchanged | | 2026-10-30 | minor | Contextual task bar: stays up through a click on another object, hides only for a drag (#4681); scores and estimates unchanged |