Let an operation say what it is about, so the panel can group without naming
Tool tabs need a taxonomy, and the taxonomy was the problem: a table in `ui/` mapping operation to tab breaks FR-DEV-3a, and a `group:` field risks what `ui-refinement.md` condemned `starts-group` for — the core deciding where the panel draws things. `Attribute` threads the needle. It says what an operation *is* — tone, colour, detail, optics, geometry, effect — which is the same category as `ParamKind` and squarely on the core's side of ARCH §4.3a's line. What is drawn, where it sits and whether it is visible stay the frontend's. There is no attribute for "the third tab", the enum's order is declaration order rather than screen order, and a frontend may render these as tabs, as headings, or ignore them. The payoff is that a tab strip can be *derived*: the groups are the attributes present in the capability list, so the interface names no operation and needs no table to keep in step. An operation joins the right group by declaring what it is, which is the one thing its author is well placed to say. Plural, because the tone curve is genuinely both — an RGB curve is tonal and the per-channel curves are chromatic, and filing it under one would hide it from half the people looking for it. Required and non-empty, enforced in `build.rs`, and the failure was checked by removing the line rather than assumed. An operation with no attribute is invisible to a panel that groups by them; a build that stops costs ten seconds, a control nobody can find costs more. The vocabulary is closed for the same reason: a typo would otherwise invent a category holding exactly one operation, which looks like a deliberate one until somebody counts. Six tests over the real chain, including the hand-written operations that `build.rs` never sees and so cannot check.
This commit is contained in:
@@ -131,12 +131,65 @@ struct TestDef {
|
||||
expect_helper_wgsl: Vec<(String, Vec<String>)>,
|
||||
}
|
||||
|
||||
/// The attributes an operation declares, validated against the vocabulary.
|
||||
///
|
||||
/// **Required, and non-empty.** An operation with no attribute is invisible to
|
||||
/// a frontend that filters by them, and a control that silently does not exist
|
||||
/// is a far worse failure than a build that stops — especially when the cause
|
||||
/// is one missing line in a YAML file nobody had reason to open. Failing here
|
||||
/// costs whoever adds an operation ten seconds; failing at runtime costs a
|
||||
/// photographer a control they cannot find and cannot know is missing.
|
||||
///
|
||||
/// The vocabulary is closed on purpose. A typo would otherwise invent a
|
||||
/// category containing exactly one operation, which is indistinguishable from
|
||||
/// a deliberate new one until somebody notices the tab with a single control
|
||||
/// in it.
|
||||
fn read_attributes(root: &Mapping) -> Result<Vec<String>, String> {
|
||||
const KNOWN: [&str; 6] = ["tone", "colour", "detail", "optics", "geometry", "effect"];
|
||||
|
||||
let value = root.get("attributes").ok_or_else(|| {
|
||||
format!(
|
||||
"missing `attributes:`; every operation must say what it is about, \
|
||||
one or more of {KNOWN:?}. It is what lets the panel group \
|
||||
operations without naming any of them (ARCH §4.3a)."
|
||||
)
|
||||
})?;
|
||||
|
||||
let list = value
|
||||
.as_sequence()
|
||||
.ok_or("`attributes:` must be a list, even with one entry")?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
for entry in list {
|
||||
let name = as_str(entry, "attributes")?;
|
||||
if !KNOWN.contains(&name) {
|
||||
return Err(format!(
|
||||
"unknown attribute {name:?}; expected one of {KNOWN:?}"
|
||||
));
|
||||
}
|
||||
if out.contains(&name.to_string()) {
|
||||
return Err(format!("attribute {name:?} is listed twice"));
|
||||
}
|
||||
out.push(name.to_string());
|
||||
}
|
||||
|
||||
if out.is_empty() {
|
||||
return Err("`attributes:` is empty; an operation with no attribute \
|
||||
would not appear in a panel that groups by them"
|
||||
.into());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// A node: either declared in full, or a pointer to a hand-written type.
|
||||
enum Node {
|
||||
Declared {
|
||||
id: String,
|
||||
label: String,
|
||||
order: i64,
|
||||
/// What the operation is about (ARCH §4.3a). Never empty — see
|
||||
/// `read_attributes`.
|
||||
attributes: Vec<String>,
|
||||
doc: Option<String>,
|
||||
placement: Option<String>,
|
||||
params: Vec<ParamDef>,
|
||||
@@ -398,6 +451,7 @@ fn read_node(path: &Path, shared: &BTreeSet<&str>) -> Result<Node, String> {
|
||||
}
|
||||
|
||||
let label = as_str(root.get("label").ok_or("missing `label:`")?, "label")?.to_string();
|
||||
let attributes = read_attributes(root)?;
|
||||
|
||||
let params = read_params(root)?;
|
||||
let param_names: BTreeSet<&str> = params.iter().map(|p| p.id.as_str()).collect();
|
||||
@@ -436,6 +490,7 @@ fn read_node(path: &Path, shared: &BTreeSet<&str>) -> Result<Node, String> {
|
||||
label,
|
||||
order,
|
||||
doc: opt_prose(root, "doc", "doc")?,
|
||||
attributes,
|
||||
placement,
|
||||
params,
|
||||
uniforms,
|
||||
@@ -1404,6 +1459,7 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
|
||||
let Node::Declared {
|
||||
id,
|
||||
label,
|
||||
attributes,
|
||||
doc,
|
||||
params,
|
||||
uniforms,
|
||||
@@ -1434,7 +1490,8 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
|
||||
out.push_str(
|
||||
" #[allow(unused_imports)]\n\
|
||||
\x20 use crate::descriptor::{\n\
|
||||
\x20 LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation,\n\
|
||||
\x20 Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId,\n\
|
||||
\x20 Presentation,\n\
|
||||
\x20 Scale, Unit, WidgetDemand, WidgetKind,\n\
|
||||
\x20 };\n\
|
||||
\x20 #[allow(unused_imports)]\n\
|
||||
@@ -1467,7 +1524,20 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
|
||||
}
|
||||
let _ = writeln!(out, " {},", p.ctor);
|
||||
}
|
||||
out.push_str(" ],\n };\n");
|
||||
out.push_str(" ],\n");
|
||||
|
||||
// What the operation is about. The panel groups by these and names no
|
||||
// operation, which is what keeps FR-DEV-3a true as the set grows.
|
||||
let attrs: Vec<String> = attributes
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let mut c = a.chars();
|
||||
let head = c.next().expect("attribute names are non-empty").to_uppercase();
|
||||
format!("Attribute::{head}{}", c.as_str())
|
||||
})
|
||||
.collect();
|
||||
let _ = writeln!(out, " attributes: &[{}],", attrs.join(", "));
|
||||
out.push_str(" };\n");
|
||||
|
||||
// Helpers: node-local definitions first, then the assembled list.
|
||||
for h in local_helpers {
|
||||
|
||||
Reference in New Issue
Block a user