Merge PR #2792: LRC-DEV-VIEWOPTIONS: View Options can turn off the loupe's "Rendering…" message

This commit is contained in:
aiongg
2026-10-25 02:24:50 +00:00
7 changed files with 109 additions and 8 deletions
+1
View File
@@ -2336,6 +2336,7 @@
"View Options": "Opzioni di visualizzazione",
"Loupe View": "Vista lente",
"Show Info Overlay": "Mostra sovrapposizione informazioni",
"Show message when loading or rendering photos": "Mostra un messaggio durante il caricamento o il rendering delle foto",
"Loupe Info 1": "Info lente 1",
"Loupe Info 2": "Info lente 2",
"Use Defaults": "Usa predefiniti",
+63
View File
@@ -5017,6 +5017,69 @@ mod tests {
assert!(!h.app.renderer.is_pending(Slot::Main));
}
/// View Options' Show message when loading or rendering photos (on by default): turned off in
/// the window, the loupe does not say "Rendering…" while an edit renders; `view.viewOptions
/// {loupeMessages}` turns it back on for an agent (refusing a value that is not true or false).
#[test]
fn view_options_turn_the_rendering_message_off() {
use crate::render::Slot;
use std::{cell::Cell, rc::Rc};
let pill = |h: &Headless| h.app.widgets.iter().any(|(w, _)| w == "loupe:rendering");
let mut h = demo([1000.0, 700.0]);
let t = Duration::from_secs(10);
h.request("ui.set", json!({"view": "detail", "right": "none"}), t);
h.settle(SETTLE);
assert!(h.app.ui.settings.loupe_messages, "on by default");
assert_eq!(h.request("ui.key", json!({"key": "j", "cmd": true}), t)["ok"], true);
h.step();
assert_eq!(h.request("ui.clickWidget", json!({"id": "check:viewOptions.loupeMessages"}), t)["ok"], true);
h.step();
assert!(!h.app.ui.settings.loupe_messages, "the tick box turns it off");
h.app.ui.dialog = None;
let open = Rc::new(Cell::new(false));
h.app.renderer.set_offload(Box::new(super::RenderGate::new(open.clone())));
h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), t);
for _ in 0..45 {
h.step();
}
assert!(h.app.renderer.is_pending(Slot::Main), "the edit's render is held");
assert!(!pill(&h), "no message while it is off");
let r = h.request("engine.execute", json!({"command": "view.viewOptions", "params": {"loupeMessages": "yes"}}), t);
assert_eq!(r["ok"], false, "{r}");
assert!(!h.app.ui.settings.loupe_messages, "a refused change changes nothing");
let r = h.request("engine.execute", json!({"command": "view.viewOptions", "params": {"loupeMessages": true}}), t);
assert_eq!(r["result"]["loupeMessages"], true, "{r}");
assert_eq!(h.app.ui.dialog, None, "a value sets it without opening the window");
assert!(h.step_until(SETTLE, pill), "the message shows again while the render is held");
open.set(true);
assert!(h.step_until(SETTLE, |h| !pill(h)), "gone once the render lands");
}
/// With nothing to show yet, the loupe says "Rendering…" in its middle only while View Options'
/// Show message when loading or rendering photos is on.
#[test]
fn an_empty_loupe_says_rendering_only_with_the_message_on() {
use std::{cell::Cell, rc::Rc};
for on in [true, false] {
let mut h = demo([1000.0, 700.0]);
let t = Duration::from_secs(10);
h.app.ui.settings.loupe_messages = on;
h.app.renderer.set_offload(Box::new(super::RenderGate::new(Rc::new(Cell::new(false)))));
h.request("ui.set", json!({"view": "detail", "right": "none"}), t);
// drop every texture (quick previews and thumbnails land outside the gate) until a
// frame has nothing to show
let empty = (0..60).any(|_| {
h.app.renderer.textures.clear();
h.step();
h.app.loupe_shown.map(|l| l.1) == Some("none")
});
assert!(empty, "a frame with nothing rendered");
let said = h.painted_text().iter().any(|text| text == "Rendering…");
assert_eq!(said, on, "message on: {on}");
}
}
/// A render that fails while the loupe shows a stand-in (the embedded JPEG, a thumbnail) left
/// the stand-in up as if it were the photo: the loupe says the photo can't be opened, with the
/// error on hover, and keeps saying so while a library change retries it.
+16 -3
View File
@@ -751,10 +751,22 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
Ok(json!({"infoOverlay": app.ui.info_overlay}))
}
"view.viewOptions" => {
// `info1` / `info2` set the lines, and `briefly` sets the Info that Show Briefly When Photo
// Changes shows, for an agent; without them the window opens
// `info1` / `info2` set the lines, `briefly` the Info that Show Briefly When Photo Changes
// shows and `loupeMessages` Show message when loading or rendering photos, for an agent;
// without any of them the window opens
let mut info = app.ui.settings.loupe_info;
let mut set = false;
// `loupeMessages` sets Show message when loading or rendering photos
let messages = match p.get("loupeMessages") {
None => app.ui.settings.loupe_messages,
Some(v) => match v.as_bool() {
Some(on) => {
set = true;
on
}
None => return Some(Err("`loupeMessages`: true|false".into())),
},
};
for (key, lines) in [("info1", &mut info.info1), ("info2", &mut info.info2)] {
let Some(v) = p.get(key) else { continue };
match serde_json::from_value(v.clone()) {
@@ -774,10 +786,11 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
}
if set {
app.ui.settings.loupe_info = info;
app.ui.settings.loupe_messages = messages;
} else {
app.ui.dialog = Some(Dialog::ViewOptions);
}
Ok(json!({"loupeInfo": app.ui.settings.loupe_info}))
Ok(json!({"loupeInfo": app.ui.settings.loupe_info, "loupeMessages": app.ui.settings.loupe_messages}))
}
"view.chooseLayoutImage" => {
let path = match p.get("path").and_then(Value::as_str) {
+5 -2
View File
@@ -844,9 +844,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
);
p.text(canvas.center() + vec2(0.0, 12.0), Align2::CENTER_CENTER, e, t.font(12.0), t.text_dim);
}
None => {
None if app.ui.settings.loupe_messages => {
p.text(canvas.center(), Align2::CENTER_CENTER, crate::i18n::tr("Rendering…"), t.font(13.0), t.text_dim);
}
None => {}
}
}
}
@@ -998,9 +999,11 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
let over = if over.is_positive() { over } else { canvas };
match failed {
Some(e) => failed_pill(ui, app, &photo, canvas, over, nav, "loupe:renderFailed", e),
None => {
// View Options' Show message when loading or rendering photos
None if app.ui.settings.loupe_messages => {
loupe_pill(ui, canvas, over, nav, "loupe:rendering", crate::i18n::tr("Rendering…"), false);
}
None => {}
}
}
if let Some(why) = photo.preview_only.as_deref().filter(|_| !fullscreen) {
+5 -2
View File
@@ -870,8 +870,9 @@ fn interface_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
}
/// View ▸ View Options (⌘J), Lightroom Classic's Loupe View tab: whether the info overlay shows and
/// which, the three lines each of Info 1 and Info 2 shows, and which of them shows briefly when the
/// photo changes (`view.viewOptions`). Changes apply as they are made.
/// which, the three lines each of Info 1 and Info 2 shows, which of them shows briefly when the
/// photo changes, and whether the loupe shows its loading and rendering message
/// (`view.viewOptions`). Changes apply as they are made.
pub(super) fn view_options(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
use crate::state::{InfoOverlay as I, LoupeInfo, LoupeInfoField as F};
heading(ui, t, crate::i18n::tr("Loupe View"));
@@ -913,6 +914,8 @@ pub(super) fn view_options(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Token
app.ui.settings.loupe_info.briefly = if briefly { info } else { I::Off };
}
}
heading(ui, t, crate::i18n::tr("General"));
check(ui, "viewOptions.loupeMessages", &mut app.ui.settings.loupe_messages, "Show message when loading or rendering photos");
}
// ----------------------------------------------------------------------------------- Slideshow
+18
View File
@@ -483,6 +483,9 @@ pub struct AppSettings {
pub film_badges: bool,
/// Grid: when to show the rating / flag / edited badges.
pub grid_badges: GridBadges,
/// View ▸ View Options ▸ Show message when loading or rendering photos: the loupe says
/// "Rendering…" while the photo it shows is not the finished render.
pub loupe_messages: bool,
/// Shortcuts the user changed (Help ▸ Keyboard Shortcuts): command id → shortcut, `""` = none.
pub keymap: crate::shortcuts::Keymap,
/// Auto (follow the system), Dark or Light.
@@ -548,6 +551,7 @@ impl Default for AppSettings {
film_names: true,
film_badges: true,
grid_badges: GridBadges::Auto,
loupe_messages: true,
keymap: Default::default(),
appearance_mode: AppearanceMode::Dark,
dark_theme: DarkTheme::Charcoal,
@@ -1747,4 +1751,18 @@ mod loupe_info_tests {
assert_eq!(newer.loupe_info, LoupeInfo::default());
assert!(!newer.film_names, "the rest of the settings are still read");
}
/// Show message when loading or rendering photos is on until it is turned off, and is saved
/// with the app's settings.
#[test]
fn the_loading_message_choice_is_saved_with_the_settings() {
assert!(AppSettings::default().loupe_messages);
let old: AppSettings = serde_json::from_value(serde_json::json!({"filmNames": false})).unwrap();
assert!(old.loupe_messages, "a settings file from before the choice existed keeps the message");
let settings = AppSettings { loupe_messages: false, ..AppSettings::default() };
let saved = serde_json::to_value(&settings).unwrap();
assert_eq!(saved["loupeMessages"], false);
let read: AppSettings = serde_json::from_value(saved).unwrap();
assert!(!read.loupe_messages);
}
}
+1 -1
View File
@@ -730,7 +730,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06-
| LRC-DEV-TAT | Targeted adjustment tools | P1 | ✅ | `cmd:develop.targeted` (`target`: curve / hue / sat / lum) | |
| LRC-DEV-DEFAULTS | Per-camera raw defaults | P1 | ✅ | `cmd:library.preferences` (`camera`, `import.perCamera`), `crates/ui-egui/src/panels/settings.rs` | Settings → Import: raw default and per-camera presets |
| LRC-DEV-PROCESS | Process versions, Update to Current Process | P1 | ✅ | `cmd:develop.updateProcess`, `cmd:develop.reset`, `cmd:develop.get`, `crates/develop/src/settings.rs`, `crates/pipeline/src/finish.rs`, `docs/process-versions.md` | each photo records the rendering process its settings are interpreted by: settings saved before this existed load as V1, new photos and Reset get the latest, virtual copies inherit, copy / paste / sync / presets keep each photo's own; Photo ▸ Update to Current Process (one undo step, enabled while a selected photo is on an older process); a number from a newer LightCraft is kept and renders with the newest known process. Only V1 exists so far: this is what lets a retuned default tone (#146) or stronger Light sliders (#196) ship without changing existing edits |
| LRC-DEV-VIEWOPTIONS | Develop view options | P2 | 🟡 | `cmd:view.viewOptions`, `crates/ui-egui/src/panels/settings.rs` (`view_options`) | View ▸ View Options… (⌘J), the Loupe View part: Show Info Overlay with Info 1 / Info 2, and the three lines of each (none, file name, capture date, dimensions, exposure, camera, lens, camera + lens) with Use Defaults; Show Briefly When Photo Changes under each Info (one at most): with the overlay off, that Info shows for 3 s each time the loupe changes photo; applied as they change, saved with the app settings; `view.viewOptions {info1, info2, briefly}` sets them for agents. Not yet: the Grid View tab, more fields (ratings, metadata, copy name), the loading-message and video-frame options |
| LRC-DEV-VIEWOPTIONS | Develop view options | P2 | 🟡 | `cmd:view.viewOptions`, `crates/ui-egui/src/panels/settings.rs` (`view_options`) | View ▸ View Options… (⌘J), the Loupe View part: Show Info Overlay with Info 1 / Info 2, and the three lines of each (none, file name, capture date, dimensions, exposure, camera, lens, camera + lens) with Use Defaults; Show Briefly When Photo Changes under each Info (one at most): with the overlay off, that Info shows for 3 s each time the loupe changes photo; General's Show message when loading or rendering photos (on by default; off, the loupe does not say "Rendering…"); applied as they change, saved with the app settings; `view.viewOptions {info1, info2, briefly, loupeMessages}` sets them for agents. Not yet: the Grid View tab, more fields (ratings, metadata, copy name), the video-frame option |
| LRC-DEV-LOUPEOVERLAY | Loupe overlay: layout image | P2 | 🟡 | `cmd:view.chooseLayoutImage`, `cmd:view.layoutImage`, `cmd:view.layoutImageOpacity`, `cmd:view.layoutImageMatte`, `crates/ui-egui/src/panels/layout_overlay.rs` | View ▸ Loupe Overlay: a PNG with transparency (a cover layout, a banner's safe areas) fitted and centred over the loupe photo, following zoom and pan; opacity and matte (the photo darkened outside the graphic's box) in steps; ⌥⌘O shows / hides it; read off the UI thread; saved with the UI state. Not yet: the Grid and Guides overlays, moving and resizing the layout with ⌘ held, free opacity / matte sliders |
| LRC-DEV-VIDEO | Video frame capture | P2 | ⬜ | | |
| LRC-DEV-HISTOGRAM-DRAG | Drag on the histogram to adjust tone regions | P2 | ✅ | `crates/ui-egui/src/panels/histogram.rs`, `crates/ui-egui/src/panels/edit.rs`, `crates/develop/src/controls.rs`, `crates/ui-egui/src/tests_histogram_drag.rs`, `cmd:develop.set`, `cmd:develop.beginInteraction`, `cmd:develop.endInteraction`, `ctl:light.blacks`, `ctl:light.shadows`, `ctl:light.exposure`, `ctl:light.highlights`, `ctl:light.whites` | five zones across the plot's width, each moving the Basic control it names: Blacks 0.00–0.15, Shadows 0.15–0.35, Exposure 0.35–0.65, Highlights 0.65–0.85, Whites 0.85–1.00; every step is a `develop.set` between `develop.beginInteraction` and `develop.endInteraction` (the sliders' own path), so the draft render follows the drag live and the whole drag lands as one undo step; the zone the press landed in stays locked for the drag, so dragging across the others changes nothing; sideways travel of the plot's width sweeps that control's whole range (Shift a tenth, as on the sliders), clamped to its spec; hovering a zone tints it and shows its value in the strip under the plot ("Exposure: +0.45 EV"), with the sideways-resize cursor; a double-click resets that zone's control to its default in one undo step; the strip under the plot (readout / EXIF) and the clipping buttons in the plot's top corners are not zones |