Wire the develop panels through globals, so a second copy is one line

Every panel in the develop column declared its inputs and its callbacks and
had `app.slint` bind each one to a property or a callback on the window root.
That is fine while a panel is drawn once. N9 draws them a second time, in the
portrait dock, and the wiring is what would have to be copied: `MaskPanel`
alone ran to forty lines of forwarding, and a callback added to one copy and
not the other compiles, renders, and simply does nothing on the layout nobody
was looking at.

So the wiring moved to Slint globals. A panel reads the global and calls the
global; Rust hooks the global instead of the window; and the instantiation in
the column is now the panel's name and a pair of braces — every one of the ten
children of the column, with no property that differs by placement left to
supply.

There is a global per panel family rather than one for all of them, and the
reason is an import cycle. Each panel's model struct — `ParamRow`, `MaskRow`,
`HistogramView` — is declared in the panel's own file, so a single global
holding `[MaskRow]` and `[ParamRow]` would have to live in a file importing
`masks.slint` and `adjust.slint` while both imported the global back, which
Slint rejects. Breaking that needs six model declarations relocated, which is a
change to the data model and not to the plumbing this is about. A global beside
the panel it serves also lets each name drop the prefix it was carrying only
because the window root is one flat namespace: `root.spot-radius` is
`Repair.radius`, and `root.peaking-on` is `Peaking.showing`.

`session.slint` is new and holds the two facts every family needs and none of
them owns: whether there is an open photograph to edit, and which mode the view
is in, with the three readings of the mode derived once instead of at each of
the dozen places that tested one. `ViewMode` moves there from `adjust.slint`,
where it was only ever a lodger.

