nightshade-editor 0.13.2

An interactive editor for the Nightshade game engine
pub mod animation;
pub mod audio;
pub mod bounding_volume;
pub mod camera;
pub mod character_controller;
pub mod decal;
pub mod grass;
pub mod helpers;
pub mod instanced_mesh;
pub mod light;
pub mod lines;
pub mod material;
pub mod mesh;
pub mod name;
pub mod navmesh;
pub mod navmesh_agent;
pub mod particle_emitter;
pub mod physics;
pub mod render_layer;
pub mod script;
pub mod text;
pub mod transform;
pub mod visibility;
pub mod water;

use super::selection::EntitySelection;
use super::undo::UndoHistory;
#[cfg(not(target_arch = "wasm32"))]
use crate::mosaic::ToastKind;
use animation::*;
use audio::*;
use bounding_volume::*;
use camera::*;
use character_controller::*;
use decal::*;
use grass::*;
use instanced_mesh::*;
use light::*;
use lines::*;
use material::*;
use mesh::*;
use name::*;
use navmesh_agent::*;
use nightshade::prelude::*;
use particle_emitter::*;
use physics::*;
use render_layer::*;
use script::*;
use text::*;
use transform::*;
use visibility::*;
use water::*;

#[derive(Clone)]
pub enum InspectorAction {
    LookupMaterial(String),
}

pub struct InspectorContext<'a> {
    pub transform_edit_pending: &'a mut Option<(Entity, LocalTransform)>,
    pub undo_history: &'a mut UndoHistory,
    #[cfg(not(target_arch = "wasm32"))]
    pub pending_notifications: &'a mut Vec<(ToastKind, String)>,
    pub actions: &'a mut Vec<InspectorAction>,
    pub selection: &'a EntitySelection,
}

pub trait ComponentInspector {
    fn name(&self) -> &str;
    fn has_component(&self, world: &World, entity: Entity) -> bool;
    fn add_component(&self, world: &mut World, entity: Entity);
    fn remove_component(&self, world: &mut World, entity: Entity);
    fn ui(
        &mut self,
        world: &mut World,
        entity: Entity,
        ui: &mut egui::Ui,
        context: &mut InspectorContext,
    );
}

macro_rules! impl_simple_inspector {
    ($struct_name:ident, $display_name:expr, $has:ident, $set:ident, $remove:ident, $default_val:expr, $ui_fn:path) => {
        pub struct $struct_name;

        impl ComponentInspector for $struct_name {
            fn name(&self) -> &str {
                $display_name
            }

            fn has_component(&self, world: &World, entity: Entity) -> bool {
                world.core.$has(entity)
            }

            fn add_component(&self, world: &mut World, entity: Entity) {
                world.core.$set(entity, $default_val);
            }

            fn remove_component(&self, world: &mut World, entity: Entity) {
                world.core.$remove(entity);
            }

            fn ui(
                &mut self,
                world: &mut World,
                entity: Entity,
                ui: &mut egui::Ui,
                context: &mut InspectorContext,
            ) {
                $ui_fn(world, entity, ui, context);
            }
        }
    };
    ($struct_name:ident, $display_name:expr, $has:ident, $set:ident, $remove:ident, $default_val:expr) => {
        pub struct $struct_name;

        impl ComponentInspector for $struct_name {
            fn name(&self) -> &str {
                $display_name
            }

            fn has_component(&self, world: &World, entity: Entity) -> bool {
                world.core.$has(entity)
            }

            fn add_component(&self, world: &mut World, entity: Entity) {
                world.core.$set(entity, $default_val);
            }

            fn remove_component(&self, world: &mut World, entity: Entity) {
                world.core.$remove(entity);
            }

            fn ui(
                &mut self,
                _world: &mut World,
                _entity: Entity,
                ui: &mut egui::Ui,
                _context: &mut InspectorContext,
            ) {
                ui.label(concat!($display_name, " (no inspector)"));
            }
        }
    };
    ($struct_name:ident, $display_name:expr, $has:ident, $remove:ident, $ui_fn:path, custom_add => $add_body:expr) => {
        pub struct $struct_name;

        impl ComponentInspector for $struct_name {
            fn name(&self) -> &str {
                $display_name
            }

            fn has_component(&self, world: &World, entity: Entity) -> bool {
                world.core.$has(entity)
            }

            fn add_component(&self, world: &mut World, entity: Entity) {
                $add_body(world, entity);
            }

            fn remove_component(&self, world: &mut World, entity: Entity) {
                world.core.$remove(entity);
            }

            fn ui(
                &mut self,
                world: &mut World,
                entity: Entity,
                ui: &mut egui::Ui,
                context: &mut InspectorContext,
            ) {
                $ui_fn(world, entity, ui, context);
            }
        }
    };
}

pub(crate) use impl_simple_inspector;

pub struct ComponentInspectorUi {
    inspectors: Vec<Box<dyn ComponentInspector>>,
}

