From 01fa8545e8552304d37fadc89da39cdf35202f60 Mon Sep 17 00:00:00 2001 From: Rahul Date: Mon, 2 Nov 2026 15:00:11 +0530 Subject: [PATCH] #3601: the Square Grid's badge band sits on the photo itself paint_cell_badges sized the badge band from the cell's whole image area, so on a portrait photo it stuck out past the photo on both sides and crossed the selection frame (which paint_cell_thumbnail draws around the photo's fitted rect). The band is now built from the rect the photo actually occupies - fitted rect, or the placeholder's - and a photo shorter than the band keeps its own top edge. Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> --- crates/ui-egui/src/panels/grid.rs | 44 +++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/panels/grid.rs b/crates/ui-egui/src/panels/grid.rs index 29579a01fb..21abd6e904 100644 --- a/crates/ui-egui/src/panels/grid.rs +++ b/crates/ui-egui/src/panels/grid.rs @@ -806,10 +806,10 @@ fn cell(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, r: Rect, square } else { r }; - paint_cell_thumbnail(app, p, &t, id, img_rect, ThumbFlags { square, state, onscreen, ppp }); + let photo_rect = paint_cell_thumbnail(app, p, &t, id, img_rect, ThumbFlags { square, state, onscreen, ppp }); // labels and badges paint_cell_caption(app, p, &t, &photo, r, square); - paint_cell_badges(app, p, &t, &photo, img_rect, BadgeFlags { square, selected, hovered: resp.hovered() }); + paint_cell_badges(app, p, &t, &photo, photo_rect, BadgeFlags { square, selected, hovered: resp.hovered() }); if let Some(name) = &photo.copy_name { paint_copy_tag(p, &t, name, img_rect, square); } @@ -838,7 +838,7 @@ pub(crate) fn cell_description(app: &LightcraftApp, photo: &lightcraft_catalog:: spoken } -fn paint_cell_thumbnail(app: &mut LightcraftApp, p: &egui::Painter, t: &Tokens, id: PhotoId, img_rect: Rect, flags: ThumbFlags) { +fn paint_cell_thumbnail(app: &mut LightcraftApp, p: &egui::Painter, t: &Tokens, id: PhotoId, img_rect: Rect, flags: ThumbFlags) -> Rect { let ThumbFlags { square, state, onscreen, ppp } = flags; let size = thumb_px(img_rect.width().max(img_rect.height()), ppp); request_thumb(app, id, size, if onscreen { 10 } else { 5 }); @@ -848,6 +848,7 @@ fn paint_cell_thumbnail(app: &mut LightcraftApp, p: &egui::Painter, t: &Tokens, let fit = super::detail::fit_texture_rect(img_rect, [tw, th]); p.image(tex.tex.id(), fit, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); selection_ring(p, t, fit, state); + fit } else { let ph = img_rect.shrink(if square { 20.0 } else { 0.0 }); p.rect_filled(ph, 0.0, t.skeleton); @@ -857,6 +858,7 @@ fn paint_cell_thumbnail(app: &mut LightcraftApp, p: &egui::Painter, t: &Tokens, // unreadable / missing file p.text(ph.center(), Align2::CENTER_CENTER, "!", t.semibold(18.0), t.text); } + ph } } @@ -890,14 +892,22 @@ fn paint_cell_caption(app: &LightcraftApp, p: &egui::Painter, t: &Tokens, photo: } } -fn paint_cell_badges(app: &LightcraftApp, p: &egui::Painter, t: &Tokens, photo: &lightcraft_catalog::Photo, img_rect: Rect, flags: BadgeFlags) { +/// The 24-pixel band the badges sit in, on the photo itself: a portrait's fitted rect is +/// narrower than the cell's image area and a landscape's is shorter, so a band sized from +/// the image area sticks out past the photo and crosses its selection frame (#3601). A photo +/// shorter than the band keeps its own top edge. +fn photo_badge_bar(photo_rect: Rect) -> Rect { + Rect::from_min_max(pos2(photo_rect.left(), (photo_rect.bottom() - 24.0).max(photo_rect.top())), photo_rect.right_bottom()) +} + +fn paint_cell_badges(app: &LightcraftApp, p: &egui::Painter, t: &Tokens, photo: &lightcraft_catalog::Photo, photo_rect: Rect, flags: BadgeFlags) { let BadgeFlags { square, selected, hovered } = flags; let show_badges = match app.ui.settings.grid_badges { crate::state::GridBadges::Auto => hovered || selected || photo.rating > 0 || photo.flag != Flag::None || photo.label.is_some(), crate::state::GridBadges::Always => true, crate::state::GridBadges::Never => false, }; - let badge_bar = Rect::from_min_max(pos2(img_rect.left(), img_rect.bottom() - 24.0), img_rect.right_bottom()); + let badge_bar = photo_badge_bar(photo_rect); if !square && photo.label.is_some() { // Justified photos fill the whole cell, so the label shows as a translucent tinted footer // over the photo's bottom edge (opaque, it would hide 24 px of every labelled photo). @@ -1662,4 +1672,28 @@ mod tests { assert_eq!(resolve_group(GroupBy::None, 220.0), GroupBy::None); assert_eq!(resolve_group(GroupBy::Year, 400.0), GroupBy::Year); } + + /// The badge band sits on the photo itself — a portrait's fitted rect is narrower than the + /// cell's image area, a landscape's is shorter — so it never sticks out past the photo or + /// crosses its selection frame (#3601). + #[test] + fn the_badge_band_stays_on_the_photo() { + let cell = Rect::from_min_size(pos2(10.0, 24.0), vec2(180.0, 180.0)); + // A photo that fills the image area: the band is the usual 24 px footer + let full = photo_badge_bar(cell); + assert!((full.height() - 24.0).abs() < 0.01, "{full:?}"); + assert!((full.bottom() - cell.bottom()).abs() < 0.01); + assert!((full.left() - cell.left()).abs() < 0.01 && (full.right() - cell.right()).abs() < 0.01); + // A portrait photo fitted inside it: the band is no wider than the photo + let portrait = Rect::from_min_size(pos2(cell.left() + 55.0, cell.top()), vec2(70.0, 180.0)); + let bar = photo_badge_bar(portrait); + assert!(bar.left() >= portrait.left() && bar.right() <= portrait.right(), "{bar:?}"); + assert!((bar.height() - 24.0).abs() < 0.01); + // A landscape photo shorter than the band: its own top edge, never a negative rect + let wide = Rect::from_min_size(pos2(cell.left(), cell.top() + 80.0), vec2(180.0, 20.0)); + let bar = photo_badge_bar(wide); + assert!((bar.top() - wide.top()).abs() < 0.01, "{bar:?}"); + assert!(bar.bottom() <= wide.bottom() + 0.01); + } } +