nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use super::TreeCache;
use crate::editor::selection::EntitySelection;
use crate::editor::undo::{UndoHistory, UndoableOperation, capture_hierarchy};
use crate::prelude::*;

pub struct TreeUiResult {
    pub open_add_node_modal: bool,
    pub project_modified: bool,
}

fn get_entity_name(world: &World, entity: Entity) -> String {
    if let Some(Name(name)) = world.get_name(entity) {
        name.clone()
    } else {
        format!("Entity {}", entity.id)
    }
}

struct TreeRenderState<'a> {
    selection: &'a mut EntitySelection,
    undo_history: &'a mut UndoHistory,
    project_modified: bool,
    tree_cache: &'a mut TreeCache,
}

pub struct WorldTreeUi;

impl WorldTreeUi {
    pub fn ui_with_context(
        world: &mut World,
        selection: &mut EntitySelection,
        undo_history: &mut UndoHistory,
        tree_cache: &mut TreeCache,
        gizmo_root: Option<Entity>,
        ui: &mut egui::Ui,
    ) -> TreeUiResult {
        if tree_cache.needs_rebuild(gizmo_root, selection.len()) {
            tree_cache.rebuild(world, world.resources.active_camera, gizmo_root);
            tree_cache.update_selection_count(selection.len());
        }

        let mut open_add_node_modal = false;

        ui.horizontal(|ui| {
            if ui.button("+ Add Node").clicked() {
                open_add_node_modal = true;
            }
        });

        ui.separator();

        let mut state = TreeRenderState {
            selection,
            undo_history,
            project_modified: false,
            tree_cache,
        };

        egui::ScrollArea::vertical()
            .auto_shrink([false, false])
            .show(ui, |ui| {
                for root_index in 0..state.tree_cache.roots.len() {
                    let root = state.tree_cache.roots[root_index];
                    Self::render_entity_recursive(world, &mut state, root, ui);
                }
            });

        TreeUiResult {
            open_add_node_modal,
            project_modified: state.project_modified,
        }
    }

    fn render_entity_recursive(
        world: &mut World,
        state: &mut TreeRenderState,
        entity: Entity,
        ui: &mut egui::Ui,
    ) {
        if state.tree_cache.editor_entities.contains(&entity) {
            return;
        }

        let name = get_entity_name(world, entity);
        let children_len = state
            .tree_cache
            .sorted_children
            .get(&entity)
            .map_or(0, |children| children.len());
        let has_children = children_len > 0;
        let selected = state.selection.contains(entity);

        if has_children {
            let id = ui.make_persistent_id(entity.id);
            egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, false)
                .show_header(ui, |ui| {
                    let response = ui.selectable_label(selected, &name);
                    Self::handle_click(state.selection, entity, &response, ui);
                    Self::show_context_menu(world, state, entity, &response);
                })
                .body(|ui| {
                    for child_index in 0..children_len {
                        let child = state.tree_cache.sorted_children[&entity][child_index];
                        Self::render_entity_recursive(world, state, child, ui);
                    }
                });
        } else {
            ui.horizontal(|ui| {
                ui.add_space(19.0);
                let response = ui.selectable_label(selected, &name);
                Self::handle_click(state.selection, entity, &response, ui);
                Self::show_context_menu(world, state, entity, &response);
            });
        }
    }

    fn handle_click(
        selection: &mut EntitySelection,
        entity: Entity,
        response: &egui::Response,
        ui: &egui::Ui,
    ) {
        if response.clicked() {
            let modifiers = ui.input(|i| i.modifiers);
            if modifiers.ctrl {
                selection.toggle(entity);
            } else if modifiers.shift {
                selection.add(entity);
            } else {
                selection.set_single(entity);
            }
        }
    }

    fn show_context_menu(
        world: &mut World,
        state: &mut TreeRenderState,
        entity: Entity,
        response: &egui::Response,
    ) {
        let has_prefab_source = world.get_prefab_source(entity).is_some();

        response.context_menu(|ui| {
            if ui.button("Add Entity").clicked() {
                Self::add_entity_internal(world, state, entity);
                ui.close();
            }

            if has_prefab_source && ui.button("Instantiate Prefab").clicked() {
                super::prefab::instantiate_prefab(world, state.tree_cache, entity);
                ui.close();
            }

            if ui.button("Remove").clicked() {
                let hierarchy = Box::new(capture_hierarchy(world, entity));
                let entity_name = world
                    .get_name(entity)
                    .map(|n| n.0.clone())
                    .unwrap_or_else(|| format!("Entity {}", entity.id));
                state.undo_history.push(
                    UndoableOperation::EntityDeleted {
                        hierarchy,
                        deleted_entity: entity,
                    },
                    format!("Delete {}", entity_name),
                );
                despawn_recursive_immediate(world, entity);
                state.selection.clear();
                state.tree_cache.mark_dirty();
                state.project_modified = true;
                ui.close();
            }
        });
    }

    fn add_entity_internal(world: &mut World, state: &mut TreeRenderState, parent_entity: Entity) {
        let entities = EntityBuilder::new()
            .with_name(Name(String::new()))
            .with_local_transform(LocalTransform::default())
            .with_global_transform(GlobalTransform::default())
            .with_parent(Parent(Some(parent_entity)))
            .spawn(world, 1);
        let new_entity = entities[0];

        if let Some(name) = world.get_name_mut(new_entity) {
            *name = Name(format!("Entity {}", new_entity.id));
        }

        let hierarchy = Box::new(capture_hierarchy(world, new_entity));
        state.undo_history.push(
            UndoableOperation::EntityCreated {
                hierarchy,
                current_entity: new_entity,
            },
            format!("Create Entity {}", new_entity.id),
        );

        state.tree_cache.mark_dirty();
        state.selection.set_single(new_entity);
        state.project_modified = true;
    }
}