diff --git a/crates/ui-egui/src/panels/grid.rs b/crates/ui-egui/src/panels/grid.rs index 7dc07602..2ffd1ea4 100644 --- a/crates/ui-egui/src/panels/grid.rs +++ b/crates/ui-egui/src/panels/grid.rs @@ -471,7 +471,8 @@ fn cell(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, r: Rect, square let Some(photo) = app.session.catalog.photo(id).cloned() else { return }; let resp = ui.interact(r, egui::Id::new(("cell", id.0)), Sense::click_and_drag()); register(ui.ctx(), format!("thumb:{}", id.0), r); - let selected = app.session.selection.contains(id); + let state = app.session.selection.state_of(id); + let selected = state != lightcraft_engine::SelectionState::NotSelected; // screen readers: the file, then rating / flag / label let mut spoken = photo.file_name.clone(); if photo.rating > 0 { @@ -486,7 +487,7 @@ fn cell(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, r: Rect, square spoken.push_str(&format!(", {} label", app.session.catalog.label_name(l))); } resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, selected, &spoken)); - let active = app.session.selection.active == Some(id); + let active = state == lightcraft_engine::SelectionState::Active; let p = ui.painter(); let img_rect = if square { p.rect_filled(r, 0.0, if selected { t.cell_selected } else { t.cell }); @@ -514,6 +515,12 @@ fn cell(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, r: Rect, square } else { let ph = img_rect.shrink(if square { 20.0 } else { 0.0 }); p.rect_filled(ph, 0.0, Color32::from_gray(38)); + // not drawn yet (or unreadable): the selection is still visible + if active { + p.rect_stroke(ph, 0.0, Stroke::new(2.0, Color32::WHITE), StrokeKind::Outside); + } else if selected { + p.rect_stroke(ph, 0.0, Stroke::new(2.0, Color32::from_gray(170)), StrokeKind::Outside); + } if app.renderer.failure(Slot::Thumb(id)).is_some() { // unreadable / missing file p.text(ph.center(), Align2::CENTER_CENTER, "!", t.semibold(18.0), t.text_dim); diff --git a/crates/ui-egui/src/tests_grid.rs b/crates/ui-egui/src/tests_grid.rs index 3fe92327..e1a815a5 100644 --- a/crates/ui-egui/src/tests_grid.rs +++ b/crates/ui-egui/src/tests_grid.rs @@ -131,3 +131,27 @@ fn grid_headers_selection_scrolling_stacks_and_shape_changes() { assert!(after.height() > after.width(), "{after:?}"); assert_cheap_frames(&mut h); } + +/// Selected photos whose thumbnails have not loaded (or can't) are still marked selected in the +/// justified grid (issue #298): the active one white, the others grey. +#[test] +fn unloaded_thumbnails_show_their_selection() { + let mut h = grid(ViewMode::PhotoGrid); + h.request("engine.execute", json!({"command": "library.selectAll"}), T); + let img = h.snapshot(SETTLE); + assert_eq!(h.view.ctx.pixels_per_point(), 1.0); + let active = h.app.session.selection.active.expect("active"); + let canvas = h.app.canvas_rect.expect("grid drawn"); + let mut checked = 0; + for (w, r) in h.app.widgets.iter().filter(|(w, _)| w.starts_with("thumb:")) { + if !canvas.shrink(4.0).contains_rect(*r) || r.min.x < 6.0 { + continue; + } + let id = w.trim_start_matches("thumb:").parse::().unwrap(); + let want = if id == active.0 { egui::Color32::WHITE } else { egui::Color32::from_gray(170) }; + let px = img[((r.min.x - 1.0) as usize, r.center().y as usize)]; + assert_eq!(px, want, "photo {id}"); + checked += 1; + } + assert!(checked >= 3, "checked {checked}"); +}