LR-VIEW-ZOOM: a middle-button drag pans the photo whatever tool is active

In Detail, dragging with the middle button (the mouse wheel pressed) pans the
zoomed photo with a hand cursor, in every tool. Remove / Heal / Clone, the
mask brushes and Crop no longer have to be left to look around a zoomed photo,
and they don't paint or move during that drag: it is handled with the pinch and
scroll gestures, before the tool overlays.

Right-click keeps the photo's context menu and Space keeps the zoom toggle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ketbome
2026-10-23 20:17:57 -03:00
co-authored by Claude Opus 5.5
parent 98702b5c44
commit 6c17d29aed
4 changed files with 80 additions and 2 deletions
+2
View File
@@ -65,6 +65,8 @@ mod tests_button;
#[cfg(test)]
mod tests_canvas_keys;
#[cfg(test)]
mod tests_canvas_pan;
#[cfg(test)]
mod tests_compare;
#[cfg(test)]
mod tests_copy_paste_dialogs;
+15 -1
View File
@@ -187,6 +187,20 @@ pub(crate) fn pan_image(app: &mut LightcraftApp, area: Rect, img: Rect, delta: e
let _ = app.run("view.navigate", json!({"pan": pan}));
}
/// A drag with the middle button (the mouse wheel pressed) pans the zoomed photo whatever tool is
/// active, so the healing brush, mask brushes and crop never paint or move while you look around;
/// the hand cursor shows while it does.
fn middle_pan(app: &mut LightcraftApp, ui: &egui::Ui, resp: &egui::Response, area: Rect, img: Rect) -> bool {
if !resp.dragged_by(egui::PointerButton::Middle) {
return false;
}
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
if img.width() > area.width() + 1.0 || img.height() > area.height() + 1.0 {
pan_image(app, area, img, resp.drag_delta());
}
true
}
/// Net vertical notches (up is positive) of unmodified line/page-unit wheel events: a real mouse wheel, not a trackpad.
fn wheel_notches(events: &[egui::Event]) -> f32 {
events
@@ -532,7 +546,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
let mut navigating = false;
for &view_area in &areas {
let view_img = in_pane(view_area, img_rect);
if navigate_gesture(app, ui, &resp, view_area, view_img, native) {
if navigate_gesture(app, ui, &resp, view_area, view_img, native) || middle_pan(app, ui, &resp, view_area, view_img) {
navigating = true;
target_rect = fit_rect(main_area, display_aspect, app.ui.zoom, native, ppp, app.ui.pan);
img_rect = target_rect;
+62
View File
@@ -0,0 +1,62 @@
//! A middle-button drag (the mouse wheel pressed) pans the zoomed photo in Detail whatever tool is
//! active: the healing brush doesn't paint meanwhile, and the hand cursor shows.
use std::time::Duration;
use egui::{Event, PointerButton, Pos2, vec2};
use serde_json::json;
use crate::headless::Headless;
use crate::{LightcraftApp, Services};
const T: Duration = Duration::from_secs(20);
fn spots(h: &Headless) -> usize {
let id = h.app.session.active().expect("active photo");
h.app.session.develop_of(id).unwrap_or_default().spots.len()
}
/// Press `button` at `from`, move to `to` over a few frames and release; the cursor seen mid-drag.
fn drag(h: &mut Headless, button: PointerButton, from: Pos2, to: Pos2) -> egui::CursorIcon {
let modifiers = egui::Modifiers::NONE;
h.app.synthetic.push(Event::PointerMoved(from));
h.app.synthetic.push(Event::PointerButton { pos: from, button, pressed: true, modifiers });
h.step();
for i in 1..=6 {
h.app.synthetic.push(Event::PointerMoved(from + (to - from) * (i as f32 / 6.0)));
h.step();
}
let cursor = h.last_cursor;
h.app.synthetic.push(Event::PointerButton { pos: to, button, pressed: false, modifiers });
h.step();
h.step();
cursor
}
#[test]
fn a_middle_drag_pans_with_the_healing_brush_and_adds_no_spot() {
let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
for (method, params) in [
("ui.set", json!({"view": "detail"})),
("engine.execute", json!({"command": "panel.remove"})),
("engine.execute", json!({"command": "view.zoom100"})),
] {
let r = h.request(method, params, T);
assert_eq!(r["ok"], true, "{r}");
h.step();
}
h.step();
let at = h.app.image_rect.expect("the photo is shown").center();
let pan = h.app.ui.pan;
let cursor = drag(&mut h, PointerButton::Middle, at, at + vec2(-150.0, -100.0));
assert_eq!(cursor, egui::CursorIcon::Grabbing, "the hand shows while panning");
assert_ne!(h.app.ui.pan, pan, "the middle drag pans");
assert_eq!(spots(&h), 0, "the healing brush doesn't paint during a middle drag");
// the left button still paints a spot, and doesn't pan
let pan = h.app.ui.pan;
let at = h.app.image_rect.expect("the photo is shown").center();
drag(&mut h, PointerButton::Primary, at, at + vec2(40.0, 0.0));
assert_eq!(spots(&h), 1, "a left drag paints a spot");
assert_eq!(h.app.ui.pan, pan);
}
File diff suppressed because one or more lines are too long