#2267: the Before label sits below the info overlay too

Review follow-up (CodeRabbit): zoomed in, the badge moves to the canvas'
top-left corner, where the info overlay draws its lines; with the overlay
on, the label was painted under its first line. The badge's top inset
now takes the overlay's extent as well as the filter pill's. The overlay
is split into its lines and an extent helper, laid out with the same
fonts, so the two can't disagree.

Test: zoomed_in_the_before_label_keeps_clear_of_the_info_overlay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
This commit is contained in:
Claude
2026-10-21 19:50:05 +00:00
parent 1df76a12da
commit bfe3ab3d58
2 changed files with 65 additions and 17 deletions
+42 -17
View File
@@ -751,8 +751,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
let shown;
// the Before label's pill, painted once the window renders are down (they would cover it)
let mut before_badge: Option<(Rect, std::sync::Arc<egui::Galley>)> = None;
// below the filter pill when it is there (the canvas' top-left corner is its place)
let badge_top_inset = if !fullscreen && filter_pill_shown(app) { FILTER_PILL_INSET + FILTER_PILL_HEIGHT } else { 0.0 };
// below what else lives in the canvas' top-left corner: the filter pill, the info overlay
let pill_inset = if !fullscreen && filter_pill_shown(app) { FILTER_PILL_INSET + FILTER_PILL_HEIGHT } else { 0.0 };
let badge_top_inset = pill_inset.max(info_overlay_extent(app, &p, &photo));
if split {
// each view stays in its pane: zoomed in, a photo reaches past it (and would cover the
// other view's margins with its whole-frame render, magnified and blurred)
@@ -983,9 +984,46 @@ fn label_badge_rect(photo: Rect, pane: Rect, text: egui::Vec2, top_inset: f32) -
/// The info overlay at the canvas' top left (`view.infoOverlay`): file name with the capture date
/// and size, or with the camera and exposure.
fn info_overlay(app: &LightcraftApp, p: &egui::Painter, canvas: Rect, photo: &lightcraft_catalog::Photo) {
let Some(lines) = info_overlay_lines(app, photo) else { return };
let t = Tokens::get(p.ctx());
let mut y = canvas.top() + INFO_OVERLAY_TOP;
for (i, l) in lines.iter().enumerate() {
// laid out uncoloured: `galley` only fills placeholder text with its colour, so a white
// layout made the shadow a second white copy (#1690)
let g = p.layout_no_wrap(l.clone(), info_overlay_font(&t, i), Color32::PLACEHOLDER);
let at = pos2(canvas.left() + 14.0, y);
// a soft shadow keeps the text readable on bright photos
p.galley(at + vec2(1.0, 1.0), g.clone(), Color32::from_black_alpha(200));
p.galley(at, g.clone(), Color32::WHITE);
y += g.size().y + INFO_OVERLAY_GAP;
}
register(p.ctx(), "canvas:infoOverlay", Rect::from_min_max(canvas.min, pos2(canvas.left() + 320.0, y)));
}
/// Where the info overlay's text starts below the canvas' top, and the gap after each line.
const INFO_OVERLAY_TOP: f32 = 12.0;
const INFO_OVERLAY_GAP: f32 = 3.0;
/// The info overlay's font for line `i` (the file name is the first, larger line).
fn info_overlay_font(t: &Tokens, i: usize) -> egui::FontId {
if i == 0 { t.semibold(15.0) } else { t.font(12.5) }
}
/// How far below the canvas' top the info overlay's text reaches (0 when it is off), so a label
/// drawn in that corner can sit below it.
fn info_overlay_extent(app: &LightcraftApp, p: &egui::Painter, photo: &lightcraft_catalog::Photo) -> f32 {
let Some(lines) = info_overlay_lines(app, photo) else { return 0.0 };
let t = Tokens::get(p.ctx());
lines.iter().enumerate().fold(INFO_OVERLAY_TOP, |y, (i, l)| {
y + p.layout_no_wrap(l.clone(), info_overlay_font(&t, i), Color32::PLACEHOLDER).size().y + INFO_OVERLAY_GAP
})
}
/// The info overlay's lines for `photo`, or `None` when it is off.
fn info_overlay_lines(app: &LightcraftApp, photo: &lightcraft_catalog::Photo) -> Option<Vec<String>> {
use crate::state::InfoOverlay;
let lines: Vec<String> = match app.ui.info_overlay {
InfoOverlay::Off => return,
InfoOverlay::Off => return None,
InfoOverlay::Basic => {
let date = photo.captured.as_deref().unwrap_or(&photo.imported);
vec![photo.file_name.clone(), format!("{} · {} × {}", pretty_date(date), photo.width, photo.height)]
@@ -1014,20 +1052,7 @@ fn info_overlay(app: &LightcraftApp, p: &egui::Painter, canvas: Rect, photo: &li
v
}
};
let t = Tokens::get(p.ctx());
let mut y = canvas.top() + 12.0;
for (i, l) in lines.iter().enumerate() {
let font = if i == 0 { t.semibold(15.0) } else { t.font(12.5) };
// laid out uncoloured: `galley` only fills placeholder text with its colour, so a white
// layout made the shadow a second white copy (#1690)
let g = p.layout_no_wrap(l.clone(), font, Color32::PLACEHOLDER);
let at = pos2(canvas.left() + 14.0, y);
// a soft shadow keeps the text readable on bright photos
p.galley(at + vec2(1.0, 1.0), g.clone(), Color32::from_black_alpha(200));
p.galley(at, g.clone(), Color32::WHITE);
y += g.size().y + 3.0;
}
register(p.ctx(), "canvas:infoOverlay", Rect::from_min_max(canvas.min, pos2(canvas.left() + 320.0, y)));
Some(lines)
}
/// What the user did to a face box this frame.
+23
View File
@@ -137,6 +137,29 @@ fn zoomed_in_the_before_label_stays_in_view() {
assert_eq!(zoomed.min, canvas.min + egui::vec2(8.0, 8.0));
}
/// With the info overlay on, zoomed in, the Before label sits below the overlay's lines instead
/// of under them (both live in the canvas' top-left corner).
#[test]
fn zoomed_in_the_before_label_keeps_clear_of_the_info_overlay() {
use crate::state::{BeforeAfter, InfoOverlay, Zoom};
let mut h = detail();
h.app.ui.before_after = BeforeAfter::Original;
h.app.ui.info_overlay = InfoOverlay::Exposure;
h.app.ui.zoom = Zoom::Percent(100.0);
h.app.ui.pan = (0.3, 0.4);
h.settle(SETTLE);
let rect = |h: &mut Headless, id: &str| -> egui::Rect {
let w = h.request("ui.widgets", json!({"filter": id}), T);
let r = w["result"][0]["rect"].as_array().unwrap_or_else(|| panic!("{id} is registered: {w}"));
let v: Vec<f32> = r.iter().filter_map(|x| x.as_f64()).map(|x| x as f32).collect();
egui::Rect::from_min_size(egui::pos2(v[0], v[1]), egui::vec2(v[2], v[3]))
};
let (badge, overlay) = (rect(&mut h, "canvas:beforeLabel"), rect(&mut h, "canvas:infoOverlay"));
let canvas = h.app.canvas_rect.expect("canvas");
assert!(badge.top() >= overlay.bottom(), "the label {badge:?} is below the overlay {overlay:?}");
assert!(canvas.contains_rect(badge), "and still in view: {badge:?} in {canvas:?}");
}
/// A crop of no size (or a hair wide), from an agent or a sidecar, gave the loupe a NaN aspect:
/// the photo was laid out at a NaN size, which egui's debug checks stop on (a panic in a debug
/// build) and nothing could be drawn or clicked. It now lays out at a finite size, fitted and at