Skip to main content

mittens_engine/engine/ecs/system/
data_renderer_system.rs

1use std::collections::HashMap;
2
3use crate::engine::ecs::component::{
4    Display, LayoutComponent, Overflow, SizeDimension, StyleComponent, TransformComponent,
5};
6use crate::engine::ecs::{ComponentId, IntentValue, SignalEmitter, World};
7use crate::scripting::component_registry::spawn_tree;
8use crate::scripting::object::Value;
9use crate::scripting::runner::MeowMeowRunner;
10
11// ── Payload types ──
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum UiItemKind {
15    Component,
16    Info,
17    EditorRoot,
18    Spacer,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct UiItem {
23    pub key: String,
24    pub kind: UiItemKind,
25    pub label: String,
26    pub selected: bool,
27    pub target_ref: Option<ComponentId>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct UiDetailItem {
32    pub name: String,
33    pub id: String,
34    pub guid: String,
35}
36
37// ── Renderer spec ──
38
39/// How to render one unit of data into a live component subtree.
40pub enum RendererSpec<T> {
41    /// Materialize an MMS component expression and spawn it.
42    Mms {
43        asset_path: &'static str,
44        export_name: &'static str,
45        to_args: fn(&T) -> Vec<Value>,
46    },
47    /// Build a component subtree directly from Rust.
48    ///
49    /// Uses a boxed closure so the renderer can capture shared state
50    /// (e.g. panel models from an `Arc<Mutex<Vec<...>>>`).
51    Rust {
52        render_fn: Box<
53            dyn Fn(&mut World, &mut dyn SignalEmitter, &T) -> Result<ComponentId, String>
54                + Send
55                + Sync,
56        >,
57    },
58}
59
60pub type ItemRendererSpec = RendererSpec<UiItem>;
61pub type DetailRendererSpec = RendererSpec<UiDetailItem>;
62
63// ── System ──
64
65/// Owns the data-to-live-subtree projection lifecycle for editor UI slots.
66///
67/// Tracks rendered subtrees per slot so that subsequent renders can remove
68/// previous content before attaching new content (full-rerender semantics).
69#[derive(Debug, Default)]
70pub struct DataRendererSystem {
71    rendered_subtrees: HashMap<ComponentId, ComponentId>,
72}
73
74impl DataRendererSystem {
75    pub fn new() -> Self {
76        Self {
77            rendered_subtrees: HashMap::new(),
78        }
79    }
80
81    /// Render a list of items into a target slot.
82    ///
83    /// Each item is rendered independently via `spec`. Results are collected
84    /// under a container that becomes the slot's single child.
85    /// Any previously rendered subtree for this slot is removed first.
86    ///
87    /// Returns the container `ComponentId` so callers can attach additional
88    /// panel-specific state (e.g. `SelectionComponent`).
89    pub fn render_list(
90        &mut self,
91        world: &mut World,
92        emit: &mut dyn SignalEmitter,
93        slot: ComponentId,
94        spec: &ItemRendererSpec,
95        items: &[UiItem],
96    ) -> Result<ComponentId, String> {
97        self.remove_previous(world, emit, slot);
98        let container = spawn_list_container(world, slot);
99
100        for item in items {
101            let child_id = render_item(world, emit, spec, item)?;
102            let _ = world.add_child(container, child_id);
103        }
104
105        emit.push_intent_now(
106            container,
107            IntentValue::Attach {
108                parents: vec![slot],
109                child: container,
110            },
111        );
112        self.rendered_subtrees.insert(slot, container);
113        mark_nearest_layout_dirty(world, slot);
114        Ok(container)
115    }
116
117    /// Render a detail view into a target slot.
118    ///
119    /// Any previously rendered subtree for this slot is removed first.
120    ///
121    /// Returns the root `ComponentId` of the rendered subtree.
122    pub fn render_detail(
123        &mut self,
124        world: &mut World,
125        emit: &mut dyn SignalEmitter,
126        slot: ComponentId,
127        spec: &DetailRendererSpec,
128        detail: &UiDetailItem,
129    ) -> Result<ComponentId, String> {
130        self.remove_previous(world, emit, slot);
131        let root = render_detail_item(world, emit, spec, detail)?;
132
133        emit.push_intent_now(
134            root,
135            IntentValue::Attach {
136                parents: vec![slot],
137                child: root,
138            },
139        );
140        self.rendered_subtrees.insert(slot, root);
141        mark_nearest_layout_dirty(world, slot);
142        Ok(root)
143    }
144
145    /// Remove any rendered content for this slot. No-op if slot is not tracked.
146    pub fn clear_slot(
147        &mut self,
148        world: &mut World,
149        emit: &mut dyn SignalEmitter,
150        slot: ComponentId,
151    ) {
152        self.remove_previous(world, emit, slot);
153    }
154
155    fn remove_previous(&mut self, world: &World, emit: &mut dyn SignalEmitter, slot: ComponentId) {
156        if let Some(prev_root) = self.rendered_subtrees.remove(&slot) {
157            if world.get_component_record(prev_root).is_some() {
158                emit.push_intent_now(
159                    prev_root,
160                    IntentValue::RemoveSubtree {
161                        component_ids: vec![prev_root],
162                    },
163                );
164            }
165        }
166    }
167}
168
169// ── Internal render dispatch ──
170
171fn render_item(
172    world: &mut World,
173    emit: &mut dyn SignalEmitter,
174    spec: &ItemRendererSpec,
175    item: &UiItem,
176) -> Result<ComponentId, String> {
177    match spec {
178        RendererSpec::Mms {
179            asset_path,
180            export_name,
181            to_args,
182        } => {
183            let args = (to_args)(item);
184            let ce = MeowMeowRunner::materialize_mms_module_component_from_file(
185                asset_path,
186                export_name,
187                args,
188                Some(world),
189                Some(emit),
190            )
191            .map_err(|e| format!("MMS materialization failed: {e}"))?;
192            spawn_tree(&ce, None, world, emit).map_err(|e| format!("spawn_tree failed: {e}"))
193        }
194        RendererSpec::Rust { render_fn } => (render_fn)(world, emit, item),
195    }
196}
197
198fn render_detail_item(
199    world: &mut World,
200    emit: &mut dyn SignalEmitter,
201    spec: &DetailRendererSpec,
202    detail: &UiDetailItem,
203) -> Result<ComponentId, String> {
204    match spec {
205        RendererSpec::Mms {
206            asset_path,
207            export_name,
208            to_args,
209        } => {
210            let args = (to_args)(detail);
211            let ce = MeowMeowRunner::materialize_mms_module_component_from_file(
212                asset_path,
213                export_name,
214                args,
215                Some(world),
216                Some(emit),
217            )
218            .map_err(|e| format!("MMS materialization failed: {e}"))?;
219            spawn_tree(&ce, None, world, emit).map_err(|e| format!("spawn_tree failed: {e}"))
220        }
221        RendererSpec::Rust { render_fn } => (render_fn)(world, emit, detail),
222    }
223}
224
225// ── Helpers ──
226
227fn spawn_list_container(world: &mut World, slot: ComponentId) -> ComponentId {
228    let name = format!("data_renderer_list_{slot:?}");
229    let root = world.add_component_boxed_named(&name, Box::new(TransformComponent::new()));
230    let style = world.add_component_boxed_named(
231        format!("{name}_style"),
232        Box::new({
233            let mut style = StyleComponent::new();
234            style.display = Some(Display::Block);
235            style.width = SizeDimension::Percent(100.0);
236            style.overflow = Overflow::Visible;
237            style
238        }),
239    );
240    let _ = world.add_child(root, style);
241    root
242}
243
244fn mark_nearest_layout_dirty(world: &mut World, start: ComponentId) {
245    let mut current = Some(start);
246    while let Some(id) = current {
247        if let Some(layout) = world.get_component_by_id_as_mut::<LayoutComponent>(id) {
248            layout.mark_dirty();
249            return;
250        }
251        current = world.parent_of(id);
252    }
253}