Wire the develop panels through globals, so a second copy is one line
Every panel in the develop column declared its inputs and its callbacks and had `app.slint` bind each one to a property or a callback on the window root. That is fine while a panel is drawn once. N9 draws them a second time, in the portrait dock, and the wiring is what would have to be copied: `MaskPanel` alone ran to forty lines of forwarding, and a callback added to one copy and not the other compiles, renders, and simply does nothing on the layout nobody was looking at. So the wiring moved to Slint globals. A panel reads the global and calls the global; Rust hooks the global instead of the window; and the instantiation in the column is now the panel's name and a pair of braces — every one of the ten children of the column, with no property that differs by placement left to supply. There is a global per panel family rather than one for all of them, and the reason is an import cycle. Each panel's model struct — `ParamRow`, `MaskRow`, `HistogramView` — is declared in the panel's own file, so a single global holding `[MaskRow]` and `[ParamRow]` would have to live in a file importing `masks.slint` and `adjust.slint` while both imported the global back, which Slint rejects. Breaking that needs six model declarations relocated, which is a change to the data model and not to the plumbing this is about. A global beside the panel it serves also lets each name drop the prefix it was carrying only because the window root is one flat namespace: `root.spot-radius` is `Repair.radius`, and `root.peaking-on` is `Peaking.showing`. `session.slint` is new and holds the two facts every family needs and none of them owns: whether there is an open photograph to edit, and which mode the view is in, with the three readings of the mode derived once instead of at each of the dozen places that tested one. `ViewMode` moves there from `adjust.slint`, where it was only ever a lodger. Nothing on screen changes. What is not here: the tool rail and the status strip still take their properties at the instantiation, because they are drawn once and N9 does not copy them; the preset sheet's own state stays on the window, because the library grid opens the same sheet and a global cannot bind the window's state — which is why `Transfer.open-presets` is handled in `presets.rs`, beside the summary it already had to compute.
This commit is contained in:
+296
-234
@@ -8,6 +8,7 @@
|
||||
// (FR-DEV-3c).
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Develop } from "session.slint";
|
||||
import { PanelHeading, Label, Value, Caption, Button, IconButton, Swatch } from "widgets.slint";
|
||||
import { SliderTrack, ControlRow, CurveEditor, Segmented, ChoiceChip, ChipGrid, Check } from "controls.slint";
|
||||
|
||||
@@ -620,6 +621,66 @@ component ParamControl inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3
|
||||
/// The frame: its shape, its angle, and the two ways of turning it over.
|
||||
///
|
||||
/// A global rather than nine properties and eight callbacks on the panel — see
|
||||
/// `session.slint` for why the develop panels stopped taking their wiring
|
||||
/// through the window root.
|
||||
///
|
||||
/// Everything here is **mirrored from the session rather than held here**, for
|
||||
/// the same reason the crop rect is: Rust clamps and wraps these, so the panel
|
||||
/// must show what was actually applied and not what the click asked for.
|
||||
///
|
||||
/// The crop overlay's own state is not here. Whether the overlay is up is
|
||||
/// `Develop.cropping`, a reading of the view's mode, and the rect itself is
|
||||
/// dragged on the photograph rather than set from this panel.
|
||||
export global Framing {
|
||||
in property <float> angle: 0.0;
|
||||
/// The straighten slider's travel either way, from the descriptor.
|
||||
in property <float> max-straighten: 45.0;
|
||||
in property <bool> flip-h: false;
|
||||
in property <bool> flip-v: false;
|
||||
/// Any of crop, angle, rotation or flips differs from neutral — what
|
||||
/// lights the section's dot and enables its reset. Distinct from the crop
|
||||
/// rect being non-full: a rotation or a flip is an edit with the crop
|
||||
/// still full.
|
||||
in property <bool> modified: false;
|
||||
|
||||
/// TRACES: FR-DEV-3
|
||||
/// The ratios the crop may be locked to, and which one is chosen.
|
||||
///
|
||||
/// Named by Rust rather than listed here: the set is `CropAspect::CHOICES`
|
||||
/// and the labels come off it, so a ratio added there appears without this
|
||||
/// file changing.
|
||||
in property <[string]> aspects;
|
||||
in property <int> aspect: 0;
|
||||
/// Whether the chosen ratio is standing on its short edge.
|
||||
in property <bool> portrait: false;
|
||||
/// Whether the chosen ratio has a second orientation at all — `Free` and
|
||||
/// the square do not, and the switch says so rather than vanishing.
|
||||
in property <bool> aspect-turnable: false;
|
||||
|
||||
/// Quarter turns, positive clockwise.
|
||||
callback rotate(int);
|
||||
callback flip-h-toggled();
|
||||
callback flip-v-toggled();
|
||||
callback angle-changed(float);
|
||||
/// TRACES: FR-DEV-3
|
||||
/// The straightening gesture is over and this is the angle to keep.
|
||||
///
|
||||
/// What the auto-crop hangs off — see `DevelopSession::auto_crop_to_angle`
|
||||
/// for why it must be the end of the drag and not every frame of it, and
|
||||
/// `PlainSlider::committed` for why it must not be `drag-changed`.
|
||||
callback angle-committed(float);
|
||||
/// Lock the crop to one of `aspects`, by index.
|
||||
callback aspect-picked(int);
|
||||
/// Stand the chosen ratio on its other edge.
|
||||
callback portrait-toggled();
|
||||
/// Crop, angle, rotation and flips back to neutral, leaving colour alone.
|
||||
callback reset();
|
||||
}
|
||||
|
||||
// Crop, rotation, flips and straightening — the framing controls.
|
||||
//
|
||||
// **Why this is hand-built when the rest of the panel is generated.** The
|
||||
@@ -634,41 +695,6 @@ component ParamControl inherits Rectangle {
|
||||
// of a descriptor or routes by index. It calls named session actions, which is
|
||||
// what a bespoke widget for a known stage is entitled to do.
|
||||
export component ComposePanel inherits Rectangle {
|
||||
in property <bool> enabled: true;
|
||||
in property <float> angle: 0.0;
|
||||
in property <float> max-straighten: 45.0;
|
||||
in property <bool> flip-h: false;
|
||||
in property <bool> flip-v: false;
|
||||
/// Any of crop, angle, rotation or flips differs from neutral.
|
||||
in property <bool> modified: false;
|
||||
|
||||
/// TRACES: FR-DEV-3
|
||||
/// The crop overlay is up, so the ratio lock is a control for a gesture
|
||||
/// that is currently on screen.
|
||||
in property <bool> cropping: false;
|
||||
/// The ratios on offer, named by Rust — see `CropAspect::CHOICES`.
|
||||
in property <[string]> aspects;
|
||||
in property <int> aspect: 0;
|
||||
in property <bool> portrait: false;
|
||||
/// Whether the chosen ratio has a second orientation. `Free` and the
|
||||
/// square do not.
|
||||
in property <bool> aspect-turnable: false;
|
||||
|
||||
callback rotate(int);
|
||||
callback flip-h-toggled();
|
||||
callback flip-v-toggled();
|
||||
callback angle-changed(float);
|
||||
/// The straightening gesture is over and this is the angle to keep.
|
||||
///
|
||||
/// What the auto-crop hangs off — see `DevelopSession::auto_crop_to_angle`
|
||||
/// for why it must be the end of the drag and not every frame of it, and
|
||||
/// `PlainSlider::committed` for why it must not be `drag-changed`.
|
||||
callback angle-committed(float);
|
||||
callback angle-reset();
|
||||
callback aspect-picked(int);
|
||||
callback portrait-toggled();
|
||||
callback reset();
|
||||
|
||||
height: layout.preferred-height;
|
||||
|
||||
layout := VerticalLayout {
|
||||
@@ -685,9 +711,9 @@ export component ComposePanel inherits Rectangle {
|
||||
// reset — without the hiding.
|
||||
GroupHeading {
|
||||
title: "COMPOSE";
|
||||
modified: root.modified;
|
||||
has-reset: root.modified;
|
||||
reset => { root.reset(); }
|
||||
modified: Framing.modified;
|
||||
has-reset: Framing.modified;
|
||||
reset => { Framing.reset(); }
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
@@ -717,14 +743,14 @@ export component ComposePanel inherits Rectangle {
|
||||
IconButton {
|
||||
icon: "rotate-ccw";
|
||||
label: "Rotate left";
|
||||
enabled: root.enabled;
|
||||
clicked => { root.rotate(-1); }
|
||||
enabled: Develop.enabled;
|
||||
clicked => { Framing.rotate(-1); }
|
||||
}
|
||||
IconButton {
|
||||
icon: "rotate-cw";
|
||||
label: "Rotate right";
|
||||
enabled: root.enabled;
|
||||
clicked => { root.rotate(1); }
|
||||
enabled: Develop.enabled;
|
||||
clicked => { Framing.rotate(1); }
|
||||
}
|
||||
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
@@ -732,33 +758,33 @@ export component ComposePanel inherits Rectangle {
|
||||
IconButton {
|
||||
icon: "flip-h";
|
||||
label: "Flip horizontally";
|
||||
active: root.flip-h;
|
||||
active: Framing.flip-h;
|
||||
accessible-checkable: true;
|
||||
accessible-checked: root.flip-h;
|
||||
enabled: root.enabled;
|
||||
clicked => { root.flip-h-toggled(); }
|
||||
accessible-checked: Framing.flip-h;
|
||||
enabled: Develop.enabled;
|
||||
clicked => { Framing.flip-h-toggled(); }
|
||||
}
|
||||
IconButton {
|
||||
icon: "flip-v";
|
||||
label: "Flip vertically";
|
||||
active: root.flip-v;
|
||||
active: Framing.flip-v;
|
||||
accessible-checkable: true;
|
||||
accessible-checked: root.flip-v;
|
||||
enabled: root.enabled;
|
||||
clicked => { root.flip-v-toggled(); }
|
||||
accessible-checked: Framing.flip-v;
|
||||
enabled: Develop.enabled;
|
||||
clicked => { Framing.flip-v-toggled(); }
|
||||
}
|
||||
}
|
||||
|
||||
PlainSlider {
|
||||
label: "Straighten";
|
||||
value: root.angle;
|
||||
value: Framing.angle;
|
||||
default-value: 0.0;
|
||||
minimum: -root.max-straighten;
|
||||
maximum: root.max-straighten;
|
||||
minimum: -Framing.max-straighten;
|
||||
maximum: Framing.max-straighten;
|
||||
unit: "°";
|
||||
changed(v) => { root.angle-changed(v); }
|
||||
committed(v) => { root.angle-committed(v); }
|
||||
reset => { root.angle-reset(); }
|
||||
changed(v) => { Framing.angle-changed(v); }
|
||||
committed(v) => { Framing.angle-committed(v); }
|
||||
reset => { Framing.angle-changed(0); }
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-3
|
||||
@@ -780,16 +806,19 @@ export component ComposePanel inherits Rectangle {
|
||||
// on top of one another — invisible in this file and obvious on
|
||||
// screen. `app.slint` and `masks.slint` both carry the same note
|
||||
// for the same reason.
|
||||
if root.cropping: Label { text: "Ratio"; body: true; }
|
||||
// **Only while the crop overlay is up.** Shown at all times these
|
||||
// would be a control for a gesture that is not on screen; hidden
|
||||
// behind a lid they would be a feature nobody finds.
|
||||
if Develop.cropping: Label { text: "Ratio"; body: true; }
|
||||
|
||||
// The same wrapping grid the generated enum rows use, for the
|
||||
// same reason: six chips in a row would be 414px of chips setting
|
||||
// the width of a column that is meant to be 280.
|
||||
if root.cropping: ChipGrid {
|
||||
options: root.aspects;
|
||||
selected: root.aspect;
|
||||
enabled: root.enabled;
|
||||
picked(i) => { root.aspect-picked(i); }
|
||||
if Develop.cropping: ChipGrid {
|
||||
options: Framing.aspects;
|
||||
selected: Framing.aspect;
|
||||
enabled: Develop.enabled;
|
||||
picked(i) => { Framing.aspect-picked(i); }
|
||||
}
|
||||
|
||||
// One chip, lit when the ratio is standing on its short edge,
|
||||
@@ -804,7 +833,7 @@ export component ComposePanel inherits Rectangle {
|
||||
// under the first column: a `ChoiceChip` placed straight into the
|
||||
// `VerticalLayout` is stretched across the whole panel, which reads
|
||||
// as a button for the section rather than as one more chip.
|
||||
if root.cropping: Rectangle {
|
||||
if Develop.cropping: Rectangle {
|
||||
height: Theme.control-height;
|
||||
|
||||
ChoiceChip {
|
||||
@@ -812,29 +841,51 @@ export component ComposePanel inherits Rectangle {
|
||||
width: 88px;
|
||||
height: 100%;
|
||||
label: "Portrait";
|
||||
selected: root.portrait;
|
||||
enabled: root.enabled && root.aspect-turnable;
|
||||
clicked => { root.portrait-toggled(); }
|
||||
selected: Framing.portrait;
|
||||
enabled: Develop.enabled && Framing.aspect-turnable;
|
||||
clicked => { Framing.portrait-toggled(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The panel: a heading per multi-parameter operation, a control per parameter.
|
||||
//
|
||||
// **Why the loop is shaped the way it is.** `rows` is flat, and Slint can
|
||||
// neither slice a model nor nest a `for` over a run of it. What it *can* do is
|
||||
// repeat over an integer — `for n in row.group-len` — so each group's heading
|
||||
// row renders its whole group by indexing back into `rows` from `group-head`,
|
||||
// and every other row renders nothing.
|
||||
//
|
||||
// **Nothing here collapses.** Every group was a `Section` with a disclosure
|
||||
// triangle until it became clear what that cost on a tablet: five of the
|
||||
// pipeline's operations carry one parameter, so the lid was most of the row,
|
||||
// and a control behind a lid is one the user does not know the pipeline has.
|
||||
// The column is closed as a whole from the status strip instead, which is the
|
||||
// control that was actually wanted.
|
||||
/// TRACES: FR-DEV-6
|
||||
/// The settings clipboard, as the develop column sees it.
|
||||
///
|
||||
/// A global rather than three properties and three callbacks on the panel —
|
||||
/// `session.slint` gives the argument.
|
||||
///
|
||||
/// The clipboard itself is a **window-lifetime** thing and not a view's: a
|
||||
/// copy is taken in develop and may be pasted onto a selection back in the
|
||||
/// library grid, and the grid's own paste stays on the window root where the
|
||||
/// grid can reach it. What is here is the develop half — the two buttons and
|
||||
/// what they would do.
|
||||
export global Transfer {
|
||||
/// Whether anything has been copied yet. Distinct from the clipboard
|
||||
/// being *neutral*: a copy of an unedited frame is a real thing to paste,
|
||||
/// since it clears the target.
|
||||
in property <bool> armed: false;
|
||||
/// What a paste would apply, at the current scope — "3 adjustments", or
|
||||
/// "Neutral".
|
||||
in property <string> summary;
|
||||
/// Whether the clipboard holds framing the current scope is dropping.
|
||||
/// Only then is it worth saying anything about the crop.
|
||||
in property <bool> framing-withheld: false;
|
||||
|
||||
callback copy();
|
||||
callback paste();
|
||||
/// TRACES: FR-DEV-6
|
||||
/// Open the named-preset sheet over the open photograph. Beside copy and
|
||||
/// paste because it is the same thought given a name — this edit, kept.
|
||||
///
|
||||
/// Handled in Rust rather than by setting the sheet's properties here, as
|
||||
/// the panel's instantiation used to: a global cannot bind the window's
|
||||
/// state, and the handler has to say what saving would capture anyway.
|
||||
/// The grid opens the same sheet with its own count, from `app.slint`.
|
||||
callback open-presets();
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-6
|
||||
// Copying this photograph's settings, and pasting settings onto it.
|
||||
//
|
||||
@@ -849,24 +900,6 @@ export component ComposePanel inherits Rectangle {
|
||||
// whether the crop is coming with it — a question the label answers by
|
||||
// naming the count the *current* scope would apply.
|
||||
export component TransferPanel inherits VerticalLayout {
|
||||
in property <bool> enabled: true;
|
||||
/// Whether anything has been copied yet. Distinct from the clipboard
|
||||
/// being *neutral*: a copy of an unedited frame is a real thing to paste,
|
||||
/// since it clears the target.
|
||||
in property <bool> armed: false;
|
||||
/// What a paste would apply — "3 adjustments", or "Neutral".
|
||||
in property <string> summary;
|
||||
/// Whether the clipboard holds framing the current scope is dropping.
|
||||
/// Only then is it worth saying anything about the crop.
|
||||
in property <bool> framing-withheld: false;
|
||||
|
||||
callback copy();
|
||||
callback paste();
|
||||
/// TRACES: FR-DEV-6
|
||||
/// Open the named-preset sheet. Beside copy and paste because it is the
|
||||
/// same thought given a name — this edit, kept.
|
||||
callback open-presets();
|
||||
|
||||
padding: Theme.gap;
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
@@ -880,9 +913,9 @@ export component TransferPanel inherits VerticalLayout {
|
||||
|
||||
Button {
|
||||
text: "Copy";
|
||||
enabled: root.enabled;
|
||||
enabled: Develop.enabled;
|
||||
horizontal-stretch: 1;
|
||||
clicked => { root.copy(); }
|
||||
clicked => { Transfer.copy(); }
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -890,9 +923,9 @@ export component TransferPanel inherits VerticalLayout {
|
||||
// Enabled on `armed` rather than on the summary being non-empty,
|
||||
// so pasting a neutral copy — which clears this image — stays
|
||||
// available. Still needs an image to paste *onto*.
|
||||
enabled: root.enabled && root.armed;
|
||||
enabled: Develop.enabled && Transfer.armed;
|
||||
horizontal-stretch: 1;
|
||||
clicked => { root.paste(); }
|
||||
clicked => { Transfer.paste(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,39 +935,117 @@ export component TransferPanel inherits VerticalLayout {
|
||||
// that does neither sitting between them would read as part of that pair.
|
||||
Button {
|
||||
text: "Presets…";
|
||||
enabled: root.enabled;
|
||||
clicked => { root.open-presets(); }
|
||||
enabled: Develop.enabled;
|
||||
clicked => { Transfer.open-presets(); }
|
||||
}
|
||||
|
||||
if root.armed: Caption {
|
||||
text: root.summary + (root.framing-withheld ? " · crop not included" : "");
|
||||
if Transfer.armed: Caption {
|
||||
text: Transfer.summary + (Transfer.framing-withheld ? " · crop not included" : "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Which mode the develop view is in.
|
||||
/// TRACES: FR-DEV-3
|
||||
/// The generated controls: what they show, what they are pointed at, and what
|
||||
/// pressing one means.
|
||||
///
|
||||
/// `crop` was a bare `bool` on the window and local masking was a panel with
|
||||
/// two toggles, so nothing stopped both being on at once — and nothing on
|
||||
/// screen said which of them the canvas and the column were obeying. One value
|
||||
/// with three states cannot be in two of them, which is the whole reason this
|
||||
/// is an enum rather than a tidier pair of flags.
|
||||
export enum ViewMode {
|
||||
/// The whole photograph. Sliders are global, the canvas pans and zooms.
|
||||
photo,
|
||||
/// The crop overlay is up and the canvas shows the uncropped frame.
|
||||
crop,
|
||||
/// The region map is drawn, a click on the photograph selects, and the
|
||||
/// column is the mask stack and the selected layer's adjustments.
|
||||
local,
|
||||
/// TRACES: FR-DEV-8
|
||||
/// The repairs are drawn on the photograph, a click places one, and the
|
||||
/// column describes whichever is selected.
|
||||
/// A global rather than eleven properties and eight callbacks spread over two
|
||||
/// panels — `session.slint` gives the argument. `GroupStrip` and `AdjustPanel`
|
||||
/// share it because they are one control between them: the strip says which
|
||||
/// slice of `rows` the panel is drawing, and a strip that filtered a different
|
||||
/// panel from the one beside it would be a control for nothing.
|
||||
///
|
||||
/// **This names no operation and no parameter.** Everything here is a model
|
||||
/// the core supplied or an index back into it (ARCH §4.3a, FR-DEV-3a).
|
||||
export global Adjustments {
|
||||
in property <[ParamRow]> rows;
|
||||
/// What these controls are pointed at — the whole photograph, or one mask
|
||||
/// layer by name.
|
||||
///
|
||||
/// A mode rather than a panel button for the reason the enum exists at
|
||||
/// all: arming a click on the canvas is something only one tool may be
|
||||
/// doing at a time, and two flags could both be true.
|
||||
spots,
|
||||
/// Supplied already resolved: whether a layer is selected and what it is
|
||||
/// called are session facts, and deriving them in the panel would need
|
||||
/// that file to reason about the mask stack.
|
||||
in property <string> scope: "ADJUST";
|
||||
|
||||
/// The groups the strip offers, already resolved, and which is chosen.
|
||||
/// -1 is "everything".
|
||||
in property <[string]> tabs;
|
||||
in property <int> active-tab: -1;
|
||||
callback tab-picked(int);
|
||||
/// TRACES: FR-UI-1 | FR-UI-7
|
||||
/// Whether the groups are chosen from the tool rail instead of from the
|
||||
/// strip above the column.
|
||||
///
|
||||
/// Owned by Rust, which composes it from the input modality and the
|
||||
/// photographer's override — see `ToolRail::groups-in-rail` for why the
|
||||
/// axis is input rather than platform or width.
|
||||
///
|
||||
/// The two controls are mutually exclusive by construction: this shows one
|
||||
/// and hides the other, so there is never a moment with two ways to answer
|
||||
/// the same question, and no state to keep in step between them.
|
||||
in property <bool> groups-in-rail: false;
|
||||
|
||||
/// The tone curve's sampled shape, evaluated in Rust by the same spline
|
||||
/// the shader runs so the drawn line cannot disagree with the applied one.
|
||||
in property <[float]> curve-samples;
|
||||
/// The curves the tone curve widget can plot, named by the core. Fewer
|
||||
/// than two of them means there is nothing to choose and no selector.
|
||||
in property <[string]> curve-channels;
|
||||
/// Which of them `curve-samples` and the row's points describe.
|
||||
in property <int> curve-channel;
|
||||
|
||||
callback param-changed(int, int, float);
|
||||
callback param-reset(int, int);
|
||||
/// Every parameter of one operation back to its default. A section's reset
|
||||
/// and a curve's reset are the same action, so they share the one callback
|
||||
/// rather than duplicating a handler that would have to be kept in step
|
||||
/// with it.
|
||||
callback curve-reset(int);
|
||||
/// Plot a different one of the curve's curves.
|
||||
callback curve-channel-picked(int);
|
||||
callback reset-all();
|
||||
|
||||
// TRACES: FR-DEV-3
|
||||
// The on-canvas sampler: which group has armed it.
|
||||
//
|
||||
// The op index rather than a bool, because arming is a question about a
|
||||
// *group* — two operations could ask for a sampler and only one of them
|
||||
// can be waiting for the click. -1 is none armed, the sentinel this file
|
||||
// already uses for "no swatch" and "everything" and for the same reason:
|
||||
// a Slint struct or property cannot carry an optional.
|
||||
in-out property <int> sampling-op: -1;
|
||||
/// The heading's picker was pressed for the operation at this index.
|
||||
///
|
||||
/// A function rather than a callback into Rust, and that is deliberate:
|
||||
/// arming changes what a click on the photograph does and nothing about
|
||||
/// the edit, so there is nothing for the session to hear about until a
|
||||
/// point is actually picked. It lived in `app.slint`'s instantiation of
|
||||
/// the panel before, which is exactly the sort of behaviour a second
|
||||
/// instantiation would have had to copy.
|
||||
public function arm-sampler(op: int) {
|
||||
self.sampling-op = self.sampling-op == op ? -1 : op;
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-3f
|
||||
// The film stock, which is not a parameter and so is not a `ParamRow`.
|
||||
//
|
||||
// A bespoke control for the same reason the mask panel is one: a stock is
|
||||
// a choice of *material*, named by an id, not a number on a slider. The
|
||||
// generic rows keep their rule — they still name no operation — and the
|
||||
// film's own exposure sliders arrive through them like any other.
|
||||
//
|
||||
// Names are supplied already resolved, and the list is whatever profiles
|
||||
// are installed: this file does not know what a Kodachrome is.
|
||||
in property <[string]> film-stocks;
|
||||
/// Index into `film-stocks`. Zero is the first entry, which Rust makes
|
||||
/// "no film" — so a fresh photograph selects it without a sentinel.
|
||||
in property <int> film-selected: 0;
|
||||
/// Whether the chosen stock is a negative with a paper to print on. False
|
||||
/// for a reversal stock, which is the picture already, and for no film.
|
||||
in property <bool> film-can-print: false;
|
||||
/// Print it, rather than view the negative as scanned.
|
||||
in property <bool> film-print: false;
|
||||
callback film-picked(int);
|
||||
callback film-print-toggled(bool);
|
||||
}
|
||||
|
||||
/// The group strip: which kind of adjustment the column is showing.
|
||||
@@ -965,16 +1076,16 @@ export enum ViewMode {
|
||||
/// joins a group without an edit here (FR-DEV-3a). With the tools gone there is
|
||||
/// no longer any hand-written word in this row at all, save "All".
|
||||
export component GroupStrip inherits Rectangle {
|
||||
in property <[string]> tabs;
|
||||
/// Index into `tabs`, or -1 for "everything".
|
||||
in property <int> active-tab: -1;
|
||||
in property <bool> enabled: true;
|
||||
|
||||
callback picked(int);
|
||||
/// Whether the strip has anything to offer and is the control that offers
|
||||
/// it: an open photograph, with the groups not already in the rail.
|
||||
///
|
||||
/// Derived here rather than bound at the instantiation, so a second
|
||||
/// composition of this column cannot get the rule slightly different.
|
||||
property <bool> live: Develop.enabled && !Adjustments.groups-in-rail;
|
||||
|
||||
background: Theme.surface;
|
||||
height: root.enabled ? layout.preferred-height : 0px;
|
||||
visible: root.enabled;
|
||||
height: root.live ? layout.preferred-height : 0px;
|
||||
visible: root.live;
|
||||
|
||||
// Scrolls rather than overflowing, exactly as the develop status strip
|
||||
// does and for the same reason: a `HorizontalLayout` given less width than
|
||||
@@ -1015,11 +1126,11 @@ export component GroupStrip inherits Rectangle {
|
||||
width: 34px;
|
||||
height: Theme.touch-target;
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.picked(-1); }
|
||||
clicked => { Adjustments.tab-picked(-1); }
|
||||
|
||||
Label {
|
||||
text: "All";
|
||||
emphasised: root.active-tab == -1 || all.has-hover;
|
||||
emphasised: Adjustments.active-tab == -1 || all.has-hover;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
@@ -1027,19 +1138,19 @@ export component GroupStrip inherits Rectangle {
|
||||
y: parent.height - 2px;
|
||||
height: 2px;
|
||||
width: parent.width;
|
||||
background: root.active-tab == -1 ? Theme.ink : transparent;
|
||||
background: Adjustments.active-tab == -1 ? Theme.ink : transparent;
|
||||
}
|
||||
}
|
||||
|
||||
for tab[i] in root.tabs: tab-area := TouchArea {
|
||||
for tab[i] in Adjustments.tabs: tab-area := TouchArea {
|
||||
width: name.preferred-width + 10px;
|
||||
height: Theme.touch-target;
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.picked(i); }
|
||||
clicked => { Adjustments.tab-picked(i); }
|
||||
|
||||
name := Label {
|
||||
text: tab;
|
||||
emphasised: root.active-tab == i || tab-area.has-hover;
|
||||
emphasised: Adjustments.active-tab == i || tab-area.has-hover;
|
||||
vertical-alignment: center;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
@@ -1050,83 +1161,34 @@ export component GroupStrip inherits Rectangle {
|
||||
y: parent.height - 2px;
|
||||
height: 2px;
|
||||
width: parent.width;
|
||||
background: root.active-tab == i ? Theme.ink : transparent;
|
||||
background: Adjustments.active-tab == i ? Theme.ink : transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The panel: a heading per multi-parameter operation, a control per parameter.
|
||||
//
|
||||
// **Why the loop is shaped the way it is.** `rows` is flat, and Slint can
|
||||
// neither slice a model nor nest a `for` over a run of it. What it *can* do is
|
||||
// repeat over an integer — `for n in row.group-len` — so each group's heading
|
||||
// row renders its whole group by indexing back into `rows` from `group-head`,
|
||||
// and every other row renders nothing.
|
||||
//
|
||||
// **Nothing here collapses.** Every group was a `Section` with a disclosure
|
||||
// triangle until it became clear what that cost on a tablet: five of the
|
||||
// pipeline's operations carry one parameter, so the lid was most of the row,
|
||||
// and a control behind a lid is one the user does not know the pipeline has.
|
||||
// The column is closed as a whole from the status strip instead, which is the
|
||||
// control that was actually wanted.
|
||||
export component AdjustPanel inherits Rectangle {
|
||||
in property <[ParamRow]> rows;
|
||||
in property <bool> enabled: true;
|
||||
/// What these controls are pointed at — the whole photograph, or one mask
|
||||
/// layer by name.
|
||||
///
|
||||
/// **The heading, not a caption beside it.** Selecting a layer re-points
|
||||
/// every one of these controls at that layer's chain, and until now the
|
||||
/// only sign of it was a sentence in the panel above. Putting the answer
|
||||
/// in the heading of the thing that changed means the scope cannot be read
|
||||
/// without also reading what it applies to.
|
||||
///
|
||||
/// Supplied already resolved: whether a layer is selected and what it is
|
||||
/// called are session facts, and deriving them here would need this file
|
||||
/// to reason about the mask stack.
|
||||
in property <string> scope: "ADJUST";
|
||||
/// The tone curve's sampled shape, evaluated in Rust by the same spline
|
||||
/// the shader runs so the drawn line cannot disagree with the applied one.
|
||||
in property <[float]> curve-samples;
|
||||
/// The curves the tone curve widget can plot, named by the core. Fewer
|
||||
/// than two of them means there is nothing to choose and no selector.
|
||||
in property <[string]> curve-channels;
|
||||
/// Which of them `curve-samples` and the row's points describe.
|
||||
in property <int> curve-channel;
|
||||
callback param-changed(int, int, float);
|
||||
callback param-reset(int, int);
|
||||
callback curve-reset(int);
|
||||
/// Plot a different one of the curve's curves.
|
||||
callback curve-channel-picked(int);
|
||||
/// Return every parameter of one operation to its default — the reset on
|
||||
/// a section's own header, beside the panel-wide one.
|
||||
callback op-reset(int);
|
||||
callback reset-all();
|
||||
|
||||
// TRACES: FR-DEV-3
|
||||
// The on-canvas sampler: which group has armed it, and the request to arm
|
||||
// one.
|
||||
//
|
||||
// The op index rather than a bool, because arming is a question about a
|
||||
// *group* — two operations could ask for a sampler and only one of them
|
||||
// can be waiting for the click. -1 is none armed, the sentinel this file
|
||||
// already uses for "no swatch" and "everything" and for the same reason:
|
||||
// a Slint struct or property cannot carry an optional.
|
||||
in property <int> sampling-op: -1;
|
||||
/// The heading's picker was pressed, for the operation at this index.
|
||||
callback sampler-armed(int);
|
||||
|
||||
// TRACES: FR-DEV-3f
|
||||
// The film stock, which is not a parameter and so is not a `ParamRow`.
|
||||
//
|
||||
// A bespoke control for the same reason the mask panel is one: a stock is
|
||||
// a choice of *material*, named by an id, not a number on a slider. The
|
||||
// generic rows above keep their rule — they still name no operation — and
|
||||
// the film's own exposure sliders arrive through them like any other.
|
||||
//
|
||||
// Names are supplied already resolved, and the list is whatever profiles
|
||||
// are installed: this file does not know what a Kodachrome is.
|
||||
in property <[string]> film-stocks;
|
||||
/// Index into `film-stocks`. Zero is the first entry, which Rust makes
|
||||
/// "no film" — so a fresh photograph selects it without a sentinel.
|
||||
in property <int> film-selected: 0;
|
||||
/// Whether the chosen stock is a negative with a paper to print on. False
|
||||
/// for a reversal stock, which is the picture already, and for no film.
|
||||
in property <bool> film-can-print: false;
|
||||
/// Print it, rather than view the negative as scanned.
|
||||
in property <bool> film-print: false;
|
||||
callback film-picked(int);
|
||||
callback film-print-toggled(bool);
|
||||
/// Whether the stock list is open. Pure interface state: it changes no
|
||||
/// parameter and the core never hears about it.
|
||||
///
|
||||
/// Private to the panel rather than in `Adjustments`, and deliberately: it
|
||||
/// is a lid, and two drawings of this panel may honestly have their lids
|
||||
/// in different positions.
|
||||
private property <bool> film-expanded: false;
|
||||
|
||||
background: Theme.surface;
|
||||
@@ -1155,7 +1217,7 @@ export component AdjustPanel inherits Rectangle {
|
||||
|
||||
HorizontalLayout {
|
||||
PanelHeading {
|
||||
text: root.scope;
|
||||
text: Adjustments.scope;
|
||||
// Elided rather than wrapped. A layer's name is the user's and
|
||||
// can be any length; a heading that wrapped would change the
|
||||
// panel's height as the selection moved, and an unwrapped one
|
||||
@@ -1169,7 +1231,7 @@ export component AdjustPanel inherits Rectangle {
|
||||
reset := TouchArea {
|
||||
width: 44px;
|
||||
height: 20px;
|
||||
clicked => { root.reset-all(); }
|
||||
clicked => { Adjustments.reset-all(); }
|
||||
Label {
|
||||
text: "reset";
|
||||
emphasised: reset.has-hover;
|
||||
@@ -1178,13 +1240,13 @@ export component AdjustPanel inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
if !root.enabled: Caption { text: "No image"; }
|
||||
if !Develop.enabled: Caption { text: "No image"; }
|
||||
|
||||
// TRACES: FR-DEV-3f
|
||||
// Above the rows, because it decides what they mean: the exposure
|
||||
// slider below is a slider on *this stock's* characteristic curve, and
|
||||
// the same number is a different picture on a different film.
|
||||
if root.enabled && root.film-stocks.length > 1: VerticalLayout {
|
||||
if Develop.enabled && Adjustments.film-stocks.length > 1: VerticalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
// **Collapsed to one row, and opened only to change it.**
|
||||
@@ -1224,7 +1286,7 @@ export component AdjustPanel inherits Rectangle {
|
||||
// The chosen stock's own name, elided rather than
|
||||
// wrapped: it is a product name of any length and
|
||||
// this column is 280px.
|
||||
text: root.film-stocks[root.film-selected];
|
||||
text: Adjustments.film-stocks[Adjustments.film-selected];
|
||||
overflow: elide;
|
||||
horizontal-alignment: right;
|
||||
horizontal-stretch: 1;
|
||||
@@ -1241,7 +1303,7 @@ export component AdjustPanel inherits Rectangle {
|
||||
if root.film-expanded: VerticalLayout {
|
||||
spacing: 0px;
|
||||
|
||||
for stock[i] in root.film-stocks: film-row := TouchArea {
|
||||
for stock[i] in Adjustments.film-stocks: film-row := TouchArea {
|
||||
// Shorter than a touch target, which is a deliberate
|
||||
// exception and the only one here: a list row's neighbours
|
||||
// are other rows, so growing the hit area the way a chip
|
||||
@@ -1250,7 +1312,7 @@ export component AdjustPanel inherits Rectangle {
|
||||
height: 32px;
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.film-picked(i);
|
||||
Adjustments.film-picked(i);
|
||||
// Closed on choosing. The answer is now in the summary
|
||||
// row, and leaving two dozen entries open after the
|
||||
// question has been answered is the behaviour this
|
||||
@@ -1260,7 +1322,7 @@ export component AdjustPanel inherits Rectangle {
|
||||
|
||||
Rectangle {
|
||||
border-radius: Theme.radius;
|
||||
background: i == root.film-selected ? Theme.active
|
||||
background: i == Adjustments.film-selected ? Theme.active
|
||||
: (film-row.pressed ? Theme.pressed
|
||||
: (film-row.has-hover ? Theme.hover : transparent));
|
||||
|
||||
@@ -1281,15 +1343,15 @@ export component AdjustPanel inherits Rectangle {
|
||||
// Only for a negative. A reversal stock has no paper — it is the
|
||||
// photograph as it comes — so offering the choice would be
|
||||
// offering something that cannot happen.
|
||||
if root.film-can-print: HorizontalLayout {
|
||||
if Adjustments.film-can-print: HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
alignment: start;
|
||||
print-toggle := TouchArea {
|
||||
width: 120px;
|
||||
height: Theme.touch-target;
|
||||
clicked => { root.film-print-toggled(!root.film-print); }
|
||||
clicked => { Adjustments.film-print-toggled(!Adjustments.film-print); }
|
||||
Label {
|
||||
text: root.film-print ? "Printed" : "Scanned negative";
|
||||
text: Adjustments.film-print ? "Printed" : "Scanned negative";
|
||||
emphasised: print-toggle.has-hover;
|
||||
}
|
||||
}
|
||||
@@ -1302,7 +1364,7 @@ export component AdjustPanel inherits Rectangle {
|
||||
// sliders needed and the instrument the sliders are judged against was
|
||||
// unreachable. The column scrolls as one now, and a scroller inside a
|
||||
// scroller would give every drag a third thing to be lost to.
|
||||
if root.enabled: VerticalLayout {
|
||||
if Develop.enabled: VerticalLayout {
|
||||
content := VerticalLayout {
|
||||
spacing: 0px;
|
||||
alignment: start;
|
||||
@@ -1336,7 +1398,7 @@ export component AdjustPanel inherits Rectangle {
|
||||
//
|
||||
// This used to nest: a group's head row drew the *whole* group
|
||||
// by repeating over `row.group-len` and indexing back into
|
||||
// `root.rows` for each member, and every other row drew
|
||||
// `Adjustments.rows` for each member, and every other row drew
|
||||
// nothing. It produced the right picture and could not be
|
||||
// dragged.
|
||||
//
|
||||
@@ -1361,7 +1423,7 @@ export component AdjustPanel inherits Rectangle {
|
||||
// It also drops the old hazard of the head row building every
|
||||
// control in its group — thirty-six live TouchAreas behind the
|
||||
// mixer's twelve visible ones.
|
||||
for row[i] in root.rows: VerticalLayout {
|
||||
for row[i] in Adjustments.rows: VerticalLayout {
|
||||
spacing: 0px;
|
||||
|
||||
// The group's name, drawn by the row that heads it, above
|
||||
@@ -1394,11 +1456,11 @@ export component AdjustPanel inherits Rectangle {
|
||||
// waiting is the panel's business, because only
|
||||
// the panel can see the others.
|
||||
has-sampler: row.group-samples;
|
||||
sampling: root.sampling-op == row.op-index;
|
||||
sample => { root.sampler-armed(row.op-index); }
|
||||
sampling: Adjustments.sampling-op == row.op-index;
|
||||
sample => { Adjustments.arm-sampler(row.op-index); }
|
||||
// Resetting is what *this* panel's groups do; the
|
||||
// heading itself has no opinion about it.
|
||||
reset => { root.op-reset(row.op-index); }
|
||||
reset => { Adjustments.curve-reset(row.op-index); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1412,16 +1474,16 @@ export component AdjustPanel inherits Rectangle {
|
||||
|
||||
ParamControl {
|
||||
data: row;
|
||||
curve-samples: root.curve-samples;
|
||||
curve-channels: root.curve-channels;
|
||||
curve-channel: root.curve-channel;
|
||||
curve-samples: Adjustments.curve-samples;
|
||||
curve-channels: Adjustments.curve-channels;
|
||||
curve-channel: Adjustments.curve-channel;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
param-changed(op, param, v) => {
|
||||
root.param-changed(op, param, v);
|
||||
Adjustments.param-changed(op, param, v);
|
||||
}
|
||||
param-reset(op, param) => { root.param-reset(op, param); }
|
||||
curve-reset(op) => { root.curve-reset(op); }
|
||||
curve-channel-picked(i) => { root.curve-channel-picked(i); }
|
||||
param-reset(op, param) => { Adjustments.param-reset(op, param); }
|
||||
curve-reset(op) => { Adjustments.curve-reset(op); }
|
||||
curve-channel-picked(i) => { Adjustments.curve-channel-picked(i); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user