diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 3b3622be..cbe113b2 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -1527,8 +1527,10 @@ pub(crate) fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) { let resp = ui.interact(cr, egui::Id::new(("film", id.0)), Sense::click()); register(ui.ctx(), format!("film:{}", id.0), cr); let sel = Some(*id) == active; + // as in the grid: every selected photo is filled, the active one framed in white (#187) + let selected = sel || app.session.selection.contains(*id); let p = ui.painter(); - if sel { + if selected { p.rect_filled(cr, 0.0, t.cell_selected); } else if resp.hovered() { p.rect_filled(cr, 0.0, t.cell_selected.gamma_multiply(0.6)); @@ -1549,6 +1551,8 @@ pub(crate) fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) { p.image(tex.tex.id(), fr, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); if sel { p.rect_stroke(fr, 0.0, Stroke::new(1.5, Color32::WHITE), StrokeKind::Outside); + } else if selected { + p.rect_stroke(fr, 0.0, Stroke::new(1.5, Color32::from_gray(170)), StrokeKind::Outside); } if app.ui.settings.film_badges && let Some(ph) = app.session.catalog.photo(*id) diff --git a/crates/ui-egui/src/tests_scroll.rs b/crates/ui-egui/src/tests_scroll.rs index 05167c47..455bd0d8 100644 --- a/crates/ui-egui/src/tests_scroll.rs +++ b/crates/ui-egui/src/tests_scroll.rs @@ -1,6 +1,6 @@ //! Headless tests of the photo grid's and the filmstrip's scrolling (issue #11): the user's //! scroll position stays put until the active photo changes; the mouse wheel scrolls the -//! filmstrip sideways. +//! filmstrip sideways. The filmstrip also shows which photos are selected (issue #187). use std::time::Duration; @@ -115,3 +115,31 @@ fn filmstrip_wheel_scrolls_and_keeps_its_position() { let cell = widget(&h, &format!("film:{second}")); assert!(cell.left() >= 0.0 && cell.right() <= 1200.0, "the new active photo is in view ({cell:?})"); } + +#[test] +fn filmstrip_shows_every_selected_photo() { + let mut h = demo("detail"); + let ids = h.app.session.visible_cloned(); + let (first, second) = (ids[0].0, ids[1].0); + // a cell's background, left of its thumbnail + let fill = |h: &mut Headless, id: u64| { + let r = widget(h, &format!("film:{id}")); + let img = h.snapshot(SETTLE); + img.pixels[(r.center().y as usize) * img.width() + r.left() as usize + 4] + }; + let active = fill(&mut h, first); + let idle = fill(&mut h, second); + assert_ne!(active, idle, "only the active photo is selected"); + // Select All: the other photos are selected too, the first stays active + let r = h.request("engine.execute", json!({"command": "library.selectAll"}), T); + assert_eq!(r["ok"], true, "{r}"); + assert_eq!(h.app.session.active().map(|p| p.0), Some(first)); + assert_eq!(fill(&mut h, second), active, "a selected photo that is not the active one looks selected"); + // back to one photo; then what a ⌘-click on a filmstrip cell runs (toggle) adds the second + let r = h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [first]}}), T); + assert_eq!(r["ok"], true, "{r}"); + assert_eq!(fill(&mut h, second), idle); + let r = h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [second], "mode": "toggle"}}), T); + assert_eq!(r["ok"], true, "{r}"); + assert_eq!(fill(&mut h, second), active); +} diff --git a/docs/parity.md b/docs/parity.md index d5a31ea9..3eeb91b3 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -171,7 +171,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | LR-VIEW-DETAIL | Single-photo view | P0 | ✅ | `cmd:view.detail`, `crates/ui-egui/src/panels/detail.rs` | | | LR-VIEW-EDIT | Edit view | P0 | ✅ | `cmd:panel.edit` | | | LR-VIEW-FULLSCREEN | Full-screen preview | P1 | ✅ | `cmd:view.fullScreenPreview`, `cmd:view.enterFullScreen` | photo on black, arrows step, Esc exits; ⇧⌘F window full screen | -| LR-VIEW-FILMSTRIP | Filmstrip | P0 | ✅ | `cmd:view.filmstrip`, `crates/ui-egui/src/panels/detail.rs` | the photo context menu on right-click; the mouse wheel scrolls it sideways; it follows the active photo only when that changes (centred if off screen), so a scrolled strip stays put | +| LR-VIEW-FILMSTRIP | Filmstrip | P0 | ✅ | `cmd:view.filmstrip`, `crates/ui-egui/src/panels/detail.rs` | the photo context menu on right-click; the mouse wheel scrolls it sideways; it follows the active photo only when that changes (centred if off screen), so a scrolled strip stays put; every selected photo is highlighted, the active one framed in white, as in the grid | | LR-VIEW-ZOOM | Zoom & pan | P0 | ✅ | `cmd:view.zoomFit`, `cmd:view.zoom100`, `cmd:view.zoomIn`, `cmd:view.zoomOut`, `cmd:view.zoomToggle`, `cmd:view.clickZoom` | steps 25–800 % (not 6–1600 %); Fill only in the bottom bar; a click on the photo (and Z / Space) eases to the click-zoom ratio (1:1 default, 2:1, 3:1, 4:1, 8:1 in the bottom bar) | | LR-VIEW-NAVIGATOR | Navigator mini map | P1 | ✅ | `cmd:view.navigator`, `crates/ui-egui/src/panels/detail.rs` | shown while zoomed (bottom right); click/drag pans | | LR-VIEW-BEFOREAFTER | Before / after | P0 | ✅ | `cmd:view.showOriginal`, `cmd:view.beforeAfter`, `cmd:view.beforeAfterSplit`, `cmd:view.beforeAfterTopBottom`, `cmd:view.beforeAfterSplitTopBottom`, `cmd:beforeAfter.setBefore`, `cmd:beforeAfter.copyAfterToBefore`, `cmd:beforeAfter.copyBeforeToAfter`, `cmd:beforeAfter.swap`, `cmd:beforeAfter.resetBefore` | all four layouts; before = the import state (defaults + import preset) or a chosen history step / version / the current settings; copy and swap (View → Before/After Settings, History row menu). The chosen before lasts for the session |