From 3c50fd58e64ae46ac95511344fa9cb22adbd19f5 Mon Sep 17 00:00:00 2001 From: JordyUSA <75485565+JordyUSA@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:02:30 +0700 Subject: [PATCH 1/2] Clippy 1.99: as_chunks for the Fill spot's completion (chunks_exact_to_as_chunks) Rust 1.99's clippy rejects `chunks_exact(3)` with a constant size (`chunks_exact_to_as_chunks`, -D warnings), which failed `cargo xtask ci` on main. Same behaviour: the completed pixels are copied as [f32; 3]. Co-Authored-By: Claude Opus 5.5 --- crates/pipeline/src/spots.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/pipeline/src/spots.rs b/crates/pipeline/src/spots.rs index 483f83f3c..f8b3fbbdf 100644 --- a/crates/pipeline/src/spots.rs +++ b/crates/pipeline/src/spots.rs @@ -247,8 +247,8 @@ fn fill_box(src: &mut [[f32; 3]], tgt: &[[f32; 3]], alpha: &[f32], bw: usize, bh let flat: Vec = tgt.iter().flatten().copied().collect(); let out = lightcraft_inpaint::complete(bw, bh, 3, &flat, &hole, &lightcraft_inpaint::CompleteParams::default()) .unwrap_or_else(|| lightcraft_inpaint::membrane_fill(bw, bh, 3, &flat, &hole)); - for (d, v) in src.iter_mut().zip(out.chunks_exact(3)) { - *d = [v[0], v[1], v[2]]; + for (d, v) in src.iter_mut().zip(out.as_chunks::<3>().0) { + *d = *v; } } From 1dc86d752c82ba28512c17352a75ace777704d2a Mon Sep 17 00:00:00 2001 From: JordyUSA <75485565+JordyUSA@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:18:42 +0700 Subject: [PATCH 2/2] LR-VIEW-HISTOGRAM: the HDR histogram follows slider drags live A drag renders drafts, which have no HDR histogram, so an HDR edit's histogram dropped to the SDR layout until the drag ended. Now, during a drag of an HDR edit, a small full-quality render (Slot::HdrHistogram, 192 px, its own stage cache) runs beside the drafts and feeds the panel at its own pace: the drafts don't wait for it. The panel also keeps an HDR edit's last HDR histogram when a render without HDR bins arrives (a draft, a cached thumbnail or stand-in), so it never swaps to the SDR layout mid-drag. Co-Authored-By: Claude Opus 5.5 --- crates/pipeline/src/lib.rs | 4 ++ crates/ui-egui/src/panels/edit.rs | 69 +++++++++++++++++++++++++++---- crates/ui-egui/src/render.rs | 42 ++++++++++++++++++- docs/parity-checklist.md | 2 +- 4 files changed, 108 insertions(+), 9 deletions(-) diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index 6fbb156e8..27d5d0543 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -471,6 +471,10 @@ pub fn settings_for<'a>(s: &'a DevelopSettings, req: &RenderRequest) -> std::bor /// Long edge (pixels) of the small HDR render [`hdr_histogram`] measures. pub const HDR_HISTOGRAM_EDGE: usize = 384; +/// Long edge of the render that gives a slider drag its live HDR histogram (drafts have none: a +/// second render per tick would slow them down), run beside the drafts by the UI. +pub const HDR_HISTOGRAM_DRAG_EDGE: usize = 192; + /// The histogram of an HDR edit's HDR rendition for a preview request `req`: a small float HDR /// render of the same framing ([`HDR_HISTOGRAM_EDGE`]), binned with SDR on the left half and the /// whole HDR range ([`Hdr::MAX_EV_LIMIT`](lightcraft_develop::Hdr::MAX_EV_LIMIT) stops above SDR diff --git a/crates/ui-egui/src/panels/edit.rs b/crates/ui-egui/src/panels/edit.rs index ed2ee4635..ecd9662de 100644 --- a/crates/ui-egui/src/panels/edit.rs +++ b/crates/ui-egui/src/panels/edit.rs @@ -6,11 +6,13 @@ use egui::{Align2, Color32, Pos2, Rect, RichText, Sense, Stroke, pos2, vec2}; use lightcraft_catalog::PhotoId; use lightcraft_develop::{ControlSpec, DevelopSettings, Section, Track, WbMode, controls}; use lightcraft_geom::Point; +use lightcraft_raster::Histogram; use serde_json::{Value, json}; use crate::LightcraftApp; use crate::icons::{Icon, paint}; use crate::render::Slot; +use crate::state::RightPanel; use crate::theme::Tokens; use crate::widgets::{BAND_COLORS, SliderOut, divider, flyout_row, hex, register, section_header, slider, text_button}; @@ -521,17 +523,44 @@ fn section( // ------------------------------------------------------------------------------ histogram +/// An HDR edit's histogram stays an HDR histogram: some renders carry only their 8-bit image's +/// (a drag's drafts, a cached thumbnail or stand-in), and showing those would swap the panel to +/// the SDR layout and back while a slider moves. Every render with HDR bins replaces the one kept for the photo, so +/// the histogram still follows the edit live; an SDR edit shows what it gets. +fn steady_hdr_histogram(ctx: &egui::Context, id: PhotoId, hdr_edit: bool, hist: Option) -> Option { + let key = egui::Id::new(("hdrHistogram", id)); + match hist { + Some(h) if h.hdr.is_some() => { + ctx.data_mut(|d| d.insert_temp(key, h.clone())); + Some(h) + } + h if hdr_edit => ctx.data(|d| d.get_temp::(key)).or(h), + h => h, + } +} + fn histogram(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { let t = Tokens::get(ui.ctx()); let w = ui.available_width(); - // the loupe render of this photo, else its thumbnail (grid view before the loupe has rendered it) - let hist = app - .renderer - .textures - .get(&Slot::Main) - .filter(|t| t.photo == id) - .and_then(|t| t.histogram.clone()) + let hdr_edit = app.session.develop_of(id).is_some_and(|d| d.hdr.enabled); + // a drag of an HDR edit: its drafts have no HDR histogram, so a small render beside them + // gives one (at its own pace: the drafts don't wait for it) + let interacting = app.session.interaction.is_some(); + if interacting && hdr_edit { + let edge = lightcraft_pipeline::HDR_HISTOGRAM_DRAG_EDGE; + if let Some(job) = app.session.render_job(id, edge, edge, false, app.ui.right != RightPanel::Crop) + && app.renderer.live_hdr_histogram.as_ref().is_none_or(|l| l.1 != job.key) + { + app.renderer.request(Slot::HdrHistogram, job, 95); + } + } + let live = app.renderer.live_hdr_histogram.as_ref().filter(|l| interacting && l.0 == id).map(|l| l.2.clone()); + // else the loupe render of this photo, else its thumbnail (grid view before the loupe has + // rendered it) + let hist = live + .or_else(|| app.renderer.textures.get(&Slot::Main).filter(|t| t.photo == id).and_then(|t| t.histogram.clone())) .or_else(|| app.renderer.textures.get(&Slot::Thumb(id)).and_then(|t| t.histogram.clone())); + let hist = steady_hdr_histogram(ui.ctx(), id, hdr_edit, hist); // an HDR edit's histogram (`pipeline::hdr_histogram`) gets a colour bar and an SDR / HDR label // row under the plot let hdr = hist.as_ref().and_then(|h| h.hdr); @@ -1435,3 +1464,29 @@ fn quick_develop(app: &mut LightcraftApp, ui: &mut egui::Ui, n: usize) { }); }); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn an_hdr_edit_keeps_its_hdr_histogram_through_8_bit_renders() { + let ctx = egui::Context::default(); + let id = PhotoId(7); + let sdr = Histogram::of_hdr(&[], 5.0); + let sdr = Histogram { hdr: None, ..sdr }; + let hdr = |v: f32| Histogram::of_hdr(&[[v, v, v]], 5.0); + // nothing kept yet: what arrives + assert_eq!(steady_hdr_histogram(&ctx, id, true, Some(sdr.clone())), Some(sdr.clone())); + assert_eq!(steady_hdr_histogram(&ctx, id, true, Some(hdr(2.0))), Some(hdr(2.0))); + // a cached thumbnail's 8-bit histogram (or none) doesn't swap the panel to SDR + assert_eq!(steady_hdr_histogram(&ctx, id, true, Some(sdr.clone())), Some(hdr(2.0))); + assert_eq!(steady_hdr_histogram(&ctx, id, true, None), Some(hdr(2.0))); + // the next HDR render replaces it: the histogram follows the slider + assert_eq!(steady_hdr_histogram(&ctx, id, true, Some(hdr(4.0))), Some(hdr(4.0))); + assert_eq!(steady_hdr_histogram(&ctx, id, true, Some(sdr.clone())), Some(hdr(4.0))); + // HDR turned off: the SDR histogram at once; other photos keep their own + assert_eq!(steady_hdr_histogram(&ctx, id, false, Some(sdr.clone())), Some(sdr.clone())); + assert_eq!(steady_hdr_histogram(&ctx, PhotoId(8), true, Some(sdr.clone())), Some(sdr)); + } +} diff --git a/crates/ui-egui/src/render.rs b/crates/ui-egui/src/render.rs index 66bce01ff..4f08e985a 100644 --- a/crates/ui-egui/src/render.rs +++ b/crates/ui-egui/src/render.rs @@ -55,6 +55,9 @@ pub enum Slot { Import(u32), /// The second window's view of the active photo. Second, + /// A slider drag's live HDR histogram: a small full-quality render of an HDR edit, beside the + /// drafts (which have no HDR histogram), with no texture ([`Renderer::live_hdr_histogram`]). + HdrHistogram, } pub struct Tex { @@ -187,6 +190,8 @@ pub struct Renderer { /// Slot → (key, priority) of the request in flight (queued or running). pending: HashMap, pub textures: HashMap, + /// The last [`Slot::HdrHistogram`] result: (photo, job key, histogram). + pub live_hdr_histogram: Option<(PhotoId, u64, Histogram)>, pub last_main_ms: f64, /// Jobs finished since start (for inspect/perf). pub completed: u64, @@ -231,7 +236,7 @@ impl Slot { /// An interactive view of the open photo: slider drags redo only the stages they feed, and a /// draft that finishes late still replaces older pixels. pub fn is_view(self) -> bool { - matches!(self, Slot::Main | Slot::Region | Slot::RegionBefore | Slot::Before | Slot::Hover) + matches!(self, Slot::Main | Slot::Region | Slot::RegionBefore | Slot::Before | Slot::Hover | Slot::HdrHistogram) } } @@ -262,6 +267,7 @@ impl Renderer { seq: 0, pending: HashMap::new(), textures: HashMap::new(), + live_hdr_histogram: None, last_main_ms: 0.0, completed: 0, keep_pixels: false, @@ -708,6 +714,13 @@ impl Renderer { if matches!(slot, Slot::Prefetch(_)) { continue; } + if slot == Slot::HdrHistogram { + if rendered.histogram.hdr.is_some() { + self.live_hdr_histogram = Some((r.photo, r.key, rendered.histogram)); + changed = true; + } + continue; + } if let Slot::ThumbQuick(id) = slot { if self.textures.contains_key(&Slot::Thumb(id)) { continue; // the real thumbnail won the race @@ -1161,6 +1174,33 @@ mod thumbnail_tests { assert!(!app.renderer.is_pending(Slot::Main)); } + #[test] + fn a_drag_gets_its_live_hdr_histogram_beside_the_drafts() { + let (mut app, work, ctx) = fixture(); + let mut jobs = Vec::new(); + for exposure in [1.0, 2.0] { + let mut edit = lightcraft_develop::DevelopSettings::default(); + edit.hdr.enabled = true; + edit.light.exposure = exposure; + app.session.catalog.apply(Op::SetDevelop { id: PhotoId(1), settings: Arc::new(edit), label: "drag".into(), edited: None }).unwrap(); + let job = app.session.render_job(PhotoId(1), 32, 32, false, true).unwrap(); + app.renderer.request(Slot::HdrHistogram, job, 95); + jobs.push(take_job(&work).1); + } + let keys: Vec = jobs.iter().map(|j| j.key).collect(); + let mut jobs = jobs.into_iter(); + let (first, last) = (jobs.next().unwrap(), jobs.next().unwrap()); + // superseded while the drag goes on, it still lands: the histogram keeps moving + finish(&mut app, &work, &ctx, Slot::HdrHistogram, first.run()); + let live = app.renderer.live_hdr_histogram.clone().expect("a live HDR histogram"); + assert_eq!((live.0, live.1), (PhotoId(1), keys[0])); + assert!(live.2.hdr.is_some(), "HDR bins"); + finish(&mut app, &work, &ctx, Slot::HdrHistogram, last.run()); + assert_eq!(app.renderer.live_hdr_histogram.as_ref().map(|l| l.1), Some(keys[1])); + // only the histogram is kept: no texture + assert!(!app.renderer.textures.contains_key(&Slot::HdrHistogram)); + } + #[test] fn library_reset_rejects_old_job_even_when_photo_id_and_key_are_reused() { let (mut app, work, ctx) = fixture(); diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 77ae26e4c..5aa003041 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -138,7 +138,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | LR-VIEW-SLIDESHOW | Slideshow | P2 | ✅ | `cmd:view.slideshow` | View ▸ Slideshow (⌥⌘↩): the photos in view full screen, every 4 s (`interval`), wrapping; Space pauses, ←/→ step, Esc ends | | 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` | | -| LR-VIEW-HISTOGRAM | Histogram | P0 | ✅ | `cmd:view.histogram`, `crates/ui-egui/src/panels/edit.rs`, `crates/pipeline/src/lib.rs` (`hdr_histogram`) | HDR edits: SDR on the left half, the HDR range (0 to 5 stops above SDR white, a fixed scale) on the right with the headroom limit marked; colour bar (white, or Visualize HDR range's colours when that is on) and SDR / HDR labels; no drag-to-adjust on the histogram | +| LR-VIEW-HISTOGRAM | Histogram | P0 | ✅ | `cmd:view.histogram`, `crates/ui-egui/src/panels/edit.rs`, `crates/pipeline/src/lib.rs` (`hdr_histogram`) | HDR edits: SDR on the left half, the HDR range (0 to 5 stops above SDR white, a fixed scale) on the right with the headroom limit marked; colour bar (white, or Visualize HDR range's colours when that is on) and SDR / HDR labels; it follows slider drags live (a small render beside the drafts, `Slot::HdrHistogram`) and never drops to the SDR layout mid-drag; no drag-to-adjust on the histogram | | LR-VIEW-HDR-DISPLAY | HDR display output | P2 | ⬜ | | | ## D. Search & filter (FILT)