Merge branch 'histogram'
# Conflicts: # ui/dr-ui/src/lib.rs
This commit is contained in:
+22
-1
@@ -4,9 +4,10 @@ import { LaunchScreen } from "launch.slint";
|
||||
import { LibraryGrid, LibraryCell, TimelineBar } from "library.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint";
|
||||
import { CollectionsPanel, CollectionRow } from "collections.slint";
|
||||
import { HistogramPanel, HistogramView } from "histogram.slint";
|
||||
import { SettingsPage } from "settings.slint";
|
||||
|
||||
export { LibraryCell, TimelineBar, CollectionRow, ActivityRow }
|
||||
export { LibraryCell, TimelineBar, CollectionRow, ActivityRow, HistogramView }
|
||||
|
||||
// Status strip — surfaces the GPU backend and adapter, which matters during
|
||||
// v0.1 because assumption A1 is exactly "does this compositing path work on
|
||||
@@ -223,6 +224,11 @@ export component AppWindow inherits Window {
|
||||
in property <int> total: 0;
|
||||
in property <string> load-error: "";
|
||||
|
||||
/// TRACES: FR-DSP-7
|
||||
/// The counted frame. Set only when the render has settled — a histogram
|
||||
/// of a draft frame is a histogram of an image nobody is reading.
|
||||
in property <HistogramView> histogram;
|
||||
|
||||
// --- zoom, pan and crop (FR-DEV-4) ---
|
||||
//
|
||||
// Zoom is a *viewing* state, not an edit: it changes the resolution the
|
||||
@@ -1500,6 +1506,21 @@ in property <bool> panel-visible: true;
|
||||
background: Theme.rule;
|
||||
}
|
||||
|
||||
// Directly under the capture metadata and above every
|
||||
// control, because it is the thing the controls are
|
||||
// judged against: exposure, blacks and whites are all
|
||||
// set by watching this move (FR-DSP-7). An instrument
|
||||
// below the sliders it reports on would have the
|
||||
// photographer looking away from it to use it.
|
||||
HistogramPanel {
|
||||
data: root.histogram;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
}
|
||||
|
||||
// Framing above the colour work, matching how the edit is
|
||||
// made rather than how it is applied: the frame is decided
|
||||
// by eye first and the pipeline runs it last (see
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
// TRACES: FR-DSP-7
|
||||
// The live histogram, and what it says about clipping.
|
||||
//
|
||||
// **Why this is hand-built when the rest of the column is generated.** The
|
||||
// develop panel is built from operation capabilities, and a histogram is not an
|
||||
// operation: it has no parameters, changes nothing about the photograph, and
|
||||
// answers a question rather than asking one. It is an *instrument* — the same
|
||||
// kind of thing as the zoom readout — so it is written, and ARCH §4.3a is
|
||||
// untroubled by it: nothing here reads a parameter out of a descriptor.
|
||||
//
|
||||
// **Everything numeric was decided in Rust.** The heights arriving here are
|
||||
// already 0..1 against a chosen scale, and the clipping figures are already
|
||||
// strings. That is not tidiness — it is the only way the arithmetic is
|
||||
// testable. A peak chosen in Slint could only be checked by looking at it, and
|
||||
// a histogram that is the wrong shape looks exactly as plausible as one that is
|
||||
// right (see `src/histogram.rs`).
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { PanelHeading, Caption, Panel } from "widgets.slint";
|
||||
|
||||
// One counted frame, ready to draw.
|
||||
//
|
||||
// Heights are 0..1 with y up, darkest level first — the same convention the
|
||||
// tone curve's samples use, so the two plots in this column cannot end up
|
||||
// disagreeing about which way is up.
|
||||
export struct HistogramView {
|
||||
// Whether a frame has been counted at all. Distinct from an all-zero
|
||||
// histogram, which would be a claim about an image rather than the absence
|
||||
// of one.
|
||||
available: bool,
|
||||
|
||||
luma: [float],
|
||||
red: [float],
|
||||
green: [float],
|
||||
blue: [float],
|
||||
|
||||
// Past the threshold worth reporting — see `CLIP_VISIBLE` in Rust.
|
||||
highlights-clipped: bool,
|
||||
shadows-clipped: bool,
|
||||
|
||||
// How much, as text. NFR-A11Y-3: the marker beside these says *whether*
|
||||
// by hue and position, and these say *how much* in a form that survives
|
||||
// being unable to tell the two apart.
|
||||
highlights-label: string,
|
||||
shadows-label: string,
|
||||
}
|
||||
|
||||
// One series, as a run of columns.
|
||||
//
|
||||
// Two shapes from one component because the two differ only in where the column
|
||||
// starts: the luminance trace is filled from the baseline, the three channels
|
||||
// are a line. Splitting them into two components would duplicate the column
|
||||
// arithmetic, which is the part that has to stay identical or the four series
|
||||
// would no longer be plotted against the same axis.
|
||||
//
|
||||
// One thin Rectangle per column: Slint has no polyline primitive, and this is
|
||||
// the same construction `CurveEditor` uses a few files away.
|
||||
component Trace inherits Rectangle {
|
||||
/// 0..1, y up.
|
||||
in property <[float]> heights;
|
||||
in property <color> ink;
|
||||
/// Filled from the baseline rather than drawn as a line.
|
||||
in property <bool> filled: false;
|
||||
in property <float> shade: 1.0;
|
||||
|
||||
background: transparent;
|
||||
|
||||
for h[i] in root.heights: Rectangle {
|
||||
// The next column's height, so a line segment can span the gap between
|
||||
// them. Without this a steep edge draws as a dotted stair rather than a
|
||||
// rising line — the same fix `CurveEditor` makes for the same reason.
|
||||
property <float> next: i + 1 < root.heights.length
|
||||
? root.heights[i + 1] : h;
|
||||
|
||||
x: parent.width * i / max(root.heights.length, 1);
|
||||
width: parent.width / max(root.heights.length, 1) + 1px;
|
||||
|
||||
y: root.filled
|
||||
? parent.height * (1.0 - h)
|
||||
: parent.height * (1.0 - max(h, self.next));
|
||||
height: root.filled
|
||||
? parent.height * h
|
||||
// A floor, so a flat stretch of the trace is still a line and not a
|
||||
// gap in one.
|
||||
: max(parent.height * abs(self.next - h), 1.5px);
|
||||
|
||||
background: root.ink;
|
||||
opacity: root.shade;
|
||||
}
|
||||
}
|
||||
|
||||
// A clipping readout: a lit marker and a figure.
|
||||
//
|
||||
// **Two affordances for one fact, and NFR-A11Y-3 is why.** No status in this
|
||||
// application is carried by hue alone, and clipping is the status most worth
|
||||
// getting right — it is the one the histogram exists to warn about. So the
|
||||
// marker appears and disappears (a shape, not a tint) and the figure states the
|
||||
// magnitude in words. Either alone is enough to read it.
|
||||
component ClipReadout inherits HorizontalLayout {
|
||||
in property <string> title;
|
||||
in property <string> figure;
|
||||
in property <bool> lit;
|
||||
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
Rectangle {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
border-radius: 1px;
|
||||
// `warn-ink`, the palette's one sanctioned hue: a blown highlight is a
|
||||
// caution about the photograph, which is exactly the kind of thing that
|
||||
// token exists to say instantly.
|
||||
background: Theme.warn-ink;
|
||||
visible: root.lit;
|
||||
}
|
||||
|
||||
Caption { text: root.title; }
|
||||
Caption { text: root.figure; warn: root.lit; }
|
||||
}
|
||||
|
||||
// TRACES: FR-DSP-7
|
||||
export component HistogramPanel inherits Rectangle {
|
||||
in property <HistogramView> data;
|
||||
|
||||
background: transparent;
|
||||
height: panel.preferred-height;
|
||||
|
||||
panel := Panel {
|
||||
// Flat, like every panel in this column: the rule between it and its
|
||||
// neighbour belongs to the column that stacks them.
|
||||
flat: true;
|
||||
width: 100%;
|
||||
|
||||
PanelHeading { text: "HISTOGRAM"; }
|
||||
|
||||
Rectangle {
|
||||
// Tall enough to read a shape off and no taller. The column is the
|
||||
// photographer's instrument panel, and every pixel this takes is a
|
||||
// slider pushed below the fold.
|
||||
height: 84px;
|
||||
background: Theme.ground;
|
||||
border-width: 1px;
|
||||
border-color: Theme.rule;
|
||||
// The traces are positioned by fraction of the plot and a value at
|
||||
// the ceiling lands exactly on the border; clipping keeps the top
|
||||
// row of pixels inside the box rather than over its edge.
|
||||
clip: true;
|
||||
|
||||
// Quarter gridlines, so a tone can be placed on the axis without
|
||||
// counting. Same construction and same weight as the tone curve's,
|
||||
// because the two plots sit in one column and any difference
|
||||
// between them would read as meaning something.
|
||||
for i in [1, 2, 3]: Rectangle {
|
||||
x: parent.width * i / 4;
|
||||
width: 1px;
|
||||
background: Theme.rule;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
// Luminance first, so it sits behind: it is the envelope the three
|
||||
// channels decompose, and a filled area drawn over a line hides it.
|
||||
Trace {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
heights: root.data.luma;
|
||||
ink: Theme.plot-luma;
|
||||
filled: true;
|
||||
}
|
||||
|
||||
// The channels over it, held back so three overlapping traces stay
|
||||
// legible where they cross — which on a neutral subject is
|
||||
// everywhere.
|
||||
Trace {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
heights: root.data.red;
|
||||
ink: Theme.plot-red;
|
||||
shade: 0.85;
|
||||
}
|
||||
Trace {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
heights: root.data.green;
|
||||
ink: Theme.plot-green;
|
||||
shade: 0.85;
|
||||
}
|
||||
Trace {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
heights: root.data.blue;
|
||||
ink: Theme.plot-blue;
|
||||
shade: 0.85;
|
||||
}
|
||||
|
||||
// The clipping markers, drawn on the plot's own ends.
|
||||
//
|
||||
// Here as well as in the figures below because this is where the
|
||||
// eye already is: a bar standing at the edge the tones are piling
|
||||
// against says which end has gone in the same glance that reads the
|
||||
// shape, and the figures underneath say how much.
|
||||
Rectangle {
|
||||
x: 0;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: Theme.warn-ink;
|
||||
visible: root.data.shadows-clipped;
|
||||
}
|
||||
Rectangle {
|
||||
x: parent.width - self.width;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: Theme.warn-ink;
|
||||
visible: root.data.highlights-clipped;
|
||||
}
|
||||
|
||||
// Nothing rendered yet. Said rather than shown as an empty plot,
|
||||
// which would be a claim that the photograph has no tones in it.
|
||||
Caption {
|
||||
text: "no frame yet";
|
||||
width: 100%;
|
||||
horizontal-alignment: center;
|
||||
y: (parent.height - self.height) / 2;
|
||||
visible: !root.data.available;
|
||||
}
|
||||
}
|
||||
|
||||
// Always drawn, never hidden. With no frame counted the figures read
|
||||
// "—", which is a different statement from "0%" and the true one: the
|
||||
// panel does not yet know. Hiding the row instead would move the
|
||||
// controls below it up and back down as each image opens.
|
||||
HorizontalLayout {
|
||||
ClipReadout {
|
||||
title: "Shadows";
|
||||
figure: root.data.shadows-label;
|
||||
lit: root.data.shadows-clipped;
|
||||
}
|
||||
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
|
||||
ClipReadout {
|
||||
title: "Highlights";
|
||||
figure: root.data.highlights-label;
|
||||
lit: root.data.highlights-clipped;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user