//! 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, SpotHandle}; /// 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.set_selected_spot(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> { thread_local! { static HANDLES: Rc> = 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.set_selected_spot(Default::default()); window.set_spot_count(0); } /// Install the repair tool's callbacks. pub(crate) fn wire( window: &AppWindow, session: &Rc>>, redraw: Rc, ) { // --- 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>> = 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.$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_spot_radius_changed, |spot: &mut dr_pipeline::Spot, v| { spot.set_radius(v) }); control!( on_spot_feather_changed, |spot: &mut dr_pipeline::Spot, v| { spot.set_feather(v) } ); control!( on_spot_opacity_changed, |spot: &mut dr_pipeline::Spot, v| { spot.set_opacity(v) } ); control!(on_spot_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.on_spot_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) { window.set_spot_count(session.spot_count() as i32); if let Some(spot) = session.selected_spot() { window.set_spot_radius(spot.radius); window.set_spot_feather(spot.feather); window.set_spot_opacity(spot.opacity); window.set_spot_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>>) { window.set_can_undo(session.borrow().as_ref().is_some_and(|s| s.can_undo())); window.set_can_redo(session.borrow().as_ref().is_some_and(|s| s.can_redo())); }