The scan/opening/status/error lines, offline mode and its retry, pinning a collection offline, the sync and thumbnail-sweep state, and the callbacks that route the grid to a rescan, a sync, another library, a panorama merge or an export — the last of what AppWindow still carried under the library- prefix — move onto the `Library` global started earlier on this branch. Rust reaches them through window.global::<Library>() rather than window.set_/get_/on_/invoke_ on the root. library-visible is the one name that stays: it is computed from active-page and active-view, the shell's own routing state, which a global cannot read. AppWindow now declares no other library- property or callback.
1281 lines
52 KiB
Rust
1281 lines
52 KiB
Rust
//! The develop screen's callback wiring.
|
||
//!
|
||
//! This used to be the bulk of `lib.rs::run` — every `window.on_*` and
|
||
//! `global::<...>().on_*` registration the develop view needs, with no
|
||
//! module of its own, unlike every other screen (see
|
||
//! `docs/dev/code-health.md` CH-1). [`wire`] is the list `run` now calls;
|
||
//! each `wire_*` below is one of the sections that function used to contain,
|
||
//! moved with its section comment kept as the function's doc comment and its
|
||
//! callbacks registered in the same order.
|
||
//!
|
||
//! [`DevelopWiring`] gathers what those sections capture — the session, the
|
||
//! shared rows model, the redraw and render closures, and the handles onto
|
||
//! the other controllers a paste or an export reaches — so each function
|
||
//! takes one reference instead of a dozen parameters.
|
||
|
||
use std::cell::{Cell, RefCell};
|
||
use std::path::PathBuf;
|
||
use std::rc::Rc;
|
||
|
||
use slint::ComponentHandle as _;
|
||
|
||
use crate::activity;
|
||
use crate::collections_ui::CollectionsController;
|
||
use crate::develop;
|
||
use crate::library_ui::LibraryController;
|
||
use crate::masks_ui;
|
||
use crate::peaking;
|
||
use crate::presets;
|
||
use crate::settings_ui::SettingsController;
|
||
use crate::spots_ui;
|
||
use crate::{
|
||
apply_crop_aspect, batch_request, drain_outbox, export, groups_in_rail, refresh_export_label,
|
||
render_open_frame, sync_crop_aspect, sync_film, sync_framing, sync_rows, Adjustments,
|
||
AppWindow, Develop, DevelopSession, Framing, Inspection, Library, ParamRow, Peaking, Render,
|
||
Steps, ViewMode,
|
||
};
|
||
|
||
/// Shared state the develop screen's callbacks capture.
|
||
///
|
||
/// Built once in [`wire`] from what `run` already owns; each field is an
|
||
/// owned `Rc` clone rather than a borrow, so every `wire_*` function below
|
||
/// can clone what it needs out of a `&DevelopWiring` and paste in the
|
||
/// closure exactly as `run` wrote it. `window` is not a field here — it is
|
||
/// not `Clone`, so it travels as an ordinary `&AppWindow` parameter beside
|
||
/// `w`, the same way `run` held it.
|
||
#[allow(clippy::type_complexity)]
|
||
pub(crate) struct DevelopWiring {
|
||
pub(crate) session: Rc<RefCell<Option<DevelopSession>>>,
|
||
pub(crate) rows: Rc<slint::VecModel<ParamRow>>,
|
||
pub(crate) redraw: Rc<dyn Fn(&AppWindow)>,
|
||
pub(crate) render_now: Render,
|
||
pub(crate) show: Rc<dyn Fn(&AppWindow)>,
|
||
pub(crate) entries: Rc<RefCell<Vec<PathBuf>>>,
|
||
pub(crate) index: Rc<RefCell<usize>>,
|
||
pub(crate) viewport: Rc<RefCell<(u32, u32)>>,
|
||
pub(crate) inspection: Rc<Cell<Inspection>>,
|
||
pub(crate) chosen_peaking: Rc<Cell<Option<dr_gpu::FocusPeaking>>>,
|
||
pub(crate) settings: Rc<SettingsController>,
|
||
pub(crate) library: Rc<LibraryController>,
|
||
pub(crate) collections: Rc<CollectionsController>,
|
||
pub(crate) activity: Rc<activity::ActivityLog>,
|
||
pub(crate) gpu: Option<dr_gpu::GpuContext>,
|
||
pub(crate) clipboard: Rc<presets::Clipboard>,
|
||
pub(crate) open_image: presets::OpenImage,
|
||
pub(crate) leave_develop: Rc<RefCell<Option<Rc<dyn Fn()>>>>,
|
||
}
|
||
|
||
/// Install every callback the develop screen answers to.
|
||
///
|
||
/// Called once from `run`, after the session, the render loop and the other
|
||
/// screens all exist — every section below reaches at least one of them.
|
||
/// The order matches what `run` registered: presets, export, the adjustment
|
||
/// panel, film, undo/redo, geometry, then navigation and peaking.
|
||
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
|
||
pub(crate) fn wire(
|
||
window: &AppWindow,
|
||
session: &Rc<RefCell<Option<DevelopSession>>>,
|
||
rows: &Rc<slint::VecModel<ParamRow>>,
|
||
redraw: &Rc<dyn Fn(&AppWindow)>,
|
||
render_now: &Render,
|
||
show: &Rc<dyn Fn(&AppWindow)>,
|
||
entries: &Rc<RefCell<Vec<PathBuf>>>,
|
||
index: &Rc<RefCell<usize>>,
|
||
viewport: &Rc<RefCell<(u32, u32)>>,
|
||
inspection: &Rc<Cell<Inspection>>,
|
||
chosen_peaking: &Rc<Cell<Option<dr_gpu::FocusPeaking>>>,
|
||
settings: &Rc<SettingsController>,
|
||
library: &Rc<LibraryController>,
|
||
collections: &Rc<CollectionsController>,
|
||
activity: &Rc<activity::ActivityLog>,
|
||
gpu: &Option<dr_gpu::GpuContext>,
|
||
clipboard: &Rc<presets::Clipboard>,
|
||
open_image: &presets::OpenImage,
|
||
leave_develop: &Rc<RefCell<Option<Rc<dyn Fn()>>>>,
|
||
) {
|
||
let w = DevelopWiring {
|
||
session: session.clone(),
|
||
rows: rows.clone(),
|
||
redraw: redraw.clone(),
|
||
render_now: render_now.clone(),
|
||
show: show.clone(),
|
||
entries: entries.clone(),
|
||
index: index.clone(),
|
||
viewport: viewport.clone(),
|
||
inspection: inspection.clone(),
|
||
chosen_peaking: chosen_peaking.clone(),
|
||
settings: settings.clone(),
|
||
library: library.clone(),
|
||
collections: collections.clone(),
|
||
activity: activity.clone(),
|
||
gpu: gpu.clone(),
|
||
clipboard: clipboard.clone(),
|
||
open_image: open_image.clone(),
|
||
leave_develop: leave_develop.clone(),
|
||
};
|
||
|
||
// TRACES: FR-DEV-3
|
||
// **The ratio lock belongs to the tool, not to the photograph.** Lives
|
||
// here, for the life of the window, rather than on a `DevelopSession`
|
||
// which is rebuilt per image — see `wire_zoom_pan_crop` — and is shared
|
||
// with `wire_rotation_flips_straighten`, which is the other place a turn
|
||
// or a flip has to keep it in step.
|
||
let crop_aspect = Rc::new(Cell::new(develop::CropAspect::default()));
|
||
let crop_portrait = Rc::new(Cell::new(false));
|
||
|
||
wire_presets(window, &w);
|
||
wire_export(window, &w);
|
||
wire_adjustments(window, &w);
|
||
wire_film(window, &w);
|
||
wire_undo_redo(window, &w);
|
||
wire_zoom_pan_crop(window, &w, &crop_aspect, &crop_portrait);
|
||
wire_rotation_flips_straighten(window, &w, &crop_aspect, &crop_portrait);
|
||
wire_navigation(window, &w);
|
||
wire_peaking(window, &w);
|
||
}
|
||
|
||
/// ---- copying settings between photographs (FR-DEV-6) ----------------
|
||
///
|
||
/// Wired after the develop session and the library both exist, because a
|
||
/// paste reaches both: onto the image on screen, or onto the grid's
|
||
/// selection through its sidecars.
|
||
fn wire_presets(window: &AppWindow, w: &DevelopWiring) {
|
||
let session = w.session.clone();
|
||
let rows = w.rows.clone();
|
||
let redraw = w.redraw.clone();
|
||
let clipboard = w.clipboard.clone();
|
||
let open_image = w.open_image.clone();
|
||
let settings = w.settings.clone();
|
||
let library = w.library.clone();
|
||
let collections = w.collections.clone();
|
||
let leave_develop = w.leave_develop.clone();
|
||
|
||
presets::wire(
|
||
window,
|
||
clipboard.clone(),
|
||
presets::Develop {
|
||
session: session.clone(),
|
||
rows: rows.clone(),
|
||
redraw: redraw.clone(),
|
||
open: open_image.clone(),
|
||
},
|
||
settings.clone(),
|
||
library.clone(),
|
||
collections.clone(),
|
||
);
|
||
|
||
// TRACES: FR-DEV-6
|
||
// The saved half of the same requirement, wired from the same bundle:
|
||
// applying a named preset to the open image is the paste path with a
|
||
// different source.
|
||
presets::wire_named(
|
||
window,
|
||
presets::NamedPresets::open(),
|
||
presets::Develop {
|
||
session: session.clone(),
|
||
rows: rows.clone(),
|
||
redraw: redraw.clone(),
|
||
open: open_image.clone(),
|
||
},
|
||
settings.clone(),
|
||
library.clone(),
|
||
collections.clone(),
|
||
);
|
||
|
||
// Close the knot left open beside `open_from_library`: the grid's
|
||
// "‹ Library" button was wired before there was a session to save.
|
||
let weak = window.as_weak();
|
||
let open_image = open_image.clone();
|
||
let session = session.clone();
|
||
let library = library.clone();
|
||
*leave_develop.borrow_mut() = Some(Rc::new(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
presets::save_open_edit(&w, &open_image.borrow(), &session, &library);
|
||
}));
|
||
|
||
// TRACES: FR-DEV-6
|
||
// The scope chips, shared by the preset sheet and the settings page.
|
||
// Wired once and rendered once: both surfaces draw one set, because
|
||
// "what does a paste carry" is one question about how this
|
||
// photographer works rather than one per place it is asked.
|
||
presets::wire_scope(window, settings.clone(), clipboard.clone());
|
||
presets::render_scope(window, &settings);
|
||
|
||
// TRACES: FR-UI-1
|
||
// Rendered from the same settings controller and at the same moment,
|
||
// so the rail and the strip cannot both be on screen after a change to
|
||
// the preference that chooses between them.
|
||
window
|
||
.global::<Adjustments>()
|
||
.set_groups_in_rail(groups_in_rail(&settings));
|
||
presets::render(window, &clipboard, &settings);
|
||
}
|
||
|
||
/// --- export (FR-EXP-6, FR-EXP-7, FR-EXP-9) ---------------------------
|
||
///
|
||
/// Two buttons, one worker. The develop view exports the image on screen and
|
||
/// the grid exports its selection; the only difference between them is who
|
||
/// renders the frames, so both hand a `Vec<Source>` to the same batch and
|
||
/// both report through the same activity row.
|
||
///
|
||
/// A frame is always rendered at full resolution rather than taken from the
|
||
/// canvas: the display render is deliberately viewport-sized (FR-DSP-1), and
|
||
/// exporting that would hand the user a soft, screen-sized file with no
|
||
/// indication anything had been lost (FR-EXP-9).
|
||
fn wire_export(window: &AppWindow, w: &DevelopWiring) {
|
||
let session = w.session.clone();
|
||
let settings = w.settings.clone();
|
||
let library = w.library.clone();
|
||
let collections = w.collections.clone();
|
||
let activity = w.activity.clone();
|
||
let gpu = w.gpu.clone();
|
||
|
||
// Replaced at each start, so the button always cancels the run it is
|
||
// sitting on and a stale token cancels nothing.
|
||
let cancel: Rc<RefCell<export::Cancel>> = Rc::new(RefCell::new(export::Cancel::default()));
|
||
// Holds the drain. Assigning a new timer drops the previous one, which
|
||
// is what takes a superseded batch's row out of the register.
|
||
let drain: Rc<RefCell<Option<slint::Timer>>> = Rc::new(RefCell::new(None));
|
||
|
||
let start = {
|
||
let settings = settings.clone();
|
||
let library = library.clone();
|
||
let activity = activity.clone();
|
||
let gpu = gpu.clone();
|
||
let cancel = cancel.clone();
|
||
let drain = drain.clone();
|
||
Rc::new(
|
||
move |window: &AppWindow, sources: Vec<export::Source>, to: export::Reporting| {
|
||
let total = sources.len();
|
||
let request = batch_request(sources, &settings, &library, gpu.as_ref());
|
||
|
||
let token = export::Cancel::default();
|
||
*cancel.borrow_mut() = token.clone();
|
||
|
||
let rx = export::spawn_batch(request, token);
|
||
|
||
// Upload the moment the batch is done rather than waiting
|
||
// for a sync pass. An export bound for the server is
|
||
// complete on disk the instant it is staged, but it is not
|
||
// where the user asked for it until this runs — and
|
||
// "Queued for Exports" sitting unchanged until somebody
|
||
// presses Sync reads as an export that did not upload.
|
||
let library_for_drain = library.clone();
|
||
export::drain_batch(
|
||
window.as_weak(),
|
||
&activity,
|
||
&drain,
|
||
rx,
|
||
total,
|
||
to,
|
||
move || drain_outbox(&library_for_drain),
|
||
);
|
||
},
|
||
)
|
||
};
|
||
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let settings = settings.clone();
|
||
let start = start.clone();
|
||
window.on_export_image(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
// One batch at a time, and the grid's counts as one. The two
|
||
// buttons share a worker slot, so starting a second run would
|
||
// drop the first's drain — leaving a batch still writing files
|
||
// with no progress row and a button that never comes back.
|
||
if w.get_export_busy() || w.global::<Library>().get_library_exporting() {
|
||
w.set_export_status("An export is already running".into());
|
||
return;
|
||
}
|
||
w.set_export_busy(true);
|
||
// Set before the render rather than after: the render is the
|
||
// one part still on this thread, so a label written afterwards
|
||
// would never be drawn in the "Rendering…" state at all.
|
||
w.set_export_status("Rendering…".into());
|
||
|
||
match render_open_frame(&w, &session, settings.snapshot().export.colour_space) {
|
||
Ok(source) => start(&w, vec![source], export::Reporting::Develop),
|
||
Err(e) => {
|
||
w.set_export_status(format!("Export failed: {e}").into());
|
||
w.set_export_busy(false);
|
||
}
|
||
}
|
||
refresh_export_label(&w, &settings);
|
||
});
|
||
}
|
||
|
||
// TRACES: FR-EXP-7
|
||
// The grid's selection. Nothing is rendered here — the worker fetches,
|
||
// decodes and renders each photograph itself, so this returns to the
|
||
// event loop immediately and a batch of three hundred is a progress bar
|
||
// rather than a frozen window.
|
||
{
|
||
let weak = window.as_weak();
|
||
let library = library.clone();
|
||
let collections = collections.clone();
|
||
let start = start.clone();
|
||
window
|
||
.global::<Library>()
|
||
.on_library_export_selection(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
// See the develop button above for why one run excludes the
|
||
// other. The button is a cancel by then, so this only catches
|
||
// a batch started from develop and left running.
|
||
if w.global::<Library>().get_library_exporting() || w.get_export_busy() {
|
||
w.global::<Library>()
|
||
.set_library_status("An export is already running".into());
|
||
return;
|
||
}
|
||
|
||
let sources = library.export_sources(&collections.selected());
|
||
if sources.is_empty() {
|
||
// Said out loud rather than ignored, matching what a paste
|
||
// or a judgement keystroke does with an empty selection.
|
||
w.global::<Library>()
|
||
.set_library_status("Select an image first".into());
|
||
return;
|
||
}
|
||
|
||
w.global::<Library>().set_library_exporting(true);
|
||
w.global::<Library>()
|
||
.set_library_status(format!("Exporting {} images…", sources.len()).into());
|
||
start(&w, sources, export::Reporting::Library);
|
||
});
|
||
}
|
||
|
||
// TRACES: NFR-ARCH-3
|
||
{
|
||
let weak = window.as_weak();
|
||
window
|
||
.global::<Library>()
|
||
.on_library_cancel_export(move || {
|
||
cancel.borrow().cancel();
|
||
if let Some(w) = weak.upgrade() {
|
||
// The worker stops at the next point it is safe to — which
|
||
// may be a frame away — so the button says "asked for" and
|
||
// not "done". The drain writes the real answer.
|
||
w.global::<Library>()
|
||
.set_library_status("Cancelling the export…".into());
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/// ---- Adjustment callbacks ------------------------------------------
|
||
///
|
||
/// Generic by construction: they carry indices into the capability list,
|
||
/// so adding an operation needs no change here (FR-DEV-3c).
|
||
fn wire_adjustments(window: &AppWindow, w: &DevelopWiring) {
|
||
let session = w.session.clone();
|
||
let rows = w.rows.clone();
|
||
let redraw = w.redraw.clone();
|
||
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window
|
||
.global::<Adjustments>()
|
||
.on_param_changed(move |op, param, value| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.set_param(op, param, value);
|
||
// TRACES: FR-DEV-3f
|
||
// The film's own exposures ride *inside* the baked tables
|
||
// rather than arriving as uniforms, because the print balance
|
||
// is solved against them — an enlarger's filtration depends on
|
||
// how the negative was exposed. So moving one has to rebuild
|
||
// the lookup, which no other slider in the panel does.
|
||
s.rebake_film_if_affected(op);
|
||
}
|
||
sync_rows(&w, &rows, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window
|
||
.global::<Adjustments>()
|
||
.on_param_reset(move |op, param| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.reset_param(op, param);
|
||
}
|
||
sync_rows(&w, &rows, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
// TRACES: FR-DEV-3
|
||
// The local-adjustment panel. Wired as a block rather than inline because
|
||
// it is a dozen callbacks that all say the same three things, and they
|
||
// read better beside each other than scattered through this function.
|
||
{
|
||
// Choosing a group re-filters the panel and nothing else — no edit,
|
||
// no render. It is navigation.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let rows = rows.clone();
|
||
window.global::<Adjustments>().on_tab_picked(move |index| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.set_active_tab(index);
|
||
}
|
||
sync_rows(&w, &rows, &session);
|
||
// The stock picker is filtered by group like everything else, and
|
||
// it is not a row — so the thing that decides whether it is on
|
||
// screen has to be recomputed here as well, or it would answer for
|
||
// whichever group happened to be open when the image was loaded.
|
||
sync_film(&w, &session);
|
||
});
|
||
}
|
||
|
||
masks_ui::wire(window, &session, &rows, &redraw);
|
||
spots_ui::wire(window, &session, redraw.clone());
|
||
|
||
// A way to land on the Identity Manager at startup, for looking at it
|
||
// without a mouse. Off unless the variable is set, so it costs a getenv
|
||
// per launch and changes nothing otherwise.
|
||
if std::env::var_os("DARKROOM_START_IDENTITY").is_some() {
|
||
window.invoke_identity_open();
|
||
}
|
||
if std::env::var_os("DARKROOM_START_SETTINGS").is_some() {
|
||
window.invoke_settings_open();
|
||
}
|
||
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window.global::<Adjustments>().on_reset_all(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.reset_all();
|
||
}
|
||
sync_rows(&w, &rows, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
// A curve is one control spanning many parameters, so resetting it
|
||
// clears all of them at once — resetting a single point would leave
|
||
// a shape the user did not ask for.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window.global::<Adjustments>().on_curve_reset(move |op| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.reset_curve(op);
|
||
}
|
||
sync_rows(&w, &rows, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
// Which of the curve's curves the plot is showing. **No redraw**, and
|
||
// that is the whole character of this control: it changes no
|
||
// parameter, so the photograph is already correct on screen and
|
||
// recomputing it would be a frame spent to produce the same pixels.
|
||
// For the same reason it records no history step — there is nothing
|
||
// to undo — and the sidecar never hears about it.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let rows = rows.clone();
|
||
window
|
||
.global::<Adjustments>()
|
||
.on_curve_channel_picked(move |index| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.set_curve_channel(index);
|
||
}
|
||
sync_rows(&w, &rows, &session);
|
||
});
|
||
}
|
||
}
|
||
|
||
/// ---- the film stock (FR-DEV-3f) -------------------------------------
|
||
///
|
||
/// Unlike the curve channel above, both of these change the photograph:
|
||
/// choosing a stock *is* the edit. So they redraw, and they leave the
|
||
/// session dirty for the sidecar in the ordinary way.
|
||
fn wire_film(window: &AppWindow, w: &DevelopWiring) {
|
||
let session = w.session.clone();
|
||
let rows = w.rows.clone();
|
||
let redraw = w.redraw.clone();
|
||
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window.global::<Adjustments>().on_film_picked(move |index| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
// Index zero is "None" — see `DevelopSession::film_choices`, which
|
||
// puts it first so that the neutral choice needs no sentinel.
|
||
let choices = DevelopSession::film_choices();
|
||
let Some((stock, _)) = choices.get(index.max(0) as usize) else {
|
||
return;
|
||
};
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
// Printed by default when the stock has a paper: a colour
|
||
// negative that has not been printed is an orange strip, and
|
||
// offering that as the first thing a photographer sees when
|
||
// they pick Portra would read as a bug rather than as a
|
||
// choice. The toggle is there for anyone who wants the scan.
|
||
let print = stock
|
||
.and_then(dr_film::find)
|
||
.and_then(dr_film::default_print)
|
||
.is_some();
|
||
s.pick_film(*stock, print);
|
||
}
|
||
sync_rows(&w, &rows, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
window
|
||
.global::<Adjustments>()
|
||
.on_film_print_toggled(move |print| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
if let Some((stock, _)) = s.film().map(|(a, b)| (a.to_string(), b)) {
|
||
s.pick_film(Some(&stock), print);
|
||
}
|
||
}
|
||
sync_film(&w, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
}
|
||
|
||
/// ---- undo and redo (FR-DEV-5) ---------------------------------------
|
||
///
|
||
/// Thin, because the history lives in the session and every mutator there
|
||
/// records into it — see `DevelopSession::history`. What is left for the
|
||
/// interface is the refresh a paste also needs: the controls are showing
|
||
/// values that have just moved underneath them.
|
||
///
|
||
/// `can-undo` and `can-redo` are not set here; `render_now` pushes them on
|
||
/// every redraw, which is every path that can change them.
|
||
fn wire_undo_redo(window: &AppWindow, w: &DevelopWiring) {
|
||
let session = w.session.clone();
|
||
let rows = w.rows.clone();
|
||
let redraw = w.redraw.clone();
|
||
let render_now = w.render_now.clone();
|
||
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window.global::<Steps>().on_undo(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
let stepped = session.borrow_mut().as_mut().is_some_and(|s| s.undo());
|
||
if stepped {
|
||
sync_rows(&w, &rows, &session);
|
||
redraw(&w);
|
||
}
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window.global::<Steps>().on_redo(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
let stepped = session.borrow_mut().as_mut().is_some_and(|s| s.redo());
|
||
if stepped {
|
||
sync_rows(&w, &rows, &session);
|
||
redraw(&w);
|
||
}
|
||
});
|
||
}
|
||
{
|
||
// TRACES: FR-DEV-7
|
||
// Holding the comparison, and letting it go.
|
||
//
|
||
// **No rows are synced and no history is touched**, and both absences
|
||
// are the feature. The panel is describing the edit the photographer
|
||
// still has; only the canvas changes, and it changes back. This is the
|
||
// whole difference between a comparison and the undo-look-redo that
|
||
// had to stand in for one — that put two real steps on the stack at
|
||
// the moment somebody was least sure of what they were doing.
|
||
//
|
||
// A full frame rather than a draft: comparing a half-resolution
|
||
// original against a sharp edit would show a difference the edit does
|
||
// not have, which is the one thing this must not do.
|
||
let weak = window.as_weak();
|
||
let render_now = render_now.clone();
|
||
window.on_compare_original(move |on| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
// A held key repeats. Answering a repeat with a re-render would
|
||
// spend a full-resolution pass per keystroke to arrive back at the
|
||
// frame already on screen.
|
||
if w.get_showing_original() == on {
|
||
return;
|
||
}
|
||
w.set_showing_original(on);
|
||
render_now(&w, false);
|
||
});
|
||
}
|
||
{
|
||
// TRACES: FR-DEV-3
|
||
// A neutral picked off the photograph.
|
||
//
|
||
// The operation is not named here and is not passed in: the click
|
||
// carries a point, the session turns it into a colour, and
|
||
// `dr_pipeline::neutral` finds the operation that asked to be driven
|
||
// by one. This callback's whole contribution is that a click on the
|
||
// canvas is a click on the canvas.
|
||
//
|
||
// The rows are re-synced because the sample moved parameters the panel
|
||
// is showing — two of them, from one press, which is exactly the case
|
||
// a paste and an undo already go through this path for.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window.on_neutral_picked(move |x, y| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
let sampled = session
|
||
.borrow_mut()
|
||
.as_mut()
|
||
.is_some_and(|s| s.sample_neutral(x, y));
|
||
// Nothing to redraw otherwise: the probe renders to its own
|
||
// target and a sample that found nothing usable moved nothing.
|
||
if sampled {
|
||
sync_rows(&w, &rows, &session);
|
||
redraw(&w);
|
||
}
|
||
});
|
||
}
|
||
|
||
// TRACES: FR-DEV-5 | FR-DEV-7
|
||
// Clicking a row. Arriving six steps away costs what arriving from one
|
||
// does, because a step is a whole state — see `History::go_to`.
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window.global::<Steps>().on_picked(move |index| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
let stepped = session
|
||
.borrow_mut()
|
||
.as_mut()
|
||
.is_some_and(|s| s.go_to_history(index));
|
||
if stepped {
|
||
sync_rows(&w, &rows, &session);
|
||
redraw(&w);
|
||
}
|
||
});
|
||
}
|
||
// TRACES: FR-DEV-5
|
||
// Named snapshots. Taking and deleting one change the list and nothing
|
||
// else — no rows, no history — so they redraw only to push the list,
|
||
// through the same path everything else pushes it on. Restoring one is
|
||
// the paste's shape: the whole edit moves, so the rows resync and the
|
||
// picture is redrawn.
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
window.global::<Steps>().on_snapshot_taken(move |name| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.take_snapshot(&name);
|
||
}
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window.global::<Steps>().on_snapshot_restored(move |id| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
let restored = session
|
||
.borrow_mut()
|
||
.as_mut()
|
||
.is_some_and(|s| s.restore_snapshot(&id));
|
||
if restored {
|
||
sync_rows(&w, &rows, &session);
|
||
// The mask panel too: a snapshot carries its layers, and
|
||
// the redraw syncs the repairs and the overlay but not the
|
||
// rows the layers are listed in.
|
||
masks_ui::sync(&w, &session);
|
||
redraw(&w);
|
||
}
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
window.global::<Steps>().on_snapshot_removed(move |id| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.delete_snapshot(&id);
|
||
}
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
// TRACES: FR-DEV-7
|
||
// Holding a snapshot against the edit, and letting it go — the
|
||
// Before button's shape, and its rule: no rows are synced and no
|
||
// history is touched. A full frame rather than a draft, for the
|
||
// reason the original is: a soft comparison shows a difference the
|
||
// edit does not have.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let render_now = render_now.clone();
|
||
window
|
||
.global::<Steps>()
|
||
.on_snapshot_compared(move |id, down| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
let changed = session
|
||
.borrow_mut()
|
||
.as_mut()
|
||
.is_some_and(|s| s.compare_snapshot(down.then_some(id.as_str())));
|
||
if changed {
|
||
render_now(&w, false);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/// ---- zoom, pan and crop ---------------------------------------------
|
||
///
|
||
/// Zoom and pan are viewing state and touch no parameter, so unlike the
|
||
/// handlers above they do not `sync_rows`.
|
||
fn wire_zoom_pan_crop(
|
||
window: &AppWindow,
|
||
w: &DevelopWiring,
|
||
crop_aspect: &Rc<Cell<develop::CropAspect>>,
|
||
crop_portrait: &Rc<Cell<bool>>,
|
||
) {
|
||
let session = w.session.clone();
|
||
let rows = w.rows.clone();
|
||
let redraw = w.redraw.clone();
|
||
let viewport = w.viewport.clone();
|
||
let inspection = w.inspection.clone();
|
||
let crop_aspect = crop_aspect.clone();
|
||
let crop_portrait = crop_portrait.clone();
|
||
|
||
window.global::<Framing>().set_aspects(
|
||
develop::CropAspect::CHOICES
|
||
.iter()
|
||
.map(|a| slint::SharedString::from(a.label()))
|
||
.collect::<Vec<_>>()
|
||
.as_slice()
|
||
.into(),
|
||
);
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
window.on_zoom_at(move |factor, at_x, at_y| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.zoom_about(factor, at_x, at_y);
|
||
}
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
window.on_pan_by(move |dx, dy| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.pan_by(dx, dy);
|
||
}
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
// TRACES: FR-UI-4
|
||
// Fit and 1:1, and the memory of where 1:1 was pointing.
|
||
//
|
||
// The remembered point is written from what the session *landed* on
|
||
// rather than from the coordinates that arrived, so a point near an
|
||
// edge is stored where the view was clamped to. Storing the request
|
||
// instead would carry a point the previous photograph could not show
|
||
// to a photograph that also cannot show it, and the drift would
|
||
// accumulate over a folder.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let viewport = viewport.clone();
|
||
let inspection = inspection.clone();
|
||
window.on_inspect_toggled(move |at_x, at_y| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
let (vw, vh) = *viewport.borrow();
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
// A negative anchor is a control with no point of its own —
|
||
// the readout button, the keyboard — asking for the last one.
|
||
// Resolved here rather than in Slint because this is where
|
||
// the memory lives.
|
||
//
|
||
// The remembered point is in fractions of the framed image and
|
||
// the session wants fractions of the *visible* area, which are
|
||
// the same thing only while the view is fitted. That is
|
||
// exactly when this branch can move the view: from a zoomed
|
||
// view the toggle goes out to fit and ignores its anchor
|
||
// entirely.
|
||
let held = inspection.get();
|
||
let (ax, ay) = if at_x < 0.0 || at_y < 0.0 {
|
||
held.at
|
||
} else {
|
||
(at_x, at_y)
|
||
};
|
||
inspection.set(match s.toggle_inspection(ax, ay, vw, vh) {
|
||
Some(at) => Inspection { at, on: true },
|
||
// The aim is kept, only switched off. See `Inspection`.
|
||
None => Inspection { on: false, ..held },
|
||
});
|
||
}
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
// TRACES: FR-UI-5
|
||
// **Entering a mode is a side effect, which is why Rust owns it** and
|
||
// the strip does not simply write the property. Each of the three has
|
||
// work to do that the interface cannot see:
|
||
//
|
||
// *Crop* drops the zoom. The handles are placed against the whole
|
||
// frame, and a zoomed view would put most of that frame off screen
|
||
// where it cannot be dragged.
|
||
//
|
||
// *Local* turns the region overlay on. It used to be a button in the
|
||
// masking panel, so the mode could be open with the overlay off —
|
||
// which is a mode you have entered that is doing nothing.
|
||
//
|
||
// *Leaving* clears the selection, and that is the fault this whole
|
||
// pass exists for: a selected layer silently re-points thirty sliders
|
||
// at that layer's chain, so a mode you have left must not leave one
|
||
// behind. After this the controls are unambiguously global again,
|
||
// which is what the panel's heading then says.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let rows = rows.clone();
|
||
window.on_mode_picked(move |mode| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
match mode {
|
||
ViewMode::Crop => {
|
||
s.reset_zoom();
|
||
let c = s.crop();
|
||
w.set_crop_x(c.x);
|
||
w.set_crop_y(c.y);
|
||
w.set_crop_w(c.width);
|
||
w.set_crop_h(c.height);
|
||
}
|
||
ViewMode::Local => s.set_overlay(true),
|
||
// TRACES: FR-DEV-8
|
||
// Nothing to arm: the circles are drawn whenever there are
|
||
// repairs, and what the mode changes is whether a click on
|
||
// the photograph makes another one. Leaving the mask
|
||
// selection behind would re-point the column at a layer's
|
||
// chain while the canvas is showing repairs, which is the
|
||
// fault this whole strip exists to prevent.
|
||
ViewMode::Spots => {
|
||
s.set_overlay(false);
|
||
s.set_active_mask(None);
|
||
}
|
||
ViewMode::Photo => {
|
||
s.set_overlay(false);
|
||
s.set_active_mask(None);
|
||
// A repair stays on the photograph; only the *selection*
|
||
// goes, so the source circle does not hang about over a
|
||
// frame nobody is repairing any more.
|
||
s.select_spot(None);
|
||
}
|
||
}
|
||
}
|
||
w.global::<Develop>().set_view_mode(mode);
|
||
masks_ui::sync(&w, &session);
|
||
if let Some(s) = session.borrow().as_ref() {
|
||
spots_ui::sync_handles(&w, s);
|
||
}
|
||
// The scope may have just changed, so the panel below is now
|
||
// describing a different chain.
|
||
sync_rows(&w, &rows, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
// The rect arrives raw from the drag; the session normalises it, holds
|
||
// it to whatever ratio is locked, and the properties are written back
|
||
// from what it actually stored. That round trip is what makes an
|
||
// over-drag slide along the edge rather than letting the overlay and
|
||
// the pipeline disagree — and it is what lets the lock reshape a drag
|
||
// without the overlay having to know a ratio exists.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let crop_aspect = crop_aspect.clone();
|
||
let crop_portrait = crop_portrait.clone();
|
||
window.on_crop_changed(move |x, y, width, height, hx, hy| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
let rect = dr_pipeline::CropRect {
|
||
x,
|
||
y,
|
||
width,
|
||
height,
|
||
};
|
||
// The overlay reports the corner it is *holding*; the point
|
||
// that must not move is the opposite one. A move reports no
|
||
// corner at all, and keeps the shape it already has — there is
|
||
// nothing to reshape, and reshaping about a centre would drag
|
||
// an over-moved rect smaller instead of sliding it along the
|
||
// edge.
|
||
if hx < 0.0 {
|
||
s.set_crop(rect);
|
||
} else {
|
||
s.set_crop_locked(
|
||
rect,
|
||
crop_aspect.get(),
|
||
crop_portrait.get(),
|
||
(1.0 - hx, 1.0 - hy),
|
||
);
|
||
}
|
||
let c = s.crop();
|
||
w.set_crop_x(c.x);
|
||
w.set_crop_y(c.y);
|
||
w.set_crop_w(c.width);
|
||
w.set_crop_h(c.height);
|
||
w.global::<Framing>().set_modified(s.framing_edits_image());
|
||
}
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
// TRACES: FR-DEV-3
|
||
// Choosing a ratio reshapes the crop there and then rather than
|
||
// waiting for the next drag. A lock that only took effect on the
|
||
// following gesture would leave the chip lit over a rect that is not
|
||
// that shape, which is the panel lying about the image.
|
||
//
|
||
// Held about the rect's centre, so the composition stays where it is
|
||
// and the shape changes around it.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let crop_aspect = crop_aspect.clone();
|
||
let crop_portrait = crop_portrait.clone();
|
||
window.global::<Framing>().on_aspect_picked(move |index| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
let Some(&aspect) = develop::CropAspect::CHOICES.get(index.max(0) as usize) else {
|
||
return;
|
||
};
|
||
crop_aspect.set(aspect);
|
||
// A ratio with no second orientation cannot stay stood on its
|
||
// short edge, or the switch would be off and the crop upright.
|
||
if !aspect.has_orientation() {
|
||
crop_portrait.set(false);
|
||
}
|
||
sync_crop_aspect(&w, &crop_aspect, &crop_portrait);
|
||
apply_crop_aspect(&w, &session, &crop_aspect, &crop_portrait);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let crop_aspect = crop_aspect.clone();
|
||
let crop_portrait = crop_portrait.clone();
|
||
window.global::<Framing>().on_portrait_toggled(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if !crop_aspect.get().has_orientation() {
|
||
return;
|
||
}
|
||
crop_portrait.set(!crop_portrait.get());
|
||
sync_crop_aspect(&w, &crop_aspect, &crop_portrait);
|
||
apply_crop_aspect(&w, &session, &crop_aspect, &crop_portrait);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
}
|
||
|
||
/// ---- rotation, flips and straightening -------------------------------
|
||
///
|
||
/// Framing edits, so unlike zoom and pan they mark the image modified — but
|
||
/// they are reached through named session actions rather than through a row
|
||
/// index, so they do not `sync_rows` either. `sync_framing` is what carries
|
||
/// the applied value back.
|
||
fn wire_rotation_flips_straighten(
|
||
window: &AppWindow,
|
||
w: &DevelopWiring,
|
||
crop_aspect: &Rc<Cell<develop::CropAspect>>,
|
||
crop_portrait: &Rc<Cell<bool>>,
|
||
) {
|
||
let session = w.session.clone();
|
||
let redraw = w.redraw.clone();
|
||
let crop_aspect = crop_aspect.clone();
|
||
let crop_portrait = crop_portrait.clone();
|
||
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
let crop_aspect = crop_aspect.clone();
|
||
let crop_portrait = crop_portrait.clone();
|
||
window.global::<Framing>().on_rotate(move |turns| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.rotate_quarters(turns);
|
||
}
|
||
// TRACES: FR-DEV-3
|
||
// The crop travels with the frame, so a rect locked to 16:9 comes
|
||
// out of the turn at 9:16. The switch has to agree, or the next
|
||
// drag would snap it back upright and undo what the turn did to
|
||
// the composition. An even number of turns lands where it started.
|
||
let aspect = crop_aspect.get();
|
||
if aspect.turns_with_the_frame() && turns.rem_euclid(2) != 0 {
|
||
crop_portrait.set(!crop_portrait.get());
|
||
sync_crop_aspect(&w, &crop_aspect, &crop_portrait);
|
||
}
|
||
sync_framing(&w, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
window.global::<Framing>().on_flip_h_toggled(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.toggle_flip_h();
|
||
}
|
||
sync_framing(&w, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
window.global::<Framing>().on_flip_v_toggled(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.toggle_flip_v();
|
||
}
|
||
sync_framing(&w, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
window.global::<Framing>().on_angle_changed(move |degrees| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.set_angle(degrees);
|
||
}
|
||
sync_framing(&w, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
// TRACES: FR-DEV-3
|
||
// **The end of a straightening gesture crops away the corners it
|
||
// exposed.** A free angle does not change the output size, by design —
|
||
// that is what leaves the frame where the user put it while the slider
|
||
// moves — so the corners of the finished photograph would otherwise be
|
||
// black wedges of undefined area.
|
||
//
|
||
// Only on release, and only on release. Doing it per frame ratchets
|
||
// the crop down through every angle the slider passed through rather
|
||
// than the one it stopped at; see `auto_crop_to_angle`.
|
||
//
|
||
// Hung off the track's `committed`, not off `engaged-changed`. The
|
||
// latter is *hover* — it has to be, because a `Flickable` withholds
|
||
// the press — so a correction keyed on it fires when the pointer first
|
||
// crosses the track, whether or not anything was dragged, and then
|
||
// never fires again for as long as the pointer stays on it. Which
|
||
// reads, exactly, as an auto-crop that works once.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
window
|
||
.global::<Framing>()
|
||
.on_angle_committed(move |_degrees| {
|
||
// The angle itself is already applied: `SliderTrack` emits its
|
||
// last `changed` before it commits, so `on_straighten_changed` has
|
||
// run with this very value. What is left is the correction that
|
||
// has to happen exactly once.
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.auto_crop_to_angle();
|
||
}
|
||
sync_framing(&w, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
// The geometry section's reset: crop, angle, rotation and flips back
|
||
// to neutral, leaving every colour adjustment where it is. The panel's
|
||
// own reset-all is the one that clears everything.
|
||
let weak = window.as_weak();
|
||
let session = session.clone();
|
||
let redraw = redraw.clone();
|
||
window.global::<Framing>().on_reset(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
if let Some(s) = session.borrow_mut().as_mut() {
|
||
s.reset_framing();
|
||
}
|
||
sync_framing(&w, &session);
|
||
redraw(&w);
|
||
});
|
||
}
|
||
}
|
||
|
||
/// Stepping through the browsing list, one image at a time.
|
||
fn wire_navigation(window: &AppWindow, w: &DevelopWiring) {
|
||
let index = w.index.clone();
|
||
let entries = w.entries.clone();
|
||
let show = w.show.clone();
|
||
|
||
{
|
||
let weak = window.as_weak();
|
||
let index = index.clone();
|
||
let entries = entries.clone();
|
||
let show = show.clone();
|
||
window.on_next_image(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
let len = entries.borrow().len();
|
||
if len == 0 {
|
||
return;
|
||
}
|
||
// Read, then write — `*x.borrow_mut() = *x.borrow() + 1` holds
|
||
// both borrows at once and panics.
|
||
let next = {
|
||
let cur = *index.borrow();
|
||
(cur + 1) % len
|
||
};
|
||
*index.borrow_mut() = next;
|
||
show(&w);
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let index = index.clone();
|
||
let entries = entries.clone();
|
||
let show = show.clone();
|
||
window.on_prev_image(move || {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
let len = entries.borrow().len();
|
||
if len == 0 {
|
||
return;
|
||
}
|
||
let prev = {
|
||
let cur = *index.borrow();
|
||
if cur == 0 {
|
||
len - 1
|
||
} else {
|
||
cur - 1
|
||
}
|
||
};
|
||
*index.borrow_mut() = prev;
|
||
show(&w);
|
||
});
|
||
}
|
||
}
|
||
|
||
/// TRACES: FR-CULL-3
|
||
/// The peaking switch and its two choices.
|
||
///
|
||
/// All three write `chosen_peaking` and then redraw, because the marks are
|
||
/// produced by a compute pass over the rendered frame: there is nothing the
|
||
/// interface can change about the overlay that does not require the frame
|
||
/// to be measured again. Turning peaking *off* redraws for the same reason
|
||
/// — that render is what drops the overlay textures and clears the flag.
|
||
fn wire_peaking(window: &AppWindow, w: &DevelopWiring) {
|
||
let chosen_peaking = w.chosen_peaking.clone();
|
||
let redraw = w.redraw.clone();
|
||
|
||
{
|
||
let weak = window.as_weak();
|
||
let chosen = chosen_peaking.clone();
|
||
let redraw = redraw.clone();
|
||
window.global::<Peaking>().on_toggled(move |on| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
// Built from the chips as they currently stand rather than from a
|
||
// remembered value: they are what the photographer can see, and an
|
||
// overlay that came back in a configuration the panel is not
|
||
// showing would be the panel lying about itself.
|
||
let next = on.then(|| dr_gpu::FocusPeaking {
|
||
sensitivity: peaking::sensitivity(w.global::<Peaking>().get_sensitivity()),
|
||
colour: peaking::colour(w.global::<Peaking>().get_colour()),
|
||
});
|
||
chosen.set(next);
|
||
w.global::<Peaking>().set_showing(next.is_some());
|
||
redraw(&w);
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let chosen = chosen_peaking.clone();
|
||
let redraw = redraw.clone();
|
||
window
|
||
.global::<Peaking>()
|
||
.on_sensitivity_picked(move |index| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
w.global::<Peaking>().set_sensitivity(index);
|
||
// Only reachable while peaking is on — the chips are not drawn
|
||
// otherwise — but written as a conditional rather than an
|
||
// `expect`, because a panel is free to change its mind about that
|
||
// and nothing here should fall over when it does.
|
||
if let Some(mut current) = chosen.get() {
|
||
current.sensitivity = peaking::sensitivity(index);
|
||
chosen.set(Some(current));
|
||
redraw(&w);
|
||
}
|
||
});
|
||
}
|
||
{
|
||
let weak = window.as_weak();
|
||
let chosen = chosen_peaking.clone();
|
||
let redraw = redraw.clone();
|
||
window.global::<Peaking>().on_colour_picked(move |index| {
|
||
let Some(w) = weak.upgrade() else { return };
|
||
w.global::<Peaking>().set_colour(index);
|
||
if let Some(mut current) = chosen.get() {
|
||
current.colour = peaking::colour(index);
|
||
chosen.set(Some(current));
|
||
redraw(&w);
|
||
}
|
||
});
|
||
}
|
||
// The chips open on whatever the vocabulary calls its default, so the
|
||
// panel and the pass agree before anything has been pressed.
|
||
window
|
||
.global::<Peaking>()
|
||
.set_sensitivity(peaking::sensitivity_index(Default::default()));
|
||
window
|
||
.global::<Peaking>()
|
||
.set_colour(peaking::colour_index(Default::default()));
|
||
}
|