mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 18:18:12 -06:00
LR-VIEW-INFOOVERLAY: the info overlay clears the macOS window controls and shows the rating and flag (#2460)
The full-screen preview hides the top bar, but on macOS the window controls stay at the top-left, where the info overlay drew the file name. When they are visible, the overlay now starts below them. In macOS full screen they are gone and the overlay keeps the corner. The overlay also shows the photo's star rating and flag under its lines, when the photo has them.
This commit is contained in:
@@ -988,7 +988,7 @@ fn label_badge_rect(photo: Rect, pane: Rect, text: egui::Vec2, top_inset: f32) -
|
||||
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;
|
||||
let mut y = canvas.top() + info_overlay_top(app, &t);
|
||||
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)
|
||||
@@ -999,10 +999,12 @@ fn info_overlay(app: &LightcraftApp, p: &egui::Painter, canvas: Rect, photo: &li
|
||||
p.galley(at, g.clone(), Color32::WHITE);
|
||||
y += g.size().y + INFO_OVERLAY_GAP;
|
||||
}
|
||||
y = overlay_badges(p, &t, pos2(canvas.left() + 14.0, y), photo);
|
||||
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.
|
||||
/// Where the info overlay's text starts below the canvas' top (without window controls over it),
|
||||
/// and the gap after each line.
|
||||
const INFO_OVERLAY_TOP: f32 = 12.0;
|
||||
const INFO_OVERLAY_GAP: f32 = 3.0;
|
||||
|
||||
@@ -1011,14 +1013,22 @@ 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.
|
||||
/// How far below the canvas' top the info overlay's text and badges reach (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)| {
|
||||
let text = lines.iter().enumerate().fold(info_overlay_top(app, &t), |y, (i, l)| {
|
||||
y + p.layout_no_wrap(l.clone(), info_overlay_font(&t, i), Color32::PLACEHOLDER).size().y + INFO_OVERLAY_GAP
|
||||
})
|
||||
});
|
||||
text + if has_overlay_badges(photo) { OVERLAY_BADGES_H } else { 0.0 }
|
||||
}
|
||||
|
||||
/// Where the info overlay's text starts below the canvas' top: the full-screen preview has no top
|
||||
/// bar, so on macOS it starts below the window controls that sit over the photo there (#2460).
|
||||
fn info_overlay_top(app: &LightcraftApp, t: &Tokens) -> f32 {
|
||||
let controls = app.ui.fullscreen && app.integrated_titlebar && !app.window_is_fullscreen;
|
||||
if controls { t.top_bar_h } else { INFO_OVERLAY_TOP }
|
||||
}
|
||||
|
||||
/// The info overlay's lines for `photo`, or `None` when it is off.
|
||||
@@ -1036,6 +1046,45 @@ fn info_overlay_lines(app: &LightcraftApp, photo: &lightcraft_catalog::Photo) ->
|
||||
Some(lines)
|
||||
}
|
||||
|
||||
/// The height of the info overlay's rating and flag row.
|
||||
const OVERLAY_BADGES_H: f32 = 17.0;
|
||||
|
||||
fn has_overlay_badges(photo: &lightcraft_catalog::Photo) -> bool {
|
||||
photo.rating > 0 || photo.flag != lightcraft_catalog::Flag::None
|
||||
}
|
||||
|
||||
/// The star rating and flag under the info overlay's lines, when the photo has either; returns
|
||||
/// the y below them.
|
||||
fn overlay_badges(p: &egui::Painter, t: &Tokens, at: Pos2, photo: &lightcraft_catalog::Photo) -> f32 {
|
||||
use crate::icons::{Icon, paint};
|
||||
use lightcraft_catalog::Flag;
|
||||
let flag = match photo.flag {
|
||||
Flag::Pick => Some((Icon::FlagPickFilled, Color32::WHITE)),
|
||||
Flag::Reject => Some((Icon::FlagReject, t.reject)),
|
||||
Flag::None => None,
|
||||
};
|
||||
if !has_overlay_badges(photo) {
|
||||
return at.y;
|
||||
}
|
||||
let stars = photo.rating.min(5);
|
||||
let mut icons: Vec<(Icon, Color32)> = vec![(Icon::StarFilled, Color32::WHITE); stars as usize];
|
||||
icons.extend(flag);
|
||||
let side = 12.0;
|
||||
let mut x = at.x;
|
||||
for (i, (icon, color)) in icons.into_iter().enumerate() {
|
||||
// the flag stands a little apart from the stars
|
||||
if i > 0 && i == stars as usize {
|
||||
x += 5.0;
|
||||
}
|
||||
let r = Rect::from_min_size(pos2(x, at.y + 1.0), vec2(side, side));
|
||||
paint(p, r.translate(vec2(1.0, 1.0)), icon, Color32::from_black_alpha(200));
|
||||
paint(p, r, icon, color);
|
||||
x += side + 2.0;
|
||||
}
|
||||
register(p.ctx(), "canvas:infoOverlayBadges", Rect::from_min_max(at, pos2(x, at.y + side + 2.0)));
|
||||
at.y + OVERLAY_BADGES_H
|
||||
}
|
||||
|
||||
/// What the user did to a face box this frame.
|
||||
enum RegionEdit {
|
||||
/// The × was clicked.
|
||||
@@ -3462,4 +3511,83 @@ mod info_overlay_tests {
|
||||
app.ui.info_overlay = InfoOverlay::Basic;
|
||||
assert_eq!(painted_lines(&app, &photo), Vec::<String>::new(), "nothing chosen, nothing shown");
|
||||
}
|
||||
|
||||
fn demo_app() -> (LightcraftApp, lightcraft_catalog::Photo) {
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), crate::Services { png: None, ..Default::default() });
|
||||
app.ui.info_overlay = crate::state::InfoOverlay::Basic;
|
||||
let photo = app.session.catalog.photos().next().map(|p| (**p).clone()).expect("the demo catalog has photos");
|
||||
(app, photo)
|
||||
}
|
||||
|
||||
/// The overlay's text shapes and the rects it registered, painted on a canvas at the window's corner.
|
||||
fn paint_overlay(app: &LightcraftApp, photo: &lightcraft_catalog::Photo) -> (Vec<egui::epaint::TextShape>, Vec<(String, Rect)>) {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let canvas = Rect::from_min_size(Pos2::ZERO, vec2(1000.0, 700.0));
|
||||
let raw = crate::headless::HeadlessView::raw_input(vec2(1000.0, 700.0), 1.0, 0.0, vec![]);
|
||||
let mut out = ctx.run_ui(raw, |ui| info_overlay(app, ui.painter(), canvas, photo));
|
||||
out.textures_delta.clear();
|
||||
fn texts(shape: &egui::epaint::Shape, out: &mut Vec<egui::epaint::TextShape>) {
|
||||
match shape {
|
||||
egui::epaint::Shape::Text(t) => out.push(t.clone()),
|
||||
egui::epaint::Shape::Vec(v) => v.iter().for_each(|s| texts(s, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut all = Vec::new();
|
||||
out.shapes.iter().for_each(|c| texts(&c.shape, &mut all));
|
||||
(all, crate::widgets::take_registry(&ctx))
|
||||
}
|
||||
|
||||
fn name_top(app: &LightcraftApp, photo: &lightcraft_catalog::Photo) -> f32 {
|
||||
let (texts, _) = paint_overlay(app, photo);
|
||||
texts.iter().filter(|t| t.galley.job.text == photo.file_name).map(|t| t.pos.y).fold(f32::INFINITY, f32::min)
|
||||
}
|
||||
|
||||
/// #2460: in the full-screen preview on macOS the window controls stay at the top-left, so the
|
||||
/// file name starts below them; in macOS full screen they are gone and it keeps the corner.
|
||||
#[test]
|
||||
fn the_file_name_clears_the_macos_window_controls_in_the_full_screen_preview() {
|
||||
let (mut app, photo) = demo_app();
|
||||
app.ui.fullscreen = true;
|
||||
assert_eq!(name_top(&app, &photo), 12.0, "no window controls over the photo");
|
||||
app.integrated_titlebar = true;
|
||||
let bar = Tokens::get(&egui::Context::default()).top_bar_h;
|
||||
assert!(name_top(&app, &photo) >= bar, "below the window controls");
|
||||
app.window_is_fullscreen = true;
|
||||
assert_eq!(name_top(&app, &photo), 12.0, "macOS full screen hides the window controls");
|
||||
app.window_is_fullscreen = false;
|
||||
app.ui.fullscreen = false;
|
||||
assert_eq!(name_top(&app, &photo), 12.0, "the loupe's canvas starts below the top bar");
|
||||
}
|
||||
|
||||
/// #2460: a rated or flagged photo shows its stars and flag under the date and size.
|
||||
#[test]
|
||||
fn the_info_overlay_shows_the_rating_and_flag_under_the_date() {
|
||||
let (_, mut photo) = demo_app();
|
||||
let (app, _) = demo_app();
|
||||
photo.rating = 0;
|
||||
photo.flag = lightcraft_catalog::Flag::None;
|
||||
let badges = |photo: &lightcraft_catalog::Photo| {
|
||||
paint_overlay(&app, photo).1.into_iter().find(|(id, _)| id == "canvas:infoOverlayBadges").map(|(_, r)| r)
|
||||
};
|
||||
assert_eq!(badges(&photo), None, "nothing to show");
|
||||
photo.rating = 3;
|
||||
let stars = badges(&photo).expect("three stars");
|
||||
photo.flag = lightcraft_catalog::Flag::Pick;
|
||||
let flagged = badges(&photo).expect("three stars and a flag");
|
||||
assert!(flagged.width() > stars.width(), "the flag follows the stars: {stars:?} {flagged:?}");
|
||||
let (texts, _) = paint_overlay(&app, &photo);
|
||||
let date_bottom = texts.iter().map(|t| t.pos.y + t.galley.size().y).fold(0.0, f32::max);
|
||||
assert!(flagged.top() >= date_bottom, "under the date and size: {flagged:?}, text ends at {date_bottom}");
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let raw = crate::headless::HeadlessView::raw_input(vec2(1000.0, 700.0), 1.0, 0.0, vec![]);
|
||||
let mut extent = 0.0;
|
||||
ctx.run_ui(raw, |ui| extent = info_overlay_extent(&app, ui.painter(), &photo)).textures_delta.clear();
|
||||
assert!(extent >= flagged.bottom(), "a label below the overlay clears the badges: {extent} < {flagged:?}");
|
||||
photo.rating = 0;
|
||||
photo.flag = lightcraft_catalog::Flag::Reject;
|
||||
assert!(badges(&photo).is_some(), "a rejected photo shows its flag alone");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| LR-VIEW-BEFOREAFTER | Before / after | P0 | ✅ | `cmd:view.showOriginal`, `cmd:view.beforeAfter`, `cmd:view.beforeAfterSplit`, `cmd:view.beforeAfterTopBottom`, `cmd:view.beforeAfterSplitTopBottom`, `cmd:beforeAfter.setBefore`, `cmd:beforeAfter.copyAfterToBefore`, `cmd:beforeAfter.copyBeforeToAfter`, `cmd:beforeAfter.swap`, `cmd:beforeAfter.resetBefore` | all four layouts; before = the import state (defaults + import preset) or a chosen history step / version / the current settings; copy and swap (View → Before/After Settings, History row menu). The chosen before lasts for the session; in side by side and top / bottom, clicks with the white-balance selector, colour samplers and targeted adjustment work in the Before view too (`crates/ui-egui/src/tests_before_after.rs`). Zoomed in, the side by side and stacked views each stay in their pane, with their labels under it. Click zoom animates both panes together, including cached window tiles; input follows the displayed image and render requests use the final size (#1821) |
|
||||
| LR-VIEW-COMPARE | Compare two photos | P1 | ✅ | `cmd:view.compare`, `cmd:compare.swap`, `cmd:compare.makeSelect`, `cmd:compare.set`, `crates/ui-egui/src/panels/compare.rs`, `crates/ui-egui/src/tests_compare.rs` | select / candidate, synced zoom + pan, arrows move the candidate; a filmstrip click puts the photo into the active side (a photo on the other side swaps), so the active photo is always one of the two shown; a ⌘/Ctrl- or Shift-click on the filmstrip changes the selection as in the grid while the two photos shown stay selected and the active photo is unchanged (#187); 1:1 to 8:1 sharp on both sides (zoom windows, see LR-VIEW-ZOOM); no zoom-link toggle; each pane says “Rendering…” (widget `compare:rendering:<pane>`) or “This photo can't be opened” (`compare:renderFailed:<pane>`) as the loupe does |
|
||||
| LR-VIEW-SURVEY | Survey view [Classic] | P2 | ✅ | `cmd:view.survey`, `crates/ui-egui/src/panels/compare.rs` | selection tiled (≤ 48), keys, the bottom bar and the Info panel's stars and colour labels act on the active photo, each tile's caption shows its colour label, hover × removes |
|
||||
| LR-VIEW-INFOOVERLAY | Info overlay on the photo | P1 | ✅ | `cmd:view.infoOverlay`, `cmd:view.viewOptions` | off / Info 1 (by default file name, capture date, size) / Info 2 (file name, exposure, camera + lens), each three lines chosen in View Options; ⌘I cycles (I in full screen; elsewhere I stays the Info panel) |
|
||||
| LR-VIEW-INFOOVERLAY | Info overlay on the photo | P1 | ✅ | `cmd:view.infoOverlay`, `cmd:view.viewOptions`, `crates/ui-egui/src/panels/detail.rs` (`info_overlay`) | off / Info 1 (by default file name, capture date, size) / Info 2 (file name, exposure, camera + lens), each three lines chosen in View Options; ⌘I cycles (I in full screen; elsewhere I stays the Info panel); the star rating and flag show under the lines when the photo has them, and in the full-screen preview on macOS the overlay starts below the window controls (issue #2460) |
|
||||
| LR-VIEW-SLIDESHOW | Slideshow | P2 | ✅ | `cmd:view.slideshow` | View ▸ Slideshow (⌥⌘↩): the photos in view full screen from the selected one, played with the library's playback settings (see LRC-SS-PLAYBACK: fades, random order, repeat, previews prepared ahead; `interval` / any setting overrides one run); Space pauses, ←/→ step (a cut), Esc ends; playback controls on pointer movement |
|
||||
| LR-VIEW-SECONDWINDOW | Second display window [Classic] | P2 | ✅ | `cmd:view.secondWindow`, `crates/ui-egui/src/panels/second.rs` | Window ▸ Second Window (⌘F11): the active photo fitted in its own native window with its own render (a floating panel where there are no native windows); loupe view only (no grid / compare / survey there) |
|
||||
| LR-VIEW-CLIPPING | Clipping indicators | P0 | ✅ | `cmd:view.clipping` | |
|
||||
@@ -798,6 +798,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-22 | minor | LR-VIEW-INFOOVERLAY: the info overlay shows the star rating and flag, and in the full-screen preview on macOS it starts below the window controls (issue #2460); status unchanged |
|
||||
| 2026-10-21 | patch | LR-IMP-ADD-DIALOG: reconcile #2362 with current main; deleted-album guard and hierarchical picker retained; counts unchanged |
|
||||
| 2026-10-20 | minor | LR-IMP-ADD-DIALOG: #2362 gives the import review the shared hierarchy/search album picker while retaining None/New album and existing destination IDs; status and feature counts unchanged |
|
||||
| 2026-10-21 | patch | KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the slider's row; leaving it (even between two frames) or Tab focus alone does not arm them (issue #1355); status unchanged |
|
||||
|
||||
Reference in New Issue
Block a user