diff --git a/crates/ui-egui/src/panels/gradient.rs b/crates/ui-egui/src/panels/gradient.rs index 2b71ea054..631d38ab7 100644 --- a/crates/ui-egui/src/panels/gradient.rs +++ b/crates/ui-egui/src/panels/gradient.rs @@ -41,6 +41,8 @@ use crate::{VectorcraftApp, icons}; /// Dragging a stop this far below the ramp removes it. const REMOVE_DISTANCE: f32 = 28.0; +/// The Delete Stop button beside the ramp, points square. +const DELETE_STOP_SIZE: f32 = 22.0; /// The Angle field's preset menu, degrees. const ANGLE_PRESETS: [f64; 9] = [-180.0, -135.0, -90.0, -45.0, 0.0, 45.0, 90.0, 135.0, 180.0]; /// The Aspect Ratio field's preset menu, percent. @@ -280,7 +282,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if hidden { if !freeform { ui.add_space(6.0); - ramp(app, ui, &g.gradient, is_grad); + ramp_row(app, ui, &g.gradient, is_grad); } return; } @@ -358,7 +360,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); }); ui.add_space(6.0); - ramp(app, ui, &g.gradient, is_grad); + ramp_row(app, ui, &g.gradient, is_grad); ui.add_space(4.0); stop_fields(app, ui, &g.gradient, is_grad); // Dither applies to gradient fills only: off and dimmed behind the Stroke proxy. @@ -569,7 +571,7 @@ fn thumbnail(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint, is_grad: } /// The selected stop's colour (click: its popover; the eyedropper samples one from the art), -/// opacity and location (a selected diamond's midpoint instead), and Delete Stop. +/// opacity and location (a selected diamond's midpoint instead). fn stop_fields(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: bool) { let stops = &g.stops; let sel = app.session.selected_stop().filter(|i| *i < stops.len() && is_grad); @@ -623,14 +625,29 @@ fn stop_fields(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: boo } } }); - let can_del = sel.is_some() && stops.len() > MIN_STOPS; - if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Stop"), false, can_del, 22.0).clicked() - && let Some(i) = sel - && let Some(v) = remove_stop(stops, i) - { - let next = i.min(v.len() - 1); - set_stops(app, &v, Some(next), Live::Released); - } + }); +} + +/// The ramp with Delete Stop at its right end, level with the ramp so it shows whenever the ramp +/// does: it deletes the selected stop, never below two. +fn ramp_row(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: bool) { + ui.horizontal_top(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + let w = (ui.available_width() - DELETE_STOP_SIZE).max(DELETE_STOP_SIZE * 2.0); + ramp(app, ui, g, is_grad, w); + ui.vertical(|ui| { + // Level with the ramp (the bar starts 10 points down and is 18 tall). + ui.add_space(8.0); + let sel = app.session.selected_stop().filter(|i| *i < g.stops.len() && is_grad); + let can_del = sel.is_some() && g.stops.len() > MIN_STOPS; + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Stop"), false, can_del, DELETE_STOP_SIZE).clicked() + && let Some(i) = sel + && let Some(v) = remove_stop(&g.stops, i) + { + let next = i.min(v.len().saturating_sub(1)); + set_stops(app, &v, Some(next), Live::Released); + } + }); }); } @@ -709,10 +726,9 @@ fn dropped_swatch(app: &VectorcraftApp, ui: &Ui, g: &Gradient, area_resp: &egui: Some(drop_color(g, g.stops.iter().position(|s| marker(s.offset).contains(p)), x_to_offset(p.x, bar.left(), bar.width()), c, link)) } -/// The gradient slider: ramp, stops below, midpoint diamonds above. -fn ramp(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: bool) { +/// The gradient slider, `w` points wide: ramp, stops below, midpoint diamonds above. +fn ramp(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: bool, w: f32) { let t = Tokens::get(ui.ctx()); - let w = ui.available_width(); let (area, area_resp) = ui.allocate_exact_size(vec2(w, 50.0), Sense::hover()); let bar = Rect::from_min_size(area.min + vec2(8.0, 10.0), vec2(w - 16.0, 18.0)); paint_ramp(ui, g, bar); diff --git a/crates/ui-egui/src/panels/gradient/tests.rs b/crates/ui-egui/src/panels/gradient/tests.rs index 83a290bb1..65602079b 100644 --- a/crates/ui-egui/src/panels/gradient/tests.rs +++ b/crates/ui-egui/src/panels/gradient/tests.rs @@ -726,3 +726,39 @@ fn the_panel_menu_saves_deletes_and_hides_presets() { p.menu_click(&mut app, text_at(&texts, "Show Presets").unwrap()); assert!(text_at(&p.frame(&mut app, vec![], Modifiers::NONE), "Presets:").is_some()); } + +/// A stop is deleted by dragging it down off the ramp or with Delete Stop, which sits at the +/// ramp's right end (so a short panel doesn't hide it below the stop fields, #5403); one undo +/// step each, and a two-stop gradient keeps both. +#[test] +fn a_stop_dragged_off_the_ramp_or_deleted_with_the_button_beside_it_goes() { + let stops = json!([{"offset": 0, "color": "#ffffff"}, {"offset": 0.5, "color": "#ff0000"}, {"offset": 1, "color": "#000000"}]); + let mut app = app(json!({"gradient": {"stops": stops}})); + let mut p = Panel::new(); + p.frame(&mut app, vec![], Modifiers::NONE); + let mid = p.widget(("grad-stop", 1)).center(); + // Dragged down off the ramp, the middle stop goes. + p.drag(&mut app, mid, mid + vec2(0.0, 60.0), Modifiers::NONE); + assert_eq!(hexes(&app), ["#ffffff", "#000000"]); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(hexes(&app), ["#ffffff", "#ff0000", "#000000"]); + // Delete Stop, level with the ramp and right of it, removes the selected stop. + app.run("gradient.selectStop", json!({"index": 1})).unwrap(); + p.frame(&mut app, vec![], Modifiers::NONE); + let bar = p.widget("grad-bar"); + let del = *p.row_after(bar.left_center(), 22.0).last().expect("Delete Stop beside the ramp"); + assert!(del.left() >= bar.right(), "right of the ramp"); + p.click(&mut app, del.center()); + assert_eq!(hexes(&app), ["#ffffff", "#000000"]); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(hexes(&app), ["#ffffff", "#ff0000", "#000000"]); + app.run("edit.redo", json!({})).unwrap(); + // Two stops stay two: dragged off, the stop comes back, and the button is off. + let first = p.widget(("grad-stop", 0)).center(); + p.drag(&mut app, first, first + vec2(0.0, 60.0), Modifiers::NONE); + assert_eq!(hexes(&app), ["#ffffff", "#000000"]); + app.run("gradient.selectStop", json!({"index": 0})).unwrap(); + p.frame(&mut app, vec![], Modifiers::NONE); + p.click(&mut app, del.center()); + assert_eq!(hexes(&app), ["#ffffff", "#000000"]); +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index a8ee089e5..cbbb336c8 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-11-05 · **Change:** minor (Artboards panel multi-select with any tool, Fill for every selected artboard; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Gradient panel: Delete Stop beside the ramp, #5403; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -27,6 +27,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-04 | minor | Transform Each's Scale has the documented Relative and Absolute radio buttons (`object.transformEach` `relative`: percentages of each object's size, or the width and height it becomes, measured on each object's own bounds) and the link that keeps the two scales in proportion; the panel lists Scale, Move, Rotate, the Options in two columns, the Reference Point and Preview (level with Copy, Cancel and OK) in the documented order. Scores and estimates unchanged. | | 2026-11-05 | minor | PDF import: a DeviceN gradient of a spot ink over process plates keeps the ink and the plates, and PDF export writes it as a DeviceN shading again (#3235); scores unchanged | | 2026-11-05 | minor | Optical kerning moves each pair part of the way toward the font's own stem spacing, with a minimum gap and tighter limits, and leaves ideographs, kana, fullwidth forms and dashes on the font's spacing, so text is no longer squeezed (#5336). Scores and estimates unchanged. | +| 2026-11-05 | minor | Gradient panel: Delete Stop sits at the right end of the ramp, so it shows whenever the ramp does instead of under the stop fields, where a short panel hid it (#5403); scores and estimates unchanged | | 2026-11-04 | minor | Type core: a Type-range Copy or Cut publishes styled art alongside plain text, so canvas Paste keeps font family, size and color after editing ends (#5636); scores and estimates unchanged. | | 2026-11-04 | minor | Selection Tools add-on: Select by Artboards selects the objects overlapping, fully inside or fully outside several chosen artboards, from a dialog that lists, searches and multi-selects the artboards and fits one in the window on double-click (#5525, #5548, #5549, a VectorCraft addition); scores and estimates unchanged. | | 2026-11-04 | minor | New Document: the units chosen in Preset Details belong to the chosen preset, as the public page lists the pane's options for the selected preset; the chosen card shows them, every other Recent and Saved card keeps the units its document or preset was made in, and the built-in tabs still follow the chosen units (#2078). Scores and estimates unchanged. | @@ -1064,6 +1065,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 Gradient panel's Delete Stop sits at the right end of the ramp, level with it, so a panel too short to show the stop fields still shows it; it deletes the selected stop (never below two), as dragging a stop down off the ramp does (#5403). - 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. diff --git a/docs/ui-parity.md b/docs/ui-parity.md index c7a988d9f..35bdd5d62 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-11-04 · **Change:** minor (Path Precision Tools: Set Corners… rounds every other anchor; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Gradient panel: Delete Stop beside the ramp, #5403; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) ## Feature submission evidence @@ -30,6 +30,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-05 | minor | Gradient panel: Delete Stop sits at the right end of the ramp, so it shows whenever the ramp does instead of under the stop fields, where a short panel hid it (#5403); scores and estimates unchanged | | 2026-11-04 | minor | Artboards panel: a Shift- or Ctrl/Cmd-click on a row selects several artboards with any tool, switching to the Artboard tool first, so a preset, orientation, size or Fill set in Properties or the Control bar goes to every selected artboard; Fill used to set the active artboard only (#5698); scores and estimates unchanged | | 2026-11-04 | minor | Isolation mode: once the layer, or a group, around the isolated object is locked or hidden, a click or a marquee with the Selection tool no longer selects the isolated art (part of #5309); scores and estimates unchanged | | 2026-11-04 | minor | Transform Each's Scale has the documented Relative and Absolute radio buttons: percentages of each object's size, or the width and height it becomes (`object.transformEach` `relative`); the two scales have the link that keeps them in proportion (`__constrain`, as the shape dialogs'), and the panel lists Scale, Move, Rotate, the Options in two columns, the Reference Point and Preview (level with Copy, Cancel and OK) in the documented order; scores and estimates unchanged | @@ -358,6 +359,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Gradient panel: Dither (30.0) | done | Dither box under the stops (`paint.editGradient` `dither`); dimmed behind the Stroke proxy, fills only; `panels::gradient` test | | Gradient panel: Method, Classic or Perceptual (30.0) | done | Method menu under the Stroke buttons for linear and radial gradients (`paint.editGradient` `method`); [public source](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html); `panels::gradient` test | | Gradient panel: Presets (30.0) | done | Presets menu by library (theme) plus Saved: applies a preset to the gradient behind the active proxy, keeping its placement, one undo step; panel menu Save Gradient Preset…, Delete Gradient Preset, Hide/Show Presets (`gradient.presets.*`); `panels::gradient` tests | +| Gradient panel: delete a stop | done | Delete Stop at the right end of the ramp deletes the selected stop, and a stop dragged down off the ramp is deleted on release, never below two stops, one undo step each; [Gradient panel options](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/gradient-tool-and-panel.html); `panels::gradient` test | | SVG Interactivity, Retype, Comments, Version History panels | missing | the last two are cloud features (out of scope) | | Workspaces (save, reset, switch); each remembers the layout changes made in it across switches and restarts until Reset | done | `UiState::workspace_changes` | | Background Export: a progress dialog with the file name, the artboard count and a bar (busy for one artboard and for formats made in one go), Cancel (Esc) and Continue in Background; it opens after 0.4 s and never over another dialog. A cancelled export writes no file | done | #2564; on the shared `vectorcraft_doc::cancel` token (#2546), so a cancelled render keeps no half-drawn pattern tile or effect raster. The bar moves between artboards, not within one. Save As, Export for Screens and Export Selection don't show it yet |