With the pointer on the gray pasteboard, wheel/Alt-wheel/pinch zoom anchored on the pointer, so each notch slid the image away from it and out of the window. The anchor is now the nearest point of the image (clamped in document space, so rotated and flipped views work); over the image nothing changes.
This commit is contained in:
parent
97c6769d6b
commit
4cf8a5fd05
|
|
@ -2779,7 +2779,9 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
|
|||
// zooms nor slides the image towards the pointer.
|
||||
let nz = crate::zoom_levels::clamp(view.zoom * f, view.doc_size);
|
||||
if nz != view.zoom {
|
||||
zoom_about(&mut view, &xf, p, nz, false, ppp);
|
||||
// Over the pasteboard the anchor is the image's nearest point (#3720).
|
||||
let at = wheel_anchor(&xf, p, view.doc_size);
|
||||
zoom_about(&mut view, &xf, at, nz, false, ppp);
|
||||
}
|
||||
}
|
||||
(Some(crate::wheel_nav::Wheel::Pan(scroll)), _) => {
|
||||
|
|
@ -3557,6 +3559,20 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
|
|||
/// Zoom to `new_zoom` about the pointer `p`. By default the document point under it stays under
|
||||
/// it; with Preferences › Tools › Zoom Clicked Point to Center (`center_on_point`) it moves to the
|
||||
/// centre of the view instead (#204).
|
||||
/// Where a wheel or pinch zoom anchors for a pointer at `p`: the pointer itself over the image,
|
||||
/// otherwise the nearest point of the image (#3720). Anchoring on a pointer out on the gray
|
||||
/// pasteboard slid the image away from it, out of the window after a few notches; Photoshop keeps
|
||||
/// the image in view. Clamped in document space, so a rotated or flipped view behaves the same.
|
||||
fn wheel_anchor(xf: &ViewXform, p: Pos2, doc_size: [u32; 2]) -> Pos2 {
|
||||
let [w, h] = doc_size;
|
||||
let d = xf.to_doc(p);
|
||||
if w == 0 || h == 0 || !(d[0].is_finite() && d[1].is_finite()) {
|
||||
return p;
|
||||
}
|
||||
let c = [d[0].clamp(0.0, f64::from(w)), d[1].clamp(0.0, f64::from(h))];
|
||||
if c == d { p } else { xf.to_screen(c[0] as f32, c[1] as f32) }
|
||||
}
|
||||
|
||||
fn zoom_about(view: &mut View, xf: &ViewXform, p: Pos2, new_zoom: f32, center_on_point: bool, ppp: f32) {
|
||||
let before = xf.to_doc(p);
|
||||
view.zoom = new_zoom;
|
||||
|
|
@ -6687,6 +6703,23 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_anchor_is_the_pointer_over_the_image_and_the_nearest_image_point_off_it() {
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));
|
||||
for (rotation, flip) in [(0.0, false), (30.0, false), (90.0, true)] {
|
||||
let xf = ViewXform { rect, zoom: 0.5, center: [400.0, 300.0], flip, rotation, aspect: 1.0 };
|
||||
let inside = xf.to_screen(100.0, 200.0);
|
||||
assert_eq!(wheel_anchor(&xf, inside, [800, 600]), inside);
|
||||
for (p, want) in [((-300.0, -200.0), [0.0, 0.0]), ((1200.0, 300.0), [800.0, 300.0]), ((400.0, 900.0), [400.0, 600.0])] {
|
||||
let a = xf.to_doc(wheel_anchor(&xf, xf.to_screen(p.0, p.1), [800, 600]));
|
||||
assert!((a[0] - want[0]).abs() < 0.01 && (a[1] - want[1]).abs() < 0.01, "rot {rotation}: {p:?} -> {a:?}");
|
||||
}
|
||||
}
|
||||
let p = pos2(5.0, 5.0);
|
||||
let xf = ViewXform { rect, zoom: 1.0, center: [400.0, 300.0], flip: false, rotation: 0.0, aspect: 1.0 };
|
||||
assert_eq!(wheel_anchor(&xf, p, [0, 0]), p, "no document: the pointer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_about_centres_the_clicked_point_with_the_preference() {
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));
|
||||
|
|
|
|||
|
|
@ -707,3 +707,62 @@ fn escape_with_targa_options_open_does_not_deselect() {
|
|||
h.run_steps(2);
|
||||
assert!(h.state().session.active().unwrap().doc.selection.is_some(), "Esc belongs to Targa Options");
|
||||
}
|
||||
|
||||
/// How much of the 400 × 300 document is on screen: the visible document span on each axis,
|
||||
/// clipped to the image (0 when the image has left the view on that axis).
|
||||
fn visible_image(h: &Harness<'static, PhotocraftApp>) -> [f32; 2] {
|
||||
let r = h.state().last_canvas_rect;
|
||||
let (a, b) = (doc_at(h, r.min), doc_at(h, r.max));
|
||||
[(b[0].min(400.0) - a[0].max(0.0)).max(0.0), (b[1].min(300.0) - a[1].max(0.0)).max(0.0)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_zoom_over_the_pasteboard_keeps_the_image_in_view() {
|
||||
// #3720: zooming in with the pointer on the gray pasteboard anchored on the pointer, so a few
|
||||
// notches pushed the image out of the window. The anchor is now the nearest point of the image.
|
||||
for (zoom_with_wheel, m) in [(true, Modifiers::NONE), (false, Modifiers::ALT)] {
|
||||
let mut h = harness();
|
||||
h.state_mut().session.edit_prefs(|p| p.general.zoom_with_scroll_wheel = zoom_with_wheel);
|
||||
let r = h.state().last_canvas_rect;
|
||||
let p = pos2(r.left() + 20.0, r.top() + 20.0);
|
||||
let d = doc_at(&h, p);
|
||||
assert!(d[0] < 0.0 && d[1] < 0.0, "the pointer starts on the pasteboard: {d:?}");
|
||||
h.hover_at(p);
|
||||
h.run_steps(2);
|
||||
let z0 = zoom(&h);
|
||||
for _ in 0..20 {
|
||||
wheel(&h, 1.0, m);
|
||||
h.run_steps(3);
|
||||
}
|
||||
h.run_steps(20);
|
||||
assert!(zoom(&h) > z0 * 5.0, "the notches zoomed in: {z0} -> {}", zoom(&h));
|
||||
let v = visible_image(&h);
|
||||
assert!(v[0] > 0.0 && v[1] > 0.0, "zoom_with_wheel {zoom_with_wheel}: the image left the view {v:?}");
|
||||
// The image's corner nearest the pointer is the anchor: it stays where it was on screen.
|
||||
let c = doc_at(&h, pos2(r.left() + 20.0, r.top() + 20.0));
|
||||
assert!(c[0] < 0.0 && c[1] < 0.0, "still pasteboard under the pointer, the corner held: {c:?}");
|
||||
// A pinch from there behaves the same.
|
||||
h.event(egui::Event::Zoom(4.0));
|
||||
h.run_steps(3);
|
||||
let v = visible_image(&h);
|
||||
assert!(v[0] > 0.0 && v[1] > 0.0, "pinch: the image left the view {v:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_zoom_over_the_image_still_anchors_on_the_pointer() {
|
||||
let mut h = harness();
|
||||
let r = h.state().last_canvas_rect;
|
||||
let p = pos2(r.center().x - 150.0, r.center().y - 100.0);
|
||||
h.hover_at(p);
|
||||
h.run_steps(2);
|
||||
let d0 = doc_at(&h, p);
|
||||
assert!(d0[0] > 0.0 && d0[1] > 0.0);
|
||||
for _ in 0..5 {
|
||||
wheel(&h, 1.0, Modifiers::ALT);
|
||||
h.run_steps(3);
|
||||
}
|
||||
h.run_steps(20);
|
||||
let d1 = doc_at(&h, p);
|
||||
assert!((d1[0] - d0[0]).abs() < 0.1 && (d1[1] - d0[1]).abs() < 0.1, "{d0:?} -> {d1:?}");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue