Offer the film stock in its own group, and let its list scroll itself

Two faults in one control, both reported from the tablet.

The stock picker appeared in every group. It is not a parameter, so it is not
a row, so the filter that hides every other control when a group is chosen
never saw it — "Kodachrome" sat at the top of Light, of Colour and of Detail
alike. Three places it does not belong, and the one it does no more prominent
than the rest. The descriptor has said `Effect` and only `Effect` since the
film moved there; nothing was asking it.

So the panel now asks. It cannot ask directly — a generated panel may not know
which operation a control belongs to — so the session answers, from what the
operation declares it is about, and a stock re-declared as something else would
move on its own. The flag is recomputed when the group changes as well as when
the film does, which is the half that would have made it stale exactly when it
mattered.

And the open list was unbounded, so it made the develop column taller and the
column scrolled as one: reaching Velvia dragged every slider below it off the
screen, an answer given once pushing aside the controls used constantly. It now
scrolls within a bounded height of its own.

That viewport is counted rather than measured, for the reason the tool rail
records a few files away: a viewport that asks a layout how tall it wants to be,
while the layout takes its height from the viewport, is a cycle Slint settles by
handing back the height it was given — and the content is then clipped in
silence rather than scrolling. Every row here is one fixed height, so
multiplying is exact.

The group rule has a test. The scrolling does not, and cannot: it is a layout,
and a layout fault is invisible to the compiler and to every assertion that can
be written about it.
This commit is contained in:
2026-09-07 20:42:15 +02:00
parent 4c217c9be6
commit 2d878c2117
5 changed files with 231 additions and 64 deletions
+65
View File
@@ -1093,6 +1093,31 @@ impl DevelopSession {
.map_or(-1, |i| i as i32)
}
/// TRACES: FR-DEV-3f
/// Whether the film stock belongs in the group currently on screen.
///
/// The stock is not a parameter, so it is not a [`ParamRow`] and the tab
/// filter that hides every other control never reached it: the picker was
/// drawn above the rows in *every* group, so "Kodachrome" sat at the top of
/// Light, of Colour and of Detail alike. Three places it does not belong,
/// and the one it does was no more prominent than the rest.
///
/// Answered here rather than in the panel because it is a question about
/// the operation — what is this control *about* — and the panel is not
/// allowed to know. It asks the descriptor, so a stock that were ever
/// re-declared as something other than an effect would move on its own.
pub fn film_in_group(&self) -> bool {
let Some(active) = self.active_tab else {
// "All" shows everything, the stock included.
return true;
};
self.graph
.capabilities()
.iter()
.find(|c| c.id == dr_pipeline::ops::film_sim::ID)
.is_some_and(|c| c.attributes.contains(&active))
}
/// Select a tab by its index in [`Self::tabs`], or `-1` for all.
pub fn set_active_tab(&mut self, index: i32) {
self.active_tab = usize::try_from(index)
@@ -7757,6 +7782,46 @@ mod tests {
/// Asserted over the real chain rather than a fixture, because the failure
/// was a property of what is actually declared: a fixture would have to be
/// written to reproduce it and would then only prove itself.
/// TRACES: FR-DEV-3f
/// The film stock is offered in its own group and in "All", nowhere else.
///
/// It is not a parameter, so it is not a row, so the filter that hides
/// every other control when a group is chosen never saw it: the picker sat
/// at the top of Light, of Colour and of Detail alike. Three places it does
/// not belong, and the one it does no more prominent than the rest.
///
/// Asserted against whatever the operation actually declares rather than
/// against a named group, so a stock re-declared as something else moves
/// here on its own and this test still describes the rule.
#[test]
fn the_film_stock_is_offered_only_where_it_belongs() {
let Some(ctx) = headless() else { return };
let (mut session, _) = grey_session(&ctx);
let film = session
.graph
.capabilities()
.into_iter()
.find(|c| c.id == dr_pipeline::ops::film_sim::ID)
.expect("the film stock is in the chain");
session.set_active_tab(-1);
assert!(
session.film_in_group(),
"\"All\" hides nothing, so the stock is offered there"
);
for (i, (attribute, label)) in session.tabs().into_iter().enumerate() {
session.set_active_tab(i as i32);
let belongs = film.attributes.contains(&attribute);
assert_eq!(
session.film_in_group(),
belongs,
"the stock is offered in {label} but the operation does not claim it"
);
}
}
#[test]
fn a_lone_parameter_is_named_after_its_operation() {
let graph = EditGraph::default_chain();
+14
View File
@@ -815,6 +815,14 @@ pub(crate) fn sync_film(window: &AppWindow, session: &Rc<RefCell<Option<DevelopS
})
.unwrap_or(0);
// Whether the picker belongs in the group on screen. Asked of the session
// rather than decided here: it is a fact about what the operation is
// about, and the answer moves if the descriptor ever does.
let in_group = session
.borrow()
.as_ref()
.is_none_or(|s| s.film_in_group());
let can_print = choices
.get(selected)
.and_then(|(id, _)| *id)
@@ -830,6 +838,7 @@ pub(crate) fn sync_film(window: &AppWindow, session: &Rc<RefCell<Option<DevelopS
.collect::<Vec<_>>(),
)));
adjustments.set_film_selected(selected as i32);
adjustments.set_film_in_group(in_group);
adjustments.set_film_can_print(can_print);
adjustments.set_film_print(chosen.map(|(_, print)| print).unwrap_or(false));
}
@@ -2711,6 +2720,11 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
s.set_active_tab(index);
}
sync_rows(&w, &rows, &session);
// The stock picker is filtered by group like everything else, and
// it is not a row — so the thing that decides whether it is on
// screen has to be recomputed here as well, or it would answer for
// whichever group happened to be open when the image was loaded.
sync_film(&w, &session);
});
}