Files
DarkRoom/ui/dr-ui/ui/develop.slint
T
dtourolle eae720ce75 Say which camera profile a photograph renders through, and offer to copy it
The info panel gains a line under the lens: "Adobe Standard · in the
file", the .dcp it came from, "· off" when the photographer switched it
off, or "No camera profile · matrix only" — the ordinary case for a
CR2, worded as a fact rather than a failure. A DNG whose embedded
profile may be copied, for a body with no installed profile, also gets
"Use this profile for every Canon EOS 6D →", which saves it into the
profiles directory; the body's CR2s render through it from their next
decode.

The profiles directory is <data>/profiles, read at start-up on desktop
and Android before anything decodes. The library open path never set
the lens line; it now sets both. Labels: Camera Profile, Use Profile,
Look Amount.
2026-10-02 22:38:07 -04:00

482 lines
20 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 { Develop } from "session.slint";
import { Transfer } from "adjust.slint";
import { Button, IconButton, PanelHeading, Label, Value, Caption, Panel } from "widgets.slint";
import { Labels, LabelMark, LabelPicker } from "labels.slint";
import { StarStrip } from "library.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;
/// TRACES: FR-CAT-5
/// The open photograph's colour label code, 0 for none, -1 where there
/// is no library to hold one (files named on the command line).
in property <int> colour-label: -1;
/// Whether the label choices are open in the strip.
property <bool> picking-label: false;
callback label-picked(int);
/// TRACES: FR-UI-5 | FR-CULL-4
/// The open photograph's stars, or -1 where there is no library to hold
/// them, and its flag: 0 none, 1 pick, 2 reject.
in property <int> rating: -1;
in property <int> flag: 0;
callback rated(int);
callback flagged(int);
callback back-to-library();
callback open-people();
callback open-settings();
/// TRACES: FR-UI-4
/// Open the gesture reference, which the window draws.
callback open-help();
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);
// Related controls sit closer together than unrelated ones: Pick and
// Reject, Undo and Redo, Copy and Paste, Help and Settings are
// each a group at `gap-sm`, with `gap` between groups. It reads as the
// pairs it is, and it is also what keeps Settings on screen at 1600
// pixels wide once Help joined the strip.
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; }
// TRACES: FR-CAT-5 | FR-UI-4 | NFR-A11Y-3
// The label, shown and set here. The button says the label's name
// beside its mark, so the strip reads "Label: Green" in words; with
// none it offers "Label". Pressing it opens the six choices in the
// strip, which close on the one chosen.
Button {
text: root.colour-label > 0 ? "Label: " + Labels.names[root.colour-label] : "Label";
active: root.picking-label;
visible: root.colour-label >= 0;
y: (parent.height - self.height) / 2;
clicked => { root.picking-label = !root.picking-label; }
}
if root.colour-label > 0: LabelMark {
code: root.colour-label;
y: (parent.height - self.height) / 2;
}
if root.picking-label && root.colour-label >= 0: LabelPicker {
current: root.colour-label;
chosen(code) => {
root.label-picked(code);
root.picking-label = false;
}
}
// TRACES: FR-UI-5 | FR-CULL-4 | NFR-A11Y-3
// The judgement, shown and set on the photograph that is open — the
// pointer and touch half of the rating and flag keys, which a tablet
// has no number row for (FR-UI-5, amended 2026-09-19). The grid's own
// stars, so a rating looks the same in both places; the star already
// set clears it, as there.
StarStrip {
rating: max(0, root.rating);
show-empty: true;
can-trash: false;
visible: root.rating >= 0;
y: (parent.height - self.height) / 2;
rate(n) => { root.rated(n); }
}
HorizontalLayout {
spacing: Theme.gap-sm;
// Pressed again, a flag comes off: two buttons that each toggle,
// rather than a third for "none" that is only ever pressed to undo
// one of the other two.
Button {
text: root.flag == 1 ? "Picked" : "Pick";
active: root.flag == 1;
visible: root.rating >= 0;
y: (parent.height - self.height) / 2;
clicked => { root.flagged(root.flag == 1 ? 0 : 1); }
}
Button {
text: root.flag == 2 ? "Rejected" : "Reject";
active: root.flag == 2;
visible: root.rating >= 0;
y: (parent.height - self.height) / 2;
clicked => { root.flagged(root.flag == 2 ? 0 : 2); }
}
}
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.
HorizontalLayout {
spacing: Theme.gap-sm;
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(); }
}
}
// TRACES: FR-DEV-6
// Copy and paste, in the strip beside undo for the reason undo is
// here: they act on the whole edit, not on any one panel, and the
// column can be put away. They sat in the column under a "SETTINGS"
// heading, which read as application settings and hid them behind a
// scroll and behind the panel toggle.
//
// The paste button names what would be pasted — "3 adjustments", and
// whether the crop is staying behind — because "Paste" alone asks the
// user to remember what they copied.
HorizontalLayout {
spacing: Theme.gap-sm;
Button {
text: "Copy";
enabled: Develop.enabled;
y: (parent.height - self.height) / 2;
clicked => { Transfer.copy(); }
}
// Enabled on `armed` rather than on a non-empty summary, so pasting a
// neutral copy — which clears this image — stays available.
Button {
text: !Transfer.armed ? "Paste"
: "Paste " + Transfer.summary
+ (Transfer.framing-withheld ? ", no crop" : "");
enabled: Develop.enabled && Transfer.armed;
y: (parent.height - self.height) / 2;
clicked => { Transfer.paste(); }
}
// The saved half — this edit, kept under a name — is at the foot
// of the tool rail now (`toolrail.slint`), as a menu beside the
// photograph rather than a sheet over it.
}
// 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(); }
}
// Only while there is something to say. Empty, it still took a slot
// and the spacing either side of it, and this strip has no pixels to
// spare: at 1600 wide they are what kept Settings on screen.
if root.export-status != "": 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(); }
}
HorizontalLayout {
spacing: Theme.gap-sm;
// TRACES: FR-UI-4
// The pointer and touch way to the reference F1 opens. Beside
// Settings, as Help is in the library header, because it is about the
// application rather than about the photograph.
//
// A square "?" rather than a word: this strip is already full at
// 1600 pixels, and a labelled button pushed Settings off its end.
// Screen readers still hear "Help".
IconButton {
icon: "help";
label: "Help";
y: (parent.height - self.height) / 2;
clicked => { root.open-help(); }
}
// 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;
}
}
/// What the file says about how the photograph was taken.
///
/// A global rather than four properties on the panel, for the reason
/// `session.slint` gives at length: the panel is drawn twice — once in the
/// develop column and once in the portrait dock — and four lines of forwarding
/// copied into the second composition is four lines that can fall behind the
/// first without anything failing.
///
/// Nothing here is a control. It is the one panel in the column that only
/// reports, which is why this global carries no callback at all.
export global Capture {
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;
/// TRACES: FR-DEV-3e
/// The camera profile the photograph renders through, and where it was
/// found — or that there is none (D20). A fact about the file and the
/// installed profiles, read once on opening, as the lens line is.
in property <string> profile;
in property <string> exposure;
in property <string> dimensions;
}
/// TRACES: FR-DEV-3e
/// The one action the info panel offers: saving the camera profile embedded
/// in this file for every photograph from the same body (D20).
///
/// A global of its own because `Capture` only reports and carries no
/// callback. `offer` is the wording, naming the body; empty when there is
/// nothing to offer, which is every file but a DNG whose profile may be
/// copied and is not installed yet.
export global ProfileOffer {
in property <string> offer;
callback adopt();
}
// The offer as a line of text that acts, rather than a button: a button
// does not wrap, and a label naming a camera is long enough to widen the
// whole column.
component OfferLine inherits Rectangle {
in property <string> text;
callback clicked();
accessible-role: button;
accessible-label: root.text;
accessible-action-default => { root.clicked(); }
height: line.preferred-height;
line := Caption {
width: 100%;
text: root.text + " →";
wrap: word-wrap;
emphasised: touch.has-hover;
}
touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.clicked(); }
}
}
// 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 {
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: Capture.camera == "" ? "—" : Capture.camera;
placeholder: Capture.camera == "";
wrap: word-wrap;
}
Label { text: Capture.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: Capture.lens;
visible: Capture.lens != "";
wrap: word-wrap;
}
Caption {
text: Capture.profile;
visible: Capture.profile != "";
wrap: word-wrap;
}
if ProfileOffer.offer != "": OfferLine {
text: ProfileOffer.offer;
clicked => { ProfileOffer.adopt(); }
}
Caption { text: Capture.dimensions; }
}
}