mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 01:54:15 +00:00
Fix #5239: a ruler double-click needs both clicks on that ruler and follows a rotated view (review)
This commit is contained in:
@@ -178,9 +178,10 @@ pub(super) fn ruler_rects(full: egui::Rect) -> [egui::Rect; 3] {
|
||||
/// A drag from a ruler onto the canvas makes a guide where the button is released (the engine's
|
||||
/// `Session::ruler_guide`): a horizontal one from the top ruler, a vertical one from the left
|
||||
/// ruler, snapped as a moved guide is (with Shift to the ruler's ticks). Released anywhere else,
|
||||
/// it makes none. A double-click on a ruler makes a guide at the pointer, across the ruler's
|
||||
/// axis (a vertical one on the top ruler, a horizontal one on the left), unsnapped, as one
|
||||
/// `guide.add` and so one undo step. The corner drags the rulers' origin or resets it on a
|
||||
/// it makes none. A double-click on a ruler (both clicks on that ruler) makes a guide at the
|
||||
/// pointer, across the ruler's axis (a vertical one on the top ruler, a horizontal one on the
|
||||
/// left; swapped in a view turned 90°, where the rulers read the other document axis, and the
|
||||
/// nearer axis in between), unsnapped, as one `guide.add` and so one undo step. The corner drags the rulers' origin or resets it on a
|
||||
/// double-click.
|
||||
pub(super) fn ruler_input(app: &mut VectorcraftApp, ui: &Ui, full: egui::Rect, canvas: egui::Rect, xf: &Xf) -> Option<Point> {
|
||||
let [top, left, corner] = ruler_rects(full);
|
||||
@@ -192,13 +193,31 @@ pub(super) fn ruler_input(app: &mut VectorcraftApp, ui: &Ui, full: egui::Rect, c
|
||||
// (right). Not focusable (`click_and_drag` is): no key acts on a ruler, so Tab skips it.
|
||||
let resp = ui.interact(r, egui::Id::unique(id), Sense::CLICK | Sense::DRAG);
|
||||
resp.context_menu(|ui| crate::menus::ruler_menu_body(app, ui, &mut unit_clicked));
|
||||
// egui counts a double-click by position and time alone, so a click on the canvas right
|
||||
// at the ruler and one on the ruler would pass for one: the pair counts only when the
|
||||
// ruler's own click before it came within the double-click delay.
|
||||
let last_click = egui::Id::unique(if vertical { "ruler-left-last-click" } else { "ruler-top-last-click" });
|
||||
let now = ui.input(|i| i.time);
|
||||
let first_here =
|
||||
ui.data(|d| d.get_temp::<f64>(last_click)).is_some_and(|t| now - t <= ui.ctx().options(|o| o.input_options.max_double_click_delay));
|
||||
if resp.clicked_by(egui::PointerButton::Primary) {
|
||||
ui.data_mut(|d| d.insert_temp(last_click, now));
|
||||
}
|
||||
if resp.double_clicked_by(egui::PointerButton::Primary)
|
||||
&& first_here
|
||||
&& let Some(p) = resp.interact_pointer_pos().or_else(|| ui.input(|i| i.pointer.latest_pos()))
|
||||
{
|
||||
// The same document point the drag path reads; the guide runs across the other axis
|
||||
// than the ruler, so it takes the pointer's coordinate along the ruler.
|
||||
// The same document point the drag path reads. A ruler reads the canvas's coordinates
|
||||
// turned with the view, so the guide is the one whose line crosses the ruler's axis:
|
||||
// the vertical one when that axis (the screen's x for the top ruler, y for the left,
|
||||
// in document directions) is nearer the document's x, else the horizontal one. At 0°
|
||||
// that is a vertical guide from the top ruler; at 90° a horizontal one; between, the
|
||||
// nearer axis, whose line still runs through the point and never along the ruler.
|
||||
let at = xf.to_doc(p);
|
||||
let mut params = json!({"vertical": !vertical, "pos": if vertical { at.y } else { at.x }});
|
||||
let (sn, cs) = xf.rot.sin_cos();
|
||||
let axis = if vertical { (sn, cs) } else { (cs, -sn) };
|
||||
let across = axis.0.abs() >= axis.1.abs();
|
||||
let mut params = json!({"vertical": across, "pos": if across { at.x } else { at.y }});
|
||||
if let Some(ab) = app.session.ruler_guide_artboard() {
|
||||
params["artboard"] = json!(ab);
|
||||
}
|
||||
|
||||
@@ -8910,3 +8910,97 @@ fn double_clicking_a_ruler_adds_a_guide_at_that_position() {
|
||||
assert_eq!(guides(&mut app), vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A fresh app with a 400 x 300 document, rulers on, the view at zoom 2 turned `rotation` degrees,
|
||||
/// and its first frame drawn.
|
||||
fn ruler_guide_app(rotation: f64) -> (VectorcraftApp, egui::Context) {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
app.ui.view.rulers = true;
|
||||
app.ui.view.guides = false;
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let view = app.view_mut().unwrap();
|
||||
view.zoom = 2.0;
|
||||
view.rotation = rotation;
|
||||
view.center = Point::new(200.0, 150.0);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
(app, ctx)
|
||||
}
|
||||
|
||||
/// One primary click at `at`, as three frames (move, press, release).
|
||||
fn click_at(app: &mut VectorcraftApp, ctx: &egui::Context, at: Pos2) {
|
||||
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)]);
|
||||
}
|
||||
|
||||
/// The ruler guides as (vertical, position).
|
||||
fn guide_list(app: &mut VectorcraftApp) -> Vec<(bool, f64)> {
|
||||
let list = app.session.execute("guide.list", &json!({})).unwrap();
|
||||
list.as_array().unwrap().iter().map(|g| (g["vertical"].as_bool().unwrap(), g["pos"].as_f64().unwrap())).collect()
|
||||
}
|
||||
|
||||
/// #5239 review: egui counts a double-click by position and time, not by widget, so a click on
|
||||
/// the canvas right at a ruler followed at once by a click on the ruler is not a double-click on
|
||||
/// the ruler. No guide; two clicks on the ruler still make one.
|
||||
#[test]
|
||||
fn a_canvas_click_then_a_ruler_click_adds_no_guide() {
|
||||
for (top, canvas_first) in [(true, true), (false, true), (true, false), (false, false)] {
|
||||
let (mut app, ctx) = ruler_guide_app(0.0);
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
// Two points less than 6 pt apart, either side of the ruler's inner edge.
|
||||
let (on_canvas, on_ruler) = if top {
|
||||
(pos2(rect.center().x, rect.top() + 1.0), pos2(rect.center().x, rect.top() - 1.0))
|
||||
} else {
|
||||
(pos2(rect.left() + 1.0, rect.center().y), pos2(rect.left() - 1.0, rect.center().y))
|
||||
};
|
||||
click_at(&mut app, &ctx, if canvas_first { on_canvas } else { on_ruler });
|
||||
click_at(&mut app, &ctx, on_ruler);
|
||||
let want = usize::from(!canvas_first);
|
||||
assert_eq!(guide_list(&mut app).len(), want, "top ruler: {top}, first click on the canvas: {canvas_first}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #5239 review: the rulers read the canvas's coordinates turned with the view, so a guide made
|
||||
/// by a double-click runs across the ruler's axis in any view: it passes through the clicked
|
||||
/// point and crosses the ruler's edge at it, never along it. At 90° the top ruler reads the
|
||||
/// document's y, so its guide is horizontal; the left ruler reads x, so its guide is vertical.
|
||||
/// A view turned part of the way takes the document axis nearer to the ruler's.
|
||||
#[test]
|
||||
fn a_ruler_double_click_follows_a_rotated_view() {
|
||||
// Rotation (degrees), top ruler's guide is vertical, left ruler's guide is vertical.
|
||||
for (rotation, top_vertical, left_vertical) in [
|
||||
(0.0, true, false),
|
||||
(90.0, false, true),
|
||||
(-90.0, false, true),
|
||||
(180.0, true, false),
|
||||
(270.0, false, true),
|
||||
(30.0, true, false),
|
||||
(60.0, false, true),
|
||||
] {
|
||||
for top in [true, false] {
|
||||
let (mut app, ctx) = ruler_guide_app(rotation);
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
let xf = Xf::new(rect, app.view().unwrap());
|
||||
let at =
|
||||
if top { pos2(rect.center().x + 37.0, rect.top() - RULER / 2.0) } else { pos2(rect.left() - RULER / 2.0, rect.center().y + 23.0) };
|
||||
click_at(&mut app, &ctx, at);
|
||||
click_at(&mut app, &ctx, at);
|
||||
let got = guide_list(&mut app);
|
||||
let label = format!("rotation {rotation}, top ruler: {top}: {got:?}");
|
||||
assert_eq!(got.len(), 1, "{label}");
|
||||
let (vertical, pos) = got[0];
|
||||
assert_eq!(vertical, if top { top_vertical } else { left_vertical }, "{label}");
|
||||
// Through the clicked point.
|
||||
let doc = xf.to_doc(at);
|
||||
assert!((pos - if vertical { doc.x } else { doc.y }).abs() < 0.01, "{label}: through {doc:?}");
|
||||
// Its direction on screen is not along the ruler: the line crosses the ruler's edge.
|
||||
let along = if vertical { Point::new(doc.x, doc.y + 1.0) } else { Point::new(doc.x + 1.0, doc.y) };
|
||||
let dir = (xf.to_screen(along) - xf.to_screen(doc)).normalized();
|
||||
let across = if top { dir.y.abs() } else { dir.x.abs() };
|
||||
assert!(across > 0.5, "{label}: the guide runs along the ruler ({dir:?})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -598,7 +598,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
- **Done:** Selection artwork drag across visible document tabs (400 ms hover); lossless target copy, source/clipboard preservation and cancellation regressions. Cross-window dragging remains separate.
|
||||
|
||||
- **Done (#5239):** a double-click on a ruler adds a guide at the pointer (vertical on the top ruler, horizontal on the left one) through `guide.add`, unsnapped and in one undo step; no scored gap or estimate reassessed.
|
||||
- **Done (#5239):** a double-click on a ruler adds a guide at the pointer (vertical on the top ruler, horizontal on the left one, swapped in a view turned 90°; both clicks on that ruler) through `guide.add`, unsnapped and in one undo step; no scored gap or estimate reassessed.
|
||||
- **Done:** custom document ruler origin (#1212): corner drag with preview, double-click reset to the active artboard, relative X/Y inputs and native save/reopen.
|
||||
- **Done:** View › Rulers › Change to Global / Artboard Rulers (`view.globalRulers`, Cmd+Alt+R): artboard rulers, a new document's, count from the active artboard, each artboard with its own zero point; rulers, X/Y fields and guide readouts follow the mode, which is saved with the document. Estimate unchanged.
|
||||
- **Done (#2849):** View › Rulers › Show Video Rulers (`view.videoRulers`): the rulers show and count the active artboard in video pixels from its top-left corner, a pixel as many points wide as the artboard's Video Ruler Pixel Aspect Ratio and one point tall ([public source](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/set-video-display-options-for-artboards.html)); Hide Video Rulers returns them to the General unit and the ruler origin. Shift-snapping a new guide to ruler ticks still uses the General unit's ticks. Estimate unchanged.
|
||||
|
||||
@@ -532,7 +532,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Width-point creation (G1):** double-click a stroked segment with the Width tool to open Width Point Edit using sampled side widths. OK creates the point and optionally adjusts adjoining widths in one undo step. Cancel or Delete discards a prospective point without changing the document or redo history. Command and pointer tests cover profiles, compound paths, adjoining adjustment, existing-point editing and Undo/Redo. Message-catalog checks and full CI passed; the original capture verifies opening and Cancel, and the dialog layout was reviewed (2026-10-13: 28 focused Width tests, all seven cargo xtask ci stages, and a headless opening/Cancel capture with original PNG review). [Variable-width strokes](https://helpx.adobe.com/illustrator/using/stroke-object.html).
|
||||
- **Escape deselects width points:** with the Width tool, Escape deselects the selected width points, on the key the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists for deselecting a width point. The path, its widths and the undo history do not change. With no width point selected, Escape does what it does with the other tools, such as leaving isolation mode.
|
||||
- **Width strokes zoomed out (#995):** a stroke with a width profile keeps its shape while the view zooms out. The canvas keeps a hairline visible (at least half a device pixel); for a profiled stroke it is the profile's widest point that gets that minimum, not the weight, so a 1 pt stroke with a 6x width point is no longer drawn twice as thick at 25% zoom than at 100%. Plain strokes are unchanged. Test: `render::tests::widthzoom`.
|
||||
- **Ruler double-click (#5239):** a double-click on the top ruler adds a vertical guide at the pointer's x and one on the left ruler a horizontal guide at its y, through `guide.add` in one undo step; the guide is not snapped, is shown even when guides were hidden, and is an artboard guide with the Artboard tool, as a drag from a ruler makes one. The [public page on rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html) doesn't describe the gesture, so it follows the request in #5239. An egui frame test covers both rulers, a lone click, and Undo.
|
||||
- **Ruler double-click (#5239):** a double-click (both clicks on that ruler) on the top ruler adds a vertical guide at the pointer's x and one on the left ruler a horizontal guide at its y (swapped in a view turned 90°, where the rulers read the other document axis; the nearer axis in between), through `guide.add` in one undo step; the guide is not snapped, is shown even when guides were hidden, and is an artboard guide with the Artboard tool, as a drag from a ruler makes one. The [public page on rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html) doesn't describe the gesture, so it follows the request in #5239. egui frame tests cover both rulers, a lone click, Undo, a canvas click before a ruler click, and rotated views.
|
||||
- **Rulers (#1212):** drag the intersection to preview and set a document zero point, or double-click it to reset to the active artboard's top-left. Transform, Properties and Control X/Y fields measure from it and convert entered positions back to absolute command coordinates. The origin survives every native format version and compression; geometry, guides and patterns stay in place. Public reference: [Use rulers](https://helpx.adobe.com/ca/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html).
|
||||
- **Global and artboard rulers:** View › Rulers › Change to Global / Artboard Rulers (Cmd+Alt+R, `view.globalRulers`). A new document has artboard rulers: the rulers and the Transform, Properties, Control and Info X/Y count from the active artboard's top-left corner, or the zero point dragged out for that artboard, which moves with it; with global rulers one document zero point serves every artboard. Guide readouts and Shift ruler-tick snapping follow the same zero point. The mode is saved with the document; files from before it open with global rulers, as they measured. Regression tests: `engine/tests/it/ruler_origin.rs`, `ui-egui/src/tests/ruler_origin.rs`, `tools/src/rulerguide.rs`. Public reference: [About rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/about-rulers.html).
|
||||
- **Video display options (#2849):** Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio, and View › Rulers › Show Video Rulers ([Set video display options for artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/set-video-display-options-for-artboards.html)). Each artboard has its own (`artboard.setProps`/`artboard.new` `showVideoSafeAreas`, `videoRulerPixelAspectRatio` 0.1–10, default 1), set in one undo step and written to the native format only when on or not 1, so other documents save byte for byte as before. The help page names the safe areas without numbers; our choice is [EBU R 95](https://tech.ebu.ch/docs/r/r095.pdf)'s (note 5): the canvas outlines the action-safe area 3.5% and the title-safe (graphics-safe) area 5% in from each edge, 1 point wide and turned with the view; exports and print leave them out. Show Video Rulers (`view.videoRulers`) shows the rulers counting the active artboard in pixels from its top-left corner, whatever the ruler origin: a pixel is as many points wide as the ratio and one point tall, so a 720 × 480 pt artboard at 0.9 reads 800 × 480. Hide Video Rulers returns them to the General unit; Shift ruler-tick snapping of new guides keeps the General unit. Regression tests: `tests/panelcmds.rs` (`artboard_video_display_options_*`, `artboard_display_marks_stay_out_of_exports`), `tests/docsetup.rs` (`an_artboard_shows_its_video_safe_areas`), `canvas.rs` (`video_rulers_count_the_artboard_in_pixels_of_its_aspect_ratio`), `tests/ruler_origin.rs` (`show_video_rulers_counts_the_active_artboard_in_pixels`), `dialogs/tests.rs` (`artboard_options_show_video_display_options`).
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Snapping panel/popover consolidating the snap options (30.0) | partial | Control bar › Snapping provides quick access through existing view commands and prefs.set; no View › Snapping submenu. Floating-panel parity remains unverified. |
|
||||
| Customisable rotation snap angle (#973) | missing | Illustrator: Constrain Angle and construction angles only |
|
||||
| Ruler guides: drag out, select, move, delete, lock, Make/Release Guides | done | #414, #451; marquee selects unlocked ruler guides |
|
||||
| A double-click on a ruler adds a guide at the pointer: a vertical one on the top ruler, a horizontal one on the left ruler, exactly where the pointer is (no snapping), shown even with guides hidden, one undo step (`guide.add`); the first click of the pair adds nothing. The [public page on rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html) doesn't describe this gesture, so it follows the request in #5239 | done | #5239; an artboard guide with the Artboard tool, as a drag from a ruler makes one; `canvas/tests.rs` `double_clicking_a_ruler_adds_a_guide_at_that_position` |
|
||||
| A double-click on a ruler adds a guide at the pointer: a vertical one on the top ruler, a horizontal one on the left ruler (swapped in a view turned 90°, where the rulers read the other document axis; the nearer axis in between), exactly where the pointer is (no snapping), shown even with guides hidden, one undo step (`guide.add`); both clicks of the pair must be on that ruler, and the first adds nothing. The [public page on rulers](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html) doesn't describe this gesture, so it follows the request in #5239 | done | #5239; an artboard guide with the Artboard tool, as a drag from a ruler makes one; `canvas/tests.rs` `double_clicking_a_ruler_adds_a_guide_at_that_position`, `a_canvas_click_then_a_ruler_click_adds_no_guide`, `a_ruler_double_click_follows_a_rotated_view` |
|
||||
| Escape cancels an existing ruler-guide press, move or copy | done | Selection and Direct/Group Selection; app-input regressions cover preselected guides, document/interaction-start selection, Undo/Redo and saved state, later on-canvas motion/release and temporary borrowing from idle Pen. All eight checks and the Direct Selection Escape control passed in current full CI. [Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area) |
|
||||
| Ruler origin: corner drag with preview, double-click reset to the active artboard, relative X/Y fields, native save/reopen | done | #1212; `document.setRulerOrigin`, `document.resetRulerOrigin`; geometry and command coordinates stay absolute |
|
||||
| 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 |
|
||||
|
||||
Reference in New Issue
Block a user