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:
2025-06-20 20:19:14 +09:00
parent fc44c73ca8
commit 1724072f7e
54 changed files with 1474 additions and 554 deletions

View File

@@ -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;
}
}
}

View 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);
}
}

View 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>