Skip to main content

rustapi/docks/
visual_code.rs

1use crate::docks::visual_code_undo::*;
2use crate::prelude::*;
3use codegridfx::DebugModule;
4use codegridfx::Module;
5use theframework::prelude::*;
6
7/// Unique identifier for entities being edited
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum EntityKey {
10    CharacterInstance(Uuid, Uuid), // (region_id, instance_id)
11    CharacterTemplate(Uuid),
12    ItemInstance(Uuid, Uuid), // (region_id, instance_id)
13    ItemTemplate(Uuid),
14}
15
16pub struct VisualCodeDock {
17    module: Module,
18    // Per-entity undo stacks
19    entity_undos: FxHashMap<EntityKey, VisualCodeUndo>,
20    current_entity: Option<EntityKey>,
21    max_undo: usize,
22    prev_module: Option<Module>,
23}
24
25impl Dock for VisualCodeDock {
26    fn new() -> Self
27    where
28        Self: Sized,
29    {
30        Self {
31            module: Module::default(),
32            entity_undos: FxHashMap::default(),
33            current_entity: None,
34            max_undo: 30,
35            prev_module: None,
36        }
37    }
38
39    fn setup(&mut self, ctx: &mut TheContext) -> TheCanvas {
40        self.module.build_canvas(ctx, "DockVisualScripting")
41    }
42
43    fn activate(
44        &mut self,
45        ui: &mut TheUI,
46        ctx: &mut TheContext,
47        project: &Project,
48        server_ctx: &mut ServerContext,
49    ) {
50        if let Some(id) = server_ctx.pc.id() {
51            if let Some(instance_id) = server_ctx.pc.get_region_character_instance_id() {
52                if let Some(region) = project.get_region(&id) {
53                    if let Some(character_instance) = region.characters.get(&instance_id) {
54                        let mut entity_key = EntityKey::CharacterInstance(id, instance_id);
55                        self.module = character_instance.module.clone();
56                        self.module.module_type = codegridfx::ModuleType::CharacterInstance;
57
58                        if self.module.routines.is_empty() {
59                            if let Some(character) =
60                                project.characters.get(&character_instance.character_id)
61                            {
62                                self.module = character.module.clone();
63                                self.module.module_type = codegridfx::ModuleType::CharacterTemplate;
64                                entity_key =
65                                    EntityKey::CharacterTemplate(character_instance.character_id);
66                            }
67                        }
68
69                        self.module.view_name = "DockVisualScripting".into();
70                        self.module.redraw(ui, ctx);
71                        self.switch_to_entity(entity_key, ctx);
72                    }
73                }
74            } else if let Some(instance_id) = server_ctx.pc.get_region_item_instance_id() {
75                if let Some(region) = project.get_region(&id) {
76                    if let Some(item_instance) = region.items.get(&instance_id) {
77                        let mut entity_key = EntityKey::ItemInstance(id, instance_id);
78                        self.module = item_instance.module.clone();
79                        self.module.module_type = codegridfx::ModuleType::ItemInstance;
80
81                        if self.module.routines.is_empty() {
82                            if let Some(item) = project.items.get(&item_instance.item_id) {
83                                self.module = item.module.clone();
84                                self.module.module_type = codegridfx::ModuleType::ItemTemplate;
85                                entity_key = EntityKey::ItemTemplate(item_instance.item_id);
86                            }
87                        }
88
89                        self.module.view_name = "DockVisualScripting".into();
90                        self.module.redraw(ui, ctx);
91                        self.switch_to_entity(entity_key, ctx);
92                    }
93                }
94            } else if server_ctx.pc.is_character() {
95                if let Some(character) = project.characters.get(&id) {
96                    self.module = character.module.clone();
97                    // Keep stored routines as-is; avoid re-running default-routine insertion
98                    // on each dock activation.
99                    self.module.module_type = codegridfx::ModuleType::CharacterTemplate;
100                    self.module.view_name = "DockVisualScripting".into();
101                    self.module.redraw(ui, ctx);
102                    self.switch_to_entity(EntityKey::CharacterTemplate(id), ctx);
103                }
104            } else if server_ctx.pc.is_item() {
105                if let Some(item) = project.items.get(&id) {
106                    self.module = item.module.clone();
107                    // Keep stored routines as-is; avoid re-running default-routine insertion
108                    // on each dock activation.
109                    self.module.module_type = codegridfx::ModuleType::ItemTemplate;
110                    self.module.view_name = "DockVisualScripting".into();
111                    self.module.redraw(ui, ctx);
112                    self.switch_to_entity(EntityKey::ItemTemplate(id), ctx);
113                }
114            }
115        }
116    }
117
118    fn apply_debug_data(
119        &mut self,
120        ui: &mut TheUI,
121        ctx: &mut TheContext,
122        project: &Project,
123        server_ctx: &ServerContext,
124        debug: &DebugModule,
125    ) {
126        if let Some(runtime_id) = self.runtime_debug_id(project, server_ctx) {
127            self.module.redraw_debug(ui, ctx, runtime_id, debug);
128        } else {
129            self.module.redraw(ui, ctx);
130        }
131    }
132
133    fn import(
134        &mut self,
135        content: String,
136        ui: &mut TheUI,
137        ctx: &mut TheContext,
138        project: &mut Project,
139        server_ctx: &mut ServerContext,
140    ) {
141        self.module = Module::from_json(&content);
142        if let Some(prev) = &self.prev_module {
143            self.module.id = prev.id;
144        }
145        self.module.redraw(ui, ctx);
146        self.handle_event(
147            &TheEvent::Custom(TheId::named("ModuleChanged"), TheValue::Empty),
148            ui,
149            ctx,
150            project,
151            server_ctx,
152        );
153    }
154
155    fn export(&self) -> Option<String> {
156        Some(self.module.to_json())
157    }
158
159    fn handle_event(
160        &mut self,
161        event: &TheEvent,
162        ui: &mut TheUI,
163        ctx: &mut TheContext,
164        project: &mut Project,
165        server_ctx: &mut ServerContext,
166    ) -> bool {
167        let redraw = self.module.handle_event(event, ui, ctx, &project.palette);
168
169        match event {
170            TheEvent::Custom(id, _) => {
171                if id.name == "ModuleChanged" {
172                    // Add undo atom before applying the change
173                    if let Some(prev) = &self.prev_module {
174                        // Guard against duplicate "ModuleChanged" emissions with unchanged state.
175                        if prev.to_json() != self.module.to_json() {
176                            let atom =
177                                VisualCodeUndoAtom::ModuleEdit(prev.clone(), self.module.clone());
178                            self.add_undo(atom, ctx);
179                        }
180                    }
181
182                    // Store current module as previous for next change
183                    self.prev_module = Some(self.module.clone());
184
185                    self.update_project_module(project, server_ctx);
186                }
187            }
188            _ => {}
189        }
190        redraw
191    }
192
193    fn supports_undo(&self) -> bool {
194        true
195    }
196
197    fn has_changes(&self) -> bool {
198        // Check if any entity has changes (index >= 0, meaning not fully undone)
199        self.entity_undos.values().any(|undo| undo.has_changes())
200    }
201
202    fn mark_saved(&mut self) {
203        for undo in self.entity_undos.values_mut() {
204            undo.index = -1;
205        }
206    }
207
208    fn undo(
209        &mut self,
210        ui: &mut TheUI,
211        ctx: &mut TheContext,
212        project: &mut Project,
213        server_ctx: &mut ServerContext,
214    ) {
215        if let Some(entity_key) = self.current_entity {
216            if let Some(undo) = self.entity_undos.get_mut(&entity_key) {
217                undo.undo(&mut self.module, ui, ctx);
218                self.prev_module = Some(self.module.clone());
219                self.set_undo_state_to_ui(ctx);
220
221                // Update the project with the undone module
222                self.update_project_module(project, server_ctx);
223            }
224        }
225    }
226
227    fn redo(
228        &mut self,
229        ui: &mut TheUI,
230        ctx: &mut TheContext,
231        project: &mut Project,
232        server_ctx: &mut ServerContext,
233    ) {
234        if let Some(entity_key) = self.current_entity {
235            if let Some(undo) = self.entity_undos.get_mut(&entity_key) {
236                undo.redo(&mut self.module, ui, ctx);
237                self.prev_module = Some(self.module.clone());
238                self.set_undo_state_to_ui(ctx);
239
240                // Update the project with the redone module
241                self.update_project_module(project, server_ctx);
242            }
243        }
244    }
245
246    fn set_undo_state_to_ui(&self, ctx: &mut TheContext) {
247        if let Some(entity_key) = self.current_entity {
248            if let Some(undo) = self.entity_undos.get(&entity_key) {
249                if undo.has_undo() {
250                    ctx.ui.set_enabled("Undo");
251                } else {
252                    ctx.ui.set_disabled("Undo");
253                }
254
255                if undo.has_redo() {
256                    ctx.ui.set_enabled("Redo");
257                } else {
258                    ctx.ui.set_disabled("Redo");
259                }
260                return;
261            }
262        }
263
264        // No entity selected or no undo stack
265        ctx.ui.set_disabled("Undo");
266        ctx.ui.set_disabled("Redo");
267    }
268}
269
270impl VisualCodeDock {
271    /// Switch to a different entity and update undo button states
272    fn switch_to_entity(&mut self, entity_key: EntityKey, ctx: &mut TheContext) {
273        self.current_entity = Some(entity_key);
274        self.prev_module = Some(self.module.clone());
275        self.set_undo_state_to_ui(ctx);
276    }
277
278    /// Add an undo atom to the current entity's undo stack
279    fn add_undo(&mut self, atom: VisualCodeUndoAtom, ctx: &mut TheContext) {
280        if let Some(entity_key) = self.current_entity {
281            let undo = self
282                .entity_undos
283                .entry(entity_key)
284                .or_insert_with(VisualCodeUndo::new);
285            undo.add(atom);
286            undo.truncate_to_limit(self.max_undo);
287            self.set_undo_state_to_ui(ctx);
288        }
289    }
290
291    /// Update the project with the current module state
292    fn update_project_module(&mut self, project: &mut Project, server_ctx: &mut ServerContext) {
293        let code = self.module.build(false);
294        let debug_code = self.module.build(true);
295
296        match self.current_entity {
297            Some(EntityKey::CharacterInstance(region_id, instance_id)) => {
298                if let Some(region) = project.get_region_mut(&region_id) {
299                    if let Some(character_instance) = region.characters.get_mut(&instance_id) {
300                        character_instance.module = self.module.clone();
301                        character_instance.source = code;
302                        character_instance.source_debug = debug_code;
303                    }
304                }
305            }
306            Some(EntityKey::CharacterTemplate(id)) => {
307                if let Some(character) = project.characters.get_mut(&id) {
308                    character.module = self.module.clone();
309                    character.source = code;
310                    character.source_debug = debug_code;
311                }
312            }
313            Some(EntityKey::ItemInstance(region_id, instance_id)) => {
314                if let Some(region) = project.get_region_mut(&region_id) {
315                    if let Some(item_instance) = region.items.get_mut(&instance_id) {
316                        item_instance.module = self.module.clone();
317                        item_instance.source = code;
318                        item_instance.source_debug = debug_code;
319                    }
320                }
321            }
322            Some(EntityKey::ItemTemplate(id)) => {
323                if let Some(item) = project.items.get_mut(&id) {
324                    item.module = self.module.clone();
325                    item.source = code;
326                    item.source_debug = debug_code;
327                }
328            }
329            None => {
330                let _ = server_ctx;
331            }
332        }
333    }
334
335    fn runtime_debug_id(&self, project: &Project, server_ctx: &ServerContext) -> Option<u32> {
336        let region = project.get_region(&server_ctx.curr_region)?;
337
338        match server_ctx.cc {
339            ContentContext::CharacterInstance(instance_id) => region
340                .map
341                .entities
342                .iter()
343                .find(|entity| entity.creator_id == instance_id)
344                .map(|entity| entity.id),
345            ContentContext::CharacterTemplate(template_id) => region
346                .characters
347                .values()
348                .find(|instance| instance.character_id == template_id)
349                .and_then(|instance| {
350                    region
351                        .map
352                        .entities
353                        .iter()
354                        .find(|entity| entity.creator_id == instance.id)
355                        .map(|entity| entity.id)
356                }),
357            ContentContext::ItemInstance(instance_id) => region
358                .map
359                .items
360                .iter()
361                .find(|item| item.creator_id == instance_id)
362                .map(|item| item.id),
363            ContentContext::ItemTemplate(template_id) => region
364                .items
365                .values()
366                .find(|instance| instance.item_id == template_id)
367                .and_then(|instance| {
368                    region
369                        .map
370                        .items
371                        .iter()
372                        .find(|item| item.creator_id == instance.id)
373                        .map(|item| item.id)
374                }),
375            _ => None,
376        }
377    }
378}