impl Default for ComponentInspectorUi {
    fn default() -> Self {
        let inspectors: Vec<Box<dyn ComponentInspector>> = vec![
            Box::new(NameInspector),
            Box::new(TransformInspector::default()),
            Box::new(VisibilityInspector),
            Box::new(CameraInspector),
            Box::new(LightInspector),
            Box::new(LinesInspector),
            Box::new(MeshInspector),
            Box::new(InstancedMeshInspector),
            Box::new(MaterialInspector::default()),
            Box::new(BoundingVolumeInspector),
            Box::new(WaterInspector),
            Box::new(DecalInspector),
            Box::new(TextInspector),
            Box::new(RenderLayerInspector),
            Box::new(ParticleEmitterInspector),
            Box::new(AudioSourceInspector),
            Box::new(AudioListenerInspector),
            Box::new(GrassRegionInspector),
            Box::new(GrassInteractorInspector),
            Box::new(NavMeshAgentInspector),
            Box::new(AnimationInspector),
            Box::new(CharacterControllerInspector),
            Box::new(PhysicsInspector),
            Box::new(ScriptInspector::default()),
        ];

        Self { inspectors }
    }
}

impl ComponentInspectorUi {
    pub fn ui(
        &mut self,
        context: &mut InspectorContext,
        world: &mut World,
        ui: &mut egui::Ui,
    ) -> bool {
        let mut project_modified = false;
        let selection_count = context.selection.len();

        if selection_count > 1 {
            ui.vertical_centered(|ui| {
                ui.add_space(8.0);
                ui.label(format!("{} entities selected", selection_count));
                ui.add_space(8.0);
            });
            return false;
        }

        if let Some(entity) = context.selection.primary() {
            ui.group(|ui| {
                ui.horizontal(|ui| {
                    ui.label("Add Component:");
                    egui::ComboBox::new("add_component", "").show_ui(ui, |ui| {
                        for inspector in self.inspectors.iter() {
                            if !inspector.has_component(world, entity)
                                && ui.button(inspector.name()).clicked()
                            {
                                inspector.add_component(world, entity);
                                project_modified = true;
                            }
                        }
                    });
                });
            });

            ui.separator();

            for inspector in self.inspectors.iter_mut() {
                if inspector.has_component(world, entity) {
                    ui.group(|ui| {
                        inspector.ui(world, entity, ui, context);
                        if ui.button("Remove Component").clicked() {
                            inspector.remove_component(world, entity);
                            project_modified = true;
                        }
                    });
                    ui.separator();
                }
            }

            tags_metadata_section_ui(entity, world, ui);
            navmesh::navmesh_section_ui(context.selection, world, ui);
            navmesh::colliders_section_ui(context.selection, world, ui);
        } else {
            ui.vertical_centered(|ui| {
                ui.add_space(8.0);
                ui.label(
                    egui::RichText::new("No entity selected").color(egui::Color32::from_gray(128)),
                );
                ui.add_space(8.0);
            });
        }
        project_modified
    }
}

fn tags_metadata_section_ui(entity: Entity, world: &mut World, ui: &mut egui::Ui) {
    let has_tags = world.resources.entity_tags.contains_key(&entity);
    let has_metadata = world.resources.entity_metadata.contains_key(&entity);

    if !has_tags && !has_metadata {
        return;
    }

    ui.group(|ui| {
        if has_tags {
            ui.label(egui::RichText::new("Tags").strong());
            let tags = world.resources.entity_tags.get(&entity).cloned();
            if let Some(tags) = tags {
                let mut remove_index = None;
                for (index, tag) in tags.iter().enumerate() {
                    ui.horizontal(|ui| {
                        ui.label(tag);
                        if ui.small_button("\u{2715}").clicked() {
                            remove_index = Some(index);
                        }
                    });
                }
                if let Some(index) = remove_index
                    && let Some(entity_tags) = world.resources.entity_tags.get_mut(&entity)
                {
                    entity_tags.remove(index);
                    if entity_tags.is_empty() {
                        world.resources.entity_tags.remove(&entity);
                    }
                }
            }
            ui.add_space(4.0);
        }

        if has_metadata {
            ui.label(egui::RichText::new("Metadata").strong());
            let metadata = world.resources.entity_metadata.get(&entity).cloned();
            if let Some(metadata) = metadata {
                for (key, value) in &metadata {
                    ui.horizontal(|ui| {
                        ui.label(egui::RichText::new(key).monospace());
                        ui.label(format_metadata_value(value));
                    });
                }
            }
        }
    });
    ui.separator();
}

fn format_metadata_value(value: &nightshade::ecs::scene::MetadataValue) -> String {
    match value {
        nightshade::ecs::scene::MetadataValue::Bool(v) => v.to_string(),
        nightshade::ecs::scene::MetadataValue::Integer(v) => v.to_string(),
        nightshade::ecs::scene::MetadataValue::Float(v) => format!("{:.3}", v),
        nightshade::ecs::scene::MetadataValue::String(v) => format!("\"{}\"", v),
        nightshade::ecs::scene::MetadataValue::Array(arr) => {
            format!("[{} items]", arr.len())
        }
        nightshade::ecs::scene::MetadataValue::Map(map) => {
            format!("{{{} entries}}", map.len())
        }
    }
}