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
+157 -2
View File
@@ -28,8 +28,9 @@ export struct ParamRow {
// section heading without knowing what the sections are.
starts-group: bool,
// Which control to build. Mirrors ParamKind.
kind: string, // "scalar" | "bool"
// Which control to build. Mirrors ParamKind, plus the widget kinds an
// operation can request through its presentation.
kind: string, // "scalar" | "bool" | "curve"
value: float,
default-value: float,
@@ -37,6 +38,16 @@ export struct ParamRow {
maximum: float,
precision: int,
unit: string,
// Curve rows only: the point coordinates, x and y interleaved.
//
// Carried on the row rather than fetched separately because a Slint
// model row is the unit of update — splitting them would let the curve
// and its points refresh out of step. Empty for every other kind.
//
// `param-index` on a curve row is the index of the *first* point
// parameter, so a drag routes back by offsetting from it.
points: [float],
}
// A slider with a label, value readout, and double-click reset.
@@ -168,12 +179,138 @@ component ParamSlider inherits Rectangle {
}
}
// A tone curve editor: a square grid with draggable control points.
//
// The curve *line* is drawn from `samples`, which Rust evaluates with the
// same spline the shader uses. Reimplementing the interpolation here would
// mean two curves that could disagree — the drawn one and the applied one —
// which is the worst possible failure for a control whose whole job is to
// show you what it is doing.
component CurveEditor inherits Rectangle {
in property <ParamRow> data;
// Polyline of the curve, y values sampled at even x. 0..1, y up.
in property <[float]> samples;
// Which point is being dragged, or -1.
in-out property <int> active-point: -1;
callback point-moved(int, float, float);
callback reset();
property <int> point-count: root.data.points.length / 2;
// Square: a tone curve is read as a deviation from the 45° diagonal, and
// that reading only works if the axes share a scale.
height: self.width;
plot := Rectangle {
background: Theme.ground;
border-width: 1px;
border-color: Theme.rule;
// Quarter gridlines and the identity diagonal, so the shape of the
// edit is legible at a glance.
for i in [1, 2, 3]: Rectangle {
x: parent.width * i / 4;
width: 1px;
background: Theme.rule;
opacity: 0.5;
}
for i in [1, 2, 3]: Rectangle {
y: parent.height * i / 4;
height: 1px;
background: Theme.rule;
opacity: 0.5;
}
// The curve. One thin rectangle per sample: Slint has no polyline
// primitive, and at this size the segments are sub-pixel anyway.
for s[i] in root.samples: Rectangle {
property <float> next: i + 1 < root.samples.length
? root.samples[i + 1] : s;
x: parent.width * i / max(root.samples.length - 1, 1);
width: parent.width / max(root.samples.length - 1, 1) + 1px;
// Span the segment vertically, so a steep section stays joined.
y: parent.height * (1.0 - max(s, self.next));
height: max(parent.height * abs(self.next - s), 1.5px);
background: Theme.accent;
}
// Control points.
for idx in [0, 1, 2, 3, 4]: Rectangle {
property <bool> exists: idx < root.point-count;
property <float> px: root.data.points[idx * 2];
property <float> py: root.data.points[idx * 2 + 1];
visible: self.exists;
x: parent.width * self.px - 5px;
y: parent.height * (1.0 - self.py) - 5px;
width: 10px;
height: 10px;
border-radius: 5px;
background: root.active-point == idx ? Theme.accent : Theme.ink;
border-width: 1px;
border-color: Theme.ground;
}
// Catches the release even when the pointer has left the grab
// target, and resets on double-click.
area := TouchArea {
width: 100%;
height: 100%;
moved => {
if (root.active-point >= 0) {
root.point-moved(
root.active-point,
clamp(self.mouse-x / parent.width, 0.0, 1.0),
clamp(1.0 - self.mouse-y / parent.height, 0.0, 1.0));
}
}
pointer-event(ev) => {
if (ev.kind == PointerEventKind.up) {
root.active-point = -1;
}
}
double-clicked => { root.reset(); }
}
// One grab target per point, above the shared area so a press picks
// the point under the pointer rather than the panel guessing.
for idx in [0, 1, 2, 3, 4]: TouchArea {
property <bool> exists: idx < root.point-count;
x: parent.width * root.data.points[idx * 2] - 11px;
y: parent.height * (1.0 - root.data.points[idx * 2 + 1]) - 11px;
width: 22px;
height: 22px;
enabled: self.exists;
pointer-event(ev) => {
if (ev.kind == PointerEventKind.down) {
root.active-point = idx;
}
}
moved => {
if (root.active-point == idx) {
root.point-moved(
idx,
clamp((self.x + self.mouse-x) / parent.width, 0.0, 1.0),
clamp(1.0 - (self.y + self.mouse-y) / parent.height, 0.0, 1.0));
}
}
}
}
}
// The panel: a heading per operation, a control per parameter.
export component AdjustPanel inherits Rectangle {
in property <[ParamRow]> rows;
in property <bool> enabled: true;
/// 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;
callback param-changed(int, int, float);
callback param-reset(int, int);
callback curve-reset(int);
callback reset-all();
background: Theme.surface;
@@ -246,6 +383,24 @@ export component AdjustPanel inherits Rectangle {
root.param-reset(row.op-index, row.param-index);
}
}
if row.kind == "curve": CurveEditor {
data: row;
samples: root.curve-samples;
// A point carries two parameters, so the parameter
// index is the row's base plus the point's offset.
// This component still knows nothing about which
// operation it belongs to.
point-moved(point, x, y) => {
root.param-changed(
row.op-index, row.param-index + point * 2, x);
root.param-changed(
row.op-index, row.param-index + point * 2 + 1, y);
}
reset => {
root.curve-reset(row.op-index);
}
}
}
}
}