diff --git a/crates/ui-egui/src/panels/edit.rs b/crates/ui-egui/src/panels/edit.rs index ed2ee463..ba36d77d 100644 --- a/crates/ui-egui/src/panels/edit.rs +++ b/crates/ui-egui/src/panels/edit.rs @@ -858,7 +858,7 @@ fn curve_editor(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, d: &Dev }; let line: Vec = (0..=96).map(|i| to_screen(i as f64 / 96.0, curve.eval(i as f64 / 96.0).clamp(0.0, 1.0))).collect(); p.add(egui::Shape::line(line, Stroke::new(2.0, color))); - // interaction: drag a point (both axes, between its neighbours), drag empty space to add and + // interaction: drag a point (both axes, between its neighbours; ⌥ = fine), drag empty space to add and // drag a new point, click empty space to add, double-click a point to delete it let drag_id = egui::Id::new(("curve-drag", &ch)); // the point being dragged (stored as `Option` so a stale value can be cleared) @@ -919,8 +919,17 @@ fn curve_editor(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, d: &Dev } else if resp.dragged() && let (Some(i), Some(q)) = (dragging, resp.interact_pointer_pos()) { - let (mut x, y) = from_screen(q); let mut v = pts.clone(); + // ⌥-drag = fine adjustment, as in Lightroom: the point moves a tenth of the pointer's + // motion (relative to where it is, so pressing ⌥ mid-drag doesn't make it jump) + let fine = ui.input(|i| i.modifiers.alt); + let (mut x, y) = match v.get(i) { + Some(cur) if fine => { + let dd = ui.input(|i| i.pointer.delta()); + ((cur.x + (dd.x / r.width()) as f64 * 0.1).clamp(0.0, 1.0), (cur.y - (dd.y / r.height()) as f64 * 0.1).clamp(0.0, 1.0)) + } + _ => from_screen(q), + }; if i < v.len() { let lo = if i == 0 { 0.0 } else { v[i - 1].x + 0.01 }; let hi = if i + 1 >= v.len() { 1.0 } else { v[i + 1].x - 0.01 }; diff --git a/crates/ui-egui/src/tests_curve.rs b/crates/ui-egui/src/tests_curve.rs index ca5848be..cc5d7130 100644 --- a/crates/ui-egui/src/tests_curve.rs +++ b/crates/ui-egui/src/tests_curve.rs @@ -69,6 +69,23 @@ fn dragging_a_point_moves_it_in_both_axes_with_one_undo_step() { assert_eq!(h.app.session.undo.len(), undo_before + 1, "one undo step per drag"); } +#[test] +fn alt_dragging_a_point_moves_it_a_tenth_as_far() { + let mut h = curve_open("master"); + exec(&mut h, "develop.curve", json!({"channel": "master", "points": [[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]]})); + h.step(); + let side = curve_side(&mut h); + // the same drag as above with ⌥ held: x 0.5 → ~0.49, y 0.5 → ~0.52 + let r = + h.request("ui.dragWidget", json!({"id": "curve", "fx": 0.5, "fy": 0.5, "dx": -0.1 * side, "dy": -0.2 * side, "steps": 12, "alt": true}), T); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + let c = pts(&develop(&h).curve.master); + assert_eq!(c.len(), 3, "{c:?}"); + assert!(c[1].0 < 0.5 && c[1].0 > 0.485, "x moves a tenth of the pointer: {c:?}"); + assert!(c[1].1 > 0.51 && c[1].1 < 0.525, "y moves a tenth of the pointer: {c:?}"); +} + fn click_widget(h: &mut Headless, id: &str, count: u64) { let r = h.request("ui.clickWidget", json!({"id": id, "count": count}), T); assert_eq!(r["ok"], true, "{id}: {r}"); diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 77ae26e4..20b81502 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -184,7 +184,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | LR-EDIT-LIGHT-WHITES | Whites | P0 | ✅ | `ctl:light.whites` | | | LR-EDIT-LIGHT-BLACKS | Blacks | P0 | ✅ | `ctl:light.blacks` | | | LR-EDIT-LIGHT-CURVE-PARAM | Parametric curve | P0 | ✅ | `ctl:curve.highlights`, `ctl:curve.lights`, `ctl:curve.darks`, `ctl:curve.shadows`, `ctl:curve.split*` | | -| LR-EDIT-LIGHT-CURVE-POINT | Point curve | P0 | ✅ | `cmd:develop.curve`, `cmd:curve.reset`, `cmd:curve.presets`, `cmd:curve.applyPreset`, `cmd:curve.savePreset`, `cmd:curve.deletePreset`, `cmd:curve.importPresets`, `cmd:curve.exportPresets`, `cmd:file.importCurvePresets`, `cmd:file.exportCurvePresets`, `crates/engine/src/cmd/curves.rs`, `crates/ui-egui/src/panels/edit.rs` | click to add a point; drag a point in both axes (between its neighbours, input / output readout, one undo step per drag); drag empty space to add and drag; double-click removes; reset: double-click a channel selector (that channel), the Reset button under the graph (every curve incl. parametric) or right-click the graph (channel / all); Point Curve preset dropdown: own Linear / Medium Contrast / Strong Contrast, user presets (save the current point curves, delete by right-click; saved with the library), import / export as `.lccurve` JSON | +| LR-EDIT-LIGHT-CURVE-POINT | Point curve | P0 | ✅ | `cmd:develop.curve`, `cmd:curve.reset`, `cmd:curve.presets`, `cmd:curve.applyPreset`, `cmd:curve.savePreset`, `cmd:curve.deletePreset`, `cmd:curve.importPresets`, `cmd:curve.exportPresets`, `cmd:file.importCurvePresets`, `cmd:file.exportCurvePresets`, `crates/engine/src/cmd/curves.rs`, `crates/ui-egui/src/panels/edit.rs` | click to add a point; drag a point in both axes (between its neighbours, input / output readout, one undo step per drag; ⌥-drag moves it a tenth as far for fine adjustment, issue #1363); drag empty space to add and drag; double-click removes; reset: double-click a channel selector (that channel), the Reset button under the graph (every curve incl. parametric) or right-click the graph (channel / all); Point Curve preset dropdown: own Linear / Medium Contrast / Strong Contrast, user presets (save the current point curves, delete by right-click; saved with the library), import / export as `.lccurve` JSON | | LR-EDIT-LIGHT-CURVE-RGB | Per-channel curves | P0 | ✅ | `cmd:develop.curve` (`channel`) | | | LR-EDIT-LIGHT-CURVE-REFINESAT | Curve saturation compensation | P1 | ✅ | `ctl:curve.refineSaturation` | | | LR-EDIT-LIGHT-CURVE-TAT | Drag-on-image curve adjust | P1 | ✅ | `cmd:develop.targeted` (`target: curve`) | |