LR-BEHAV-BG-TASKS: name the failed filmstrip thumbnail, firmer light placeholders, paper print cells

A failed thumbnail in the detail view's filmstrip is announced with the
file name and "This photo can't be opened" (the painted ! had no name).
The skeleton placeholder is set further off the surfaces in Silver and
Paper (1.3 / 1.2 : 1 on the grid before, about 1.7 / 1.6 after) and the
test floor for the light themes is 1.15. The print preview's empty and
loading cells go back to fixed greys: its page is paper in every theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford
2026-10-22 02:02:48 -04:00
co-authored by Claude Opus 5.5
parent 3de9654e39
commit 316f82dbd2
5 changed files with 38 additions and 10 deletions
+10 -3
View File
@@ -2836,7 +2836,8 @@ pub(crate) fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) {
continue;
}
let resp = ui.interact(cr, egui::Id::new(("film", id.0)), Sense::click());
crate::widgets::announce(&resp, app.session.catalog.photo(*id).map(|p| p.file_name.clone()).unwrap_or_default());
let failed = app.renderer.thumb(*id).is_none() && app.renderer.failure(Slot::Thumb(*id)).is_some();
crate::widgets::announce(&resp, film_spoken(app.session.catalog.photo(*id).map_or("", |p| p.file_name.as_str()), failed));
register(ui.ctx(), format!("film:{}", id.0), cr);
let state = app.session.selection.state_of(*id);
let sel = state == lightcraft_engine::SelectionState::Active;
@@ -2862,8 +2863,8 @@ pub(crate) fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) {
let thumb = app.renderer.thumb(*id);
if thumb.is_none() {
p.rect_filled(img_area, 0.0, t.skeleton);
if app.renderer.failure(Slot::Thumb(*id)).is_some() {
p.text(img_area.center(), Align2::CENTER_CENTER, "!", t.semibold(14.0), t.text_dim);
if failed {
p.text(img_area.center(), Align2::CENTER_CENTER, "!", t.semibold(14.0), t.text);
}
}
if let Some(tex) = thumb {
@@ -2934,6 +2935,12 @@ pub(crate) fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) {
});
}
/// What a screen reader says for a filmstrip thumbnail: the file, and why there is no picture
/// when the thumbnail failed (the painted `!` has no name of its own).
pub(crate) fn film_spoken(file_name: &str, failed: bool) -> String {
if failed { format!("{file_name}, {}", crate::i18n::tr("This photo can't be opened")) } else { file_name.to_string() }
}
/// Rating, flag and edited badges along a filmstrip thumbnail's bottom edge (Settings → Interface).
fn film_badges(p: &egui::Painter, t: &Tokens, fr: Rect, ph: &lightcraft_catalog::Photo) {
use crate::icons::{Icon, paint};
+4 -2
View File
@@ -805,11 +805,13 @@ fn cell(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, r: Rect, square
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);
// a light theme's skeleton is itself mid-grey: the ring takes the dim text colour
let ring = if t.skeleton.r() > 0x80 { t.text_dim } else { Color32::from_gray(170) };
p.rect_stroke(ph, 0.0, Stroke::new(2.0, ring), 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);
p.text(ph.center(), Align2::CENTER_CENTER, "!", t.semibold(18.0), t.text);
}
}
// labels and badges
+3 -2
View File
@@ -856,8 +856,9 @@ fn paint_cell(
clip.add(egui::Shape::mesh(mesh));
}
None => {
let t = Tokens::get(painter.ctx());
clip.rect_filled(shown, 0.0, if photo.is_some() { t.skeleton } else { t.track });
// the page is paper in every theme (its colour is the print's, not the theme's), so
// these stay fixed greys: a photo still to come, and an empty cell
clip.rect_filled(shown, 0.0, if photo.is_some() { Color32::from_gray(52) } else { Color32::from_gray(200) });
}
}
if im.stroke_enabled && im.stroke_width_pt > 0.0 {
+19 -1
View File
@@ -175,7 +175,25 @@ fn the_thumbnail_placeholder_stands_apart_in_every_theme() {
let t = Tokens::for_kind(kind);
for (name, behind) in [("grid", t.grid_bg), ("cell", t.cell), ("selected cell", t.cell_selected), ("canvas", t.canvas)] {
let c = contrast(t.skeleton, behind);
assert!(c >= 1.1, "{kind:?}: the placeholder on the {name} is {c:.2}:1");
// the light themes' surfaces are close together, so the placeholder has to be set
// further off them to read at all
let floor = if matches!(kind, ThemeKind::Silver | ThemeKind::Paper) { 1.15 } else { 1.1 };
assert!(c >= floor, "{kind:?}: the placeholder on the {name} is {c:.2}:1");
}
let mark = contrast(t.text, t.skeleton);
assert!(mark >= 3.0, "{kind:?}: the failure mark on the placeholder is {mark:.2}:1");
}
}
#[test]
fn a_failed_filmstrip_thumbnail_is_announced_with_its_reason() {
let spoken = |failed: bool| {
let (nodes, _, _) = frames(Vec::new(), |ui| {
let resp = ui.interact(Rect::from_min_size(pos2(10.0, 10.0), vec2(120.0, 80.0)), egui::Id::new("film-test"), egui::Sense::click());
crate::widgets::announce(&resp, crate::panels::detail::film_spoken("IMG_0001.jpg", failed));
});
nodes.into_iter().find(|n| n.role == "Button").map(|n| n.label)
};
assert_eq!(spoken(false).as_deref(), Some("IMG_0001.jpg"));
assert_eq!(spoken(true).as_deref(), Some("IMG_0001.jpg, This photo can't be opened"));
}
+2 -2
View File
@@ -194,7 +194,7 @@ impl Tokens {
grid_bg: rgb(0xd9d9d9),
cell: rgb(0xcbcbcb),
cell_selected: rgb(0xb4b4b4),
skeleton: rgb(0xbfbfbf),
skeleton: rgb(0xa6a6a6),
divider: rgb(0xc4c4c4),
inset: rgb(0xdcdcdc),
field: rgb(0xf6f6f6),
@@ -225,7 +225,7 @@ impl Tokens {
grid_bg: rgb(0xf2f2f2),
cell: rgb(0xe8e8e8),
cell_selected: rgb(0xd2d2d2),
skeleton: rgb(0xdddddd),
skeleton: rgb(0xbcbcbc),
divider: rgb(0xe0e0e0),
inset: rgb(0xf0f0f0),
field: rgb(0xffffff),