nord_format/panel.rs
1//! What a body's fields *are* to a player: which controls sit together, and which of
2//! them the instrument is using for the state the file holds.
3//!
4//! The field registry answers where a field sits, what it accepts and what kind of
5//! control it is. Three things it cannot answer, because none of them is a property of a
6//! placement:
7//!
8//! - **Order.** The registry lists fields in bit order, so an organ layer's nine drawbars
9//! need not be adjacent and a knob need not follow the switch that arms it.
10//! - **Grouping.** A dotted prefix is the only structure a path carries, and a body as
11//! flat as the Electro 5 program has one prefix per panel and nothing below it.
12//! - **Relevance.** Which controls the instrument is actually using is *stateful*: an
13//! Electro 5 keeps every organ model's registration and plays one, a Stage keeps every
14//! layer and enables some.
15//!
16//! A [`Panel`] states all three, as data, per format. It is hand-authored — semantics
17//! cannot be derived from bit placement — and it is inspectable and testable rather than
18//! a pile of closures.
19//!
20//! # What a caller may rely on
21//!
22//! - [`of`] answers for a decoded file, and answers `None` where nobody has authored a
23//! layout. **Absence is normal**: a caller falls back to whatever it does today, and no
24//! format is required to have one.
25//! - [`Panel::resolve`] is the whole render path: one call over a body's `fields()`
26//! returns the groups with their fields, their effective relevance, and whatever no
27//! group named. It indexes the field list once, so it does not cost a scan per member.
28//! [`Panel::named`] and [`Panel::leftovers`] are the same questions asked of a body's
29//! *specs*, for a caller inspecting a layout without a file in hand.
30//! - A [`Group`] names its members in **reading order** — the order the panel puts them
31//! in, not the order the bits do.
32//! - A path a group names is a real registry path of that body, and no path is named
33//! twice. A test in this module holds both against every layout the crate ships, so a
34//! layout cannot quietly rot as a body gains fields.
35//! - [`Panel::exhaustive`] says whether the groups account for every registered field. If
36//! it is false, the leftovers can be non-empty and a caller still has somewhere to put
37//! them.
38//! - A morph slot is **not** named by any group: it belongs to the parameter it morphs,
39//! which the group names, and [`FieldSpec::morph_parent`] resolves the relation. It is
40//! why a Stage body, most of whose fields are morph slots, takes a layout of a
41//! hundred-odd lines rather than one line per field.
42//!
43//! # Relevance is not visibility
44//!
45//! [`Group::is_relevant`] answers one question: *for the state this file holds, is the
46//! instrument using these controls?* A group that is not relevant is still state the file
47//! carries and still writable — an organ registration for a model that is not selected is
48//! kept, not cleared. Whether that means hidden, dimmed, or shown behind a fold is the
49//! caller's decision, and it may reasonably differ by depth: a whole section nobody is
50//! playing is worth hiding, where the second of two registrations is worth showing
51//! quietly.
52//!
53//! A condition is a set of value matches, any one of which satisfies it, and a nested
54//! group is relevant only if its parent is — so a disjunction is a wider condition and a
55//! conjunction is another level of nesting. That is deliberately less than a predicate
56//! language: every condition stays comparable, printable and checkable against the
57//! field's own legal values.
58
59use std::collections::{HashMap, HashSet};
60
61use crate::fields::{Field, FieldSpec};
62use crate::formats::{ne5, ns4};
63use crate::{Entity, Live, Program};
64
65/// One body's controls, grouped the way its instrument groups them.
66///
67/// ⚠️ Not the Electro 5's `CenterPanel` and friends, which are *bodies* — nested
68/// `#[bitbody]`s at a byte range. This is the panel as a reader sees it, and it cuts
69/// across those bodies freely.
70#[derive(Debug)]
71pub struct Panel {
72 /// The sections, in the order a reader meets them.
73 pub groups: &'static [Group],
74 /// Whether the groups account for every field the body registers.
75 ///
76 /// True is checked by this module's tests, so an exhaustive layout stays exhaustive
77 /// as the body grows: a newly declared field fails the test until it is placed. False
78 /// means [`Self::leftovers`] can be non-empty and a caller needs somewhere to put it.
79 pub exhaustive: bool,
80}
81
82/// How a group is chosen when it is one of several stored alternatives: writing `value`
83/// to `field` makes the instrument play this one.
84///
85/// Not a relevance condition. The other alternatives stay relevant — a stored preset the
86/// instrument is not playing is still state the panel offers — where a group whose
87/// [`Group::when`] fails is one the instrument is not using at all.
88#[derive(Debug)]
89pub struct Selection {
90 /// The field that chooses between the sibling groups.
91 pub field: &'static str,
92 /// The value that selects this group, spelled as [`Field::value`] spells it.
93 pub value: &'static str,
94}
95
96impl Selection {
97 /// Whether the body's current value selects this group.
98 pub fn selected(&self, fields: &[Field]) -> bool {
99 fields
100 .iter()
101 .find(|field| field.path == self.field)
102 .is_some_and(|field| field.value == self.value)
103 }
104}
105
106/// One run of controls under a title, and the state that makes them relevant.
107#[derive(Debug)]
108pub struct Group {
109 pub title: &'static str,
110 /// Registry paths, in reading order.
111 ///
112 /// A member ending in `.*` is a nested body's prefix and stands for every field that
113 /// body registers, in registry order — the whole of `organ_a`, without naming its
114 /// nineteen fields.
115 pub members: &'static [&'static str],
116 /// Groups within this one. Nesting has no depth limit; the layouts here go three
117 /// deep at most, and a caller should recurse rather than assume.
118 pub groups: &'static [Group],
119 /// What makes this group relevant, or `None` for a group that always is.
120 pub when: Option<Relevance>,
121 /// How this group is selected, where it is one of several stored alternatives such
122 /// as the two organ presets; `None` for a group that is not an alternative.
123 pub selected_by: Option<Selection>,
124}
125
126/// A condition on the body's own values: satisfied when **any** match holds.
127#[derive(Debug)]
128pub struct Relevance {
129 pub any_of: &'static [Match],
130}
131
132/// One field holding one of a set of values.
133#[derive(Debug)]
134pub struct Match {
135 /// A registry path of the same body.
136 pub field: &'static str,
137 /// The values that satisfy it, spelled as [`Field::value`] spells them — which is
138 /// also what `set_field` takes. A test checks each against the field's own legal
139 /// values, so a renamed variant fails rather than silently never matching.
140 pub is: &'static [&'static str],
141}
142
143/// A body's fields by path, so resolving a layout is one pass rather than a scan per
144/// member.
145type Index<'a> = HashMap<&'a str, &'a Field>;
146
147impl Match {
148 /// Whether the field holds one of the values.
149 ///
150 /// A path the body does not register holds nothing, so an unknown field never
151 /// satisfies a match. Scans `fields`; [`Panel::resolve`] answers the same question
152 /// off an index when a whole layout is being drawn.
153 pub fn holds(&self, fields: &[Field]) -> bool {
154 fields
155 .iter()
156 .find(|field| field.path == self.field)
157 .is_some_and(|field| self.matched(field))
158 }
159
160 fn holds_in(&self, index: &Index) -> bool {
161 index
162 .get(self.field)
163 .is_some_and(|field| self.matched(field))
164 }
165
166 fn matched(&self, field: &Field) -> bool {
167 self.is.iter().any(|value| *value == field.value)
168 }
169}
170
171impl Relevance {
172 /// Whether any match holds. An empty condition is satisfied.
173 pub fn holds(&self, fields: &[Field]) -> bool {
174 self.any_of.is_empty() || self.any_of.iter().any(|m| m.holds(fields))
175 }
176
177 fn holds_in(&self, index: &Index) -> bool {
178 self.any_of.is_empty() || self.any_of.iter().any(|m| m.holds_in(index))
179 }
180}
181
182impl Group {
183 /// Whether the instrument is using this group's controls, for the state `fields`
184 /// holds.
185 ///
186 /// ⚠️ This answers for the group alone. A nested group is relevant only if its parent
187 /// is too, and nothing here walks up to check — a caller recursing top-down has the
188 /// answer already, and one starting in the middle does not have a group to start
189 /// from.
190 pub fn is_relevant(&self, fields: &[Field]) -> bool {
191 self.when.as_ref().is_none_or(|when| when.holds(fields))
192 }
193
194 /// This group's own members, in reading order, with any `prefix.*` expanded against
195 /// the registry. Members of nested groups are not included.
196 ///
197 /// A `prefix.*` names that body's **controls**: a morph slot whose parameter the same
198 /// body declares is not one, because it is drawn on that parameter. A slot whose
199 /// parameter is missing has nothing to ride on and is named like any other field.
200 ///
201 /// A member the body does not register is skipped rather than reported: the tests
202 /// hold layouts to naming only real fields, so a caller need not carry the case.
203 pub fn members_of<'a>(&self, specs: &'a [FieldSpec]) -> Vec<&'a str> {
204 members_in(self.members, specs, |member| {
205 specs.iter().find(|spec| spec.name == member)
206 })
207 .into_iter()
208 .map(|spec| spec.name.as_str())
209 .collect()
210 }
211
212 /// Every group under this one, this one included, depth first.
213 fn walk(&self) -> Vec<&Group> {
214 let mut out = vec![self];
215 for group in self.groups {
216 out.extend(group.walk());
217 }
218 out
219 }
220}
221
222/// What a layout reads off a registered field, whether it is holding the body's specs or
223/// one body's values: where the field sits, and which parameter it rides on if it is a
224/// morph slot.
225trait Placed {
226 fn path(&self) -> &str;
227 fn morph_parent(&self) -> Option<String>;
228}
229
230impl Placed for FieldSpec {
231 fn path(&self) -> &str {
232 &self.name
233 }
234
235 fn morph_parent(&self) -> Option<String> {
236 FieldSpec::morph_parent(self)
237 }
238}
239
240impl Placed for Field {
241 fn path(&self) -> &str {
242 &self.path
243 }
244
245 fn morph_parent(&self) -> Option<String> {
246 self.spec.morph_parent()
247 }
248}
249
250/// The items `members` names, in reading order, with any `prefix.*` expanded — a body's
251/// controls, morph slots left to the parameters they are drawn on.
252///
253/// `find` resolves a plain member: a scan where a caller holds only the list, an index
254/// lookup where a whole layout is being resolved against one body.
255fn members_in<'a, T: Placed>(
256 members: &[&str],
257 items: &'a [T],
258 find: impl Fn(&str) -> Option<&'a T>,
259) -> Vec<&'a T> {
260 let mut out = Vec::new();
261 for member in members {
262 match member.strip_suffix(".*") {
263 Some(prefix) => out.extend(
264 items
265 .iter()
266 .filter(|item| under(item.path(), prefix))
267 .filter(|item| item.morph_parent().is_none()),
268 ),
269 None => out.extend(find(member)),
270 }
271 }
272 out
273}
274
275/// The items `claimed` does not answer for, in registry order. A morph slot whose
276/// parameter is claimed is not among them: it is drawn on that parameter's control.
277fn unclaimed<T: Placed>(items: &[T], claimed: impl Fn(&str) -> bool) -> Vec<&T> {
278 items
279 .iter()
280 .filter(|item| !claimed(item.path()))
281 .filter(|item| !item.morph_parent().is_some_and(|parent| claimed(&parent)))
282 .collect()
283}
284
285/// Whether `path` is a field of the body at `prefix` — one dotted segment deeper, not
286/// merely sharing the leading text.
287fn under(path: &str, prefix: &str) -> bool {
288 path.strip_prefix(prefix)
289 .and_then(|rest| rest.strip_prefix('.'))
290 .is_some_and(|leaf| !leaf.contains('.'))
291}
292
293impl Panel {
294 /// Every group in the layout, sections and their nested clusters alike, depth first.
295 ///
296 /// ⚠️ Not [`groups`](Self::groups), which is the top level alone.
297 pub fn walk(&self) -> Vec<&Group> {
298 self.groups.iter().flat_map(Group::walk).collect()
299 }
300
301 /// Every path the layout names, in layout order, globs expanded.
302 pub fn named<'a>(&self, specs: &'a [FieldSpec]) -> Vec<&'a str> {
303 self.walk()
304 .into_iter()
305 .flat_map(|group| group.members_of(specs))
306 .collect()
307 }
308
309 /// The registered fields no group names, in registry order.
310 ///
311 /// A morph slot whose parameter is named is not among them: it is drawn on that
312 /// parameter's control, so a caller that has rendered the parameter has rendered it.
313 pub fn leftovers<'a>(&self, specs: &'a [FieldSpec]) -> Vec<&'a str> {
314 let named = self.named(specs);
315 unclaimed(specs, |path| named.contains(&path))
316 .into_iter()
317 .map(|spec| spec.name.as_str())
318 .collect()
319 }
320}
321
322/// One group with the fields it names, resolved against a body — what a caller draws.
323pub struct Section<'a> {
324 pub group: &'a Group,
325 /// Whether the instrument is using these controls: this group's own condition **and**
326 /// every ancestor's. [`Group::is_relevant`] on `group` answers for this level alone,
327 /// where a caller wants to tell "the section is off" from "this cluster is not the
328 /// selected one".
329 pub relevant: bool,
330 /// This group's own fields, in reading order, `prefix.*` expanded.
331 pub fields: Vec<&'a Field>,
332 /// The nested groups, resolved the same way.
333 pub groups: Vec<Section<'a>>,
334}
335
336/// A whole layout resolved against one body: the render path, in one call.
337pub struct Resolved<'a> {
338 pub sections: Vec<Section<'a>>,
339 /// The fields no group named, in registry order — empty for an exhaustive layout.
340 /// A morph slot whose parameter was named is not among them; it is drawn on that
341 /// parameter's control.
342 pub leftovers: Vec<&'a Field>,
343}
344
345impl Panel {
346 /// The layout against one body's field values: every group with its own fields and
347 /// its effective relevance, plus whatever no group named.
348 ///
349 /// One pass builds an index of the body's paths, so drawing a layout costs about one
350 /// walk of the field list however many members the groups name — which matters at the
351 /// Stage bodies' scale.
352 pub fn resolve<'a>(&'a self, fields: &'a [Field]) -> Resolved<'a> {
353 let index: Index<'a> = fields.iter().map(|f| (f.path.as_str(), f)).collect();
354 let mut claimed: HashSet<&'a str> = HashSet::new();
355 let sections = self
356 .groups
357 .iter()
358 .map(|group| resolve_group(group, fields, &index, true, &mut claimed))
359 .collect();
360 let leftovers = unclaimed(fields, |path| claimed.contains(path));
361 Resolved {
362 sections,
363 leftovers,
364 }
365 }
366}
367
368fn resolve_group<'a>(
369 group: &'a Group,
370 fields: &'a [Field],
371 index: &Index<'a>,
372 parent_relevant: bool,
373 claimed: &mut HashSet<&'a str>,
374) -> Section<'a> {
375 let relevant = parent_relevant && group.when.as_ref().is_none_or(|when| when.holds_in(index));
376
377 let own = members_in(group.members, fields, |member| index.get(member).copied());
378 claimed.extend(own.iter().map(|field| field.path.as_str()));
379
380 let groups = group
381 .groups
382 .iter()
383 .map(|nested| resolve_group(nested, fields, index, relevant, claimed))
384 .collect();
385
386 Section {
387 group,
388 relevant,
389 fields: own,
390 groups,
391 }
392}
393
394/// The layout for a decoded file's body, or `None` where none has been authored.
395///
396/// A live buffer is its model's program body under another tag, so the two share a
397/// layout.
398pub fn of(entity: &Entity) -> Option<&'static Panel> {
399 match entity {
400 Entity::Program(Program::Electro5(_)) | Entity::Live(Live::Electro5(_)) => {
401 Some(&ne5::program::PANEL)
402 }
403 Entity::Program(Program::Stage4(_)) | Entity::Live(Live::Stage4(_)) => {
404 Some(&ns4::program::PANEL)
405 }
406 _ => None,
407 }
408}
409
410/// A layout and the registry it describes.
411///
412/// Every layout the crate ships is listed in [`AUTHORED`], which is what the consistency
413/// tests walk — so a layout is checked by existing, not by anyone remembering to check
414/// it.
415pub struct Authored {
416 pub name: &'static str,
417 pub panel: &'static Panel,
418 pub specs: fn() -> Vec<FieldSpec>,
419}
420
421pub const AUTHORED: &[Authored] = &[
422 Authored {
423 name: "ne5::Program",
424 panel: &ne5::program::PANEL,
425 specs: ne5::Program::field_specs,
426 },
427 Authored {
428 name: "ns4::Program",
429 panel: &ns4::program::PANEL,
430 specs: ns4::Program::field_specs,
431 },
432];
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use std::collections::HashSet;
438
439 /// A layout may only name fields the body registers — including through a
440 /// `prefix.*`, which must reach something.
441 #[test]
442 fn every_named_path_is_a_real_field() {
443 for authored in AUTHORED {
444 let specs = (authored.specs)();
445 let known: HashSet<&str> = specs.iter().map(|spec| spec.name.as_str()).collect();
446 for group in authored.panel.walk() {
447 for member in group.members {
448 match member.strip_suffix(".*") {
449 Some(prefix) => assert!(
450 specs.iter().any(|spec| under(&spec.name, prefix)),
451 "{}: {} names no field under {prefix}",
452 authored.name,
453 group.title,
454 ),
455 None => assert!(
456 known.contains(member),
457 "{}: {} names {member}, which is not a field",
458 authored.name,
459 group.title,
460 ),
461 }
462 }
463 }
464 }
465 }
466
467 /// A morph slot is drawn on the parameter it moves, so a layout never names one
468 /// itself — and a `prefix.*` leaves them out for the same reason.
469 #[test]
470 fn no_group_names_a_morph_slot_whose_parameter_is_declared() {
471 for authored in AUTHORED {
472 let specs = (authored.specs)();
473 for path in authored.panel.named(&specs) {
474 let spec = specs.iter().find(|spec| spec.name == path).expect(path);
475 assert!(
476 spec.morph_parent().is_none(),
477 "{}: {path} is a morph slot of {:?}",
478 authored.name,
479 spec.morph_parent(),
480 );
481 }
482 }
483 }
484
485 /// One field, one group. Two groups naming the same field would draw it twice and
486 /// disagree about when it is relevant.
487 #[test]
488 fn no_field_is_named_twice() {
489 for authored in AUTHORED {
490 let specs = (authored.specs)();
491 let mut seen = HashSet::new();
492 for path in authored.panel.named(&specs) {
493 assert!(
494 seen.insert(path),
495 "{}: {path} is in two groups",
496 authored.name
497 );
498 }
499 }
500 }
501
502 /// A condition is checked against the field's own legal values, so a renamed variant
503 /// fails here rather than becoming a condition that never holds.
504 #[test]
505 fn every_condition_names_a_field_and_values_it_accepts() {
506 for authored in AUTHORED {
507 let specs = (authored.specs)();
508 for group in authored.panel.walk() {
509 let Some(when) = &group.when else { continue };
510 for m in when.any_of {
511 let spec = specs
512 .iter()
513 .find(|spec| spec.name == m.field)
514 .unwrap_or_else(|| {
515 panic!(
516 "{}: {} tests {}, which is not a field",
517 authored.name, group.title, m.field
518 )
519 });
520 let legal = (spec.legal)();
521 // A field too wide to enumerate lists nothing; its values are its
522 // stored bits and there is nothing to check them against.
523 if legal.is_empty() {
524 continue;
525 }
526 for value in m.is {
527 assert!(
528 legal.iter().any(|l| l == value),
529 "{}: {} tests {} for {value}, which it does not accept",
530 authored.name,
531 group.title,
532 m.field,
533 );
534 }
535 }
536 }
537 }
538 }
539
540 /// A selection is written back through `set_field`, so it names a registered field
541 /// and a value that field accepts — and the selector is not among the group it
542 /// selects, or a caller drawing only the selected group would lose the switch.
543 #[test]
544 fn every_selection_names_a_field_and_a_value_it_accepts() {
545 for authored in AUTHORED {
546 let specs = (authored.specs)();
547 for group in authored.panel.walk() {
548 let Some(selection) = &group.selected_by else {
549 continue;
550 };
551 let spec = specs
552 .iter()
553 .find(|spec| spec.name == selection.field)
554 .unwrap_or_else(|| {
555 panic!(
556 "{}: {} is selected by {}, which is not a field",
557 authored.name, group.title, selection.field
558 )
559 });
560 assert!(
561 (spec.legal)().iter().any(|value| value == selection.value),
562 "{}: {} does not accept {}",
563 authored.name,
564 selection.field,
565 selection.value,
566 );
567 assert!(
568 !group.members_of(&specs).contains(&selection.field),
569 "{}: {} contains its own selector {}",
570 authored.name,
571 group.title,
572 selection.field,
573 );
574 }
575 }
576 }
577
578 /// A layout that claims to account for every field has to keep doing so as the body
579 /// gains fields — which is the point of saying it in the first place.
580 #[test]
581 fn an_exhaustive_layout_leaves_nothing_out() {
582 for authored in AUTHORED {
583 if !authored.panel.exhaustive {
584 continue;
585 }
586 let specs = (authored.specs)();
587 assert_eq!(
588 authored.panel.leftovers(&specs),
589 Vec::<&str>::new(),
590 "{} claims to be exhaustive",
591 authored.name,
592 );
593 }
594 }
595
596 /// A prefix member reaches that body's own fields and no deeper.
597 #[test]
598 fn a_prefix_names_one_bodys_fields() {
599 assert!(under("organ_a.drawbar_1", "organ_a"));
600 assert!(!under("organ_ab.drawbar_1", "organ_a"));
601 assert!(!under("organ_a.inner.leaf", "organ_a"));
602 assert!(!under("drawbar_1", "organ_a"));
603 }
604}