Make "Choose folder" an actual folder picker
It previously fetched the folder list and threw it away into a status line — a button that looked like it worked and did not. Now it opens a browsable picker: click a folder to descend, ".." to go back, "Use this folder" to select, "Cancel" to leave the root unchanged. Descends one level per click because that is what the backend supports: Depth: infinity is frequently disabled server-side and prohibitively expensive where it is not (ARCH §8.4). The chosen root persists immediately on confirm, so it survives a crash before the library is opened. Confirming at the account root is allowed — a user may legitimately keep everything at the top level — and cancelling leaves any previous selection untouched, which a test asserts. Verified against nextcloud.tourolle.paris at both depths: 30 folders at the root, 21 year-folders inside PhotosRaw. 19 launch tests, 38 in dr-ui.
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
//! it. This is a diagnostic, not the export path (FR-EXP-*).
|
||||
|
||||
use dr_gpu::{AdjustPass, Demosaicer, GpuContext};
|
||||
use dr_pipeline::ops::{colour, colour_mixer, contrast, exposure, tone, white_balance};
|
||||
use dr_pipeline::ops::{colour, colour_mixer, contrast, curve, exposure, tone, white_balance};
|
||||
use dr_pipeline::{EditGraph, ParamId};
|
||||
|
||||
fn main() {
|
||||
@@ -86,6 +86,16 @@ fn main() {
|
||||
"mixer_one" => {
|
||||
graph.set_param(colour_mixer::ID, ParamId("green_sat"), 100.0);
|
||||
}
|
||||
// A classic S-curve: shadows down, highlights up, mid held.
|
||||
"curve_s" => {
|
||||
graph.set_param(curve::ID, curve::P1_Y, 0.15);
|
||||
graph.set_param(curve::ID, curve::P3_Y, 0.85);
|
||||
}
|
||||
// The inverse, a film-like lifted-shadow look.
|
||||
"curve_lift" => {
|
||||
graph.set_param(curve::ID, curve::P0_Y, 0.12);
|
||||
graph.set_param(curve::ID, curve::P1_Y, 0.32);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -736,9 +736,20 @@ mod tests {
|
||||
|
||||
let mut g = EditGraph::default_chain();
|
||||
for cap in EditGraph::default_chain().capabilities() {
|
||||
for p in &cap.params {
|
||||
if let dr_pipeline::ParamKind::Scalar { max, .. } = p.kind {
|
||||
g.set_param(cap.id, p.id, max * 0.6);
|
||||
for (i, p) in cap.params.iter().enumerate() {
|
||||
if let dr_pipeline::ParamKind::Scalar { min, max, .. } = p.kind {
|
||||
// Stepped away from each parameter's own default by a
|
||||
// varying fraction. A single shared value would leave the
|
||||
// tone curve inactive: its neutral is the *relationship*
|
||||
// between its points, so setting them all alike keeps it
|
||||
// on the identity diagonal.
|
||||
let step = (max - min) * (0.15 + 0.05 * (i % 4) as f32);
|
||||
let v = if p.default + step <= max {
|
||||
p.default + step
|
||||
} else {
|
||||
p.default - step
|
||||
};
|
||||
g.set_param(cap.id, p.id, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,62 +102,39 @@ static CURVE_HELPERS: &[Helper] = &[
|
||||
helpers::LUMINANCE,
|
||||
helpers::APPLY_TONE_GAIN,
|
||||
Helper {
|
||||
name: "curve_eval",
|
||||
name: "curve_span",
|
||||
source: "\
|
||||
// Evaluate a monotone cubic Hermite spline through five points.
|
||||
// One span of a monotone cubic Hermite spline.
|
||||
//
|
||||
// Fritsch-Carlson (1980): compute secant slopes, take a smooth average for
|
||||
// the interior tangents, then *limit* each tangent to three times the
|
||||
// adjoining secant. That limiter is what prevents overshoot — an
|
||||
// unconstrained spline can dip below a point's neighbour, inverting tones
|
||||
// and putting a dark halo through a smooth gradient.
|
||||
//
|
||||
// Points arrive pre-sorted by x with a minimum separation enforced on the
|
||||
// CPU, so no division here can be by zero.
|
||||
fn curve_eval(xs: array<f32, 5>, ys: array<f32, 5>, x: f32) -> f32 {
|
||||
// Outside the point range the curve is flat, matching how the endpoints
|
||||
// read in the widget: nothing exists beyond them to interpolate toward.
|
||||
if (x <= xs[0]) { return ys[0]; }
|
||||
if (x >= xs[4]) { return ys[4]; }
|
||||
|
||||
// Locate the span. Five points is few enough that a chain of comparisons
|
||||
// beats any cleverer search.
|
||||
var i = 0;
|
||||
if (x >= xs[3]) { i = 3; }
|
||||
else if (x >= xs[2]) { i = 2; }
|
||||
else if (x >= xs[1]) { i = 1; }
|
||||
|
||||
let x0 = xs[i];
|
||||
let x1 = xs[i + 1];
|
||||
let y0 = ys[i];
|
||||
let y1 = ys[i + 1];
|
||||
// Takes the span's endpoints and the secants either side of it, rather than
|
||||
// an array and an index. **No dynamic indexing anywhere in this file**:
|
||||
// indexing a `array<f32, 5>` by a runtime value made RADV (Mesa 26.1) crash
|
||||
// the process with SIGSEGV during pipeline creation, not merely fail to
|
||||
// compile. Five points means four spans, so unrolling costs a short branch
|
||||
// chain and removes the hazard entirely.
|
||||
fn curve_span(
|
||||
x0: f32, y0: f32, x1: f32, y1: f32,
|
||||
s_prev: f32, s_next: f32, x: f32,
|
||||
) -> f32 {
|
||||
let h = x1 - x0;
|
||||
let secant = (y1 - y0) / h;
|
||||
|
||||
// Secants either side of each knot, duplicated at the ends so the
|
||||
// boundary tangents match the adjoining secant.
|
||||
var s_prev = secant;
|
||||
if (i > 0) {
|
||||
s_prev = (ys[i] - ys[i - 1]) / (xs[i] - xs[i - 1]);
|
||||
}
|
||||
var s_next = secant;
|
||||
if (i + 2 <= 4) {
|
||||
s_next = (ys[i + 2] - ys[i + 1]) / (xs[i + 2] - xs[i + 1]);
|
||||
}
|
||||
|
||||
// Tangents: the average of adjoining secants, but zero wherever the data
|
||||
// turns, which is what pins a local extremum in place.
|
||||
// Tangents: the average of the adjoining secants, but zero wherever the
|
||||
// data turns, which is what pins a local extremum in place.
|
||||
var m0 = 0.5 * (s_prev + secant);
|
||||
var m1 = 0.5 * (secant + s_next);
|
||||
if (s_prev * secant <= 0.0) { m0 = 0.0; }
|
||||
if (secant * s_next <= 0.0) { m1 = 0.0; }
|
||||
|
||||
// A flat span must stay flat.
|
||||
if (abs(secant) < 0.000001) {
|
||||
// A flat span must stay flat.
|
||||
m0 = 0.0;
|
||||
m1 = 0.0;
|
||||
} else {
|
||||
// The Fritsch-Carlson limiter.
|
||||
// The Fritsch-Carlson (1980) limiter: cap each tangent at three
|
||||
// times the secant. This is what prevents overshoot — an
|
||||
// unconstrained spline can dip below a point's neighbour, inverting
|
||||
// tones and putting a dark halo through a smooth gradient.
|
||||
let a = m0 / secant;
|
||||
let b = m1 / secant;
|
||||
let magnitude = a * a + b * b;
|
||||
@@ -178,6 +155,36 @@ fn curve_eval(xs: array<f32, 5>, ys: array<f32, 5>, x: f32) -> f32 {
|
||||
let h11 = t3 - t2;
|
||||
|
||||
return h00 * y0 + h10 * h * m0 + h01 * y1 + h11 * h * m1;
|
||||
}",
|
||||
},
|
||||
Helper {
|
||||
name: "curve_eval",
|
||||
source: "\
|
||||
// Evaluate the five-point tone curve at `x`.
|
||||
//
|
||||
// Spans are unrolled and secants passed explicitly; see `curve_span` for why
|
||||
// there is no array indexing here. Points arrive pre-sorted with a minimum
|
||||
// separation enforced on the CPU, so no division can be by zero.
|
||||
fn curve_eval(
|
||||
x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32,
|
||||
x3: f32, y3: f32, x4: f32, y4: f32, x: f32,
|
||||
) -> f32 {
|
||||
// Outside the point range the curve is flat, matching how the endpoints
|
||||
// read in the widget: nothing exists beyond them to interpolate toward.
|
||||
if (x <= x0) { return y0; }
|
||||
if (x >= x4) { return y4; }
|
||||
|
||||
let s0 = (y1 - y0) / (x1 - x0);
|
||||
let s1 = (y2 - y1) / (x2 - x1);
|
||||
let s2 = (y3 - y2) / (x3 - x2);
|
||||
let s3 = (y4 - y3) / (x4 - x3);
|
||||
|
||||
// The outermost secants are duplicated, so the boundary tangents match
|
||||
// the span they adjoin.
|
||||
if (x < x1) { return curve_span(x0, y0, x1, y1, s0, s1, x); }
|
||||
if (x < x2) { return curve_span(x1, y1, x2, y2, s0, s2, x); }
|
||||
if (x < x3) { return curve_span(x2, y2, x3, y3, s1, s3, x); }
|
||||
return curve_span(x3, y3, x4, y4, s2, s3, x);
|
||||
}",
|
||||
},
|
||||
];
|
||||
@@ -302,9 +309,7 @@ if (luma > 0.0001) {
|
||||
// would not correspond to the middle of the visible range.
|
||||
let encoded = pow(clamp(luma, 0.0, 1.0), 1.0 / 2.2);
|
||||
|
||||
let xs = array<f32, 5>(x0, x1, x2, x3, x4);
|
||||
let ys = array<f32, 5>(y0, y1, y2, y3, y4);
|
||||
let curved = curve_eval(xs, ys, encoded);
|
||||
let curved = curve_eval(x0, y0, x1, y1, x2, y2, x3, y3, x4, y4, encoded);
|
||||
|
||||
let decoded = pow(clamp(curved, 0.0, 1.0), 2.2);
|
||||
// Applied as a ratio so hue is preserved, exactly as contrast does.
|
||||
|
||||
Reference in New Issue
Block a user