using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using GlitchyEngine.Core; using GlitchyEngine.Extensions; using GlitchyEngine.Math; using GlitchyEngine.Math.Attributes; using ImGuiNET; using Component = GlitchyEngine.Core.Component; namespace GlitchyEngine.Editor; class DidNotChange { } internal class EntityEditor { public static readonly DidNotChange DidNotChange = new DidNotChange(); private delegate object? ShowCustomEditorMethod(object? reference, Type fieldType, string fieldName); private static Dictionary _customEditors = new(); static EntityEditor() { foreach (Type type in TypeExtension.EnumerateAllTypes()) { if (type.TryGetCustomAttribute(out var attribute)) { MethodInfo? showEditorMethod = type.GetMethod("ShowEditor", BindingFlags.Static | BindingFlags.Public, null, new []{ typeof(object), typeof(Type), typeof(string) }, null); if (showEditorMethod == null) { Log.Error($"No ShowEditor-method found for type {type}"); } else { ShowCustomEditorMethod method = showEditorMethod.GetDelegate(); _customEditors.Add(attribute.Type, method); } } } } public static void BeginNewRow() { if (!StartNewProperty_NewRow) { StartNewProperty_NewRow = true; return; } ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); } public static bool StartNewProperty_NewRow = true; /// /// Starts a new property by creating a new table row, writing the name in the first column and entering the second column. /// /// The label that will be show in the label column. /// A string containing the propertyName as ImGui id private static string StartNewProperty(string propertyName) { //BeginNewRow(); ImGui.TextUnformatted(propertyName); ImGuiExtension.AttachTooltip(propertyName); ImGui.TableSetColumnIndex(1); return $"##{propertyName}"; } private static bool TryShowCustomEditor(object? reference, Type fieldType, string fieldName, out object? editedValue) { editedValue = DidNotChange; try { // Try to match the concrete type first (e.g. Foo -> Foo and Foo -> List) // Note: Foo wont match a serializer for Foo<> if (_customEditors.TryGetValue(fieldType, out ShowCustomEditorMethod customEditorMethod)) { editedValue = customEditorMethod(reference, fieldType, fieldName); return true; } if (fieldType.IsGenericType && _customEditors.TryGetValue(fieldType.GetGenericTypeDefinition(), out customEditorMethod)) { editedValue = customEditorMethod(reference, fieldType, fieldName); return true; } } catch (Exception e) { Log.Exception(e); } return false; } public static bool ShowFieldInEditor(FieldInfo fieldInfo) { if (fieldInfo.IsPublic) { // Public fields are visible by default (No HideInEditorAttribute) if (fieldInfo.HasCustomAttribute()) return false; return true; } // Private, protected and internal fields are hidden by default (No ShowInEditorAttribute) if (fieldInfo.HasCustomAttribute()) return true; return false; } public static void ShowDefaultEntityEditor(UUID entityId, Type entityType) { // TODO: Call custom editors here! ScriptGlue.Entity_GetScriptInstance(entityId, out object? instance); if (instance != null) ShowEditor(entityType, instance); } private static T? GetAttribute(IEnumerable? attributes) where T : Attribute { return (T?)attributes?.FirstOrDefault(a => a is T); } private static object ShowDecimalEditor(object? reference, Type fieldType, string fieldName, IEnumerable? attributes) { if (reference is not decimal value) return DidNotChange; string stringValue = value.ToString(CultureInfo.InvariantCulture); byte[] bytes = new byte[30 * 4]; Encoding.UTF8.GetBytes(stringValue, 0, stringValue.Length, bytes, 0); ImGui.PushID("Decimal"); string fieldId = StartNewProperty(fieldName); if (ImGui.InputText(fieldId, bytes, (uint)bytes.Length, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CharsDecimal)) { string text = Encoding.UTF8.GetString(bytes); if (decimal.TryParse(text, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out decimal newValue)) { ImGui.PopID(); return newValue; } } ImGui.PopID(); return DidNotChange; } private static object ShowPrimitiveEditor(object reference, Type fieldType, string fieldName, IEnumerable? attributes) { Debug.Assert(reference != null); string fieldId = StartNewProperty(fieldName); object newValue = DidNotChange; unsafe void DragScalar(ImGuiDataType dataType) where T : unmanaged { 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); // Get Min and Max Values from Type T T min = ReadStaticField("MinValue"); T max = ReadStaticField("MaxValue"); float dragSpeed = 1.0f; // RangeAttribute takes precedence over Minimum- and Maximum-Attribute if (range != null) { min = ChangeTypeSafe(range.Min); max = ChangeTypeSafe(range.Max); dragSpeed = 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(fieldId, dataType, (IntPtr)(&value), (IntPtr)(&min), (IntPtr)(&max))) { newValue = value; } } else { if (ImGui.DragScalar(fieldId, dataType, (IntPtr)(&value), dragSpeed, (IntPtr)(&min), (IntPtr)(&max))) { newValue = value; } } } if (fieldType == typeof(bool)) { bool value = (bool)reference; if (ImGui.Checkbox(fieldId, ref value)) newValue = value; } else if (fieldType == typeof(char)) { unsafe { // TODO: Allow escaped unicode chars \u XXXX // TODO: Add text input validation, so that it isn't possible to type invalid chars char* value = stackalloc char[4]; switch((char)reference) { case '\a': value[0] = '\\'; value[1] = 'a'; break; case '\b': value[0] = '\\'; value[1] = 'b'; break; case '\f': value[0] = '\\'; value[1] = 'f'; break; case '\n': value[0] = '\\'; value[1] = 'n'; break; case '\r': value[0] = '\\'; value[1] = 'r'; break; case '\t': value[0] = '\\'; value[1] = 't'; break; case '\v': value[0] = '\\'; value[1] = 'v'; break; default: value[0] = (char)reference; value[1] = '\0'; break; }; // Enough space to store two code points byte* buffer = stackalloc byte[8]; int encodedBytes = Encoding.UTF8.GetBytes(value, 2, buffer, 8); // Place string delimiter after encoded UTF8 sequence. buffer[encodedBytes] = (byte)'\0'; if (ImGui.InputText(fieldId, (IntPtr)buffer, 8)) { if (buffer[0] == '\\' && buffer[1] != '\0') { newValue = (char)buffer[1] switch { 'a' => newValue = '\a', 'b' => newValue = '\b', 'f' => newValue = '\f', 'n' => newValue = '\n', 'r' => newValue = '\r', 't' => newValue = '\t', 'v' => newValue = '\v', _ => newValue = '\\' }; } else { // Only decode first codepoint Encoding.UTF8.GetChars(buffer, 4, value, 4); newValue = value[0]; } } } } 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) { string fieldId = StartNewProperty(fieldName); object newValue = DidNotChange; if (ImGui.BeginCombo(fieldId, 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); object? newValue = DidNotChange; BeginNewRow(); if (TryShowCustomEditor(reference, fieldType, fieldName, out newValue)) { } else if (fieldType.IsGenericType) { if (fieldType.GetGenericTypeDefinition() == typeof(List<>)) { newValue = ShowListEditor(fieldType, reference, fieldName); } else { ImGui.TextColored(new Vector4(1, 0, 0, 1), $"{fieldName}: Type {fieldType} is not implemented."); } } else if (fieldType.IsArray) { newValue = ShowArrayEditor(fieldType, reference, fieldName); } 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(decimal)) { newValue = ShowDecimalEditor(reference, fieldType, fieldName, attributes); } else if (fieldType == typeof(ColorRGBA)) { string fieldId = StartNewProperty(fieldName); ColorRGBA value = (ColorRGBA)reference!; if (ImGui.ColorEdit4(fieldId, ref Unsafe.As(ref value))) newValue = value; } else if (fieldType.TryGetCustomAttribute(out VectorAttribute vectorAttribute)) { newValue = ShowVectorEditor(reference, fieldType, fieldName, vectorAttribute); } else { newValue = ShowObjectEditor(reference, fieldType, fieldName, attributes); } } else if (fieldType.IsSubclassOf(typeof(Entity)) || fieldType == typeof(Entity)) { newValue = ShowEntityDropTarget(fieldName, fieldType, reference); } else if (fieldType.IsSubclassOf(typeof(Component))) { newValue = ShowComponentDropTarget(fieldName, fieldType, reference); } else if (fieldType == typeof(string)) { newValue = ShowStringEditor(reference, fieldType, fieldName, attributes); } else { newValue = ShowObjectEditor(reference, fieldType, fieldName, attributes); } ImGui.EndDisabled(); return newValue; } private static object ShowVectorEditor(object? reference, Type fieldType, string fieldName, VectorAttribute vectorAttribute) { object newValue = DidNotChange; string fieldId = StartNewProperty(fieldName); if (vectorAttribute.Type == typeof(bool)) { if (vectorAttribute.ComponentCount == 2) { bool2 value = (bool2)reference!; if (ImGuiExtension.Checkbox2(fieldId, ref value)) newValue = value; } else if (vectorAttribute.ComponentCount == 3) { bool3 value = (bool3)reference!; if (ImGuiExtension.Checkbox3(fieldId, ref value)) newValue = value; } else if (vectorAttribute.ComponentCount == 4) { bool4 value = (bool4)reference!; if (ImGuiExtension.Checkbox4(fieldId, ref value)) newValue = value; } } else if (vectorAttribute.Type == typeof(int)) { if (vectorAttribute.ComponentCount == 2) { int2 value = (int2)reference!; if (ImGui.DragInt2(fieldId, ref value.X)) newValue = value; } else if (vectorAttribute.ComponentCount == 3) { int3 value = (int3)reference!; if (ImGui.DragInt3(fieldId, ref value.X)) newValue = value; } else if (vectorAttribute.ComponentCount == 4) { int4 value = (int4)reference!; if (ImGui.DragInt4(fieldId, ref value.X)) newValue = value; } } else if (vectorAttribute.Type == typeof(uint)) { if (vectorAttribute.ComponentCount == 2) { uint2 value = (uint2)reference!; if (ImGuiExtension.DragUInt2(fieldId, ref value)) newValue = value; } else if (vectorAttribute.ComponentCount == 3) { uint3 value = (uint3)reference!; if (ImGuiExtension.DragUInt3(fieldId, ref value)) newValue = value; } else if (vectorAttribute.ComponentCount == 4) { uint4 value = (uint4)reference!; if (ImGuiExtension.DragUInt4(fieldId, ref value)) newValue = value; } } else if (vectorAttribute.Type == typeof(float)) { if (vectorAttribute.ComponentCount == 2) { float2 value = (float2)reference!; if (ImGui.DragFloat2(fieldId, ref Unsafe.As(ref value))) newValue = value; } else if (vectorAttribute.ComponentCount == 3) { float3 value = (float3)reference!; if (ImGui.DragFloat3(fieldId, ref Unsafe.As(ref value))) newValue = value; } else if (vectorAttribute.ComponentCount == 4) { float4 value = (float4)reference!; if (ImGui.DragFloat4(fieldId, ref Unsafe.As(ref value))) newValue = value; } } else if (vectorAttribute.Type == typeof(double)) { if (vectorAttribute.ComponentCount == 2) { double2 value = (double2)reference!; if (ImGuiExtension.DragDouble2(fieldId, ref value)) newValue = value; } else if (vectorAttribute.ComponentCount == 3) { double3 value = (double3)reference!; if (ImGuiExtension.DragDouble3(fieldId, ref value)) newValue = value; } else if (vectorAttribute.ComponentCount == 4) { double4 value = (double4)reference!; if (ImGuiExtension.DragDouble4(fieldId, ref value)) newValue = value; } } return newValue; } private static object? ShowObjectEditor(object? reference, Type fieldType, string fieldName, IEnumerable attributes) { object? newValue = DidNotChange; //ReadonlyAttribute? readonlyAttribute = GetAttribute(attributes); // TODO: Disabling stuff //ImGui.EndDisabled(); ImGui.BeginDisabled(reference == null); if (reference == null) ImGui.SetNextItemOpen(false); bool open = ImGui.TreeNodeEx(fieldName, ImGuiTreeNodeFlags.AllowOverlap | ImGuiTreeNodeFlags.SpanAllColumns); ImGui.EndDisabled(); ImGui.TableSetColumnIndex(1); if (!fieldType.IsValueType) { if (reference == null) { if (ImGui.BeginCombo("##Create", "Create instance...", ImGuiComboFlags.HeightSmall | ImGuiComboFlags.NoArrowButton)) { foreach (Type t in TypeExtension.FindDerivedTypes(fieldType)) { if (ImGui.Selectable(t.Name)) { newValue = ActivatorExtension.CreateInstanceSafe(t); } } ImGui.EndCombo(); } } else { if (ImGui.SmallButton("Remove")) { newValue = null; } } } else { newValue = reference; } if (open) { //ImGui.BeginDisabled(readonlyAttribute != null); Debug.Assert(reference != null); ShowEditor(reference!.GetType(), reference); ImGui.TreePop(); } return newValue; } private static object? ShowComponentDropTarget(string fieldName, Type fieldType, object? currentValue) { object? newValue = DidNotChange; string fieldId = StartNewProperty(fieldName); Component? component = currentValue as Component; string entityName = "None"; if (component != null) { entityName = component.Entity.Name; } Type? actualType = component?.GetType(); entityName += $" ({(actualType ?? fieldType).Name})"; if (ImGui.Button(entityName)) { if (component?.Entity != null) { // TODO: Hightlight Entity in hierarchy // TODO: Highlight Component in editor } } if (ImGui.BeginDragDropTarget()) { ImGuiPayloadPtr peekPayload = ImGui.AcceptDragDropPayload("ENTITY", ImGuiDragDropFlags.AcceptPeekOnly); bool allowDrop = false; unsafe { if (peekPayload.NativePtr != null) { UUID draggedId = *(UUID*)peekPayload.Data; Entity draggedEntity = new Entity(draggedId); allowDrop = draggedEntity.HasComponent(fieldType); } if (allowDrop) { ImGuiPayloadPtr payload = ImGui.AcceptDragDropPayload("ENTITY"); if (payload.NativePtr != null) { UUID droppedId = *(UUID*)payload.Data; Entity draggedEntity = new Entity(droppedId); try { newValue = draggedEntity.GetComponent(fieldType); } catch (Exception e) { Log.Exception(e); } } } } ImGui.EndDragDropTarget(); } return newValue; } private static object? ShowEntityDropTarget(string fieldName, Type fieldType, object? currentValue) { object? newValue = DidNotChange; string fieldId = StartNewProperty(fieldName); string entityName = "None"; Entity? entity = currentValue as Entity; if (entity != null) { entityName = entity.Name; } Type? actualType = entity?.GetType(); entityName += $" ({(actualType ?? fieldType).Name})"; if (ImGui.Button(entityName)) { if (entity != null) { // TODO: Hightlight Entity in hierarchy } } if (ImGui.BeginDragDropTarget()) { ImGuiPayloadPtr peekPayload = ImGui.AcceptDragDropPayload("ENTITY", ImGuiDragDropFlags.AcceptPeekOnly); bool allowDrop = false; unsafe { if (peekPayload.NativePtr != null) { UUID draggedId = *(UUID*)peekPayload.Data; Entity draggedEntity = new Entity(draggedId); // TODO: Check if entity is valid? if (fieldType == typeof(Entity)) { allowDrop = true; } else { Entity? scriptInstance = Entity.GetScriptReference(draggedId, typeof(Entity)); allowDrop = fieldType.IsInstanceOfType(scriptInstance); } } if (allowDrop) { ImGuiPayloadPtr payload = ImGui.AcceptDragDropPayload("ENTITY"); if (payload.NativePtr != null) { UUID droppedId = *(UUID*)payload.Data; if (fieldType == typeof(Entity)) { newValue = new Entity(droppedId); } else { newValue = Entity.GetScriptReference(droppedId, typeof(Entity)); } } } } ImGui.EndDragDropTarget(); } ImGui.SameLine(); if (ImGui.Button("...")) { // TODO: Show entity selector } ImGuiExtension.AttachTooltip("Search entity..."); return newValue; } private static object ShowStringEditor(object? reference, Type fieldType, string fieldName, IEnumerable? attributes) { string fieldId = StartNewProperty(fieldName); object newValue = DidNotChange; TextFieldAttribute? textField = GetAttribute(attributes); string value = reference as string ?? ""; if (textField?.Multiline == true) { if (ImGui.InputTextMultiline(fieldId, ref value, 1000, new Vector2(-1.0f, ImGui.GetTextLineHeight() * textField.TextFieldLines))) newValue = value; } else { if (ImGui.InputText(fieldId, ref value, 1000, ImGuiInputTextFlags.EnterReturnsTrue)) newValue = value; } return newValue; } public static void ShowEditor(Type type, object? reference) { int i = 0; // Iterate all fields foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) { i++; ImGui.PushID(i); if (ShowFieldInEditor(field)) { object value = field.GetValue(reference); IEnumerable attributes = field.GetCustomAttributes(); LabelAttribute? label = GetAttribute(attributes); string prettyName; if (label != null) { prettyName = label.Label ?? field.Name; } else { prettyName = field.Name.ToPrettyName(); } object? newValue = ShowFieldEditor(value, value?.GetType() ?? field.FieldType, prettyName, attributes); if (newValue != DidNotChange) { // Reference was changed inside Field Editor -> write back to field (only necessary for value types) field.SetValue(reference, newValue); } } ImGui.PopID(); } ShowButtons(type, reference); } /// /// Shows all buttons in the UI. /// private static void ShowButtons(Type type, object? reference) { ImGui.PushID("Buttons"); int i = 0; // Iterate all methods foreach (MethodInfo method in type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) { var showButton = method.GetCustomAttribute(); if (showButton == null) continue; if (Application.IsInEditMode && !showButton.Visibility.HasFlag(ButtonVisibility.InEditMode)) continue; if (Application.IsInPlayMode && !showButton.Visibility.HasFlag(ButtonVisibility.InPlayMode)) continue; i++; ImGui.PushID(i); ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); if (method.GetParameters().Length > 0) { Log.Error($"Method {method.Name} cannot be executed from editor, because it expects arguments."); } else if (ImGui.Button(showButton.ButtonText)) { try { method.Invoke(reference, null); } catch (TargetInvocationException ex) { Log.Exception(ex.InnerException ?? ex); } catch (Exception ex) { Log.Exception(ex); } } ImGui.PopID(); } ImGui.PopID(); } struct ListPayload { public IList List; public int Element; } private static object? ShowListEditor(Type fieldType, object? fieldValue, string fieldName) { Type elementType = fieldType.GenericTypeArguments[0]; void AddElement(IList? list, out object? newList) { newList = DidNotChange; if (list == null) { newList = Activator.CreateInstance(fieldType); list = newList as IList; Debug.Assert(list != null); } try { object? newElement = ActivatorExtension.CreateDefaultValue(elementType); list?.Add(newElement); } catch (Exception ex) { Log.Error($"Failed to add new element to list: {ex}"); } } void RemoveElement(IList? list, out object? newList) { newList = DidNotChange; if (list == null) return; if (list.Count == 0) { newList = null; } else { list.RemoveAt(list.Count - 1); } } return ShowGenericListEditor(fieldType, elementType, (IEnumerable?)fieldValue, fieldName, AddElement, RemoveElement); } private static object? ShowArrayEditor(Type fieldType, object? fieldValue, string fieldName) { Debug.Assert(fieldType.IsArray); Type? elementType = fieldType.GetElementType(); Debug.Assert(elementType != null); Array? myArray = fieldValue as Array; if (myArray?.Rank > 1) { throw new NotImplementedException("Multidimensional arrays are not yet supported."); } void AddElement(IList? list, out object? newList) { newList = DidNotChange; int newIndex = 0; if (list == null) { newList = Array.CreateInstance(elementType, 1); } else { newIndex = list.Count; Array newArray = Array.CreateInstance(elementType, list.Count + 1); Array.Copy((Array)list, newArray, list.Count); newList = newArray; } object? newElement = ActivatorExtension.CreateDefaultValue(elementType!); ((IList)newList)[newIndex] = newElement; } void RemoveElement(IList? list, out object? newList) { newList = DidNotChange; if (list == null) return; if (list.Count == 0) { newList = null; } else { Array newArray = Array.CreateInstance(elementType, list.Count - 1); Array.Copy((Array)list, newArray, newArray.Length); newList = newArray; } } return ShowGenericListEditor(fieldType, elementType!, (IEnumerable?)fieldValue, fieldName, AddElement, RemoveElement); } public delegate void ModifyList(IList? currentList, out object? oldList); private static object? ShowGenericListEditor(Type listType, Type elementType, IEnumerable? list, string fieldName, ModifyList addElement, ModifyList removeElement) { object? newList = DidNotChange; IList? myList = list as IList; ImGui.BeginDisabled(myList == null); if (myList == null) ImGui.SetNextItemOpen(false); bool listOpen = ImGui.TreeNodeEx(fieldName, ImGuiTreeNodeFlags.AllowOverlap | ImGuiTreeNodeFlags.SpanAllColumns | ImGuiTreeNodeFlags.Framed); ImGui.EndDisabled(); ImGui.TableSetColumnIndex(1); var addButtonWidth = ImGui.CalcTextSize("+").X + 2 * ImGui.GetStyle().FramePadding.X; var removeButtonWidth = ImGui.CalcTextSize("-").X + 2 * ImGui.GetStyle().FramePadding.X; // TODO: Problem: when opening the list the buttons will move ImGui.SameLine(ImGui.GetContentRegionAvail().X - addButtonWidth - ImGui.GetStyle().FramePadding.X - removeButtonWidth); if (ImGui.SmallButton("+")) { addElement(myList, out newList); } ImGuiExtension.AttachTooltip("Add a new Element at the end of the list."); ImGui.SameLine(ImGui.GetContentRegionAvail().X - removeButtonWidth); ImGui.BeginDisabled(myList == null); if (ImGui.SmallButton("-")) { removeElement(myList, out newList); } ImGuiExtension.AttachTooltip("Remove the last Element from the list."); ImGui.EndDisabled(); if (listOpen) { Debug.Assert(myList != null, "Opened tree node even though the list is null!"); // Returns true if something was dropped into the droptarget bool DropTarget(int insertIndex, string tooltip) { bool dropped = false; if (ImGui.BeginDragDropTarget()) { ImGui.SetTooltip(tooltip); var payloadPtr = ImGui.AcceptDragDropPayload("SCRIPT_EDITOR_LIST_ELEMENT"); unsafe { if (payloadPtr.NativePtr != null) { ref ListPayload payload = ref Unsafe.AsRef((void*)payloadPtr.Data); // Make sure the index is still correct if (payload.Element < myList!.Count) { object item = myList[payload.Element]; myList.RemoveAt(payload.Element); if (payload.Element >= insertIndex) myList.Insert(insertIndex, item); else // If the original index is before the insert index, the insert index will be moved after removing the element // thus we have to subtract one. myList.Insert(insertIndex - 1, item); dropped = true; } } } ImGui.EndDragDropTarget(); } return dropped; } //// Drop at index 0 //ImGui.Separator(); //DropTarget(0, "Drop before Element 0."); bool orderChanged = false; for (int i = 0, id = 0; i < myList!.Count; i++, id++) { ImGui.PushID(id); object element = myList[i]; BeginNewRow(); // A Bullet point to grab the element ImGuiExtension.ListElementGrabber(); if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.SourceAllowNullID)) { unsafe { ListPayload payload = new() { List = myList, Element = i }; ImGui.SetDragDropPayload("SCRIPT_EDITOR_LIST_ELEMENT", (IntPtr)Unsafe.AsPointer(ref payload), (uint)Unsafe.SizeOf()); } ImGui.SetTooltip($"Move Element {i}"); ImGui.EndDragDropSource(); } orderChanged |= DropTarget(i, $"Drop before Element {i}."); if (ImGui.BeginPopupContextWindow($"ListElementContext{i}")) { if (ImGui.MenuItem($"Remove Element {i}")) { myList.RemoveAt(i); i--; ImGui.EndPopup(); ImGui.PopID(); continue; } ImGui.EndPopup(); } StartNewProperty_NewRow = false; object? newValue = ShowFieldEditor(element, element?.GetType() ?? elementType, $"Element {i}"); // Don't apply changes, when the order changed if (newValue != DidNotChange && !orderChanged) { myList[i] = newValue; } //// Drop after current element //ImGui.Separator(); //orderChanged |= DropTarget(i + 1, $"Drop after Element {i}."); ImGui.PopID(); } ImGui.TreePop(); } return newList; } 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); } }