From 9b4ba1aa804672acd2222a0604ce0436557b1b1c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 18:29:29 +0000 Subject: [PATCH 1/4] Fix #316: the section eye switches off Light, Color and Detail too The pipeline honoured a section's eye only where it applies Effects, Optics, Geometry and Calibration, so turning the eye off on Light, Color or Detail changed nothing. DevelopSettings::effective() resets the switched-off sections to their defaults (Light with the tone curve; Color with white balance, mixers, grading and point colour; Detail), and both the CPU and GPU renderers render those. Borrowed, so free, when every section is on. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8 --- crates/develop/src/lib.rs | 26 ++++++++++++++++++++++++++ crates/gpu/src/lib.rs | 3 +++ crates/pipeline/src/lib.rs | 3 +++ crates/pipeline/src/tests.rs | 29 +++++++++++++++++++++++++++++ docs/parity.md | 2 +- docs/whats-new.md | 4 ++++ 6 files changed, 66 insertions(+), 1 deletion(-) diff --git a/crates/develop/src/lib.rs b/crates/develop/src/lib.rs index ff4daedd..bfce1893 100644 --- a/crates/develop/src/lib.rs +++ b/crates/develop/src/lib.rs @@ -68,6 +68,32 @@ impl DevelopSettings { !self.disabled_sections.iter().any(|s| s == section) } + /// The settings as rendered: an Edit-panel section whose eye is off contributes nothing (issue + /// #316). Light (with its tone curve), Color (white balance, presence, the mixers, colour grading + /// and point colour) and Detail are reset to their defaults here; Effects, Optics, Geometry and + /// Calibration the pipeline switches off where it applies them. Borrowed when every section is on. + pub fn effective(&self) -> std::borrow::Cow<'_, DevelopSettings> { + let off = |section: &str| !self.section_enabled(section); + if !(off("light") || off("color") || off("detail")) { + return std::borrow::Cow::Borrowed(self); + } + let mut d = self.clone(); + if off("light") { + d.reset_section(Section::Light); + d.reset_section(Section::Curve); + } + if off("color") { + for section in [Section::Color, Section::Mixer, Section::BwMix, Section::Grading, Section::PointColor] { + d.reset_section(section); + } + d.point_colors.clear(); + } + if off("detail") { + d.reset_section(Section::Detail); + } + std::borrow::Cow::Owned(d) + } + pub fn set_section_enabled(&mut self, section: &str, on: bool) { self.disabled_sections.retain(|s| s != section); if !on { diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index 70d6246a..5ef3011b 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -294,6 +294,9 @@ pub fn stage_bytes(stages: &StageCache) -> usize { pub fn render(src: &Arc, info: &SourceInfo, s: &DevelopSettings, req: &RenderRequest, stages: Option<&StageCache>) -> Option { #[cfg(not(target_arch = "wasm32"))] { + // sections switched off with their eye render as if at their defaults (issue #316) + let effective = s.effective(); + let s: &DevelopSettings = &effective; // the kernel writes 8-bit output: high-bit-depth exports (and soft proofs) render on the CPU if !enabled() || req.depth != lightcraft_pipeline::OutputDepth::U8 || req.proof.is_some() { return None; diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index 15fbd7f3..3feaa153 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -365,6 +365,9 @@ enum Src<'a> { } fn render_impl(src: Src<'_>, info: &SourceInfo, s: &DevelopSettings, req: &RenderRequest, cache: Option<&StageCache>) -> Rendered { + // sections switched off with their eye render as if at their defaults (issue #316) + let effective = s.effective(); + let s: &DevelopSettings = &effective; // `Instant::now()` panics on wasm32-unknown-unknown: only read the clock when profiling. let lap = |what: &str, t: &mut Option| { if let Some(t) = t { diff --git a/crates/pipeline/src/tests.rs b/crates/pipeline/src/tests.rs index 61e0de7c..00578bda 100644 --- a/crates/pipeline/src/tests.rs +++ b/crates/pipeline/src/tests.rs @@ -237,6 +237,35 @@ fn exposure_after_spatial_filters_equals_exposing_the_source() { assert!(d <= 2, "max difference {d}"); } +/// A section whose eye is off renders as if it were at its defaults (issue #316): Light, Color +/// and Detail, which the pipeline doesn't switch off where it applies them. +#[test] +fn sections_switched_off_render_as_defaults() { + let src = Rgb32f::from_fn(32, 24, |x, y| [0.05 + x as f32 / 40.0, 0.1 + y as f32 / 30.0, 0.3]); + let info = SourceInfo::default(); + let req = RenderRequest::fit(32, 24); + let plain = render(&src, &info, &DevelopSettings::default(), &req).image; + let mut s = DevelopSettings::default(); + s.light.exposure = 1.5; + s.light.contrast = 40.0; + s.color.saturation = -60.0; + s.color.vibrance = 30.0; + s.detail.sharpen_amount = 120.0; + let edited = render(&src, &info, &s, &req).image; + assert_ne!(edited.data, plain.data); + for section in ["light", "color", "detail"] { + s.set_section_enabled(section, false); + } + assert_eq!(render(&src, &info, &s, &req).image.data, plain.data, "every edited section off = the unedited photo"); + // one section back on brings back only its own adjustments + s.set_section_enabled("light", true); + let mut light_only = DevelopSettings::default(); + light_only.light.exposure = 1.5; + light_only.light.contrast = 40.0; + assert_eq!(render(&src, &info, &s, &req).image.data, render(&src, &info, &light_only, &req).image.data); + assert!(matches!(DevelopSettings::default().effective(), std::borrow::Cow::Borrowed(_)), "nothing is copied when every section is on"); +} + #[test] fn calibration_shifts_colours_but_keeps_greys() { let info = SourceInfo { raw: true, ..Default::default() }; diff --git a/docs/parity.md b/docs/parity.md index d5a31ea9..5aea3bea 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -263,7 +263,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | LR-EDIT-GEOM-GRID | Grid while transforming | P2 | ✅ | `crates/ui-egui/src/panels/detail.rs` | a fine grid over the photo while a geometry slider is dragged | | LR-EDIT-LENSBLUR | Lens blur | P2 | ⬜ | | settings field reserved, not rendered | | LR-EDIT-CALIB | Calibration [Classic] | P1 | ✅ | `ctl:calibration.*` | shadows tint, red/green/blue primary hue and saturation; read/written in XMP | -| LR-EDIT-SECTION-TOGGLE | Section on/off | P1 | ✅ | `cmd:develop.sectionEnabled` | | +| LR-EDIT-SECTION-TOGGLE | Section on/off | P1 | ✅ | `cmd:develop.sectionEnabled`, `crates/develop/src/lib.rs` (`effective`) | the eye on a section header hides its adjustments: Light (with the curve), Color (white balance, presence, mixers, grading, point colour), Effects, Detail, Optics, Geometry, Calibration; masks stay on (issue #316 fixed Light, Color and Detail, which had no effect) | | LR-EDIT-RESET | Reset all / section / slider | P0 | ✅ | `cmd:develop.reset`, `cmd:develop.resetSection`, `cmd:develop.resetControl`, `crates/ui-egui/src/widgets.rs` (double-click) | no "reset to open" | | LR-EDIT-SHOWORIG | Show original | P0 | ✅ | `cmd:view.showOriginal` | | diff --git a/docs/whats-new.md b/docs/whats-new.md index ea0243f4..d70414fd 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -13,6 +13,10 @@ with a counterpart here come along, the rest is listed. - 23 new built-in presets: Portrait, Landscape, Urban, Food, Seasons, Vintage and B&W toners. +### Editing +- The eye on the Light, Color and Detail section headers now hides their adjustments, as it already did for Effects, + Optics, Geometry and Calibration (issue #316). + ### Reliability - LightCraft no longer crashes at launch on Windows PCs whose Vulkan driver is broken (issue #136, e.g. some Intel UHD 630 drivers): on Windows the window and GPU rendering use DirectX 12 only and never load the Vulkan driver unless From 68267810b33fd378bf4b5ce38396443df0c985a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 18:30:43 +0000 Subject: [PATCH 2/4] Fix #316: headless test of the section eye Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8 --- crates/ui-egui/src/headless.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index 976ecd55..fda3f315 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -1053,6 +1053,24 @@ mod tests { assert_ne!(h.app.session.active(), Some(first), "advanced"); } + /// The eye on a section header switches the section off and on again, one undo step each + /// (issue #316). + #[test] + fn section_eye_switches_a_section_off() { + let mut h = demo([1300.0, 900.0]); + let t = Duration::from_secs(10); + h.request("ui.set", json!({"view": "detail", "right": "edit"}), t); + h.settle(SETTLE); + let light_on = |h: &Headless| h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().section_enabled("light"); + for expected in [false, true] { + // the eye shows while the pointer is on the header + assert_eq!(h.request("ui.hoverWidget", json!({"id": "section:light"}), t)["ok"], true); + assert_eq!(h.request("ui.clickWidget", json!({"id": "sectionEye:light"}), t)["ok"], true); + assert_eq!(light_on(&h), expected); + } + assert!(h.app.ui.section_open("light"), "the click didn't fold the section"); + } + /// Return commits a tool panel back to Edit; elsewhere it does nothing. #[test] fn return_commits_the_crop_tool() { From a9cd761d525f25fb8eb5b27047ffa5aebc34e20a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 18:33:25 +0000 Subject: [PATCH 3/4] Fix #316: the section eye stays while the pointer is on it The eye was drawn only while the header itself was hovered; with the pointer on the eye, egui reports the eye (on top) as hovered and not the header, so the eye vanished under the pointer and could not be clicked. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8 --- crates/ui-egui/src/widgets.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index d89a9ce3..907bae10 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -270,8 +270,11 @@ pub fn section_header(ui: &mut Ui, id: &str, title: &str, open: bool, enabled: O paint(p, chev, if open { Icon::ChevronDown } else { Icon::ChevronRight }, t.text_label); p.text(pos2(r.left() + 44.0, r.center().y), Align2::LEFT_CENTER, title, t.semibold(14.0), t.text); let mut toggled = None; + // shown while the pointer is anywhere on the header: `resp.hovered()` turns false as soon as the + // pointer is over the eye itself (the eye is on top), which hid the eye before it could be + // clicked (issue #316) if let Some(on) = enabled - && (resp.hovered() || !on) + && (ui.rect_contains_pointer(r) || !on) { let eye = Rect::from_center_size(pos2(r.right() - 34.0, r.center().y), vec2(18.0, 18.0)); let er = ui.interact(eye, ui.id().with(("eye", id)), Sense::click()); From aaa46003d515698573cae2e9c5c2416e49e534dc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 18:34:35 +0000 Subject: [PATCH 4/4] Fix #316: the header keeps its hover shade while the pointer is on the eye Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8 --- crates/ui-egui/src/widgets.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 907bae10..e5d890d7 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -263,7 +263,7 @@ pub fn section_header(ui: &mut Ui, id: &str, title: &str, open: bool, enabled: O resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::CollapsingHeader, true, open, title)); register(ui.ctx(), format!("section:{id}"), r); let p = ui.painter(); - if resp.hovered() { + if ui.rect_contains_pointer(r) { p.rect_filled(r, 0.0, t.chrome.gamma_multiply(1.06)); } let chev = Rect::from_center_size(pos2(r.left() + 30.0, r.center().y), vec2(14.0, 14.0));