1use 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
10pub const COMPONENT_EDITOR_VIEW_ID: &str = "view:component-editor";
12pub const COMPONENT_EDITOR_MANY_PARAM_FIXTURE: &str = "many-param";
14pub const COMPONENT_EDITOR_NO_PARAM_FIXTURE: &str = "no-param";
16pub const COMPONENT_EDITOR_INVALID_VALUE_FIXTURE: &str = "invalid-value";
18pub const COMPONENT_EDITOR_TRACE_ONLY_FIXTURE: &str = "trace-only";
20
21pub 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
31pub 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
61pub 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
72pub 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}