Offer the categories in the masks panel
Everything before this was reachable only from an example that writes PPMs. This is the part a photographer can touch: a list under the subjects, click one, get a mask layer for every pixel of that category. ## Under the subjects, and the order is the argument Clicking the photograph is how a local adjustment usually starts, so the things the model *found* come first. A category is the move you reach for deliberately — grade the sky, not this one bird — and putting it second says so without a word of explanation. Coverage is shown for the same reason a subject's score is: it tells the photographer whether a category is worth a click before they spend one finding out. ## Not a new tab, which is what was asked for A develop tab is derived from `Attribute`, not declared — the tabs exist because operations claim an attribute, and no amount of Slint adds one. A seventh attribute would have meant duplicating every adjustment once per category, and the combinatorics get silly by the third. Reached through masks instead, a category composes with every adjustment that already exists, and inherits feather and falloff rather than needing its own. What was described as "per-category sliders with feather and decay" is exactly what this is; only the door is different. ## Verified as far as it can be here Compiles, populates, round-trips, 560 dr-ui tests green. **Not clicked** — synthetic input is blocked on this setup, so how it looks and feels is unverified and wants a human at the window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,7 @@ use slint::{ComponentHandle as _, Model as _, ModelRc, VecModel};
|
||||
|
||||
use crate::develop::{Abandon, DevelopSession, RefinedInstance, Segmented, SessionId};
|
||||
use crate::segmentation;
|
||||
use crate::{sync_rows, AppWindow, GradientHandle, MaskRow, ParamRow, SubjectRow};
|
||||
use crate::{sync_rows, AppWindow, CategoryRow, GradientHandle, MaskRow, ParamRow, SubjectRow};
|
||||
|
||||
/// What the adjust panel's heading says when the controls are global.
|
||||
///
|
||||
@@ -146,12 +146,28 @@ impl Running {
|
||||
}
|
||||
}
|
||||
|
||||
/// A descriptor name as a list label.
|
||||
///
|
||||
/// Only the first letter, because the names in `models/scene/categories.txt`
|
||||
/// are already the words a photographer would use — "sky", "vegetation" — and
|
||||
/// a lookup table would be a second place to edit every time one is added.
|
||||
/// Title case on a multi-word name would be wrong anyway: "Swimming pool", not
|
||||
/// "Swimming Pool".
|
||||
fn category_label(name: &str) -> String {
|
||||
let mut chars = name.chars();
|
||||
match chars.next() {
|
||||
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Push every mask-related property from the session into the window.
|
||||
pub(crate) fn sync(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSession>>>) {
|
||||
let slot = session.borrow();
|
||||
let Some(s) = slot.as_ref() else {
|
||||
window.set_mask_rows(ModelRc::new(VecModel::<MaskRow>::default()));
|
||||
window.set_subject_rows(ModelRc::new(VecModel::<SubjectRow>::default()));
|
||||
window.set_category_rows(ModelRc::new(VecModel::<CategoryRow>::default()));
|
||||
window.set_segmented(false);
|
||||
window.set_adjust_scope(GLOBAL_SCOPE.into());
|
||||
clear_handles(window);
|
||||
@@ -193,6 +209,19 @@ pub(crate) fn sync(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSessio
|
||||
.collect();
|
||||
window.set_subject_rows(ModelRc::new(VecModel::from(subjects)));
|
||||
|
||||
// Already largest-first from the precompute, and already filtered to the
|
||||
// ones with enough coverage to be worth a control.
|
||||
let categories: Vec<CategoryRow> = s
|
||||
.categories()
|
||||
.iter()
|
||||
.map(|c| CategoryRow {
|
||||
name: c.name.as_ref().into(),
|
||||
label: category_label(&c.name).into(),
|
||||
coverage: c.coverage,
|
||||
})
|
||||
.collect();
|
||||
window.set_category_rows(ModelRc::new(VecModel::from(categories)));
|
||||
|
||||
window.set_segmented(s.has_segmentation());
|
||||
window.set_adjust_scope(scope_label(s).into());
|
||||
sync_handles(window, s);
|
||||
@@ -636,6 +665,22 @@ pub(crate) fn wire(
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
let rows = rows.clone();
|
||||
window.on_add_category_mask(move |name| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.add_category_mask(&name);
|
||||
}
|
||||
sync(&w, &session);
|
||||
sync_rows(&w, &rows, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
@@ -796,6 +841,7 @@ pub(crate) fn reset(window: &AppWindow) {
|
||||
window.set_segmented(false);
|
||||
window.set_mask_rows(ModelRc::new(VecModel::<MaskRow>::default()));
|
||||
window.set_subject_rows(ModelRc::new(VecModel::<SubjectRow>::default()));
|
||||
window.set_category_rows(ModelRc::new(VecModel::<CategoryRow>::default()));
|
||||
window.set_adjust_scope(GLOBAL_SCOPE.into());
|
||||
clear_handles(window);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user