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.
298 lines
11 KiB
Rust
298 lines
11 KiB
Rust
//! TRACES: FR-DEV-8
|
|
//! Wiring the repair tool to the develop session.
|
|
//!
|
|
//! Translation only, exactly as [`crate::masks_ui`] is: circles out in one
|
|
//! direction, gestures in the other, and every decision in
|
|
//! [`crate::develop::DevelopSession`]. What this module does decide is when the
|
|
//! canvas is redrawn and when the panel is rebuilt, and those are the two
|
|
//! things that go wrong quietly here — see [`sync_handles`] for the one that
|
|
//! took a gesture out from under the finger.
|
|
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use slint::{ComponentHandle as _, Model as _, ModelRc, VecModel};
|
|
|
|
use crate::develop::DevelopSession;
|
|
use crate::{AppWindow, Repair, SpotHandle, Steps};
|
|
|
|
/// TRACES: FR-DEV-8 | FR-UI-3
|
|
/// Move the circles to where the repairs now are.
|
|
///
|
|
/// # Why this is not `set_spot_handles(VecModel::from(…))`
|
|
///
|
|
/// **A fresh model kills the gesture that is moving them.** The circles are a
|
|
/// repeater over this model, and handing Slint a new `ModelRc` makes it throw
|
|
/// the repeated items away and build new ones — the `TouchArea` holding the
|
|
/// pointer included. The drag then dies under the finger with the button still
|
|
/// down. `masks_ui::sync_handles` carries the same warning, and `develop.rs`
|
|
/// carries it about the parameter rows, where it broke slider drags: this is
|
|
/// the third place the same mistake is available, which is why it is written
|
|
/// down in all three.
|
|
///
|
|
/// So the model is kept and its rows are rewritten in place.
|
|
pub(crate) fn sync_handles(window: &AppWindow, session: &DevelopSession) {
|
|
let next = session.spot_handles();
|
|
let model = handle_model();
|
|
|
|
while model.row_count() > next.len() {
|
|
model.remove(model.row_count() - 1);
|
|
}
|
|
for (i, handle) in next.into_iter().enumerate() {
|
|
if i < model.row_count() {
|
|
// Only where it actually moved: an unchanged row written back is
|
|
// still a change notification.
|
|
if model.row_data(i).as_ref() != Some(&handle) {
|
|
model.set_row_data(i, handle);
|
|
}
|
|
} else {
|
|
model.push(handle);
|
|
}
|
|
}
|
|
|
|
window.set_spot_handles(ModelRc::from(model));
|
|
window
|
|
.global::<Repair>()
|
|
.set_selected(session.selected_spot_id().unwrap_or_default().into());
|
|
}
|
|
|
|
/// The circles' model, held for the life of the process — one shared identity,
|
|
/// for the reason [`sync_handles`] gives.
|
|
fn handle_model() -> Rc<VecModel<SpotHandle>> {
|
|
thread_local! {
|
|
static HANDLES: Rc<VecModel<SpotHandle>> = Rc::new(VecModel::default());
|
|
}
|
|
HANDLES.with(Clone::clone)
|
|
}
|
|
|
|
/// Take the repairs off the canvas between photographs.
|
|
///
|
|
/// Emptied rather than replaced, keeping the model's identity for the reason
|
|
/// [`sync_handles`] gives.
|
|
pub(crate) fn reset(window: &AppWindow) {
|
|
let model = handle_model();
|
|
while model.row_count() > 0 {
|
|
model.remove(model.row_count() - 1);
|
|
}
|
|
window.set_spot_handles(ModelRc::from(model));
|
|
window.global::<Repair>().set_selected(Default::default());
|
|
window.global::<Repair>().set_count(0);
|
|
}
|
|
|
|
/// Install the repair tool's callbacks.
|
|
pub(crate) fn wire(
|
|
window: &AppWindow,
|
|
session: &Rc<RefCell<Option<DevelopSession>>>,
|
|
redraw: Rc<dyn Fn(&AppWindow)>,
|
|
) {
|
|
// --- placing ----------------------------------------------------------
|
|
{
|
|
let weak = window.as_weak();
|
|
let session = session.clone();
|
|
let redraw = redraw.clone();
|
|
window.on_spot_placed(move |x, y| {
|
|
let Some(w) = weak.upgrade() else { return };
|
|
let placed = session
|
|
.borrow_mut()
|
|
.as_mut()
|
|
.and_then(|s| s.place_spot(x, y));
|
|
if placed.is_none() {
|
|
// The letterbox margin, or a full set. Neither is an error and
|
|
// neither should clear the selection: a near-miss that threw
|
|
// away what the column was describing would read as hostile.
|
|
return;
|
|
}
|
|
if let Some(s) = session.borrow().as_ref() {
|
|
sync_handles(&w, s);
|
|
sync_panel(&w, s);
|
|
}
|
|
sync_undo(&w, &session);
|
|
redraw(&w);
|
|
});
|
|
}
|
|
|
|
// --- dragging ---------------------------------------------------------
|
|
//
|
|
// The repair as it stood when the press landed, held for the gesture. A
|
|
// drag is applied to *that* rather than accumulated frame by frame: the
|
|
// clamps would otherwise compound, so a source dragged past its limit and
|
|
// back would not return to where it started, and the result would depend on
|
|
// how many pointer events the platform happened to deliver.
|
|
let dragging: Rc<RefCell<Option<dr_pipeline::Spot>>> = Rc::new(RefCell::new(None));
|
|
{
|
|
let weak = window.as_weak();
|
|
let session = session.clone();
|
|
let redraw = redraw.clone();
|
|
let dragging = dragging.clone();
|
|
window.on_spot_handle_dragged(move |id, role, from_x, from_y, to_x, to_y| {
|
|
let Some(w) = weak.upgrade() else { return };
|
|
let origin = dragging.borrow().clone();
|
|
let started = session.borrow_mut().as_mut().and_then(|s| {
|
|
s.drag_spot(
|
|
id.as_str(),
|
|
role,
|
|
origin.as_ref(),
|
|
(from_x, from_y),
|
|
(to_x, to_y),
|
|
)
|
|
});
|
|
if started.is_none() {
|
|
return;
|
|
}
|
|
*dragging.borrow_mut() = started;
|
|
// Only the circles. Rebuilding the panel on every frame of a drag
|
|
// is work for no difference, and a full rebuild would take the
|
|
// gesture out from under the finger — see `sync_handles`.
|
|
if let Some(s) = session.borrow().as_ref() {
|
|
sync_handles(&w, s);
|
|
}
|
|
redraw(&w);
|
|
});
|
|
}
|
|
{
|
|
let weak = window.as_weak();
|
|
let session = session.clone();
|
|
let dragging = dragging.clone();
|
|
window.on_spot_handle_released(move || {
|
|
// Forgotten on release, so the next gesture measures from wherever
|
|
// this one left the repair.
|
|
if dragging.borrow_mut().take().is_none() {
|
|
// A press with no movement — a selection, not a drag. Recording
|
|
// a step would put an identical snapshot on the undo stack.
|
|
return;
|
|
}
|
|
if let Some(s) = session.borrow_mut().as_mut() {
|
|
s.commit_spot_drag();
|
|
}
|
|
if let Some(w) = weak.upgrade() {
|
|
sync_undo(&w, &session);
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- the column's controls --------------------------------------------
|
|
//
|
|
// One closure shape, four callbacks: each hands the session a change to
|
|
// make to whichever repair is selected, and the session decides whether
|
|
// there is one. Reading the selection here instead would put the same
|
|
// `Option` check in four places and let them disagree.
|
|
macro_rules! control {
|
|
($install:ident, $apply:expr) => {{
|
|
let weak = window.as_weak();
|
|
let session = session.clone();
|
|
let redraw = redraw.clone();
|
|
window.global::<Repair>().$install(move |value| {
|
|
let Some(w) = weak.upgrade() else { return };
|
|
let changed = session
|
|
.borrow_mut()
|
|
.as_mut()
|
|
.is_some_and(|s| s.set_selected_spot(|spot| $apply(spot, value)));
|
|
if !changed {
|
|
return;
|
|
}
|
|
if let Some(s) = session.borrow().as_ref() {
|
|
sync_handles(&w, s);
|
|
sync_panel(&w, s);
|
|
}
|
|
sync_undo(&w, &session);
|
|
redraw(&w);
|
|
});
|
|
}};
|
|
}
|
|
|
|
control!(on_radius_changed, |spot: &mut dr_pipeline::Spot, v| {
|
|
spot.set_radius(v)
|
|
});
|
|
control!(on_feather_changed, |spot: &mut dr_pipeline::Spot, v| {
|
|
spot.set_feather(v)
|
|
});
|
|
control!(on_opacity_changed, |spot: &mut dr_pipeline::Spot, v| {
|
|
spot.set_opacity(v)
|
|
});
|
|
control!(on_mode_picked, |spot: &mut dr_pipeline::Spot, i| {
|
|
spot.mode = if i == 1 {
|
|
dr_pipeline::SpotMode::Clone
|
|
} else {
|
|
dr_pipeline::SpotMode::Heal
|
|
}
|
|
});
|
|
|
|
// --- selecting and removing -------------------------------------------
|
|
{
|
|
let weak = window.as_weak();
|
|
let session = session.clone();
|
|
let redraw = redraw.clone();
|
|
window.on_spot_selected(move |id| {
|
|
let Some(w) = weak.upgrade() else { return };
|
|
if let Some(s) = session.borrow_mut().as_mut() {
|
|
s.select_spot(Some(id.as_str()));
|
|
}
|
|
if let Some(s) = session.borrow().as_ref() {
|
|
sync_handles(&w, s);
|
|
sync_panel(&w, s);
|
|
}
|
|
// The canvas changes — the selected repair gains its source circle
|
|
// — but no pixel does, so this is a repaint of the overlay rather
|
|
// than of the photograph. `redraw` is the only hook there is, and
|
|
// it is cheap enough: the render caches on the invalidation key,
|
|
// which a selection does not move.
|
|
redraw(&w);
|
|
});
|
|
}
|
|
{
|
|
let weak = window.as_weak();
|
|
let session = session.clone();
|
|
let redraw = redraw.clone();
|
|
window.global::<Repair>().on_removed(move |id| {
|
|
let Some(w) = weak.upgrade() else { return };
|
|
let removed = session
|
|
.borrow_mut()
|
|
.as_mut()
|
|
.is_some_and(|s| s.remove_spot(id.as_str()));
|
|
if !removed {
|
|
return;
|
|
}
|
|
if let Some(s) = session.borrow().as_ref() {
|
|
sync_handles(&w, s);
|
|
sync_panel(&w, s);
|
|
}
|
|
sync_undo(&w, &session);
|
|
redraw(&w);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// TRACES: FR-DEV-8
|
|
/// Push the selected repair's settings into the column.
|
|
///
|
|
/// Separate from [`sync_handles`] because they answer different questions and
|
|
/// change at different times: the circles move on every frame of a drag and on
|
|
/// every pan, and these move when a repair is selected or a control is used.
|
|
pub(crate) fn sync_panel(window: &AppWindow, session: &DevelopSession) {
|
|
let repair = window.global::<Repair>();
|
|
repair.set_count(session.spot_count() as i32);
|
|
if let Some(spot) = session.selected_spot() {
|
|
repair.set_radius(spot.radius);
|
|
repair.set_feather(spot.feather);
|
|
repair.set_opacity(spot.opacity);
|
|
repair.set_mode(match spot.mode {
|
|
dr_pipeline::SpotMode::Heal => 0,
|
|
dr_pipeline::SpotMode::Clone => 1,
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Push whether undo and redo have anywhere to go.
|
|
///
|
|
/// Every repair is a history step, so every one of them moves these — and a
|
|
/// disabled undo button after an edit that *is* undoable is the kind of small
|
|
/// lie that stops people trusting the button at all.
|
|
fn sync_undo(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSession>>>) {
|
|
window
|
|
.global::<Steps>()
|
|
.set_can_undo(session.borrow().as_ref().is_some_and(|s| s.can_undo()));
|
|
window
|
|
.global::<Steps>()
|
|
.set_can_redo(session.borrow().as_ref().is_some_and(|s| s.can_redo()));
|
|
}
|