use std::any::TypeId;
use std::ops::RangeInclusive;
use bevy::color::Srgba;
use bevy::ecs::reflect::{AppTypeRegistry, ReflectComponent};
use bevy::prelude::*;
use bevy::reflect::attributes::CustomAttributes;
use bevy::reflect::enums::{DynamicEnum, DynamicVariant, VariantInfo};
use bevy::reflect::{GetPath, PartialReflect, Reflect, ReflectRef, TypeInfo, TypeRegistry};
use bevy::ui::Checked;
use bevy::ui_widgets::{ValueChange, observe};
use crate::choice::ChoiceValue;
use crate::color::ColorValue;
use crate::number::{NumberValue, VecChange, VecValue};
use crate::section::SectionId;
use crate::skin::Role;
use crate::text::{TextEditing, TextInputValue};
use crate::tooltip::Tooltip;
use crate::widgets::{
choice, color_field, label, number_field, section, text_input, toggle, vec_field,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum InspectTarget {
Component {
entity: Entity,
type_id: TypeId,
},
Resource {
type_id: TypeId,
},
}
impl InspectTarget {
pub fn component<T: Component + Reflect>(entity: Entity) -> Self {
Self::Component {
entity,
type_id: TypeId::of::<T>(),
}
}
pub fn resource<T: Resource + Reflect>() -> Self {
Self::Resource {
type_id: TypeId::of::<T>(),
}
}
fn type_id(self) -> TypeId {
match self {
Self::Component { type_id, .. } | Self::Resource { type_id } => type_id,
}
}
}
#[derive(Component, Clone, Debug)]
pub struct Inspector {
pub target: InspectTarget,
rows: Vec<Row>,
}
#[derive(Reflect, Clone, Copy, Debug, PartialEq)]
pub struct InspectStep(pub f32);
#[derive(EntityEvent, Clone, Debug, PartialEq, Eq)]
pub struct InspectorChange {
#[event_target]
pub entity: Entity,
pub path: String,
}
#[derive(Component, Clone, Debug, PartialEq, Eq)]
pub struct InspectRow {
pub inspector: Entity,
pub path: String,
}
#[derive(Clone, Debug)]
struct Row {
entity: Entity,
path: String,
kind: Kind,
}
#[derive(Clone, Debug, PartialEq)]
enum Kind {
Bool,
Number,
Choice(Vec<String>),
Vec(usize),
Colour,
Text,
ReadOnly(String),
}
enum Field {
Bool {
value: bool,
},
Number {
value: f32,
step: f32,
range: RangeInclusive<f32>,
},
Choice {
options: Vec<String>,
selected: usize,
},
Vec {
values: Vec<f32>,
},
Colour {
value: Srgba,
},
Text {
value: String,
},
Group {
fields: Vec<Named>,
},
ReadOnly {
text: String,
},
}
struct Named {
name: String,
path: String,
docs: Option<String>,
field: Field,
}
enum Edit {
Bool(bool),
Number(f64),
Variant(String),
Vec(Vec<f32>),
Colour(Srgba),
Text(String),
}
enum Update {
Bool(bool),
Number(f32),
Choice(usize),
Vec(Vec<f32>),
Colour(Srgba),
Text(String),
ReadOnly(String),
}
pub fn inspector(target: InspectTarget) -> impl Bundle {
(
Node {
flex_direction: FlexDirection::Column,
..default()
},
Role::Inspector,
Inspector {
target,
rows: Vec::new(),
},
)
}
fn locate(world: &World, target: InspectTarget) -> Option<Entity> {
match target {
InspectTarget::Component { entity, .. } => Some(entity),
InspectTarget::Resource { type_id } => {
let id = world.components().get_id(type_id)?;
world.resource_entities().get(id)
}
}
}
fn read<'w>(
world: &'w World,
registry: &TypeRegistry,
target: InspectTarget,
) -> Option<&'w dyn Reflect> {
let reflect = registry.get(target.type_id())?.data::<ReflectComponent>()?;
let entity = world.get_entity(locate(world, target)?).ok()?;
reflect.reflect(entity)
}
fn field_at<'v>(value: &'v dyn Reflect, path: &str) -> Option<&'v dyn PartialReflect> {
if path.is_empty() {
return Some(value.as_partial_reflect());
}
value.reflect_path(format!(".{path}").as_str()).ok()
}
fn humanise(name: &str) -> String {
let mut words = name.replace('_', " ");
if let Some(first) = words.get_mut(0..1) {
first.make_ascii_uppercase();
}
words
}
fn docs_of(docs: Option<&str>) -> Option<String> {
let text = docs?
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ");
(!text.is_empty()).then_some(text)
}
fn number_of(value: &dyn PartialReflect) -> Option<f64> {
macro_rules! numbers {
($($t:ty),*) => {
$(if let Some(n) = value.try_downcast_ref::<$t>() {
return Some(*n as f64);
})*
};
}
numbers!(f32, f64, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
None
}
fn is_integer(value: &dyn PartialReflect) -> bool {
macro_rules! integers {
($($t:ty),*) => { $(value.try_downcast_ref::<$t>().is_some())||* };
}
integers!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize)
}
fn set_number(value: &mut dyn PartialReflect, new: f64) -> Option<bool> {
macro_rules! floats {
($($t:ty),*) => {
$(if let Some(n) = value.try_downcast_mut::<$t>() {
let new = new as $t;
let changed = *n != new;
*n = new;
return Some(changed);
})*
};
}
macro_rules! integers {
($($t:ty),*) => {
$(if let Some(n) = value.try_downcast_mut::<$t>() {
let new = new.round() as $t;
let changed = *n != new;
*n = new;
return Some(changed);
})*
};
}
floats!(f32, f64);
integers!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
None
}
fn vector_of(value: &dyn PartialReflect) -> Option<Vec<f32>> {
if let Some(v) = value.try_downcast_ref::<Vec2>() {
return Some(v.to_array().to_vec());
}
if let Some(v) = value.try_downcast_ref::<Vec3>() {
return Some(v.to_array().to_vec());
}
value
.try_downcast_ref::<Vec4>()
.map(|v| v.to_array().to_vec())
}
fn colour_of(value: &dyn PartialReflect) -> Option<Srgba> {
if let Some(c) = value.try_downcast_ref::<Srgba>() {
return Some(*c);
}
value.try_downcast_ref::<Color>().map(|c| Srgba::from(*c))
}
fn unit_variants(value: &dyn PartialReflect) -> Option<Vec<String>> {
let ReflectRef::Enum(e) = value.reflect_ref() else {
return None;
};
let TypeInfo::Enum(info) = e.get_represented_type_info()? else {
return None;
};
info.iter()
.all(|v| matches!(v, VariantInfo::Unit(_)))
.then(|| info.iter().map(|v| v.name().to_string()).collect())
}
fn range_of(attributes: &CustomAttributes) -> RangeInclusive<f32> {
if let Some(r) = attributes.get::<RangeInclusive<f32>>() {
return *r.start()..=*r.end();
}
if let Some(r) = attributes.get::<RangeInclusive<f64>>() {
return *r.start() as f32..=*r.end() as f32;
}
if let Some(r) = attributes.get::<RangeInclusive<i32>>() {
return *r.start() as f32..=*r.end() as f32;
}
f32::NEG_INFINITY..=f32::INFINITY
}
fn describe(
value: &dyn PartialReflect,
attributes: Option<&CustomAttributes>,
path: &str,
) -> Field {
if let Some(value) = value.try_downcast_ref::<bool>() {
return Field::Bool { value: *value };
}
if let Some(number) = number_of(value) {
let range = attributes.map_or(f32::NEG_INFINITY..=f32::INFINITY, range_of);
let default_step = if is_integer(value) {
1.0
} else if range.start().is_finite() && range.end().is_finite() {
((range.end() - range.start()) / 100.0).max(f32::EPSILON)
} else {
0.1
};
let step = attributes
.and_then(|a| a.get::<InspectStep>())
.map_or(default_step, |s| s.0);
return Field::Number {
value: number as f32,
step,
range,
};
}
if let Some(values) = vector_of(value) {
return Field::Vec { values };
}
if let Some(value) = colour_of(value) {
return Field::Colour { value };
}
if let Some(value) = value.try_downcast_ref::<String>() {
return Field::Text {
value: value.clone(),
};
}
match value.reflect_ref() {
ReflectRef::Struct(s) => {
let info = s.get_represented_struct_info();
let fields = (0..s.field_len())
.filter_map(|i| {
let name = s.name_at(i)?;
let field = info.and_then(|info| info.field_at(i));
let path = join(path, name);
Some(Named {
name: humanise(name),
field: describe(
s.field_at(i)?,
field.map(|f| f.custom_attributes()),
&path,
),
docs: docs_of(field.and_then(|f| f.docs())),
path,
})
})
.collect();
Field::Group { fields }
}
ReflectRef::TupleStruct(t) => {
let info = t.get_represented_tuple_struct_info();
let fields = (0..t.field_len())
.filter_map(|i| {
let field = info.and_then(|info| info.field_at(i));
let path = join(path, &i.to_string());
Some(Named {
name: i.to_string(),
field: describe(t.field(i)?, field.map(|f| f.custom_attributes()), &path),
docs: docs_of(field.and_then(|f| f.docs())),
path,
})
})
.collect();
Field::Group { fields }
}
ReflectRef::Enum(e) => match unit_variants(value) {
Some(options) => {
let selected = e.variant_index();
Field::Choice { options, selected }
}
None => Field::ReadOnly {
text: format!("{value:?}"),
},
},
_ => Field::ReadOnly {
text: format!("{value:?}"),
},
}
}
fn join(path: &str, name: &str) -> String {
if path.is_empty() {
name.to_string()
} else {
format!("{path}.{name}")
}
}
pub fn build_inspectors(
added: Query<(Entity, &Inspector), Added<Inspector>>,
mut commands: Commands,
) {
for (root, _) in &added {
commands.queue(move |world: &mut World| build(world, root));
}
}
fn build(world: &mut World, root: Entity) {
let Some(target) = world.get::<Inspector>(root).map(|i| i.target) else {
return;
};
let registry = world.resource::<AppTypeRegistry>().clone();
let described = {
let registry = registry.read();
read(world, ®istry, target).map(|value| {
let field = describe(value.as_partial_reflect(), None, "");
(value.reflect_type_path().to_string(), field)
})
};
let mut rows = Vec::new();
match described {
Some((type_path, Field::Group { fields })) => {
for named in fields {
spawn_named(world, root, root, &type_path, named, &mut rows);
}
}
Some((type_path, field)) => {
let named = Named {
name: type_path.rsplit("::").next().unwrap_or("Value").to_string(),
path: String::new(),
docs: None,
field,
};
spawn_named(world, root, root, &type_path, named, &mut rows);
}
None => {
world.spawn((
label("Nothing to inspect: the type is not registered for reflection, or it is not there."),
ChildOf(root),
));
}
}
if let Some(mut inspector) = world.get_mut::<Inspector>(root) {
inspector.rows = rows;
}
}
fn spawn_named(
world: &mut World,
inspector: Entity,
parent: Entity,
type_path: &str,
named: Named,
rows: &mut Vec<Row>,
) {
let Named {
name,
path,
docs,
field,
} = named;
let row = InspectRow {
inspector,
path: path.clone(),
};
let (entity, kind) = match field {
Field::Bool { value } => (
world
.spawn((toggle(name, value), observe(on_bool), row))
.id(),
Kind::Bool,
),
Field::Number { value, step, range } => (
world
.spawn((
number_field(name, value, step, range),
observe(on_number),
row,
))
.id(),
Kind::Number,
),
Field::Choice { options, selected } => (
world
.spawn((
choice(name, options.clone(), selected),
observe(on_choice),
row,
))
.id(),
Kind::Choice(options),
),
Field::Vec { values } => {
let count = values.len();
let unbounded = f32::NEG_INFINITY..=f32::INFINITY;
let mut entity = match count {
2 => world.spawn(vec_field(name, [values[0], values[1]], 0.1, unbounded)),
3 => world.spawn(vec_field(
name,
[values[0], values[1], values[2]],
0.1,
unbounded,
)),
_ => world.spawn(vec_field(
name,
[values[0], values[1], values[2], values[3]],
0.1,
unbounded,
)),
};
entity.insert((observe(on_vec), row));
(entity.id(), Kind::Vec(count))
}
Field::Colour { value } => (
world
.spawn((color_field(name, value), observe(on_colour), row))
.id(),
Kind::Colour,
),
Field::Text { value } => (
world
.spawn((text_input(name, value), observe(on_text), row))
.id(),
Kind::Text,
),
Field::ReadOnly { text } => (
world.spawn((label(format!("{name}: {text}")), row)).id(),
Kind::ReadOnly(name),
),
Field::Group { fields } => {
let id = format!("inspect:{type_path}.{path}");
let group = world
.spawn((section(name, true, ()), SectionId::new(id)))
.id();
if let Some(docs) = docs {
world.entity_mut(group).insert(Tooltip::new(docs));
}
world.entity_mut(group).insert(ChildOf(parent));
let body = world
.get::<Children>(group)
.and_then(|kids| kids.get(1).copied());
if let Some(body) = body {
for named in fields {
spawn_named(world, inspector, body, type_path, named, rows);
}
}
return;
}
};
if let Some(docs) = docs {
world.entity_mut(entity).insert(Tooltip::new(docs));
}
world.entity_mut(entity).insert(ChildOf(parent));
rows.push(Row { entity, path, kind });
}
fn on_bool(change: On<ValueChange<bool>>, rows: Query<&InspectRow>, mut commands: Commands) {
ask(
&rows,
change.source,
Edit::Bool(change.value),
&mut commands,
);
}
fn on_number(change: On<ValueChange<f32>>, rows: Query<&InspectRow>, mut commands: Commands) {
ask(
&rows,
change.source,
Edit::Number(f64::from(change.value)),
&mut commands,
);
}
fn on_choice(
change: On<ValueChange<usize>>,
rows: Query<&InspectRow>,
inspectors: Query<&Inspector>,
mut commands: Commands,
) {
let Ok(row) = rows.get(change.source) else {
return;
};
let name = inspectors.get(row.inspector).ok().and_then(|i| {
i.rows
.iter()
.find(|r| r.entity == change.source)
.and_then(|r| match &r.kind {
Kind::Choice(options) => options.get(change.value).cloned(),
_ => None,
})
});
if let Some(name) = name {
ask(&rows, change.source, Edit::Variant(name), &mut commands);
}
}
fn on_vec(change: On<VecChange>, rows: Query<&InspectRow>, mut commands: Commands) {
ask(
&rows,
change.entity,
Edit::Vec(change.values.clone()),
&mut commands,
);
}
fn on_colour(change: On<ValueChange<Srgba>>, rows: Query<&InspectRow>, mut commands: Commands) {
ask(
&rows,
change.source,
Edit::Colour(change.value),
&mut commands,
);
}
fn on_text(change: On<ValueChange<String>>, rows: Query<&InspectRow>, mut commands: Commands) {
ask(
&rows,
change.source,
Edit::Text(change.value.clone()),
&mut commands,
);
}
fn ask(rows: &Query<&InspectRow>, row: Entity, edit: Edit, commands: &mut Commands) {
let Ok(row) = rows.get(row) else {
return;
};
let (inspector, path) = (row.inspector, row.path.clone());
commands.queue(move |world: &mut World| apply(world, inspector, path, edit));
}
fn apply(world: &mut World, inspector: Entity, path: String, edit: Edit) {
let Some(target) = world.get::<Inspector>(inspector).map(|i| i.target) else {
return;
};
let registry = world.resource::<AppTypeRegistry>().clone();
let Some(reflect) = registry
.read()
.get(target.type_id())
.and_then(|r| r.data::<ReflectComponent>())
.cloned()
else {
return;
};
let Some(holder) = locate(world, target) else {
return;
};
let Ok(mut entity) = world.get_entity_mut(holder) else {
return;
};
let Some(mut value) = reflect.reflect_mut(&mut entity) else {
return;
};
let changed = {
let root = value.bypass_change_detection();
let field = if path.is_empty() {
Some(root.as_partial_reflect_mut())
} else {
root.reflect_path_mut(format!(".{path}").as_str()).ok()
};
field.is_some_and(|field| write(field, edit))
};
if changed {
value.set_changed();
world.trigger(InspectorChange {
entity: inspector,
path,
});
}
}
fn write(field: &mut dyn PartialReflect, edit: Edit) -> bool {
match edit {
Edit::Bool(new) => field.try_downcast_mut::<bool>().is_some_and(|f| {
let changed = *f != new;
*f = new;
changed
}),
Edit::Number(new) => set_number(field, new).unwrap_or(false),
Edit::Text(new) => field.try_downcast_mut::<String>().is_some_and(|f| {
let changed = *f != new;
*f = new;
changed
}),
Edit::Colour(new) => {
if let Some(f) = field.try_downcast_mut::<Srgba>() {
let changed = *f != new;
*f = new;
changed
} else if let Some(f) = field.try_downcast_mut::<Color>() {
let new = Color::Srgba(new);
let changed = *f != new;
*f = new;
changed
} else {
false
}
}
Edit::Vec(new) => {
if let (Some(f), [x, y]) = (
field.try_downcast_mut::<Vec2>(),
new[..].try_into().unwrap_or([f32::NAN; 2]),
) {
let new = Vec2::new(x, y);
let changed = *f != new;
*f = new;
changed
} else if let (Some(f), [x, y, z]) = (
field.try_downcast_mut::<Vec3>(),
new[..].try_into().unwrap_or([f32::NAN; 3]),
) {
let new = Vec3::new(x, y, z);
let changed = *f != new;
*f = new;
changed
} else if let (Some(f), [x, y, z, w]) = (
field.try_downcast_mut::<Vec4>(),
new[..].try_into().unwrap_or([f32::NAN; 4]),
) {
let new = Vec4::new(x, y, z, w);
let changed = *f != new;
*f = new;
changed
} else {
false
}
}
Edit::Variant(name) => {
let same =
matches!(field.reflect_ref(), ReflectRef::Enum(e) if e.variant_name() == name);
if same {
return false;
}
let variant = DynamicEnum::new(name, DynamicVariant::Unit);
field.try_apply(&variant).is_ok()
}
}
}
pub fn sync_inspectors(world: &mut World, inspectors: &mut QueryState<(Entity, &Inspector)>) {
let Some(registry) = world.get_resource::<AppTypeRegistry>().cloned() else {
return;
};
let mut updates: Vec<(Entity, Update)> = Vec::new();
{
let registry = registry.read();
let world_ref: &World = world;
for (_, inspector) in inspectors.iter(world_ref) {
let Some(value) = read(world_ref, ®istry, inspector.target) else {
continue;
};
for row in &inspector.rows {
if let Some(update) = differs(world_ref, row, value) {
updates.push((row.entity, update));
}
}
}
}
for (row, update) in updates {
let Ok(mut entity) = world.get_entity_mut(row) else {
continue;
};
match update {
Update::Bool(on) => {
if on {
entity.insert(Checked);
} else {
entity.remove::<Checked>();
}
}
Update::Number(n) => {
entity.insert(NumberValue(n));
}
Update::Choice(i) => {
entity.insert(ChoiceValue(i));
}
Update::Vec(v) => {
entity.insert(VecValue(v));
}
Update::Colour(c) => {
entity.insert(ColorValue(c));
}
Update::Text(t) => {
entity.insert(TextInputValue(t));
}
Update::ReadOnly(text) => {
if let Some(mut t) = entity.get_mut::<Text>() {
t.0 = text;
}
}
}
}
}
fn differs(world: &World, row: &Row, target: &dyn Reflect) -> Option<Update> {
let field = field_at(target, &row.path)?;
let entity = world.get_entity(row.entity).ok()?;
match &row.kind {
Kind::Bool => {
let on = *field.try_downcast_ref::<bool>()?;
(entity.contains::<Checked>() != on).then_some(Update::Bool(on))
}
Kind::Number => {
let n = number_of(field)? as f32;
(entity.get::<NumberValue>()?.0 != n).then_some(Update::Number(n))
}
Kind::Choice(_) => {
let ReflectRef::Enum(e) = field.reflect_ref() else {
return None;
};
let index = e.variant_index();
(entity.get::<ChoiceValue>()?.0 != index).then_some(Update::Choice(index))
}
Kind::Vec(_) => {
let values = vector_of(field)?;
(entity.get::<VecValue>()?.0 != values).then_some(Update::Vec(values))
}
Kind::Colour => {
let c = colour_of(field)?;
let now = entity.get::<ColorValue>()?.0;
let far = [
(c.red, now.red),
(c.green, now.green),
(c.blue, now.blue),
(c.alpha, now.alpha),
]
.iter()
.any(|(a, b)| (a - b).abs() > 1e-4);
far.then_some(Update::Colour(c))
}
Kind::Text => {
if entity.contains::<TextEditing>() {
return None;
}
let text = field.try_downcast_ref::<String>()?;
(&entity.get::<TextInputValue>()?.0 != text).then(|| Update::Text(text.clone()))
}
Kind::ReadOnly(name) => {
let text = format!("{name}: {field:?}");
(entity.get::<Text>()?.0 != text).then_some(Update::ReadOnly(text))
}
}
}