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:
+366
-168
@@ -84,183 +84,250 @@ impl DevelopSession {
|
||||
/// Built entirely from the capability list. The `kind` string chooses the
|
||||
/// widget; nothing switches on a parameter's identity.
|
||||
pub fn rows(&self) -> Vec<ParamRow> {
|
||||
let mut rows = Vec::new();
|
||||
for (op_index, op) in self.graph.capabilities().iter().enumerate() {
|
||||
// Framing has a panel of its own.
|
||||
rows_from(&self.graph.capabilities())
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this frontend has an implementation of `widget`.
|
||||
///
|
||||
/// The single source of truth for what the panel can draw. A function rather
|
||||
/// than a constant list so that a kind whose availability depends on something
|
||||
/// — a canvas-hosted widget needs a canvas — has one obvious place to say so.
|
||||
pub(crate) fn draws(widget: WidgetKind) -> bool {
|
||||
match widget {
|
||||
WidgetKind::ToneCurve => true,
|
||||
// Not implemented here. The `is_on_canvas` kinds additionally need a
|
||||
// host the panel does not have: the canvas draws those, and the
|
||||
// panel's job is the affordance that turns them on.
|
||||
WidgetKind::ColourWheel
|
||||
| WidgetKind::CropOverlay
|
||||
| WidgetKind::GradientHandle
|
||||
| WidgetKind::BrushMask
|
||||
| WidgetKind::WhitePoint => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The panel model for a set of capabilities.
|
||||
///
|
||||
/// Free-standing rather than a method, and that is the point: it needs no GPU,
|
||||
/// no decoded image and no session, so the whole descriptor-to-panel path can
|
||||
/// be exercised against a hand-built capability list. That is what the
|
||||
/// FR-DEV-3c acceptance test asks for — an operation the frontend has never
|
||||
/// heard of appearing in a generated panel — and it cannot be asserted at all
|
||||
/// if generating a row requires a device.
|
||||
pub(crate) fn rows_from(caps: &[OpCapability]) -> Vec<ParamRow> {
|
||||
let mut rows = Vec::new();
|
||||
for (op_index, op) in caps.iter().enumerate() {
|
||||
// Framing has a panel of its own.
|
||||
//
|
||||
// The one place this side names a stage, and the exception proves
|
||||
// the rule: every *other* operation is rendered from its
|
||||
// descriptor alone. Framing is skipped because its parameters are
|
||||
// not sliders in any useful sense — four crop edges are dragged on
|
||||
// the photograph and a quarter turn is a button — so it is
|
||||
// presented by `GeometryPanel` instead of generated here. Emitting
|
||||
// both would show the same eight values twice, in one good control
|
||||
// surface and one bad one.
|
||||
if op.id == dr_pipeline::framing::ID {
|
||||
continue;
|
||||
}
|
||||
// Where this operation's rows begin. The panel groups by walking
|
||||
// back to it, so it has to be taken before any row is pushed.
|
||||
let group_head = rows.len();
|
||||
// An operation may ask for one widget spanning several
|
||||
// parameters. Honouring it is optional — dropping this block
|
||||
// renders the same parameters as ordinary sliders, and the edit
|
||||
// still works — which is exactly why the hint is a hint.
|
||||
if let Some(presentation) = &op.presentation {
|
||||
// **The widget registry, and the only one.**
|
||||
//
|
||||
// The one place this side names a stage, and the exception proves
|
||||
// the rule: every *other* operation is rendered from its
|
||||
// descriptor alone. Framing is skipped because its parameters are
|
||||
// not sliders in any useful sense — four crop edges are dragged on
|
||||
// the photograph and a quarter turn is a button — so it is
|
||||
// presented by `GeometryPanel` instead of generated here. Emitting
|
||||
// both would show the same eight values twice, in one good control
|
||||
// surface and one bad one.
|
||||
if op.id == dr_pipeline::framing::ID {
|
||||
// `choose` walks the operation's preference list and hands
|
||||
// back the first entry this frontend implements (ARCH §4.3a).
|
||||
// Everything not listed here is unimplemented by definition,
|
||||
// so an operation asking for a colour wheel gets sliders
|
||||
// rather than an error — which is the designed behaviour, not
|
||||
// a gap: every parameter is an individually addressable
|
||||
// scalar, so the edit still works.
|
||||
//
|
||||
// The `match` inside is exhaustive on purpose. Adding a
|
||||
// `WidgetKind` to the core stops this compiling until someone
|
||||
// has decided, here, whether this frontend draws it.
|
||||
let row = presentation.choose(draws).and_then(|widget| match widget {
|
||||
WidgetKind::ToneCurve => curve_row(op_index, group_head, op, presentation),
|
||||
// Named but not drawn here. Listed rather than caught
|
||||
// by a wildcard so that the next kind added to the
|
||||
// core surfaces as a compile error in this file.
|
||||
WidgetKind::ColourWheel
|
||||
| WidgetKind::CropOverlay
|
||||
| WidgetKind::GradientHandle
|
||||
| WidgetKind::BrushMask
|
||||
| WidgetKind::WhitePoint => None,
|
||||
});
|
||||
if let Some(row) = row {
|
||||
rows.push(row);
|
||||
continue;
|
||||
}
|
||||
// Where this operation's rows begin. The panel groups by walking
|
||||
// back to it, so it has to be taken before any row is pushed.
|
||||
let group_head = rows.len();
|
||||
// An operation may ask for one widget spanning several
|
||||
// parameters. Honouring it is optional — dropping this block
|
||||
// renders the same parameters as ordinary sliders, and the edit
|
||||
// still works — which is exactly why the hint is a hint.
|
||||
if let Some(presentation) = &op.presentation {
|
||||
// A `match` rather than an `if let`: when a second widget
|
||||
// kind is added, this stops compiling until it is handled,
|
||||
// rather than silently falling through to sliders.
|
||||
let row = match presentation.widget {
|
||||
WidgetKind::Curve => self.curve_row(op_index, group_head, op, presentation),
|
||||
};
|
||||
if let Some(row) = row {
|
||||
rows.push(row);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Whether anything in this operation has been touched, aggregated
|
||||
// before the rows are built so every row of the group can carry
|
||||
// the same answer — the panel's heading is one of them and cannot
|
||||
// see the others.
|
||||
//
|
||||
// Derived here rather than asked of the core: a group is a
|
||||
// composition this side invented, so whether one is modified is
|
||||
// this side's question to answer (ARCH §4.3a).
|
||||
let group_modified = op.params.iter().any(|p| p.value != p.default);
|
||||
let group_len = op.params.len() as i32;
|
||||
|
||||
// The aspect the previous row belonged to, so a run can be told
|
||||
// from its continuation. Reset per operation: two operations that
|
||||
// happened to facet on the same key are still two groups.
|
||||
let mut previous_aspect: Option<&str> = None;
|
||||
|
||||
for param_index in presentation_order(&op.params) {
|
||||
let p = &op.params[param_index];
|
||||
let (kind, min, max, precision, unit) = match &p.kind {
|
||||
ParamKind::Scalar {
|
||||
min,
|
||||
max,
|
||||
unit,
|
||||
precision,
|
||||
..
|
||||
} => (
|
||||
"scalar",
|
||||
*min,
|
||||
*max,
|
||||
i32::from(*precision),
|
||||
unit_suffix(*unit),
|
||||
),
|
||||
ParamKind::Bool => ("bool", 0.0, 1.0, 0, ""),
|
||||
};
|
||||
|
||||
// A faceted parameter is named by its *subject* — the band —
|
||||
// because its aspect is already written above the run it sits
|
||||
// in. Unfaceted parameters keep their own label, which is
|
||||
// every operation but the mixer.
|
||||
let param_label = match &p.facet {
|
||||
Some(f) => labels::resolve(f.subject.0),
|
||||
None => labels::resolve(p.label.0),
|
||||
};
|
||||
let aspect = p.facet.as_ref().map(|f| f.aspect.0);
|
||||
let starts_facet = aspect.is_some() && aspect != previous_aspect;
|
||||
previous_aspect = aspect;
|
||||
|
||||
rows.push(ParamRow {
|
||||
op_index: op_index as i32,
|
||||
param_index: param_index as i32,
|
||||
op_label: labels::resolve(op.label.0).into(),
|
||||
param_label: param_label.into(),
|
||||
facet_label: aspect.map(labels::resolve).unwrap_or_default().into(),
|
||||
starts_facet,
|
||||
// -1 rather than an `Option`, which a Slint struct cannot
|
||||
// carry: 0° is red, so no value in range can stand for
|
||||
// "no swatch".
|
||||
swatch_hue: p.facet.as_ref().and_then(|f| f.subject_hue).unwrap_or(-1.0),
|
||||
group_head: group_head as i32,
|
||||
group_len,
|
||||
group_modified,
|
||||
kind: kind.into(),
|
||||
value: p.value,
|
||||
default_value: p.default,
|
||||
minimum: min,
|
||||
maximum: max,
|
||||
precision,
|
||||
unit: unit.into(),
|
||||
// Only curve rows carry points.
|
||||
points: slint::ModelRc::new(slint::VecModel::from(Vec::<f32>::new())),
|
||||
});
|
||||
}
|
||||
}
|
||||
rows
|
||||
|
||||
// Whether anything in this operation has been touched, aggregated
|
||||
// before the rows are built so every row of the group can carry
|
||||
// the same answer — the panel's heading is one of them and cannot
|
||||
// see the others.
|
||||
//
|
||||
// Derived here rather than asked of the core: a group is a
|
||||
// composition this side invented, so whether one is modified is
|
||||
// this side's question to answer (ARCH §4.3a).
|
||||
let group_modified = op.params.iter().any(|p| p.value != p.default);
|
||||
let group_len = op.params.len() as i32;
|
||||
|
||||
// The aspect the previous row belonged to, so a run can be told
|
||||
// from its continuation. Reset per operation: two operations that
|
||||
// happened to facet on the same key are still two groups.
|
||||
let mut previous_aspect: Option<&str> = None;
|
||||
|
||||
for param_index in presentation_order(&op.params) {
|
||||
let p = &op.params[param_index];
|
||||
// Empty for every kind but `Enum`, which is what the panel
|
||||
// keys on to build a segmented control rather than a slider.
|
||||
let mut choices: Vec<slint::SharedString> = Vec::new();
|
||||
let (kind, min, max, precision, unit) = match &p.kind {
|
||||
ParamKind::Scalar {
|
||||
min,
|
||||
max,
|
||||
unit,
|
||||
precision,
|
||||
..
|
||||
} => (
|
||||
"scalar",
|
||||
*min,
|
||||
*max,
|
||||
i32::from(*precision),
|
||||
unit_suffix(*unit),
|
||||
),
|
||||
ParamKind::Bool => ("bool", 0.0, 1.0, 0, ""),
|
||||
// The value is a variant index, so the range is the list's
|
||||
// own bounds and the precision is whole numbers. Labels are
|
||||
// resolved here, against this crate's catalogue, because
|
||||
// the core deals in localisation keys only (NFR-A11Y-1).
|
||||
ParamKind::Enum { variants } => {
|
||||
choices = variants
|
||||
.iter()
|
||||
.map(|v| labels::resolve(v.0).into())
|
||||
.collect();
|
||||
("enum", 0.0, variants.len().saturating_sub(1) as f32, 0, "")
|
||||
}
|
||||
};
|
||||
|
||||
// A faceted parameter is named by its *subject* — the band —
|
||||
// because its aspect is already written above the run it sits
|
||||
// in. Unfaceted parameters keep their own label, which is
|
||||
// every operation but the mixer.
|
||||
let param_label = match &p.facet {
|
||||
Some(f) => labels::resolve(f.subject.0),
|
||||
None => labels::resolve(p.label.0),
|
||||
};
|
||||
let aspect = p.facet.as_ref().map(|f| f.aspect.0);
|
||||
let starts_facet = aspect.is_some() && aspect != previous_aspect;
|
||||
previous_aspect = aspect;
|
||||
|
||||
rows.push(ParamRow {
|
||||
op_index: op_index as i32,
|
||||
param_index: param_index as i32,
|
||||
op_label: labels::resolve(op.label.0).into(),
|
||||
param_label: param_label.into(),
|
||||
facet_label: aspect.map(labels::resolve).unwrap_or_default().into(),
|
||||
starts_facet,
|
||||
// -1 rather than an `Option`, which a Slint struct cannot
|
||||
// carry: 0° is red, so no value in range can stand for
|
||||
// "no swatch".
|
||||
swatch_hue: p.facet.as_ref().and_then(|f| f.subject_hue).unwrap_or(-1.0),
|
||||
group_head: group_head as i32,
|
||||
group_len,
|
||||
group_modified,
|
||||
kind: kind.into(),
|
||||
value: p.value,
|
||||
default_value: p.default,
|
||||
minimum: min,
|
||||
maximum: max,
|
||||
precision,
|
||||
unit: unit.into(),
|
||||
// Only curve rows carry points.
|
||||
points: slint::ModelRc::new(slint::VecModel::from(Vec::<f32>::new())),
|
||||
choices: slint::ModelRc::new(slint::VecModel::from(choices)),
|
||||
});
|
||||
}
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
/// One row standing for a whole curve.
|
||||
///
|
||||
/// Returns `None` if the operation's parameters do not look like point
|
||||
/// coordinates, in which case the caller falls back to sliders rather than
|
||||
/// rendering a broken widget.
|
||||
fn curve_row(
|
||||
op_index: usize,
|
||||
group_head: usize,
|
||||
op: &OpCapability,
|
||||
presentation: &Presentation,
|
||||
) -> Option<ParamRow> {
|
||||
// Points are x/y pairs, so an odd count means the operation and this
|
||||
// code disagree about the layout.
|
||||
if presentation.params.len() < 2 || !presentation.params.len().is_multiple_of(2) {
|
||||
log::warn!("{}: curve widget needs an even parameter count", op.id);
|
||||
return None;
|
||||
}
|
||||
|
||||
/// One row standing for a whole curve.
|
||||
///
|
||||
/// Returns `None` if the operation's parameters do not look like point
|
||||
/// coordinates, in which case the caller falls back to sliders rather
|
||||
/// than rendering a broken widget.
|
||||
fn curve_row(
|
||||
&self,
|
||||
op_index: usize,
|
||||
group_head: usize,
|
||||
op: &OpCapability,
|
||||
presentation: &Presentation,
|
||||
) -> Option<ParamRow> {
|
||||
// Points are x/y pairs, so an odd count means the operation and this
|
||||
// code disagree about the layout.
|
||||
if presentation.params.len() < 2 || !presentation.params.len().is_multiple_of(2) {
|
||||
log::warn!("{}: curve widget needs an even parameter count", op.id);
|
||||
// The widget addresses points by offset from the first, so they must
|
||||
// be contiguous in the capability list.
|
||||
let base = op
|
||||
.params
|
||||
.iter()
|
||||
.position(|p| p.id == presentation.params[0])?;
|
||||
for (i, id) in presentation.params.iter().enumerate() {
|
||||
if op.params.get(base + i).map(|p| p.id) != Some(*id) {
|
||||
log::warn!("{}: curve parameters are not contiguous", op.id);
|
||||
return None;
|
||||
}
|
||||
|
||||
// The widget addresses points by offset from the first, so they must
|
||||
// be contiguous in the capability list.
|
||||
let base = op
|
||||
.params
|
||||
.iter()
|
||||
.position(|p| p.id == presentation.params[0])?;
|
||||
for (i, id) in presentation.params.iter().enumerate() {
|
||||
if op.params.get(base + i).map(|p| p.id) != Some(*id) {
|
||||
log::warn!("{}: curve parameters are not contiguous", op.id);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let points: Vec<f32> = presentation
|
||||
.params
|
||||
.iter()
|
||||
.filter_map(|id| op.params.iter().find(|p| p.id == *id))
|
||||
.map(|p| p.value)
|
||||
.collect();
|
||||
|
||||
Some(ParamRow {
|
||||
op_index: op_index as i32,
|
||||
// The first point parameter; the widget offsets from here.
|
||||
param_index: base as i32,
|
||||
op_label: labels::resolve(op.label.0).into(),
|
||||
param_label: String::new().into(),
|
||||
// A widget spanning a whole operation is not a row in anyone's
|
||||
// grid, so it heads no run and carries no swatch.
|
||||
facet_label: String::new().into(),
|
||||
starts_facet: false,
|
||||
swatch_hue: -1.0,
|
||||
group_head: group_head as i32,
|
||||
// One widget standing for every parameter of the operation, so
|
||||
// the group it heads is itself and nothing else.
|
||||
group_len: 1,
|
||||
group_modified: op.params.iter().any(|p| p.value != p.default),
|
||||
kind: "curve".into(),
|
||||
value: 0.0,
|
||||
default_value: 0.0,
|
||||
minimum: 0.0,
|
||||
maximum: 1.0,
|
||||
precision: 4,
|
||||
unit: String::new().into(),
|
||||
points: slint::ModelRc::new(slint::VecModel::from(points)),
|
||||
})
|
||||
}
|
||||
|
||||
let points: Vec<f32> = presentation
|
||||
.params
|
||||
.iter()
|
||||
.filter_map(|id| op.params.iter().find(|p| p.id == *id))
|
||||
.map(|p| p.value)
|
||||
.collect();
|
||||
|
||||
Some(ParamRow {
|
||||
op_index: op_index as i32,
|
||||
// The first point parameter; the widget offsets from here.
|
||||
param_index: base as i32,
|
||||
op_label: labels::resolve(op.label.0).into(),
|
||||
param_label: String::new().into(),
|
||||
// A widget spanning a whole operation is not a row in anyone's
|
||||
// grid, so it heads no run and carries no swatch.
|
||||
facet_label: String::new().into(),
|
||||
starts_facet: false,
|
||||
swatch_hue: -1.0,
|
||||
group_head: group_head as i32,
|
||||
// One widget standing for every parameter of the operation, so
|
||||
// the group it heads is itself and nothing else.
|
||||
group_len: 1,
|
||||
group_modified: op.params.iter().any(|p| p.value != p.default),
|
||||
kind: "curve".into(),
|
||||
value: 0.0,
|
||||
default_value: 0.0,
|
||||
minimum: 0.0,
|
||||
maximum: 1.0,
|
||||
precision: 4,
|
||||
unit: String::new().into(),
|
||||
points: slint::ModelRc::new(slint::VecModel::from(points)),
|
||||
// A curve is not a choice between named alternatives.
|
||||
choices: slint::ModelRc::new(slint::VecModel::from(Vec::<slint::SharedString>::new())),
|
||||
})
|
||||
}
|
||||
|
||||
impl DevelopSession {
|
||||
/// The curve's shape, sampled for drawing.
|
||||
///
|
||||
/// Evaluated with `dr_pipeline`'s own spline, so the line the user drags
|
||||
@@ -1091,6 +1158,135 @@ mod tests {
|
||||
/// A widget hint only collapses an operation to one row when it is
|
||||
/// *honoured*; `rows` falls back to sliders otherwise, and mirroring that
|
||||
/// here is what keeps the test honest when a hint stops applying.
|
||||
/// TRACES: FR-DEV-3c
|
||||
/// An operation this file has never heard of, appearing in the panel.
|
||||
///
|
||||
/// The acceptance test requirements.md names for FR-DEV-3c: "a test
|
||||
/// operation added to the registry appears in a generated panel with no
|
||||
/// frontend change". Built as a capability rather than a real node so it
|
||||
/// costs the pipeline nothing — what is being asserted is the mapping from
|
||||
/// descriptor to control, and that mapping does not care whether a shader
|
||||
/// exists behind it.
|
||||
#[test]
|
||||
fn an_operation_the_frontend_has_never_heard_of_gets_controls() {
|
||||
use dr_pipeline::{LocalizedKey, ParamCapability};
|
||||
|
||||
let invented = OpCapability {
|
||||
id: OpId("invented"),
|
||||
label: LocalizedKey("op.invented"),
|
||||
active: false,
|
||||
presentation: None,
|
||||
params: vec![
|
||||
ParamCapability {
|
||||
id: ParamId("strength"),
|
||||
label: LocalizedKey("param.invented.strength"),
|
||||
kind: ParamKind::Scalar {
|
||||
min: -100.0,
|
||||
max: 100.0,
|
||||
scale: dr_pipeline::Scale::Linear,
|
||||
unit: Unit::Percent,
|
||||
precision: 0,
|
||||
},
|
||||
default: 0.0,
|
||||
value: 25.0,
|
||||
facet: None,
|
||||
},
|
||||
ParamCapability {
|
||||
id: ParamId("method"),
|
||||
label: LocalizedKey("param.invented.method"),
|
||||
kind: ParamKind::Enum {
|
||||
variants: &[
|
||||
LocalizedKey("param.invented.method.fast"),
|
||||
LocalizedKey("param.invented.method.exact"),
|
||||
],
|
||||
},
|
||||
default: 0.0,
|
||||
value: 1.0,
|
||||
facet: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let rows = rows_from(&[invented]);
|
||||
assert_eq!(rows.len(), 2, "each parameter should become one row");
|
||||
|
||||
// The scalar becomes a slider carrying its declared range and unit.
|
||||
assert_eq!(rows[0].kind, "scalar");
|
||||
assert_eq!(rows[0].minimum, -100.0);
|
||||
assert_eq!(rows[0].maximum, 100.0);
|
||||
assert_eq!(rows[0].value, 25.0);
|
||||
|
||||
// The enum becomes a choice, with its range spanning the variant
|
||||
// indices and the variant names resolved for drawing. Nothing in this
|
||||
// file names the operation or either parameter to make that happen.
|
||||
assert_eq!(rows[1].kind, "enum");
|
||||
assert_eq!(rows[1].minimum, 0.0);
|
||||
assert_eq!(rows[1].maximum, 1.0);
|
||||
assert_eq!(rows[1].precision, 0);
|
||||
assert_eq!(slint::Model::row_count(&rows[1].choices), 2);
|
||||
// The value is the selected index, which is what the segmented control
|
||||
// reads — an enum needs no separate selection field.
|
||||
assert_eq!(rows[1].value, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unimplemented_widget_falls_back_to_sliders_rather_than_vanishing() {
|
||||
// ARCH §4.3a: falling off the end of the preference list is not an
|
||||
// error. An operation asking only for a widget this frontend does not
|
||||
// draw must still yield one control per parameter, or declaring a
|
||||
// preference would be a way to make an edit unreachable.
|
||||
use dr_pipeline::{LocalizedKey, ParamCapability, WidgetDemand};
|
||||
|
||||
let wheel = OpCapability {
|
||||
id: OpId("grading"),
|
||||
label: LocalizedKey("op.grading"),
|
||||
active: false,
|
||||
presentation: Some(Presentation {
|
||||
widgets: &[WidgetKind::ColourWheel],
|
||||
demand: WidgetDemand {
|
||||
two_dimensional: true,
|
||||
precise_pointing: false,
|
||||
},
|
||||
params: &[ParamId("hue"), ParamId("strength")],
|
||||
}),
|
||||
params: vec![
|
||||
ParamCapability {
|
||||
id: ParamId("hue"),
|
||||
label: LocalizedKey("param.grading.hue"),
|
||||
kind: ParamKind::Scalar {
|
||||
min: 0.0,
|
||||
max: 360.0,
|
||||
scale: dr_pipeline::Scale::Linear,
|
||||
unit: Unit::None,
|
||||
precision: 0,
|
||||
},
|
||||
default: 0.0,
|
||||
value: 0.0,
|
||||
facet: None,
|
||||
},
|
||||
ParamCapability {
|
||||
id: ParamId("strength"),
|
||||
label: LocalizedKey("param.grading.strength"),
|
||||
kind: ParamKind::Scalar {
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
scale: dr_pipeline::Scale::Linear,
|
||||
unit: Unit::None,
|
||||
precision: 2,
|
||||
},
|
||||
default: 0.0,
|
||||
value: 0.0,
|
||||
facet: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert!(!draws(WidgetKind::ColourWheel), "precondition");
|
||||
let rows = rows_from(&[wheel]);
|
||||
assert_eq!(rows.len(), 2, "both parameters must remain reachable");
|
||||
assert!(rows.iter().all(|r| r.kind == "scalar"));
|
||||
}
|
||||
|
||||
fn rows_of(caps: &[OpCapability]) -> Vec<(usize, usize)> {
|
||||
let mut rows = Vec::new();
|
||||
for op in caps {
|
||||
@@ -1381,7 +1577,9 @@ mod tests {
|
||||
.presentation
|
||||
.as_ref()
|
||||
.expect("the curve declares a widget");
|
||||
assert_eq!(presentation.widget, WidgetKind::Curve);
|
||||
// Asked the way the panel asks it: the first preference this frontend
|
||||
// implements, not a fixed single kind.
|
||||
assert_eq!(presentation.choose(draws), Some(WidgetKind::ToneCurve));
|
||||
// Every parameter is owned by the widget, so none is left over to be
|
||||
// rendered as a stray slider.
|
||||
assert_eq!(presentation.params.len(), curve_cap.params.len());
|
||||
|
||||
Reference in New Issue
Block a user