use std::{any::TypeId, marker::PhantomData};
use bevy::{prelude::*, window::WindowId};
use bevy_egui::{egui, EguiContext, EguiPlugin};
use pretty_type_name::pretty_type_name_str;
use crate::{Context, Inspectable, InspectableRegistry};
#[allow(missing_debug_implementations)]
pub struct InspectorPlugin<T> {
marker: PhantomData<T>,
exclusive_access: bool,
initial_value: Option<Box<dyn Fn(&mut World) -> T + Send + Sync + 'static>>,
window_id: WindowId,
}
impl<T: Default + Send + Sync + 'static> Default for InspectorPlugin<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: FromWorld + Send + Sync + 'static> InspectorPlugin<T> {
pub fn new() -> Self {
InspectorPlugin {
exclusive_access: true,
marker: PhantomData,
initial_value: Some(Box::new(T::from_world)),
window_id: WindowId::primary(),
}
}
}
impl<T> InspectorPlugin<T> {
pub fn new_insert_manually() -> Self {
InspectorPlugin {
marker: PhantomData,
exclusive_access: true,
initial_value: None,
window_id: WindowId::primary(),
}
}
pub fn shared(self) -> Self {
InspectorPlugin {
exclusive_access: false,
..self
}
}
pub fn on_window(self, window_id: WindowId) -> Self {
InspectorPlugin { window_id, ..self }
}
}
#[derive(Clone)]
pub struct InspectorWindowData {
pub name: String,
pub window_id: WindowId,
pub visible: bool,
}
#[derive(Default)]
pub struct InspectorWindows(bevy::utils::HashMap<TypeId, InspectorWindowData>);
impl InspectorWindows {
fn insert<T: 'static>(&mut self, name: String, window_id: WindowId) {
let data = InspectorWindowData {
name,
window_id,
visible: true,
};
self.0.insert(TypeId::of::<T>(), data);
}
fn contains_id(&self, type_id: TypeId) -> bool {
self.0.iter().any(|(&id, _)| id == type_id)
}
fn contains_name(&self, name: &str) -> bool {
self.0.iter().any(|(_, data)| data.name == name)
}
#[track_caller]
pub fn window_data<T: 'static>(&self) -> &InspectorWindowData {
self.0
.get(&TypeId::of::<T>())
.ok_or_else(|| {
format!(
"inspector window `{}` not initialized",
std::any::type_name::<T>()
)
})
.unwrap()
}
#[track_caller]
pub fn window_data_mut<T: 'static>(&mut self) -> &mut InspectorWindowData {
self.0
.get_mut(&TypeId::of::<T>())
.ok_or_else(|| {
format!(
"inspector window `{}` not initialized",
std::any::type_name::<T>()
)
})
.unwrap()
}
}
impl<T> Plugin for InspectorPlugin<T>
where
T: Inspectable + Send + Sync + 'static,
{
fn build(&self, app: &mut App) {
if let Some(get_value) = &self.initial_value {
let world = &mut app.world;
let resource = get_value(world);
app.insert_resource(resource);
}
T::setup(app);
if self.exclusive_access {
app.add_system(exclusive_access_ui::<T>.exclusive_system());
} else {
app.add_system(shared_access_ui::<T>.exclusive_system());
}
if !app.world.contains_resource::<EguiContext>() {
app.add_plugin(EguiPlugin);
}
let world = &mut app.world;
world.get_resource_or_insert_with(InspectableRegistry::default);
let mut inspector_windows = world.get_resource_or_insert_with(InspectorWindows::default);
let type_id = TypeId::of::<T>();
let full_type_name = std::any::type_name::<T>();
if inspector_windows.contains_id(type_id) {
panic!(
"InspectorPlugin for {} is already registered",
full_type_name,
);
}
let type_name: String = pretty_type_name_str(full_type_name);
if inspector_windows.contains_name(&type_name) {
if inspector_windows.contains_name(full_type_name) {
panic!("two types with different type_id but same type_name");
} else {
inspector_windows.insert::<T>(full_type_name.into(), self.window_id);
}
} else {
inspector_windows.insert::<T>(type_name, self.window_id);
}
}
}
fn shared_access_ui<T>(
data: Option<ResMut<T>>,
egui_context: ResMut<EguiContext>,
inspector_windows: Res<InspectorWindows>,
) where
T: Inspectable + Send + Sync + 'static,
{
let mut data = match data {
Some(data) => data,
None => return,
};
let window_data = inspector_windows.window_data::<T>();
let ctx = egui_context.ctx_for_window(window_data.window_id);
if !window_data.visible {
return;
}
egui::Window::new(&window_data.name)
.resizable(false)
.vscroll(true)
.show(egui_context.ctx(), |ui| {
default_settings(ui);
let mut context = Context::new_shared(Some(ctx));
data.ui(ui, T::Attributes::default(), &mut context);
});
}
fn exclusive_access_ui<T>(world: &mut World)
where
T: Inspectable + Send + Sync + 'static,
{
world.resource_scope(|world, inspector_windows: Mut<InspectorWindows>| {
world.resource_scope(|world, mut data: Mut<T>| {
let window_data = inspector_windows.window_data::<T>();
if !window_data.visible {
return;
}
let egui_context = world.get_resource::<EguiContext>().unwrap();
let ctx = match egui_context.try_ctx_for_window(window_data.window_id) {
Some(ctx) => ctx.clone(),
None => return,
};
let mut context = Context::new_world_access(Some(&ctx), world);
#[allow(clippy::cast_ref_to_mut)]
let value = unsafe { &mut *(data.as_ref() as *const T as *mut T) };
let mut changed = false;
egui::Window::new(&window_data.name)
.resizable(false)
.vscroll(true)
.show(&ctx, |ui| {
default_settings(ui);
changed = value.ui(ui, T::Attributes::default(), &mut context);
});
if changed {
data.as_mut();
}
});
});
}
pub(crate) fn default_settings(ui: &mut egui::Ui) {
ui.style_mut().wrap = Some(false);
}