diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index b8aaff934..c0e4e7550 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1888,5 +1888,6 @@ "Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed", "ONNX model": "ONNX model", "commercial use subject to GPL conditions": "commercial use subject to GPL conditions", - "Click to type a value": "Klicken, um einen Wert einzugeben" + "Click to type a value": "Klicken, um einen Wert einzugeben", + "Angle": "Winkel" } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index 9f0362c32..f72868627 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1733,5 +1733,6 @@ "Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed", "ONNX model": "ONNX model", "commercial use subject to GPL conditions": "commercial use subject to GPL conditions", - "Click to type a value": "Hacer clic para escribir un valor" + "Click to type a value": "Hacer clic para escribir un valor", + "Angle": "Ángulo" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 1617546b4..73cc6bbc6 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1733,5 +1733,6 @@ "Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed", "ONNX model": "ONNX model", "commercial use subject to GPL conditions": "commercial use subject to GPL conditions", - "Click to type a value": "クリックして値を入力" + "Click to type a value": "クリックして値を入力", + "Angle": "角度" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index f87b93699..5e14b32dc 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1733,5 +1733,6 @@ "Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed", "ONNX model": "ONNX model", "commercial use subject to GPL conditions": "commercial use subject to GPL conditions", - "Click to type a value": "Clique para digitar um valor" + "Click to type a value": "Clique para digitar um valor", + "Angle": "Ângulo" } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index b7d5a0b64..51640d270 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1406,5 +1406,6 @@ "Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed", "ONNX model": "ONNX model", "commercial use subject to GPL conditions": "commercial use subject to GPL conditions", - "Click to type a value": "Щёлкните, чтобы ввести значение" + "Click to type a value": "Щёлкните, чтобы ввести значение", + "Angle": "Угол" } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index ec8621b2d..1e45e9a5b 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1733,5 +1733,6 @@ "Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed", "ONNX model": "ONNX model", "commercial use subject to GPL conditions": "commercial use subject to GPL conditions", - "Click to type a value": "点击以输入数值" + "Click to type a value": "点击以输入数值", + "Angle": "角度" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 901994155..a53288f51 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1733,5 +1733,6 @@ "Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed", "ONNX model": "ONNX model", "commercial use subject to GPL conditions": "commercial use subject to GPL conditions", - "Click to type a value": "按一下以輸入數值" + "Click to type a value": "按一下以輸入數值", + "Angle": "角度" } diff --git a/crates/ui-egui/src/panels/right.rs b/crates/ui-egui/src/panels/right.rs index 239212d79..2c8d50ebe 100644 --- a/crates/ui-egui/src/panels/right.rs +++ b/crates/ui-egui/src/panels/right.rs @@ -185,6 +185,32 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { if let Some(spec) = ang { let out = slider(ui, &spec, d.crop.geometry.angle, true, Some("Straighten")); super::edit::apply_slider_out(app, &spec, out, |app, v| app.run("crop.straighten", json!({"angle": v}))); + // an exact angle, typed (issue #534): a field of its own, besides the slider's value + padded(ui, |ui| { + ui.horizontal(|ui| { + ui.label(crate::i18n::tr("Angle")); + let mut angle = d.crop.geometry.angle; + let r = ui.add( + egui::DragValue::new(&mut angle) + .range(spec.min..=spec.max) + .clamp_existing_to_range(false) + .speed(0.05) + .fixed_decimals(2) + .suffix("°"), + ); + 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() { + let _ = app.run("crop.straighten", json!({"angle": (angle.clamp(spec.min, spec.max) * 100.0).round() / 100.0})); + } + if r.drag_stopped() { + let _ = app.run("develop.endInteraction", json!({})); + } + }); + }); } padded(ui, |ui| { ui.horizontal(|ui| { diff --git a/crates/ui-egui/src/tests_crop_rotate.rs b/crates/ui-egui/src/tests_crop_rotate.rs index 3ef63f421..5654b66af 100644 --- a/crates/ui-egui/src/tests_crop_rotate.rs +++ b/crates/ui-egui/src/tests_crop_rotate.rs @@ -128,6 +128,25 @@ fn command_held_shows_the_straighten_crosshair() { assert!(!has(&h, "cropRotateCursor")); } +/// The Crop panel has an angle field: click it, type an exact angle, Return. +#[test] +fn the_crop_panel_has_an_angle_field() { + 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}"); + } + let r = h.request("ui.text", json!({"text": "-3.25"}), T); + assert_eq!(r["ok"], true, "{r}"); + 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, "typed -3.25°, got {}", angle(&h)); +} + /// Rotating with the pointer at the canvas's corner keeps the angle readout on the canvas (and so /// on screen), rather than spilling past its edge. #[test] @@ -147,3 +166,15 @@ fn the_readout_stays_on_the_canvas() { let readout = h.app.widgets.iter().find(|(w, _)| w == "cropAngleReadout").map(|(_, r)| *r).expect("the readout while rotating"); assert!(canvas.contains_rect(readout), "{readout:?} in {canvas:?}"); } + +/// Dragging the angle field changes the angle as one undo step, like the slider. +#[test] +fn dragging_the_angle_field_is_one_undo_step() { + let mut h = crop_tool(); + let undo = h.app.session.undo.len(); + let r = h.request("ui.dragWidget", json!({"id": "cropAngleField", "dx": 40.0, "dy": 0.0, "steps": 6}), T); + assert_eq!(r["ok"], true, "{r}"); + h.settle(SETTLE); + assert!(angle(&h) > 0.0, "dragged right: {}", angle(&h)); + assert_eq!(h.app.session.undo.len(), undo + 1, "one step for the whole drag"); +}