Offer the lens profile as a tick box, since applying it silently reads as absent

The develop panel's Optics group is three manual sliders: distortion, chromatic
aberration and lens vignetting. The automatic correction was already there — the
file's EXIF lens is matched against the bundled Lensfun database on open and the
coefficients are fanned out to all three — but nothing in the interface said so
except a line of grey text under the camera reading "· corrected", and there was
no way to decline it. From the outside that is indistinguishable from the
feature not existing, which is how it was read.

`dr-lens` states the rule this breaks: an automatic correction that silently
does nothing is worse than one the user can see is unavailable. The caption
satisfied the letter of it and not the point — a photographer looking for
"apply the lens profile" found three sliders and no switch.

So the profile is now a control. It is a capability rather than a flag on the
session, because everything a photographer sets travels one road: the capability
list feeds the generated panel, `Preset` captures it, the sidecar stores it and
the undo stack replays it. A bool on the side would have needed adding to each
of those four by hand and would have been forgotten in at least one — which is
exactly how the mask stack came to be missing from the history.

It is on by default, which is what `switch_on` is for: the coefficients are a
measurement of the lens that took the photograph, so accepting them is neutral
and declining them is the edit. The sidecar therefore stores nothing for the
ordinary case and the correction still happens.

The switch appears only where a profile was matched. A tick box on a photograph
whose lens the database has never heard of would be a control that looks
available and does nothing, which is the failure the rule above names rather
than an instance of following it — those photographs are told "· no profile" in
words instead, and one whose box is unticked now says "· profile off", which is
a third fact and not either of the other two.

Two things had to be built underneath. `ParamKind::Bool` was in the core's
closed enum and mapped to a row kind here, and had no control behind it in
`adjust.slint`: a parameter declaring itself a switch was flattened into a row
that drew nothing at all. Nothing shipped had one until now, so the gap cost
nothing and was invisible. And `Check` self-toggled, which is right for a
settings page that owns its value and wrong for a panel row that is a view of
the edit graph — the click would have answered by replacing the binding with a
literal, and the next undo or pasted preset would have moved the value with the
tick left where the finger put it. It now takes `controlled`, and the generated
row uses it.

The manual sliders are unchanged and still trim whatever the profile leaves, so
switching it off is "correct this by hand" rather than "stop correcting".
This commit is contained in:
2026-09-07 00:27:47 +02:00
parent 2cd49d1cb7
commit af89433aee
9 changed files with 571 additions and 23 deletions
+60 -2
View File
@@ -59,9 +59,9 @@
//! wearing the same lens, which defeats the point of a lens profile.
use std::fmt::Write as _;
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use crate::descriptor::{OpDescriptor, ParamId};
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
use crate::operation::{Helper, Uniform};
/// Lateral chromatic aberration, as a per-channel radial scale.
@@ -90,6 +90,64 @@ pub struct LensProfile {
pub vignetting: Option<crate::ops::vignetting::Pa>,
}
/// TRACES: FR-DEV-3
/// The lens profile itself, as one switch a photographer can reach.
///
/// **Not an operation and not a warp** — it corrects nothing on its own. It
/// is the answer to "use the measured profile for this lens, or not", and the
/// three corrections that read the coefficients ([`crate::ops::Distortion`],
/// [`crate::ops::Aberration`] and the vignetting node) are where the
/// correction actually happens. Framing is in the capability list on the same
/// terms: something a photographer sets which is not an `Operation`.
///
/// # Why this exists at all
///
/// The profile arrives from the file's EXIF and a database, and applying it
/// silently was the whole of the interface for it. That reads as the feature
/// being absent: the corrections in the panel are manual sliders, the
/// automatic part is a line of grey text under the camera, and nothing
/// anywhere says "this is on and you may turn it off". `dr_lens`'s honesty
/// rule — an automatic correction the user cannot see is worse than one they
/// can see is unavailable — asks for a control here, not just a caption.
///
/// # Why it is a parameter rather than a flag on the session
///
/// Everything a photographer sets travels by one road: the capability list
/// feeds the generated panel, [`crate::Preset`] captures it, the sidecar
/// stores it and the undo stack replays it (FR-DEV-3c). A `bool` on the
/// session would have needed its own place in each of those four, and would
/// have been forgotten in at least one — which is exactly how the mask stack
/// came to be missing from the history. As a parameter it is in all of them
/// with nothing registered.
pub mod profile_switch {
use super::*;
pub const ID: OpId = OpId("lens_profile");
pub const APPLY: ParamId = ParamId("apply");
/// On by default: the profile is a measurement of the lens that took the
/// photograph, so applying it is the neutral state and declining it is
/// the edit. See [`ParamDescriptor::switch_on`].
pub(crate) static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
Arc::new(OpDescriptor {
id: ID,
label: LocalizedKey("op.lens_profile"),
params: vec![ParamDescriptor::switch_on(
"apply",
"param.lens_profile.apply",
)],
// Optics, so it sits with the corrections it drives rather than in
// a group of its own.
attributes: vec![Attribute::Optics],
})
});
/// This switch's description, for a caller building a control from it.
pub fn descriptor() -> Arc<OpDescriptor> {
DESCRIPTOR.clone()
}
}
/// A coordinate-domain operation, applied before the source is sampled.
///
/// Object-safe for the same reason [`crate::operation::Operation`] is: the