From 872a400966071f477bd1984c624412d36092089b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Wed, 22 Nov 2023 01:59:57 +0100 Subject: [PATCH] Scripts: More range attributes for fields - Added Minimum- and Maximum attributes - Documentation for Min, Max and Range - EntityEditor: Refactoring + Range Attributes --- ScriptCore/Editor/EntityEditor.cs | 242 +++++++++++++++++++--------- ScriptCore/Editor/RangeAttribute.cs | 101 +++++++++++- 2 files changed, 262 insertions(+), 81 deletions(-) diff --git a/ScriptCore/Editor/EntityEditor.cs b/ScriptCore/Editor/EntityEditor.cs index 79e0361..d74bf38 100644 --- a/ScriptCore/Editor/EntityEditor.cs +++ b/ScriptCore/Editor/EntityEditor.cs @@ -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,14 +41,155 @@ internal class EntityEditor ShowEditor(entityType, instance); } - public static object ShowFieldEditor(object reference, Type fieldType, string fieldName, IEnumerable attributes = null) + private static T GetAttribute(IEnumerable attributes) where T : Attribute { - T GetAttribute() where T : Attribute + return (T)attributes?.FirstOrDefault(a => a is T); + } + + private static object ShowPrimitiveEditor(object reference, Type fieldType, string fieldName, IEnumerable attributes) + { + object newValue = DidNotChange; + + unsafe void DragScalar(ImGuiDataType dataType) where T : unmanaged { - return (T)attributes?.FirstOrDefault(a => a is T); + 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}."); + + return default; + } + } + + RangeAttribute range = GetAttribute(attributes); + + T min = ReadStaticField("MinValue"); + T max = ReadStaticField("MaxValue"); + float speed = 1.0f; + + // RangeAttribute takes precedence over Minimum- and Maximum-Attribute + if (range != null) + { + min = ChangeTypeSafe(range.Min); + max = ChangeTypeSafe(range.Max); + speed = range.Speed; + } + else + { + MinimumAttribute minimum = GetAttribute(attributes); + MaximumAttribute maximum = GetAttribute(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 (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; + } + } } - ReadonlyAttribute readonlyAttribute = GetAttribute(); + // TODO: allow editing in edit-mode + if (reference != null) + { + if (fieldType == typeof(bool)) + { + bool value = (bool)reference; + if (ImGui.Checkbox(fieldName, ref value)) + newValue = value; + } + else if (fieldType == typeof(char)) + { + unsafe + { + char value = (char)reference; + + if (ImGui.InputText(fieldName, (IntPtr)(&value), 2)) + newValue = value; + } + } + else if (fieldType == typeof(byte)) + DragScalar(ImGuiDataType.U8); + else if (fieldType == typeof(sbyte)) + DragScalar(ImGuiDataType.S8); + else if (fieldType == typeof(short)) + DragScalar(ImGuiDataType.S16); + else if (fieldType == typeof(ushort)) + DragScalar(ImGuiDataType.U16); + else if (fieldType == typeof(int)) + DragScalar(ImGuiDataType.S32); + else if (fieldType == typeof(uint)) + DragScalar(ImGuiDataType.U32); + else if (fieldType == typeof(long)) + DragScalar(ImGuiDataType.S64); + else if (fieldType == typeof(ulong)) + DragScalar(ImGuiDataType.U64); + else if (fieldType == typeof(float)) + DragScalar(ImGuiDataType.Float); + else if (fieldType == typeof(double)) + DragScalar(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 attributes = null) + { + ReadonlyAttribute readonlyAttribute = GetAttribute(attributes); ImGui.BeginDisabled(readonlyAttribute != null); @@ -63,6 +205,11 @@ internal class EntityEditor { 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) { @@ -70,82 +217,11 @@ internal class EntityEditor } 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(); - } + newValue = ShowEnumEditor(reference, fieldType, fieldName); } else if (fieldType.IsPrimitive) { - ImGui.Text($"{fieldName} Primitive"); - - unsafe void DragScalar(ImGuiDataType dataType) where T : unmanaged - { - RangeAttribute range = GetAttribute(); - - T min = default; - T max = default; - - if (range != null) - { - min = (T)Convert.ChangeType(range.Min, typeof(T)); - max = (T)Convert.ChangeType(range.Max, typeof(T)); - } - - var value = (T)reference; - if (ImGui.DragScalar(fieldName, dataType, (IntPtr)(&value), 1, (IntPtr)(&min), (IntPtr)(&max))) - { - newValue = value; - } - } - - if (reference != null) - { - if (fieldType == typeof(bool)) - { - bool value = (bool)reference; - if (ImGui.Checkbox(fieldName, ref value)) - newValue = value; - } - else if (fieldType == typeof(char)) - { - unsafe - { - char value = (char)reference; - - if (ImGui.InputText(fieldName, (IntPtr)(&value), 2)) - newValue = value; - } - } - else if (fieldType == typeof(byte)) - DragScalar(ImGuiDataType.U8); - else if (fieldType == typeof(sbyte)) - DragScalar(ImGuiDataType.S8); - else if (fieldType == typeof(short)) - DragScalar(ImGuiDataType.S16); - else if (fieldType == typeof(ushort)) - DragScalar(ImGuiDataType.U16); - else if (fieldType == typeof(int)) - DragScalar(ImGuiDataType.S32); - else if (fieldType == typeof(uint)) - DragScalar(ImGuiDataType.U32); - else if (fieldType == typeof(long)) - DragScalar(ImGuiDataType.S64); - else if (fieldType == typeof(ulong)) - DragScalar(ImGuiDataType.U64); - else if (fieldType == typeof(float)) - DragScalar(ImGuiDataType.Float); - else if (fieldType == typeof(double)) - DragScalar(ImGuiDataType.Double); - } + newValue = ShowPrimitiveEditor(reference, fieldType, fieldName, attributes); } else if (fieldType.IsValueType) { @@ -361,4 +437,16 @@ internal class EntityEditor if (baseType.IsAssignableFrom(type) && !type.IsAbstract) yield return type; } } + + private static T ReadStaticField(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); + } } diff --git a/ScriptCore/Editor/RangeAttribute.cs b/ScriptCore/Editor/RangeAttribute.cs index 3099647..e66e40e 100644 --- a/ScriptCore/Editor/RangeAttribute.cs +++ b/ScriptCore/Editor/RangeAttribute.cs @@ -2,17 +2,110 @@ using System; namespace GlitchyEngine.Editor; +/// +/// Specifies a minimum and maximum value that can be set using the editor for the field. +/// +/// +/// This only affects the editor. The range does not affect scripts changing the value of the field. +/// +[AttributeUsage(AttributeTargets.Field)] public sealed class RangeAttribute : Attribute { - public double Min { get; set; } - public double Max { get; set; } + /// + /// The minimum value that can be assigned to the field using the editor. + /// + public double Min { get; private set; } + + /// + /// The maximum value that can be assigned to the field using the editor. + /// + public double Max { get; private set; } + + /// + /// The speed with which the value will be changed when dragging in the editor. + /// + public float Speed { get; private set; } - public RangeAttribute(double min, double max) + /// + /// If set to , the editor field will be a slider instead of a number field. + /// + public bool Slider { get; private set; } + + + /// + /// Initializes a new instance of the with a minimum, maximum value and optionally a speed. + /// + /// + /// + /// + 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; } -} \ No newline at end of file +} + +/// +/// Specifies a minimum value that can be set using the editor for the field. +/// +/// +/// If the field also has a , the takes precedence. +///

+/// If the field also has a , and is larger than , then no range will be applied. +///

+/// The value specified only affects the editor. It does not affect scripts changing the value of the field. +///
+[AttributeUsage(AttributeTargets.Field)] +public sealed class MinimumAttribute : Attribute +{ + /// + /// The minimum value that can be assigned to the field using the editor. + /// + public double Min { get; private set; } + + /// + /// Initializes a new instance of the with a minimum value. + /// + /// + public MinimumAttribute(double min) + { + Min = min; + } +} + + +/// +/// Specifies a maximum value that can be set using the editor for the field. +/// +/// +/// If the field also has a , the takes precedence. +///

+/// If the field also has a , and is larger than , then no range will be applied. +///

+/// The value specified only affects the editor. It does not affect scripts changing the value of the field. +///
+[AttributeUsage(AttributeTargets.Field)] +public sealed class MaximumAttribute : Attribute +{ + /// + /// The maximum value that can be assigned to the field using the editor. + /// + public double Max { get; private set; } + + /// + /// Initializes a new instance of the with a maximum value. + /// + /// + public MaximumAttribute(double max) + { + Max = max; + } +} \ No newline at end of file