LR-CROP-ASPECT: remember the last crop aspect as the default

This commit is contained in:
Daniel Spruce
2026-10-10 10:33:44 +11:00
parent 88ea1a804b
commit 06550937e8
5 changed files with 69 additions and 1 deletions
+28
View File
@@ -1572,6 +1572,34 @@ mod tests {
assert!(ids(&mut h, "notice:previewOnly:").is_empty());
}
#[test]
fn crop_remembers_last_aspect_across_photos() {
let mut h = demo([1200.0, 800.0]);
let photos = h.app.session.visible_cloned();
h.app.ui.right = crate::state::RightPanel::Crop;
h.app.run("crop.aspect", json!({"aspect": "original"})).unwrap();
let saved = serde_json::to_value(&h.app.ui).unwrap();
assert_eq!(saved["cropDefaultAspect"], "original");
for &photo in &photos[1..3] {
h.app.run("library.select", json!({"ids": [photo.0]})).unwrap();
h.app.apply_crop_default();
let p = h.app.session.catalog.photo(photo).unwrap();
assert_eq!(p.develop.crop.aspect, Some((p.width * 100, p.height * 100)));
}
h.app.run("crop.aspect", json!({"aspect": "1x1"})).unwrap();
let existing = h.app.session.develop_of(photos[2]).unwrap().crop;
h.app.run("library.select", json!({"ids": [photos[3].0]})).unwrap();
h.app.apply_crop_default();
assert_eq!(h.app.session.develop_of(photos[3]).unwrap().crop.aspect, Some((100, 100)));
h.app.run("crop.aspect", json!({"aspect": "free"})).unwrap();
h.app.run("library.select", json!({"ids": [photos[2].0]})).unwrap();
h.app.apply_crop_default();
assert_eq!(h.app.session.develop_of(photos[2]).unwrap().crop, existing);
h.app.ui = serde_json::from_value(saved).unwrap();
assert_eq!(h.app.ui.crop_default_aspect, Some(json!("original")));
h.settle(SETTLE);
}
/// While cropping: O cycles the guides, ⇧O mirrors them, A locks / unlocks the aspect.
#[test]
fn crop_keys() {
+33
View File
@@ -349,6 +349,17 @@ impl LightcraftApp {
return r;
}
let r = self.session.execute(id, &params).map_err(|e| e.to_string());
if r.is_ok() && matches!(id, "crop.aspect" | "crop.rotateAspect") {
self.ui.crop_default_aspect = if id == "crop.aspect" && params.get("aspect").and_then(Value::as_str) == Some("original") {
Some(serde_json::json!("original"))
} else {
self.session
.active()
.and_then(|photo| self.session.develop_of(photo))
.map(|d| d.crop.aspect.map_or(serde_json::json!("free"), |(w, h)| serde_json::json!([w as f64 / 100.0, h as f64 / 100.0])))
};
self.ui.crop_default_photo = self.session.active();
}
if r.is_ok() && id == "photo.label" {
let label = params.get("label").and_then(Value::as_str).and_then(lightcraft_catalog::ColorLabel::parse);
let text = match label {
@@ -378,6 +389,27 @@ impl LightcraftApp {
r
}
/// Initialize the crop tool on untouched photos without replacing existing crops.
pub(crate) fn apply_crop_default(&mut self) {
if self.ui.right != state::RightPanel::Crop {
self.ui.crop_default_photo = None;
return;
}
let photo = self.session.active();
if self.ui.crop_default_photo == photo {
return;
}
self.ui.crop_default_photo = photo;
if let Some(photo) = photo
&& self.session.develop_of(photo).is_some_and(|d| d.crop == lightcraft_develop::Crop::default())
&& let Some(aspect) = self.ui.crop_default_aspect.clone()
&& aspect != serde_json::json!("free")
&& let Err(e) = self.session.execute("crop.aspect", &serde_json::json!({"aspect": aspect}))
{
self.ui.status = e.to_string();
}
}
/// Show a transient toast at the bottom of the canvas (like the reference app's HUD).
/// Advance a running slideshow (wrapping around at the end).
fn slideshow_tick(&mut self, ctx: &egui::Context) {
@@ -695,6 +727,7 @@ impl LightcraftApp {
}
fn logic_inner(&mut self, ctx: &egui::Context) {
self.apply_crop_default();
if !self.styled {
theme::install_fonts_with_chinese(ctx, self.chinese_font.as_ref());
theme::apply(ctx);
+1
View File
@@ -129,6 +129,7 @@ fn custom_aspect(app: &mut LightcraftApp, ui: &mut egui::Ui) {
}
fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
app.apply_crop_default();
let d = app.session.develop_of(id).unwrap_or_default();
header(ui, "Crop");
padded(ui, |ui| {
+6
View File
@@ -321,6 +321,10 @@ pub struct UiState {
/// Mirroring of the triangle / spiral crop guides (0..4).
pub crop_overlay_orient: u8,
pub crop_overlay: CropOverlay,
/// Last explicitly chosen aspect, retained across photos and app launches.
pub crop_default_aspect: Option<serde_json::Value>,
#[serde(skip)]
pub crop_default_photo: Option<lightcraft_catalog::PhotoId>,
pub show_filenames: bool,
/// What the square grid's caption shows: `filename`, `exposure` (shutter · aperture · ISO ·
/// focal length) or `date`.
@@ -703,6 +707,8 @@ impl Default for UiState {
mask_overlay_opacity: 50.0,
mask_pins: true,
crop_overlay: CropOverlay::Thirds,
crop_default_aspect: None,
crop_default_photo: None,
crop_overlay_orient: 0,
show_filenames: true,
grid_info: "filename".into(),
+1 -1
View File
@@ -308,7 +308,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|---|---|---|---|---|---|
| LR-CROP-RECT | Crop rectangle | P0 | ✅ | `cmd:crop.set`, `cmd:crop.drag`, `crates/geom/src/crop_drag.rs`, `crates/ui-egui/src/panels/detail.rs` | double-click inside the box applies the crop (like Return) |
| LR-CROP-ASPECT | Aspect ratios | P0 | ✅ | `cmd:crop.aspect`, `cmd:crop.rotateAspect`, `crates/ui-egui/src/panels/right.rs` | menu with presets + custom W × H, visible Lock toggle (also A), button names the active ratio; every handle honours the lock and the drag stops at the image edge instead of shifting the frame (issue #295); "Original" is the photo's own shape (no separate As Shot) |
| LR-CROP-ASPECT | Aspect ratios | P0 | ✅ | `cmd:crop.aspect`, `cmd:crop.rotateAspect`, `crates/ui-egui/src/panels/right.rs` | menu with presets + custom W × H, visible Lock toggle (also A), button names the active ratio; every handle honours the lock and the drag stops at the image edge instead of shifting the frame (issue #295); "Original" is the photo's own shape (no separate As Shot); the last chosen aspect is saved as the default for untouched photos in Crop, resolving Original per photo and preserving existing crops |
| LR-CROP-STRAIGHTEN | Straighten tool | P0 | ✅ | `cmd:crop.straighten`, `cmd:crop.autoStraighten` | Straighten Tool button: drag along a horizon/vertical; double-click or Auto levels automatically |
| LR-CROP-AUTO | Auto straighten | P1 | ✅ | `cmd:crop.autoStraighten` | crop-angle leveling from detected horizon/plumb lines (consensus required) |
| LR-CROP-ANGLE | Angle slider | P0 | ✅ | `ctl:crop.angle` | |