mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-31 05:28:25 -06:00
LR-CROP-STRAIGHTEN: rotation pointer review fixes: ⌘ crosshair, readout on the canvas, no lag
From review: - With ⌘ held a drag draws a level line, but outside the crop box the pointer showed the rotation arrow (the crosshair was set, then overwritten in the same frame). The crop tool now leaves the straighten crosshair alone while ⌘ is held and nothing is rotating. - The angle readout ran past the canvas (and the window) near its right or bottom edge. It now flips to the pointer's other side and is kept inside the canvas (readout_rect). - The readout showed the angle from before this frame's rotation. It is now drawn after the drag is applied, with the angle just set. Headless tests: ⌘ held outside the box gives the crosshair and no glyph; rotating at the canvas's corner keeps the readout on the canvas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
959cfa65f3
commit
e88be40e8c
@@ -1251,7 +1251,9 @@ fn crop_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Respons
|
||||
// while rotating, the pointer may leave the canvas: it still shows rotation and the angle
|
||||
let rotating = matches!(app.gesture, Some(Gesture::CropRotate { .. }));
|
||||
let pointer = resp.hover_pos().or_else(|| if rotating { ui.input(|i| i.pointer.latest_pos()) } else { None });
|
||||
if let Some(hq) = pointer {
|
||||
// with ⌘ held a drag draws a level line (the straighten crosshair, set above), not a rotation
|
||||
let straightening = !rotating && ui.input(|i| i.modifiers.command);
|
||||
if let Some(hq) = pointer.filter(|_| !straightening) {
|
||||
let near = handles.iter().position(|h| h.distance(hq) < 12.0).filter(|_| !rotating);
|
||||
let cursor = match near {
|
||||
Some(0 | 2) => egui::CursorIcon::ResizeNwSe,
|
||||
@@ -1266,10 +1268,10 @@ fn crop_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Respons
|
||||
}
|
||||
};
|
||||
ui.ctx().set_cursor_icon(cursor);
|
||||
if rotating {
|
||||
angle_readout(ui, hq, d.crop.geometry.angle);
|
||||
}
|
||||
}
|
||||
// the angle next to the pointer, drawn once this frame's rotation is applied (below)
|
||||
let readout_at = pointer.filter(|_| rotating);
|
||||
let mut shown_angle = d.crop.geometry.angle;
|
||||
// double-click inside the crop box applies the crop (same as Return / Done)
|
||||
if resp.double_clicked()
|
||||
&& let Some(q) = resp.interact_pointer_pos()
|
||||
@@ -1310,7 +1312,8 @@ fn crop_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Respons
|
||||
let c = map.screen(Point::new(0.5, 0.5));
|
||||
let a = (q - c).angle();
|
||||
let ang = (start_angle + (a - a0).to_degrees() as f64).clamp(-45.0, 45.0);
|
||||
let _ = app.run("crop.straighten", json!({"angle": (ang * 100.0).round() / 100.0}));
|
||||
shown_angle = (ang * 100.0).round() / 100.0;
|
||||
let _ = app.run("crop.straighten", json!({"angle": shown_angle}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1319,6 +1322,9 @@ fn crop_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Respons
|
||||
app.gesture = None;
|
||||
let _ = app.run("develop.endInteraction", json!({}));
|
||||
}
|
||||
if let Some(at) = readout_at {
|
||||
angle_readout(ui, at, shown_angle, map.rect);
|
||||
}
|
||||
let _ = id;
|
||||
}
|
||||
|
||||
@@ -1331,6 +1337,16 @@ pub(crate) fn crop_angle_label(angle: f64) -> String {
|
||||
format!("{shown}°")
|
||||
}
|
||||
|
||||
/// Where the angle readout of `size` goes for a pointer `at`: below right of it, flipped to the
|
||||
/// left / above where that would leave `bounds`, then kept inside them.
|
||||
fn readout_rect(at: Pos2, size: egui::Vec2, bounds: Rect) -> Rect {
|
||||
let x = if at.x + 18.0 + size.x > bounds.right() { at.x - 18.0 - size.x } else { at.x + 18.0 };
|
||||
let y = if at.y + 14.0 + size.y > bounds.bottom() { at.y - 14.0 - size.y } else { at.y + 14.0 };
|
||||
let x = x.clamp(bounds.left(), (bounds.right() - size.x).max(bounds.left()));
|
||||
let y = y.clamp(bounds.top(), (bounds.bottom() - size.y).max(bounds.top()));
|
||||
Rect::from_min_size(pos2(x, y), size)
|
||||
}
|
||||
|
||||
/// On top of everything (the tooltip layer), so neither the photo nor a panel covers it.
|
||||
fn top_painter(ui: &egui::Ui, name: &'static str) -> egui::Painter {
|
||||
ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new(name)))
|
||||
@@ -1345,12 +1361,13 @@ fn rotate_cursor(ui: &egui::Ui, at: Pos2) {
|
||||
register(ui.ctx(), "cropRotateCursor", r);
|
||||
}
|
||||
|
||||
/// While rotating: the angle, next to the pointer.
|
||||
fn angle_readout(ui: &egui::Ui, at: Pos2, angle: f64) {
|
||||
/// While rotating: the angle, next to the pointer, kept on the canvas (`bounds`): on the pointer's
|
||||
/// other side when it would run past an edge.
|
||||
fn angle_readout(ui: &egui::Ui, at: Pos2, angle: f64, bounds: Rect) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let p = top_painter(ui, "crop-angle-readout");
|
||||
let galley = p.layout_no_wrap(crop_angle_label(angle), t.font(12.5), Color32::WHITE);
|
||||
let rect = Rect::from_min_size(at + vec2(18.0, 14.0), galley.size() + vec2(12.0, 6.0));
|
||||
let rect = readout_rect(at, galley.size() + vec2(12.0, 6.0), bounds);
|
||||
p.rect_filled(rect, 4.0, Color32::from_black_alpha(170));
|
||||
p.galley(rect.min + vec2(6.0, 3.0), galley, Color32::WHITE);
|
||||
register(ui.ctx(), "cropAngleReadout", rect);
|
||||
|
||||
@@ -114,3 +114,36 @@ fn the_straighten_value_says_it_can_be_typed() {
|
||||
h.painted_text().iter().filter(|t| t.contains("lick")).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
/// With ⌘ held a drag draws a level line instead of rotating, so the pointer is the crosshair, not
|
||||
/// the rotation arrow.
|
||||
#[test]
|
||||
fn command_held_shows_the_straighten_crosshair() {
|
||||
let mut h = crop_tool();
|
||||
let img = h.app.image_rect.expect("the photo on screen");
|
||||
h.events.push(egui::Event::ModifiersChanged(egui::Modifiers::COMMAND));
|
||||
h.events.push(egui::Event::PointerMoved(egui::pos2(img.left() - 24.0, img.center().y)));
|
||||
h.settle(SETTLE);
|
||||
assert_eq!(h.last_cursor, egui::CursorIcon::Crosshair);
|
||||
assert!(!has(&h, "cropRotateCursor"));
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn the_readout_stays_on_the_canvas() {
|
||||
let mut h = crop_tool();
|
||||
let img = h.app.image_rect.expect("the photo on screen");
|
||||
let canvas = h.app.canvas_rect.expect("the canvas");
|
||||
let corner = canvas.right_bottom() - egui::vec2(4.0, 4.0);
|
||||
let (x, y) = ((corner.x - img.left()) / img.width(), (corner.y - img.top()) / img.height());
|
||||
let r = h.request(
|
||||
"ui.pointer",
|
||||
json!({"events": [{"kind": "down", "x": x, "y": y - 0.1}, {"kind": "drag", "x": x, "y": y - 0.05}, {"kind": "drag", "x": x, "y": y}]}),
|
||||
T,
|
||||
);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
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:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user