use bevy::{
ecs::{
archetype::Archetype,
component::{ComponentId, Components},
reflect::{AppTypeRegistry, ReflectComponent},
},
prelude::*,
reflect::serde::TypedReflectDeserializer,
};
use jackdaw_feathers::{
icons::{EditorFont, Icon, IconFont},
panel_header, tokens,
};
use jackdaw_widgets::collapsible::{
CollapsibleBody, CollapsibleHeader, CollapsibleSection, ToggleCollapsible,
};
use serde::de::DeserializeSeed;
use super::entity_browser::{
RemoteEntityProxy, RemoteProxyIndex, RemoteSceneCache, RemoteSelection,
};
use crate::inspector::{ComponentDisplay, InspectorGroupSection, component_display};
#[derive(Component)]
pub struct RemoteInspector;
#[derive(Component, Default)]
struct PopulatedComponents(Vec<ComponentId>);
#[derive(Component, Clone, Copy)]
struct PreviousRemoteSelection(Option<u64>);
#[derive(Component)]
pub struct RemoteInspectorNeedsRebuild {
proxy_entity: Entity,
fallback_components: Vec<(String, serde_json::Value)>,
}
pub fn remote_inspector() -> impl Bundle {
(
Node {
height: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
..Default::default()
},
BackgroundColor(tokens::PANEL_BG),
children![
panel_header::panel_header("Remote Inspector"),
(
RemoteInspector,
PopulatedComponents::default(),
PreviousRemoteSelection(None),
Node {
flex_direction: FlexDirection::Column,
row_gap: Val::Px(tokens::SPACING_SM),
overflow: Overflow::scroll_y(),
flex_grow: 1.0,
min_height: Val::Px(0.0),
padding: UiRect::all(Val::Px(tokens::SPACING_SM)),
..Default::default()
},
),
],
)
}
pub fn populate_remote_proxy(world: &mut World) {
let inspector_entity = {
let mut query = world.query_filtered::<Entity, With<RemoteInspector>>();
let Some(e) = query.iter(world).next() else {
return;
};
e
};
let current_selection = world.resource::<RemoteSelection>().selected;
let prev = world
.get::<PreviousRemoteSelection>(inspector_entity)
.map(|p| p.0);
if prev == Some(current_selection) {
return;
}
world
.entity_mut(inspector_entity)
.insert(PreviousRemoteSelection(current_selection));
cleanup_proxy_components(world, inspector_entity);
despawn_inspector_children(world, inspector_entity);
let Some(selected_bits) = current_selection else {
spawn_placeholder(world, inspector_entity, "No entity selected");
return;
};
let proxy_entity = {
let index = world.resource::<RemoteProxyIndex>();
index.map.get(&selected_bits).copied()
};
let Some(proxy_entity) = proxy_entity else {
spawn_placeholder(world, inspector_entity, "Proxy not found");
return;
};
let remote_components: Vec<(String, serde_json::Value)> = {
let cache = world.resource::<RemoteSceneCache>();
cache
.entities
.iter()
.find(|e| e.entity == selected_bits)
.map(|e| {
e.components
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
})
.unwrap_or_default()
};
if remote_components.is_empty() {
spawn_placeholder(world, inspector_entity, "No component data");
return;
}
let mut populated_ids: Vec<ComponentId> = Vec::new();
let mut fallback_components: Vec<(String, serde_json::Value)> = Vec::new();
{
let registry_arc = world.resource::<AppTypeRegistry>().clone();
let registry = registry_arc.read();
for (type_path, json_value) in &remote_components {
let Some(registration) = registry.get_with_type_path(type_path) else {
fallback_components.push((type_path.clone(), json_value.clone()));
continue;
};
let Some(reflect_component) = registration.data::<ReflectComponent>() else {
fallback_components.push((type_path.clone(), json_value.clone()));
continue;
};
let deserializer = TypedReflectDeserializer::new(registration, ®istry);
let Ok(reflected) = deserializer.deserialize(json_value) else {
fallback_components.push((type_path.clone(), json_value.clone()));
continue;
};
let insert_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut entity_mut = world.entity_mut(proxy_entity);
reflect_component.insert(&mut entity_mut, reflected.as_ref(), ®istry);
}));
if insert_result.is_ok() {
let type_id = registration.type_id();
if let Some(id) = world.components().get_id(type_id) {
populated_ids.push(id);
}
} else {
fallback_components.push((type_path.clone(), json_value.clone()));
}
}
}
world
.entity_mut(inspector_entity)
.insert(PopulatedComponents(populated_ids));
world
.entity_mut(inspector_entity)
.insert(RemoteInspectorNeedsRebuild {
proxy_entity,
fallback_components,
});
}
#[allow(clippy::too_many_arguments)]
pub fn build_remote_inspector_displays(
mut commands: Commands,
components: &Components,
type_registry: Res<AppTypeRegistry>,
names: Query<&Name>,
icon_font: Res<IconFont>,
editor_font: Res<EditorFont>,
inspector_query: Query<(Entity, &RemoteInspectorNeedsRebuild), With<RemoteInspector>>,
entity_query: Query<(&Archetype, EntityRef)>,
materials: Res<Assets<StandardMaterial>>,
) {
let Ok((inspector_entity, rebuild)) = inspector_query.single() else {
return;
};
let proxy_entity = rebuild.proxy_entity;
let fallback_components = rebuild.fallback_components.clone();
commands
.entity(inspector_entity)
.remove::<RemoteInspectorNeedsRebuild>();
let Ok((archetype, entity_ref)) = entity_query.get(proxy_entity) else {
return;
};
let empty_jsn_paths = std::collections::HashSet::new();
component_display::build_inspector_displays(
&mut commands,
components,
&type_registry,
proxy_entity,
archetype,
entity_ref,
inspector_entity,
1,
&names,
&icon_font,
&editor_font,
true, &materials,
&empty_jsn_paths,
);
if !fallback_components.is_empty() {
spawn_fallback_section(
&mut commands,
inspector_entity,
proxy_entity,
&fallback_components,
&icon_font,
&editor_font,
);
}
}
fn spawn_fallback_section(
commands: &mut Commands,
inspector_entity: Entity,
source_entity: Entity,
fallback_components: &[(String, serde_json::Value)],
icon_font: &IconFont,
editor_font: &EditorFont,
) {
let section = commands
.spawn((
ComponentDisplay,
InspectorGroupSection,
CollapsibleSection { collapsed: false },
Node {
flex_direction: FlexDirection::Column,
width: Val::Percent(100.0),
..Default::default()
},
ChildOf(inspector_entity),
))
.id();
let header = commands
.spawn((
CollapsibleHeader,
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
width: Val::Percent(100.0),
padding: UiRect::axes(Val::Px(tokens::SPACING_SM), Val::Px(tokens::SPACING_SM)),
column_gap: Val::Px(tokens::SPACING_SM),
..Default::default()
},
BackgroundColor(tokens::PANEL_BG),
ChildOf(section),
))
.id();
let section_for_toggle = section;
commands
.entity(header)
.observe(move |_: On<Pointer<Click>>, mut commands: Commands| {
commands.trigger(ToggleCollapsible {
entity: section_for_toggle,
});
});
commands.spawn((
Text::new(String::from(Icon::FileBraces.unicode())),
TextFont {
font: icon_font.0.clone(),
font_size: tokens::FONT_MD,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
ChildOf(header),
));
commands.spawn((
Text::new("Other Components (JSON)"),
TextFont {
font: editor_font.0.clone(),
font_size: tokens::FONT_MD,
weight: FontWeight::BOLD,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
ChildOf(header),
));
let group_body = commands
.spawn((
CollapsibleBody,
Node {
flex_direction: FlexDirection::Column,
width: Val::Percent(100.0),
border: UiRect::left(Val::Px(1.0)),
margin: UiRect::left(Val::Px(tokens::SPACING_MD)),
..Default::default()
},
BorderColor::all(tokens::BORDER_SUBTLE),
ChildOf(section),
))
.id();
for (type_path, json_value) in fallback_components {
let short_name = type_path
.rsplit("::")
.next()
.unwrap_or(type_path)
.to_string();
let (display_entity, body_entity) = component_display::spawn_component_display(
commands,
&short_name,
source_entity,
None,
&icon_font.0,
&editor_font.0,
false,
);
commands.entity(display_entity).insert(ChildOf(group_body));
let json_text =
serde_json::to_string_pretty(json_value).unwrap_or_else(|_| format!("{json_value}"));
commands.spawn((
Text::new(json_text),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
Node {
max_width: Val::Percent(100.0),
..Default::default()
},
ChildOf(body_entity),
));
}
}
fn spawn_placeholder(world: &mut World, inspector_entity: Entity, message: &str) {
world.spawn((
ComponentDisplay,
Text::new(message.to_string()),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
Node {
padding: UiRect::all(Val::Px(tokens::SPACING_MD)),
..Default::default()
},
ChildOf(inspector_entity),
));
}
fn cleanup_proxy_components(world: &mut World, inspector_entity: Entity) {
let component_ids: Vec<ComponentId> = world
.get::<PopulatedComponents>(inspector_entity)
.map(|p| p.0.clone())
.unwrap_or_default();
if component_ids.is_empty() {
return;
}
let proxies: Vec<Entity> = {
let mut query = world.query_filtered::<Entity, With<RemoteEntityProxy>>();
query.iter(world).collect()
};
for proxy in proxies {
for &comp_id in &component_ids {
world.entity_mut(proxy).remove_by_id(comp_id);
}
}
world
.entity_mut(inspector_entity)
.insert(PopulatedComponents::default());
}
fn despawn_inspector_children(world: &mut World, inspector_entity: Entity) {
let children: Vec<Entity> = world
.get::<Children>(inspector_entity)
.map(|c| c.iter().collect())
.unwrap_or_default();
for child in children {
if world.get::<ComponentDisplay>(child).is_some() {
if let Ok(ec) = world.get_entity_mut(child) {
ec.despawn();
}
}
}
}