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);
}
}
});
}