Bind the two develop gestures that were described and not bound

FR-DEV-16's book said resetting a control and hiding a mask layer were
reachable by pointer and by finger, and stopped there. The reason was
honest: the generated rows have no focus, so "reset the focused control"
named a thing the panel could not point at. But a photographer at the
keyboard means something narrower than focus. They mean the slider they
just dragged too far, and that is a thing the panel can remember.

So the Adjustments global keeps the last control moved — two indices,
written where the panel forwards the change and cleared when the next
photograph opens, so a reset cannot reach back into the previous edit
through an index that happens to be shared. R puts it back, through the
same callback the track's double-click takes, and is silent until
something has moved.

The mask layer needs no such notion, because the panel already has a
selection: the rows the edge controls point at. H hides or shows those,
through the path the ring at the head of the row takes, so it is an edit
and a history step exactly as the ring is. A mixed selection goes to
shown, since the layer nobody can see is the one being asked about.

Both tags now carry the key, and the book says so.
This commit is contained in:
2026-09-12 01:08:09 +02:00
parent 2836ec2881
commit 9cc52fd72b
10 changed files with 147 additions and 55 deletions
+6 -4
View File
@@ -111,7 +111,7 @@ Noise reduction and capture sharpening are judgements about single pixels, and a
The edit on screen is saved on the way out, so stepping through a folder is as much a departure as going back to the grid and loses nothing. The edit on screen is saved on the way out, so stepping through a folder is as much a departure as going back to the grid and loses nothing.
<sub>`ui/dr-ui/ui/app.slint:2500`</sub> <sub>`ui/dr-ui/ui/app.slint:2522`</sub>
### See the photograph before you edited it ### See the photograph before you edited it
@@ -121,12 +121,13 @@ The edit on screen is saved on the way out, so stepping through a folder is as m
Held rather than toggled, and no split screen: a split halves the working image on the tablet the column was sized for, and the comparison photographers describe making is a flick back and forth. It takes no history step, so checking whether a frame is overcooked costs nothing to undo afterwards. Held rather than toggled, and no split screen: a split halves the working image on the tablet the column was sized for, and the comparison photographers describe making is a flick back and forth. It takes no history step, so checking whether a frame is overcooked costs nothing to undo afterwards.
<sub>`ui/dr-ui/ui/app.slint:2624`</sub> <sub>`ui/dr-ui/ui/app.slint:2646`</sub>
### Put one control back to its default ### Put one control back to its default
- **Touch** — Double-tap its track - **Touch** — Double-tap its track
- **Pointer** — Double-click its track, or right-click it - **Pointer** — Double-click its track, or right-click it
- **Keyboard** — R, for the control last moved
The column is 280px wide and the colour mixer alone puts thirty-six of these in it, so a reset button per row would be most of the width. Two ways in with a pointer because right-click is the one a hand already reaches for and double-click is the one that needs no second button. A group's own reset is in its heading; this is the single control. The column is 280px wide and the colour mixer alone puts thirty-six of these in it, so a reset button per row would be most of the width. Two ways in with a pointer because right-click is the one a hand already reaches for and double-click is the one that needs no second button. A group's own reset is in its heading; this is the single control.
@@ -136,8 +137,9 @@ The column is 280px wide and the colour mixer alone puts thirty-six of these in
- **Touch** — Tap the ring at the head of its row - **Touch** — Tap the ring at the head of its row
- **Pointer** — Click the ring at the head of its row - **Pointer** — Click the ring at the head of its row
- **Keyboard** — H, for the selected layer history step, unlike holding "Before" — the layer really is off until it is switched back on.
Disabling a layer is the before-and-after a local edit constantly wants, so it is one press away rather than inside the row. It is an edit and does take a history step, unlike holding "Before" — the layer really is off until it is switched back on. Disabling a layer is the before-and-after a local edit constantly wants, so it is one press away rather than inside the row. It is an edit and does take a
<sub>`ui/dr-ui/ui/masks.slint:209`</sub> <sub>`ui/dr-ui/ui/masks.slint:209`</sub>
@@ -148,7 +150,7 @@ Disabling a layer is the before-and-after a local edit constantly wants, so it i
A mask is judged by seeing where it falls, and two are judged by seeing where they meet — so each row has its own eye rather than the panel having one, and the eye is drawn in the colour the mask shows in, so the row says which shape on the picture is its. Nothing about the edit changes: this is how the photograph is looked at, and takes no history step. A mask is judged by seeing where it falls, and two are judged by seeing where they meet — so each row has its own eye rather than the panel having one, and the eye is drawn in the colour the mask shows in, so the row says which shape on the picture is its. Nothing about the edit changes: this is how the photograph is looked at, and takes no history step.
<sub>`ui/dr-ui/ui/masks.slint:274`</sub> <sub>`ui/dr-ui/ui/masks.slint:275`</sub>
## Collections sidebar ## Collections sidebar
+11
View File
@@ -533,6 +533,17 @@ which knows nothing of a layer's shaping and nothing at all about a gradient, a
so choosing a subject or a category produced a layer whose extent was invisible, and every control so choosing a subject or a category produced a layer whose extent was invisible, and every control
in FR-DEV-19a and FR-DEV-19b acted on something the photographer could not see. in FR-DEV-19a and FR-DEV-19b acted on something the photographer could not see.
**FR-DEV-16 — A keyboard vocabulary for develop.** Every develop gesture that can be reached from
the keyboard is bound, tagged beside its implementation, and generated into the gesture book
(FR-UI-4): stepping through the folder, fit and 1:1, holding the original, undo and redo, copying
and pasting settings, cycling the adjustment groups, resetting the control last moved, and showing
or hiding the selected mask layer. None is keyboard-only — each has a pointer and a touch route
(FR-DEV-3b) — and the book is regenerated from the tags, so it cannot describe a binding the
application does not have.
Editing rhythm depends on the hands staying put: reaching for a menu breaks the concentration of an
edit the same way it breaks the pace of a cull. A binding nobody can discover is the same as no
binding, which is why the generated book is part of the requirement rather than documentation of it.
### 3.4 Display and interaction ### 3.4 Display and interaction
**FR-DSP-1 — Proxy-resolution rendering.** The develop view renders at the resolution actually **FR-DSP-1 — Proxy-resolution rendering.** The develop view renders at the resolution actually
+50 -49
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -111,14 +111,14 @@ pub const GESTURES: &[Gesture] = &[
section: "Develop", section: "Develop",
touch: "Double-tap its track", touch: "Double-tap its track",
pointer: "Double-click its track, or right-click it", pointer: "Double-click its track, or right-click it",
keys: "", keys: "R, for the control last moved",
}, },
Gesture { Gesture {
title: "Show or hide one mask layer", title: "Show or hide one mask layer",
section: "Develop", section: "Develop",
touch: "Tap the ring at the head of its row", touch: "Tap the ring at the head of its row",
pointer: "Click the ring at the head of its row", pointer: "Click the ring at the head of its row",
keys: "", keys: "H, for the selected layer history step, unlike holding \"Before\" — the layer really is off until it is switched back on.",
}, },
Gesture { Gesture {
title: "Show or hide one mask on the photograph", title: "Show or hide one mask on the photograph",
+6
View File
@@ -414,6 +414,12 @@ fn reset_view_state(window: &AppWindow) {
// the first click on the *next* photograph sample it, which is a click // the first click on the *next* photograph sample it, which is a click
// nobody meant to spend. // nobody meant to spend.
window.global::<Adjustments>().set_sampling_op(-1); window.global::<Adjustments>().set_sampling_op(-1);
// TRACES: FR-DEV-16
// And the control last moved, which R resets: the indices are into this
// photograph's rows, and a reset that reached back into the previous edit
// would land on whichever control happens to share them.
window.global::<Adjustments>().set_touched_op(-1);
window.global::<Adjustments>().set_touched_param(-1);
// TRACES: FR-DSP-7 // TRACES: FR-DSP-7
// Emptied rather than left standing: the previous photograph's histogram // Emptied rather than left standing: the previous photograph's histogram
// beside the next one's filename is a confident, precise lie, and the gap // beside the next one's filename is a confident, precise lie, and the gap
+28
View File
@@ -666,6 +666,34 @@ pub(crate) fn wire(
}); });
} }
{ {
{
// TRACES: FR-DEV-16
// The keyboard's copy of the ring. A mixed selection goes to shown:
// a layer nobody can see is the one being asked about, and turning
// the rest off to match it would hide work in the name of hiding it.
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.global::<Masking>().on_selected_toggled(move || {
let Some(w) = weak.upgrade() else { return };
{
let mut slot = session.borrow_mut();
let Some(s) = slot.as_mut() else { return };
let ids: Vec<String> = s.active_masks().to_vec();
if ids.is_empty() {
return;
}
let all_shown = ids
.iter()
.all(|id| s.masks().get(id).is_some_and(|l| l.enabled));
for id in &ids {
s.set_mask_enabled(id, !all_shown);
}
}
sync(&w, &session);
redraw(&w);
});
}
let weak = window.as_weak(); let weak = window.as_weak();
let session = session.clone(); let session = session.clone();
let redraw = redraw.clone(); let redraw = redraw.clone();
+14
View File
@@ -995,6 +995,18 @@ export global Adjustments {
callback param-changed(int, int, float); callback param-changed(int, int, float);
callback param-reset(int, int); callback param-reset(int, int);
/// TRACES: FR-DEV-16
/// The control last moved, so a key can put it back.
///
/// The generated rows have no focus: they are a model the repeater
/// rebuilds, and a focus ring for them would be a larger change than the
/// binding wants. What a photographer means by "reset that" from the
/// keyboard is the slider they just dragged too far, and that is a thing
/// the panel can remember without any of them being focused. -1 is none,
/// which is where every photograph starts — see `lib.rs`, which clears
/// these on open so the key cannot reach back into the previous edit.
in-out property <int> touched-op: -1;
in-out property <int> touched-param: -1;
/// Every parameter of one operation back to its default. A section's reset /// Every parameter of one operation back to its default. A section's reset
/// and a curve's reset are the same action, so they share the one callback /// and a curve's reset are the same action, so they share the one callback
/// rather than duplicating a handler that would have to be kept in step /// rather than duplicating a handler that would have to be kept in step
@@ -1511,6 +1523,8 @@ export component AdjustPanel inherits Rectangle {
curve-channel: Adjustments.curve-channel; curve-channel: Adjustments.curve-channel;
drag-changed(on) => { root.slider-dragging = on; } drag-changed(on) => { root.slider-dragging = on; }
param-changed(op, param, v) => { param-changed(op, param, v) => {
Adjustments.touched-op = op;
Adjustments.touched-param = param;
Adjustments.param-changed(op, param, v); Adjustments.param-changed(op, param, v);
} }
param-reset(op, param) => { Adjustments.param-reset(op, param); } param-reset(op, param) => { Adjustments.param-reset(op, param); }
+22
View File
@@ -2497,6 +2497,28 @@ in property <bool> panel-visible: true;
root.inspect-toggled(-1, -1); root.inspect-toggled(-1, -1);
return accept; return accept;
} }
// TRACES: FR-DEV-16
// R puts back the control last moved. Bare, like
// Z: the modifier chords above have all returned.
// Nothing has been moved on this photograph until
// something has, so the key is silent until then
// rather than resetting a control nobody named.
if ((event.text == "r" || event.text == "R")
&& Adjustments.touched-op >= 0) {
Adjustments.param-reset(
Adjustments.touched-op,
Adjustments.touched-param);
return accept;
}
// TRACES: FR-DEV-16
// H hides or shows the selected mask layer — the
// ring at the head of its row, from the keyboard.
// With nothing selected there is nothing to hide,
// and Rust says so by doing nothing.
if (event.text == "h" || event.text == "H") {
Masking.selected-toggled();
return accept;
}
// GESTURE: Move to the next or previous photograph // GESTURE: Move to the next or previous photograph
// where: Develop // where: Develop
// touch: Tap a frame in the roll along the // touch: Tap a frame in the roll along the
+1
View File
@@ -295,6 +295,7 @@ export component SliderTrack inherits Rectangle {
// where: Develop // where: Develop
// touch: Double-tap its track // touch: Double-tap its track
// pointer: Double-click its track, or right-click it // pointer: Double-click its track, or right-click it
// keys: R, for the control last moved
// why: The column is 280px wide and the colour mixer alone // why: The column is 280px wide and the colour mixer alone
// puts thirty-six of these in it, so a reset button per // puts thirty-six of these in it, so a reset button per
// row would be most of the width. Two ways in with a // row would be most of the width. Two ways in with a
+7
View File
@@ -213,6 +213,7 @@ component MaskEntry inherits Rectangle {
// why: Disabling a layer is the before-and-after a local // why: Disabling a layer is the before-and-after a local
// edit constantly wants, so it is one press away rather // edit constantly wants, so it is one press away rather
// than inside the row. It is an edit and does take a // than inside the row. It is an edit and does take a
// keys: H, for the selected layer
// history step, unlike holding "Before" — the layer // history step, unlike holding "Before" — the layer
// really is off until it is switched back on. // really is off until it is switched back on.
// //
@@ -819,6 +820,12 @@ export global Masking {
callback add-range(bool); callback add-range(bool);
callback add-subject(int); callback add-subject(int);
callback add-category(string); callback add-category(string);
/// TRACES: FR-DEV-16
/// The same switch for whichever layers are selected, from the keyboard.
/// No id, because the selection is the session's and this file cannot
/// see it; Rust answers by flipping each selected layer through the same
/// path the ring takes, so it is one history step per layer either way.
callback selected-toggled();
} }
export component MaskPanel inherits Rectangle { export component MaskPanel inherits Rectangle {