Skip to main content

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}