1use serde::{Deserialize, Serialize};
13use serde_json;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
20pub enum UiComponent {
21 Group {
26 label: String,
27 children: Vec<UiComponent>,
28 },
29
30 Row { children: Vec<UiComponent> },
32
33 Column { children: Vec<UiComponent> },
35
36 Tabs { tabs: Vec<UiTab> },
38
39 Knob {
44 label: String,
45 param_id: u32,
46 min: f32,
47 max: f32,
48 #[serde(default)]
49 logarithmic: bool,
50 },
51
52 Slider {
54 label: String,
55 param_id: u32,
56 min: f32,
57 max: f32,
58 },
59
60 Toggle { label: String, param_id: u32 },
62
63 Dropdown {
65 label: String,
66 param_id: u32,
67 options: Vec<String>,
68 },
69
70 ButtonGroup {
72 label: String,
73 param_id: u32,
74 options: Vec<String>,
75 },
76
77 ColorPicker {
79 label: String,
80 r_param_id: u32,
81 g_param_id: u32,
82 b_param_id: u32,
83 },
84
85 HueSlider { label: String, param_id: u32 },
87
88 XYPad {
93 label: String,
94 x_param_id: u32,
95 y_param_id: u32,
96 },
97
98 Curves { label: String, param_id: u32 },
100
101 AudioScope { label: String, source: AudioSource },
103
104 BeatButton {
106 label: String,
107 trigger_id: u32,
108 division: NoteDivision,
109 },
110
111 ActionButton { label: String, action_id: String },
113
114 TriggerButton { label: String, param_id: u32 },
116
117 ModulationSlot { label: String, target_param_id: u32 },
119
120 ControlPreview {
122 label: String,
123 port_name: String,
124 #[serde(default)]
126 history_length: u32,
127 },
128
129 ControlTargetSelector {
134 label: String,
135 port_name: String,
137 },
138
139 Label { text: String },
144
145 Separator,
147
148 Spacer { height: f32 },
150
151 Custom {
159 widget_id: String,
160 #[serde(default)]
161 props: std::collections::HashMap<String, String>,
162 },
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct UiTab {
168 pub label: String,
169 pub children: Vec<UiComponent>,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174pub enum AudioSource {
175 Master,
177 Low,
179 Mid,
181 High,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
187pub enum NoteDivision {
188 Whole,
190 Half,
192 #[default]
194 Quarter,
195 Eighth,
197 Sixteenth,
199}
200
201impl UiComponent {
206 pub fn group(label: impl Into<String>, children: Vec<UiComponent>) -> Self {
208 Self::Group {
209 label: label.into(),
210 children,
211 }
212 }
213
214 pub fn row(children: Vec<UiComponent>) -> Self {
216 Self::Row { children }
217 }
218
219 pub fn column(children: Vec<UiComponent>) -> Self {
221 Self::Column { children }
222 }
223
224 pub fn knob(label: impl Into<String>, param_id: u32, min: f32, max: f32) -> Self {
226 Self::Knob {
227 label: label.into(),
228 param_id,
229 min,
230 max,
231 logarithmic: false,
232 }
233 }
234
235 pub fn knob_log(label: impl Into<String>, param_id: u32, min: f32, max: f32) -> Self {
237 Self::Knob {
238 label: label.into(),
239 param_id,
240 min,
241 max,
242 logarithmic: true,
243 }
244 }
245
246 pub fn slider(label: impl Into<String>, param_id: u32, min: f32, max: f32) -> Self {
248 Self::Slider {
249 label: label.into(),
250 param_id,
251 min,
252 max,
253 }
254 }
255
256 pub fn toggle(label: impl Into<String>, param_id: u32) -> Self {
258 Self::Toggle {
259 label: label.into(),
260 param_id,
261 }
262 }
263
264 pub fn dropdown(label: impl Into<String>, param_id: u32, options: Vec<String>) -> Self {
266 Self::Dropdown {
267 label: label.into(),
268 param_id,
269 options,
270 }
271 }
272
273 pub fn button_group(label: impl Into<String>, param_id: u32, options: Vec<String>) -> Self {
275 Self::ButtonGroup {
276 label: label.into(),
277 param_id,
278 options,
279 }
280 }
281
282 pub fn xy_pad(label: impl Into<String>, x_param_id: u32, y_param_id: u32) -> Self {
284 Self::XYPad {
285 label: label.into(),
286 x_param_id,
287 y_param_id,
288 }
289 }
290
291 pub fn audio_scope(label: impl Into<String>, source: AudioSource) -> Self {
293 Self::AudioScope {
294 label: label.into(),
295 source,
296 }
297 }
298
299 pub fn beat_button(label: impl Into<String>, trigger_id: u32, division: NoteDivision) -> Self {
301 Self::BeatButton {
302 label: label.into(),
303 trigger_id,
304 division,
305 }
306 }
307
308 pub fn action_button(label: impl Into<String>, action_id: impl Into<String>) -> Self {
310 Self::ActionButton {
311 label: label.into(),
312 action_id: action_id.into(),
313 }
314 }
315
316 pub fn trigger_button(label: impl Into<String>, param_id: u32) -> Self {
318 Self::TriggerButton {
319 label: label.into(),
320 param_id,
321 }
322 }
323
324 pub fn mod_slot(label: impl Into<String>, target_param_id: u32) -> Self {
326 Self::ModulationSlot {
327 label: label.into(),
328 target_param_id,
329 }
330 }
331
332 pub fn control_preview(
334 label: impl Into<String>,
335 port_name: impl Into<String>,
336 history_length: u32,
337 ) -> Self {
338 Self::ControlPreview {
339 label: label.into(),
340 port_name: port_name.into(),
341 history_length,
342 }
343 }
344
345 pub fn control_target_selector(label: impl Into<String>, port_name: impl Into<String>) -> Self {
347 Self::ControlTargetSelector {
348 label: label.into(),
349 port_name: port_name.into(),
350 }
351 }
352
353 pub fn label(text: impl Into<String>) -> Self {
355 Self::Label { text: text.into() }
356 }
357
358 pub fn separator() -> Self {
360 Self::Separator
361 }
362
363 pub fn spacer(height: f32) -> Self {
365 Self::Spacer { height }
366 }
367
368 pub fn custom(widget_id: impl Into<String>) -> Self {
370 Self::Custom {
371 widget_id: widget_id.into(),
372 props: std::collections::HashMap::new(),
373 }
374 }
375
376 pub fn custom_with_props(
378 widget_id: impl Into<String>,
379 props: std::collections::HashMap<String, String>,
380 ) -> Self {
381 Self::Custom {
382 widget_id: widget_id.into(),
383 props,
384 }
385 }
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct LayoutConfig {
397 pub version: u32,
399
400 pub name: String,
402
403 pub sections: Vec<SectionConfig>,
405
406 #[serde(default)]
408 pub mod_slots: Vec<ModSlotConfig>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct SectionConfig {
414 pub widget_id: String,
416
417 #[serde(default = "default_true")]
419 pub visible: bool,
420
421 #[serde(default)]
423 pub position: Option<LayoutPosition>,
424
425 #[serde(default)]
427 pub size: Option<LayoutSize>,
428
429 #[serde(default)]
431 pub props: std::collections::HashMap<String, String>,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct ModSlotConfig {
437 pub slot_id: String,
439
440 pub after_section: Option<String>,
442
443 pub mod_id: Option<String>,
445
446 #[serde(default)]
448 pub position: Option<LayoutPosition>,
449
450 #[serde(default)]
452 pub size: Option<LayoutSize>,
453}
454
455#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
457pub struct LayoutPosition {
458 pub x: f32,
460 pub y: f32,
462}
463
464#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
466pub struct LayoutSize {
467 pub width: f32,
469 pub height: f32,
471}
472
473fn default_true() -> bool {
474 true
475}
476
477impl Default for LayoutConfig {
478 fn default() -> Self {
479 Self {
480 version: 1,
481 name: "Default".to_string(),
482 sections: vec![
483 SectionConfig::new("header"),
484 SectionConfig::new("audio_status"),
485 SectionConfig::new("frequency_bars"),
486 SectionConfig::new("effects"),
487 SectionConfig::new("textures"),
488 SectionConfig::new("recording"),
489 SectionConfig::new("modulation"),
490 SectionConfig::new("blend_mode"),
491 SectionConfig::new("color_scheme"),
492 SectionConfig::new("simulation"),
493 SectionConfig::new("midi"),
494 SectionConfig::new("footer"),
495 ],
496 mod_slots: vec![ModSlotConfig {
497 slot_id: "mod_slot_1".to_string(),
498 after_section: Some("simulation".to_string()),
499 mod_id: None,
500 position: None,
501 size: None,
502 }],
503 }
504 }
505}
506
507impl SectionConfig {
508 pub fn new(widget_id: impl Into<String>) -> Self {
509 Self {
510 widget_id: widget_id.into(),
511 visible: true,
512 position: None,
513 size: None,
514 props: std::collections::HashMap::new(),
515 }
516 }
517
518 pub fn with_visibility(mut self, visible: bool) -> Self {
519 self.visible = visible;
520 self
521 }
522
523 pub fn with_position(mut self, x: f32, y: f32) -> Self {
524 self.position = Some(LayoutPosition { x, y });
525 self
526 }
527
528 pub fn with_size(mut self, width: f32, height: f32) -> Self {
529 self.size = Some(LayoutSize { width, height });
530 self
531 }
532}
533
534impl LayoutConfig {
535 pub fn to_ui_component(&self) -> UiComponent {
537 let mut children: Vec<UiComponent> = Vec::new();
538
539 for (i, section) in self.sections.iter().enumerate() {
540 if !section.visible {
541 continue;
542 }
543
544 if i > 0 {
546 children.push(UiComponent::spacer(10.0));
547 }
548
549 for mod_slot in &self.mod_slots {
551 if mod_slot.after_section.as_deref() == Some(§ion.widget_id) {
552 if let Some(mod_id) = &mod_slot.mod_id {
553 children.push(UiComponent::spacer(10.0));
555 children.push(UiComponent::custom_with_props(
556 "mod_ui",
557 [("mod_id".to_string(), mod_id.clone())]
558 .into_iter()
559 .collect(),
560 ));
561 }
562 }
563 }
564
565 children.push(UiComponent::custom_with_props(
567 section.widget_id.clone(),
568 section.props.clone(),
569 ));
570 }
571
572 UiComponent::column(children)
573 }
574
575 pub fn to_json(&self) -> Result<String, serde_json::Error> {
577 serde_json::to_string_pretty(self)
578 }
579
580 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
582 serde_json::from_str(json)
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 #[test]
591 fn test_ui_component_serialization() {
592 let ui = UiComponent::group(
593 "Test",
594 vec![
595 UiComponent::knob("Intensity", 0, 0.0, 1.0),
596 UiComponent::toggle("Enable", 1),
597 ],
598 );
599
600 let json = serde_json::to_string_pretty(&ui).unwrap();
601 println!("{}", json);
602
603 let parsed: UiComponent = serde_json::from_str(&json).unwrap();
604 match parsed {
605 UiComponent::Group { label, children } => {
606 assert_eq!(label, "Test");
607 assert_eq!(children.len(), 2);
608 }
609 _ => panic!("Expected Group"),
610 }
611 }
612
613 #[test]
614 fn test_layout_config_serialization() {
615 let config = LayoutConfig::default();
616 let json = config.to_json().unwrap();
617 println!("Default layout:\n{}", json);
618
619 let parsed = LayoutConfig::from_json(&json).unwrap();
620 assert_eq!(parsed.version, 1);
621 assert_eq!(parsed.name, "Default");
622 assert_eq!(parsed.sections.len(), 12);
623 assert_eq!(parsed.mod_slots.len(), 1);
624 }
625
626 #[test]
627 fn test_layout_config_to_ui_component() {
628 let mut config = LayoutConfig::default();
629 config.sections[2].visible = false; config.sections[5].visible = false; let ui = config.to_ui_component();
634 match ui {
635 UiComponent::Column { children } => {
636 assert!(children.len() < 24); }
639 _ => panic!("Expected Column"),
640 }
641 }
642
643 #[test]
644 fn test_layout_config_with_mod_slot() {
645 let mut config = LayoutConfig::default();
646 config.mod_slots[0].mod_id = Some("chromatic_aberration".to_string());
647
648 let ui = config.to_ui_component();
649 let json = serde_json::to_string_pretty(&ui).unwrap();
650 println!("Layout with MOD:\n{}", json);
651
652 assert!(json.contains("mod_ui"));
654 assert!(json.contains("chromatic_aberration"));
655 }
656}