diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index c1bfd0b5..09ccb55a 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -18,6 +18,8 @@ pub const UI_COMMANDS: &[UiCommand] = &[ ("view.leftPanel", "My Photos Panel", Some("Cmd+Shift+L"), "View"), ("view.beforeAfter", "Compare Before and After", Some("Y"), "View"), ("view.beforeAfterSplit", "Before/After Split", Some("Shift+Y"), "View"), + ("view.beforeAfterTopBottom", "Before/After Top/Bottom", Some("Alt+Y"), "View"), + ("view.beforeAfterSplitTopBottom", "Before/After Split Top/Bottom", Some("Alt+Shift+Y"), "View"), ("view.showOriginal", "Show Original", Some("\\"), "View"), ("view.zoomFit", "Zoom to Fit", Some("Cmd+0"), "View"), ("view.zoom100", "Zoom 100%", Some("Cmd+1"), "View"), @@ -125,6 +127,14 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option { + app.ui.before_after = if app.ui.before_after == BeforeAfter::TopBottom { BeforeAfter::Off } else { BeforeAfter::TopBottom }; + Ok(Value::Null) + } + "view.beforeAfterSplitTopBottom" => { + app.ui.before_after = if app.ui.before_after == BeforeAfter::SplitTopBottom { BeforeAfter::Off } else { BeforeAfter::SplitTopBottom }; + Ok(Value::Null) + } "view.showOriginal" => { app.ui.before_after = if app.ui.before_after == BeforeAfter::Original { BeforeAfter::Off } else { BeforeAfter::Original }; Ok(Value::Null) diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 5abcca6a..f0f252e6 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -124,15 +124,25 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let ppp = ui.ctx().pixels_per_point(); let area = canvas.shrink(if crop_tool { 48.0 } else { 24.0 }); let native = [photo.width.max(1) as usize, photo.height.max(1) as usize]; - let split = app.ui.before_after == BeforeAfter::SideBySide; - let areas: Vec = if split { - let half = area.width() / 2.0 - 6.0; - vec![ - Rect::from_min_size(area.min, vec2(half, area.height())), - Rect::from_min_size(pos2(area.center().x + 6.0, area.top()), vec2(half, area.height())), - ] - } else { - vec![area] + // two views (before, after): side by side or stacked + let split = matches!(app.ui.before_after, BeforeAfter::SideBySide | BeforeAfter::TopBottom); + let split_view = matches!(app.ui.before_after, BeforeAfter::Split | BeforeAfter::SplitTopBottom); + let areas: Vec = match app.ui.before_after { + BeforeAfter::SideBySide => { + let half = area.width() / 2.0 - 6.0; + vec![ + Rect::from_min_size(area.min, vec2(half, area.height())), + Rect::from_min_size(pos2(area.center().x + 6.0, area.top()), vec2(half, area.height())), + ] + } + BeforeAfter::TopBottom => { + let half = area.height() / 2.0 - 14.0; + vec![ + Rect::from_min_size(area.min, vec2(area.width(), half)), + Rect::from_min_size(pos2(area.left(), area.center().y + 14.0), vec2(area.width(), half)), + ] + } + _ => vec![area], }; let main_area = *areas.last().unwrap_or(&area); let img_rect = fit_rect(main_area, aspect, app.ui.zoom, native, ppp, app.ui.pan); @@ -174,7 +184,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { app.renderer.request_quick(Slot::Preview, q, 110); } let show_before = app.ui.before_after == BeforeAfter::Original || ui.input(|i| i.key_down(egui::Key::Backslash)); - if (split || show_before || app.ui.before_after == BeforeAfter::Split) + if (split || show_before || split_view) && let Some(job) = app.session.render_job(id, rw.max(8), rh.max(8), true, !crop_tool) { app.renderer.request(Slot::Before, job, 90); @@ -221,6 +231,14 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } p.line_segment([pos2(mid, img_rect.top()), pos2(mid, img_rect.bottom())], Stroke::new(1.5, Color32::WHITE)); } + if app.ui.before_after == BeforeAfter::SplitTopBottom { + let mid = img_rect.center().y; + if let Some(tex) = app.renderer.textures.get(&Slot::Before).filter(|t| t.photo == id) { + let top = Rect::from_min_max(img_rect.min, pos2(img_rect.right(), mid)); + p.image(tex.tex.id(), top, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 0.5)), Color32::WHITE); + } + p.line_segment([pos2(img_rect.left(), mid), pos2(img_rect.right(), mid)], Stroke::new(1.5, Color32::WHITE)); + } if app.ui.show_clipping && !show_before { clipping_overlay(app, &p, img_rect); } diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 34b3b15a..d40a6ce0 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -55,6 +55,10 @@ pub enum BeforeAfter { Original, SideBySide, Split, + /// Before above after. + TopBottom, + /// One image split horizontally: before above the line. + SplitTopBottom, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] diff --git a/docs/parity.md b/docs/parity.md index 53e90e1a..edeead7f 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -45,10 +45,10 @@ and prints the summary; `cargo xtask parity --write` refreshes the summary table | V. Preferences (PREF) | 0 | 3 | 5 | 3 | 0/1 (0%) | 0/4 (0%) | | W. Cloud & AI infrastructure (CLOUD / AI) | 0 | 1 | 1 | 2 | — | — | | X. Cross-cutting behaviours (BEHAV) | 10 | 2 | 5 | 1 | 7/8 (88%) | 3/5 (60%) | -| Y. Menus | 41 | 20 | 24 | 8 | 36/47 (77%) | 3/23 (13%) | -| Z. Keyboard shortcuts (desktop) | 44 | 16 | 20 | 1 | 39/52 (75%) | 5/23 (22%) | +| Y. Menus | 42 | 19 | 24 | 8 | 37/47 (79%) | 3/23 (13%) | +| Z. Keyboard shortcuts (desktop) | 45 | 16 | 19 | 1 | 39/52 (75%) | 6/23 (26%) | | Lightroom Classic extras | 1 | 17 | 61 | 9 | — | 1/21 (5%) | -| **Total** | 182 | 111 | 207 | 37 | 147/198 (74%) | 32/144 (22%) | +| **Total** | 184 | 110 | 206 | 37 | 148/198 (75%) | 33/144 (23%) | ## Top gaps @@ -124,7 +124,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. **Still missing / broken:** - No command yet: F full-screen preview, ⇧⌘F window full screen, ⇧⌘V paste selected, - ⌘, settings, ⌘F focus search, ⌘G / ⇧⌘G stacks, ⌥Y top/bottom before/after, A visualize spots, `[` `]` / ⇧`[` ⇧`]` + ⌘, settings, ⌘F focus search, ⌘G / ⇧⌘G stacks, A visualize spots, `[` `]` / ⇧`[` ⇧`]` brush size/feather, ⌃H / ⌃M merges, F1 help, ⇧6–9 label + advance. - `⌫` in the Masking panel deletes the active mask; spots have no pin selection yet (⌫ does nothing in Remove). - `H` opens Remove; Lightroom also uses it (Classic) to hide pins — no pin toggle yet. @@ -195,7 +195,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | LR-VIEW-FILMSTRIP | Filmstrip | P0 | ✅ | `cmd:view.filmstrip` | | | LR-VIEW-ZOOM | Zoom & pan | P0 | ✅ | `cmd:view.zoomFit`, `cmd:view.zoom100`, `cmd:view.zoomIn`, `cmd:view.zoomOut`, `cmd:view.zoomToggle` | steps 25–800 % (not 6–1600 %); Fill only in the bottom bar | | LR-VIEW-NAVIGATOR | Navigator mini map | P1 | ⬜ | | | -| LR-VIEW-BEFOREAFTER | Before / after | P0 | 🟡 | `cmd:view.showOriginal`, `cmd:view.beforeAfter`, `cmd:view.beforeAfterSplit` | no top/bottom layouts; "before" is always the original | +| LR-VIEW-BEFOREAFTER | Before / after | P0 | 🟡 | `cmd:view.showOriginal`, `cmd:view.beforeAfter`, `cmd:view.beforeAfterSplit`, `cmd:view.beforeAfterTopBottom`, `cmd:view.beforeAfterSplitTopBottom` | all four layouts; "before" is always the original (no before-state from history) | | LR-VIEW-COMPARE | Compare two photos | P1 | ⬜ | | | | LR-VIEW-SURVEY | Survey view [Classic] | P2 | ⬜ | | | | LR-VIEW-INFOOVERLAY | Info overlay on the photo | P1 | ⬜ | | | @@ -581,7 +581,7 @@ visible menu bar yet: the menu model is only exposed through the control channel | MENU-VIEW-HISTOGRAM | Show/Hide histogram | P0 | ✅ | `cmd:view.histogram` | | | MENU-VIEW-INFOOVERLAY | Show info overlay | P1 | ⬜ | | | | MENU-VIEW-SHOWORIGINAL | Show Original | P0 | ✅ | `cmd:view.showOriginal` | | -| MENU-VIEW-BEFOREAFTER | Before/After submenu | P0 | 🟡 | `cmd:view.beforeAfter`, `cmd:view.beforeAfterSplit` | left/right and split left/right only | +| MENU-VIEW-BEFOREAFTER | Before/After submenu | P0 | ✅ | `cmd:view.beforeAfter`, `cmd:view.beforeAfterSplit`, `cmd:view.beforeAfterTopBottom`, `cmd:view.beforeAfterSplitTopBottom` | | | MENU-VIEW-ZOOM | Zoom in / out / toggle / fit / 1:1 | P0 | ✅ | `cmd:view.zoomIn`, `cmd:view.zoomOut`, `cmd:view.zoomToggle`, `cmd:view.zoomFit`, `cmd:view.zoom100` | | | MENU-VIEW-CLIPPING | Show Clipping | P0 | ✅ | `cmd:view.clipping` | | | MENU-VIEW-MASKOVERLAY | Mask overlay / cycle colour | P0 | 🟡 | `cmd:view.maskOverlay` | no colour cycle | @@ -696,7 +696,7 @@ From `06-shortcuts.md` part 1. Evidence is our binding; conflicts are explained | KEY-FLAGADVANCE | Flag and advance — ⇧Z / ⇧X / ⇧U | P1 | 🟡 | `cmd:photo.flag` (`advance`) | ⇧X and ⇧U bound; pick-and-advance has no key (⇧P = presets) | | KEY-NEXTPREV | Next / previous — → / ← | P0 | ✅ | `cmd:library.next`, `cmd:library.previous` | | | KEY-BA-CYCLE | Before/after — Y | P0 | ✅ | `cmd:view.beforeAfter` | toggles side by side (no cycling) | -| KEY-BA-TOPBOTTOM | Before/after top/bottom — ⌥Y | P1 | ⬜ | | | +| KEY-BA-TOPBOTTOM | Before/after top/bottom — ⌥Y | P1 | ✅ | `cmd:view.beforeAfterTopBottom`, `cmd:view.beforeAfterSplitTopBottom` | ⌥Y, ⇧⌥Y | | KEY-BA-SPLIT | Split before/after — ⇧Y | P0 | ✅ | `cmd:view.beforeAfterSplit` | | | KEY-CHOOSECOPY | Choose settings to copy — ⇧⌘C | P0 | ✅ | `cmd:dialog.copySettings` | | | KEY-RESETALL | Reset all — ⇧⌘R | P0 | ✅ | `cmd:develop.reset` | |