From f8fba22c2b9b13ec00e0d39acd91a444d62c261b Mon Sep 17 00:00:00 2001 From: Matt Hunzinger Date: Fri, 29 Nov 2024 22:31:55 -0500 Subject: [PATCH] Create inspector module --- Cargo.lock | 3 + Cargo.toml | 11 +++- examples/ecs/inspector.rs | 26 +++++++++ src/inspector.rs | 116 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 ++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 examples/ecs/inspector.rs create mode 100644 src/inspector.rs diff --git a/Cargo.lock b/Cargo.lock index c1c55d7..9d1adf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,10 @@ dependencies = [ "bevy_ecs", "bevy_hierarchy", "bevy_math", + "bevy_reflect", + "bevy_text", "bevy_time", + "bevy_ui", "bevy_utils", "reqwest", "serde", diff --git a/Cargo.toml b/Cargo.toml index e22af39..44672c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,10 @@ repository = "https://github.com/actuate-rs/actuate" animation = ["ecs", "dep:bevy_math", "dep:bevy_time"] ecs = ["dep:bevy_app", "dep:bevy_ecs", "dep:bevy_hierarchy", "dep:bevy_utils"] executor = [] +inspector = ["dep:bevy_reflect", "dep:bevy_text", "dep:bevy_ui"] rt = ["executor", "tokio/rt-multi-thread"] tracing = ["dep:tracing"] -full = ["animation", "ecs", "rt", "tracing"] +full = ["animation", "ecs", "inspector", "rt", "tracing"] default = [] [workspace] @@ -27,7 +28,10 @@ bevy_app = { version = "0.15.0", optional = true } bevy_ecs = { version = "0.15.0", optional = true } bevy_hierarchy = { version = "0.15.0", optional = true } bevy_math = { version = "0.15.0", optional = true } +bevy_reflect = { version = "0.15.0", optional = true } +bevy_text = { version = "0.15.0", optional = true } bevy_time = { version = "0.15.0", optional = true } +bevy_ui = { version = "0.15.0", optional = true } bevy_utils = { version = "0.15.0", optional = true } slotmap = "1.0.7" thiserror = "2.0.3" @@ -63,6 +67,11 @@ name = "http" path = "examples/ecs/http.rs" required-features = ["ecs", "rt"] +[[example]] +name = "inspector" +path = "examples/ecs/inspector.rs" +required-features = ["inspector"] + [[example]] name = "timer" path = "examples/ecs/timer.rs" diff --git a/examples/ecs/inspector.rs b/examples/ecs/inspector.rs new file mode 100644 index 0000000..42fa136 --- /dev/null +++ b/examples/ecs/inspector.rs @@ -0,0 +1,26 @@ +// Inspector UI example. + +use actuate::{inspector::Inspector, prelude::*}; +use bevy::prelude::*; + +#[derive(Data)] +struct Example; + +impl Compose for Example { + fn compose(_cx: Scope) -> impl Compose { + Inspector {} + } +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + commands.spawn((Node::default(), Composition::new(Example))); +} + +fn main() { + App::new() + .add_plugins((DefaultPlugins, ActuatePlugin)) + .add_systems(Startup, setup) + .run(); +} diff --git a/src/inspector.rs b/src/inspector.rs new file mode 100644 index 0000000..f9a2eca --- /dev/null +++ b/src/inspector.rs @@ -0,0 +1,116 @@ +use crate::prelude::*; +use bevy_ecs::prelude::*; +use bevy_reflect::{NamedField, PartialReflect, ReflectFromPtr}; +use bevy_text::prelude::*; +use bevy_ui::prelude::*; + +#[derive(Data)] +struct FieldItem { + field: NamedField, + reflect: Box, +} + +#[derive(Data)] +struct Item { + name: String, + fields: Vec, +} + +impl PartialEq for Item { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +#[derive(Data)] +/// ECS inspector UI composable. +pub struct Inspector {} + +impl Compose for Inspector { + fn compose(cx: Scope) -> impl Compose { + let resources = use_mut(&cx, Vec::::new); + + use_world(&cx, move |world: &World| { + let mut new_resources = Vec::new(); + + for (info, ptr) in world.iter_resources() { + let type_registry = world.resource::().read(); + + let Some(type_id) = info.type_id() else { + continue; + }; + + let Some(registration) = type_registry.get(type_id) else { + continue; + }; + + let reflect_from_ptr = registration.data::().unwrap(); + let reflect = unsafe { reflect_from_ptr.as_reflect(ptr) }; + + let mut fields = Vec::new(); + + match reflect.reflect_ref() { + bevy_reflect::ReflectRef::Struct(dyn_struct) => { + let info = dyn_struct.get_represented_struct_info().unwrap(); + for (field_info, field) in info.iter().zip(dyn_struct.iter_fields()) { + fields.push(FieldItem { + field: field_info.clone(), + reflect: field.clone_value(), + }); + field.clone_value(); + } + } + _ => {} + } + + new_resources.push(Item { + name: info.name().to_owned(), + fields, + }); + } + + SignalMut::set_if_neq(resources, new_resources); + }); + + spawn(Node { + flex_direction: FlexDirection::Column, + ..Default::default() + }) + .content(( + spawn(Text::new("Resources")), + compose::from_iter(resources, |item| { + spawn(Node { + flex_direction: FlexDirection::Column, + margin: UiRect::left(Val::Px(10.)), + ..Default::default() + }) + .content(( + spawn(( + Text::new(item.name.to_string()), + TextFont { + font_size: 12., + ..Default::default() + }, + Node { + margin: UiRect::left(Val::Px(10.)), + ..Default::default() + }, + )), + compose::from_iter(Signal::map(item, |i| &i.fields), |item| { + spawn(( + Text::new(format!("{}: {:?}", item.field.name(), item.reflect)), + TextFont { + font_size: 10., + ..Default::default() + }, + Node { + margin: UiRect::left(Val::Px(20.)), + ..Default::default() + }, + )) + }), + )) + }), + )) + } +} diff --git a/src/lib.rs b/src/lib.rs index df5a489..f8b49d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,6 +172,11 @@ pub mod ecs; /// Task execution context. pub mod executor; +#[cfg(feature = "inspector")] +#[cfg_attr(docsrs, doc(cfg(feature = "inspector")))] +/// ECS inspector tool. +pub mod inspector; + /// Clone-on-write value. /// /// This represents either a borrowed or owned value.