Files
DarkRoom/ui/dr-ui/ui/develop.slint
T
dtourolleandClaude Opus 5 c4ddcbe0f7 Look the lens up and say plainly whether one was found
`dr-lens` has held a complete Lensfun lookup — distortion, TCA and vignetting
coefficients from a lens name, a focal length and an aperture — with no
dependents anywhere in the workspace. The three corrections it feeds now
exist in the graph, so this connects the two and finishes the chain.

The coefficient structs stay duplicated. `dr-pipeline` is organised around
having no dependencies so its codegen is testable without a device or a
database (ARCH §6.5a), and `dr-lens` carries an XML parser and 5.5 MB of
profile data. Neither crate can convert to the other, so the conversion goes
above both, in `develop.rs`, which is the only place that sees them together.

Both traits grow the same defaulted door. The optical corrections do not sit
on the same side of the fetch — distortion and CA rewrite coordinates and are
`Warp`s, vignetting applies a gain to the pixel already there and is an
ordinary node — and fanning a profile out by which trait each happens to
implement would make the caller reason about that distinction. Each correction
takes its own share of the whole profile instead, and `set_lens_profile` walks
both lists identically.

The lookup happens in `set_source_metadata` rather than in its caller, because
that is the one place a session is told which file it came from. Doing it
there makes it unforgettable, in the shape `FilmRebake` already uses for the
other derived thing — and, more to the point, makes *clearing* unforgettable:
a session that opened a second photograph while still holding the first one's
profile would correct it for the wrong optics, invisibly, in a way that looks
exactly like the lens.

It needs the whole shot and not just a name. Distortion is interpolated across
a zoom's focal range and vignetting depends strongly on aperture — a fast
prime can be two stops down in the corners wide open and clean by f/8 — so a
lookup missing either returns coefficients measured for a shot nobody took.
Missing any of the three refuses rather than guesses.

A profile is derived, not persisted: it comes from the file's EXIF and a
database, so it is not a parameter, not in the sidecar and not undoable. What
is an edit is the manual trim beside it, which each correction composes with
the measurement — so a photographer can lean on it, override it, or work
without one.

`InfoPanel` gains a lens line, and it distinguishes three cases rather than
two. `dr-lens` states the rule it exists for: an automatic correction that
silently did nothing is worse than one the user can see is unavailable. A
session with no header draws nothing, a header naming no lens reads "Lens not
recorded", and a lens the database has never heard of reads "· no profile".
Collapsing the last two would send somebody hunting for a profile that was
never missing — which, for third-party and adapted glass, is the ordinary case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 15:11:32 +02:00

