From 73eaa84d346f6fad648b966b9fea4ade026dcd36 Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Mon, 19 Oct 2026 01:46:38 +0300 Subject: [PATCH 1/4] G1: Alt-drag an existing ruler guide to swap its orientation Match the public shortcut reference: Alt while dragging converts horizontal and vertical, with snapping on the new axis; guide.move accepts vertical on the index form. Co-authored-by: Cursor --- crates/engine/src/cmd/docmenu.rs | 5 +- crates/engine/src/tests_menucmds.rs | 8 +++ crates/tools/src/rulerguide.rs | 99 ++++++++++++++++++----------- 3 files changed, 75 insertions(+), 37 deletions(-) diff --git a/crates/engine/src/cmd/docmenu.rs b/crates/engine/src/cmd/docmenu.rs index 2cbd4a3b9..4ad95af58 100644 --- a/crates/engine/src/cmd/docmenu.rs +++ b/crates/engine/src/cmd/docmenu.rs @@ -207,7 +207,7 @@ pub fn specs() -> Vec { "Move Guide", [], None, - "{index, pos: pt} | {dx?, dy?: pt, copy?: bool} put ruler guide `index` at `pos`, or move the selected guides (vertical ones by dx, horizontal ones by dy); copy leaves them and selects the moved copies", + "{index, pos: pt, vertical?: bool} | {dx?, dy?: pt, copy?: bool} put ruler guide `index` at `pos` (and swap its orientation when `vertical` is given), or move the selected guides (vertical ones by dx, horizontal ones by dy); copy leaves them and selects the moved copies", guides_unlocked, guide_move ), @@ -769,6 +769,9 @@ pub(crate) fn guide_move(s: &mut Session, p: &Value) -> Result { s.edit("Move Guide", |d, _| { if let Some(g) = d.guides.get_mut(i) { g.pos = pos; + if p.get("vertical").is_some() { + g.vertical = bool_or(p, "vertical", false); + } } Ok(()) })?; diff --git a/crates/engine/src/tests_menucmds.rs b/crates/engine/src/tests_menucmds.rs index 8b93e5532..5578610e1 100644 --- a/crates/engine/src/tests_menucmds.rs +++ b/crates/engine/src/tests_menucmds.rs @@ -2011,6 +2011,14 @@ fn ruler_guides_and_lock() { assert!(s.execute("guide.remove", &json!({"index": 0})).is_err()); } +#[test] +fn guide_move_can_swap_orientation() { + let mut s = session(); + s.execute("guide.add", &json!({"vertical": false, "pos": 50})).unwrap(); + s.execute("guide.move", &json!({"index": 0, "pos": 120, "vertical": true})).unwrap(); + assert_eq!((s.doc().unwrap().doc.guides[0].vertical, s.doc().unwrap().doc.guides[0].pos), (true, 120.0)); +} + /// #1534: Lock Guides locks the paths made into guides too: they can't be selected (so neither /// moved, reshaped nor deleted), by a click, a marquee or Select All, and locking lets them go. #[test] diff --git a/crates/tools/src/rulerguide.rs b/crates/tools/src/rulerguide.rs index 1f9bc8e47..3e2ad223b 100644 --- a/crates/tools/src/rulerguide.rs +++ b/crates/tools/src/rulerguide.rs @@ -83,6 +83,9 @@ fn guide_cursor(vertical: bool) -> Cursor { #[derive(Clone, Copy, Debug)] struct Drag { index: usize, + /// Orientation when the drag began (Alt swaps from this, as for a new guide). + source_vertical: bool, + /// Effective orientation while dragging (Alt swaps from `source_vertical`). vertical: bool, /// Where the guide was, and where it is now (x of a vertical guide, y of a horizontal one). from: f64, @@ -239,6 +242,7 @@ impl GuideEdit { let (shift, selected) = (ev.mods.shift, cx.selection.guides.contains(&index)); self.drag = Some(Drag { index, + source_vertical: g.vertical, vertical: g.vertical, from: g.pos, at: g.pos, @@ -267,7 +271,7 @@ impl GuideEdit { PointerKind::Up => { self.drag = None; self.snap = GuideSnap::default(); - if d.began && on_ruler(cx, d.vertical, ev.pos) { + if d.began && on_ruler(cx, d.source_vertical != ev.mods.alt, ev.pos) { // Dropped on its ruler: the guide goes (a copy is just not made). let mut out = vec![Action::Cancel]; if !ev.mods.alt { @@ -298,15 +302,24 @@ impl GuideEdit { return out; } d.began = true; - out.push(Action::Begin(if ev.mods.alt { "Copy Guide" } else { "Move Guide" }.into())); + out.push(Action::Begin("Move Guide".into())); self.snap = GuideSnap::new(cx); } + let vertical = d.source_vertical != ev.mods.alt; let moved = p - d.start; - let at = self.snap.snap(cx, d.vertical, d.from + if d.vertical { moved.x } else { moved.y }, ev.mods.shift); - (d.at, d.pointer) = (at, p); - // The other selected guides follow the pointer: vertical ones across, horizontal ones down. - let (dx, dy) = if d.vertical { (at - d.from, moved.y) } else { (moved.x, at - d.from) }; - out.push(Action::Preview("guide.move".into(), json!({ "dx": dx, "dy": dy, "copy": ev.mods.alt }))); + let at = if vertical == d.source_vertical { + self.snap.snap(cx, d.source_vertical, d.from + if d.source_vertical { moved.x } else { moved.y }, ev.mods.shift) + } else { + self.snap.snap(cx, vertical, if vertical { p.x } else { p.y }, ev.mods.shift) + }; + (d.at, d.pointer, d.vertical) = (at, p, vertical); + if vertical != d.source_vertical { + out.push(Action::Preview("guide.move".into(), json!({ "index": d.index, "pos": at, "vertical": vertical }))); + } else { + // The other selected guides follow the pointer: vertical ones across, horizontal ones down. + let (dx, dy) = if d.source_vertical { (at - d.from, moved.y) } else { (moved.x, at - d.from) }; + out.push(Action::Preview("guide.move".into(), json!({ "dx": dx, "dy": dy }))); + } out } @@ -603,7 +616,7 @@ mod tests { } #[test] - fn shift_click_toggles_and_alt_drag_copies() { + fn shift_click_toggles_and_alt_drag_swaps_orientation() { let d = guides_doc(); let mut s = Selection::default(); s.set_guides([0, 1]); @@ -614,10 +627,11 @@ mod tests { assert_eq!(g.press(&c, &ev(PointerKind::Down, 50.0, 400.0, shift)), Some(vec![])); let deselect = Action::Exec("guide.select".into(), json!({"indexes": [1], "toggle": true})); assert_eq!(g.pointer(&c, &ev(PointerKind::Up, 50.0, 400.0, shift)), Some(vec![deselect])); + // Guide 1 is horizontal at y = 400; Alt-drag turns it vertical at the pointer's x. g.press(&c, &ev(PointerKind::Down, 50.0, 400.0, alt)); - let a = g.pointer(&c, &ev(PointerKind::Drag, 50.0, 420.0, alt)).unwrap(); - assert_eq!(a[0], Action::Begin("Copy Guide".into())); - assert_eq!(a[1], Action::Preview("guide.move".into(), json!({"dx": 0.0, "dy": 20.0, "copy": true}))); + let a = g.pointer(&c, &ev(PointerKind::Drag, 80.0, 420.0, alt)).unwrap(); + assert_eq!(a[0], Action::Begin("Move Guide".into())); + assert_eq!(a[1], Action::Preview("guide.move".into(), json!({"index": 1, "pos": 80.0, "vertical": true}))); } #[test] @@ -675,7 +689,11 @@ mod tests { }; if let Some(from) = from { assert_eq!(command, "guide.move"); - from + p[if vertical { "dx" } else { "dy" }].as_f64().unwrap() + if p.get("index").is_some() { + p["pos"].as_f64().unwrap() + } else { + from + p[if vertical { "dx" } else { "dy" }].as_f64().unwrap() + } } else { assert_eq!(command, "guide.add"); assert_eq!(p["vertical"], json!(vertical)); @@ -720,23 +738,20 @@ mod tests { let mut selection = Selection::default(); selection.set_guides([index]); let c = ToolContext { unit, zoom, smart_guides: false, snap_to_point: false, ..cx(&doc, &selection, &paint) }; - for copy in [false, true] { - let mut guide = GuideEdit::default(); - let (start, to, want) = if vertical { - (Point::new(from, 50.0), Point::new(pointer.x, 50.0), expected.x) - } else { - (Point::new(50.0, from), Point::new(50.0, pointer.y), expected.y) - }; - assert!(guide.press(&c, &ev(PointerKind::Down, start.x, start.y, Mods::default())).unwrap().is_empty()); - let mods = Mods { shift: true, alt: copy, ..Default::default() }; - let actions = guide.pointer(&c, &ev(PointerKind::Drag, to.x, to.y, mods)).unwrap(); - assert_eq!(actions.first(), Some(&Action::Begin(if copy { "Copy Guide" } else { "Move Guide" }.into()))); - let actual = guide_preview_position(&actions, vertical, Some(from)); - assert!((actual - want).abs() < 1e-8, "{unit:?} {origin:?} vertical={vertical} copy={copy} actual={actual} want={want}"); - assert!(matches!(actions.last(), Some(Action::Preview(_, p)) if p["copy"] == json!(copy))); - assert_eq!(guide.pointer(&c, &ev(PointerKind::Up, to.x, to.y, mods)), Some(vec![Action::Commit])); - assert!(!guide.busy()); - } + let mut guide = GuideEdit::default(); + let (start, to, want) = if vertical { + (Point::new(from, 50.0), Point::new(pointer.x, 50.0), expected.x) + } else { + (Point::new(50.0, from), Point::new(50.0, pointer.y), expected.y) + }; + assert!(guide.press(&c, &ev(PointerKind::Down, start.x, start.y, Mods::default())).unwrap().is_empty()); + let mods = Mods { shift: true, ..Default::default() }; + let actions = guide.pointer(&c, &ev(PointerKind::Drag, to.x, to.y, mods)).unwrap(); + assert_eq!(actions.first(), Some(&Action::Begin("Move Guide".into()))); + let actual = guide_preview_position(&actions, vertical, Some(from)); + assert!((actual - want).abs() < 1e-8, "{unit:?} {origin:?} vertical={vertical} actual={actual} want={want}"); + assert_eq!(guide.pointer(&c, &ev(PointerKind::Up, to.x, to.y, mods)), Some(vec![Action::Commit])); + assert!(!guide.busy()); } } } @@ -840,13 +855,25 @@ mod tests { "commands keep absolute coordinates" ); check(new.overlays(&c)); - for copy in [false, true] { - let mut moved = GuideEdit::default(); - let (start, to) = if vertical { ((from, 50.0), (222.0, 50.0)) } else { ((50.0, from), (50.0, 238.0)) }; - moved.press(&c, &ev(PointerKind::Down, start.0, start.1, Mods::default())).unwrap(); - moved.pointer(&c, &ev(PointerKind::Drag, to.0, to.1, Mods { alt: copy, ..Default::default() })).unwrap(); - check(moved.overlays(&c)); - } + let mut moved = GuideEdit::default(); + let (start, to) = if vertical { ((from, 50.0), (222.0, 50.0)) } else { ((50.0, from), (50.0, 238.0)) }; + moved.press(&c, &ev(PointerKind::Down, start.0, start.1, Mods::default())).unwrap(); + moved.pointer(&c, &ev(PointerKind::Drag, to.0, to.1, Mods::default())).unwrap(); + check(moved.overlays(&c)); + // Alt swaps orientation; the readout follows the effective axis. + let mut flipped = GuideEdit::default(); + flipped.press(&c, &ev(PointerKind::Down, start.0, start.1, Mods::default())).unwrap(); + flipped.pointer(&c, &ev(PointerKind::Drag, to.0, to.1, Mods { alt: true, ..Default::default() })).unwrap(); + let flipped_vertical = !vertical; + let want_flip = format!( + "{}: {}", + if flipped_vertical { "X" } else { "Y" }, + c.len(if flipped_vertical { to.0 } else { to.1 }) + ); + assert!( + matches!(flipped.overlays(&c).last(), Some(Overlay::Measure { text, .. }) if text == &want_flip), + "want {want_flip}" + ); let quiet = ToolContext { measurement_labels: false, ..c }; assert!(new.overlays(&quiet).is_empty(), "measurement-label preference is preserved"); } From 0d09d6a18826af3574fee275c400d9bf3e37d6c6 Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Mon, 19 Oct 2026 01:46:47 +0300 Subject: [PATCH 2/4] Docs: mark existing ruler-guide Alt orientation as done in ui-parity Co-authored-by: Cursor --- docs/ui-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ui-parity.md b/docs/ui-parity.md index e4eee210b..c16ed6fe4 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -115,7 +115,7 @@ tools, never checked side by side) isn't captured row by row. | Ruler guides as Layers rows: shown, hidden, locked and deleted with their layer; rows dragged to another layer | done | `guide.setLayer`; guides from older files go on a layer | | Objects made into guides: drawn in the guide colour and style of Preferences; Lock Guides locks them too (no select, move or delete); a dragged selection snaps to ruler guides with Smart Guides on | done | #1534 (#1543, #1583) | | A selected ruler guide's X or Y in the Transform panel; typing a value moves it | not scored | #1659 (part of #1597); several guides move together; not in Properties or the Control bar. Not established by the documentation we could find: the help pages on guides, rulers and the Transform, Control and Properties panels (read again on 2026-10-14) don't describe it, and the one public statement is a user's [community answer](https://community.adobe.com/questions-652/adding-a-new-custom-guide-785586), which is not documentation. Kept as a convenience pending the owner's decision; no parity claim | -| Alt swaps a ruler guide's orientation while dragging | missing | | +| Alt swaps a ruler guide's orientation while dragging | done | Alt-drag converts horizontal ↔ vertical with snapping on the new axis; programmatic `guide.move` copy unchanged | | Alt swaps a new ruler guide's orientation while dragging | done | Both rulers; preview, snapping and final placement follow the current modifier; existing-guide Alt-copy is unchanged. | | Global/artboard ruler mode switch (View › Rulers › Change to Global / Artboard Rulers, Cmd+Alt+R) | done | `view.globalRulers`; a new document has artboard rulers, counting from the active artboard's top-left corner or the zero point set for it (kept relative to the artboard); global rulers keep one document zero point. Rulers, Transform, Properties, Control and Info X/Y, guide readouts and Shift ruler-tick snapping follow the mode; saved with the document, older files open with global rulers. Behaviour from [About rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/about-rulers.html) and [Use rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html) | | Video rulers | missing | menu stub | From 32a322531a4584f8a4fcbaf825d16d11c74e4617 Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Mon, 19 Oct 2026 07:51:20 +0300 Subject: [PATCH 3/4] G1: fix guide.move preview copy flag and ruler-drop Alt handling. Align ruler-drop detection with source orientation and alt-flip readout tests with relative origins. Co-authored-by: Cursor --- crates/tools/src/rulerguide.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/tools/src/rulerguide.rs b/crates/tools/src/rulerguide.rs index 3e2ad223b..bb1b54a42 100644 --- a/crates/tools/src/rulerguide.rs +++ b/crates/tools/src/rulerguide.rs @@ -271,7 +271,7 @@ impl GuideEdit { PointerKind::Up => { self.drag = None; self.snap = GuideSnap::default(); - if d.began && on_ruler(cx, d.source_vertical != ev.mods.alt, ev.pos) { + if d.began && on_ruler(cx, d.source_vertical, ev.pos) { // Dropped on its ruler: the guide goes (a copy is just not made). let mut out = vec![Action::Cancel]; if !ev.mods.alt { @@ -318,7 +318,7 @@ impl GuideEdit { } else { // The other selected guides follow the pointer: vertical ones across, horizontal ones down. let (dx, dy) = if d.source_vertical { (at - d.from, moved.y) } else { (moved.x, at - d.from) }; - out.push(Action::Preview("guide.move".into(), json!({ "dx": dx, "dy": dy }))); + out.push(Action::Preview("guide.move".into(), json!({ "dx": dx, "dy": dy, "copy": false }))); } out } @@ -865,10 +865,11 @@ mod tests { flipped.press(&c, &ev(PointerKind::Down, start.0, start.1, Mods::default())).unwrap(); flipped.pointer(&c, &ev(PointerKind::Drag, to.0, to.1, Mods { alt: true, ..Default::default() })).unwrap(); let flipped_vertical = !vertical; + let origin = c.ruler_origin; let want_flip = format!( "{}: {}", if flipped_vertical { "X" } else { "Y" }, - c.len(if flipped_vertical { to.0 } else { to.1 }) + c.len(if flipped_vertical { to.0 - origin.x } else { to.1 - origin.y }) ); assert!( matches!(flipped.overlays(&c).last(), Some(Overlay::Measure { text, .. }) if text == &want_flip), From 2ceb183bfee75452e0b034de49ce16e56129ba48 Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Mon, 19 Oct 2026 23:17:28 +0300 Subject: [PATCH 4/4] G1: restore Alt-drag copy for existing ruler guides (#2997). Drop the undocumented orientation-swap on moved guides; Alt again previews guide.move with copy, matching new-guide Alt orientation only. Co-authored-by: Cursor --- crates/engine/src/cmd/docmenu.rs | 5 +- crates/engine/src/tests_menucmds.rs | 8 --- crates/tools/src/rulerguide.rs | 100 ++++++++++------------------ docs/ui-parity.md | 4 +- 4 files changed, 39 insertions(+), 78 deletions(-) diff --git a/crates/engine/src/cmd/docmenu.rs b/crates/engine/src/cmd/docmenu.rs index 4ad95af58..2cbd4a3b9 100644 --- a/crates/engine/src/cmd/docmenu.rs +++ b/crates/engine/src/cmd/docmenu.rs @@ -207,7 +207,7 @@ pub fn specs() -> Vec { "Move Guide", [], None, - "{index, pos: pt, vertical?: bool} | {dx?, dy?: pt, copy?: bool} put ruler guide `index` at `pos` (and swap its orientation when `vertical` is given), or move the selected guides (vertical ones by dx, horizontal ones by dy); copy leaves them and selects the moved copies", + "{index, pos: pt} | {dx?, dy?: pt, copy?: bool} put ruler guide `index` at `pos`, or move the selected guides (vertical ones by dx, horizontal ones by dy); copy leaves them and selects the moved copies", guides_unlocked, guide_move ), @@ -769,9 +769,6 @@ pub(crate) fn guide_move(s: &mut Session, p: &Value) -> Result { s.edit("Move Guide", |d, _| { if let Some(g) = d.guides.get_mut(i) { g.pos = pos; - if p.get("vertical").is_some() { - g.vertical = bool_or(p, "vertical", false); - } } Ok(()) })?; diff --git a/crates/engine/src/tests_menucmds.rs b/crates/engine/src/tests_menucmds.rs index 5578610e1..8b93e5532 100644 --- a/crates/engine/src/tests_menucmds.rs +++ b/crates/engine/src/tests_menucmds.rs @@ -2011,14 +2011,6 @@ fn ruler_guides_and_lock() { assert!(s.execute("guide.remove", &json!({"index": 0})).is_err()); } -#[test] -fn guide_move_can_swap_orientation() { - let mut s = session(); - s.execute("guide.add", &json!({"vertical": false, "pos": 50})).unwrap(); - s.execute("guide.move", &json!({"index": 0, "pos": 120, "vertical": true})).unwrap(); - assert_eq!((s.doc().unwrap().doc.guides[0].vertical, s.doc().unwrap().doc.guides[0].pos), (true, 120.0)); -} - /// #1534: Lock Guides locks the paths made into guides too: they can't be selected (so neither /// moved, reshaped nor deleted), by a click, a marquee or Select All, and locking lets them go. #[test] diff --git a/crates/tools/src/rulerguide.rs b/crates/tools/src/rulerguide.rs index bb1b54a42..1f9bc8e47 100644 --- a/crates/tools/src/rulerguide.rs +++ b/crates/tools/src/rulerguide.rs @@ -83,9 +83,6 @@ fn guide_cursor(vertical: bool) -> Cursor { #[derive(Clone, Copy, Debug)] struct Drag { index: usize, - /// Orientation when the drag began (Alt swaps from this, as for a new guide). - source_vertical: bool, - /// Effective orientation while dragging (Alt swaps from `source_vertical`). vertical: bool, /// Where the guide was, and where it is now (x of a vertical guide, y of a horizontal one). from: f64, @@ -242,7 +239,6 @@ impl GuideEdit { let (shift, selected) = (ev.mods.shift, cx.selection.guides.contains(&index)); self.drag = Some(Drag { index, - source_vertical: g.vertical, vertical: g.vertical, from: g.pos, at: g.pos, @@ -271,7 +267,7 @@ impl GuideEdit { PointerKind::Up => { self.drag = None; self.snap = GuideSnap::default(); - if d.began && on_ruler(cx, d.source_vertical, ev.pos) { + if d.began && on_ruler(cx, d.vertical, ev.pos) { // Dropped on its ruler: the guide goes (a copy is just not made). let mut out = vec![Action::Cancel]; if !ev.mods.alt { @@ -302,24 +298,15 @@ impl GuideEdit { return out; } d.began = true; - out.push(Action::Begin("Move Guide".into())); + out.push(Action::Begin(if ev.mods.alt { "Copy Guide" } else { "Move Guide" }.into())); self.snap = GuideSnap::new(cx); } - let vertical = d.source_vertical != ev.mods.alt; let moved = p - d.start; - let at = if vertical == d.source_vertical { - self.snap.snap(cx, d.source_vertical, d.from + if d.source_vertical { moved.x } else { moved.y }, ev.mods.shift) - } else { - self.snap.snap(cx, vertical, if vertical { p.x } else { p.y }, ev.mods.shift) - }; - (d.at, d.pointer, d.vertical) = (at, p, vertical); - if vertical != d.source_vertical { - out.push(Action::Preview("guide.move".into(), json!({ "index": d.index, "pos": at, "vertical": vertical }))); - } else { - // The other selected guides follow the pointer: vertical ones across, horizontal ones down. - let (dx, dy) = if d.source_vertical { (at - d.from, moved.y) } else { (moved.x, at - d.from) }; - out.push(Action::Preview("guide.move".into(), json!({ "dx": dx, "dy": dy, "copy": false }))); - } + let at = self.snap.snap(cx, d.vertical, d.from + if d.vertical { moved.x } else { moved.y }, ev.mods.shift); + (d.at, d.pointer) = (at, p); + // The other selected guides follow the pointer: vertical ones across, horizontal ones down. + let (dx, dy) = if d.vertical { (at - d.from, moved.y) } else { (moved.x, at - d.from) }; + out.push(Action::Preview("guide.move".into(), json!({ "dx": dx, "dy": dy, "copy": ev.mods.alt }))); out } @@ -616,7 +603,7 @@ mod tests { } #[test] - fn shift_click_toggles_and_alt_drag_swaps_orientation() { + fn shift_click_toggles_and_alt_drag_copies() { let d = guides_doc(); let mut s = Selection::default(); s.set_guides([0, 1]); @@ -627,11 +614,10 @@ mod tests { assert_eq!(g.press(&c, &ev(PointerKind::Down, 50.0, 400.0, shift)), Some(vec![])); let deselect = Action::Exec("guide.select".into(), json!({"indexes": [1], "toggle": true})); assert_eq!(g.pointer(&c, &ev(PointerKind::Up, 50.0, 400.0, shift)), Some(vec![deselect])); - // Guide 1 is horizontal at y = 400; Alt-drag turns it vertical at the pointer's x. g.press(&c, &ev(PointerKind::Down, 50.0, 400.0, alt)); - let a = g.pointer(&c, &ev(PointerKind::Drag, 80.0, 420.0, alt)).unwrap(); - assert_eq!(a[0], Action::Begin("Move Guide".into())); - assert_eq!(a[1], Action::Preview("guide.move".into(), json!({"index": 1, "pos": 80.0, "vertical": true}))); + let a = g.pointer(&c, &ev(PointerKind::Drag, 50.0, 420.0, alt)).unwrap(); + assert_eq!(a[0], Action::Begin("Copy Guide".into())); + assert_eq!(a[1], Action::Preview("guide.move".into(), json!({"dx": 0.0, "dy": 20.0, "copy": true}))); } #[test] @@ -689,11 +675,7 @@ mod tests { }; if let Some(from) = from { assert_eq!(command, "guide.move"); - if p.get("index").is_some() { - p["pos"].as_f64().unwrap() - } else { - from + p[if vertical { "dx" } else { "dy" }].as_f64().unwrap() - } + from + p[if vertical { "dx" } else { "dy" }].as_f64().unwrap() } else { assert_eq!(command, "guide.add"); assert_eq!(p["vertical"], json!(vertical)); @@ -738,20 +720,23 @@ mod tests { let mut selection = Selection::default(); selection.set_guides([index]); let c = ToolContext { unit, zoom, smart_guides: false, snap_to_point: false, ..cx(&doc, &selection, &paint) }; - let mut guide = GuideEdit::default(); - let (start, to, want) = if vertical { - (Point::new(from, 50.0), Point::new(pointer.x, 50.0), expected.x) - } else { - (Point::new(50.0, from), Point::new(50.0, pointer.y), expected.y) - }; - assert!(guide.press(&c, &ev(PointerKind::Down, start.x, start.y, Mods::default())).unwrap().is_empty()); - let mods = Mods { shift: true, ..Default::default() }; - let actions = guide.pointer(&c, &ev(PointerKind::Drag, to.x, to.y, mods)).unwrap(); - assert_eq!(actions.first(), Some(&Action::Begin("Move Guide".into()))); - let actual = guide_preview_position(&actions, vertical, Some(from)); - assert!((actual - want).abs() < 1e-8, "{unit:?} {origin:?} vertical={vertical} actual={actual} want={want}"); - assert_eq!(guide.pointer(&c, &ev(PointerKind::Up, to.x, to.y, mods)), Some(vec![Action::Commit])); - assert!(!guide.busy()); + for copy in [false, true] { + let mut guide = GuideEdit::default(); + let (start, to, want) = if vertical { + (Point::new(from, 50.0), Point::new(pointer.x, 50.0), expected.x) + } else { + (Point::new(50.0, from), Point::new(50.0, pointer.y), expected.y) + }; + assert!(guide.press(&c, &ev(PointerKind::Down, start.x, start.y, Mods::default())).unwrap().is_empty()); + let mods = Mods { shift: true, alt: copy, ..Default::default() }; + let actions = guide.pointer(&c, &ev(PointerKind::Drag, to.x, to.y, mods)).unwrap(); + assert_eq!(actions.first(), Some(&Action::Begin(if copy { "Copy Guide" } else { "Move Guide" }.into()))); + let actual = guide_preview_position(&actions, vertical, Some(from)); + assert!((actual - want).abs() < 1e-8, "{unit:?} {origin:?} vertical={vertical} copy={copy} actual={actual} want={want}"); + assert!(matches!(actions.last(), Some(Action::Preview(_, p)) if p["copy"] == json!(copy))); + assert_eq!(guide.pointer(&c, &ev(PointerKind::Up, to.x, to.y, mods)), Some(vec![Action::Commit])); + assert!(!guide.busy()); + } } } } @@ -855,26 +840,13 @@ mod tests { "commands keep absolute coordinates" ); check(new.overlays(&c)); - let mut moved = GuideEdit::default(); - let (start, to) = if vertical { ((from, 50.0), (222.0, 50.0)) } else { ((50.0, from), (50.0, 238.0)) }; - moved.press(&c, &ev(PointerKind::Down, start.0, start.1, Mods::default())).unwrap(); - moved.pointer(&c, &ev(PointerKind::Drag, to.0, to.1, Mods::default())).unwrap(); - check(moved.overlays(&c)); - // Alt swaps orientation; the readout follows the effective axis. - let mut flipped = GuideEdit::default(); - flipped.press(&c, &ev(PointerKind::Down, start.0, start.1, Mods::default())).unwrap(); - flipped.pointer(&c, &ev(PointerKind::Drag, to.0, to.1, Mods { alt: true, ..Default::default() })).unwrap(); - let flipped_vertical = !vertical; - let origin = c.ruler_origin; - let want_flip = format!( - "{}: {}", - if flipped_vertical { "X" } else { "Y" }, - c.len(if flipped_vertical { to.0 - origin.x } else { to.1 - origin.y }) - ); - assert!( - matches!(flipped.overlays(&c).last(), Some(Overlay::Measure { text, .. }) if text == &want_flip), - "want {want_flip}" - ); + for copy in [false, true] { + let mut moved = GuideEdit::default(); + let (start, to) = if vertical { ((from, 50.0), (222.0, 50.0)) } else { ((50.0, from), (50.0, 238.0)) }; + moved.press(&c, &ev(PointerKind::Down, start.0, start.1, Mods::default())).unwrap(); + moved.pointer(&c, &ev(PointerKind::Drag, to.0, to.1, Mods { alt: copy, ..Default::default() })).unwrap(); + check(moved.overlays(&c)); + } let quiet = ToolContext { measurement_labels: false, ..c }; assert!(new.overlays(&quiet).is_empty(), "measurement-label preference is preserved"); } diff --git a/docs/ui-parity.md b/docs/ui-parity.md index c16ed6fe4..095db1413 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -115,8 +115,8 @@ tools, never checked side by side) isn't captured row by row. | Ruler guides as Layers rows: shown, hidden, locked and deleted with their layer; rows dragged to another layer | done | `guide.setLayer`; guides from older files go on a layer | | Objects made into guides: drawn in the guide colour and style of Preferences; Lock Guides locks them too (no select, move or delete); a dragged selection snaps to ruler guides with Smart Guides on | done | #1534 (#1543, #1583) | | A selected ruler guide's X or Y in the Transform panel; typing a value moves it | not scored | #1659 (part of #1597); several guides move together; not in Properties or the Control bar. Not established by the documentation we could find: the help pages on guides, rulers and the Transform, Control and Properties panels (read again on 2026-10-14) don't describe it, and the one public statement is a user's [community answer](https://community.adobe.com/questions-652/adding-a-new-custom-guide-785586), which is not documentation. Kept as a convenience pending the owner's decision; no parity claim | -| Alt swaps a ruler guide's orientation while dragging | done | Alt-drag converts horizontal ↔ vertical with snapping on the new axis; programmatic `guide.move` copy unchanged | -| Alt swaps a new ruler guide's orientation while dragging | done | Both rulers; preview, snapping and final placement follow the current modifier; existing-guide Alt-copy is unchanged. | +| Alt swaps a ruler guide's orientation while dragging | missing | Alt-drag copies the selected guides ([Use rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html)); `guide.move` `{copy: true}` | +| Alt swaps a new ruler guide's orientation while dragging | done | Both rulers; preview, snapping and final placement follow the current modifier ([public shortcut reference](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). | | Global/artboard ruler mode switch (View › Rulers › Change to Global / Artboard Rulers, Cmd+Alt+R) | done | `view.globalRulers`; a new document has artboard rulers, counting from the active artboard's top-left corner or the zero point set for it (kept relative to the artboard); global rulers keep one document zero point. Rulers, Transform, Properties, Control and Info X/Y, guide readouts and Shift ruler-tick snapping follow the mode; saved with the document, older files open with global rulers. Behaviour from [About rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/about-rulers.html) and [Use rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html) | | Video rulers | missing | menu stub |