forked from Misaki/GhostEngine
Add component editors and UI controls
Added the `HierarchyEditor` and `LocalToWorldEditor` classes to implement custom component editing functionality. Added the `Vector3Field` control for 3D vector manipulation and its corresponding XAML definition. Added the `ComponentDataView` and `ComponentObject` classes to manage component data display and access. Added the `CustomEditorAttribute` to mark classes as custom editors for specific components. Changed the `IInspectable` interface to use properties for `Icon`, `HeaderContent`, and `InspectorContent`. Changed the `PropertyField` class to enhance UI control binding capabilities. Changed the `EditorWorldManager` to improve world data loading and deserialization processes. Changed the `EntityNode` and `WorldNode` classes to update entity construction and component querying. Changed the `StaticResource` class to include new binding flags for component properties. Changed the `InspectorService` to remove old contract references and adopt new interfaces. Changed the `QueryEnumerable` and related files to update generic constraints for improved type safety. Changed the `QueryItem` class to reflect new generic constraints and enhance deconstruction. Changed the `World.Query` methods to utilize the updated generic constraints. Updated the `SerializationTest` to align with new entity creation and management practices.
This commit is contained in:
@@ -1,10 +1,30 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System.Reflection;
|
||||
using Windows.Globalization.NumberFormatting;
|
||||
|
||||
namespace Ghost.Editor.Controls;
|
||||
|
||||
public sealed partial class PropertyField : ContentControl
|
||||
{
|
||||
private static readonly Dictionary<Type, DependencyProperty> _valueProperties = new()
|
||||
{
|
||||
{ typeof(TextBox), TextBox.TextProperty },
|
||||
{ typeof(NumberBox), NumberBox.ValueProperty },
|
||||
{ typeof(ToggleButton), ToggleButton.IsCheckedProperty },
|
||||
{ typeof(ToggleSwitch), ToggleSwitch.IsOnProperty },
|
||||
{ typeof(ComboBox), Selector.SelectedValueProperty },
|
||||
{ typeof(RangeBase), RangeBase.ValueProperty },
|
||||
};
|
||||
|
||||
private object? sourceObject;
|
||||
private FieldInfo? propertyInfo;
|
||||
private Type? _fieldType;
|
||||
|
||||
private object? _lastValue;
|
||||
|
||||
public string Label
|
||||
{
|
||||
get => (string)GetValue(LabelProperty);
|
||||
@@ -21,4 +41,106 @@ public sealed partial class PropertyField : ContentControl
|
||||
{
|
||||
DefaultStyleKey = typeof(PropertyField);
|
||||
}
|
||||
|
||||
private static DependencyProperty? GetValueProperty(Type? fieldType)
|
||||
{
|
||||
while (fieldType != null)
|
||||
{
|
||||
if (_valueProperties.TryGetValue(fieldType, out var dp))
|
||||
{
|
||||
return dp;
|
||||
}
|
||||
fieldType = fieldType.BaseType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static TField ConfigureField<TField>(PropertyField propertyField, FieldInfo fieldInfo, object sourceObject, Func<TField> factory)
|
||||
where TField : FrameworkElement
|
||||
{
|
||||
propertyField.sourceObject = sourceObject;
|
||||
propertyField.propertyInfo = fieldInfo;
|
||||
propertyField._fieldType = typeof(TField);
|
||||
|
||||
var field = factory();
|
||||
|
||||
var dp = GetValueProperty(typeof(TField));
|
||||
field.SetBinding(dp, new Binding
|
||||
{
|
||||
Source = sourceObject,
|
||||
Path = new PropertyPath(fieldInfo.Name),
|
||||
Mode = BindingMode.TwoWay,
|
||||
});
|
||||
return field;
|
||||
}
|
||||
|
||||
public static PropertyField Create(string label, FieldInfo fieldInfo, object sourceObject)
|
||||
{
|
||||
var propertyField = new PropertyField
|
||||
{
|
||||
Label = label
|
||||
};
|
||||
|
||||
FrameworkElement content;
|
||||
switch (fieldInfo.FieldType)
|
||||
{
|
||||
case Type t when t == typeof(string):
|
||||
content = ConfigureField(propertyField, fieldInfo, sourceObject, () => new TextBox());
|
||||
break;
|
||||
case Type t when t == typeof(int) || t == typeof(float) || t == typeof(double):
|
||||
content = ConfigureField(propertyField, fieldInfo, sourceObject, () => new NumberBox
|
||||
{
|
||||
SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Hidden,
|
||||
AcceptsExpression = true,
|
||||
NumberFormatter = new DecimalFormatter
|
||||
{
|
||||
FractionDigits = t == typeof(int) ? 0 : 9,
|
||||
}
|
||||
});
|
||||
break;
|
||||
case Type t when t == typeof(bool):
|
||||
content = ConfigureField(propertyField, fieldInfo, sourceObject, () => new ToggleSwitch());
|
||||
break;
|
||||
case Type t when t == typeof(Enum):
|
||||
content = ConfigureField(propertyField, fieldInfo, sourceObject, () => new ComboBox
|
||||
{
|
||||
ItemsSource = Enum.GetValues(t),
|
||||
SelectedValuePath = "Value",
|
||||
});
|
||||
break;
|
||||
default:
|
||||
content = new TextBlock
|
||||
{
|
||||
Text = $"Unsupported type: {fieldInfo.FieldType.Name}",
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Red)
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
propertyField.Content = content;
|
||||
return propertyField;
|
||||
}
|
||||
|
||||
public void UpdateValue()
|
||||
{
|
||||
if (sourceObject == null || propertyInfo == null || _fieldType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentValue = propertyInfo.GetValue(sourceObject);
|
||||
if (Equals(currentValue, _lastValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dp = GetValueProperty(_fieldType);
|
||||
if (dp != null)
|
||||
{
|
||||
SetValue(dp, propertyInfo.GetValue(sourceObject));
|
||||
_lastValue = currentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
96
Ghost.App/Controls/BasicInput/Vector3Field.cs
Normal file
96
Ghost.App/Controls/BasicInput/Vector3Field.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Ghost.Editor.Event;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ghost.Editor.Controls;
|
||||
|
||||
// TODO: value update event
|
||||
public sealed partial class Vector3Field : Control
|
||||
{
|
||||
private bool _suppressCallback;
|
||||
|
||||
public Vector3 Value
|
||||
{
|
||||
get => new((float)X, (float)Y, (float)Z);
|
||||
set
|
||||
{
|
||||
if (value == Value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_suppressCallback = true;
|
||||
|
||||
X = value.X;
|
||||
Y = value.Y;
|
||||
Z = value.Z;
|
||||
|
||||
_suppressCallback = false;
|
||||
}
|
||||
}
|
||||
|
||||
public double X
|
||||
{
|
||||
get => (double)GetValue(XProperty);
|
||||
set => SetValue(XProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty XProperty =
|
||||
DependencyProperty.Register(nameof(X), typeof(double), typeof(Vector3Field), new PropertyMetadata(0.0, ValueChanged));
|
||||
|
||||
public double Y
|
||||
{
|
||||
get => (double)GetValue(YProperty);
|
||||
set => SetValue(YProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty YProperty =
|
||||
DependencyProperty.Register(nameof(Y), typeof(double), typeof(Vector3Field), new PropertyMetadata(0.0, ValueChanged));
|
||||
|
||||
public double Z
|
||||
{
|
||||
get => (double)GetValue(ZProperty);
|
||||
set => SetValue(ZProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ZProperty =
|
||||
DependencyProperty.Register(nameof(Z), typeof(double), typeof(Vector3Field), new PropertyMetadata(0.0, ValueChanged));
|
||||
|
||||
public event ValueChangedEventHandler<Vector3>? OnValueChanged;
|
||||
|
||||
private static void ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is Vector3Field vector3Field)
|
||||
{
|
||||
if (vector3Field._suppressCallback)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldValue = vector3Field.Value;
|
||||
if (e.Property == XProperty)
|
||||
{
|
||||
var f = (float)(double)e.OldValue;
|
||||
oldValue.X = f;
|
||||
}
|
||||
else if (e.Property == YProperty)
|
||||
{
|
||||
var f = (float)(double)e.OldValue;
|
||||
oldValue.Y = f;
|
||||
}
|
||||
else if (e.Property == ZProperty)
|
||||
{
|
||||
var f = (float)(double)e.OldValue;
|
||||
oldValue.Z = f;
|
||||
}
|
||||
|
||||
vector3Field.OnValueChanged?.Invoke(vector3Field, new ValueChangedEventArgs<Vector3>(oldValue, vector3Field.Value));
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3Field()
|
||||
{
|
||||
DefaultStyleKey = typeof(Vector3Field);
|
||||
}
|
||||
}
|
||||
41
Ghost.App/Controls/BasicInput/Vector3Field.xaml
Normal file
41
Ghost.App/Controls/BasicInput/Vector3Field.xaml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Ghost.Editor.Controls">
|
||||
|
||||
<Style TargetType="local:Vector3Field">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:Vector3Field">
|
||||
<Grid ColumnSpacing="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="X" />
|
||||
<NumberBox Grid.Column="1" Value="{Binding X, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Text="Y" />
|
||||
<NumberBox Grid.Column="3" Value="{TemplateBinding Y}" />
|
||||
<TextBlock
|
||||
Grid.Column="4"
|
||||
VerticalAlignment="Center"
|
||||
Text="Z" />
|
||||
<NumberBox Grid.Column="5" Value="{TemplateBinding Z}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -2,6 +2,8 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/Controls/BasicInput/PropertyField.xaml" />
|
||||
<ResourceDictionary Source="/Controls/BasicInput/Vector3Field.xaml" />
|
||||
|
||||
<ResourceDictionary Source="/Controls/Internal/InternalControls.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
122
Ghost.App/Controls/Internal/ComponentDataView.cs
Normal file
122
Ghost.App/Controls/Internal/ComponentDataView.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Ghost.Editor.Core.Inspector;
|
||||
using Ghost.Editor.Resources;
|
||||
using Ghost.Editor.Utilities;
|
||||
using Ghost.Entities;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ghost.Editor.Controls.Internal;
|
||||
|
||||
internal unsafe sealed partial class ComponentDataView : Control
|
||||
{
|
||||
private StackPanel? _contentContainer;
|
||||
|
||||
private readonly World? _world;
|
||||
private readonly Entity _entity = Entity.Invalid;
|
||||
private readonly Type? _componentType;
|
||||
|
||||
private EventHandler<object>? _updateHandler;
|
||||
private IComponentEditor? _customEditor;
|
||||
private PropertyField[]? _propertyFields;
|
||||
|
||||
public string HeaderText
|
||||
{
|
||||
get => (string)GetValue(HeaderTextProperty);
|
||||
set => SetValue(HeaderTextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty HeaderTextProperty =
|
||||
DependencyProperty.Register(nameof(HeaderText), typeof(string), typeof(ComponentDataView), new PropertyMetadata(string.Empty));
|
||||
|
||||
internal ComponentDataView()
|
||||
{
|
||||
DefaultStyleKey = typeof(ComponentDataView);
|
||||
|
||||
Unloaded += (s, e) =>
|
||||
{
|
||||
CompositionTarget.Rendering -= _updateHandler;
|
||||
_contentContainer = null;
|
||||
_customEditor = null;
|
||||
_propertyFields = null;
|
||||
};
|
||||
}
|
||||
|
||||
public ComponentDataView(string header, World world, Entity entity, Type componentType) : this()
|
||||
{
|
||||
HeaderText = header;
|
||||
_world = world;
|
||||
_entity = entity;
|
||||
_componentType = componentType;
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
_contentContainer = (StackPanel)GetTemplateChild("ContentContainer");
|
||||
|
||||
base.OnApplyTemplate();
|
||||
ReBuild();
|
||||
}
|
||||
|
||||
private void ReflectionUpdate(object? sender, object e)
|
||||
{
|
||||
if (_propertyFields == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var propertyField in _propertyFields)
|
||||
{
|
||||
propertyField.UpdateValue();
|
||||
}
|
||||
}
|
||||
|
||||
private void CustomEditorUpdate(object? sender, object e)
|
||||
{
|
||||
_customEditor!.Update(new ComponentObject(_world!, _entity));
|
||||
}
|
||||
|
||||
public void ReBuild()
|
||||
{
|
||||
if (_contentContainer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_contentContainer.Children.Clear();
|
||||
if (_world == null || _componentType == null || _entity == Entity.Invalid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var componentObject = new ComponentObject(_world, _entity);
|
||||
var editorType = TypeCache.GetTypes().FirstOrDefault(t =>
|
||||
typeof(IComponentEditor).IsAssignableFrom(t) &&
|
||||
t.GetCustomAttribute<CustomEditorAttribute>()?.TargetType.IsAssignableFrom(_componentType) == true);
|
||||
|
||||
if (editorType != null)
|
||||
{
|
||||
_customEditor = (IComponentEditor)Activator.CreateInstance(editorType)!;
|
||||
_customEditor.Create(componentObject, _contentContainer);
|
||||
}
|
||||
else
|
||||
{
|
||||
var fields = _componentType.GetFields(StaticResource.componentPropertyBindingFlags);
|
||||
_propertyFields = new PropertyField[fields.Length];
|
||||
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
var component = _world.ComponentStorage.ComponentPools[_componentType.TypeHandle.Value].Get(_entity);
|
||||
var propertyField = PropertyField.Create(field.Name, field, component);
|
||||
|
||||
_propertyFields[i] = propertyField;
|
||||
_contentContainer.Children.Add(propertyField);
|
||||
}
|
||||
}
|
||||
|
||||
_updateHandler = _customEditor == null ? ReflectionUpdate : CustomEditorUpdate;
|
||||
CompositionTarget.Rendering += _updateHandler;
|
||||
}
|
||||
}
|
||||
27
Ghost.App/Controls/Internal/ComponentDataView.xaml
Normal file
27
Ghost.App/Controls/Internal/ComponentDataView.xaml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Ghost.Editor.Controls.Internal">
|
||||
|
||||
<Style TargetType="local:ComponentDataView">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:ComponentDataView">
|
||||
<StackPanel Margin="0,0,0,16">
|
||||
<Border
|
||||
Padding="8"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{ThemeResource SolidBackgroundFillColorSecondaryBrush}">
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{TemplateBinding HeaderText}" />
|
||||
</Border>
|
||||
<StackPanel
|
||||
x:Name="ContentContainer"
|
||||
Margin="8,2,2,0"
|
||||
Spacing="2" />
|
||||
</StackPanel>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,4 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries />
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="/Controls/Internal/ComponentDataView.xaml" />
|
||||
<ResourceDictionary Source="/Controls/Internal/NavigationTabView.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
Reference in New Issue
Block a user