nightshade-editor 0.13.4

An interactive editor for the Nightshade game engine
use crate::Editor;
use crate::app_context::PrefabMetadata;
use crate::engine_editor::{
    EditorContext, UndoHistory, UndoableOperation, WorldTreeUi, capture_hierarchy, selection,
};
use crate::project_io::{self, ProjectState};
use nightshade::prelude::*;

enum MapAction {
    SwitchTo(String),
    CreateNew(String),
    DeleteCurrent,
}

impl Editor {
    pub fn left_panel_ui(&mut self, world: &mut World, root_ui: &mut egui::Ui) {
        egui::Panel::left("left_panel")
            .default_size(200.0)
            .show_inside(root_ui, |ui| {
                ui.heading("Nodes");
                ui.separator();

                let gizmo_root = self.context.editor.gizmo_root();
                let tree_result = WorldTreeUi::ui_with_context(
                    world,
                    &mut self.context.editor.selection,
                    &mut self.context.editor.undo_history,
                    &mut self.tree_cache,
                    gizmo_root,
                    ui,
                );
                if tree_result.open_add_node_modal {
                    self.context.ui.popup.add_node_open = true;
                    self.context.ui.popup.add_node_search.clear();
                }
                if tree_result.project_modified {
                    self.project_state.mark_modified();
                }

                ui.add_space(8.0);
                ui.separator();

                let mut map_action = None;
                egui::ScrollArea::vertical()
                    .id_salt("left_panel_scroll")
                    .show(ui, |ui| {
                        render_history_section(
                            &mut self.context.editor,
                            &mut self.project_state,
                            world,
                            ui,
                        );
                        map_action = render_maps_section(&self.project, ui);
                        render_prefabs_section(
                            &mut self.context.assets.prefabs,
                            &mut self.context.editor.undo_history,
                            &mut self.project_state,
                            &mut self.context.ui.tree_dirty,
                            world,
                            ui,
                        );
                    });

                if let Some(action) = map_action {
                    match action {
                        MapAction::SwitchTo(name) => self.switch_to_map(world, &name),
                        MapAction::CreateNew(name) => self.create_new_map(world, name),
                        MapAction::DeleteCurrent => self.delete_current_map(world),
                    }
                }
            });
    }
}

fn render_history_section(
    context: &mut EditorContext,
    project_state: &mut ProjectState,
    world: &mut World,
    ui: &mut egui::Ui,
) {
    ui.collapsing("History", |ui| {
        ui.horizontal(|ui| {
            if ui
                .add_enabled(context.undo_history.can_undo(), egui::Button::new("Undo"))
                .clicked()
                && selection::undo_with_selection_update(context, world)
            {
                project_state.mark_modified();
            }
            if ui
                .add_enabled(context.undo_history.can_redo(), egui::Button::new("Redo"))
                .clicked()
                && selection::redo_with_selection_update(context, world)
            {
                project_state.mark_modified();
            }
        });

        ui.separator();

        let undo_stack = context.undo_history.undo_stack();
        let redo_stack = context.undo_history.redo_stack();

        if undo_stack.is_empty() && redo_stack.is_empty() {
            ui.label("No history");
        } else {
            egui::ScrollArea::vertical()
                .max_height(200.0)
                .id_salt("history_scroll")
                .show(ui, |ui| {
                    for entry in redo_stack.iter().rev() {
                        ui.label(
                            egui::RichText::new(&entry.description)
                                .color(egui::Color32::from_gray(100)),
                        );
                    }

                    if !redo_stack.is_empty() || !undo_stack.is_empty() {
                        ui.separator();
                        ui.label(
                            egui::RichText::new("--- Current ---")
                                .strong()
                                .color(egui::Color32::from_rgb(100, 200, 100)),
                        );
                        ui.separator();
                    }

                    for entry in undo_stack.iter().rev() {
                        ui.label(&entry.description);
                    }
                });
        }
    });
}

fn render_maps_section(
    project: &Option<project_io::EditorProjectFile>,
    ui: &mut egui::Ui,
) -> Option<MapAction> {
    let mut action = None;
    ui.collapsing("Maps", |ui| {
        if let Some(project) = project {
            let data = project_io::project_data(project);
            let active_scene_name = data.and_then(|d| d.active_scene_name.clone());
            let mut map_names: Vec<String> = data
                .map(|d| d.scenes.keys().cloned().collect())
                .unwrap_or_default();
            map_names.sort();
            for map_name in &map_names {
                let is_active = active_scene_name.as_ref() == Some(map_name);
                let display_name = if is_active {
                    format!("{}", map_name)
                } else {
                    map_name.clone()
                };
                if ui.selectable_label(is_active, &display_name).clicked() && !is_active {
                    action = Some(MapAction::SwitchTo(map_name.clone()));
                }
            }
        } else {
            ui.label("No project loaded");
        }
        ui.separator();
        ui.horizontal(|ui| {
            if ui.button("+ New").clicked() {
                let map_count = project
                    .as_ref()
                    .and_then(|p| project_io::project_data(p).map(|d| d.scenes.len()))
                    .unwrap_or(0);
                action = Some(MapAction::CreateNew(format!("Map {}", map_count + 1)));
            }
            if ui
                .add_enabled(
                    project
                        .as_ref()
                        .map(|p| {
                            project_io::project_data(p)
                                .map(|d| d.scenes.len() > 1)
                                .unwrap_or(false)
                        })
                        .unwrap_or(false),
                    egui::Button::new("Delete"),
                )
                .clicked()
            {
                action = Some(MapAction::DeleteCurrent);
            }
        });
    });
    action
}

fn render_prefabs_section(
    prefabs: &mut Vec<PrefabMetadata>,
    undo_history: &mut UndoHistory,
    project_state: &mut ProjectState,
    tree_dirty: &mut bool,
    world: &mut World,
    ui: &mut egui::Ui,
) {
    ui.collapsing("Prefabs", |ui| {
        if prefabs.is_empty() {
            ui.label("No prefabs loaded");
            ui.label("Drop .gltf/.glb files or use Import menu");
        } else {
            let mut prefab_to_remove = None;
            for (index, prefab_metadata) in prefabs.iter().enumerate() {
                ui.horizontal(|ui| {
                    let label = if let Some(source) = &prefab_metadata.source_path {
                        format!("{} ({})", prefab_metadata.prefab.name, source)
                    } else {
                        prefab_metadata.prefab.name.clone()
                    };

                    if ui.button(&label).clicked() {
                        let entity = spawn_prefab_with_animations(
                            world,
                            &prefab_metadata.prefab,
                            &prefab_metadata.animations,
                            nalgebra_glm::vec3(0.0, 0.0, 0.0),
                        );
                        let hierarchy = Box::new(capture_hierarchy(world, entity));
                        undo_history.push(
                            UndoableOperation::EntityCreated {
                                hierarchy,
                                current_entity: entity,
                            },
                            format!("Spawn prefab {}", prefab_metadata.prefab.name),
                        );
                        *tree_dirty = true;
                        project_state.mark_modified();
                    }
                    if ui.small_button("x").clicked() {
                        prefab_to_remove = Some(index);
                    }
                });
            }
            if let Some(index) = prefab_to_remove {
                prefabs.remove(index);
            }
        }
    });
}