Let a paste carry some kinds of edit and not others

FR-DEV-6 asks for presets "covering a subset of the edit graph". What
landed with the named presets covered two subsets: everything, and
everything but the crop. "Match the colour but not the sharpening" had
no way to be said.

`Scope` is now a set of `Attribute` — the same six kinds every operation
already declares and the develop panel already builds its tabs from. The
photographer ticking "tone and colour" is naming the groups they
navigate by, and neither this module nor the interface has to name an
operation to do it (FR-DEV-3c).

The pleasing part is what left. Framing used to be excluded by an
explicit test against one operation's id; it is now excluded because
Geometry is not in the default set. The special case dissolved into the
general rule, and the argument for it — a crop is a decision about *this*
photograph, and carrying it across forty destroys forty compositions —
is now a statement about a kind of edit rather than about a node. All
thirty-three existing preset tests pass unchanged, which is the evidence
that the generalisation kept its promises.

One decision that is a field rather than a rule, because the two cases
genuinely differ. An operation this build cannot classify — from a newer
version, arriving over sync — travels under "everything" and "everything
but the crop", because those are claims about the whole edit and an
unrecognised operation is part of it (FR-NC-8). It does not travel under
a hand-picked set, because that is a claim about kinds, and an unknown
kind is not one of the kinds that were ticked.

The settings page's "Copy crop and rotation" checkbox is gone, replaced
by the same chips the preset sheet draws. It asked the right first
question — geometry is the kind whose accidental travel destroys work —
but it was the only question a boolean could ask. The field stays in
`Settings`, read exactly once to seed the new set, so anyone who had
ticked it keeps their behaviour.

The chips are deliberately not in the develop column. Six of them there
would set the width of the whole sidebar, which is the bug `ChipGrid`'s
comment records at length.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 20:38:19 +02:00
co-authored by Claude Opus 5
parent eb39599f12
commit bb35665bd2
11 changed files with 689 additions and 133 deletions
+6 -16
View File
@@ -111,7 +111,12 @@ impl SettingsController {
/// accidentally write back a stale copy of the fields it was not editing —
/// with a save on every keystroke, two controls holding their own snapshots
/// would overwrite each other.
fn edit(&self, f: impl FnOnce(&mut Settings)) {
///
/// `pub(crate)` rather than private because the settings page is no longer
/// the only surface that writes a preference: the scope chips are drawn
/// here *and* in the preset sheet, and both change one stored set. The
/// closure is what makes that safe, which is why the shape stays.
pub(crate) fn edit(&self, f: impl FnOnce(&mut Settings)) {
{
let mut settings = self.settings.borrow_mut();
f(&mut settings);
@@ -145,7 +150,6 @@ pub fn render(window: &AppWindow, controller: &SettingsController) {
window.set_settings_thumbnail_budget(budget::label(s.cache.thumbnail_budget_bytes).into());
window.set_settings_thumbnail_unlimited(s.cache.thumbnail_budget_bytes.is_none());
window.set_settings_keep_opened(s.cache.keep_opened_originals);
window.set_settings_copy_includes_framing(s.develop.copy_includes_framing);
window.set_settings_cache_usage(controller.usage_label.borrow().clone().into());
// --- library -------------------------------------------------------
@@ -627,20 +631,6 @@ pub fn wire<F, G>(
});
}
// TRACES: FR-DEV-6
// Whether a copied edit carries the crop. Off by default — see
// `DevelopSettings::copy_includes_framing` for why that is the safe
// direction rather than merely the conservative one.
{
let weak = window.as_weak();
let ctl = controller.clone();
window.on_settings_copy_includes_framing_toggled(move |on| {
let Some(w) = weak.upgrade() else { return };
ctl.edit(|s| s.develop.copy_includes_framing = on);
render(&w, &ctl);
});
}
{
let weak = window.as_weak();
let ctl = controller.clone();