From 7e109e880fdad1592bc8eb0e884c9d9d9eafe10a Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 11:55:11 -0400 Subject: [PATCH] LR-CROP-STRAIGHTEN: the Angle field applies a typed angle on Return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The field applied every keystroke: an undo step and a render per key, and half-typed angles (-3° on the way to -3.25°). It now applies on Return or when focus leaves, as one undo step, and ignores changes that leave the angle as it is. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/right.rs | 9 +++++++-- crates/ui-egui/src/tests_crop_rotate.rs | 27 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/right.rs b/crates/ui-egui/src/panels/right.rs index 2c8d50ebe..91d7f181d 100644 --- a/crates/ui-egui/src/panels/right.rs +++ b/crates/ui-egui/src/panels/right.rs @@ -196,14 +196,19 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { .clamp_existing_to_range(false) .speed(0.05) .fixed_decimals(2) - .suffix("°"), + .suffix("°") + // a typed angle applies on Return (or leaving the field), not keystroke by + // keystroke: no half-typed angles, one undo step + .update_while_editing(false), ); crate::widgets::register(ui.ctx(), "cropAngleField", r.rect); // a drag on the field is one undo step, like the slider's if r.drag_started() { let _ = app.run("develop.beginInteraction", json!({"label": "Straighten"})); } - if r.changed() && angle.is_finite() { + // egui reports a change while text is typed even when the value isn't updated yet: + // apply only a new angle + if r.changed() && angle.is_finite() && angle != d.crop.geometry.angle { let _ = app.run("crop.straighten", json!({"angle": (angle.clamp(spec.min, spec.max) * 100.0).round() / 100.0})); } if r.drag_stopped() { diff --git a/crates/ui-egui/src/tests_crop_rotate.rs b/crates/ui-egui/src/tests_crop_rotate.rs index 9cd69cc2b..75a74bfc4 100644 --- a/crates/ui-egui/src/tests_crop_rotate.rs +++ b/crates/ui-egui/src/tests_crop_rotate.rs @@ -179,6 +179,33 @@ fn dragging_the_angle_field_is_one_undo_step() { assert_eq!(h.app.session.undo.len(), undo + 1, "one step for the whole drag"); } +/// Typing into the Angle field one key at a time applies the angle once, on Return: no undo step +/// or render per keystroke, no half-typed angles (-3° on the way to -3.25°). +#[test] +fn typing_in_the_angle_field_applies_on_return() { + let mut h = crop_tool(); + let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.settle(SETTLE); + for key in ["a", "Backspace"] { + let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T); + assert_eq!(r["ok"], true, "{r}"); + } + h.settle(SETTLE); + let (before, undo) = (angle(&h), h.app.session.undo.len()); + for ch in ["-", "3", ".", "2", "5"] { + let r = h.request("ui.text", json!({"text": ch}), T); + assert_eq!(r["ok"], true, "{r}"); + h.settle(SETTLE); + } + assert_eq!((angle(&h), h.app.session.undo.len()), (before, undo), "nothing applied while typing"); + let r = h.request("ui.key", json!({"key": "Enter"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.settle(SETTLE); + assert!((angle(&h) + 3.25).abs() < 1e-9, "applied on Return: {}", angle(&h)); + assert_eq!(h.app.session.undo.len(), undo + 1, "as one undo step"); +} + /// Rotating in the margin beside a narrow photo keeps the readout next to the pointer: it is kept /// on the canvas, not pinned to the photo's edge. #[test]