Let a grouped slider be dragged, by flattening the panel that drew it
Build and test / Desktop (Linux) (push) Failing after 1h4m48s
Build and test / Layer separation (push) Successful in 34s
🐳 Android image / Build and push (push) Successful in 4s
Build and test / android-image (push) Successful in 4s
Traceability / Requirement traces (push) Failing after 1m4s
Build and test / Android (aarch64) (push) Failing after 9m43s
Build and test / Desktop (Linux) (push) Failing after 1h4m48s
Build and test / Layer separation (push) Successful in 34s
🐳 Android image / Build and push (push) Successful in 4s
Build and test / android-image (push) Successful in 4s
Traceability / Requirement traces (push) Failing after 1m4s
Build and test / Android (aarch64) (push) Failing after 9m43s
White balance, highlights and shadows, and the mixer took a press, jumped once, and went dead under the finger. Exposure and contrast dragged perfectly — which is what made it read as a slider bug rather than a layout one. The panel nested. A group's head row drew the *whole* group, repeating over `row.group-len` and indexing back into `root.rows`; every other row drew nothing. So the inner repeater's model was read off the head row, and depended on that row's identity. Moving any parameter in the group rewrites that row — its own value changed, or `group-modified` flipped for its neighbours — which re-evaluated the repeater, rebuilt its items, and destroyed the `TouchArea` holding the live gesture. A lone parameter had no inner repeater, so the five single-parameter operations were never affected. Now each row draws only itself, so an update touches one control and nothing structural. The facet heading comes off `starts-facet`, which Rust already marks on the first row of a run, and the group heading is drawn by the row that heads it. It also retires the old hazard of a head row building every control in its group — thirty-six live TouchAreas behind the mixer's twelve visible ones. The same identity hazard reached the rows themselves through `ModelRc`, which compares by identity rather than contents: a fresh empty model per row per call made every row differ from itself, so `sync_rows` rewrote all of them on every event. `points` now shares one empty model as `choices` already did. Both are held by tests, because the failure is invisible in a still — every value is right and the panel looks perfect. Curve rows are excluded: their points model carries live coordinates, is rebuilt by design, and `sync_rows` writes values through the existing model rather than swapping it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+56
-66
@@ -705,46 +705,50 @@ export component AdjustPanel inherits Rectangle {
|
||||
// narrow to hit confidently is the problem it was added to fix.
|
||||
padding-right: Theme.touch-target;
|
||||
|
||||
// One iteration per row, but only a group's *first* row draws
|
||||
// anything — and it draws the whole group. Every other row
|
||||
// renders nothing at all.
|
||||
// **One row, one element, and each renders only itself.**
|
||||
//
|
||||
// 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
|
||||
// nothing. It produced the right picture and could not be
|
||||
// dragged.
|
||||
//
|
||||
// The reason is worth writing down, because it is invisible in
|
||||
// a screenshot. The inner repeater's model was `row.group-len`
|
||||
// — read off the head row — so it depended on the head row's
|
||||
// *identity*. Moving any parameter in the group rewrites that
|
||||
// row: its own value changed, or `group-modified` flipped for
|
||||
// its neighbours. Rewriting it re-evaluated the repeater, which
|
||||
// rebuilt its items, which destroyed the `TouchArea` holding
|
||||
// the gesture. The slider took the press, jumped once, and went
|
||||
// dead under the finger for the rest of the drag.
|
||||
//
|
||||
// It only ever affected multi-parameter operations — white
|
||||
// balance, highlights and shadows, the mixer — because a lone
|
||||
// parameter had no inner repeater to rebuild. Exposure and
|
||||
// contrast dragged perfectly the whole time, which is exactly
|
||||
// what made it look like a slider bug rather than a layout one.
|
||||
//
|
||||
// Flat, each element depends only on its own `row`, so an
|
||||
// update touches one control and touches nothing structural.
|
||||
// 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 {
|
||||
spacing: 0px;
|
||||
|
||||
// **A group of one is not a group.**
|
||||
// The group's name, drawn by the row that heads it, above
|
||||
// its own control rather than around the whole run.
|
||||
//
|
||||
// Five of the pipeline's operations carry a single
|
||||
// parameter — exposure, contrast, saturation, vibrance,
|
||||
// brilliance — and wrapping each in a Section produced a
|
||||
// collapsible heading, a disclosure triangle, a modified
|
||||
// dot and a hover reset around one slider, with the
|
||||
// operation's name printed in caps directly above the same
|
||||
// word as the slider's own label. Five times over, that is
|
||||
// a column that reads as chrome with controls hidden in it.
|
||||
//
|
||||
// So a lone parameter is drawn bare. It loses the group
|
||||
// reset, which cost nothing: the slider already resets on
|
||||
// double-click and right-click, and the section's reset was
|
||||
// a hover-only affordance no finger could reach anyway.
|
||||
//
|
||||
// `row` is the entry here — the group's head is its only
|
||||
// member — so there is nothing to index back into.
|
||||
if row.group-head == i && row.group-len == 1: ParamControl {
|
||||
data: row;
|
||||
curve-samples: root.curve-samples;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
param-changed(op, param, v) => {
|
||||
root.param-changed(op, param, v);
|
||||
}
|
||||
param-reset(op, param) => { root.param-reset(op, param); }
|
||||
curve-reset(op) => { root.curve-reset(op); }
|
||||
}
|
||||
|
||||
// `if` rather than a zero height: a hidden-but-present
|
||||
// section would still *build* its whole group, so every
|
||||
// control would exist once per row of its own group —
|
||||
// thirty-six live TouchAreas behind the colour mixer's
|
||||
// twelve visible ones. The conditional builds nothing.
|
||||
// **A group of one is not a group.** Five of the pipeline's
|
||||
// operations carry a single parameter — exposure, contrast,
|
||||
// saturation, vibrance, brilliance — and giving each a
|
||||
// heading printed the operation's name in caps directly
|
||||
// above the same word as the slider's own label. Five times
|
||||
// over, that is a column that reads as chrome with controls
|
||||
// hidden in it. So a lone parameter is drawn bare; it loses
|
||||
// the group reset, which costs nothing, since the slider
|
||||
// already resets on double-click and right-click.
|
||||
if row.group-head == i && row.group-len > 1: VerticalLayout {
|
||||
spacing: 0px;
|
||||
padding-top: Theme.gap-sm;
|
||||
@@ -760,39 +764,25 @@ export component AdjustPanel inherits Rectangle {
|
||||
// heading itself has no opinion about it.
|
||||
reset => { root.op-reset(row.op-index); }
|
||||
}
|
||||
}
|
||||
|
||||
// The group's own rows, addressed by offset from its
|
||||
// head. `root.rows[...]` rather than the loop's `row`:
|
||||
// this repeats over a count, so `n` is a number and
|
||||
// the row has to be fetched.
|
||||
for n in row.group-len: VerticalLayout {
|
||||
property <ParamRow> entry: root.rows[row.group-head + n];
|
||||
// A group whose parameters form a grid names each run once.
|
||||
// Rust has already stacked the rows so a run is contiguous
|
||||
// and marked its first, which is what lets a flat loop draw
|
||||
// a heading that belongs to several rows.
|
||||
if row.starts-facet: FacetHeading {
|
||||
title: row.facet-label;
|
||||
}
|
||||
|
||||
spacing: 0px;
|
||||
|
||||
// A group whose parameters form a grid names each
|
||||
// run once. Rust has already stacked the rows so a
|
||||
// run is contiguous and marked its first row, for
|
||||
// the same reason `group-head` exists: this model
|
||||
// is flat and a `for` cannot nest inside a
|
||||
// boundary discovered at runtime.
|
||||
if entry.starts-facet: FacetHeading {
|
||||
title: entry.facet-label;
|
||||
}
|
||||
|
||||
ParamControl {
|
||||
data: entry;
|
||||
curve-samples: root.curve-samples;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
param-changed(op, param, v) => {
|
||||
root.param-changed(op, param, v);
|
||||
}
|
||||
param-reset(op, param) => {
|
||||
root.param-reset(op, param);
|
||||
}
|
||||
curve-reset(op) => { root.curve-reset(op); }
|
||||
}
|
||||
ParamControl {
|
||||
data: row;
|
||||
curve-samples: root.curve-samples;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
param-changed(op, param, v) => {
|
||||
root.param-changed(op, param, v);
|
||||
}
|
||||
param-reset(op, param) => { root.param-reset(op, param); }
|
||||
curve-reset(op) => { root.curve-reset(op); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user