1use crate::{
26 border::BorderBuilder,
27 check_box::{CheckBox, CheckBoxBuilder},
28 core::{
29 algebra::Vector2,
30 err,
31 log::Log,
32 pool::{Handle, ObjectOrVariant},
33 reflect::{prelude::*, CastError, Reflect},
34 type_traits::prelude::*,
35 uuid_provider,
36 visitor::prelude::*,
37 },
38 expander::ExpanderBuilder,
39 formatted_text::WrapMode,
40 grid::{Column, GridBuilder, Row},
41 inspector::editors::{
42 PropertyEditorBuildContext, PropertyEditorDefinitionContainer, PropertyEditorInstance,
43 PropertyEditorMessageContext, PropertyEditorTranslationContext,
44 },
45 menu::{ContextMenuBuilder, MenuItem, MenuItemBuilder, MenuItemContent, MenuItemMessage},
46 message::{DeliveryMode, MessageData, MessageDirection, UiMessage},
47 popup::{Popup, PopupBuilder, PopupMessage},
48 stack_panel::{StackPanel, StackPanelBuilder},
49 text::{Text, TextBuilder},
50 utils::{make_arrow, make_simple_tooltip, ArrowDirection},
51 widget::{Widget, WidgetBuilder, WidgetMessage},
52 BuildContext, Control, RcUiNodeHandle, Thickness, UiNode, UserInterface, VerticalAlignment,
53};
54use copypasta::ClipboardProvider;
55use fyrox_graph::{
56 constructor::{ConstructorProvider, GraphNodeConstructor},
57 SceneGraph,
58};
59use std::{
60 any::{Any, TypeId},
61 fmt::{Debug, Display, Formatter},
62 sync::Arc,
63};
64
65pub mod editors;
66
67#[derive(Debug, Clone, PartialEq)]
70pub enum CollectionAction {
71 Add(ObjectValue),
73 Remove(usize),
75 ItemChanged {
77 index: usize,
79 action: FieldAction,
81 },
82}
83impl MessageData for CollectionAction {}
84
85#[derive(Debug, Clone)]
87pub enum InheritableAction {
88 Revert,
90}
91
92#[derive(Debug, Clone)]
94pub enum FieldAction {
95 CollectionAction(Box<CollectionAction>),
97 InspectableAction(Box<PropertyChanged>),
99 ObjectAction(ObjectValue),
101 InheritableAction(InheritableAction),
104}
105
106#[derive(Debug)]
108pub enum PropertyAction {
109 Modify {
111 value: Box<dyn Reflect>,
113 },
114 AddItem {
116 value: Box<dyn Reflect>,
118 },
119 RemoveItem {
121 index: usize,
123 },
124 Revert,
126}
127
128impl Display for PropertyAction {
129 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
130 match self {
131 PropertyAction::Modify { value } => write!(
132 f,
133 "A property needs to be modified with given value: {value:?}"
134 ),
135 PropertyAction::AddItem { value } => write!(
136 f,
137 "An item needs to be added to a collection property: {value:?}"
138 ),
139 PropertyAction::RemoveItem { index } => write!(
140 f,
141 "An item needs to be removed from a collection property. Index: {index}"
142 ),
143 PropertyAction::Revert => f.write_str("Revert value to parent"),
144 }
145 }
146}
147
148impl PropertyAction {
149 pub fn from_field_action(field_action: &FieldAction) -> Self {
153 match field_action {
154 FieldAction::ObjectAction(ref value) => Self::Modify {
155 value: value.clone().into_box_reflect(),
156 },
157 FieldAction::CollectionAction(ref collection_changed) => match **collection_changed {
158 CollectionAction::Add(ref value) => Self::AddItem {
159 value: value.clone().into_box_reflect(),
160 },
161 CollectionAction::Remove(index) => Self::RemoveItem { index },
162 CollectionAction::ItemChanged {
163 action: ref property,
164 ..
165 } => Self::from_field_action(property),
166 },
167 FieldAction::InspectableAction(ref inspectable) => {
168 Self::from_field_action(&inspectable.action)
169 }
170 FieldAction::InheritableAction { .. } => Self::Revert,
171 }
172 }
173
174 #[allow(clippy::type_complexity)]
176 pub fn apply(
177 self,
178 path: &str,
179 target: &mut dyn Reflect,
180 result_callback: &mut dyn FnMut(Result<Option<Box<dyn Reflect>>, Self>),
181 ) {
182 match self {
183 PropertyAction::Modify { value } => {
184 let mut value = Some(value);
185 target.resolve_path_mut(path, &mut |result| {
186 if let Ok(field) = result {
187 if let Err(value) = field.set(value.take().unwrap()) {
188 result_callback(Err(Self::Modify { value }))
189 } else {
190 result_callback(Ok(None))
191 }
192 } else {
193 result_callback(Err(Self::Modify {
194 value: value.take().unwrap(),
195 }))
196 }
197 });
198 }
199 PropertyAction::AddItem { value } => {
200 let mut value = Some(value);
201 target.resolve_path_mut(path, &mut |result| {
202 if let Ok(field) = result {
203 field.as_list_mut(&mut |result| {
204 if let Some(list) = result {
205 if let Err(value) = list.reflect_push(value.take().unwrap()) {
206 result_callback(Err(Self::AddItem { value }))
207 } else {
208 result_callback(Ok(None))
209 }
210 } else {
211 result_callback(Err(Self::AddItem {
212 value: value.take().unwrap(),
213 }))
214 }
215 })
216 } else {
217 result_callback(Err(Self::AddItem {
218 value: value.take().unwrap(),
219 }))
220 }
221 })
222 }
223 PropertyAction::RemoveItem { index } => target.resolve_path_mut(path, &mut |result| {
224 if let Ok(field) = result {
225 field.as_list_mut(&mut |result| {
226 if let Some(list) = result {
227 if let Some(value) = list.reflect_remove(index) {
228 result_callback(Ok(Some(value)))
229 } else {
230 result_callback(Err(Self::RemoveItem { index }))
231 }
232 } else {
233 result_callback(Err(Self::RemoveItem { index }))
234 }
235 })
236 } else {
237 result_callback(Err(Self::RemoveItem { index }))
238 }
239 }),
240 PropertyAction::Revert => {
241 result_callback(Err(Self::Revert))
243 }
244 }
245 }
246}
247
248pub trait Value: Reflect + Send {
250 fn clone_box(&self) -> Box<dyn Value>;
251
252 fn into_box_reflect(self: Box<Self>) -> Box<dyn Reflect>;
253}
254
255impl<T> Value for T
256where
257 T: Reflect + Clone + Debug + Send,
258{
259 fn clone_box(&self) -> Box<dyn Value> {
260 Box::new(self.clone())
261 }
262
263 fn into_box_reflect(self: Box<Self>) -> Box<dyn Reflect> {
264 Box::new(*self.into_any().downcast::<T>().unwrap())
265 }
266}
267
268#[derive(Debug)]
271pub struct ObjectValue {
272 pub value: Box<dyn Value>,
273}
274
275impl Clone for ObjectValue {
276 fn clone(&self) -> Self {
277 Self {
278 value: self.value.clone_box(),
279 }
280 }
281}
282
283impl PartialEq for ObjectValue {
284 fn eq(&self, other: &Self) -> bool {
285 let ptr_a = &*self.value as *const _ as *const ();
287 let ptr_b = &*other.value as *const _ as *const ();
288 std::ptr::eq(ptr_a, ptr_b)
290 }
291}
292
293impl ObjectValue {
294 pub fn cast_value<T: 'static>(&self, func: &mut dyn FnMut(Option<&T>)) {
295 (*self.value).as_any(&mut |any| func(any.downcast_ref::<T>()))
296 }
297
298 pub fn cast_clone<T: Clone + 'static>(&self, func: &mut dyn FnMut(Option<T>)) {
299 (*self.value).as_any(&mut |any| func(any.downcast_ref::<T>().cloned()))
300 }
301
302 pub fn try_override<T: Clone + 'static>(&self, value: &mut T) -> bool {
303 let mut result = false;
304 (*self.value).as_any(&mut |any| {
305 if let Some(self_value) = any.downcast_ref::<T>() {
306 *value = self_value.clone();
307 result = true;
308 }
309 });
310 false
311 }
312
313 pub fn into_box_reflect(self) -> Box<dyn Reflect> {
314 self.value.into_box_reflect()
315 }
316}
317
318impl PartialEq for FieldAction {
319 fn eq(&self, other: &Self) -> bool {
320 match (self, other) {
321 (FieldAction::CollectionAction(l), FieldAction::CollectionAction(r)) => {
322 std::ptr::eq(&**l, &**r)
323 }
324 (FieldAction::InspectableAction(l), FieldAction::InspectableAction(r)) => {
325 std::ptr::eq(&**l, &**r)
326 }
327 (FieldAction::ObjectAction(l), FieldAction::ObjectAction(r)) => l == r,
328 _ => false,
329 }
330 }
331}
332
333impl FieldAction {
334 pub fn object<T: Value>(value: T) -> Self {
335 Self::ObjectAction(ObjectValue {
336 value: Box::new(value),
337 })
338 }
339}
340
341#[derive(Debug, Clone, PartialEq)]
343pub struct PropertyChanged {
344 pub name: String,
346 pub action: FieldAction,
348}
349
350impl PropertyChanged {
351 pub fn path(&self) -> String {
352 let mut path = self.name.clone();
353 match self.action {
354 FieldAction::CollectionAction(ref collection_changed) => {
355 if let CollectionAction::ItemChanged {
356 action: ref property,
357 index,
358 } = **collection_changed
359 {
360 match property {
361 FieldAction::InspectableAction(inspectable) => {
362 path += format!("[{}].{}", index, inspectable.path()).as_ref();
363 }
364 _ => path += format!("[{index}]").as_ref(),
365 }
366 }
367 }
368 FieldAction::InspectableAction(ref inspectable) => {
369 path += format!(".{}", inspectable.path()).as_ref();
370 }
371 FieldAction::ObjectAction(_) | FieldAction::InheritableAction { .. } => {}
372 }
373 path
374 }
375
376 pub fn is_inheritable(&self) -> bool {
377 match self.action {
378 FieldAction::CollectionAction(ref collection_changed) => match **collection_changed {
379 CollectionAction::Add(_) => false,
380 CollectionAction::Remove(_) => false,
381 CollectionAction::ItemChanged {
382 action: ref property,
383 ..
384 } => match property {
385 FieldAction::InspectableAction(inspectable) => inspectable.is_inheritable(),
386 FieldAction::InheritableAction(_) => true,
387 _ => false,
388 },
389 },
390 FieldAction::InspectableAction(ref inspectable) => inspectable.is_inheritable(),
391 FieldAction::ObjectAction(_) => false,
392 FieldAction::InheritableAction(_) => true,
393 }
394 }
395}
396
397#[derive(Debug, Clone, PartialEq)]
399pub enum InspectorMessage {
400 Context(InspectorContext),
402 PropertyChanged(PropertyChanged),
405 PropertyContextMenuOpened {
407 path: String,
409 },
410 PropertyContextMenuStatus {
412 can_clone: bool,
414 can_paste: bool,
416 },
417 CopyValue {
418 path: String,
420 },
421 PasteValue {
426 dest: String,
428 },
429}
430impl MessageData for InspectorMessage {}
431
432pub trait InspectorEnvironment: Any + Send + Sync + ComponentProvider {
440 fn name(&self) -> String;
441 fn as_any(&self) -> &dyn Any;
442}
443
444#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
526#[reflect(derived_type = "UiNode")]
527pub struct Inspector {
528 pub widget: Widget,
529 #[reflect(hidden)]
530 #[visit(skip)]
531 pub context: InspectorContext,
532}
533
534impl ConstructorProvider<UiNode, UserInterface> for Inspector {
535 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
536 GraphNodeConstructor::new::<Self>().with_variant("Inspector", |ui| {
537 InspectorBuilder::new(WidgetBuilder::new().with_name("Inspector"))
538 .build(&mut ui.build_ctx())
539 .to_base()
540 .into()
541 })
542 }
543}
544
545crate::define_widget_deref!(Inspector);
546
547impl Inspector {
548 pub fn handle_context_menu_message(
549 inspector: Handle<Inspector>,
550 message: &UiMessage,
551 ui: &mut UserInterface,
552 object: &mut dyn Reflect,
553 clipboard_value: &mut Option<Box<dyn Reflect>>,
554 ) {
555 if let Some(inspector_message) = message.data::<InspectorMessage>() {
556 if ui.has_descendant_or_equal(message.destination(), inspector) {
557 Inspector::handle_context_menu_message_ex(
558 inspector,
559 inspector_message,
560 ui,
561 object,
562 clipboard_value,
563 );
564 }
565 }
566 }
567
568 pub fn handle_context_menu_message_ex(
569 inspector: Handle<Inspector>,
570 msg: &InspectorMessage,
571 ui: &mut UserInterface,
572 object: &mut dyn Reflect,
573 clipboard_value: &mut Option<Box<dyn Reflect>>,
574 ) {
575 let object_type_name = object.type_name();
576
577 match msg {
578 InspectorMessage::PropertyContextMenuOpened { path } => {
579 let mut can_clone = false;
580 let mut can_paste = false;
581
582 object.resolve_path(path, &mut |result| {
583 if let Ok(field) = result {
584 can_clone = field.try_clone_box().is_some();
585
586 if let Some(clipboard_value) = clipboard_value {
587 clipboard_value.as_any(&mut |clipboard_value| {
588 field.as_any(&mut |field| {
589 can_paste = field.type_id() == clipboard_value.type_id();
590 })
591 });
592 }
593 }
594 });
595
596 ui.send(
597 inspector,
598 InspectorMessage::PropertyContextMenuStatus {
599 can_clone,
600 can_paste,
601 },
602 );
603 }
604 InspectorMessage::CopyValue { path } => {
605 object.resolve_path(path, &mut |field| {
606 if let Ok(field) = field {
607 if let Some(field) = field.try_clone_box() {
608 clipboard_value.replace(field);
609 } else {
610 err!(
611 "Unable to clone the field {}, because it is non-cloneable! \
612 Field type is: {}",
613 path,
614 field.type_name()
615 );
616 }
617 } else {
618 err!(
619 "There's no {} field in the object of type {}!",
620 path,
621 object_type_name
622 );
623 }
624 });
625 }
626 InspectorMessage::PasteValue { dest } => {
627 let mut pasted = false;
628
629 if let Some(value) = clipboard_value.as_ref() {
630 if let Some(value) = value.try_clone_box() {
631 let mut value = Some(value);
632 object.resolve_path_mut(dest, &mut |field| {
633 if let Ok(field) = field {
634 if field.set(value.take().unwrap()).is_err() {
635 err!(
636 "Unable to paste a value from the clipboard to the field {}, \
637 types don't match!",
638 dest
639 )
640 } else {
641 pasted = true;
642 }
643 } else {
644 err!(
645 "There's no {} field in the object of type {}!",
646 dest,
647 object_type_name
648 );
649 }
650 });
651 } else {
652 err!(
653 "Unable to clone the field {}, because it is non-cloneable! \
654 Field type is: {}",
655 dest,
656 value.type_name()
657 );
658 }
659 } else {
660 err!("Nothing to paste!");
661 }
662
663 if pasted {
664 if let Ok(inspector) = ui.try_get(inspector) {
665 let ctx = inspector.context.clone();
666
667 if let Err(errs) =
668 ctx.sync(object, ui, 0, true, Default::default(), Default::default())
669 {
670 for err in errs {
671 Log::err(err.to_string());
672 }
673 }
674 }
675 }
676 }
677 _ => (),
678 }
679 }
680
681 pub fn context(&self) -> &InspectorContext {
682 &self.context
683 }
684
685 fn find_property_container(
686 &self,
687 from: Handle<UiNode>,
688 ui: &UserInterface,
689 ) -> Option<&ContextEntry> {
690 let mut parent_handle = from;
691
692 while let Ok(parent) = ui.try_get_node(parent_handle) {
693 for entry in self.context.entries.iter() {
694 if entry.property_container == parent_handle {
695 return Some(entry);
696 }
697 }
698
699 parent_handle = parent.parent;
700 }
701
702 None
703 }
704}
705
706pub const HEADER_MARGIN: Thickness = Thickness {
708 left: 2.0,
709 top: 1.0,
710 right: 4.0,
711 bottom: 1.0,
712};
713
714#[derive(Debug)]
716pub enum InspectorError {
717 CastError(CastError),
719 OutOfSync,
721 Custom(String),
725 Group(Vec<InspectorError>),
727}
728
729impl std::error::Error for InspectorError {}
730
731impl Display for InspectorError {
732 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
733 match self {
734 InspectorError::CastError(cast_error) => Display::fmt(cast_error, f),
735 InspectorError::OutOfSync => f.write_str(
736 "The object type has changed and the inspector context is no longer valid.",
737 ),
738 InspectorError::Custom(message) => f.write_str(message),
739 InspectorError::Group(inspector_errors) => {
740 f.write_str("Multiple errors:\n")?;
741 for err in inspector_errors {
742 writeln!(f, " {err}")?;
743 }
744 Ok(())
745 }
746 }
747 }
748}
749
750impl From<CastError> for InspectorError {
751 fn from(e: CastError) -> Self {
752 Self::CastError(e)
753 }
754}
755
756#[derive(Clone, Debug)]
758pub struct ContextEntry {
759 pub property_name: String,
761 pub property_display_name: String,
763 pub property_tag: String,
765 pub property_value_type_id: TypeId,
767 pub property_editor_definition_container: Arc<PropertyEditorDefinitionContainer>,
769 pub property_editor: Handle<UiNode>,
771 pub property_debug_output: String,
774 pub property_container: Handle<UiNode>,
778 pub property_path: String,
779}
780
781impl PartialEq for ContextEntry {
782 fn eq(&self, other: &Self) -> bool {
783 let ptr_a = &*self.property_editor_definition_container as *const _ as *const ();
785 let ptr_b = &*other.property_editor_definition_container as *const _ as *const ();
786
787 self.property_editor == other.property_editor
788 && self.property_name == other.property_name
789 && self.property_value_type_id ==other.property_value_type_id
790 && std::ptr::eq(ptr_a, ptr_b)
792 }
793}
794
795#[derive(Default, Clone)]
797pub struct Menu {
798 pub copy_value_as_string: Handle<MenuItem>,
800 pub copy_value: Handle<MenuItem>,
801 pub paste_value: Handle<MenuItem>,
802 pub menu: Option<RcUiNodeHandle>,
804}
805
806#[derive(Clone)]
808pub struct InspectorContext {
809 pub stack_panel: Handle<StackPanel>,
818 pub menu: Menu,
820 pub entries: Vec<ContextEntry>,
823 pub property_definitions: Arc<PropertyEditorDefinitionContainer>,
826 pub environment: Option<Arc<dyn InspectorEnvironment>>,
829 pub object_type_id: TypeId,
831 pub name_column_width: f32,
833 pub has_parent_object: bool,
838}
839
840impl PartialEq for InspectorContext {
841 fn eq(&self, other: &Self) -> bool {
842 self.entries == other.entries
843 }
844}
845
846fn object_type_id(object: &dyn Reflect) -> TypeId {
847 let mut object_type_id = None;
848 object.as_any(&mut |any| object_type_id = Some(any.type_id()));
849 object_type_id.unwrap()
850}
851
852impl Default for InspectorContext {
853 fn default() -> Self {
854 Self {
855 stack_panel: Default::default(),
856 menu: Default::default(),
857 entries: Default::default(),
858 property_definitions: Arc::new(
859 PropertyEditorDefinitionContainer::with_default_editors(),
860 ),
861 environment: None,
862 object_type_id: ().type_id(),
863 name_column_width: 150.0,
864 has_parent_object: false,
865 }
866 }
867}
868
869impl Debug for InspectorContext {
870 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
871 writeln!(f, "InspectorContext")
872 }
873}
874
875pub fn make_property_margin(layer_index: usize) -> Thickness {
881 let mut margin = HEADER_MARGIN;
882 margin.left += 10.0 + layer_index as f32 * 10.0;
883 margin
884}
885
886fn make_expander_margin(layer_index: usize) -> Thickness {
887 let mut margin = HEADER_MARGIN;
888 margin.left += layer_index as f32 * 10.0;
889 margin
890}
891
892fn make_expander_check_box(
893 layer_index: usize,
894 property_name: &str,
895 property_description: &str,
896 ctx: &mut BuildContext,
897) -> Handle<CheckBox> {
898 let description = if property_description.is_empty() {
899 property_name.to_string()
900 } else {
901 format!("{property_name}\n\n{property_description}")
902 };
903
904 let handle = CheckBoxBuilder::new(
905 WidgetBuilder::new()
906 .with_vertical_alignment(VerticalAlignment::Center)
907 .with_margin(make_expander_margin(layer_index)),
908 )
909 .with_background(
910 BorderBuilder::new(
911 WidgetBuilder::new()
912 .with_vertical_alignment(VerticalAlignment::Center)
913 .with_min_size(Vector2::new(4.0, 4.0)),
914 )
915 .with_stroke_thickness(Thickness::zero().into())
916 .build(ctx),
917 )
918 .with_content(
919 TextBuilder::new(
920 WidgetBuilder::new()
921 .with_opt_tooltip(make_tooltip(ctx, &description))
922 .with_height(16.0)
923 .with_margin(Thickness::left(2.0)),
924 )
925 .with_vertical_text_alignment(VerticalAlignment::Center)
926 .with_text(property_name)
927 .build(ctx),
928 )
929 .checked(Some(true))
930 .with_check_mark(make_arrow(ctx, ArrowDirection::Bottom, 8.0))
931 .with_uncheck_mark(make_arrow(ctx, ArrowDirection::Right, 8.0))
932 .build(ctx);
933
934 ctx[handle].accepts_input = false;
936
937 handle
938}
939
940pub fn make_expander_container(
948 layer_index: usize,
949 property_name: &str,
950 description: &str,
951 header: Handle<impl ObjectOrVariant<UiNode>>,
952 content: Handle<impl ObjectOrVariant<UiNode>>,
953 width: f32,
954 ctx: &mut BuildContext,
955) -> Handle<UiNode> {
956 ExpanderBuilder::new(WidgetBuilder::new())
957 .with_checkbox(make_expander_check_box(
958 layer_index,
959 property_name,
960 description,
961 ctx,
962 ))
963 .with_expander_column(Column::strict(width))
964 .with_expanded(true)
965 .with_header(header)
966 .with_content(content)
967 .build(ctx)
968 .to_base()
969}
970
971fn create_header(ctx: &mut BuildContext, text: &str, layer_index: usize) -> Handle<Text> {
972 TextBuilder::new(WidgetBuilder::new().with_margin(make_property_margin(layer_index)))
973 .with_text(text)
974 .with_vertical_text_alignment(VerticalAlignment::Center)
975 .build(ctx)
976}
977
978fn make_tooltip(ctx: &mut BuildContext, text: &str) -> Option<RcUiNodeHandle> {
979 if text.is_empty() {
980 None
981 } else {
982 Some(make_simple_tooltip(ctx, text))
983 }
984}
985
986fn make_simple_property_container(
987 title: Handle<Text>,
988 editor: Handle<impl ObjectOrVariant<UiNode>>,
989 description: &str,
990 width: f32,
991 ctx: &mut BuildContext,
992) -> Handle<UiNode> {
993 ctx[editor.to_base()].set_row(0).set_column(1);
994
995 let tooltip = make_tooltip(ctx, description);
996 ctx[title].set_tooltip(tooltip);
997
998 GridBuilder::new(WidgetBuilder::new().with_child(title).with_child(editor))
999 .add_row(Row::auto())
1000 .add_columns(vec![Column::strict(width), Column::stretch()])
1001 .build(ctx)
1002 .to_base()
1003}
1004
1005#[derive(Default, Clone)]
1008pub struct PropertyFilter(pub Option<Arc<dyn Fn(&dyn Reflect) -> bool + Send + Sync>>);
1009
1010impl PropertyFilter {
1011 pub fn new<T>(func: T) -> Self
1012 where
1013 T: Fn(&dyn Reflect) -> bool + 'static + Send + Sync,
1014 {
1015 Self(Some(Arc::new(func)))
1016 }
1017
1018 pub fn pass(&self, value: &dyn Reflect) -> bool {
1019 match self.0.as_ref() {
1020 None => true,
1021 Some(filter) => (filter)(value),
1022 }
1023 }
1024}
1025
1026fn assign_tab_indices(container: Handle<impl ObjectOrVariant<UiNode>>, ui: &mut UserInterface) {
1027 let mut counter = 0;
1028 let mut widgets_list = Vec::new();
1029 for (descendant_handle, descendant_ref) in ui.traverse_iter(container) {
1030 if descendant_ref.accepts_input {
1031 widgets_list.push((descendant_handle, counter));
1032 counter += 1;
1033 }
1034 }
1035
1036 for (descendant, tab_index) in widgets_list {
1037 ui.node_mut(descendant)
1038 .tab_index
1039 .set_value_and_mark_modified(Some(counter - tab_index));
1040 }
1041}
1042
1043pub struct InspectorContextArgs<'a, 'b, 'c> {
1044 pub object: &'a dyn Reflect,
1045 pub ctx: &'b mut BuildContext<'c>,
1046 pub definition_container: Arc<PropertyEditorDefinitionContainer>,
1047 pub environment: Option<Arc<dyn InspectorEnvironment>>,
1048 pub layer_index: usize,
1049 pub generate_property_string_values: bool,
1050 pub filter: PropertyFilter,
1051 pub name_column_width: f32,
1052 pub base_path: String,
1053 pub has_parent_object: bool,
1058}
1059
1060impl InspectorContext {
1061 pub fn from_object(context: InspectorContextArgs) -> Self {
1075 let InspectorContextArgs {
1076 object,
1077 ctx,
1078 definition_container,
1079 environment,
1080 layer_index,
1081 generate_property_string_values,
1082 filter,
1083 name_column_width,
1084 base_path,
1085 has_parent_object,
1086 } = context;
1087
1088 let mut entries = Vec::new();
1089
1090 let mut editors = Vec::new();
1091 object.fields_ref(&mut |fields_ref| {
1092 for (i, info) in fields_ref.iter().enumerate() {
1093 let field_text = if generate_property_string_values {
1094 format!("{:?}", info.value.field_value_as_reflect())
1095 } else {
1096 Default::default()
1097 };
1098
1099 if !filter.pass(info.value.field_value_as_reflect()) {
1100 continue;
1101 }
1102
1103 let description = if info.doc.is_empty() {
1104 info.display_name.to_string()
1105 } else {
1106 format!("{}\n\n{}", info.display_name, info.doc)
1107 };
1108
1109 if let Some(definition) = definition_container
1110 .definitions()
1111 .get(&info.value.type_id())
1112 {
1113 let property_path = if base_path.is_empty() {
1114 info.name.to_string()
1115 } else {
1116 format!("{}.{}", base_path, info.name)
1117 };
1118
1119 let editor = match definition.property_editor.create_instance(
1120 PropertyEditorBuildContext {
1121 build_context: ctx,
1122 property_info: info,
1123 environment: environment.clone(),
1124 definition_container: definition_container.clone(),
1125 layer_index,
1126 generate_property_string_values,
1127 filter: filter.clone(),
1128 name_column_width,
1129 base_path: property_path.clone(),
1130 has_parent_object,
1131 },
1132 ) {
1133 Ok(instance) => {
1134 let (container, editor) = match instance {
1135 PropertyEditorInstance::Simple { editor } => (
1136 make_simple_property_container(
1137 create_header(ctx, info.display_name, layer_index),
1138 editor,
1139 &description,
1140 name_column_width,
1141 ctx,
1142 ),
1143 editor,
1144 ),
1145 PropertyEditorInstance::Custom { container, editor } => {
1146 (container, editor)
1147 }
1148 };
1149
1150 entries.push(ContextEntry {
1151 property_editor: editor,
1152 property_value_type_id: definition.property_editor.value_type_id(),
1153 property_editor_definition_container: definition_container.clone(),
1154 property_name: info.name.to_string(),
1155 property_display_name: info.display_name.to_string(),
1156 property_tag: info.tag.to_string(),
1157 property_debug_output: field_text.clone(),
1158 property_container: container,
1159 property_path,
1160 });
1161
1162 if info.read_only {
1163 ctx[editor].set_enabled(false);
1164 }
1165
1166 container
1167 }
1168 Err(e) => {
1169 Log::err(format!(
1170 "Unable to create property editor instance: Reason {e:?}"
1171 ));
1172 make_simple_property_container(
1173 create_header(ctx, info.display_name, layer_index),
1174 TextBuilder::new(WidgetBuilder::new().on_row(i).on_column(1))
1175 .with_wrap(WrapMode::Word)
1176 .with_vertical_text_alignment(VerticalAlignment::Center)
1177 .with_text(format!(
1178 "Unable to create property \
1179 editor instance: Reason {e:?}"
1180 ))
1181 .build(ctx),
1182 &description,
1183 name_column_width,
1184 ctx,
1185 )
1186 }
1187 };
1188
1189 editors.push(editor);
1190 } else {
1191 editors.push(make_simple_property_container(
1192 create_header(ctx, info.display_name, layer_index),
1193 TextBuilder::new(WidgetBuilder::new().on_row(i).on_column(1))
1194 .with_wrap(WrapMode::Word)
1195 .with_vertical_text_alignment(VerticalAlignment::Center)
1196 .with_text(format!(
1197 "Property Editor Is Missing For Type {}!",
1198 info.value.type_name()
1199 ))
1200 .build(ctx),
1201 &description,
1202 name_column_width,
1203 ctx,
1204 ));
1205 }
1206 }
1207 });
1208
1209 let copy_value_as_string;
1210 let copy_value;
1211 let paste_value;
1212 let menu = ContextMenuBuilder::new(
1213 PopupBuilder::new(WidgetBuilder::new().with_visibility(false))
1214 .with_content(
1215 StackPanelBuilder::new(
1216 WidgetBuilder::new()
1217 .with_child({
1218 copy_value_as_string = MenuItemBuilder::new(WidgetBuilder::new())
1219 .with_content(MenuItemContent::text("Copy Value as String"))
1220 .build(ctx);
1221 copy_value_as_string
1222 })
1223 .with_child({
1224 copy_value = MenuItemBuilder::new(WidgetBuilder::new())
1225 .with_content(MenuItemContent::text("Copy Value"))
1226 .build(ctx);
1227 copy_value
1228 })
1229 .with_child({
1230 paste_value = MenuItemBuilder::new(WidgetBuilder::new())
1231 .with_content(MenuItemContent::text("Paste Value"))
1232 .build(ctx);
1233 paste_value
1234 }),
1235 )
1236 .build(ctx),
1237 )
1238 .with_restrict_picking(false),
1239 )
1240 .build(ctx);
1241 let menu = RcUiNodeHandle::new(menu, ctx.sender());
1242
1243 let stack_panel = StackPanelBuilder::new(
1244 WidgetBuilder::new()
1245 .with_context_menu(menu.clone())
1246 .with_children(editors),
1247 )
1248 .build(ctx);
1249
1250 if layer_index == 0 {
1252 assign_tab_indices(stack_panel, ctx.inner_mut());
1253 }
1254
1255 Self {
1256 stack_panel,
1257 menu: Menu {
1258 copy_value_as_string,
1259 copy_value,
1260 paste_value,
1261 menu: Some(menu),
1262 },
1263 entries,
1264 property_definitions: definition_container,
1265 environment,
1266 object_type_id: object_type_id(object),
1267 name_column_width,
1268 has_parent_object,
1269 }
1270 }
1271
1272 pub fn sync(
1283 &self,
1284 object: &dyn Reflect,
1285 ui: &mut UserInterface,
1286 layer_index: usize,
1287 generate_property_string_values: bool,
1288 filter: PropertyFilter,
1289 base_path: String,
1290 ) -> Result<(), Vec<InspectorError>> {
1291 if object_type_id(object) != self.object_type_id {
1292 return Err(vec![InspectorError::OutOfSync]);
1293 }
1294
1295 let mut sync_errors = Vec::new();
1296
1297 object.fields_ref(&mut |fields_ref| {
1298 for info in fields_ref {
1299 if !filter.pass(info.value.field_value_as_reflect()) {
1300 continue;
1301 }
1302
1303 if let Some(constructor) = self
1304 .property_definitions
1305 .definitions()
1306 .get(&info.value.type_id())
1307 {
1308 if let Some(property_editor) = self.find_property_editor(info.name) {
1309 let ctx = PropertyEditorMessageContext {
1310 instance: property_editor.property_editor,
1311 ui,
1312 property_info: info,
1313 definition_container: self.property_definitions.clone(),
1314 layer_index,
1315 environment: self.environment.clone(),
1316 generate_property_string_values,
1317 filter: filter.clone(),
1318 name_column_width: self.name_column_width,
1319 base_path: base_path.clone(),
1320 has_parent_object: self.has_parent_object,
1321 };
1322
1323 match constructor.property_editor.create_message(ctx) {
1324 Ok(message) => {
1325 if let Some(mut message) = message {
1326 message.delivery_mode = DeliveryMode::SyncOnly;
1327 ui.send_message(message);
1328 }
1329 }
1330 Err(e) => sync_errors.push(e),
1331 }
1332 } else {
1333 sync_errors.push(InspectorError::OutOfSync);
1334 }
1335 }
1336 }
1337 });
1338
1339 if layer_index == 0 {
1340 if ui.is_valid_handle(self.stack_panel) {
1343 assign_tab_indices(self.stack_panel, ui);
1344 }
1345 }
1346
1347 if sync_errors.is_empty() {
1348 Ok(())
1349 } else {
1350 Err(sync_errors)
1351 }
1352 }
1353
1354 pub fn property_editors(&self) -> impl Iterator<Item = &ContextEntry> + '_ {
1356 self.entries.iter()
1357 }
1358
1359 pub fn find_property_editor(&self, name: &str) -> Option<&ContextEntry> {
1361 self.entries.iter().find(|e| e.property_name == name)
1362 }
1363
1364 pub fn find_property_editor_by_tag(&self, tag: &str) -> Option<&ContextEntry> {
1366 self.entries.iter().find(|e| e.property_tag == tag)
1367 }
1368
1369 pub fn find_property_editor_widget(&self, name: &str) -> Handle<UiNode> {
1372 self.find_property_editor(name)
1373 .map(|e| e.property_editor)
1374 .unwrap_or_default()
1375 }
1376}
1377
1378uuid_provider!(Inspector = "c599c0f5-f749-4033-afed-1a9949c937a1");
1379
1380impl Control for Inspector {
1381 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
1382 self.widget.handle_routed_message(ui, message);
1383
1384 if let Some(msg) = message.data_for::<InspectorMessage>(self.handle) {
1385 match msg {
1386 InspectorMessage::Context(ctx) => {
1387 for child in self.children() {
1389 ui.send(*child, WidgetMessage::Remove);
1390 }
1391
1392 ui.send(ctx.stack_panel, WidgetMessage::LinkWith(self.handle));
1394
1395 self.context = ctx.clone();
1396 }
1397 InspectorMessage::PropertyContextMenuStatus {
1398 can_clone,
1399 can_paste,
1400 } => {
1401 ui.send(
1402 self.context.menu.copy_value,
1403 WidgetMessage::Enabled(*can_clone),
1404 );
1405 ui.send(
1406 self.context.menu.paste_value,
1407 WidgetMessage::Enabled(*can_paste),
1408 );
1409 }
1410 _ => (),
1411 }
1412 }
1413
1414 if let Some(PopupMessage::RelayedMessage(popup_message)) = message.data() {
1415 if let Some(mut clipboard) = ui.clipboard_mut() {
1416 if let Some(MenuItemMessage::Click) = popup_message.data() {
1417 if popup_message.destination() == self.context.menu.copy_value_as_string {
1418 if let Some(entry) = self.find_property_container(message.destination(), ui)
1419 {
1420 Log::verify(
1421 clipboard.set_contents(entry.property_debug_output.clone()),
1422 );
1423 }
1424 } else if popup_message.destination() == self.context.menu.copy_value {
1425 if let Some(entry) = self.find_property_container(message.destination(), ui)
1426 {
1427 ui.post(
1428 self.handle,
1429 InspectorMessage::CopyValue {
1430 path: entry.property_path.clone(),
1431 },
1432 );
1433 }
1434 } else if popup_message.destination() == self.context.menu.paste_value {
1435 if let Some(entry) = self.find_property_container(message.destination(), ui)
1436 {
1437 ui.post(
1438 self.handle,
1439 InspectorMessage::PasteValue {
1440 dest: entry.property_path.clone(),
1441 },
1442 );
1443 }
1444 }
1445 }
1446 }
1447 }
1448
1449 if message.delivery_mode != DeliveryMode::SyncOnly {
1452 let env = self.context.environment.clone();
1453 for entry in self.context.entries.iter() {
1454 if message.destination() == entry.property_editor {
1455 if let Some(args) = entry
1456 .property_editor_definition_container
1457 .definitions()
1458 .get(&entry.property_value_type_id)
1459 .and_then(|e| {
1460 e.property_editor
1461 .translate_message(PropertyEditorTranslationContext {
1462 environment: env.clone(),
1463 name: &entry.property_name,
1464 message,
1465 definition_container: self.context.property_definitions.clone(),
1466 })
1467 })
1468 {
1469 ui.post(self.handle, InspectorMessage::PropertyChanged(args));
1470 }
1471 }
1472 }
1473 }
1474 }
1475
1476 fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
1477 if let Some(PopupMessage::Open) = message.data() {
1478 if let Some(menu) = self.context.menu.menu.clone() {
1479 if message.direction() == MessageDirection::FromWidget
1480 && menu.handle() == message.destination()
1481 {
1482 if let Ok(popup) = ui.try_get_of_type::<Popup>(menu.handle()) {
1483 if let Some(entry) = self.find_property_container(popup.owner, ui) {
1484 ui.post(
1485 self.handle,
1486 InspectorMessage::PropertyContextMenuOpened {
1487 path: entry.property_path.clone(),
1488 },
1489 );
1490 }
1491 }
1492 }
1493 }
1494 }
1495 }
1496}
1497
1498pub struct InspectorBuilder {
1500 widget_builder: WidgetBuilder,
1501 context: InspectorContext,
1502}
1503
1504impl InspectorBuilder {
1505 pub fn new(widget_builder: WidgetBuilder) -> Self {
1506 Self {
1507 widget_builder,
1508 context: Default::default(),
1509 }
1510 }
1511
1512 pub fn with_context(mut self, context: InspectorContext) -> Self {
1514 self.context = context;
1515 self
1516 }
1517
1518 pub fn with_opt_context(mut self, context: Option<InspectorContext>) -> Self {
1521 if let Some(context) = context {
1522 self.context = context;
1523 }
1524 self
1525 }
1526
1527 pub fn build(self, ctx: &mut BuildContext) -> Handle<Inspector> {
1528 let canvas = Inspector {
1529 widget: self
1530 .widget_builder
1531 .with_preview_messages(true)
1532 .with_child(self.context.stack_panel)
1533 .build(ctx),
1534 context: self.context,
1535 };
1536 ctx.add(canvas)
1537 }
1538}
1539
1540#[cfg(test)]
1541mod test {
1542 use crate::inspector::InspectorBuilder;
1543 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
1544
1545 #[test]
1546 fn test_deletion() {
1547 test_widget_deletion(|ctx| InspectorBuilder::new(WidgetBuilder::new()).build(ctx));
1548 }
1549}