LRC-LIB-REFVIEW: Reference view (⇧R)
A reference photo (photo menu ▸ Set as Reference Photo, or the active photo) shown fixed on the left beside the active photo on the right, which the Edit panel keeps working on — for matching a look by eye. View ▸ Reference View (⇧R); reference pane menu ▸ Clear Reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5d864c0374
commit
81d015b5f2
|
|
@ -549,13 +549,18 @@ impl LightcraftApp {
|
|||
}
|
||||
panels::bottombar::show(self, ui);
|
||||
let t = theme::Tokens::get(&ctx);
|
||||
let bg =
|
||||
if matches!(self.ui.view, state::ViewMode::Detail | state::ViewMode::Compare | state::ViewMode::Survey) { t.canvas } else { t.grid_bg };
|
||||
let bg = if matches!(self.ui.view, state::ViewMode::Detail | state::ViewMode::Compare | state::ViewMode::Survey | state::ViewMode::Reference)
|
||||
{
|
||||
t.canvas
|
||||
} else {
|
||||
t.grid_bg
|
||||
};
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(bg)).show(ui, |ui| match self.ui.view {
|
||||
state::ViewMode::PhotoGrid | state::ViewMode::SquareGrid => panels::grid::show(self, ui),
|
||||
state::ViewMode::Detail => panels::detail::show(self, ui),
|
||||
state::ViewMode::Compare => panels::compare::show_compare(self, ui),
|
||||
state::ViewMode::Survey => panels::compare::show_survey(self, ui),
|
||||
state::ViewMode::Reference => panels::compare::show_reference(self, ui),
|
||||
});
|
||||
panels::second::show(self, &ctx);
|
||||
panels::dialogs::show(self, &ctx);
|
||||
|
|
|
|||
|
|
@ -251,6 +251,7 @@ pub fn checked(app: &LightcraftApp, id: &str) -> Option<bool> {
|
|||
"view.detail" => Some(u.view == ViewMode::Detail),
|
||||
"view.compare" => Some(u.view == ViewMode::Compare),
|
||||
"view.survey" => Some(u.view == ViewMode::Survey),
|
||||
"view.reference" => Some(u.view == ViewMode::Reference),
|
||||
"view.leftPanel" => Some(u.left_panel),
|
||||
"view.filmstrip" => Some(u.filmstrip),
|
||||
"view.histogram" => Some(u.histogram),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
|||
("view.detail", "Detail", Some("D"), "View"),
|
||||
("view.compare", "Compare", Some("Shift+C"), "View"),
|
||||
("view.survey", "Survey", Some("N"), "View"),
|
||||
("view.reference", "Reference View", Some("Shift+R"), "View"),
|
||||
("photo.setReference", "Set as Reference Photo", None, ""),
|
||||
("compare.swap", "Swap Compare Photos", None, "View"),
|
||||
("compare.makeSelect", "Make Candidate the Select", None, "View"),
|
||||
("view.autoAdvance", "Auto Advance", None, "Photo"),
|
||||
|
|
@ -246,6 +248,30 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
app.ui.view = ViewMode::Survey;
|
||||
Ok(json!({"photos": crate::panels::compare::survey_photos(app).len()}))
|
||||
}
|
||||
"view.reference" => {
|
||||
// the reference: the one set before, else the active photo (the next one becomes active)
|
||||
let vis = app.session.visible_cloned();
|
||||
let reference = app
|
||||
.ui
|
||||
.reference
|
||||
.filter(|r| app.session.catalog.photo(lightcraft_catalog::PhotoId(*r)).is_some())
|
||||
.or_else(|| app.session.active().map(|a| a.0));
|
||||
let Some(r) = reference else { return Some(Err("select a photo to use as the reference".into())) };
|
||||
app.ui.reference = Some(r);
|
||||
if app.session.active().is_none_or(|a| a.0 == r)
|
||||
&& let Some(i) = vis.iter().position(|x| x.0 == r)
|
||||
&& let Some(next) = vis.get(i + 1).or(i.checked_sub(1).and_then(|j| vis.get(j)))
|
||||
{
|
||||
let _ = app.session.execute("library.select", &json!({"ids": [next.0]}));
|
||||
}
|
||||
app.ui.view = ViewMode::Reference;
|
||||
Ok(json!({"reference": r, "active": app.session.active().map(|a| a.0)}))
|
||||
}
|
||||
"photo.setReference" => {
|
||||
let id = p.get("id").and_then(Value::as_u64).or_else(|| app.session.active().map(|a| a.0));
|
||||
app.ui.reference = id;
|
||||
Ok(json!({"reference": id}))
|
||||
}
|
||||
"compare.swap" => crate::panels::compare::swap(app),
|
||||
"compare.makeSelect" => crate::panels::compare::make_select(app),
|
||||
"view.autoAdvance" => {
|
||||
|
|
|
|||
|
|
@ -337,3 +337,31 @@ pub fn show_survey(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
ui.painter().text(pos2(canvas.right() - 16.0, canvas.top() + 10.0), Align2::RIGHT_TOP, msg, t.font(12.0), t.text_dim);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference view: the reference photo (left, fixed) beside the active photo (right) — the one
|
||||
/// the Edit panel works on, so a look can be matched by eye.
|
||||
pub fn show_reference(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
let canvas = area_and_filmstrip(app, ui);
|
||||
let reference = app.ui.reference.map(PhotoId).filter(|r| app.session.catalog.photo(*r).is_some());
|
||||
let (Some(r), Some(active)) = (reference, app.session.active()) else {
|
||||
super::empty_message(ui, canvas, "No reference photo", "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)");
|
||||
return;
|
||||
};
|
||||
let area = canvas.shrink(18.0);
|
||||
let half = (area.width() - 16.0) / 2.0;
|
||||
let zoom = app.ui.zoom;
|
||||
let left = Rect::from_min_size(area.min, vec2(half, area.height()));
|
||||
let right = Rect::from_min_size(pos2(area.right() - half, area.top()), vec2(half, area.height()));
|
||||
let (_, lresp) = photo_tile(app, ui, r, Slot::Compare(0), left, "Reference", zoom);
|
||||
let (img, resp) = photo_tile(app, ui, active, Slot::Compare(1), right, "Active", zoom);
|
||||
app.image_rect = Some(img);
|
||||
pan(app, &resp, img);
|
||||
lresp.context_menu(|ui| {
|
||||
if ui.button("Clear Reference").clicked() {
|
||||
app.ui.reference = None;
|
||||
app.ui.view = ViewMode::Detail;
|
||||
}
|
||||
});
|
||||
let mid = area.center().x;
|
||||
ui.painter().line_segment([pos2(mid, area.top()), pos2(mid, area.bottom())], Stroke::new(1.0, Tokens::get(ui.ctx()).divider));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -592,6 +592,9 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
if ui.button("Open in Detail").clicked() {
|
||||
let _ = app.run("view.detail", json!({}));
|
||||
}
|
||||
if ui.button("Set as Reference Photo").clicked() {
|
||||
let _ = app.run("photo.setReference", json!({"id": id.0}));
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button("Set Rating", |ui| {
|
||||
for r in 0..=5 {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ pub enum ViewMode {
|
|||
Compare,
|
||||
/// The selected photos tiled.
|
||||
Survey,
|
||||
/// A reference photo (left, fixed) beside the active photo (right, being edited).
|
||||
Reference,
|
||||
}
|
||||
|
||||
/// The right-hand tool/panel shown next to the tool strip.
|
||||
|
|
@ -296,6 +298,9 @@ pub struct UiState {
|
|||
/// Compare view: (select, candidate) photo ids.
|
||||
#[serde(skip)]
|
||||
pub compare: Option<(u64, u64)>,
|
||||
/// Reference view: the reference photo.
|
||||
#[serde(skip)]
|
||||
pub reference: Option<u64>,
|
||||
/// Transient toast text and its expiry (seconds of app time).
|
||||
#[serde(skip)]
|
||||
pub toast: Option<(String, f64)>,
|
||||
|
|
@ -515,6 +520,7 @@ impl Default for UiState {
|
|||
window_fullscreen: None,
|
||||
filter_bar: false,
|
||||
compare: None,
|
||||
reference: None,
|
||||
toast: None,
|
||||
status: String::new(),
|
||||
dialog: None,
|
||||
|
|
|
|||
|
|
@ -546,3 +546,22 @@ fn grid_info_cycles_caption() {
|
|||
exec(&mut h, "view.gridInfo", json!({"info": "exposure"}));
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_view_pins_a_photo_beside_the_active_one() {
|
||||
let mut h = detail("panel.edit");
|
||||
let first = h.app.session.active().unwrap();
|
||||
exec(&mut h, "photo.setReference", json!({}));
|
||||
let r = h.request("ui.key", json!({"key": "r", "shift": true}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
assert_eq!(h.app.ui.view, crate::state::ViewMode::Reference);
|
||||
assert_eq!(h.app.ui.reference, Some(first.0));
|
||||
let active = h.app.session.active().unwrap();
|
||||
assert_ne!(active, first, "the next photo is the one being edited");
|
||||
// edits go to the active photo, the reference stays put
|
||||
exec(&mut h, "develop.set", json!({"control": "light.exposure", "value": 0.7}));
|
||||
assert_eq!(h.app.session.develop_of(active).unwrap().light.exposure, 0.7);
|
||||
assert_ne!(h.app.session.develop_of(first).unwrap().light.exposure, 0.7);
|
||||
h.settle(SETTLE);
|
||||
assert!(h.app.renderer.textures.get(&crate::render::Slot::Compare(0)).is_some_and(|t| t.photo == first), "the reference is drawn");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,10 +47,10 @@ and prints the summary; `cargo xtask parity --write` refreshes the summary table
|
|||
| X. Cross-cutting behaviours (BEHAV) | 14 | 1 | 2 | 1 | 8/8 (100%) | 5/5 (100%) |
|
||||
| Y. Menus | 80 | 1 | 4 | 8 | 46/47 (98%) | 23/23 (100%) |
|
||||
| Z. Keyboard shortcuts (desktop) | 74 | 3 | 3 | 1 | 49/52 (94%) | 22/23 (96%) |
|
||||
| Lightroom Classic extras | 24 | 8 | 47 | 9 | — | 20/21 (95%) |
|
||||
| **Total** | 377 | 25 | 98 | 37 | 193/198 (97%) | 136/144 (94%) |
|
||||
| Lightroom Classic extras | 25 | 8 | 46 | 9 | — | 20/21 (95%) |
|
||||
| **Total** | 378 | 25 | 97 | 37 | 193/198 (97%) | 136/144 (94%) |
|
||||
|
||||
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **77.9%** of 500 in-scope rows — P0 98.7% of 198 · P1 96.2% of 144 · P2 35.1% of 158.
|
||||
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **78.1%** of 500 in-scope rows — P0 98.7% of 198 · P1 96.2% of 144 · P2 35.8% of 158.
|
||||
<!-- /parity:summary -->
|
||||
|
||||
## Top gaps
|
||||
|
|
@ -691,7 +691,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06-
|
|||
| LRC-LIB-VIEWS | Grid / loupe / compare / survey / people | P1 | 🟡 | `cmd:view.photoGrid`, `cmd:view.squareGrid`, `cmd:view.detail`, `cmd:view.compare`, `cmd:view.survey`, `cmd:view.gridInfo` | grid, square grid, loupe, compare, survey, second window; square-grid captions: file name, exposure or capture date (View ▸ Grid Info); no people view |
|
||||
| LRC-LIB-COMPARE | Compare view | P1 | ✅ | `cmd:view.compare` | ⇧C (C is Crop here); swap, make select; zoom always linked |
|
||||
| LRC-LIB-SURVEY | Survey view | P2 | ✅ | `cmd:view.survey` | N |
|
||||
| LRC-LIB-REFVIEW | Reference view | P2 | ⬜ | | |
|
||||
| LRC-LIB-REFVIEW | Reference view | P2 | ✅ | `cmd:view.reference`, `cmd:photo.setReference`, `crates/ui-egui/src/panels/compare.rs` (`show_reference`) | View ▸ Reference View (⇧R): the reference photo beside the active one (edited with the Edit panel); photo menu ▸ Set as Reference Photo |
|
||||
| LRC-LIB-CATALOG-PANEL | Catalog sets | P1 | ✅ | `cmd:library.source` (`missing`) | all, recently added, picks, recently deleted, missing photos (when any) |
|
||||
| LRC-LIB-FOLDERS | Disk folder tree | P1 | ✅ | `cmd:library.browse`, `cmd:folder.rename`, `cmd:folder.move`, `crates/ui-egui/src/panels/left.rs` (`local_section`, `folder_tree`) | standard places + the browsed folder + Browse Folder…, each an expandable tree of subfolders, breadcrumb navigation; context menu: Rename Folder…, Move Folder To…, Show in Finder (on disk, photos relinked; like the file manager, not an undo step) |
|
||||
| LRC-LIB-COLLECTIONS | Collections & sets | P1 | ✅ | `cmd:album.create`, `cmd:album.createSmart`, `cmd:album.toggleTarget`, `cmd:album.setTarget`, `cmd:album.clearQuick` | albums + folders (sets), smart albums (rule editor), Quick Collection and target album (B in the grids adds / removes the selection; album menu ▸ Set as Target Album; the target is marked +) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue