use crate::{
asset::preview::cache::IconRequest,
fyrox::{
asset::manager::ResourceManager,
core::{
dyntype::DynTypeConstructorContainer,
log::{Log, MessageKind},
pool::{ErasedHandle, Handle},
reflect::prelude::*,
type_traits::prelude::*,
},
engine::SerializationContext,
graph::SceneGraph,
gui::{
border::BorderBuilder,
button::ButtonMessage,
grid::{Column, GridBuilder, Row},
inspector::InspectorContextArgs,
inspector::{
editors::PropertyEditorDefinitionContainer, InspectorBuilder, InspectorContext,
InspectorEnvironment, InspectorError, InspectorMessage,
},
message::{MessageDirection, UiMessage},
scroll_viewer::ScrollViewerBuilder,
stack_panel::StackPanelBuilder,
style::{resource::StyleResource, resource::StyleResourceExt, Style},
text::{TextBuilder, TextMessage},
widget::WidgetBuilder,
window::{WindowBuilder, WindowTitle},
BuildContext, Thickness, UiNode, UserInterface,
},
scene::SceneContainer,
},
load_image,
message::MessageSender,
plugin::EditorPlugin,
plugins::absm::animation_container_ref,
scene::{controller::SceneController, EntityInfo, GameScene, Selection},
ui_scene::UiScene,
utils::{make_square_image_button_with_tooltip, window_content},
Editor, Message, WidgetMessage, WrapMode,
};
use fyrox::gui::button::Button;
use fyrox::gui::stack_panel::StackPanel;
use fyrox::gui::text::Text;
use fyrox::gui::window::Window;
use std::{any::Any, sync::mpsc::Sender, sync::Arc};
pub mod editors;
pub mod handlers;
#[derive(Clone, Debug)]
pub struct AnimationDefinition {
name: String,
handle: ErasedHandle,
}
#[derive(ComponentProvider)]
pub struct EditorEnvironment {
pub resource_manager: ResourceManager,
pub serialization_context: Arc<SerializationContext>,
pub dyn_type_constructors: Arc<DynTypeConstructorContainer>,
pub available_animations: Vec<AnimationDefinition>,
pub sender: MessageSender,
pub icon_request_sender: Sender<IconRequest>,
#[component(include)]
pub style: Option<StyleResource>,
}
impl EditorEnvironment {
pub fn try_get_from(
environment: &Option<Arc<dyn InspectorEnvironment>>,
) -> Result<&Self, InspectorError> {
let environment = &**environment.as_ref().ok_or(InspectorError::Custom(
"Missing InspectorEnvironment".into(),
))?;
environment
.as_any()
.downcast_ref::<Self>()
.ok_or(InspectorError::Custom(format!(
"Expected InspectorEnvironment to be EditorEnvironment, found: {}",
environment.name(),
)))
}
}
impl InspectorEnvironment for EditorEnvironment {
fn name(&self) -> String {
format!("EditorEnvironment:{:?}", self.type_id())
}
fn as_any(&self) -> &dyn Any {
self
}
}
pub struct InspectorPlugin {
pub(crate) window: Handle<Window>,
pub inspector: Handle<fyrox::gui::inspector::Inspector>,
pub head: Handle<StackPanel>,
pub footer: Handle<UiNode>,
warning_text: Handle<Text>,
type_name_text: Handle<Text>,
docs_button: Handle<Button>,
clipboard: Option<Box<dyn Reflect>>,
}
fn fetch_available_animations(
selection: &Selection,
controller: &dyn SceneController,
scenes: &SceneContainer,
) -> Vec<AnimationDefinition> {
if let Some(ui_scene) = controller.downcast_ref::<UiScene>() {
if let Some(absm_selection) = selection.as_absm::<UiNode>() {
if let Some((_, animation_player)) =
animation_container_ref(&ui_scene.ui, absm_selection.absm_node_handle)
{
return animation_player
.pair_iter()
.map(|(handle, anim)| AnimationDefinition {
name: anim.name().to_string(),
handle: handle.into(),
})
.collect();
}
}
}
if let Some(game_scene) = controller.downcast_ref::<GameScene>() {
if let Some(absm_selection) = selection.as_absm() {
if let Some((_, animation_player)) = animation_container_ref(
&scenes[game_scene.scene].graph,
absm_selection.absm_node_handle,
) {
return animation_player
.pair_iter()
.map(|(handle, anim)| AnimationDefinition {
name: anim.name().to_string(),
handle: handle.into(),
})
.collect();
}
}
}
Default::default()
}
fn current_widget_style(
selection: &Selection,
controller: &dyn SceneController,
) -> Option<StyleResource> {
if let Some(ui_scene) = controller.downcast_ref::<UiScene>() {
if let Some(ui_selection) = selection.as_ui() {
return ui_scene
.ui
.try_get_node(ui_selection.widgets[0])
.ok()
.and_then(|n| n.style.clone());
}
}
None
}
fn print_errors(sync_errors: &[InspectorError]) {
for error in sync_errors {
Log::writeln(
MessageKind::Error,
format!("Failed to sync property. Reason: {error:?}"),
)
}
}
fn is_out_of_sync(sync_errors: &[InspectorError]) -> bool {
sync_errors
.iter()
.any(|err| matches!(err, &InspectorError::OutOfSync))
}
impl InspectorPlugin {
pub fn new(ctx: &mut BuildContext) -> Self {
let warning_text_str =
"Multiple objects are selected, showing properties of the first object only!\
Only common properties will be editable!";
let head = StackPanelBuilder::new(WidgetBuilder::new()).build(ctx);
let footer = BorderBuilder::new(WidgetBuilder::new().on_row(3))
.build(ctx)
.to_base();
let inspector =
InspectorBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(3.0)))
.build(ctx);
let content =
StackPanelBuilder::new(WidgetBuilder::new().with_child(head).with_child(inspector))
.build(ctx);
let warning_text;
let type_name_text;
let docs_button;
let window = WindowBuilder::new(WidgetBuilder::new().with_name("Inspector"))
.with_title(WindowTitle::text("Inspector"))
.with_tab_label("Inspector")
.with_content(
GridBuilder::new(
WidgetBuilder::new()
.with_child({
warning_text = TextBuilder::new(
WidgetBuilder::new()
.with_visibility(false)
.with_margin(Thickness::left(4.0))
.with_foreground(ctx.style.property(Style::BRUSH_ERROR))
.on_row(0),
)
.with_wrap(WrapMode::Word)
.with_text(warning_text_str)
.build(ctx);
warning_text
})
.with_child(
GridBuilder::new(
WidgetBuilder::new()
.on_row(1)
.with_child({
type_name_text = TextBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(4.0))
.on_row(0)
.on_column(0),
)
.with_wrap(WrapMode::Letter)
.build(ctx);
type_name_text
})
.with_child({
docs_button = make_square_image_button_with_tooltip(
ctx,
load_image!("../../../resources/doc.png"),
"Open Documentation",
Some(0),
);
ctx[docs_button].set_column(1);
docs_button
}),
)
.add_row(Row::auto())
.add_column(Column::stretch())
.add_column(Column::auto())
.build(ctx),
)
.with_child(
ScrollViewerBuilder::new(WidgetBuilder::new().on_row(2))
.with_content(content)
.build(ctx),
)
.with_child(footer),
)
.add_row(Row::auto())
.add_row(Row::auto())
.add_row(Row::stretch())
.add_row(Row::auto())
.add_column(Column::stretch())
.build(ctx),
)
.build(ctx);
Self {
window,
inspector,
head,
warning_text,
type_name_text,
docs_button,
clipboard: None,
footer,
}
}
fn sync_to(
&mut self,
obj: &dyn Reflect,
ui: &mut UserInterface,
) -> Result<(), Vec<InspectorError>> {
let ctx = ui[self.inspector].context().clone();
ctx.sync(obj, ui, 0, true, Default::default(), Default::default())
}
fn change_context(
&mut self,
obj: &dyn Reflect,
ui: &mut UserInterface,
resource_manager: ResourceManager,
serialization_context: Arc<SerializationContext>,
dyn_type_constructors: Arc<DynTypeConstructorContainer>,
available_animations: &[AnimationDefinition],
sender: &MessageSender,
icon_request_sender: Sender<IconRequest>,
has_parent_object: bool,
style: Option<StyleResource>,
property_editors: Arc<PropertyEditorDefinitionContainer>,
) {
let environment = Arc::new(EditorEnvironment {
resource_manager,
serialization_context,
available_animations: available_animations.to_vec(),
sender: sender.clone(),
icon_request_sender,
style,
dyn_type_constructors,
});
let context = InspectorContext::from_object(InspectorContextArgs {
object: obj,
ctx: &mut ui.build_ctx(),
definition_container: property_editors,
environment: Some(environment),
layer_index: 0,
generate_property_string_values: true,
filter: Default::default(),
name_column_width: 150.0,
base_path: Default::default(),
has_parent_object,
});
ui.send(self.inspector, InspectorMessage::Context(context));
ui.send_sync(
self.type_name_text,
TextMessage::Text(format!("Type Name: {}", obj.type_name())),
);
}
fn clear(&self, ui: &UserInterface) {
ui.send(
self.inspector,
InspectorMessage::Context(Default::default()),
);
}
}
impl EditorPlugin for InspectorPlugin {
fn on_sync_to_model(&mut self, editor: &mut Editor) {
let ui = editor.engine.user_interfaces.first_mut();
let entry = editor.scenes.current_scene_entry_mut();
let mut need_clear = true;
ui.send(
self.warning_text,
WidgetMessage::Visibility(entry.selection.len() > 1),
);
entry.selection.first_selected_entity(
&*entry.controller,
&editor.engine.scenes,
&mut |entity_info| {
let EntityInfo {
entity,
has_inheritance_parent,
read_only,
} = entity_info;
if let Err(errors) = self.sync_to(entity, ui) {
if is_out_of_sync(&errors) {
let available_animations = fetch_available_animations(
&entry.selection,
&*entry.controller,
&editor.engine.scenes,
);
let style = current_widget_style(&entry.selection, &*entry.controller);
self.change_context(
entity,
ui,
editor.engine.resource_manager.clone(),
editor.engine.serialization_context.clone(),
editor.engine.dyn_type_constructors.clone(),
&available_animations,
&editor.message_sender,
editor.asset_browser.preview_sender.clone(),
has_inheritance_parent,
style,
editor.property_editors.clone(),
);
need_clear = false;
} else {
print_errors(&errors);
}
} else {
need_clear = false;
}
for widget in [self.inspector.to_base::<UiNode>(), self.head.to_base()] {
ui.send(widget, WidgetMessage::Enabled(!read_only));
}
},
);
if need_clear {
self.clear(ui);
}
}
fn on_mode_changed(&mut self, editor: &mut Editor) {
let ui = editor.engine.user_interfaces.first();
ui.send(
window_content(self.window, ui),
WidgetMessage::Enabled(editor.mode.is_edit()),
);
}
fn on_ui_message(&mut self, message: &mut UiMessage, editor: &mut Editor) {
let entry = editor.scenes.current_scene_entry_mut();
if (message.destination() == self.inspector
|| editor
.engine
.user_interfaces
.first()
.is_node_child_of(message.destination(), self.inspector))
&& message.direction() == MessageDirection::FromWidget
{
if let Some(msg) = message.data::<InspectorMessage>() {
match msg {
InspectorMessage::CopyValue { path } => {
entry.selection.first_selected_entity(
&*entry.controller,
&editor.engine.scenes,
&mut |entity_info| {
entity_info.entity.resolve_path(path, &mut |result| {
if let Ok(result) = result {
self.clipboard = result.try_clone_box();
}
});
},
);
}
InspectorMessage::PasteValue { dest } => {
if let Some(value) = self.clipboard.as_ref() {
entry
.selection
.paste_property(dest, &**value, &editor.message_sender);
}
}
InspectorMessage::PropertyContextMenuOpened { path } => {
let mut can_paste = false;
let mut can_clone = false;
entry.selection.first_selected_entity(
&*entry.controller,
&editor.engine.scenes,
&mut |entity_info| {
entity_info.entity.resolve_path(path, &mut |result| {
if let Ok(property) = result {
can_clone = property.try_clone_box().is_some();
if let Some(value) = self.clipboard.as_ref() {
value.as_any(&mut |value| {
property.as_any(&mut |property| {
can_paste =
property.type_id() == value.type_id();
})
})
}
}
});
},
);
editor.engine.user_interfaces.first().send(
message.destination(),
InspectorMessage::PropertyContextMenuStatus {
can_clone,
can_paste,
},
)
}
_ => (),
}
}
}
if let Some(InspectorMessage::PropertyChanged(args)) =
message.data_from::<InspectorMessage>(self.inspector)
{
entry.selection.on_property_changed(
&mut *entry.controller,
args,
&mut editor.engine,
&editor.message_sender,
);
} else if let Some(ButtonMessage::Click) = message.data() {
if message.destination() == self.docs_button {
if let Some(doc) = entry
.selection
.provide_docs(&*entry.controller, &editor.engine)
{
editor.message_sender.send(Message::ShowDocumentation(doc));
}
}
}
}
}