diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts
index 0183907..e66c83f 100644
--- a/studio-frontend/src/app/app.element.ts
+++ b/studio-frontend/src/app/app.element.ts
@@ -231,16 +231,30 @@ export class AppElement extends LitElement {
@space-change=${this.onTransformSpaceChange}
>
-
+
+
+
+
+ ${this.sceneObjects.map(obj => html`
+
+ `)}
+
+
` : nothing}
diff --git a/studio-frontend/src/app/inspector.element.ts b/studio-frontend/src/app/inspector.element.ts
index dcf244a..b8063ad 100644
--- a/studio-frontend/src/app/inspector.element.ts
+++ b/studio-frontend/src/app/inspector.element.ts
@@ -2,20 +2,250 @@ import { on } from "@storyteller/framework";
import { type InspectorDomChanged } from "dist/studio/studio";
import { LitElement, html, unsafeCSS, nothing, HTMLTemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
+import { property } from "lodash";
+
+//vdom example
+/// {
+///"subtree":0,
+/// "dirty_scopes":[],
+/// "templates":[{"name":"studio\\src\\ui\\inspector.rs:46:15:160899","roots":[{"type":"Element","tag":"div","namespace":null,"attrs":[],"children":[{"type":"Text","text":"Inspector"},{"type":"Element","tag":"div","namespace":null,"attrs":[],"children":[{"type":"Text","text":"hello world"}]}]}],"node_paths":[],"attr_paths":[]}],
+/// "edits":[{"type":"LoadTemplate","name":"studio\\src\\ui\\inspector.rs:46:15:160899","index":0,"id":1},{"type":"AppendChildren","id":0,"m":1}]}
+
@customElement("sts-inspector")
export class AppElement extends LitElement {
@state() vdom?: string | null;
+ @state() generation = 0;
+ @state() vdom_elements?: VdomElement[] | null;
- @on("window:inspector-dom-changed")
- onInspectorDomChanged({ detail: state}: InspectorDomChanged): void {
- console.log("inspector dom changed:", state);
- }
+ // @on("window:inspector-dom-changed")
+ // onInspectorDomChanged({ detail: state}: InspectorDomChanged): void {
+ // console.log("inspector dom changed:", state);
+ // //change vdom and activate render
+ // this.vdom = null;
+ // this.vdom = state;
+ // this.generation += 1;
- protected override render = () => html`
-
+ // // Activate the render function
+ // this.requestUpdate();
+ // }
-
- `;
+ protected override render() {
+ this.shadowRoot
+
+ return html`
+
+
+
+
+ `
+
+ //convert string vdom to json object
+ // if (this.vdom) {
+ // var vdom = JSON.parse(this.vdom);
+
+ // var templates = vdom["templates"];
+ // var edits = vdom["edits"];
+
+ // console.log("edits", edits);
+ // console.log("templates", templates);
+
+ // var element_stack = [];
+
+ // var element_tree = [];
+
+ // //loop through edits
+ // for (const edit of edits) {
+
+ // var type = edit["type"];
+ // if (type == "LoadTemplate") {
+ // var index = edit["index"];
+ // var template_name = edit["name"];
+ // var id = edit["id"];
+
+ // var target_root = null;
+ // for (const template of templates) {
+ // if (template["name"] == template_name) {
+ // target_root = template["roots"][index];
+ // break;
+ // }
+ // }
+ // if (target_root == null) {
+ // console.log("template not found:", template_name);
+ // continue;
+ // } else {
+ // target_root["id"] = id;
+ // element_stack.push(target_root);
+ // }
+ // } else if (type == "AppendChildren") {
+ // var id = edit["id"];
+ // var count = edit["m"];
+ // if (id == 0) {
+ // for (var i = 0; i < count; i++) {
+ // var current_element = element_stack.pop();
+ // element_tree.push(current_element);
+ // }
+ // } else {
+ // //TODO
+ // }
+ // } else if (type == "HydrateText") {
+ // var child_path = edit["path"];
+ // var target_node = element_stack[element_stack.length - 1];
+
+ // for (const path of child_path) {
+ // target_node = target_node["children"][path];
+ // }
+
+ // //add text node to target_node
+ // if (target_node["children"] == undefined) {
+ // target_node["children"] = [];
+ // }
+ // target_node["children"].push({type: "Text", text: edit["value"], id: edit["id"]});
+
+
+ // }
+ // }
+
+ // console.log("element tree", element_tree);
+
+ // //convert any to VdomElement in element_tree
+ // var vdom_elements = element_tree.map((element) => {
+ // return element as VdomElement;
+ // });
+
+ // this.vdom_elements = vdom_elements;
+
+ // return html`
+
+ //
+
+ //
+ // ${this.vdom_elements.map((element, index) => html``)}
+ //
`
+
+
+ // } else {
+ // //For each vdom element render InspectorElement
+ // return html`
+
+ // `
+ // }
+
+
+ }
+
+}
+
+export interface VdomElement {
+ id?: number;
+ type?: string;
+ text?: string;
+ tag?: string;
+ namespace?: string;
+ attrs?: any[];
+ children?: VdomElement[];
+}
+
+
+/// Element for recursive render vdom tree
+@customElement("sts-inspector-element")
+export class InspectorElement extends LitElement {
+ @property({ type: Object, attribute: true })
+ vdom: VdomElement;
+
+ @property({ type: Number, attribute: true })
+ generation: number;
+
+ protected override render() {
+ var tag = this.vdom["tag"];
+ var type = this.vdom.type;
+
+ if (type == "Text") {
+ return html`${this.vdom["text"]}
`
+ } else if (type == "Dynamic") {
+ return html`
+
+ ${this.vdom["children"].map((element) => html``)}
+
+ `
+ }
+
+ if (tag == "div") {
+ return html`
+
+
+
+ ${this.vdom["children"].map((element) => html``)}
+
`
+ } else {
+ return html`${this.vdom["text"]}
`
+ }
+ }
}
\ No newline at end of file
diff --git a/studio-frontend/src/dioxus-machine.ts b/studio-frontend/src/dioxus-machine.ts
new file mode 100644
index 0000000..f02fb0c
--- /dev/null
+++ b/studio-frontend/src/dioxus-machine.ts
@@ -0,0 +1,226 @@
+import { type InspectorDomChanged } from "dist/studio/studio";
+
+console.log("Hello, World!");
+
+
+function load_child(stack : any[], child_path : number[]) {
+ // iterate through each number and get that child
+ var node : any = stack[stack.length - 1];
+ for (const ptr of child_path) {
+ var steps = ptr;
+ var m = 0;
+ for (node = node.firstChild; m < steps; m++) {
+ node = node.nextSibling;
+ }
+ }
+ return node;
+}
+
+function laod_template(edit : any, stack : any[]) {
+
+}
+
+function apply_muts(
+ event : InspectorDomChanged
+) {
+
+ if (event.detail !== null) {
+
+ var lit_root = document.getElementById("inspector");
+ var shadowroot = lit_root?.shadowRoot;
+ var root = shadowroot?.getElementById("inspector");
+ console.log("root", root);
+ if (root) {
+ //remove all children
+ while (root.firstChild) {
+ root.removeChild(root.firstChild);
+ }
+ } else {
+ console.log("root not found");
+ return;
+ }
+
+ var vdom = JSON.parse(event.detail);
+ var templates = vdom["templates"];
+ var edits = vdom["edits"];
+
+ console.log("edits", edits);
+ console.log("templates", templates);
+
+ var element_stack = [];
+ var nodes : any = {}
+
+
+
+ nodes[0] = document.createElement("div");
+ root.appendChild(nodes[0]);
+ element_stack.push(root);
+
+ //loop through edits
+ for (const edit of edits) {
+
+ var type = edit["type"];
+ if (type == "LoadTemplate") {
+ // var index = edit["index"];
+ // var template_name = edit["name"];
+ // var id = edit["id"];
+
+ // var target_root = null;
+ // for (const template of templates) {
+ // if (template["name"] == template_name) {
+ // target_root = template["roots"][index];
+ // break;
+ // }
+ // }
+ // if (target_root == null) {
+ // console.log("template not found:", template_name);
+ // continue;
+ // } else {
+ // target_root["id"] = id;
+ // element_stack.push(target_root);
+ // }
+
+ var id = edit["id"];
+ var index = edit["index"];
+ var template_name = edit["name"];
+
+ var template_node = null;
+ for (const template of templates) {
+ if (template["name"] == template_name) {
+ template_node = template["roots"][index];
+ break;
+ }
+ }
+
+ if (template_node == null) {
+ console.log("template not found:", template_name);
+ continue;
+ } else {
+ var node = recursive_render(null, template_node);
+ nodes[id] = node;
+ element_stack.push(node);
+ }
+
+ } else if (type == "AppendChildren") {
+ // var id = edit["id"];
+ // var count = edit["m"];
+ // if (id == 0) {
+ // for (var i = 0; i < count; i++) {
+ // var current_element = element_stack.pop();
+ // element_tree.push(current_element);
+ // }
+ // } else {
+ // //TODO
+ // }
+
+ var id = edit["id"];
+ var many = edit["m"];
+
+ var target_node = nodes[id];
+ for (var i = 0; i < many; i++) {
+ var current_element = element_stack.pop();
+ target_node.appendChild(current_element);
+ }
+ } else if (type == "HydrateText") {
+ // var child_path = edit["path"];
+ // var target_node = element_stack[element_stack.length - 1];
+
+ // for (const path of child_path) {
+ // target_node = target_node["children"][path];
+ // }
+
+ // if (target_node == null) {
+ // console.log("target_node not found:", child_path);
+ // continue;
+ // }
+
+ // //add text node to target_node
+ // if (target_node["children"] == undefined) {
+ // target_node["children"] = [];
+ // }
+ // target_node["children"].push({type: "Text", text: edit["value"], id: edit["id"]});
+
+ var node : HTMLElement = load_child(element_stack, edit["path"]);
+ if (node) {
+ node.textContent = edit["value"];
+ } else {
+ console.log("node not found:", edit["path"]);
+ }
+ } else if (type == "ReplacePlaceholder") {
+ var child_path = edit["path"];
+ var manu = edit["m"];
+
+ var els = [];
+ for (var i = 0; i < manu; i++) {
+ var el = element_stack.pop();
+ els.push(el);
+ }
+ var target_node = load_child(element_stack, child_path);
+ target_node.replaceWith(...els);
+ console.log("")
+ }
+ }
+
+
+ } else {
+ console.log('Selection has been cleared.');
+ }
+}
+
+document.addEventListener('inspector-dom-changed', (event: CustomEvent) => {
+ // Cast the event to the InspectorDomChanged type to access its details in TypeScript
+ const customEvent = event as InspectorDomChanged;
+
+ // Now you can access the detail property, which is either a string or null
+ apply_muts(customEvent);
+});
+
+
+export interface VdomElement {
+ id?: number;
+ type?: string;
+ text?: string;
+ tag?: string;
+ namespace?: string;
+ attrs?: any[];
+ children?: VdomElement[];
+}
+
+function recursive_render(
+ root: HTMLElement | null,
+ element: VdomElement,
+) {
+ //create element
+ var tag = element["tag"];
+ if (element.type == "Text") {
+ tag = "div";
+ } else if (element.type == "Dynamic") {
+ tag = "div";
+ }
+
+ var new_element = document.createElement(element["tag"] as string);
+
+ //set text
+ if (element.text != undefined) {
+ new_element.textContent = element.text;
+ }
+
+ //set id
+ if (element.id != undefined) {
+ new_element.id = element.id.toString();
+ }
+
+ //parent to root
+ if (root != null) {
+ root.appendChild(new_element);
+ }
+
+ //render children
+ if (element["children"] != undefined) {
+ for (const child of element["children"]) {
+ recursive_render(new_element, child);
+ }
+ }
+
+ return new_element;
+}
\ No newline at end of file
diff --git a/studio-frontend/src/main.ts b/studio-frontend/src/main.ts
index cde5d45..538a170 100644
--- a/studio-frontend/src/main.ts
+++ b/studio-frontend/src/main.ts
@@ -1 +1,2 @@
import "./app/app.element";
+import "./dioxus-machine";
diff --git a/studio/Cargo.toml b/studio/Cargo.toml
index 7aac79c..898752d 100644
--- a/studio/Cargo.toml
+++ b/studio/Cargo.toml
@@ -41,6 +41,9 @@ space_editor = {git = "https://github.com/rewin123/space_editor", rev = "ce83f86
anyhow.workspace = true
dioxus = "0.4.3"
futures-util = "0.3.30"
+sledgehammer_bindgen = { version = "0.3.1", default-features = false, optional = true }
+sledgehammer_utils = { version = "0.2", optional = true }
+pretty-type-name = "1.0.1"
[features]
wasm = [
@@ -51,6 +54,8 @@ wasm = [
"dep:serde-wasm-bindgen",
"dep:web-sys",
"bevy_rapier3d/wasm-bindgen",
+ "dep:sledgehammer_bindgen",
+ "dep:sledgehammer_utils",
]
desktop = []
diff --git a/studio/inspector.ts b/studio/inspector.ts
new file mode 100644
index 0000000..0627df9
--- /dev/null
+++ b/studio/inspector.ts
@@ -0,0 +1,10 @@
+
+console.log("Hello, World!");
+
+var root;
+var node_stack = [];
+
+export function start() {
+ root = document.getElementById("inspector");
+ console.log("root", root);
+}
\ No newline at end of file
diff --git a/studio/scripts/copy-assets.mjs b/studio/scripts/copy-assets.mjs
index 17a0592..e950bac 100644
--- a/studio/scripts/copy-assets.mjs
+++ b/studio/scripts/copy-assets.mjs
@@ -16,6 +16,9 @@ export default async function main() {
let count = 0;
const start = performance.now();
+ //copy inspector.ts to dist
+ fs.copyFileSync(path.join(__dirname, "../inspector.ts"), path.join(__dirname, "../../dist/studio/inspector.ts"));
+
try {
/** @type string[] */
let ignore = [];
diff --git a/studio/src/ui/inspector.rs b/studio/src/ui/inspector.rs
index 8df8085..32799b7 100644
--- a/studio/src/ui/inspector.rs
+++ b/studio/src/ui/inspector.rs
@@ -1,193 +1,192 @@
use std::sync::{Arc, Mutex, RwLock};
-use bevy::{prelude::*, utils::hashbrown::HashMap, window::PrimaryWindow};
-use dioxus::{core::{ElementId, Mutations}, prelude::*};
+use bevy::{ecs::world::unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}, prelude::*, reflect::TypeData, utils::hashbrown::HashMap, window::PrimaryWindow};
+use dioxus::{prelude::*, rsx::*};
+use serde::{Deserialize, Serialize};
+use space_editor::{prelude::Selected, space_editor_ui::inspector::inspect};
use wasm_bindgen::JsCast;
use web_sys::HtmlElement;
use crate::{ui::inspector::wasm::notify_dom_changed, wasm::{StaticBuffer, WasmMessageBuffer}};
+use super::utils::{ComponentProp, InspectedEntity};
+
pub struct InspectorPlugin;
impl Plugin for InspectorPlugin {
fn build(&self, app: &mut App) {
- app.add_systems(Startup, init);
- app.add_systems(Update, apply_muts);
+ app.init_resource::();
+
+ app.add_systems(Update, show_selected);
+
+ app.world.resource_mut::().add_simple::();
+ app.world.resource_mut::().add_simple::();
}
}
-struct InspectorDom {
- dom: VirtualDom,
+#[derive(Resource, Default)]
+struct InspectorFunctions {
+ get_reflects: Vec) -> Option> + Send + Sync>>
}
-fn init(
- world: &mut World,
-) {
- let mut dom = VirtualDom::new(inspector);
-
- world.insert_non_send_resource(InspectorDom { dom });
-
- // let muts = dom.rebuild();
-
- // info!("{:?}", muts);
-
- // let json = serde_json::to_string(&muts).unwrap();
-
- // q_mutation_queue.edits.push(json.clone());
-
- // info!("{:?}", json);
-
- // #[cfg(feature = "wasm")]
- // {
- // wasm::MUTATION_BUFFER.write_message(json);
- // notify_dom_changed(q_window);
-}
-
-fn recursive_find(
- id : &str,
- element: &web_sys::Node,
-) -> Option {
- if let Some(el) = element.as_any().downcast_ref::() {
- if el.id() == *id {
- return Some(el.clone());
- }
- }
- let children = element.child_nodes();
-
- for i in 0..children.length() {
- if let Some(child) = children.get(i) {
- if let Some(element) = recursive_find(id, &child) {
- return Some(element);
- }
- }
+impl InspectorFunctions {
+ fn add_getter) -> Option> + Send + Sync + 'static>(&mut self, f: F) {
+ self.get_reflects.push(Box::new(f));
}
- None
-}
+ fn add_simple(&mut self) where T: bevy::prelude::Component + Reflect + FromReflect + 'static {
-fn apply_muts(
- mut dom : NonSendMut,
-) {
- if let Some(window) = web_sys::window() {
- window.capture_events();
- window.release_events();
- info!("apply_muts 1");
- if let Some(document) = window.document() {
-
- info!("apply_muts 2");
-
- if let Some(mut page_root) = document.get_elements_by_name("sts-inspector").item(0) {
- let mut page_root = page_root.dyn_into::().unwrap();
- info!("apply_muts 3");
-
- let dom = &mut dom.dom;
- let muts = dom.render_immediate();
-
- let mut element_stack = vec![];
-
- for m in muts.edits.iter() {
- match m {
- dioxus::core::Mutation::AppendChildren { id, m } => {
- for stack_idx in 0..*m {
- let (id, template) = element_stack.pop().unwrap();
- recursive_add_dom(
- &id,
- &template,
- &mut page_root,
- &document,
- );
- }
- },
- dioxus::core::Mutation::AssignId { path, id } => todo!(),
- dioxus::core::Mutation::CreatePlaceholder { id } => todo!(),
- dioxus::core::Mutation::CreateTextNode { value, id } => todo!(),
- dioxus::core::Mutation::HydrateText { path, value, id } => todo!(),
- dioxus::core::Mutation::LoadTemplate { name, index, id } => {
- let mut template = None;
- for t in muts.templates.iter() {
- if t.name == *name {
- template = Some(t);
- break;
- }
- }
-
- if let Some(template) = template {
- element_stack.push((id.clone(), template.roots[*index].clone()));
- }
- },
- dioxus::core::Mutation::ReplaceWith { id, m } => todo!(),
- dioxus::core::Mutation::ReplacePlaceholder { path, m } => todo!(),
- dioxus::core::Mutation::InsertAfter { id, m } => todo!(),
- dioxus::core::Mutation::InsertBefore { id, m } => todo!(),
- dioxus::core::Mutation::SetAttribute { name, value, id, ns } => todo!(),
- dioxus::core::Mutation::SetText { value, id } => todo!(),
- dioxus::core::Mutation::NewEventListener { name, id } => todo!(),
- dioxus::core::Mutation::RemoveEventListener { name, id } => todo!(),
- dioxus::core::Mutation::Remove { id } => todo!(),
- dioxus::core::Mutation::PushRoot { id } => todo!(),
+ unsafe {
+ self.add_getter(
+ |cell| {
+ if let Some(val) = cell.get::() {
+ Some(Box::new(::from_reflect(val).unwrap()) as Box)
+ } else {
+ None
}
}
+ )
+ }
+ }
+
+ fn get_reflect_copy(&mut self, cell: &UnsafeEntityCell<'_>) -> Vec> {
+ self.get_reflects.iter_mut().map(|f| (*f)(cell)).flatten().collect()
+ }
+}
+
+
+fn web_inspector(cx: Scope) -> dioxus::core::Element {
+
+ let e = cx.props;
+
+ let components = e.components.iter().map(|c: &Box| ComponentProp { component: c.clone_value() }).collect::>();
+ let components_rendered = components.iter().map(|c : &ComponentProp| {
+ rsx! {
+ web_reflect { component: c.component.clone_value() }
+ }
+ });
+
+ if e.id.is_none() {
+ render! {
+ div {
+ "No entity selected"
+ }
+ }
+ } else {
+ let id_name = format!("{:?}", e.id.unwrap());
+ render! {
+ div {
+ id_name,
+ components_rendered
}
}
}
}
-fn recursive_add_dom(
- element_id: &ElementId,
- template: &TemplateNode,
- parent: &mut web_sys::Element,
- document: &web_sys::Document,
-) {
- match *template {
- TemplateNode::Element { tag, namespace, attrs, children } => {
- let mut element = document.create_element(tag).unwrap();
- element.set_attribute("id", format!("element_{}", element_id.0).as_str()).unwrap();
-
- for attr in attrs {
- match *attr {
- TemplateAttribute::Static { name, value, namespace } => {
- element.set_attribute(name, value).unwrap();
- },
- TemplateAttribute::Dynamic { id } => {
- // TODO
- },
+fn web_reflect(cx: Scope) -> dioxus::core::Element {
+ let res = match cx.props.component.reflect_ref() {
+
+ bevy::reflect::ReflectRef::Struct(v) => {
+ let field_count = v.field_len();
+ render! {
+ div {
+ v.type_name().split("::").last().unwrap(),
+ (0..field_count).map(|i| {
+ let field = v.field_at(i).unwrap();
+ render! {
+ web_reflect { component: field.clone_value() }
+
+ }
+ })
+
}
}
+ },
+ bevy::reflect::ReflectRef::TupleStruct(_) => {
+ render! {
+ div {
- parent.append_child(&element).unwrap();
-
- for child in children {
- recursive_add_dom(element_id, child, &mut element, document);
+ }
}
},
- TemplateNode::Text { text } => {
- let text_node = document.create_element("p").unwrap();
- text_node.set_text_content(Some(text));
- parent.append_child(&text_node).unwrap();
+ bevy::reflect::ReflectRef::Tuple(_) => {
+ render! {
+ div {
+
+ }
+ }
},
- TemplateNode::Dynamic { id } => {
- todo!()
+ bevy::reflect::ReflectRef::List(_) => {
+ render! {
+ div {
+
+ }
+ }
},
- TemplateNode::DynamicText { id } => {
- todo!()
+ bevy::reflect::ReflectRef::Array(_) => {
+ render! {
+ div {
+
+ }
+ }
},
+ bevy::reflect::ReflectRef::Map(_) => {
+ render! {
+ div {
+
+ }
+ }
+ },
+ bevy::reflect::ReflectRef::Enum(_) => {
+ render! {
+ div {
+
+ }
+ }
+ },
+ bevy::reflect::ReflectRef::Value(_) => {
+ render! {
+ div {
+
+ }
+ }
+ },
+ };
+ return res;
+}
+
+
+fn show_selected(
+ world: &mut World
+) {
+ unsafe {
+ let cell = world.as_unsafe_world_cell();
+
+ let Ok(selected_entity) = cell.world_mut().query_filtered::>().get_single(cell.world()) else {
+ return;
+ };
+
+ let mut functions = cell.get_resource_mut::().unwrap();
+
+ let e = cell.get_entity(selected_entity).unwrap();
+
+ let mut cell = InspectedEntity::none();
+ cell.id = Some(e.id());
+ cell.components = functions.get_reflect_copy(&e);
+
+
+ let mut dom = VirtualDom::new_with_props(web_inspector, cell);
+
+ let muts = dom.rebuild();
+
+ let q_window = world.query_filtered::<&Window, With>().single(world);
+
+ let _ = notify_dom_changed(q_window, serde_json::to_string(&muts).unwrap());
+
}
}
-fn inspector(
- cx: Scope
-) -> Element {
-
- cx.render(rsx!{
- div {
- "Inspector",
- div {
- "hello world"
- }
- }
- })
-}
#[cfg(feature = "wasm")]
@@ -218,10 +217,11 @@ declare global {
"#;
pub(super) fn notify_dom_changed(
- q_window: Query<&Window, With>,
+ q_window: &Window,
+ msg : String,
) -> Result<(), String> {
- wasm::dispatch_to_js(q_window.single(), "inspector-dom-changed", &Option::::None)
+ wasm::dispatch_to_js(q_window, "inspector-dom-changed", &msg)
}
pub static MUTATION_BUFFER: WasmMessageBuffer = WasmMessageBuffer::new();
@@ -232,4 +232,4 @@ declare global {
MUTATION_BUFFER.take_message().unwrap_or("{}".to_string())
}
-}
\ No newline at end of file
+}
diff --git a/studio/src/ui/mod.rs b/studio/src/ui/mod.rs
index 32a9ef7..1e853c3 100644
--- a/studio/src/ui/mod.rs
+++ b/studio/src/ui/mod.rs
@@ -4,6 +4,7 @@ use self::fps::FpsCounterPlugin;
mod fps;
mod inspector;
+mod utils;
pub struct UiLayerPlugin;
diff --git a/studio/src/ui/utils.rs b/studio/src/ui/utils.rs
new file mode 100644
index 0000000..eaeab26
--- /dev/null
+++ b/studio/src/ui/utils.rs
@@ -0,0 +1,193 @@
+
+use bevy::prelude::*;
+use dioxus::prelude::*;
+
+#[derive(Props)]
+pub struct InspectedEntity {
+ pub id: Option,
+ pub components: Vec>,
+}
+
+#[derive(Props)]
+pub struct ComponentProp {
+ pub component: Box,
+}
+
+impl Clone for ComponentProp {
+ fn clone(&self) -> Self {
+ Self {
+ component: self.component.clone_value()
+ }
+ }
+}
+
+impl<'a> PartialEq for ComponentProp {
+ fn eq(&self, other: &Self) -> bool {
+ recursive_reflect_eq(self.component.as_ref(), other.component.as_ref())
+ }
+}
+
+pub fn recursive_reflect_eq(a : &dyn Reflect, b : &dyn Reflect) -> bool {
+ let ref_a = a.reflect_ref();
+ let ref_b = b.reflect_ref();
+
+ match ref_a {
+ bevy::reflect::ReflectRef::Struct(a) => {
+ let bevy::reflect::ReflectRef::Struct(b) = b.reflect_ref() else {
+ return false;
+ };
+
+ if a.field_len() != b.field_len() {
+ return false;
+ }
+
+ for i in 0..a.field_len() {
+ if !recursive_reflect_eq(a.field_at(i).unwrap(), b.field_at(i).unwrap()) {
+ return false;
+ }
+ }
+ },
+ bevy::reflect::ReflectRef::TupleStruct(a) => {
+ let bevy::reflect::ReflectRef::TupleStruct(b) = b.reflect_ref() else {
+ return false;
+ };
+
+ if a.field_len() != b.field_len() {
+ return false;
+ }
+
+ for i in 0..a.field_len() {
+ if !recursive_reflect_eq(a.field(i).unwrap(), b.field(i).unwrap()) {
+ return false;
+ }
+ }
+ },
+ bevy::reflect::ReflectRef::Tuple(a) => {
+ let bevy::reflect::ReflectRef::Tuple(b) = b.reflect_ref() else {
+ return false;
+ };
+
+ if a.field_len() != b.field_len() {
+ return false;
+ }
+
+ for i in 0..a.field_len() {
+ if !recursive_reflect_eq(a.field(i).unwrap(), b.field(i).unwrap()) {
+ return false;
+ }
+ }
+ },
+ bevy::reflect::ReflectRef::List(a) => {
+ let bevy::reflect::ReflectRef::List(b) = b.reflect_ref() else {
+ return false;
+ };
+
+ if a.len() != b.len() {
+ return false;
+ }
+
+ for i in 0..a.len() {
+ if !recursive_reflect_eq(a.get(i).unwrap(), b.get(i).unwrap()) {
+ return false;
+ }
+ }
+ },
+ bevy::reflect::ReflectRef::Array(a) => {
+ let bevy::reflect::ReflectRef::Array(b) = b.reflect_ref() else {
+ return false;
+ };
+
+ if a.len() != b.len() {
+ return false;
+ }
+
+ for i in 0..a.len() {
+ if !recursive_reflect_eq(a.get(i).unwrap(), b.get(i).unwrap()) {
+ return false;
+ }
+ }
+ },
+ bevy::reflect::ReflectRef::Map(a) => {
+ let bevy::reflect::ReflectRef::Map(b) = b.reflect_ref() else {
+ return false;
+ };
+
+ if a.len() != b.len() {
+ return false;
+ };
+
+ for i in 0..a.len() {
+ let (a_key, a_value) = a.get_at(i).unwrap();
+ let Some(b_value) = b.get(a_key) else {
+ return false;
+ };
+
+ if !recursive_reflect_eq(a_value, b_value) {
+ return false;
+ }
+ }
+ },
+ bevy::reflect::ReflectRef::Enum(a) => {
+ let bevy::reflect::ReflectRef::Enum(b) = b.reflect_ref() else {
+ return false;
+ };
+
+ if a.variant_name() != b.variant_name() {
+ return false;
+ }
+
+ if a.field_len() != b.field_len() {
+ return false;
+ }
+
+ for i in 0..a.field_len() {
+ if !recursive_reflect_eq(a.field_at(i).unwrap(), b.field_at(i).unwrap()) {
+ return false;
+ }
+ }
+ },
+ bevy::reflect::ReflectRef::Value(a) => {
+ let bevy::reflect::ReflectRef::Value(b) = b.reflect_ref() else {
+ return false;
+ };
+
+ if let Some(res) = a.reflect_partial_eq(b) {
+ return res;
+ } else {
+ return false;
+ }
+ },
+ }
+
+ false
+}
+
+impl PartialEq for InspectedEntity {
+ fn eq(&self, other: &Self) -> bool {
+ if self.id != other.id {
+ return false;
+ }
+
+ if self.components.len() != other.components.len() {
+ return false;
+ }
+
+ for i in 0..self.components.len() {
+
+ if !recursive_reflect_eq(self.components[i].as_ref(), other.components[i].as_ref()) {
+ return false;
+ }
+ }
+
+ true
+ }
+}
+
+impl InspectedEntity {
+ pub fn none() -> Self {
+ Self {
+ id: None,
+ components: Vec::new(),
+ }
+ }
+}
\ No newline at end of file