From 82a703111e4d1d206c00ad67129f725e082649a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 13:09:20 +0000 Subject: [PATCH 1/3] #2267: the Before label stays in view when zoomed in Zoomed in, the photo's corner is off the canvas, and the "Before" label (\ key or the Before view) left the screen with it. The label's pill now sits at the canvas' corner when the photo reaches past it, on whichever axes it does; at Fit nothing changes. Same rule for the hovered preset's name. Stacked on #2274 (the dark pill behind the label, same lines). Tests: the pill geometry with the photo inside and past the pane, and a headless check that at 100 % the registered label rect stays inside the canvas. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK --- crates/ui-egui/src/panels/detail.rs | 31 +++++++++++++++++++++++------ crates/ui-egui/src/tests_zoom.rs | 28 ++++++++++++++++++++++++++ docs/whats-new.md | 6 ++++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 5769e0356..6a5ad4ef7 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -717,16 +717,17 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { shown = draw(Slot::Before, img_rect, canvas); // on the photo itself, so it gets the hover label's dark pill to stay readable over light pictures let g = p.layout_no_wrap(crate::i18n::tr("Before").to_string(), t.font(12.0), Color32::WHITE); - let bg = label_badge_rect(img_rect, g.size()); + let bg = label_badge_rect(img_rect, canvas, g.size()); p.rect_filled(bg, 4.0, Color32::from_black_alpha(160)); p.galley(bg.min + vec2(8.0, 4.0), g, Color32::WHITE); + register(ui.ctx(), "canvas:beforeLabel", bg); } else if let Some((_key, label)) = &hover_key && let Some(tex) = app.renderer.textures.get(&Slot::Hover).filter(|t| t.photo == id) { let hover_rect = fit_texture_rect(img_rect, tex.size); p.image(tex.tex.id(), hover_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); let g = p.layout_no_wrap(label.clone(), t.font(12.0), Color32::WHITE); - let bg = label_badge_rect(hover_rect, g.size()); + let bg = label_badge_rect(hover_rect, canvas, g.size()); p.rect_filled(bg, 4.0, Color32::from_black_alpha(160)); p.galley(bg.min + vec2(8.0, 4.0), g, Color32::WHITE); shown = "hover"; @@ -915,9 +916,12 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } /// The dark pill behind a label drawn over the photo (`Before`, a hovered preset's name): at the -/// photo's top left, inset 8 points, with the `text` size plus padding. -fn label_badge_rect(photo: Rect, text: egui::Vec2) -> Rect { - Rect::from_min_size(photo.left_top() + vec2(8.0, 8.0), text + vec2(16.0, 8.0)) +/// photo's top left, inset 8 points, with the `text` size plus padding. Zoomed in, the photo +/// reaches past its `pane` (the canvas): the pill then sits at the pane's top left, so the label +/// stays in view (issue #2267: at 100 % it left the screen with the photo's corner). +fn label_badge_rect(photo: Rect, pane: Rect, text: egui::Vec2) -> Rect { + let corner = pos2(photo.left().max(pane.left()), photo.top().max(pane.top())); + Rect::from_min_size(corner + vec2(8.0, 8.0), text + vec2(16.0, 8.0)) } /// The info overlay at the canvas' top left (`view.infoOverlay`): file name with the capture date @@ -2727,13 +2731,28 @@ mod preview_geometry_tests { #[test] fn before_label_badge_wraps_the_text() { let text = vec2(40.0, 14.0); + let pane = Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0)); let photo = Rect::from_min_size(pos2(20.0, 30.0), vec2(400.0, 300.0)); - let bg = label_badge_rect(photo, text); + let bg = label_badge_rect(photo, pane, text); assert_eq!(bg.min, pos2(28.0, 38.0)); assert_eq!(bg.size(), vec2(56.0, 22.0)); assert!(photo.contains_rect(bg)); } + /// Zoomed in, the photo's corner is off the canvas: the pill moves to the canvas' corner + /// instead of leaving with it (issue #2267), on whichever axes the photo reaches past. + #[test] + fn before_label_badge_stays_in_the_pane_when_the_photo_reaches_past_it() { + let text = vec2(40.0, 14.0); + let pane = Rect::from_min_size(pos2(100.0, 50.0), vec2(800.0, 600.0)); + let both = label_badge_rect(Rect::from_min_size(pos2(-900.0, -700.0), vec2(3000.0, 2000.0)), pane, text); + assert_eq!(both.min, pos2(108.0, 58.0)); + assert!(pane.contains_rect(both)); + let left_only = label_badge_rect(Rect::from_min_size(pos2(-900.0, 120.0), vec2(3000.0, 400.0)), pane, text); + assert_eq!(left_only.min, pos2(108.0, 128.0)); + assert!(pane.contains_rect(left_only)); + } + #[test] fn portrait_preview_keeps_its_ratio_inside_a_landscape_frame() { let frame = Rect::from_min_size(pos2(0.0, 0.0), vec2(400.0, 200.0)); diff --git a/crates/ui-egui/src/tests_zoom.rs b/crates/ui-egui/src/tests_zoom.rs index b0595d27d..069a5778d 100644 --- a/crates/ui-egui/src/tests_zoom.rs +++ b/crates/ui-egui/src/tests_zoom.rs @@ -109,6 +109,34 @@ fn zoomed_before_after_views_stay_in_their_panes() { } } +/// The "Before" label (\ or the Before view) at 100 %: the photo's corner is off screen, and the +/// label used to leave with it (issue #2267). It now sits at the canvas' corner, in view. +#[test] +fn zoomed_in_the_before_label_stays_in_view() { + use crate::state::{BeforeAfter, Zoom}; + let mut h = detail(); + h.app.ui.before_after = BeforeAfter::Original; + let label = |h: &mut Headless| -> egui::Rect { + let w = h.request("ui.widgets", json!({"filter": "canvas:beforeLabel"}), T); + let r = w["result"][0]["rect"].as_array().unwrap_or_else(|| panic!("the Before label is registered: {w}")); + let v: Vec = 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])) + }; + h.app.ui.zoom = Zoom::Fit; + h.settle(SETTLE); + let (img, canvas) = (h.app.image_rect.expect("photo on screen"), h.app.canvas_rect.expect("canvas")); + let fitted = label(&mut h); + assert_eq!(fitted.min, img.min + egui::vec2(8.0, 8.0), "at Fit the label sits on the photo's corner"); + h.app.ui.zoom = Zoom::Percent(100.0); + h.app.ui.pan = (0.3, 0.4); + h.settle(SETTLE); + let img = h.app.image_rect.expect("photo on screen"); + assert!(img.min.x < canvas.min.x && img.min.y < canvas.min.y, "100 % reaches past the canvas ({img:?} in {canvas:?})"); + let zoomed = label(&mut h); + assert!(canvas.contains_rect(zoomed), "the label stays in view: {zoomed:?} in {canvas:?}"); + assert_eq!(zoomed.min, canvas.min + egui::vec2(8.0, 8.0)); +} + /// 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 diff --git a/docs/whats-new.md b/docs/whats-new.md index 2f02ffb91..3e3cb721b 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -1,5 +1,11 @@ # What's new in LightCraft +## 2026-10-19 + +### The Before label stays in view when zoomed in +- Zoomed in, the "Before" label (\ key or the Before view) now sits at the canvas' corner instead of leaving the + screen with the photo's corner (issue #2267). + ## 2026-10-14 ### Model downloads time out on a name that doesn't resolve From 1ea75ff9ea1c122150a09c717aabed5f49bdc2c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:37:44 +0000 Subject: [PATCH 2/3] #2267: the Before label paints over the window renders, below the filter pill, inside the pane Review follow-up: - the badge is painted after the region window renders, which zoomed in covered it (the very case the fix is for); - with a filter active, the pill in the canvas' corner has that corner: the badge sits below it, also when the photo's corner is just under it; - a wide label near the pane's right edge stays 8 points inside it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK --- crates/ui-egui/src/panels/detail.rs | 70 ++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 6a5ad4ef7..ff2a56dad 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -703,6 +703,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { p.text(pos2(canvas.right() - 16.0, canvas.top() + 14.0), Align2::RIGHT_CENTER, label, t.font(12.5), Color32::from_gray(60)); } 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)> = 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 }; 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) @@ -715,19 +719,17 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { p.text(label_at(img_rect, main_area), Align2::LEFT_CENTER, crate::i18n::tr("After"), t.font(12.0), t.text_dim); } else if show_before { shown = draw(Slot::Before, img_rect, canvas); - // on the photo itself, so it gets the hover label's dark pill to stay readable over light pictures + // on the photo itself, so it gets the hover label's dark pill to stay readable over light + // pictures; painted after the window renders below, which would cover it zoomed in let g = p.layout_no_wrap(crate::i18n::tr("Before").to_string(), t.font(12.0), Color32::WHITE); - let bg = label_badge_rect(img_rect, canvas, g.size()); - p.rect_filled(bg, 4.0, Color32::from_black_alpha(160)); - p.galley(bg.min + vec2(8.0, 4.0), g, Color32::WHITE); - register(ui.ctx(), "canvas:beforeLabel", bg); + before_badge = Some((label_badge_rect(img_rect, canvas, g.size(), badge_top_inset), g)); } else if let Some((_key, label)) = &hover_key && let Some(tex) = app.renderer.textures.get(&Slot::Hover).filter(|t| t.photo == id) { let hover_rect = fit_texture_rect(img_rect, tex.size); p.image(tex.tex.id(), hover_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); let g = p.layout_no_wrap(label.clone(), t.font(12.0), Color32::WHITE); - let bg = label_badge_rect(hover_rect, canvas, g.size()); + let bg = label_badge_rect(hover_rect, canvas, g.size(), badge_top_inset); p.rect_filled(bg, 4.0, Color32::from_black_alpha(160)); p.galley(bg.min + vec2(8.0, 4.0), g, Color32::WHITE); shown = "hover"; @@ -778,6 +780,11 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let clip = crate::region::window_clip(mode, v.before, v.visible, canvas, img_rect); draw_window(&p.with_clip_rect(clip), app, &window_ctx, v, view); } + if let Some((bg, g)) = before_badge { + p.rect_filled(bg, 4.0, Color32::from_black_alpha(160)); + p.galley(bg.min + vec2(8.0, 4.0), g, Color32::WHITE); + register(ui.ctx(), "canvas:beforeLabel", bg); + } // the line of a wipe over both if app.ui.before_after == BeforeAfter::Split { p.line_segment([pos2(line.x, img_rect.top()), pos2(line.x, img_rect.bottom())], Stroke::new(1.5, Color32::WHITE)); @@ -918,10 +925,13 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { /// The dark pill behind a label drawn over the photo (`Before`, a hovered preset's name): at the /// photo's top left, inset 8 points, with the `text` size plus padding. Zoomed in, the photo /// reaches past its `pane` (the canvas): the pill then sits at the pane's top left, so the label -/// stays in view (issue #2267: at 100 % it left the screen with the photo's corner). -fn label_badge_rect(photo: Rect, pane: Rect, text: egui::Vec2) -> Rect { - let corner = pos2(photo.left().max(pane.left()), photo.top().max(pane.top())); - Rect::from_min_size(corner + vec2(8.0, 8.0), text + vec2(16.0, 8.0)) +/// stays in view (issue #2267: at 100 % it left the screen with the photo's corner), below the +/// `top_inset` the filter pill takes in that corner, and never past the pane's right edge. +fn label_badge_rect(photo: Rect, pane: Rect, text: egui::Vec2, top_inset: f32) -> Rect { + let size = text + vec2(16.0, 8.0); + let left = photo.left().max(pane.left()).min(pane.right() - size.x - 16.0).max(pane.left()); + let corner = pos2(left, photo.top().max(pane.top() + top_inset)); + Rect::from_min_size(corner + vec2(8.0, 8.0), size) } /// The info overlay at the canvas' top left (`view.infoOverlay`): file name with the capture date @@ -1147,6 +1157,16 @@ fn region_overlay( edit } +/// The filter pill's inset from the canvas' top-left corner and its height (the same inset from +/// the left as from the top, so the pill sits in the corner, #1645). +const FILTER_PILL_INSET: f32 = 3.0; +const FILTER_PILL_HEIGHT: f32 = 20.0; + +/// Whether [`filter_pill`] has something to show (a filter is active). +fn filter_pill_shown(app: &LightcraftApp) -> bool { + !lightcraft_engine::filter_chips(&app.session.filter, &app.session.catalog).is_empty() +} + /// A small pill at the loupe's top left naming the active filters: the filmstrip and Next / Previous /// follow them, so a filter must never be invisible here. A click clears them all. It sits in the /// canvas margin, so the photo does not move. @@ -1160,10 +1180,9 @@ fn filter_pill(app: &mut LightcraftApp, ui: &mut egui::Ui, canvas: Rect) { let label = chips.iter().map(|c| crate::panels::chips::display_label(c, &app.session.filter, &app.session.catalog)).collect::>().join(" · "); let g = p.layout_no_wrap(crate::i18n::tr_format!("Filtered: {label}", label = label), t.font(12.0), Color32::from_gray(225)); - // the same inset from the left as from the top, so the pill sits in the corner (#1645) - let inset = 3.0; + let inset = FILTER_PILL_INSET; let w = (g.size().x + 40.0).min((canvas.width() - 2.0 * inset).max(60.0)); - let r = Rect::from_min_size(canvas.min + vec2(inset, inset), vec2(w, 20.0)); + let r = Rect::from_min_size(canvas.min + vec2(inset, inset), vec2(w, FILTER_PILL_HEIGHT)); let resp = ui.interact(r, egui::Id::new("loupe-filter-pill"), Sense::click()); crate::widgets::announce(&resp, crate::i18n::tr("Filters")); register(ui.ctx(), "loupe:filters", r); @@ -2733,7 +2752,7 @@ mod preview_geometry_tests { let text = vec2(40.0, 14.0); let pane = Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0)); let photo = Rect::from_min_size(pos2(20.0, 30.0), vec2(400.0, 300.0)); - let bg = label_badge_rect(photo, pane, text); + let bg = label_badge_rect(photo, pane, text, 0.0); assert_eq!(bg.min, pos2(28.0, 38.0)); assert_eq!(bg.size(), vec2(56.0, 22.0)); assert!(photo.contains_rect(bg)); @@ -2745,14 +2764,33 @@ mod preview_geometry_tests { fn before_label_badge_stays_in_the_pane_when_the_photo_reaches_past_it() { let text = vec2(40.0, 14.0); let pane = Rect::from_min_size(pos2(100.0, 50.0), vec2(800.0, 600.0)); - let both = label_badge_rect(Rect::from_min_size(pos2(-900.0, -700.0), vec2(3000.0, 2000.0)), pane, text); + let both = label_badge_rect(Rect::from_min_size(pos2(-900.0, -700.0), vec2(3000.0, 2000.0)), pane, text, 0.0); assert_eq!(both.min, pos2(108.0, 58.0)); assert!(pane.contains_rect(both)); - let left_only = label_badge_rect(Rect::from_min_size(pos2(-900.0, 120.0), vec2(3000.0, 400.0)), pane, text); + let left_only = label_badge_rect(Rect::from_min_size(pos2(-900.0, 120.0), vec2(3000.0, 400.0)), pane, text, 0.0); assert_eq!(left_only.min, pos2(108.0, 128.0)); assert!(pane.contains_rect(left_only)); } + /// With a filter active, the pill in the canvas' corner has that corner: the label sits below + /// it, whether the photo is past the pane or just under the pill. A photo lower down is left + /// alone. A wide label near the pane's right edge stays inside the pane. + #[test] + fn before_label_badge_keeps_clear_of_the_filter_pill_and_the_right_edge() { + let text = vec2(40.0, 14.0); + let pane = Rect::from_min_size(pos2(100.0, 50.0), vec2(800.0, 600.0)); + let inset = super::FILTER_PILL_INSET + super::FILTER_PILL_HEIGHT; + let past = label_badge_rect(Rect::from_min_size(pos2(-900.0, -700.0), vec2(3000.0, 2000.0)), pane, text, inset); + assert_eq!(past.min, pos2(108.0, 50.0 + inset + 8.0)); + let under = label_badge_rect(Rect::from_min_size(pos2(200.0, 60.0), vec2(400.0, 300.0)), pane, text, inset); + assert_eq!(under.min.y, 50.0 + inset + 8.0, "a photo whose corner is under the pill moves down too"); + let lower = label_badge_rect(Rect::from_min_size(pos2(200.0, 200.0), vec2(400.0, 300.0)), pane, text, inset); + assert_eq!(lower.min, pos2(208.0, 208.0)); + let wide = label_badge_rect(Rect::from_min_size(pos2(860.0, 200.0), vec2(400.0, 300.0)), pane, text, 0.0); + assert!(pane.contains_rect(wide), "{wide:?}"); + assert_eq!(wide.max.x, pane.right() - 8.0, "8 points from the pane's right edge, like the left inset"); + } + #[test] fn portrait_preview_keeps_its_ratio_inside_a_landscape_frame() { let frame = Rect::from_min_size(pos2(0.0, 0.0), vec2(400.0, 200.0)); From bfe3ab3d5889f96b5f7a04bb383e0ed60abb5ae5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 19:50:05 +0000 Subject: [PATCH 3/3] #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 Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK --- crates/ui-egui/src/panels/detail.rs | 59 ++++++++++++++++++++--------- crates/ui-egui/src/tests_zoom.rs | 23 +++++++++++ 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 7cb4613bd..dcda57484 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -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)> = 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> { use crate::state::InfoOverlay; let lines: Vec = 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. diff --git a/crates/ui-egui/src/tests_zoom.rs b/crates/ui-egui/src/tests_zoom.rs index 069a5778d..bfad3910c 100644 --- a/crates/ui-egui/src/tests_zoom.rs +++ b/crates/ui-egui/src/tests_zoom.rs @@ -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 = 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