Scripts: More range attributes for fields

- Added Minimum- and Maximum attributes
- Documentation for Min, Max and Range
- EntityEditor: Refactoring + Range Attributes
This commit is contained in:
Simon Lübeß
2023-11-22 01:59:57 +01:00
parent ffe86a18cf
commit 872a400966
2 changed files with 262 additions and 81 deletions
+135 -47
View File
@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Text;
using GlitchyEngine.Core;
@@ -40,73 +41,85 @@ internal class EntityEditor
ShowEditor(entityType, instance);
}
public static object ShowFieldEditor(object reference, Type fieldType, string fieldName, IEnumerable<Attribute> attributes = null)
{
T GetAttribute<T>() where T : Attribute
private static T GetAttribute<T>(IEnumerable<Attribute> attributes) where T : Attribute
{
return (T)attributes?.FirstOrDefault(a => a is T);
}
ReadonlyAttribute readonlyAttribute = GetAttribute<ReadonlyAttribute>();
ImGui.BeginDisabled(readonlyAttribute != null);
private static object ShowPrimitiveEditor(object reference, Type fieldType, string fieldName, IEnumerable<Attribute> attributes)
{
object newValue = DidNotChange;
if (fieldType.IsGenericType)
{
if (fieldType.GetGenericTypeDefinition() == typeof(List<>))
{
newValue = ShowListEditor(fieldType, reference, fieldName);
}
else if (fieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
ImGui.Text($"{fieldName} Dictionary");
}
}
else if (fieldType.IsArray)
{
ImGui.Text($"{fieldName} Array");
}
else if (fieldType.IsEnum)
{
if (ImGui.BeginCombo(fieldName, reference.ToString()))
{
foreach (object enumValue in Enum.GetValues(fieldType))
{
if (ImGui.Selectable(enumValue.ToString(), enumValue == reference))
{
newValue = enumValue;
}
}
ImGui.EndCombo();
}
}
else if (fieldType.IsPrimitive)
{
ImGui.Text($"{fieldName} Primitive");
unsafe void DragScalar<T>(ImGuiDataType dataType) where T : unmanaged
{
RangeAttribute range = GetAttribute<RangeAttribute>();
T ChangeTypeSafe(object value)
{
try
{
return (T)Convert.ChangeType(value, typeof(T));
}
catch (OverflowException e)
{
// Log.Error($"Could not apply value: {e.Message}");
ImGui.TextColored(new Vector4(1, 0, 0, 1), $"Value {value} can not be converted to type {typeof(T)}: {e.Message}.");
T min = default;
T max = default;
return default;
}
}
RangeAttribute range = GetAttribute<RangeAttribute>(attributes);
T min = ReadStaticField<T>("MinValue");
T max = ReadStaticField<T>("MaxValue");
float speed = 1.0f;
// RangeAttribute takes precedence over Minimum- and Maximum-Attribute
if (range != null)
{
min = (T)Convert.ChangeType(range.Min, typeof(T));
max = (T)Convert.ChangeType(range.Max, typeof(T));
min = ChangeTypeSafe(range.Min);
max = ChangeTypeSafe(range.Max);
speed = range.Speed;
}
else
{
MinimumAttribute minimum = GetAttribute<MinimumAttribute>(attributes);
MaximumAttribute maximum = GetAttribute<MaximumAttribute>(attributes);
// Don't use range, if it is invalid
if (minimum != null && maximum != null && minimum.Min > maximum.Max)
{
//Log.Error($"Minimum value ({minimum.Min}) specified for field {fieldName} must not be greater than the maximum value ({maximum.Max})! Ignoring range...");
ImGui.TextColored(new Vector4(1, 0, 0, 1), $"Minimum value ({minimum.Min}) specified for field {fieldName} must not be greater than the maximum value ({maximum.Max})! Ignoring range...");
}
else
{
if (minimum != null)
min = ChangeTypeSafe(minimum.Min);
if (maximum != null)
max = ChangeTypeSafe(maximum.Max);
}
}
var value = (T)reference;
if (ImGui.DragScalar(fieldName, dataType, (IntPtr)(&value), 1, (IntPtr)(&min), (IntPtr)(&max)))
if (range?.Slider == true)
{
if (ImGui.SliderScalar(fieldName, dataType, (IntPtr)(&value), (IntPtr)(&min), (IntPtr)(&max)))
{
newValue = value;
}
}
else
{
if (ImGui.DragScalar(fieldName, dataType, (IntPtr)(&value), speed, (IntPtr)(&min), (IntPtr)(&max)))
{
newValue = value;
}
}
}
// TODO: allow editing in edit-mode
if (reference != null)
{
if (fieldType == typeof(bool))
@@ -145,8 +158,71 @@ internal class EntityEditor
DragScalar<float>(ImGuiDataType.Float);
else if (fieldType == typeof(double))
DragScalar<double>(ImGuiDataType.Double);
else
{
ImGui.TextColored(new Vector4(1, 0, 0, 1), $"{fieldName}: Type {fieldType} is not implemented.");
}
}
return newValue;
}
private static object ShowEnumEditor(object reference, Type fieldType, string fieldName)
{
object newValue = DidNotChange;
if (ImGui.BeginCombo(fieldName, reference.ToString()))
{
foreach (object enumValue in Enum.GetValues(fieldType))
{
if (ImGui.Selectable(enumValue.ToString(), enumValue == reference))
{
newValue = enumValue;
}
}
ImGui.EndCombo();
}
return newValue;
}
public static object ShowFieldEditor(object reference, Type fieldType, string fieldName, IEnumerable<Attribute> attributes = null)
{
ReadonlyAttribute readonlyAttribute = GetAttribute<ReadonlyAttribute>(attributes);
ImGui.BeginDisabled(readonlyAttribute != null);
object newValue = DidNotChange;
if (fieldType.IsGenericType)
{
if (fieldType.GetGenericTypeDefinition() == typeof(List<>))
{
newValue = ShowListEditor(fieldType, reference, fieldName);
}
else if (fieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
ImGui.Text($"{fieldName} Dictionary");
}
else
{
// TODO: Try to use a custom editor
ImGui.TextColored(new Vector4(1, 0, 0, 1), $"{fieldName}: Type {fieldType} is not implemented.");
}
}
else if (fieldType.IsArray)
{
ImGui.Text($"{fieldName} Array");
}
else if (fieldType.IsEnum)
{
newValue = ShowEnumEditor(reference, fieldType, fieldName);
}
else if (fieldType.IsPrimitive)
{
newValue = ShowPrimitiveEditor(reference, fieldType, fieldName, attributes);
}
else if (fieldType.IsValueType)
{
if (fieldType == typeof(bool2))
@@ -361,4 +437,16 @@ internal class EntityEditor
if (baseType.IsAssignableFrom(type) && !type.IsAbstract) yield return type;
}
}
private static T ReadStaticField<T>(string name)
{
FieldInfo field = typeof(T).GetField(name, BindingFlags.Public | BindingFlags.Static);
if (field == null)
{
throw new InvalidOperationException($"Type {typeof(T).Name} has no static field \"{name}\"");
}
return (T)field.GetValue(null);
}
}
+96 -3
View File
@@ -2,17 +2,110 @@ using System;
namespace GlitchyEngine.Editor;
/// <summary>
/// Specifies a minimum and maximum value that can be set using the editor for the field.
/// </summary>
/// <remarks>
/// This only affects the editor. The range does <b>not</b> affect scripts changing the value of the field.
/// </remarks>
[AttributeUsage(AttributeTargets.Field)]
public sealed class RangeAttribute : Attribute
{
public double Min { get; set; }
public double Max { get; set; }
/// <summary>
/// The minimum value that can be assigned to the field using the editor.
/// </summary>
public double Min { get; private set; }
public RangeAttribute(double min, double max)
/// <summary>
/// The maximum value that can be assigned to the field using the editor.
/// </summary>
public double Max { get; private set; }
/// <summary>
/// The speed with which the value will be changed when dragging in the editor.
/// </summary>
public float Speed { get; private set; }
/// <summary>
/// If set to <see langword="true"/>, the editor field will be a slider instead of a number field.
/// </summary>
public bool Slider { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="RangeAttribute"/> with a minimum, maximum value and optionally a speed.
/// </summary>
/// <param name="min"><inheritdoc cref="Min"/></param>
/// <param name="max"><inheritdoc cref="Max"/></param>
/// <param name="speed"><inheritdoc cref="Speed"/></param>
public RangeAttribute(double min, double max, float speed = 1.0f, bool slider = false)
{
if (max < min)
throw new ArgumentOutOfRangeException(nameof(max), $"{nameof(max)} must be larger than {nameof(min)}.");
if (speed <= 0)
throw new ArgumentOutOfRangeException(nameof(speed), $"{nameof(speed)} must be larger than zero.");
Min = min;
Max = max;
Speed = speed;
Slider = slider;
}
}
/// <summary>
/// Specifies a minimum value that can be set using the editor for the field.
/// </summary>
/// <remarks>
/// If the field also has a <see cref="RangeAttribute"/>, the <see cref="RangeAttribute"/> takes precedence.
/// <br/><br/>
/// If the field also has a <see cref="MaximumAttribute"/>, and <see cref="Min"/> is larger than <see cref="MaximumAttribute.Max"/>, then no range will be applied.
/// <br/><br/>
/// The value specified only affects the editor. It does <b>not</b> affect scripts changing the value of the field.
/// </remarks>
[AttributeUsage(AttributeTargets.Field)]
public sealed class MinimumAttribute : Attribute
{
/// <summary>
/// The minimum value that can be assigned to the field using the editor.
/// </summary>
public double Min { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="RangeAttribute"/> with a minimum value.
/// </summary>
/// <param name="min"><inheritdoc cref="Min"/></param>
public MinimumAttribute(double min)
{
Min = min;
}
}
/// <summary>
/// Specifies a maximum value that can be set using the editor for the field.
/// </summary>
/// <remarks>
/// If the field also has a <see cref="RangeAttribute"/>, the <see cref="RangeAttribute"/> takes precedence.
/// <br/><br/>
/// If the field also has a <see cref="MinimumAttribute"/>, and <see cref="MinimumAttribute.Min"/> is larger than <see cref="Max"/>, then no range will be applied.
/// <br/><br/>
/// The value specified only affects the editor. It does <b>not</b> affect scripts changing the value of the field.
/// </remarks>
[AttributeUsage(AttributeTargets.Field)]
public sealed class MaximumAttribute : Attribute
{
/// <summary>
/// The maximum value that can be assigned to the field using the editor.
/// </summary>
public double Max { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="RangeAttribute"/> with a maximum value.
/// </summary>
/// <param name="max"><inheritdoc cref="Max"/></param>
public MaximumAttribute(double max)
{
Max = max;
}
}