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