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:
2026-08-09 15:50:40 +02:00
parent f630a3ff81
commit 2ca1716a29
10 changed files with 888 additions and 65 deletions
+50 -45
View File
@@ -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.