// Copyright © Gamesmiths Guild. #if TOOLS using System; using System.Collections.Generic; using Gamesmiths.Forge.Godot.Resources.Statescript; using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers; using Godot; using Godot.Collections; using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128; 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 StatescriptVariableType[] _allowedValueTypes = []; private Button? _toggleButton; private VBoxContainer? _elementsContainer; private VBoxContainer? _contentContainer; /// 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; _allowedValueTypes = ResolveAllowedValueTypes(expectedType); _valueType = GetDefaultValueType(expectedType); if (property?.Resolver is VariantResolverResource variantRes) { if (IsAllowedValueType(variantRes.ValueType)) { _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); var root = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; AddChild(root); if (_allowedValueTypes.Length > 1) { root.AddChild(CreateTypeRow()); } _contentContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; root.AddChild(_contentContainer); RebuildContent(); } /// 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 bool TryGetInlineSummary(out string summary) { if (_isArray) { summary = string.Empty; return false; } summary = InlineConstantSummaryFormatter.FormatVariant(_currentValue, _valueType); return true; } /// public override InlineSummaryBadgeKind GetInlineSummaryBadgeKind() { return InlineConstantSummaryFormatter.GetBadgeKind(_valueType); } /// public override void ClearCallbacks() { base.ClearCallbacks(); _onChanged = null; _toggleButton = null; _elementsContainer = null; _contentContainer = null; } private void RebuildContent() { if (_contentContainer is null) { return; } foreach (Node child in _contentContainer.GetChildren()) { _contentContainer.RemoveChild(child); child.Free(); } if (_isArray) { _contentContainer.AddChild(CreateArrayEditor()); } else { _contentContainer.AddChild(CreateValueEditor()); } } private HBoxContainer CreateTypeRow() { var row = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; row.AddChild(new Label { Text = "Type:", CustomMinimumSize = new GodotVector2(45, 0), HorizontalAlignment = HorizontalAlignment.Right, }); var dropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill }; for (int i = 0; i < _allowedValueTypes.Length; i++) { dropdown.AddItem(StatescriptVariableTypeConverter.GetDisplayName(_allowedValueTypes[i])); if (_allowedValueTypes[i] == _valueType) { dropdown.Selected = i; } } dropdown.ItemSelected += OnTypeDropdownItemSelected; row.AddChild(dropdown); return row; } private void OnTypeDropdownItemSelected(long index) { int selectedIndex = (int)index; if (selectedIndex < 0 || selectedIndex >= _allowedValueTypes.Length) { return; } if (_valueType == _allowedValueTypes[selectedIndex]) { return; } _valueType = _allowedValueTypes[selectedIndex]; ResetValuesForCurrentType(); RebuildContent(); _onChanged?.Invoke(); RaiseLayoutSizeChanged(); } 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 (int i = 0; i < _arrayValues.Count; i++) { int 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(); } private StatescriptVariableType[] ResolveAllowedValueTypes(Type expectedType) { Type[] allowedExpectedTypes = GetAllowedExpectedTypes(expectedType); for (int i = 0; i < allowedExpectedTypes.Length; i++) { if (allowedExpectedTypes[i] == typeof(ForgeVariant128)) { return StatescriptVariableTypeConverter.GetAllTypes(); } } var result = new List(); for (int i = 0; i < allowedExpectedTypes.Length; i++) { Type allowedType = allowedExpectedTypes[i]; if (StatescriptVariableTypeConverter.TryFromSystemType(allowedType, out StatescriptVariableType valueType) && !result.Contains(valueType)) { result.Add(valueType); } } return result.Count > 0 ? [.. result] : [StatescriptVariableType.Int]; } private StatescriptVariableType GetDefaultValueType(Type expectedType) { if (expectedType == typeof(ForgeVariant128)) { return StatescriptVariableType.Int; } if (_allowedValueTypes.Length > 0) { return _allowedValueTypes[0]; } return StatescriptVariableTypeConverter.TryFromSystemType(expectedType, out StatescriptVariableType valueType) ? valueType : StatescriptVariableType.Int; } private bool IsAllowedValueType(StatescriptVariableType valueType) { for (int i = 0; i < _allowedValueTypes.Length; i++) { if (_allowedValueTypes[i] == valueType) { return true; } } return false; } private void ResetValuesForCurrentType() { GodotVariant defaultValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(_valueType); if (_isArray) { for (int i = 0; i < _arrayValues.Count; i++) { _arrayValues[i] = defaultValue; } } else { _currentValue = defaultValue; } } /// /// 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