Give the controls a vocabulary, and let a node ask for one
widgets.slint set the rule — screens consume components, and a bare `Theme.*` at a call site means a component is missing — and it set it for chrome only. The controls never got the same treatment, so they were written wherever they were first needed and copied from there. **The slider was private to the develop panel.** `SliderTrack`, with the fifty-line preamble explaining how it wrests a drag away from a Flickable, lived inside adjust.slint and no other screen could reach it. It shows: export quality is a 1-to-100 value, and the settings page offered a free-text box for it, with the range written in a hint and enforced nowhere. `to-float()` answers 0 for anything it cannot parse, so a typo saved a quality of 0 and the page displayed the 0 back as though it had been asked for. The tick-box was written twice, in launch.slint and settings.slint, from the same 18px box and the same handler; the second carried a comment deferring the lift until a third caller appeared. The label-and-hint header was written three times inside settings.slint alone. controls.slint is the input layer beside widgets.slint's chrome layer, and the constraint that makes it reusable is that **nothing in it knows about `ParamRow`** — that struct is the develop panel's flattening of the capability model, and a control that imported it could only ever be used by the develop panel. The primitives take plain numbers; the ParamRow-shaped wrappers stay in the panel that owns the model. 658 lines came out of the three screens. `SliderRow` is the slider-plus-number-box ARCH §4.3 names as the pointer presentation of a bounded scalar, and quality is its first adopter. It commits on gesture end rather than on every movement, because the settings page saves to disk on change and a two-second drag is a couple of hundred writes where a text field committed once. The develop panel keeps the live stream — that is what its pipeline is for — so `SliderTrack` now reports both. **The other half is the descriptor.** FR-DEV-3a and ARCH §4.3a already specify more than was built: an ordered preference list of widgets rather than one, the demands a widget makes, and kinds beyond scalar and bool. - `Presentation.widgets` is now a list, walked by `choose`, falling back to plain sliders. Falling off the end is not an error, and there is a test asserting an operation asking only for an unimplemented widget still yields one control per parameter. - `WidgetDemand` carries what a widget inherently needs — two-dimensional dragging, precise pointing — and no pixels, breakpoints or platform names. - `WidgetKind` grows to the specified set. There is deliberately no `Colour` *kind*: a colour is three numbers, and a value type that is not an `f32` would reach through the graph, the uniform block and the sidecar format to buy what `ColourWheel` over three scalars already describes. Every widget here is a hint over ordinary scalars, which is what keeps the fallback honest. - `ParamKind::Enum` is the one new shape, and it fits because a variant index is exact in binary32. `kind: enum` with a `variants:` list works in `ops/*.yaml`, so a node declaring one gets a segmented control with no UI file edited — which is the promise ops/mod.rs already makes. The panel's dispatch was duplicated: a lone parameter and a grouped one each wrote out their own list of kinds, so `enum` would have had to be added twice and a kind added to one would appear or vanish depending on how many parameters its operation happened to declare. `ParamControl` is now the only such chain. `rows_from` is free-standing rather than a method, which is what lets the FR-DEV-3c acceptance test requirements.md asks for actually be written: an operation the frontend has never heard of, appearing in a generated panel, with no GPU in sight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -586,9 +586,51 @@ fn param_ctor(
|
||||
max,
|
||||
)
|
||||
}
|
||||
// A fixed list of named alternatives. The value is the chosen index,
|
||||
// so the range is the list's own bounds and a node declaring one needs
|
||||
// no `min:`/`max:` of its own.
|
||||
//
|
||||
// Variants are localisation keys, like every other label in a node —
|
||||
// the core never holds a display string (NFR-A11Y-1). The default is
|
||||
// always the first, so `reset` means the same thing here as everywhere
|
||||
// else; a node whose neutral choice is not first has listed them in
|
||||
// the wrong order.
|
||||
"enum" => {
|
||||
let variants = spec
|
||||
.get("variants")
|
||||
.and_then(Value::as_sequence)
|
||||
.ok_or_else(|| format!("`{ctx}` is `kind: enum` and needs a `variants:` list"))?;
|
||||
if variants.len() < 2 {
|
||||
return Err(format!(
|
||||
"`{ctx}.variants` lists {} choice(s); a control the user \
|
||||
cannot change is not a control",
|
||||
variants.len()
|
||||
));
|
||||
}
|
||||
let keys = variants
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
as_str(v, &format!("{ctx}.variants[{i}]"))
|
||||
.map(|k| format!("LocalizedKey({k:?})"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
(
|
||||
format!(
|
||||
"ParamDescriptor::choice({:?}, {:?}, &[{}])",
|
||||
id,
|
||||
label,
|
||||
keys.join(", ")
|
||||
),
|
||||
0.0,
|
||||
0.0,
|
||||
(variants.len() - 1) as f64,
|
||||
)
|
||||
}
|
||||
other => {
|
||||
return Err(format!(
|
||||
"`{ctx}.kind` is `{other}`; expected stops, amount, switch, fraction or scalar"
|
||||
"`{ctx}.kind` is `{other}`; expected stops, amount, switch, \
|
||||
fraction, scalar or enum"
|
||||
))
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user