266 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The develop view's own chrome: the strip across the top of the window and
// the capture-metadata block in the column beside the canvas.
//
// Split out of `app.slint`, where they sat above `AppWindow` and were read as
// part of the application shell. They are not: neither is instantiated
// anywhere but the develop view, and the shell's job — choosing which of the
// five screens is up — is easier to read without two unrelated components in
// front of it.
import { Theme } from "theme.slint";
import { Button, PanelHeading, Label, Value, Caption, Panel } from "widgets.slint";
// Status strip — surfaces the GPU backend and adapter, which matters during
// v0.1 because assumption A1 is exactly "does this compositing path work on
// this hardware". Seeing the adapter at a glance makes vendor differences
// obvious during the S1/S2 spikes.
export component StatusBar inherits Rectangle {
in property <string> adapter;
in property <string> backend;
in property <string> layout-class;
in property <int> fps;
/// FR-UI-1: the layout class, so the instrumentation can stand down on a
/// strip too narrow to carry both it and the controls.
in property <bool> expanded: true;
in property <string> filename;
in property <string> position;
/// Only offered where there is a library to go back to — with files named
/// on the command line there is no grid behind this view.
in property <bool> can-return-to-library: false;
/// Whether the develop column is currently shown, for the toggle's label.
in property <bool> panel-visible: true;
/// What the export button says. Rust owns it because the answer depends
/// on settings this component does not see — the format, and whether the
/// destination is this device or the server.
in property <string> export-label: "Export";
in property <bool> export-busy: false;
/// What the last export did. Sits beside the button rather than in a
/// dialogue: an export that succeeded needs no acknowledging, and one
/// that failed needs its reason where the retry is.
in property <string> export-status;
/// Whether the edit can be stepped either way (FR-DEV-5).
in property <bool> can-undo: false;
in property <bool> can-redo: false;
callback back-to-library();
callback open-people();
callback open-settings();
callback toggle-panel();
callback export-image();
callback undo();
callback redo();
// 44px and `surface`, the same bar the library and settings draw.
//
// It was 28px — the control height — which was a reasonable size for a
// readout and the wrong size for the thing it actually is: this strip
// carries the only way out of develop, and it was half the height of the
// identical control on the two screens either side of it. Three screens
// whose headers do not agree read as three applications, and the one place
// the user needs to find a way back was the one drawn smallest.
//
// Controls keep their own 28px and are centred in it, exactly as the
// library header centres the same buttons: a `Button` sets a fixed height,
// which a HorizontalLayout parks at the top of the row rather than
// centring for it.
height: 44px;
background: Theme.surface;
// Scrolls rather than overflowing. A layout given less width than its
// children need does not shrink them — it overruns the edge and reports
// the oversized minimum to whatever contains it. Collapsing the
// instrumentation below buys back about 200 logical pixels, which is
// enough for a tablet; it is not enough for a phone, and nothing here
// should be unreachable merely because the window is narrow.
Flickable {
width: 100%;
height: 100%;
viewport-height: self.height;
viewport-width: max(self.width, strip.preferred-width);
strip := HorizontalLayout {
width: parent.viewport-width;
height: parent.viewport-height;
padding-left: Theme.gap;
padding-right: Theme.gap;
spacing: Theme.gap;
alignment: start;
// Kept instantiated with `visible` rather than wrapped in an `if`:
// the strip is inside the layout that `expanded` feeds, and adding a
// conditional child here is the shape that has already caused binding
// loops in this file (see the panel below).
Button {
text: "‹ Library";
y: (parent.height - self.height) / 2;
visible: root.can-return-to-library;
clicked => { root.back-to-library(); }
}
// The third screen, reachable from the second. Beside the way back
// because it is the same kind of move — leaving this photograph for
// somewhere else in the library — and because a mode you can only
// reach from one of the other two is not a peer of them, whatever the
// navigation model says.
Button {
text: "Identity";
y: (parent.height - self.height) / 2;
visible: root.can-return-to-library;
clicked => { root.open-people(); }
}
Value { text: root.filename; compact: true; overflow: elide; }
Caption { text: root.position; }
Rectangle { horizontal-stretch: 1; }
// --- instrumentation lives in Settings now ------------------------
//
// The render backend, the layout class and the frame rate are
// developer readouts. They cost about 200 logical pixels of a strip
// that also carries the only way out of develop, the undo pair, the
// panel toggle and the export button — and a `HorizontalLayout` given
// less width than its children's minimums does not shrink them, it
// runs off the end.
//
// A tablet in portrait is 768 logical pixels and was already below the
// breakpoint that hid them, so it kept its header. Landscape is 1200
// and is not, so the readouts came back and the header ran off the
// right-hand edge there instead — a breakpoint that hides a problem at
// one size and not another is a workaround, not a fix.
//
// The backend, the frame rate and the layout class used to live here.
// They are diagnostics — read once when something looks wrong, and
// never again — and as permanent furniture in a 44px strip they cost
// three slots that the controls beside them actually needed. On a
// tablet in landscape the header ran off the right-hand edge, and this
// was a third of the reason.
//
// They are in Settings under ABOUT now, next to the version, which is
// where someone goes when they have a bug to report rather than a
// photograph to edit.
// Undo and redo, in the strip rather than in the develop column: the
// panel can be put away, and the one control that takes back a
// mis-drag must not go away with it. Disabled rather than hidden, so
// the pair keeps its place and the keyboard shortcut has something
// visible to correspond to.
Button {
text: "Undo";
enabled: root.can-undo;
y: (parent.height - self.height) / 2;
clicked => { root.undo(); }
}
Button {
text: "Redo";
enabled: root.can-redo;
y: (parent.height - self.height) / 2;
clicked => { root.redo(); }
}
// Show or hide the develop column. On a tablet the panel is 280px of a
// screen that is mostly photograph, and the whole point of opening an
// image is to look at it — so being able to put the instruments away
// and bring them back is worth a control of its own. The glyph points
// the way the panel will move.
Button {
text: root.panel-visible ? "Hide panel ›" : "‹ Panel";
active: root.panel-visible;
y: (parent.height - self.height) / 2;
clicked => { root.toggle-panel(); }
}
Caption {
text: root.export-status;
vertical-alignment: center;
overflow: elide;
}
// The export itself, beside the settings that shape it. This is the
// screen where a photograph is finished, so it is the screen where
// one is asked for — the grid can export a selection later, but a
// single finished frame is exported from in front of it.
Button {
text: root.export-busy ? "Exporting…" : root.export-label;
enabled: !root.export-busy;
y: (parent.height - self.height) / 2;
clicked => { root.export-image(); }
}
// Reachable from develop as well as from the grid: export defaults are
// most likely to be wanted with a finished photograph on screen, which
// is exactly where this bar is and the library header is not.
Button {
text: "Settings";
y: (parent.height - self.height) / 2;
clicked => { root.open-settings(); }
}
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: Theme.rule;
}
}
// Capture metadata. Read-only; the adjustment controls live in AdjustPanel,
// which is generated from pipeline capabilities rather than written here.
//
// Flat, like everything else in this column. This was briefly a collapsible
// section so that every panel could be put away; the sidebar as a whole now
// closes from the status strip instead, which is the control that was actually
// wanted, and the per-group lids only stood between the user and the controls.
export component InfoPanel inherits Rectangle {
in property <string> camera;
/// TRACES: FR-DEV-3
/// The lens, and whether it matched a correction profile.
///
/// Here rather than beside the optical sliders, and the reason is what the
/// line is *for*. `dr-lens` states the rule — an automatic correction that
/// silently did nothing is worse than one the user can see is unavailable
/// — and the fact it reports is a fact about the file: which lens took
/// this photograph, and whether the database has heard of it. That is the
/// same kind of thing as the body and the exposure, and it wants reading
/// once on opening rather than hunting for under a group filter.
in property <string> lens;
in property <string> exposure;
in property <string> dimensions;
background: transparent;
height: panel.preferred-height;
panel := Panel {
// Flat: this abuts the adjust panel below it and the rule between
// them is drawn by the column that stacks the two.
flat: true;
width: 100%;
PanelHeading { text: "IMAGE"; }
Value {
text: root.camera == "" ? "—" : root.camera;
placeholder: root.camera == "";
wrap: word-wrap;
}
Label { text: root.exposure; }
// Dim, like the dimensions below it: this is something the file says,
// not something the photographer chose. An empty string draws nothing
// — a session built from pixels with no header has no lens to report,
// and an empty row is honest where "Unknown" would be noise.
Caption {
text: root.lens;
visible: root.lens != "";
wrap: word-wrap;
}
Caption { text: root.dimensions; }
}
}