Let the photographer look at the pixels, and keep looking
Noise reduction and capture sharpening are judgements about individual pixels, and at a fitted view several of the file's pixels are averaged into each one on screen. The frame therefore looks cleaner and softer than it is, the photographer corrects for a softness the display invented, and over-sharpening is the documented result. Nothing in the develop view reached 1:1 at all: the wheel and the pinch zoom by ratios, the double tap dropped straight to fit, and the only readout was a percentage nobody was aiming at. So the double tap now does what FR-UI-4 always said it did — toggle fit and 1:1 — and the zoom readout, which used to be a dead "Fit" button on a fitted photograph, becomes the way in when there is nothing to clear. Z does the same from the keyboard, and the back gesture goes out through the same toggle so putting the magnifier down really puts it down. 1:1 is computed from the file's own resolution against the viewport rather than fixed at some multiple, because that is the only version of it that answers the question the two detail controls are asking. The point being inspected and whether the magnifier is up are held beside the session rather than in it, on the argument focus peaking already makes: a session is one photograph and this is a way of looking at a folder of them. Checking the same eye across forty portraits is the reason to reach 1:1 in the first place, and a magnification that reset with the session would make that forty zooms and forty pans instead of forty keystrokes. It stays a viewing state throughout — the view is kept out of `is_active`, `output_size`, the sidecar and the export, and an export still suspends it — so none of this reaches the file.
This commit is contained in:
+147
-3
@@ -398,6 +398,68 @@ fn reset_view_state(window: &AppWindow) {
|
||||
spots_ui::reset(window);
|
||||
}
|
||||
|
||||
/// TRACES: FR-UI-4
|
||||
/// Where the 1:1 view is pointing, and whether it is currently on.
|
||||
///
|
||||
/// **Two facts rather than an `Option`, and the second one is why.** Returning
|
||||
/// to fit turns the magnifier off; it does not mean the photographer has
|
||||
/// stopped caring about the corner they were inspecting. An `Option` emptied
|
||||
/// on the way out would send the next press of the same control back to the
|
||||
/// centre of the frame, which is the one place nobody was looking.
|
||||
///
|
||||
/// The point is in fractions of the *framed* image, because that is the only
|
||||
/// coordinate system two different photographs share.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Inspection {
|
||||
at: (f32, f32),
|
||||
on: bool,
|
||||
}
|
||||
|
||||
impl Default for Inspection {
|
||||
fn default() -> Self {
|
||||
// The centre, which is where a magnifier that has never been aimed
|
||||
// points. `(0.0, 0.0)` would be the top-left corner — a real place,
|
||||
// and never the one meant.
|
||||
Self {
|
||||
at: (0.5, 0.5),
|
||||
on: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-UI-4
|
||||
/// Put a newly opened photograph under the magnifier the last one was under.
|
||||
///
|
||||
/// The other half of `reset_view_state`, and deliberately not part of it: that
|
||||
/// function empties what belonged to one photograph, and the inspection point
|
||||
/// belongs to the *pass*. Checking the same eye on forty portraits is why a
|
||||
/// 1:1 view is worth reaching at all, and a magnification that reset with the
|
||||
/// session would make it forty zooms and forty pans instead of forty
|
||||
/// keystrokes.
|
||||
///
|
||||
/// Called after the stored edit has landed rather than before it, because the
|
||||
/// point is in fractions of the *framed* image: a sidecar carrying a crop
|
||||
/// changes what a fraction refers to, and inspecting before it arrived would
|
||||
/// land somewhere the photograph does not have any more.
|
||||
///
|
||||
/// Silently does nothing where the file could not be opened for editing —
|
||||
/// there is no view to place, and the next photograph that does open is still
|
||||
/// inspected.
|
||||
fn resume_inspection(
|
||||
session: &Rc<RefCell<Option<DevelopSession>>>,
|
||||
viewport: &Rc<RefCell<(u32, u32)>>,
|
||||
inspection: &Rc<Cell<Inspection>>,
|
||||
) {
|
||||
let held = inspection.get();
|
||||
if !held.on {
|
||||
return;
|
||||
}
|
||||
let (vw, vh) = *viewport.borrow();
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.inspect_at(held.at.0, held.at.1, vw, vh);
|
||||
}
|
||||
}
|
||||
|
||||
/// Push the framing back to the geometry panel.
|
||||
///
|
||||
/// Separate from [`sync_rows`] because framing is no longer *in* the rows —
|
||||
@@ -1646,6 +1708,25 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
// histogram beside it.
|
||||
let chosen_peaking: Rc<Cell<Option<dr_gpu::FocusPeaking>>> = Rc::new(Cell::new(None));
|
||||
|
||||
// TRACES: FR-UI-4
|
||||
// The point being inspected at 1:1, in fractions of the framed image, or
|
||||
// `None` where the view is fitted.
|
||||
//
|
||||
// **Held here rather than on the session, on exactly the argument
|
||||
// `chosen_peaking` above makes.** A session is one photograph and this is
|
||||
// a way of looking at a folder of them: checking the same eye across forty
|
||||
// portraits is the entire reason a 1:1 view is worth reaching, and a
|
||||
// magnification that reset with the session would ask for the same zoom
|
||||
// and the same pan forty times. So `reset_view_state` empties the
|
||||
// histogram and leaves this alone, and the two open paths below put the
|
||||
// next photograph under the magnifier the last one was left under.
|
||||
//
|
||||
// It is a *viewing* state throughout. It never reaches the graph's crop,
|
||||
// the sidecar or an export — `Framing::view` is kept out of all three, and
|
||||
// `render_the_file` suspends it — so a photographer left at 4× exports the
|
||||
// whole frame, as they did before this existed.
|
||||
let inspection: Rc<Cell<Inspection>> = Rc::new(Cell::new(Inspection::default()));
|
||||
|
||||
let render_now: Render = {
|
||||
let session = session.clone();
|
||||
let viewport = viewport.clone();
|
||||
@@ -1964,6 +2045,8 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
let rows = rows.clone();
|
||||
let open_image = open_image.clone();
|
||||
let library_for_show = library.clone();
|
||||
let viewport_for_show = viewport.clone();
|
||||
let inspection_for_show = inspection.clone();
|
||||
Rc::new(move |window: &AppWindow| {
|
||||
// The image about to be replaced is the last chance to persist its
|
||||
// edit — stepping to the next frame is as much a departure as
|
||||
@@ -2023,6 +2106,14 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
presets::apply_stored_edit(window, &sidecar, &session, &rows);
|
||||
}
|
||||
|
||||
// TRACES: FR-UI-4
|
||||
// And under the same magnifier as the frame
|
||||
// before it, if one was up. After the sidecar,
|
||||
// because the point is a fraction of the framed
|
||||
// image and the sidecar is what decides how the
|
||||
// frame is cropped.
|
||||
resume_inspection(&session, &viewport_for_show, &inspection_for_show);
|
||||
|
||||
// Through `sync_rows` rather than setting rows
|
||||
// directly, so the curve's drawn shape is
|
||||
// refreshed by the same path that refreshes the
|
||||
@@ -2075,6 +2166,8 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
let gpu = gpu.clone();
|
||||
let activity = activity.clone();
|
||||
let open_image = open_image.clone();
|
||||
let viewport = viewport.clone();
|
||||
let inspection = inspection.clone();
|
||||
*open_from_library.borrow_mut() = Some(Rc::new(move |path: String| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
|
||||
@@ -2163,6 +2256,8 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
// may run again for the next photograph.
|
||||
let library = library.clone();
|
||||
let path = path.clone();
|
||||
let viewport = viewport.clone();
|
||||
let inspection = inspection.clone();
|
||||
let sidecar_rx = Rc::new(sidecar_rx);
|
||||
let timer = Rc::new(slint::Timer::default());
|
||||
let held = timer.clone();
|
||||
@@ -2248,6 +2343,16 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
&rows,
|
||||
&redraw,
|
||||
);
|
||||
// TRACES: FR-UI-4
|
||||
// Under the same magnifier as the last
|
||||
// frame, on the terms `resume_inspection`
|
||||
// sets out. The sidecar may still be in
|
||||
// flight here, unlike the local path — so
|
||||
// a photograph whose stored crop lands a
|
||||
// moment later is inspected against the
|
||||
// uncropped frame until it does, which
|
||||
// moves the point rather than losing it.
|
||||
resume_inspection(&session, &viewport, &inspection);
|
||||
sync_rows(&w, &rows, &session);
|
||||
redraw(&w);
|
||||
}
|
||||
@@ -2756,13 +2861,46 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
});
|
||||
}
|
||||
{
|
||||
// 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();
|
||||
window.on_zoom_reset(move || {
|
||||
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() {
|
||||
s.reset_zoom();
|
||||
// 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);
|
||||
});
|
||||
@@ -3446,7 +3584,13 @@ fn back_one_step(w: &AppWindow) -> bool {
|
||||
BackStep::CloseSettings => w.invoke_settings_close(),
|
||||
BackStep::CancelBrowse => w.invoke_launch_browse_cancel(),
|
||||
BackStep::LeaveMode => w.invoke_mode_picked(ViewMode::Photo),
|
||||
BackStep::ResetZoom => w.invoke_zoom_reset(),
|
||||
// TRACES: FR-UI-4
|
||||
// Through the toggle rather than through `zoom_reset`, because a
|
||||
// zoomed view is now something the photographer is *holding* across
|
||||
// photographs: putting it down has to put the magnifier down too, or
|
||||
// the next frame would open under a magnification that the back
|
||||
// gesture had just dismissed.
|
||||
BackStep::ResetZoom => w.invoke_inspect_toggled(-1.0, -1.0),
|
||||
BackStep::ToLibrary => w.invoke_back_to_library(),
|
||||
BackStep::ClearScope => w.invoke_collection_select(0),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user