Skip to main content

mittens_engine/engine/ecs/system/
panel_system.rs

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
555/// Build the panel layout tree: all panel component expressions + mount.
556/// Returns `(panel_mount_root, layout_root_id)` on success.
557pub 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}