Grid: selected photos whose thumbnail isn't drawn yet still show as selected (#298)

The grid and filmstrip now both ask Selection::state_of.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford 2026-10-08 11:29:05 -04:00
parent 5b80fc6bcf
commit 6c19090fde
No known key found for this signature in database
2 changed files with 33 additions and 2 deletions

View File

@ -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);

View File

@ -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::<u64>().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}");
}