Group the panel by what operations say they are about

A strip of groups over the adjust panel — Light, Colour, Detail — derived
from the attributes the operations declare. `adjust.slint` names none of
them: the strings arrive resolved and the panel only draws them, so a new
operation joins the right group by saying what it is and this file does not
change (FR-DEV-3a).

A group nothing carries is not offered, so a tab never opens onto nothing.
Geometry is left out because its one operation prefers an on-canvas widget
and is skipped by the row builder — a Geometry tab would be empty while
`GeometryPanel` holds the real controls. The strip appears only when there
is more than one group to choose between; a single tab is a control with one
option.

The selected group is underlined rather than filled. The accent means
*modified* everywhere else in this interface, and spending it on "which tab"
would blunt the one signal the panel has.

**The trap, and it nearly bit again.** `op_index` on a row counts over every
capability, not over the ones a filter kept — it is how a row routes back to
the core. Renumbering it while filtering would make a slider drive a
different operation, which looks like a rendering fault rather than a
routing one. `rows_filtered` keeps `enumerate` over the full list and only
`group_head` is a position within the emitted rows; a test moves a value
through a filtered row and checks it lands where it was asked to.

Six tests, including that a nonsense index falls back to showing everything
rather than to showing nothing.
This commit is contained in:
2026-08-22 10:22:47 +02:00
parent 7421837c8a
commit 924a837389
6 changed files with 344 additions and 15 deletions
+53
View File
@@ -383,3 +383,56 @@ fn an_untouched_layer_is_left_alone() {
assert!(ours.merge(&theirs, Some(&base)).is_empty());
assert_eq!(ours.masks.len(), 2, "our new layer is not a conflict");
}
/// Print a real sidecar carrying a subject mask, for eyeballing the format.
///
/// Ignored because it asserts nothing — it exists so the on-disk shape can be
/// looked at without reverse-engineering it from the writer.
///
/// ```sh
/// cargo test -p dr-pipeline --test mask_sidecar show_a_sidecar -- --ignored --nocapture
/// ```
#[test]
#[ignore = "prints the format rather than checking it"]
fn show_a_sidecar() {
let mut graph = EditGraph::default_chain();
graph.set_param(dr_pipeline::OpId("exposure"), ParamId("exposure"), 0.35);
let mut subject = MaskLayer::new(
"m1",
MaskSource::Subject {
signature: 0x9f2c_41aa,
index: 0,
class: "person".into(),
score: 0.94,
},
);
subject.name = "person".into();
subject.invert = true;
subject.feather = 0.02;
subject.falloff = dr_pipeline::mask::Falloff::Gaussian;
subject.morphology = dr_pipeline::mask::Morphology::Dilate;
subject.morph_radius = 0.012;
subject.set_param("saturation", ParamId("saturation"), -100.0);
subject.set_param("exposure", ParamId("exposure"), -0.4);
graph.masks_mut().push(subject);
let mut grad = MaskLayer::new(
"m2",
MaskSource::Linear {
centre: (0.5, 0.25),
angle: 1.5708,
width: 0.4,
},
);
grad.set_param("exposure", ParamId("exposure"), -0.6);
graph.masks_mut().push(grad);
let mut sidecar = Sidecar::new();
let mut v = Version::from_graph("default", "Default", &graph);
v.is_default = true;
v.rating = 4;
sidecar.put(v);
println!("\n{}", sidecar.to_text());
}