diff --git a/ROADMAP.md b/ROADMAP.md index 85cc9ea73..d7a15d992 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -349,6 +349,7 @@ Newest first. - **2026-10-15** · Define Lists… is listed under Type › Bulleted & Numbered Lists in command search and Edit › Menus, matching the menu, and the parity doc no longer lists a Type › Paragraph Rules… menu item that does not exist. No readiness percentage change. - **2026-10-15** · Pressing Enter in the Layers panel's rename field without typing leaves an unnamed object unnamed, so it keeps following its automatic label instead of taking it as a fixed name. No readiness percentage change. +- **2026-10-15** · Apply Gradient (.) with the Stroke box in front puts a new default gradient on the stroke in one undo step and leaves the fill's swatch and tint as they were. No readiness percentage change. - **2026-10-14** · The Type tool draws a new text frame over a locked empty frame, as InDesign does, instead of turning the locked frame into a text frame. No readiness percentage change. - **2026-10-14** · 100% black shows as pure black on screen by default, as InDesign's Appearance of Black does out of the box (Display All Blacks as Rich Black); Preferences › Appearance of Black › Display All Blacks Accurately still shows it as the dark grey it prints, and a saved choice is kept. No readiness percentage change. - **2026-10-14** · Pathfinder keeps the frontmost object, as InDesign does: a photo placed over shapes and intersected with them shows through the shapes (it was dropped, and the shapes kept their black fill); Add, Exclude Overlap and Minus Back take the front object's fill too, Subtract still keeps the back one. No readiness percentage change. diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 7e6ca1cd2..dfd8c4ce4 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -583,17 +583,10 @@ pub(crate) fn apply_gradient(app: &mut DesignApp) -> Result { if crate::panels::colors_affect_text(app) { return Err("no gradient to apply yet: make one in the Gradient panel".into()); } - let fill = crate::panels::color_target(app).map(|t| t.0); let stops = json!([{"location": 0, "color": "#ffffff"}, {"location": 100, "color": "#000000"}]); - let r = app.run("object.gradient", json!({"stops": stops}))?; - let sw = r["swatch"].as_str().unwrap_or_default().to_string(); - app.ui.last_gradient = Some(sw.clone()); - if stroke { - // The gradient was made on the fill: it goes to the stroke, the fill gets its swatch back. - crate::panels::apply_swatch(app, true, &sw, None)?; - if let Some(f) = fill { - app.run("object.fill", json!({"swatch": f}))?; - } + let r = app.run("object.gradient", json!({"stops": stops, "target": if stroke { "stroke" } else { "fill" }}))?; + if let Some(sw) = r["swatch"].as_str() { + app.ui.last_gradient = Some(sw.to_string()); } Ok(r) } @@ -767,6 +760,31 @@ mod tests { } } + /// A new default gradient on the stroke in front is one undo step; the fill keeps its swatch and tint. + #[test] + fn apply_gradient_to_the_stroke_is_one_undo_step() { + use serde_json::json; + let mut app = app(true); + app.run("file.new", json!({})).unwrap(); + let id = app.run("frame.create", json!({"rect": [72, 72, 200, 200], "content": "unassigned"})).unwrap()["id"].clone(); + app.run("selection.set", json!({"ids": [id]})).unwrap(); + app.run("object.fill", json!({"swatch": "[Paper]", "tint": 0.5})).unwrap(); + app.run("object.stroke", json!({"swatch": "[Black]", "tint": 0.7})).unwrap(); + let paints = |app: &crate::DesignApp| { + let st = app.session.active().unwrap(); + let it = st.doc.item(st.selection.items[0]).unwrap(); + (it.fill.swatch.clone(), it.fill.tint, it.stroke.swatch.clone(), it.stroke.tint) + }; + let before = paints(&app); + app.ui.stroke_front = true; + app.run("color.applyGradient", json!({})).unwrap(); + let after = paints(&app); + assert_eq!((&after.0, after.1), (&before.0, before.1), "the fill is untouched"); + assert_ne!(after.2, before.2, "the stroke is the gradient"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(paints(&app), before, "one undo restores the stroke"); + } + /// The Fill/Stroke proxy's keys run on the selection and stay out of text being typed. #[test] fn fill_stroke_proxy_keys() {