1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::engine::ecs::component::{
5 DataComponent, DataValue, SelectableComponent, SelectionComponent, SerializeComponent,
6 TransformComponent,
7};
8use crate::engine::ecs::system::data_renderer_system::{
9 DataRendererSystem, DetailRendererSpec, ItemRendererSpec, UiDetailItem, UiItem,
10};
11use crate::engine::ecs::system::editor::world_panel::WorldPanelModel;
12use crate::engine::ecs::{ComponentId, SignalEmitter, World};
13use crate::scripting::component_registry::spawn_tree;
14use crate::scripting::object::{CeChild, MaterializedCE, Value};
15use crate::scripting::runner::MeowMeowRunner;
16
17pub const EDITOR_RUNTIME_UI_ROOT_NAME: &str = "editor_runtime_ui_root";
18pub const PANEL_LAYOUT_MOUNT_NAME: &str = "editor_panel_layout_mount";
19pub const PANEL_LAYOUT_ROOT_NAME: &str = "editor_panel_layout_root";
20pub const PANEL_LAYOUT_SELECTION_NAME: &str = "editor_panel_layout_selection";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum PanelKind {
24 World,
25 Inspector,
26 Paint,
27 Color,
28 Assets,
29 Grid,
30 Pose,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum PanelSlotKind {
35 List,
36 Detail,
37 Status,
38 Sidebar,
39 Toolbar,
40 Footer,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum PanelControlKind {
45 Selection,
46 TitleLabel,
47 PinButton,
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub struct PanelShellSpec {
52 pub panel_kind: PanelKind,
53 pub asset_path: String,
54 pub export_name: String,
55 pub args: Vec<crate::scripting::object::Value>,
56 pub root_selector: String,
57 pub slot_selectors: HashMap<PanelSlotKind, String>,
58 pub control_selectors: HashMap<PanelControlKind, String>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PanelInstance {
63 pub panel_kind: PanelKind,
64 pub editor_root: ComponentId,
65 pub root: ComponentId,
66 pub slots: HashMap<PanelSlotKind, ComponentId>,
67 pub controls: HashMap<PanelControlKind, ComponentId>,
68 pub instance_id: Option<u64>,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub enum PanelActionKind {
73 Select,
74 Toggle,
75 Delete,
76 Add,
77 Focus,
78 Pin,
79 ActivateField,
80 EditField,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct PanelActionPayload {
85 pub panel_kind: PanelKind,
86 pub action_kind: PanelActionKind,
87 pub item_key: Option<String>,
88 pub target_component: Option<ComponentId>,
89 pub instance_id: Option<u64>,
90 pub field_key: Option<String>,
91}
92
93pub struct PanelLayoutMountSpec {
94 pub anchor_pos: (f32, f32, f32),
95 pub total_height_gu: f64,
96 pub available_width_gu: f64,
97 pub text_scale: f64,
98 pub mount_name: String,
99 pub layout_name: String,
100 pub children: Vec<MaterializedCE>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct SpawnedPanelInstance {
105 pub mount_root: ComponentId,
106 pub instance: PanelInstance,
107}
108
109pub fn find_named_root(world: &World, name: &str) -> Option<ComponentId> {
110 world.all_components().find(|&component_id| {
111 world.parent_of(component_id).is_none()
112 && world
113 .component_label(component_id)
114 .is_some_and(|label| label == name)
115 })
116}
117
118pub fn get_or_create_runtime_ui_root(world: &mut World) -> ComponentId {
119 if let Some(runtime_ui_root) = find_named_root(world, EDITOR_RUNTIME_UI_ROOT_NAME) {
120 return runtime_ui_root;
121 }
122
123 let runtime_ui_root = world.add_component_boxed_named(
124 EDITOR_RUNTIME_UI_ROOT_NAME,
125 Box::new(TransformComponent::new()),
126 );
127 let runtime_ui_selectable = world.add_component_boxed_named(
128 "editor_runtime_ui_selectable",
129 Box::new(SelectableComponent::off()),
130 );
131 let _ = world.add_child(runtime_ui_root, runtime_ui_selectable);
132 let runtime_ui_serialize = world.add_component_boxed_named(
133 "editor_runtime_ui_serialize",
134 Box::new(SerializeComponent::off()),
135 );
136 let _ = world.add_child(runtime_ui_root, runtime_ui_serialize);
137
138 runtime_ui_root
139}
140
141pub fn panel_layout_root_id(world: &World, panel_query_root: ComponentId) -> Option<ComponentId> {
142 world.find_component(panel_query_root, "#editor_panel_layout_root")
143}
144
145pub fn panel_layout_selection_id(
146 world: &World,
147 panel_query_root: ComponentId,
148) -> Option<ComponentId> {
149 world.find_component(panel_query_root, "#editor_panel_layout_selection")
150}
151
152pub fn ensure_panel_layout_selection(
153 world: &mut World,
154 layout_root_id: ComponentId,
155) -> ComponentId {
156 if let Some(existing) = world.find_component(layout_root_id, "#editor_panel_layout_selection") {
157 return existing;
158 }
159
160 let selection = world.add_component_boxed_named(
161 PANEL_LAYOUT_SELECTION_NAME,
162 Box::new(SelectionComponent::new()),
163 );
164 let _ = world.add_child(layout_root_id, selection);
165 selection
166}
167
168pub fn is_descendant_or_self(world: &World, ancestor: ComponentId, node: ComponentId) -> bool {
169 let mut current = Some(node);
170 while let Some(component_id) = current {
171 if component_id == ancestor {
172 return true;
173 }
174 current = world.parent_of(component_id);
175 }
176 false
177}
178
179pub fn build_panel_shell_component_expr(
180 world: &mut World,
181 emit: &mut dyn SignalEmitter,
182 spec: &PanelShellSpec,
183) -> Result<MaterializedCE, String> {
184 MeowMeowRunner::materialize_mms_module_component_from_file(
185 &spec.asset_path,
186 &spec.export_name,
187 spec.args.clone(),
188 Some(world),
189 Some(emit),
190 )
191}
192
193pub fn decorate_panel_root_ce(
194 mut panel_root: MaterializedCE,
195 margin_right_gu: f64,
196) -> MaterializedCE {
197 panel_root.children.insert(
198 0,
199 CeChild::Spawn(MaterializedCE {
200 component_type: "Option".to_string(),
201 component_property_assignment_only: false,
202 ctor_method: None,
203 ctor_args: Vec::new(),
204 calls: Vec::new(),
205 named: Vec::new(),
206 positionals: Vec::new(),
207 deferred_block: None,
208 children: Vec::new(),
209 }),
210 );
211 panel_root.children.insert(
212 1,
213 CeChild::Spawn(MaterializedCE {
214 component_type: "Raycastable".to_string(),
215 component_property_assignment_only: false,
216 ctor_method: Some("enabled".to_string()),
217 ctor_args: Vec::new(),
218 calls: vec![(
219 "interaction_priority".to_string(),
220 vec![Value::Number(100.0)],
221 )],
222 named: Vec::new(),
223 positionals: Vec::new(),
224 deferred_block: None,
225 children: Vec::new(),
226 }),
227 );
228
229 if let Some(CeChild::Spawn(style_ce)) = panel_root.children.iter_mut().find(|child| {
230 matches!(
231 child,
232 CeChild::Spawn(MaterializedCE {
233 component_type,
234 ..
235 }) if component_type == "Style"
236 )
237 }) {
238 style_ce.calls.push((
239 "display".to_string(),
240 vec![Value::String("inline-block".to_string())],
241 ));
242 style_ce.calls.push((
243 "margin_right".to_string(),
244 vec![Value::Number(margin_right_gu)],
245 ));
246 }
247
248 panel_root
249}
250
251pub fn build_panel_layout_mount_ce(spec: PanelLayoutMountSpec) -> MaterializedCE {
252 let shared_layout_root = MaterializedCE {
253 component_type: "LayoutRoot".to_string(),
254 component_property_assignment_only: false,
255 ctor_method: None,
256 ctor_args: Vec::new(),
257 calls: vec![
258 (
259 "available_width".to_string(),
260 vec![Value::Number(spec.available_width_gu)],
261 ),
262 (
263 "available_height".to_string(),
264 vec![Value::Number(spec.total_height_gu)],
265 ),
266 (
267 "unit_scale".to_string(),
268 vec![Value::Number(spec.text_scale)],
269 ),
270 ],
271 named: vec![("name".to_string(), Value::String(spec.layout_name))],
272 positionals: Vec::new(),
273 deferred_block: None,
274 children: spec.children.into_iter().map(CeChild::Spawn).collect(),
275 };
276
277 MaterializedCE {
278 component_type: "T".to_string(),
279 component_property_assignment_only: false,
280 ctor_method: Some("position".to_string()),
281 ctor_args: vec![
282 Value::Number(spec.anchor_pos.0 as f64),
283 Value::Number(spec.anchor_pos.1 as f64),
284 Value::Number(spec.anchor_pos.2 as f64),
285 ],
286 calls: Vec::new(),
287 named: vec![("name".to_string(), Value::String(spec.mount_name))],
288 positionals: Vec::new(),
289 deferred_block: None,
290 children: vec![CeChild::Spawn(shared_layout_root)],
291 }
292}
293
294pub fn spawn_panel_layout_mount(
295 world: &mut World,
296 emit: &mut dyn SignalEmitter,
297 spec: PanelLayoutMountSpec,
298) -> Result<(ComponentId, ComponentId), String> {
299 let mount_name = spec.mount_name.clone();
300 let layout_name = spec.layout_name.clone();
301 let mount_ce = build_panel_layout_mount_ce(spec);
302 let mount_root = spawn_tree(&mount_ce, None, world, emit)?;
303 let layout_root = world
304 .find_component(mount_root, &format!("#{layout_name}"))
305 .ok_or_else(|| format!("missing layout root #{layout_name} under {mount_name}"))?;
306 Ok((mount_root, layout_root))
307}
308
309pub fn resolve_panel_instance(
310 world: &World,
311 editor_root: ComponentId,
312 spec: &PanelShellSpec,
313 root: ComponentId,
314 instance_id: Option<u64>,
315) -> Option<PanelInstance> {
316 let shell_root = world.find_component(root, &spec.root_selector)?;
317 let mut slots = HashMap::new();
318 for (kind, selector) in &spec.slot_selectors {
319 let slot = world.find_component(shell_root, selector)?;
320 slots.insert(*kind, slot);
321 }
322 let mut controls = HashMap::new();
323 for (kind, selector) in &spec.control_selectors {
324 let control = world.find_component(shell_root, selector)?;
325 controls.insert(*kind, control);
326 }
327 Some(PanelInstance {
328 panel_kind: spec.panel_kind,
329 editor_root,
330 root: shell_root,
331 slots,
332 controls,
333 instance_id,
334 })
335}
336
337pub fn spawn_panel_instance(
338 world: &mut World,
339 emit: &mut dyn SignalEmitter,
340 spec: &PanelShellSpec,
341 instance_id: Option<u64>,
342 margin_right_gu: f64,
343) -> Result<SpawnedPanelInstance, String> {
344 let panel_ce = build_panel_shell_component_expr(world, emit, spec)?;
345 let panel_ce = decorate_panel_root_ce(panel_ce, margin_right_gu);
346 let mount_root = spawn_tree(&panel_ce, None, world, emit)?;
347 let instance = resolve_panel_instance(world, mount_root, spec, mount_root, instance_id)
348 .ok_or_else(|| {
349 format!(
350 "failed to resolve {:?} panel instance from root selector {}",
351 spec.panel_kind, spec.root_selector
352 )
353 })?;
354 Ok(SpawnedPanelInstance {
355 mount_root,
356 instance,
357 })
358}
359
360pub fn render_list_into_slot(
361 world: &mut World,
362 emit: &mut dyn SignalEmitter,
363 panel: &PanelInstance,
364 slot_kind: PanelSlotKind,
365 spec: &ItemRendererSpec,
366 items: &[UiItem],
367 renderer: &mut DataRendererSystem,
368) -> Result<ComponentId, String> {
369 let slot = panel
370 .slots
371 .get(&slot_kind)
372 .ok_or_else(|| format!("{:?} has no slot {:?}", panel.panel_kind, slot_kind))?;
373 renderer.render_list(world, emit, *slot, spec, items)
374}
375
376pub fn render_detail_into_slot(
377 world: &mut World,
378 emit: &mut dyn SignalEmitter,
379 panel: &PanelInstance,
380 slot_kind: PanelSlotKind,
381 spec: &DetailRendererSpec,
382 detail: &UiDetailItem,
383 renderer: &mut DataRendererSystem,
384) -> Result<ComponentId, String> {
385 let slot = panel
386 .slots
387 .get(&slot_kind)
388 .ok_or_else(|| format!("{:?} has no slot {:?}", panel.panel_kind, slot_kind))?;
389 renderer.render_detail(world, emit, *slot, spec, detail)
390}
391
392pub fn clear_slot_on_panel(
393 world: &mut World,
394 emit: &mut dyn SignalEmitter,
395 panel: &PanelInstance,
396 slot_kind: PanelSlotKind,
397 renderer: &mut DataRendererSystem,
398) {
399 if let Some(slot) = panel.slots.get(&slot_kind) {
400 renderer.clear_slot(world, emit, *slot);
401 }
402}
403
404pub fn decode_panel_action_payload(
405 world: &World,
406 node: ComponentId,
407 payload_name: &str,
408 panel_kind: PanelKind,
409 action_kind: PanelActionKind,
410 instance_id: Option<u64>,
411 field_key: Option<String>,
412) -> Option<PanelActionPayload> {
413 let mut current = Some(node);
414 while let Some(component_id) = current {
415 if let Some(payload) = world.children_of(component_id).iter().find_map(|&child| {
416 (world.component_label(child) == Some(payload_name))
417 .then_some(child)
418 .and_then(|id| world.get_component_by_id_as::<DataComponent>(id))
419 }) {
420 return Some(PanelActionPayload {
421 panel_kind,
422 action_kind,
423 item_key: data_text(payload, "row_name").or_else(|| data_text(payload, "label")),
424 target_component: payload.get_component("target_component"),
425 instance_id,
426 field_key: field_key.or_else(|| data_text(payload, "field_key")),
427 });
428 }
429 current = world.parent_of(component_id);
430 }
431 None
432}
433
434pub fn data_text(data: &DataComponent, key: &str) -> Option<String> {
435 match data.get(key) {
436 Some(DataValue::Text(value)) => Some(value.clone()),
437 _ => None,
438 }
439}
440
441pub fn build_editor_panel_component_expr(
442 world: &mut World,
443 emit: &mut dyn SignalEmitter,
444 asset_path: &str,
445 export_name: &str,
446 args: Vec<Value>,
447 panel_kind: PanelKind,
448 panel_kind_label: &str,
449) -> Option<MaterializedCE> {
450 let result = build_panel_shell_component_expr(
451 world,
452 emit,
453 &PanelShellSpec {
454 panel_kind,
455 asset_path: asset_path.to_string(),
456 export_name: export_name.to_string(),
457 args,
458 root_selector: String::new(),
459 slot_selectors: HashMap::new(),
460 control_selectors: HashMap::new(),
461 },
462 )
463 .map_err(|error| {
464 eprintln!("[InspectorSystemStopgapMmsAdapter] {panel_kind_label} render error: {error}");
465 });
466 result.ok()
467}
468
469pub fn build_placeholder_panel_component_expr(title_name: &str, title: &str) -> MaterializedCE {
470 MaterializedCE {
471 component_type: "T".to_string(),
472 component_property_assignment_only: false,
473 ctor_method: None,
474 ctor_args: Vec::new(),
475 calls: Vec::new(),
476 named: vec![("name".to_string(), Value::String(title_name.to_string()))],
477 positionals: Vec::new(),
478 deferred_block: None,
479 children: vec![CeChild::Spawn(MaterializedCE {
480 component_type: "T".to_string(),
481 component_property_assignment_only: false,
482 ctor_method: None,
483 ctor_args: Vec::new(),
484 calls: Vec::new(),
485 named: vec![(
486 "name".to_string(),
487 Value::String(format!("{title_name}_title")),
488 )],
489 positionals: Vec::new(),
490 deferred_block: None,
491 children: vec![CeChild::Spawn(MaterializedCE {
492 component_type: "Text".to_string(),
493 component_property_assignment_only: false,
494 ctor_method: None,
495 ctor_args: Vec::new(),
496 calls: Vec::new(),
497 named: vec![(
498 "name".to_string(),
499 Value::String(format!("{title_name}_label")),
500 )],
501 positionals: vec![Value::String(title.to_string())],
502 deferred_block: None,
503 children: Vec::new(),
504 })],
505 })],
506 }
507}
508
509pub fn world_panel_asset_path() -> &'static str {
510 concat!(env!("CARGO_MANIFEST_DIR"), "/assets/components/panels.mms")
511}
512
513pub fn icons_asset_path() -> &'static str {
514 concat!(env!("CARGO_MANIFEST_DIR"), "/assets/components/icons.mms")
515}
516
517pub fn world_panel_status_asset_path() -> &'static str {
518 concat!(
519 env!("CARGO_MANIFEST_DIR"),
520 "/assets/components/panel_items.mms"
521 )
522}
523
524pub fn inspector_panel_asset_path() -> &'static str {
525 concat!(env!("CARGO_MANIFEST_DIR"), "/assets/components/panels.mms")
526}
527
528pub fn inspector_details_asset_path() -> &'static str {
529 concat!(
530 env!("CARGO_MANIFEST_DIR"),
531 "/assets/components/inspector_details.mms"
532 )
533}
534
535pub fn asset_panel_asset_path() -> &'static str {
536 concat!(env!("CARGO_MANIFEST_DIR"), "/assets/components/panels.mms")
537}
538
539pub fn paint_panel_asset_path() -> &'static str {
540 concat!(env!("CARGO_MANIFEST_DIR"), "/assets/components/panels.mms")
541}
542
543pub fn grid_panel_asset_path() -> &'static str {
544 concat!(env!("CARGO_MANIFEST_DIR"), "/assets/components/panels.mms")
545}
546
547pub fn editor_settings_panel_asset_path() -> &'static str {
548 concat!(env!("CARGO_MANIFEST_DIR"), "/assets/components/panels.mms")
549}
550
551pub fn pose_panel_asset_path() -> &'static str {
552 concat!(env!("CARGO_MANIFEST_DIR"), "/assets/components/panels.mms")
553}
554
555pub fn spawn_editor_panel_layout_tree(
558 world: &mut World,
559 emit: &mut dyn SignalEmitter,
560 model: &WorldPanelModel,
561 working_file_path: &Path,
562 world_panel_pos: (f32, f32, f32),
563) -> Option<(ComponentId, ComponentId)> {
564 let world_panel_title_color = Value::Array(vec![
565 Value::Number(0.90),
566 Value::Number(1.00),
567 Value::Number(0.92),
568 Value::Number(1.0),
569 ]);
570 let world_panel_bg = Value::Array(vec![
571 Value::Number(0.18),
572 Value::Number(0.78),
573 Value::Number(0.22),
574 Value::Number(0.95),
575 ]);
576 let world_panel_item_bg = Value::Array(vec![
577 Value::Number(0.92),
578 Value::Number(0.97),
579 Value::Number(0.92),
580 Value::Number(1.0),
581 ]);
582
583 let asset_panel_title_color = world_panel_title_color.clone();
584 let asset_panel_bg = world_panel_bg.clone();
585 let asset_panel_item_bg = world_panel_item_bg.clone();
586
587 let paint_panel_title_color = world_panel_title_color.clone();
588 let paint_panel_bg = world_panel_bg.clone();
589 let paint_panel_item_bg = world_panel_item_bg.clone();
590
591 let working_file_path_str = working_file_path.to_string_lossy().to_string();
592
593 let world_panel = match build_editor_panel_component_expr(
594 world,
595 emit,
596 world_panel_asset_path(),
597 "world_panel",
598 vec![
599 Value::String(model.title.clone()),
600 Value::Array(Vec::new()),
601 world_panel_title_color.clone(),
602 world_panel_bg.clone(),
603 world_panel_item_bg.clone(),
604 Value::String(working_file_path_str),
605 ],
606 PanelKind::World,
607 "world panel",
608 ) {
609 Some(panel) => panel,
610 None => return None,
611 };
612
613 let asset_items_val = Value::Array(Vec::new());
614
615 let asset_panel = match build_editor_panel_component_expr(
616 world,
617 emit,
618 asset_panel_asset_path(),
619 "asset_panel",
620 vec![
621 Value::String("Assets".to_string()),
622 asset_items_val,
623 asset_panel_title_color.clone(),
624 asset_panel_bg.clone(),
625 asset_panel_item_bg.clone(),
626 ],
627 PanelKind::Assets,
628 "asset panel",
629 ) {
630 Some(panel) => panel,
631 None => return None,
632 };
633
634 let paint_panel = match build_editor_panel_component_expr(
635 world,
636 emit,
637 paint_panel_asset_path(),
638 "paint_panel",
639 vec![
640 Value::String("Paint".to_string()),
641 paint_panel_title_color.clone(),
642 paint_panel_bg.clone(),
643 paint_panel_item_bg.clone(),
644 ],
645 PanelKind::Paint,
646 "paint panel",
647 ) {
648 Some(panel) => panel,
649 None => return None,
650 };
651
652 let color_panel = match build_editor_panel_component_expr(
653 world,
654 emit,
655 paint_panel_asset_path(),
656 "color_panel",
657 vec![
658 Value::String("Color".to_string()),
659 paint_panel_title_color.clone(),
660 paint_panel_bg.clone(),
661 ],
662 PanelKind::Color,
663 "color panel",
664 ) {
665 Some(panel) => panel,
666 None => return None,
667 };
668
669 let grid_panel = match build_editor_panel_component_expr(
670 world,
671 emit,
672 grid_panel_asset_path(),
673 "grid_panel",
674 vec![
675 Value::String("Grids".to_string()),
676 Value::Array(Vec::new()),
677 world_panel_title_color.clone(),
678 world_panel_bg.clone(),
679 world_panel_item_bg.clone(),
680 ],
681 PanelKind::Grid,
682 "grid panel",
683 ) {
684 Some(panel) => panel,
685 None => return None,
686 };
687
688 let pose_panel = match build_editor_panel_component_expr(
689 world,
690 emit,
691 pose_panel_asset_path(),
692 "pose_capture_panel",
693 vec![
694 Value::String("Poses".to_string()),
695 world_panel_title_color.clone(),
696 world_panel_bg.clone(),
697 ],
698 PanelKind::Pose,
699 "pose capture panel",
700 ) {
701 Some(panel) => panel,
702 None => return None,
703 };
704
705 let editor_settings_panel = match build_editor_panel_component_expr(
706 world,
707 emit,
708 editor_settings_panel_asset_path(),
709 "editor_settings_panel",
710 vec![
711 Value::String("Editor".to_string()),
712 world_panel_title_color.clone(),
713 world_panel_bg.clone(),
714 ],
715 PanelKind::Inspector,
716 "editor settings panel",
717 ) {
718 Some(panel) => panel,
719 None => return None,
720 };
721
722 let anchor_pos = world_panel_pos;
723
724 let total_height_gu = 60.5f64
725 .max(60.5)
726 .max(60.5)
727 .max(32.0)
728 .max(60.5)
729 .max(60.5)
730 .max(11.5)
731 * 2.0
732 + 2.0
733 + (0.5 * 2.0);
734
735 let world_panel = decorate_panel_root_ce(world_panel, 2.0);
736 let paint_panel = decorate_panel_root_ce(paint_panel, 2.0);
737 let color_panel = decorate_panel_root_ce(color_panel, 2.0);
738 let asset_panel = decorate_panel_root_ce(asset_panel, 2.0);
739 let grid_panel = decorate_panel_root_ce(grid_panel, 2.0);
740 let pose_panel = decorate_panel_root_ce(pose_panel, 2.0);
741 let editor_settings_panel = decorate_panel_root_ce(editor_settings_panel, 2.0);
742
743 let (panel_mount_root, layout_root_id) = match spawn_panel_layout_mount(
744 world,
745 emit,
746 PanelLayoutMountSpec {
747 anchor_pos,
748 total_height_gu,
749 available_width_gu: 200000.0,
750 text_scale: 0.08,
751 mount_name: PANEL_LAYOUT_MOUNT_NAME.to_string(),
752 layout_name: PANEL_LAYOUT_ROOT_NAME.to_string(),
753 children: vec![
754 editor_settings_panel,
755 paint_panel,
756 color_panel,
757 grid_panel,
758 pose_panel,
759 asset_panel,
760 world_panel,
761 ],
762 },
763 ) {
764 Ok(ids) => ids,
765 Err(error) => {
766 eprintln!("[InspectorSystemStopgapMmsAdapter] panel layout spawn error: {error}");
767 return None;
768 }
769 };
770
771 Some((panel_mount_root, layout_root_id))
772}
773
774#[cfg(test)]
775mod tests {
776 use super::{PanelLayoutMountSpec, build_panel_layout_mount_ce};
777 use crate::scripting::object::{CeChild, Value};
778
779 #[test]
780 fn build_panel_layout_mount_ce_places_layout_root_directly_under_mount() {
781 let mount = build_panel_layout_mount_ce(PanelLayoutMountSpec {
782 anchor_pos: (1.0, 2.0, 3.0),
783 total_height_gu: 10.0,
784 available_width_gu: 20.0,
785 text_scale: 0.08,
786 mount_name: "panel_mount".to_string(),
787 layout_name: "panel_layout".to_string(),
788 children: Vec::new(),
789 });
790
791 assert_eq!(mount.component_type, "T");
792 assert_eq!(mount.children.len(), 1);
793 let CeChild::Spawn(layout_root) = &mount.children[0] else {
794 panic!("expected spawned layout root");
795 };
796 assert_eq!(layout_root.component_type, "LayoutRoot");
797 assert_eq!(
798 layout_root.named,
799 vec![(
800 "name".to_string(),
801 Value::String("panel_layout".to_string())
802 )]
803 );
804 }
805}