Skip to main content

sim_lib_view_daw/
component.rs

1//! Generic component editor scenes.
2
3use sim_kernel::{Expr, NumberLiteral, Symbol};
4use sim_lib_scene::node;
5use sim_value::{
6    access::{field, field_str, field_sym},
7    build::{float, int, list, map, sym, text, vector},
8};
9
10/// Stable lens id for the generic component editor.
11pub const COMPONENT_EDITOR_VIEW_ID: &str = "view:component-editor";
12/// Fixture name for a component descriptor carrying many parameters.
13pub const COMPONENT_EDITOR_MANY_PARAM_FIXTURE: &str = "many-param";
14/// Fixture name for a component descriptor with no parameters.
15pub const COMPONENT_EDITOR_NO_PARAM_FIXTURE: &str = "no-param";
16/// Fixture name for a component descriptor holding an invalid value.
17pub const COMPONENT_EDITOR_INVALID_VALUE_FIXTURE: &str = "invalid-value";
18/// Fixture name for a component descriptor that only carries a trace.
19pub const COMPONENT_EDITOR_TRACE_ONLY_FIXTURE: &str = "trace-only";
20
21/// Returns the names of the built-in component editor fixtures.
22pub fn component_editor_fixture_names() -> [&'static str; 4] {
23    [
24        COMPONENT_EDITOR_MANY_PARAM_FIXTURE,
25        COMPONENT_EDITOR_NO_PARAM_FIXTURE,
26        COMPONENT_EDITOR_INVALID_VALUE_FIXTURE,
27        COMPONENT_EDITOR_TRACE_ONLY_FIXTURE,
28    ]
29}
30
31/// Builds the component editor Scene from a component descriptor.
32pub fn component_editor_view(descriptor: &Expr) -> Expr {
33    let mut children = vec![
34        summary_view(descriptor),
35        ports_view(descriptor),
36        parameter_groups_view(descriptor),
37    ];
38    if let Some(errors) = validation_errors_view(descriptor) {
39        children.push(errors);
40    }
41    if let Some(trace) = trace_view(descriptor) {
42        children.push(trace);
43    }
44    if let Some(route) = specialized_route_view(descriptor) {
45        children.push(route);
46    }
47    node(
48        "stack",
49        vec![
50            ("lens", sym(COMPONENT_EDITOR_VIEW_ID)),
51            ("role", sym("component-editor")),
52            ("component", component_expr(descriptor)),
53            ("name", text(component_name(descriptor))),
54            ("category", category_expr(descriptor)),
55            ("trace-available", Expr::Bool(trace_available(descriptor))),
56            ("children", list(children)),
57        ],
58    )
59}
60
61/// Returns the descriptor for the named editor fixture, if it exists.
62pub fn component_editor_fixture(name: &str) -> Option<Expr> {
63    match name {
64        COMPONENT_EDITOR_MANY_PARAM_FIXTURE => Some(many_param_descriptor()),
65        COMPONENT_EDITOR_NO_PARAM_FIXTURE => Some(no_param_descriptor()),
66        COMPONENT_EDITOR_INVALID_VALUE_FIXTURE => Some(invalid_value_descriptor()),
67        COMPONENT_EDITOR_TRACE_ONLY_FIXTURE => Some(trace_only_descriptor()),
68        _ => None,
69    }
70}
71
72/// Renders the named fixture into a component editor Scene snapshot.
73pub fn component_editor_snapshot(name: &str) -> Option<Expr> {
74    component_editor_fixture(name).map(|descriptor| component_editor_view(&descriptor))
75}
76
77fn summary_view(descriptor: &Expr) -> Expr {
78    node(
79        "box",
80        vec![
81            ("role", sym("component-summary")),
82            ("component", component_expr(descriptor)),
83            ("name", text(component_name(descriptor))),
84            ("category", category_expr(descriptor)),
85            ("trace-available", Expr::Bool(trace_available(descriptor))),
86            (
87                "children",
88                list(vec![node(
89                    "badge",
90                    vec![
91                        (
92                            "status",
93                            sym(if trace_available(descriptor) {
94                                "trace"
95                            } else {
96                                "plain"
97                            }),
98                        ),
99                        ("label", text(component_name(descriptor))),
100                    ],
101                )]),
102            ),
103        ],
104    )
105}
106
107fn ports_view(descriptor: &Expr) -> Expr {
108    let rows = sequence(field(descriptor, "ports"))
109        .iter()
110        .map(|port| {
111            map(vec![
112                (
113                    "id",
114                    field(port, "id").cloned().unwrap_or_else(|| sym("port")),
115                ),
116                (
117                    "media",
118                    field(port, "media")
119                        .cloned()
120                        .unwrap_or_else(|| sym("unknown")),
121                ),
122                (
123                    "direction",
124                    field(port, "direction")
125                        .cloned()
126                        .unwrap_or_else(|| sym("unknown")),
127                ),
128                (
129                    "channels",
130                    field(port, "channels").cloned().unwrap_or_else(|| int(1)),
131                ),
132                (
133                    "required",
134                    field(port, "required").cloned().unwrap_or(Expr::Bool(true)),
135                ),
136            ])
137        })
138        .collect();
139    node(
140        "table",
141        vec![("role", sym("component-ports")), ("rows", vector(rows))],
142    )
143}
144
145fn parameter_groups_view(descriptor: &Expr) -> Expr {
146    let groups = sequence(field(descriptor, "parameter-groups"));
147    if groups.is_empty() {
148        return node(
149            "box",
150            vec![
151                ("role", sym("parameter-empty")),
152                (
153                    "children",
154                    list(vec![node("text", vec![("text", text("No parameters"))])]),
155                ),
156            ],
157        );
158    }
159    let children = groups
160        .iter()
161        .map(|group| parameter_group_view(group, descriptor))
162        .collect();
163    node(
164        "stack",
165        vec![
166            ("role", sym("parameter-groups")),
167            ("dir", sym("column")),
168            ("children", list(children)),
169        ],
170    )
171}
172
173fn parameter_group_view(group: &Expr, descriptor: &Expr) -> Expr {
174    let params = sequence(field(group, "params"))
175        .iter()
176        .map(|param| parameter_editor_view(param, descriptor))
177        .collect();
178    node(
179        "box",
180        vec![
181            ("role", sym("parameter-group")),
182            (
183                "group",
184                field(group, "name").cloned().unwrap_or_else(|| sym("main")),
185            ),
186            (
187                "label",
188                text(field_str(group, "label").unwrap_or("Parameters")),
189            ),
190            ("children", list(params)),
191        ],
192    )
193}
194
195fn parameter_editor_view(param: &Expr, descriptor: &Expr) -> Expr {
196    let editor = editor_symbol(param);
197    let id = field(param, "id")
198        .cloned()
199        .unwrap_or_else(|| Expr::Symbol(Symbol::new("param")));
200    node(
201        "field",
202        vec![
203            ("role", sym("component-param-editor")),
204            ("editor", Expr::Symbol(editor.clone())),
205            ("param", id.clone()),
206            (
207                "label",
208                text(field_str(param, "label").unwrap_or("Parameter")),
209            ),
210            (
211                "unit",
212                field(param, "unit")
213                    .cloned()
214                    .unwrap_or_else(|| sym("unitless")),
215            ),
216            ("value", value_for_param(param, descriptor, &id)),
217            (
218                "normalized-value",
219                field(param, "normalized-value")
220                    .or_else(|| field(param, "normalized-default"))
221                    .cloned()
222                    .unwrap_or(Expr::Nil),
223            ),
224            ("range", field(param, "range").cloned().unwrap_or(Expr::Nil)),
225            (
226                "enum-values",
227                field(param, "enum-values")
228                    .cloned()
229                    .unwrap_or_else(|| vector(Vec::new())),
230            ),
231            ("read-only", Expr::Bool(editor_is_readonly(&editor, param))),
232            ("errors", vector(errors_for_param(descriptor, &id))),
233        ],
234    )
235}
236
237fn validation_errors_view(descriptor: &Expr) -> Option<Expr> {
238    let errors = sequence(field(descriptor, "validation-errors"));
239    if errors.is_empty() {
240        return None;
241    }
242    Some(node(
243        "box",
244        vec![
245            ("role", sym("validation-errors")),
246            (
247                "children",
248                list(
249                    errors
250                        .iter()
251                        .map(|error| {
252                            node(
253                                "field",
254                                vec![
255                                    ("role", sym("param-error")),
256                                    (
257                                        "param",
258                                        field(error, "param")
259                                            .cloned()
260                                            .unwrap_or_else(|| sym("component")),
261                                    ),
262                                    (
263                                        "message",
264                                        text(
265                                            field_str(error, "message").unwrap_or("invalid value"),
266                                        ),
267                                    ),
268                                ],
269                            )
270                        })
271                        .collect(),
272                ),
273            ),
274        ],
275    ))
276}
277
278fn trace_view(descriptor: &Expr) -> Option<Expr> {
279    let fields = sequence(field(descriptor, "trace-fields"));
280    if fields.is_empty() && !trace_available(descriptor) {
281        return None;
282    }
283    let children = if fields.is_empty() {
284        vec![node(
285            "badge",
286            vec![("status", sym("trace")), ("label", text("Trace available"))],
287        )]
288    } else {
289        fields
290            .iter()
291            .map(|field_expr| trace_field_view(field_expr))
292            .collect()
293    };
294    Some(node(
295        "box",
296        vec![
297            ("role", sym("trace-fields")),
298            ("trace-available", Expr::Bool(trace_available(descriptor))),
299            ("children", list(children)),
300        ],
301    ))
302}
303
304fn trace_field_view(field_expr: &Expr) -> Expr {
305    node(
306        "field",
307        vec![
308            ("role", sym("component-trace-field")),
309            (
310                "editor",
311                field(field_expr, "editor").cloned().unwrap_or_else(|| {
312                    Expr::Symbol(Symbol::qualified(
313                        "component-editor/editor",
314                        "trace-readonly",
315                    ))
316                }),
317            ),
318            (
319                "param",
320                field(field_expr, "id")
321                    .cloned()
322                    .unwrap_or_else(|| sym("trace")),
323            ),
324            (
325                "label",
326                text(field_str(field_expr, "label").unwrap_or("Trace")),
327            ),
328            (
329                "value",
330                field(field_expr, "value").cloned().unwrap_or(Expr::Nil),
331            ),
332            ("read-only", Expr::Bool(true)),
333        ],
334    )
335}
336
337fn specialized_route_view(descriptor: &Expr) -> Option<Expr> {
338    let target = field_sym(descriptor, "specialized-view")?;
339    Some(node(
340        "button",
341        vec![
342            ("role", sym("specialized-route")),
343            ("action", sym("route-specialized-view")),
344            ("target", Expr::Symbol(target)),
345            ("label", text("Open specialized view")),
346        ],
347    ))
348}
349
350fn component_expr(descriptor: &Expr) -> Expr {
351    field(descriptor, "component")
352        .cloned()
353        .unwrap_or_else(|| Expr::Symbol(Symbol::new("component")))
354}
355
356fn component_name(descriptor: &Expr) -> String {
357    field_str(descriptor, "name")
358        .map(str::to_owned)
359        .unwrap_or_else(|| symbol_text(&component_expr(descriptor)))
360}
361
362fn category_expr(descriptor: &Expr) -> Expr {
363    field(descriptor, "category")
364        .cloned()
365        .unwrap_or_else(|| Expr::Symbol(Symbol::new("generic")))
366}
367
368fn trace_available(descriptor: &Expr) -> bool {
369    matches!(field(descriptor, "trace-available"), Some(Expr::Bool(true)))
370}
371
372fn value_for_param(param: &Expr, descriptor: &Expr, id: &Expr) -> Expr {
373    field(descriptor, "current-values")
374        .and_then(|values| value_by_key(values, id))
375        .cloned()
376        .or_else(|| field(param, "value").cloned())
377        .or_else(|| field(param, "raw-default").cloned())
378        .or_else(|| field(param, "normalized-default").cloned())
379        .unwrap_or(Expr::Nil)
380}
381
382fn value_by_key<'a>(map_expr: &'a Expr, needle: &Expr) -> Option<&'a Expr> {
383    let Expr::Map(entries) = map_expr else {
384        return None;
385    };
386    entries
387        .iter()
388        .find_map(|(key, value)| (key == needle).then_some(value))
389}
390
391fn errors_for_param(descriptor: &Expr, id: &Expr) -> Vec<Expr> {
392    sequence(field(descriptor, "validation-errors"))
393        .into_iter()
394        .filter(|error| field(error, "param") == Some(id))
395        .cloned()
396        .collect()
397}
398
399fn editor_symbol(param: &Expr) -> Symbol {
400    if let Some(symbol) = field_sym(param, "editor") {
401        return symbol;
402    }
403    if !sequence(field(param, "enum-values")).is_empty() {
404        return Symbol::qualified("component-editor/editor", "enum");
405    }
406    if matches!(field(param, "read-only"), Some(Expr::Bool(true))) {
407        return Symbol::qualified("component-editor/editor", "fixed-point");
408    }
409    Symbol::qualified("component-editor/editor", "normalized")
410}
411
412fn editor_is_readonly(editor: &Symbol, param: &Expr) -> bool {
413    editor.name.as_ref() == "fixed-point"
414        || editor.name.as_ref() == "trace-readonly"
415        || matches!(field(param, "read-only"), Some(Expr::Bool(true)))
416}
417
418fn sequence(value: Option<&Expr>) -> Vec<&Expr> {
419    match value {
420        Some(Expr::List(items)) | Some(Expr::Vector(items)) => items.iter().collect(),
421        _ => Vec::new(),
422    }
423}
424
425fn symbol_text(expr: &Expr) -> String {
426    match expr {
427        Expr::Symbol(symbol) => symbol.to_string(),
428        Expr::String(text) => text.clone(),
429        _ => "component".to_owned(),
430    }
431}
432
433fn many_param_descriptor() -> Expr {
434    descriptor(
435        ("component-fixture/many-param", "Many Param Component"),
436        vec![
437            port("input", "audio-rate", "input"),
438            port("output", "audio-rate", "output"),
439        ],
440        vec![group(
441            "main",
442            vec![
443                param(
444                    "gain-steps",
445                    "Gain Steps",
446                    "integer-range",
447                    number("i64", "64"),
448                    Some(range(0.0, 127.0, 64.0)),
449                    Vec::new(),
450                    false,
451                ),
452                param(
453                    "waveform",
454                    "Waveform",
455                    "enum",
456                    Expr::Symbol(Symbol::qualified("component-fixture/waveform", "sine")),
457                    None,
458                    vec!["sine", "square"],
459                    false,
460                ),
461                param(
462                    "bypass",
463                    "Bypass",
464                    "toggle",
465                    Expr::Bool(false),
466                    None,
467                    Vec::new(),
468                    false,
469                ),
470                param(
471                    "cutoff-readout",
472                    "Cutoff Readout",
473                    "fixed-point",
474                    text("440.00 Hz"),
475                    None,
476                    Vec::new(),
477                    true,
478                ),
479                param(
480                    "mix",
481                    "Mix",
482                    "normalized",
483                    float(0.75),
484                    Some(range(0.0, 1.0, 0.75)),
485                    Vec::new(),
486                    false,
487                ),
488            ],
489        )],
490        vector(Vec::new()),
491        (
492            true,
493            vector(vec![trace_field(
494                "last-output",
495                "Last Output",
496                text("0.25"),
497            )]),
498        ),
499        Expr::Symbol(Symbol::qualified("view/component", "many-param")),
500    )
501}
502
503fn no_param_descriptor() -> Expr {
504    descriptor(
505        ("component-fixture/no-param", "No Param Component"),
506        vec![port("output", "metadata", "output")],
507        Vec::new(),
508        vector(Vec::new()),
509        (false, vector(Vec::new())),
510        Expr::Nil,
511    )
512}
513
514fn invalid_value_descriptor() -> Expr {
515    let id = Expr::Symbol(Symbol::qualified("component-fixture/param", "gain"));
516    descriptor(
517        ("component-fixture/invalid-value", "Invalid Value Component"),
518        vec![port("output", "control-rate", "output")],
519        vec![group(
520            "main",
521            vec![param(
522                "gain",
523                "Gain",
524                "normalized",
525                float(2.0),
526                Some(range(0.0, 1.0, 0.5)),
527                Vec::new(),
528                false,
529            )],
530        )],
531        vector(vec![map(vec![
532            ("param", id),
533            ("message", text("value must be between 0 and 1")),
534        ])]),
535        (false, vector(Vec::new())),
536        Expr::Nil,
537    )
538}
539
540fn trace_only_descriptor() -> Expr {
541    descriptor(
542        ("component-fixture/trace-only", "Trace Only Component"),
543        vec![port("trace", "trace", "output")],
544        Vec::new(),
545        vector(Vec::new()),
546        (
547            true,
548            vector(vec![
549                trace_field("frame", "Frame", number("i64", "128")),
550                trace_field("state", "State", text("held")),
551            ]),
552        ),
553        Expr::Nil,
554    )
555}
556
557fn descriptor(
558    identity: (&str, &str),
559    ports: Vec<Expr>,
560    groups: Vec<Expr>,
561    validation_errors: Expr,
562    trace: (bool, Expr),
563    specialized_view: Expr,
564) -> Expr {
565    map(vec![
566        (
567            "tag",
568            Expr::Symbol(Symbol::qualified("component-editor", "descriptor")),
569        ),
570        ("component", Expr::Symbol(Symbol::new(identity.0))),
571        ("name", text(identity.1)),
572        (
573            "category",
574            Expr::Symbol(Symbol::qualified("component-editor/category", "fixture")),
575        ),
576        ("ports", vector(ports)),
577        ("parameter-groups", vector(groups)),
578        ("current-values", current_values_from_groups(&[])),
579        ("validation-errors", validation_errors),
580        ("trace-available", Expr::Bool(trace.0)),
581        ("trace-fields", trace.1),
582        ("specialized-view", specialized_view),
583    ])
584}
585
586fn group(name: &str, params: Vec<Expr>) -> Expr {
587    map(vec![
588        (
589            "name",
590            Expr::Symbol(Symbol::qualified("component-fixture/group", name)),
591        ),
592        ("label", text("Parameters")),
593        ("params", vector(params)),
594    ])
595}
596
597fn port(id: &str, media: &str, direction: &str) -> Expr {
598    map(vec![
599        (
600            "id",
601            Expr::Symbol(Symbol::qualified("component-fixture/port", id)),
602        ),
603        (
604            "media",
605            Expr::Symbol(Symbol::qualified("audio-synth/port-media", media)),
606        ),
607        (
608            "direction",
609            Expr::Symbol(Symbol::qualified("audio-synth/port-direction", direction)),
610        ),
611        ("channels", int(1)),
612        ("required", Expr::Bool(true)),
613    ])
614}
615
616fn param(
617    id: &str,
618    label: &str,
619    editor: &str,
620    value: Expr,
621    range: Option<Expr>,
622    enum_values: Vec<&str>,
623    read_only: bool,
624) -> Expr {
625    let id_expr = Expr::Symbol(Symbol::qualified("component-fixture/param", id));
626    map(vec![
627        ("id", id_expr),
628        ("label", text(label)),
629        (
630            "unit",
631            Expr::Symbol(Symbol::qualified("component-fixture/unit", "unitless")),
632        ),
633        (
634            "editor",
635            Expr::Symbol(Symbol::qualified("component-editor/editor", editor)),
636        ),
637        ("value", value),
638        ("normalized-value", float(0.5)),
639        ("range", range.unwrap_or(Expr::Nil)),
640        (
641            "enum-values",
642            vector(
643                enum_values
644                    .into_iter()
645                    .map(|name| Expr::Symbol(Symbol::qualified("component-fixture/enum", name)))
646                    .collect(),
647            ),
648        ),
649        ("read-only", Expr::Bool(read_only)),
650    ])
651}
652
653fn range(min: f64, max: f64, default: f64) -> Expr {
654    map(vec![
655        ("min", float(min)),
656        ("max", float(max)),
657        ("default", float(default)),
658    ])
659}
660
661fn trace_field(id: &str, label: &str, value: Expr) -> Expr {
662    map(vec![
663        (
664            "id",
665            Expr::Symbol(Symbol::qualified("component-fixture/trace", id)),
666        ),
667        ("label", text(label)),
668        (
669            "editor",
670            Expr::Symbol(Symbol::qualified(
671                "component-editor/editor",
672                "trace-readonly",
673            )),
674        ),
675        ("value", value),
676        ("read-only", Expr::Bool(true)),
677    ])
678}
679
680fn current_values_from_groups(_groups: &[Expr]) -> Expr {
681    Expr::Map(Vec::new())
682}
683
684fn number(domain: &str, canonical: &str) -> Expr {
685    Expr::Number(NumberLiteral {
686        domain: Symbol::new(domain),
687        canonical: canonical.to_owned(),
688    })
689}