mittens_engine/engine/ecs/system/
data_renderer_system.rs1use 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#[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
37pub enum RendererSpec<T> {
41 Mms {
43 asset_path: &'static str,
44 export_name: &'static str,
45 to_args: fn(&T) -> Vec<Value>,
46 },
47 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#[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 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 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 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
169fn 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
225fn 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}