mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 11:37:13 -06:00
#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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user