Nothing on screen changes. What is not here: the tool rail and the status strip
still take their properties at the instantiation, because they are drawn once
and N9 does not copy them; the preset sheet's own state stays on the window,
because the library grid opens the same sheet and a global cannot bind the
window's state — which is why `Transfer.open-presets` is handled in
`presets.rs`, beside the summary it already had to compute.
This commit is contained in:
2026-09-07 20:01:01 +02:00
parent dd14243dba
commit 0a5eab0487
17 changed files with 1245 additions and 1341 deletions
+174 -143
View File
@@ -31,7 +31,8 @@ use slint::{ComponentHandle as _, Model as _, ModelRc, VecModel};
use crate::develop::{Abandon, DevelopSession, RefinedInstance, Segmented, SessionId};
use crate::segmentation;
use crate::{
sync_rows, AppWindow, CategoryRow, GradientHandle, MaskRow, ParamRow, PartRow, SubjectRow,
sync_rows, Adjustments, AppWindow, CategoryRow, GradientHandle, MaskRow, Masking, ParamRow,
PartRow, Steps, SubjectRow,
};
/// What the adjust panel's heading says when the controls are global.
@@ -166,12 +167,15 @@ fn category_label(name: &str) -> String {
/// 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 masking = window.global::<Masking>();
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());
masking.set_masks(ModelRc::new(VecModel::<MaskRow>::default()));
masking.set_subjects(ModelRc::new(VecModel::<SubjectRow>::default()));
masking.set_categories(ModelRc::new(VecModel::<CategoryRow>::default()));
masking.set_segmented(false);
window
.global::<Adjustments>()
.set_scope(GLOBAL_SCOPE.into());
clear_handles(window);
window.set_overlay_on(false);
return;
@@ -213,7 +217,7 @@ pub(crate) fn sync(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSessio
}
})
.collect();
window.set_mask_rows(ModelRc::new(VecModel::from(rows)));
masking.set_masks(ModelRc::new(VecModel::from(rows)));
let subjects: Vec<SubjectRow> = s
.detected_subjects()
@@ -225,7 +229,7 @@ pub(crate) fn sync(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSessio
score,
})
.collect();
window.set_subject_rows(ModelRc::new(VecModel::from(subjects)));
masking.set_subjects(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.
@@ -238,7 +242,7 @@ pub(crate) fn sync(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSessio
coverage: c.coverage,
})
.collect();
window.set_category_rows(ModelRc::new(VecModel::from(categories)));
masking.set_categories(ModelRc::new(VecModel::from(categories)));
// TRACES: FR-DEV-19a
// Only the selected layer's parts, and only when exactly one is selected:
@@ -258,15 +262,17 @@ pub(crate) fn sync(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSessio
.collect(),
_ => Vec::new(),
};
window.set_mask_parts(ModelRc::new(VecModel::from(parts)));
masking.set_parts(ModelRc::new(VecModel::from(parts)));
let (radius, hardness, flow) = s.brush();
window.set_brush_radius(radius);
window.set_brush_hardness(hardness);
window.set_brush_flow(flow);
masking.set_brush_radius(radius);
masking.set_brush_hardness(hardness);
masking.set_brush_flow(flow);
window.set_segmented(s.has_segmentation());
window.set_adjust_scope(scope_label(s).into());
masking.set_segmented(s.has_segmentation());
window
.global::<Adjustments>()
.set_scope(scope_label(s).into());
sync_handles(window, s);
// The overlay is regenerated only when there is one to draw. It is a
@@ -382,7 +388,7 @@ pub(crate) fn wire(
let redraw = redraw.clone();
let rows = rows.clone();
let running = running.clone();
window.on_segment_image(move |fine| {
window.global::<Masking>().on_segment(move |fine| {
let Some(w) = weak.upgrade() else { return };
// Both the photograph and the device come out of the session, so
// no session is nothing to look at and nothing to look with.
@@ -399,7 +405,7 @@ pub(crate) fn wire(
// Set before the thread rather than by it, so there is no moment
// in which the press has been taken and nothing on screen says so.
// The button reads "Looking…" and goes insensitive off this.
w.set_segmenting(true);
w.global::<Masking>().set_segmenting(true);
let (tx, rx) = std::sync::mpsc::channel();
// The only thing the button decides. Everything else about the
@@ -433,7 +439,7 @@ pub(crate) fn wire(
let redraw = redraw.clone();
let rows = rows.clone();
let running = refining.clone();
window.on_mask_refined(move |id| {
window.global::<Masking>().on_mask_refined(move |id| {
let Some(w) = weak.upgrade() else { return };
let Some(index) = session
.borrow()
@@ -452,7 +458,7 @@ pub(crate) fn wire(
return;
}
w.set_refining(true);
w.global::<Masking>().set_refining(true);
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
@@ -515,8 +521,10 @@ pub(crate) fn wire(
s.commit_gradient_drag();
}
if let Some(w) = weak.upgrade() {
w.set_can_undo(session.borrow().as_ref().is_some_and(|s| s.can_undo()));
w.set_can_redo(session.borrow().as_ref().is_some_and(|s| s.can_redo()));
w.global::<Steps>()
.set_can_undo(session.borrow().as_ref().is_some_and(|s| s.can_undo()));
w.global::<Steps>()
.set_can_redo(session.borrow().as_ref().is_some_and(|s| s.can_redo()));
}
});
}
@@ -551,40 +559,42 @@ pub(crate) fn wire(
let weak = window.as_weak();
let session = session.clone();
let rows = rows.clone();
window.on_mask_selected(move |id, extend| {
let Some(w) = weak.upgrade() else { return };
{
let mut slot = session.borrow_mut();
let Some(s) = slot.as_mut() else { return };
if extend {
// Control/command-click: add or remove this one layer,
// keeping whatever else was already selected.
s.toggle_active_mask(&id);
} else if s.active_masks() == [id.to_string()] {
// Clicking the sole selected layer again deselects it,
// which is how the panel gets back to the whole
// photograph without a separate "edit globally" control.
// Only when it is the *only* one selected — a plain click
// on one row of a multi-selection collapses to just that
// row rather than clearing everything, which is the more
// useful reading of "I clicked a specific layer".
s.set_active_mask(None);
} else {
s.set_active_mask(Some(&id));
window
.global::<Masking>()
.on_mask_selected(move |id, extend| {
let Some(w) = weak.upgrade() else { return };
{
let mut slot = session.borrow_mut();
let Some(s) = slot.as_mut() else { return };
if extend {
// Control/command-click: add or remove this one layer,
// keeping whatever else was already selected.
s.toggle_active_mask(&id);
} else if s.active_masks() == [id.to_string()] {
// Clicking the sole selected layer again deselects it,
// which is how the panel gets back to the whole
// photograph without a separate "edit globally" control.
// Only when it is the *only* one selected — a plain click
// on one row of a multi-selection collapses to just that
// row rather than clearing everything, which is the more
// useful reading of "I clicked a specific layer".
s.set_active_mask(None);
} else {
s.set_active_mask(Some(&id));
}
}
}
sync(&w, &session);
// The scope changed, so the adjust panel below is now describing a
// different chain.
sync_rows(&w, &rows, &session);
});
sync(&w, &session);
// The scope changed, so the adjust panel below is now describing a
// different chain.
sync_rows(&w, &rows, &session);
});
}
{
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
let rows = rows.clone();
window.on_mask_removed(move |id| {
window.global::<Masking>().on_mask_removed(move |id| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.remove_mask(&id);
@@ -598,7 +608,7 @@ pub(crate) fn wire(
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_toggled(move |id, on| {
window.global::<Masking>().on_mask_toggled(move |id, on| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_enabled(&id, on);
@@ -611,27 +621,31 @@ pub(crate) fn wire(
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_invert_toggled(move |id, on| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_invert(&id, on);
}
sync(&w, &session);
redraw(&w);
});
window
.global::<Masking>()
.on_mask_invert_toggled(move |id, on| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_invert(&id, on);
}
sync(&w, &session);
redraw(&w);
});
}
{
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_opacity_changed(move |id, value| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_opacity(&id, value);
}
sync(&w, &session);
redraw(&w);
});
window
.global::<Masking>()
.on_mask_opacity_changed(move |id, value| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_opacity(&id, value);
}
sync(&w, &session);
redraw(&w);
});
}
// --- the edge treatment -------------------------------------------------
@@ -643,14 +657,16 @@ pub(crate) fn wire(
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_feather_changed(move |id, value| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_feather(&id, value);
}
sync(&w, &session);
redraw(&w);
});
window
.global::<Masking>()
.on_mask_feather_changed(move |id, value| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_feather(&id, value);
}
sync(&w, &session);
redraw(&w);
});
}
// TRACES: FR-DEV-10
// A range mask's band, and a colour range's arc.
@@ -667,27 +683,31 @@ pub(crate) fn wire(
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_band_changed(move |id, lo, hi, softness| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_band(&id, lo, hi, softness);
}
sync(&w, &session);
redraw(&w);
});
window
.global::<Masking>()
.on_mask_band_changed(move |id, lo, hi, softness| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_band(&id, lo, hi, softness);
}
sync(&w, &session);
redraw(&w);
});
}
{
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_hue_changed(move |id, hue, width| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_hue(&id, hue, width);
}
sync(&w, &session);
redraw(&w);
});
window
.global::<Masking>()
.on_mask_hue_changed(move |id, hue, width| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_hue(&id, hue, width);
}
sync(&w, &session);
redraw(&w);
});
}
// The refine control. Same shape as the feather's handler, and a separate
// one for the same reason every other mask control has its own: the
@@ -700,53 +720,61 @@ pub(crate) fn wire(
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_refine_changed(move |id, value| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_refine(&id, value);
}
sync(&w, &session);
redraw(&w);
});
window
.global::<Masking>()
.on_mask_refine_changed(move |id, value| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_refine(&id, value);
}
sync(&w, &session);
redraw(&w);
});
}
{
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_falloff_picked(move |id, index| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_falloff(&id, index.max(0) as usize);
}
sync(&w, &session);
redraw(&w);
});
window
.global::<Masking>()
.on_mask_falloff_picked(move |id, index| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_falloff(&id, index.max(0) as usize);
}
sync(&w, &session);
redraw(&w);
});
}
{
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_morphology_picked(move |id, index| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_morphology(&id, index.max(0) as usize);
}
sync(&w, &session);
redraw(&w);
});
window
.global::<Masking>()
.on_mask_morphology_picked(move |id, index| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_morphology(&id, index.max(0) as usize);
}
sync(&w, &session);
redraw(&w);
});
}
{
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_morph_radius_changed(move |id, value| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_morph_radius(&id, value);
}
sync(&w, &session);
redraw(&w);
});
window
.global::<Masking>()
.on_mask_morph_radius_changed(move |id, value| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_morph_radius(&id, value);
}
sync(&w, &session);
redraw(&w);
});
}
// --- adding layers ------------------------------------------------------
@@ -761,7 +789,7 @@ pub(crate) fn wire(
let redraw = redraw.clone();
window.on_paint_began(move |x, y| {
let Some(w) = weak.upgrade() else { return };
let erase = w.get_mask_tool() == 2;
let erase = w.global::<Masking>().get_tool() == 2;
if let Some(s) = session.borrow_mut().as_mut() {
s.begin_mask_stroke(x, y, erase);
}
@@ -800,7 +828,7 @@ pub(crate) fn wire(
{
let weak = window.as_weak();
let session = session.clone();
window.on_brush_changed(move |radius, hardness, flow| {
window.global::<Masking>().on_brush_changed(move |radius, hardness, flow| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_brush(radius, hardness, flow);
@@ -811,7 +839,7 @@ pub(crate) fn wire(
{
let weak = window.as_weak();
let session = session.clone();
window.on_mask_tool_picked(move |_tool| {
window.global::<Masking>().on_tool_picked(move |_tool| {
// The tool itself lives in the interface — it arms a gesture and
// changes no pixel — so nothing is set here. What this does is
// re-sync, because arming the brush is what makes the parts of a
@@ -826,7 +854,7 @@ pub(crate) fn wire(
let session = session.clone();
let redraw = redraw.clone();
let rows = rows.clone();
window.on_mask_part_selected(move |_id, index| {
window.global::<Masking>().on_part_selected(move |_id, index| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_active_part(index.max(0) as usize);
@@ -841,7 +869,7 @@ pub(crate) fn wire(
let session = session.clone();
let redraw = redraw.clone();
let rows = rows.clone();
window.on_mask_part_added(move |id, join| {
window.global::<Masking>().on_part_added(move |id, join| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.add_mask_part(&id, join.max(0) as usize);
@@ -849,8 +877,8 @@ pub(crate) fn wire(
// Armed, because adding an empty painted part and leaving the
// pointer in "select" would look like the button did nothing —
// the part covers no pixels until something is painted into it.
if w.get_mask_tool() == 0 {
w.set_mask_tool(1);
if w.global::<Masking>().get_tool() == 0 {
w.global::<Masking>().set_tool(1);
}
sync(&w, &session);
sync_rows(&w, &rows, &session);
@@ -862,7 +890,7 @@ pub(crate) fn wire(
let session = session.clone();
let redraw = redraw.clone();
let rows = rows.clone();
window.on_mask_part_removed(move |id, index| {
window.global::<Masking>().on_part_removed(move |id, index| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.remove_mask_part(&id, index.max(0) as usize);
@@ -876,7 +904,7 @@ pub(crate) fn wire(
let weak = window.as_weak();
let session = session.clone();
let redraw = redraw.clone();
window.on_mask_part_join_picked(move |id, index, join| {
window.global::<Masking>().on_part_join_picked(move |id, index, join| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.set_mask_part_join(&id, index.max(0) as usize, join.max(0) as usize);
@@ -890,7 +918,7 @@ pub(crate) fn wire(
let session = session.clone();
let redraw = redraw.clone();
let rows = rows.clone();
window.on_add_gradient_mask(move |radial| {
window.global::<Masking>().on_add_gradient(move |radial| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.add_gradient_mask(radial);
@@ -906,7 +934,7 @@ pub(crate) fn wire(
let session = session.clone();
let redraw = redraw.clone();
let rows = rows.clone();
window.on_add_range_mask(move |chromatic| {
window.global::<Masking>().on_add_range(move |chromatic| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.add_range_mask(chromatic);
@@ -921,7 +949,7 @@ pub(crate) fn wire(
let session = session.clone();
let redraw = redraw.clone();
let rows = rows.clone();
window.on_add_category_mask(move |name| {
window.global::<Masking>().on_add_category(move |name| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.add_category_mask(&name);
@@ -937,7 +965,7 @@ pub(crate) fn wire(
let session = session.clone();
let redraw = redraw.clone();
let rows = rows.clone();
window.on_add_subject_mask(move |index| {
window.global::<Masking>().on_add_subject(move |index| {
let Some(w) = weak.upgrade() else { return };
if let Some(s) = session.borrow_mut().as_mut() {
s.add_subject_mask(index.max(0) as usize);
@@ -1012,7 +1040,7 @@ fn watch(
Ok(None) => {}
Err(e) => log::warn!("segmentation failed: {e}"),
}
w.set_segmenting(false);
w.global::<Masking>().set_segmenting(false);
sync(&w, &session);
sync_rows(&w, &rows, &session);
redraw(&w);
@@ -1069,7 +1097,7 @@ fn watch_refine(
Ok(None) => {}
Err(e) => log::warn!("refine failed: {e}"),
}
w.set_refining(false);
w.global::<Masking>().set_refining(false);
sync(&w, &session);
sync_rows(&w, &rows, &session);
redraw(&w);
@@ -1087,13 +1115,16 @@ fn watch_refine(
/// follows the view mode, which `reset_view_state` returns to `photo`.
pub(crate) fn reset(window: &AppWindow) {
window.set_overlay_on(false);
window.set_segmenting(false);
window.set_refining(false);
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());
let masking = window.global::<Masking>();
masking.set_segmenting(false);
masking.set_refining(false);
masking.set_segmented(false);
masking.set_masks(ModelRc::new(VecModel::<MaskRow>::default()));
masking.set_subjects(ModelRc::new(VecModel::<SubjectRow>::default()));
masking.set_categories(ModelRc::new(VecModel::<CategoryRow>::default()));
window
.global::<Adjustments>()
.set_scope(GLOBAL_SCOPE.into());
clear_handles(window);
}