// Copyright © Gamesmiths Guild. #if TOOLS using System; using Gamesmiths.Forge.Godot.Resources.Statescript; using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers; using Godot; using Godot.Collections; using GodotVariant = Godot.Variant; using GodotVector2 = Godot.Vector2; namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers; /// /// Resolver editor that holds a constant (inline) value. The user edits the value directly in the node. /// [Tool] internal sealed partial class VariantResolverEditor : NodeEditorProperty { private StatescriptVariableType _valueType; private bool _isArray; private bool _isArrayExpanded; private GodotVariant _currentValue; private Array _arrayValues = []; private Action? _onChanged; private Button? _toggleButton; private VBoxContainer? _elementsContainer; /// public override string DisplayName => "Constant"; /// public override string ResolverTypeId => "Variant"; /// public override bool IsCompatibleWith(Type expectedType) { return true; } /// public override void Setup( StatescriptGraph graph, StatescriptNodeProperty? property, Type expectedType, Action onChanged, bool isArray) { _isArray = isArray; _onChanged = onChanged; if (!StatescriptVariableTypeConverter.TryFromSystemType(expectedType, out _valueType)) { _valueType = StatescriptVariableType.Int; } if (property?.Resolver is VariantResolverResource variantRes) { _valueType = variantRes.ValueType; if (_isArray) { _arrayValues = [.. variantRes.ArrayValues]; _isArrayExpanded = variantRes.IsArrayExpanded; } else { _currentValue = variantRes.Value; } } else if (_isArray) { _arrayValues = []; } else { _currentValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(_valueType); } CustomMinimumSize = new GodotVector2(200, 40); if (_isArray) { VBoxContainer arrayEditor = CreateArrayEditor(); AddChild(arrayEditor); } else { Control valueEditor = CreateValueEditor(); AddChild(valueEditor); } } /// public override void SaveTo(StatescriptNodeProperty property) { if (_isArray) { property.Resolver = new VariantResolverResource { ValueType = _valueType, IsArray = true, ArrayValues = [.. _arrayValues], IsArrayExpanded = _isArrayExpanded, }; } else { property.Resolver = new VariantResolverResource { Value = _currentValue, ValueType = _valueType, }; } } /// public override void ClearCallbacks() { base.ClearCallbacks(); _onChanged = null; } private Control CreateValueEditor() { if (_valueType == StatescriptVariableType.Bool) { return StatescriptEditorControls.CreateBoolEditor(_currentValue.AsBool(), OnBoolValueChanged); } if (StatescriptEditorControls.IsIntegerType(_valueType) || StatescriptEditorControls.IsFloatType(_valueType)) { return StatescriptEditorControls.CreateNumericSpinSlider( _valueType, _currentValue.AsDouble(), OnNumericValueChanged); } if (StatescriptEditorControls.IsVectorType(_valueType)) { return StatescriptEditorControls.CreateVectorEditor( _valueType, x => StatescriptEditorControls.GetVectorComponent(_currentValue, _valueType, x), OnVectorValueChanged); } return new Label { Text = _valueType.ToString() }; } private void OnBoolValueChanged(bool x) { _currentValue = GodotVariant.From(x); _onChanged?.Invoke(); } private void OnNumericValueChanged(double x) { _currentValue = GodotVariant.From(x); _onChanged?.Invoke(); } private void OnVectorValueChanged(double[] x) { _currentValue = StatescriptEditorControls.BuildVectorVariant(_valueType, x); _onChanged?.Invoke(); } private VBoxContainer CreateArrayEditor() { var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; _elementsContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill, Visible = _isArrayExpanded, }; var headerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; vBox.AddChild(headerRow); _toggleButton = new Button { Text = $"Array (size {_arrayValues.Count})", SizeFlagsHorizontal = SizeFlags.ExpandFill, ToggleMode = true, ButtonPressed = _isArrayExpanded, }; _toggleButton.Toggled += OnArrayToggled; headerRow.AddChild(_toggleButton); Texture2D addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons"); var addButton = new Button { Icon = addIcon, Flat = true, TooltipText = "Add Element", CustomMinimumSize = new GodotVector2(24, 24), }; addButton.Pressed += OnAddElementPressed; headerRow.AddChild(addButton); vBox.AddChild(_elementsContainer); RebuildArrayElements(); return vBox; } private void OnArrayToggled(bool toggled) { if (_elementsContainer is not null) { _elementsContainer.Visible = toggled; } _isArrayExpanded = toggled; _onChanged?.Invoke(); RaiseLayoutSizeChanged(); } private void OnAddElementPressed() { GodotVariant defaultValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(_valueType); _arrayValues.Add(defaultValue); _onChanged?.Invoke(); if (_elementsContainer is not null) { _elementsContainer.Visible = true; } _isArrayExpanded = true; RebuildArrayElements(); RaiseLayoutSizeChanged(); } private void RebuildArrayElements() { if (_elementsContainer is null || _toggleButton is null) { return; } foreach (Node child in _elementsContainer.GetChildren()) { _elementsContainer.RemoveChild(child); child.Free(); } _toggleButton.Text = $"Array (size {_arrayValues.Count})"; Texture2D removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons"); for (var i = 0; i < _arrayValues.Count; i++) { var capturedIndex = i; if (StatescriptEditorControls.IsVectorType(_valueType)) { var elementVBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; _elementsContainer.AddChild(elementVBox); var labelRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; elementVBox.AddChild(labelRow); labelRow.AddChild(new Label { Text = $"[{i}]", SizeFlagsHorizontal = SizeFlags.ExpandFill, }); AddArrayRemoveButton(labelRow, removeIcon, capturedIndex); VBoxContainer vectorEditor = StatescriptEditorControls.CreateVectorEditor( _valueType, x => { return StatescriptEditorControls.GetVectorComponent( _arrayValues[capturedIndex], _valueType, x); }, x => { _arrayValues[capturedIndex] = StatescriptEditorControls.BuildVectorVariant(_valueType, x); _onChanged?.Invoke(); }); elementVBox.AddChild(vectorEditor); } else { var elementRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; _elementsContainer.AddChild(elementRow); elementRow.AddChild(new Label { Text = $"[{i}]" }); if (_valueType == StatescriptVariableType.Bool) { elementRow.AddChild(StatescriptEditorControls.CreateBoolEditor( _arrayValues[capturedIndex].AsBool(), x => { _arrayValues[capturedIndex] = GodotVariant.From(x); _onChanged?.Invoke(); })); } else { EditorSpinSlider spin = StatescriptEditorControls.CreateNumericSpinSlider( _valueType, _arrayValues[capturedIndex].AsDouble(), x => { _arrayValues[capturedIndex] = GodotVariant.From(x); _onChanged?.Invoke(); }); elementRow.AddChild(spin); } AddArrayRemoveButton(elementRow, removeIcon, capturedIndex); } } } private void AddArrayRemoveButton( HBoxContainer row, Texture2D removeIcon, int elementIndex) { var removeButton = new Button { Icon = removeIcon, Flat = true, TooltipText = "Remove Element", CustomMinimumSize = new GodotVector2(24, 24), }; var handler = new ArrayRemoveHandler(this, elementIndex); removeButton.AddChild(handler); removeButton.Pressed += handler.HandlePressed; row.AddChild(removeButton); } private void OnRemoveElement(int elementIndex) { _arrayValues.RemoveAt(elementIndex); _onChanged?.Invoke(); RebuildArrayElements(); RaiseLayoutSizeChanged(); } /// /// Godot-compatible signal handler for array element remove buttons. Holds the element index and a reference to the /// owning editor so the Pressed signal can be handled without a lambda. /// [Tool] private sealed partial class ArrayRemoveHandler : Node { private readonly VariantResolverEditor _editor; private readonly int _elementIndex; public ArrayRemoveHandler() { _editor = null!; } public ArrayRemoveHandler(VariantResolverEditor editor, int elementIndex) { _editor = editor; _elementIndex = elementIndex; } public void HandlePressed() { _editor.OnRemoveElement(_elementIndex); } } } #endif