use crate::{
border::BorderBuilder,
check_box::{CheckBox, CheckBoxBuilder},
core::{
algebra::Vector2,
err,
log::Log,
pool::{Handle, ObjectOrVariant},
reflect::{prelude::*, CastError, Reflect},
type_traits::prelude::*,
uuid_provider,
visitor::prelude::*,
},
expander::ExpanderBuilder,
formatted_text::WrapMode,
grid::{Column, GridBuilder, Row},
inspector::editors::{
PropertyEditorBuildContext, PropertyEditorDefinitionContainer, PropertyEditorInstance,
PropertyEditorMessageContext, PropertyEditorTranslationContext,
},
menu::{ContextMenuBuilder, MenuItem, MenuItemBuilder, MenuItemContent, MenuItemMessage},
message::{DeliveryMode, MessageData, MessageDirection, UiMessage},
popup::{Popup, PopupBuilder, PopupMessage},
stack_panel::{StackPanel, StackPanelBuilder},
text::{Text, TextBuilder},
utils::{make_arrow, make_simple_tooltip, ArrowDirection},
widget::{Widget, WidgetBuilder, WidgetMessage},
BuildContext, Control, RcUiNodeHandle, Thickness, UiNode, UserInterface, VerticalAlignment,
};
use copypasta::ClipboardProvider;
use fyrox_graph::{
constructor::{ConstructorProvider, GraphNodeConstructor},
SceneGraph,
};
use std::{
any::{Any, TypeId},
fmt::{Debug, Display, Formatter},
sync::Arc,
};
pub mod editors;
#[derive(Debug, Clone, PartialEq)]
pub enum CollectionAction {
Add(ObjectValue),
Remove(usize),
ItemChanged {
index: usize,
action: FieldAction,
},
}
impl MessageData for CollectionAction {}
#[derive(Debug, Clone)]
pub enum InheritableAction {
Revert,
}
#[derive(Debug, Clone)]
pub enum FieldAction {
CollectionAction(Box<CollectionAction>),
InspectableAction(Box<PropertyChanged>),
ObjectAction(ObjectValue),
InheritableAction(InheritableAction),
}
#[derive(Debug)]
pub enum PropertyAction {
Modify {
value: Box<dyn Reflect>,
},
AddItem {
value: Box<dyn Reflect>,
},
RemoveItem {
index: usize,
},
Revert,
}
impl Display for PropertyAction {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PropertyAction::Modify { value } => write!(
f,
"A property needs to be modified with given value: {value:?}"
),
PropertyAction::AddItem { value } => write!(
f,
"An item needs to be added to a collection property: {value:?}"
),
PropertyAction::RemoveItem { index } => write!(
f,
"An item needs to be removed from a collection property. Index: {index}"
),
PropertyAction::Revert => f.write_str("Revert value to parent"),
}
}
}
impl PropertyAction {
pub fn from_field_action(field_action: &FieldAction) -> Self {
match field_action {
FieldAction::ObjectAction(ref value) => Self::Modify {
value: value.clone().into_box_reflect(),
},
FieldAction::CollectionAction(ref collection_changed) => match **collection_changed {
CollectionAction::Add(ref value) => Self::AddItem {
value: value.clone().into_box_reflect(),
},
CollectionAction::Remove(index) => Self::RemoveItem { index },
CollectionAction::ItemChanged {
action: ref property,
..
} => Self::from_field_action(property),
},
FieldAction::InspectableAction(ref inspectable) => {
Self::from_field_action(&inspectable.action)
}
FieldAction::InheritableAction { .. } => Self::Revert,
}
}
#[allow(clippy::type_complexity)]
pub fn apply(
self,
path: &str,
target: &mut dyn Reflect,
result_callback: &mut dyn FnMut(Result<Option<Box<dyn Reflect>>, Self>),
) {
match self {
PropertyAction::Modify { value } => {
let mut value = Some(value);
target.resolve_path_mut(path, &mut |result| {
if let Ok(field) = result {
if let Err(value) = field.set(value.take().unwrap()) {
result_callback(Err(Self::Modify { value }))
} else {
result_callback(Ok(None))
}
} else {
result_callback(Err(Self::Modify {
value: value.take().unwrap(),
}))
}
});
}
PropertyAction::AddItem { value } => {
let mut value = Some(value);
target.resolve_path_mut(path, &mut |result| {
if let Ok(field) = result {
field.as_list_mut(&mut |result| {
if let Some(list) = result {
if let Err(value) = list.reflect_push(value.take().unwrap()) {
result_callback(Err(Self::AddItem { value }))
} else {
result_callback(Ok(None))
}
} else {
result_callback(Err(Self::AddItem {
value: value.take().unwrap(),
}))
}
})
} else {
result_callback(Err(Self::AddItem {
value: value.take().unwrap(),
}))
}
})
}
PropertyAction::RemoveItem { index } => target.resolve_path_mut(path, &mut |result| {
if let Ok(field) = result {
field.as_list_mut(&mut |result| {
if let Some(list) = result {
if let Some(value) = list.reflect_remove(index) {
result_callback(Ok(Some(value)))
} else {
result_callback(Err(Self::RemoveItem { index }))
}
} else {
result_callback(Err(Self::RemoveItem { index }))
}
})
} else {
result_callback(Err(Self::RemoveItem { index }))
}
}),
PropertyAction::Revert => {
result_callback(Err(Self::Revert))
}
}
}
}
pub trait Value: Reflect + Send {
fn clone_box(&self) -> Box<dyn Value>;
fn into_box_reflect(self: Box<Self>) -> Box<dyn Reflect>;
}
impl<T> Value for T
where
T: Reflect + Clone + Debug + Send,
{
fn clone_box(&self) -> Box<dyn Value> {
Box::new(self.clone())
}
fn into_box_reflect(self: Box<Self>) -> Box<dyn Reflect> {
Box::new(*self.into_any().downcast::<T>().unwrap())
}
}
#[derive(Debug)]
pub struct ObjectValue {
pub value: Box<dyn Value>,
}
impl Clone for ObjectValue {
fn clone(&self) -> Self {
Self {
value: self.value.clone_box(),
}
}
}
impl PartialEq for ObjectValue {
fn eq(&self, other: &Self) -> bool {
let ptr_a = &*self.value as *const _ as *const ();
let ptr_b = &*other.value as *const _ as *const ();
std::ptr::eq(ptr_a, ptr_b)
}
}
impl ObjectValue {
pub fn cast_value<T: 'static>(&self, func: &mut dyn FnMut(Option<&T>)) {
(*self.value).as_any(&mut |any| func(any.downcast_ref::<T>()))
}
pub fn cast_clone<T: Clone + 'static>(&self, func: &mut dyn FnMut(Option<T>)) {
(*self.value).as_any(&mut |any| func(any.downcast_ref::<T>().cloned()))
}
pub fn try_override<T: Clone + 'static>(&self, value: &mut T) -> bool {
let mut result = false;
(*self.value).as_any(&mut |any| {
if let Some(self_value) = any.downcast_ref::<T>() {
*value = self_value.clone();
result = true;
}
});
false
}
pub fn into_box_reflect(self) -> Box<dyn Reflect> {
self.value.into_box_reflect()
}
}
impl PartialEq for FieldAction {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(FieldAction::CollectionAction(l), FieldAction::CollectionAction(r)) => {
std::ptr::eq(&**l, &**r)
}
(FieldAction::InspectableAction(l), FieldAction::InspectableAction(r)) => {
std::ptr::eq(&**l, &**r)
}
(FieldAction::ObjectAction(l), FieldAction::ObjectAction(r)) => l == r,
_ => false,
}
}
}
impl FieldAction {
pub fn object<T: Value>(value: T) -> Self {
Self::ObjectAction(ObjectValue {
value: Box::new(value),
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PropertyChanged {
pub name: String,
pub action: FieldAction,
}
impl PropertyChanged {
pub fn path(&self) -> String {
let mut path = self.name.clone();
match self.action {
FieldAction::CollectionAction(ref collection_changed) => {
if let CollectionAction::ItemChanged {
action: ref property,
index,
} = **collection_changed
{
match property {
FieldAction::InspectableAction(inspectable) => {
path += format!("[{}].{}", index, inspectable.path()).as_ref();
}
_ => path += format!("[{index}]").as_ref(),
}
}
}
FieldAction::InspectableAction(ref inspectable) => {
path += format!(".{}", inspectable.path()).as_ref();
}
FieldAction::ObjectAction(_) | FieldAction::InheritableAction { .. } => {}
}
path
}
pub fn is_inheritable(&self) -> bool {
match self.action {
FieldAction::CollectionAction(ref collection_changed) => match **collection_changed {
CollectionAction::Add(_) => false,
CollectionAction::Remove(_) => false,
CollectionAction::ItemChanged {
action: ref property,
..
} => match property {
FieldAction::InspectableAction(inspectable) => inspectable.is_inheritable(),
FieldAction::InheritableAction(_) => true,
_ => false,
},
},
FieldAction::InspectableAction(ref inspectable) => inspectable.is_inheritable(),
FieldAction::ObjectAction(_) => false,
FieldAction::InheritableAction(_) => true,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum InspectorMessage {
Context(InspectorContext),
PropertyChanged(PropertyChanged),
PropertyContextMenuOpened {
path: String,
},
PropertyContextMenuStatus {
can_clone: bool,
can_paste: bool,
},
CopyValue {
path: String,
},
PasteValue {
dest: String,
},
}
impl MessageData for InspectorMessage {}
pub trait InspectorEnvironment: Any + Send + Sync + ComponentProvider {
fn name(&self) -> String;
fn as_any(&self) -> &dyn Any;
}
#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
#[reflect(derived_type = "UiNode")]
pub struct Inspector {
pub widget: Widget,
#[reflect(hidden)]
#[visit(skip)]
pub context: InspectorContext,
}
impl ConstructorProvider<UiNode, UserInterface> for Inspector {
fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
GraphNodeConstructor::new::<Self>().with_variant("Inspector", |ui| {
InspectorBuilder::new(WidgetBuilder::new().with_name("Inspector"))
.build(&mut ui.build_ctx())
.to_base()
.into()
})
}
}
crate::define_widget_deref!(Inspector);
impl Inspector {
pub fn handle_context_menu_message(
inspector: Handle<Inspector>,
message: &UiMessage,
ui: &mut UserInterface,
object: &mut dyn Reflect,
clipboard_value: &mut Option<Box<dyn Reflect>>,
) {
if let Some(inspector_message) = message.data::<InspectorMessage>() {
if ui.has_descendant_or_equal(message.destination(), inspector) {
Inspector::handle_context_menu_message_ex(
inspector,
inspector_message,
ui,
object,
clipboard_value,
);
}
}
}
pub fn handle_context_menu_message_ex(
inspector: Handle<Inspector>,
msg: &InspectorMessage,
ui: &mut UserInterface,
object: &mut dyn Reflect,
clipboard_value: &mut Option<Box<dyn Reflect>>,
) {
let object_type_name = object.type_name();
match msg {
InspectorMessage::PropertyContextMenuOpened { path } => {
let mut can_clone = false;
let mut can_paste = false;
object.resolve_path(path, &mut |result| {
if let Ok(field) = result {
can_clone = field.try_clone_box().is_some();
if let Some(clipboard_value) = clipboard_value {
clipboard_value.as_any(&mut |clipboard_value| {
field.as_any(&mut |field| {
can_paste = field.type_id() == clipboard_value.type_id();
})
});
}
}
});
ui.send(
inspector,
InspectorMessage::PropertyContextMenuStatus {
can_clone,
can_paste,
},
);
}
InspectorMessage::CopyValue { path } => {
object.resolve_path(path, &mut |field| {
if let Ok(field) = field {
if let Some(field) = field.try_clone_box() {
clipboard_value.replace(field);
} else {
err!(
"Unable to clone the field {}, because it is non-cloneable! \
Field type is: {}",
path,
field.type_name()
);
}
} else {
err!(
"There's no {} field in the object of type {}!",
path,
object_type_name
);
}
});
}
InspectorMessage::PasteValue { dest } => {
let mut pasted = false;
if let Some(value) = clipboard_value.as_ref() {
if let Some(value) = value.try_clone_box() {
let mut value = Some(value);
object.resolve_path_mut(dest, &mut |field| {
if let Ok(field) = field {
if field.set(value.take().unwrap()).is_err() {
err!(
"Unable to paste a value from the clipboard to the field {}, \
types don't match!",
dest
)
} else {
pasted = true;
}
} else {
err!(
"There's no {} field in the object of type {}!",
dest,
object_type_name
);
}
});
} else {
err!(
"Unable to clone the field {}, because it is non-cloneable! \
Field type is: {}",
dest,
value.type_name()
);
}
} else {
err!("Nothing to paste!");
}
if pasted {
if let Ok(inspector) = ui.try_get(inspector) {
let ctx = inspector.context.clone();
if let Err(errs) =
ctx.sync(object, ui, 0, true, Default::default(), Default::default())
{
for err in errs {
Log::err(err.to_string());
}
}
}
}
}
_ => (),
}
}
pub fn context(&self) -> &InspectorContext {
&self.context
}
fn find_property_container(
&self,
from: Handle<UiNode>,
ui: &UserInterface,
) -> Option<&ContextEntry> {
let mut parent_handle = from;
while let Ok(parent) = ui.try_get_node(parent_handle) {
for entry in self.context.entries.iter() {
if entry.property_container == parent_handle {
return Some(entry);
}
}
parent_handle = parent.parent;
}
None
}
}
pub const HEADER_MARGIN: Thickness = Thickness {
left: 2.0,
top: 1.0,
right: 4.0,
bottom: 1.0,
};
#[derive(Debug)]
pub enum InspectorError {
CastError(CastError),
OutOfSync,
Custom(String),
Group(Vec<InspectorError>),
}
impl std::error::Error for InspectorError {}
impl Display for InspectorError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
InspectorError::CastError(cast_error) => Display::fmt(cast_error, f),
InspectorError::OutOfSync => f.write_str(
"The object type has changed and the inspector context is no longer valid.",
),
InspectorError::Custom(message) => f.write_str(message),
InspectorError::Group(inspector_errors) => {
f.write_str("Multiple errors:\n")?;
for err in inspector_errors {
writeln!(f, " {err}")?;
}
Ok(())
}
}
}
}
impl From<CastError> for InspectorError {
fn from(e: CastError) -> Self {
Self::CastError(e)
}
}
#[derive(Clone, Debug)]
pub struct ContextEntry {
pub property_name: String,
pub property_display_name: String,
pub property_tag: String,
pub property_value_type_id: TypeId,
pub property_editor_definition_container: Arc<PropertyEditorDefinitionContainer>,
pub property_editor: Handle<UiNode>,
pub property_debug_output: String,
pub property_container: Handle<UiNode>,
pub property_path: String,
}
impl PartialEq for ContextEntry {
fn eq(&self, other: &Self) -> bool {
let ptr_a = &*self.property_editor_definition_container as *const _ as *const ();
let ptr_b = &*other.property_editor_definition_container as *const _ as *const ();
self.property_editor == other.property_editor
&& self.property_name == other.property_name
&& self.property_value_type_id ==other.property_value_type_id
&& std::ptr::eq(ptr_a, ptr_b)
}
}
#[derive(Default, Clone)]
pub struct Menu {
pub copy_value_as_string: Handle<MenuItem>,
pub copy_value: Handle<MenuItem>,
pub paste_value: Handle<MenuItem>,
pub menu: Option<RcUiNodeHandle>,
}
#[derive(Clone)]
pub struct InspectorContext {
pub stack_panel: Handle<StackPanel>,
pub menu: Menu,
pub entries: Vec<ContextEntry>,
pub property_definitions: Arc<PropertyEditorDefinitionContainer>,
pub environment: Option<Arc<dyn InspectorEnvironment>>,
pub object_type_id: TypeId,
pub name_column_width: f32,
pub has_parent_object: bool,
}
impl PartialEq for InspectorContext {
fn eq(&self, other: &Self) -> bool {
self.entries == other.entries
}
}
fn object_type_id(object: &dyn Reflect) -> TypeId {
let mut object_type_id = None;
object.as_any(&mut |any| object_type_id = Some(any.type_id()));
object_type_id.unwrap()
}
impl Default for InspectorContext {
fn default() -> Self {
Self {
stack_panel: Default::default(),
menu: Default::default(),
entries: Default::default(),
property_definitions: Arc::new(
PropertyEditorDefinitionContainer::with_default_editors(),
),
environment: None,
object_type_id: ().type_id(),
name_column_width: 150.0,
has_parent_object: false,
}
}
}
impl Debug for InspectorContext {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "InspectorContext")
}
}
pub fn make_property_margin(layer_index: usize) -> Thickness {
let mut margin = HEADER_MARGIN;
margin.left += 10.0 + layer_index as f32 * 10.0;
margin
}
fn make_expander_margin(layer_index: usize) -> Thickness {
let mut margin = HEADER_MARGIN;
margin.left += layer_index as f32 * 10.0;
margin
}
fn make_expander_check_box(
layer_index: usize,
property_name: &str,
property_description: &str,
ctx: &mut BuildContext,
) -> Handle<CheckBox> {
let description = if property_description.is_empty() {
property_name.to_string()
} else {
format!("{property_name}\n\n{property_description}")
};
let handle = CheckBoxBuilder::new(
WidgetBuilder::new()
.with_vertical_alignment(VerticalAlignment::Center)
.with_margin(make_expander_margin(layer_index)),
)
.with_background(
BorderBuilder::new(
WidgetBuilder::new()
.with_vertical_alignment(VerticalAlignment::Center)
.with_min_size(Vector2::new(4.0, 4.0)),
)
.with_stroke_thickness(Thickness::zero().into())
.build(ctx),
)
.with_content(
TextBuilder::new(
WidgetBuilder::new()
.with_opt_tooltip(make_tooltip(ctx, &description))
.with_height(16.0)
.with_margin(Thickness::left(2.0)),
)
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_text(property_name)
.build(ctx),
)
.checked(Some(true))
.with_check_mark(make_arrow(ctx, ArrowDirection::Bottom, 8.0))
.with_uncheck_mark(make_arrow(ctx, ArrowDirection::Right, 8.0))
.build(ctx);
ctx[handle].accepts_input = false;
handle
}
pub fn make_expander_container(
layer_index: usize,
property_name: &str,
description: &str,
header: Handle<impl ObjectOrVariant<UiNode>>,
content: Handle<impl ObjectOrVariant<UiNode>>,
width: f32,
ctx: &mut BuildContext,
) -> Handle<UiNode> {
ExpanderBuilder::new(WidgetBuilder::new())
.with_checkbox(make_expander_check_box(
layer_index,
property_name,
description,
ctx,
))
.with_expander_column(Column::strict(width))
.with_expanded(true)
.with_header(header)
.with_content(content)
.build(ctx)
.to_base()
}
fn create_header(ctx: &mut BuildContext, text: &str, layer_index: usize) -> Handle<Text> {
TextBuilder::new(WidgetBuilder::new().with_margin(make_property_margin(layer_index)))
.with_text(text)
.with_vertical_text_alignment(VerticalAlignment::Center)
.build(ctx)
}
fn make_tooltip(ctx: &mut BuildContext, text: &str) -> Option<RcUiNodeHandle> {
if text.is_empty() {
None
} else {
Some(make_simple_tooltip(ctx, text))
}
}
fn make_simple_property_container(
title: Handle<Text>,
editor: Handle<impl ObjectOrVariant<UiNode>>,
description: &str,
width: f32,
ctx: &mut BuildContext,
) -> Handle<UiNode> {
ctx[editor.to_base()].set_row(0).set_column(1);
let tooltip = make_tooltip(ctx, description);
ctx[title].set_tooltip(tooltip);
GridBuilder::new(WidgetBuilder::new().with_child(title).with_child(editor))
.add_row(Row::auto())
.add_columns(vec![Column::strict(width), Column::stretch()])
.build(ctx)
.to_base()
}
#[derive(Default, Clone)]
pub struct PropertyFilter(pub Option<Arc<dyn Fn(&dyn Reflect) -> bool + Send + Sync>>);
impl PropertyFilter {
pub fn new<T>(func: T) -> Self
where
T: Fn(&dyn Reflect) -> bool + 'static + Send + Sync,
{
Self(Some(Arc::new(func)))
}
pub fn pass(&self, value: &dyn Reflect) -> bool {
match self.0.as_ref() {
None => true,
Some(filter) => (filter)(value),
}
}
}
fn assign_tab_indices(container: Handle<impl ObjectOrVariant<UiNode>>, ui: &mut UserInterface) {
let mut counter = 0;
let mut widgets_list = Vec::new();
for (descendant_handle, descendant_ref) in ui.traverse_iter(container) {
if descendant_ref.accepts_input {
widgets_list.push((descendant_handle, counter));
counter += 1;
}
}
for (descendant, tab_index) in widgets_list {
ui.node_mut(descendant)
.tab_index
.set_value_and_mark_modified(Some(counter - tab_index));
}
}
pub struct InspectorContextArgs<'a, 'b, 'c> {
pub object: &'a dyn Reflect,
pub ctx: &'b mut BuildContext<'c>,
pub definition_container: Arc<PropertyEditorDefinitionContainer>,
pub environment: Option<Arc<dyn InspectorEnvironment>>,
pub layer_index: usize,
pub generate_property_string_values: bool,
pub filter: PropertyFilter,
pub name_column_width: f32,
pub base_path: String,
pub has_parent_object: bool,
}
impl InspectorContext {
pub fn from_object(context: InspectorContextArgs) -> Self {
let InspectorContextArgs {
object,
ctx,
definition_container,
environment,
layer_index,
generate_property_string_values,
filter,
name_column_width,
base_path,
has_parent_object,
} = context;
let mut entries = Vec::new();
let mut editors = Vec::new();
object.fields_ref(&mut |fields_ref| {
for (i, info) in fields_ref.iter().enumerate() {
let field_text = if generate_property_string_values {
format!("{:?}", info.value.field_value_as_reflect())
} else {
Default::default()
};
if !filter.pass(info.value.field_value_as_reflect()) {
continue;
}
let description = if info.doc.is_empty() {
info.display_name.to_string()
} else {
format!("{}\n\n{}", info.display_name, info.doc)
};
if let Some(definition) = definition_container
.definitions()
.get(&info.value.type_id())
{
let property_path = if base_path.is_empty() {
info.name.to_string()
} else {
format!("{}.{}", base_path, info.name)
};
let editor = match definition.property_editor.create_instance(
PropertyEditorBuildContext {
build_context: ctx,
property_info: info,
environment: environment.clone(),
definition_container: definition_container.clone(),
layer_index,
generate_property_string_values,
filter: filter.clone(),
name_column_width,
base_path: property_path.clone(),
has_parent_object,
},
) {
Ok(instance) => {
let (container, editor) = match instance {
PropertyEditorInstance::Simple { editor } => (
make_simple_property_container(
create_header(ctx, info.display_name, layer_index),
editor,
&description,
name_column_width,
ctx,
),
editor,
),
PropertyEditorInstance::Custom { container, editor } => {
(container, editor)
}
};
entries.push(ContextEntry {
property_editor: editor,
property_value_type_id: definition.property_editor.value_type_id(),
property_editor_definition_container: definition_container.clone(),
property_name: info.name.to_string(),
property_display_name: info.display_name.to_string(),
property_tag: info.tag.to_string(),
property_debug_output: field_text.clone(),
property_container: container,
property_path,
});
if info.read_only {
ctx[editor].set_enabled(false);
}
container
}
Err(e) => {
Log::err(format!(
"Unable to create property editor instance: Reason {e:?}"
));
make_simple_property_container(
create_header(ctx, info.display_name, layer_index),
TextBuilder::new(WidgetBuilder::new().on_row(i).on_column(1))
.with_wrap(WrapMode::Word)
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_text(format!(
"Unable to create property \
editor instance: Reason {e:?}"
))
.build(ctx),
&description,
name_column_width,
ctx,
)
}
};
editors.push(editor);
} else {
editors.push(make_simple_property_container(
create_header(ctx, info.display_name, layer_index),
TextBuilder::new(WidgetBuilder::new().on_row(i).on_column(1))
.with_wrap(WrapMode::Word)
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_text(format!(
"Property Editor Is Missing For Type {}!",
info.value.type_name()
))
.build(ctx),
&description,
name_column_width,
ctx,
));
}
}
});
let copy_value_as_string;
let copy_value;
let paste_value;
let menu = ContextMenuBuilder::new(
PopupBuilder::new(WidgetBuilder::new().with_visibility(false))
.with_content(
StackPanelBuilder::new(
WidgetBuilder::new()
.with_child({
copy_value_as_string = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Copy Value as String"))
.build(ctx);
copy_value_as_string
})
.with_child({
copy_value = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Copy Value"))
.build(ctx);
copy_value
})
.with_child({
paste_value = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Paste Value"))
.build(ctx);
paste_value
}),
)
.build(ctx),
)
.with_restrict_picking(false),
)
.build(ctx);
let menu = RcUiNodeHandle::new(menu, ctx.sender());
let stack_panel = StackPanelBuilder::new(
WidgetBuilder::new()
.with_context_menu(menu.clone())
.with_children(editors),
)
.build(ctx);
if layer_index == 0 {
assign_tab_indices(stack_panel, ctx.inner_mut());
}
Self {
stack_panel,
menu: Menu {
copy_value_as_string,
copy_value,
paste_value,
menu: Some(menu),
},
entries,
property_definitions: definition_container,
environment,
object_type_id: object_type_id(object),
name_column_width,
has_parent_object,
}
}
pub fn sync(
&self,
object: &dyn Reflect,
ui: &mut UserInterface,
layer_index: usize,
generate_property_string_values: bool,
filter: PropertyFilter,
base_path: String,
) -> Result<(), Vec<InspectorError>> {
if object_type_id(object) != self.object_type_id {
return Err(vec![InspectorError::OutOfSync]);
}
let mut sync_errors = Vec::new();
object.fields_ref(&mut |fields_ref| {
for info in fields_ref {
if !filter.pass(info.value.field_value_as_reflect()) {
continue;
}
if let Some(constructor) = self
.property_definitions
.definitions()
.get(&info.value.type_id())
{
if let Some(property_editor) = self.find_property_editor(info.name) {
let ctx = PropertyEditorMessageContext {
instance: property_editor.property_editor,
ui,
property_info: info,
definition_container: self.property_definitions.clone(),
layer_index,
environment: self.environment.clone(),
generate_property_string_values,
filter: filter.clone(),
name_column_width: self.name_column_width,
base_path: base_path.clone(),
has_parent_object: self.has_parent_object,
};
match constructor.property_editor.create_message(ctx) {
Ok(message) => {
if let Some(mut message) = message {
message.delivery_mode = DeliveryMode::SyncOnly;
ui.send_message(message);
}
}
Err(e) => sync_errors.push(e),
}
} else {
sync_errors.push(InspectorError::OutOfSync);
}
}
}
});
if layer_index == 0 {
if ui.is_valid_handle(self.stack_panel) {
assign_tab_indices(self.stack_panel, ui);
}
}
if sync_errors.is_empty() {
Ok(())
} else {
Err(sync_errors)
}
}
pub fn property_editors(&self) -> impl Iterator<Item = &ContextEntry> + '_ {
self.entries.iter()
}
pub fn find_property_editor(&self, name: &str) -> Option<&ContextEntry> {
self.entries.iter().find(|e| e.property_name == name)
}
pub fn find_property_editor_by_tag(&self, tag: &str) -> Option<&ContextEntry> {
self.entries.iter().find(|e| e.property_tag == tag)
}
pub fn find_property_editor_widget(&self, name: &str) -> Handle<UiNode> {
self.find_property_editor(name)
.map(|e| e.property_editor)
.unwrap_or_default()
}
}
uuid_provider!(Inspector = "c599c0f5-f749-4033-afed-1a9949c937a1");
impl Control for Inspector {
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.widget.handle_routed_message(ui, message);
if let Some(msg) = message.data_for::<InspectorMessage>(self.handle) {
match msg {
InspectorMessage::Context(ctx) => {
for child in self.children() {
ui.send(*child, WidgetMessage::Remove);
}
ui.send(ctx.stack_panel, WidgetMessage::LinkWith(self.handle));
self.context = ctx.clone();
}
InspectorMessage::PropertyContextMenuStatus {
can_clone,
can_paste,
} => {
ui.send(
self.context.menu.copy_value,
WidgetMessage::Enabled(*can_clone),
);
ui.send(
self.context.menu.paste_value,
WidgetMessage::Enabled(*can_paste),
);
}
_ => (),
}
}
if let Some(PopupMessage::RelayedMessage(popup_message)) = message.data() {
if let Some(mut clipboard) = ui.clipboard_mut() {
if let Some(MenuItemMessage::Click) = popup_message.data() {
if popup_message.destination() == self.context.menu.copy_value_as_string {
if let Some(entry) = self.find_property_container(message.destination(), ui)
{
Log::verify(
clipboard.set_contents(entry.property_debug_output.clone()),
);
}
} else if popup_message.destination() == self.context.menu.copy_value {
if let Some(entry) = self.find_property_container(message.destination(), ui)
{
ui.post(
self.handle,
InspectorMessage::CopyValue {
path: entry.property_path.clone(),
},
);
}
} else if popup_message.destination() == self.context.menu.paste_value {
if let Some(entry) = self.find_property_container(message.destination(), ui)
{
ui.post(
self.handle,
InspectorMessage::PasteValue {
dest: entry.property_path.clone(),
},
);
}
}
}
}
}
if message.delivery_mode != DeliveryMode::SyncOnly {
let env = self.context.environment.clone();
for entry in self.context.entries.iter() {
if message.destination() == entry.property_editor {
if let Some(args) = entry
.property_editor_definition_container
.definitions()
.get(&entry.property_value_type_id)
.and_then(|e| {
e.property_editor
.translate_message(PropertyEditorTranslationContext {
environment: env.clone(),
name: &entry.property_name,
message,
definition_container: self.context.property_definitions.clone(),
})
})
{
ui.post(self.handle, InspectorMessage::PropertyChanged(args));
}
}
}
}
}
fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
if let Some(PopupMessage::Open) = message.data() {
if let Some(menu) = self.context.menu.menu.clone() {
if message.direction() == MessageDirection::FromWidget
&& menu.handle() == message.destination()
{
if let Ok(popup) = ui.try_get_of_type::<Popup>(menu.handle()) {
if let Some(entry) = self.find_property_container(popup.owner, ui) {
ui.post(
self.handle,
InspectorMessage::PropertyContextMenuOpened {
path: entry.property_path.clone(),
},
);
}
}
}
}
}
}
}
pub struct InspectorBuilder {
widget_builder: WidgetBuilder,
context: InspectorContext,
}
impl InspectorBuilder {
pub fn new(widget_builder: WidgetBuilder) -> Self {
Self {
widget_builder,
context: Default::default(),
}
}
pub fn with_context(mut self, context: InspectorContext) -> Self {
self.context = context;
self
}
pub fn with_opt_context(mut self, context: Option<InspectorContext>) -> Self {
if let Some(context) = context {
self.context = context;
}
self
}
pub fn build(self, ctx: &mut BuildContext) -> Handle<Inspector> {
let canvas = Inspector {
widget: self
.widget_builder
.with_preview_messages(true)
.with_child(self.context.stack_panel)
.build(ctx),
context: self.context,
};
ctx.add(canvas)
}
}
#[cfg(test)]
mod test {
use crate::inspector::InspectorBuilder;
use crate::{test::test_widget_deletion, widget::WidgetBuilder};
#[test]
fn test_deletion() {
test_widget_deletion(|ctx| InspectorBuilder::new(WidgetBuilder::new()).build(ctx));
}
}