// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
///
/// Shared factory methods for creating value-editor controls used by both the variable panel and resolver editors.
///
internal static partial class StatescriptEditorControls
{
private static readonly Color _axisXColor = new(0.96f, 0.37f, 0.37f);
private static readonly Color _axisYColor = new(0.54f, 0.83f, 0.01f);
private static readonly Color _axisZColor = new(0.33f, 0.55f, 0.96f);
private static readonly Color _axisWColor = new(0.66f, 0.66f, 0.66f);
private static StyleBox? _cachedPanelStyle;
///
/// Returns for integer-like variable types.
///
/// The variable type to check.
public static bool IsIntegerType(StatescriptVariableType type)
{
return type is StatescriptVariableType.Int or StatescriptVariableType.UInt
or StatescriptVariableType.Long or StatescriptVariableType.ULong
or StatescriptVariableType.Short or StatescriptVariableType.UShort
or StatescriptVariableType.Byte or StatescriptVariableType.SByte
or StatescriptVariableType.Char;
}
///
/// Returns for floating-point variable types.
///
/// The variable type to check.
public static bool IsFloatType(StatescriptVariableType type)
{
return type is StatescriptVariableType.Float or StatescriptVariableType.Double
or StatescriptVariableType.Decimal;
}
///
/// Returns for multi-component vector/quaternion/plane variable types.
///
/// The variable type to check.
public static bool IsVectorType(StatescriptVariableType type)
{
return type is StatescriptVariableType.Vector2 or StatescriptVariableType.Vector3
or StatescriptVariableType.Vector4 or StatescriptVariableType.Plane
or StatescriptVariableType.Quaternion;
}
///
/// Creates a wrapping a for boolean editing.
///
/// The initial value of the boolean.
/// An action invoked on value change.
/// A containing a .
public static PanelContainer CreateBoolEditor(bool value, Action onChanged)
{
var container = new PanelContainer
{
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
};
container.AddThemeStyleboxOverride("panel", GetPanelStyle());
var checkButton = new CheckBox
{
Text = "On",
ButtonPressed = value,
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
};
var handler = new BoolSignalHandler { OnChanged = onChanged };
checkButton.AddChild(handler);
checkButton.Toggled += handler.HandleToggled;
container.AddChild(checkButton);
return container;
}
///
/// Creates an configured for the given numeric variable type.
///
/// The type of the numeric variable.
/// The initial value of the numeric variable.
/// An action invoked on value change.
/// An configured for the specified numeric variable type.
public static EditorSpinSlider CreateNumericSpinSlider(
StatescriptVariableType type,
double value,
Action? onChanged = null)
{
NumericConfig config = GetNumericConfig(type);
var spin = new EditorSpinSlider
{
Step = config.Step,
Rounded = config.IsInteger,
EditingInteger = config.IsInteger,
MinValue = config.MinValue,
MaxValue = config.MaxValue,
AllowGreater = config.AllowBeyondRange,
AllowLesser = config.AllowBeyondRange,
ControlState = config.IsInteger
? EditorSpinSlider.ControlStateEnum.Default
: EditorSpinSlider.ControlStateEnum.Hide,
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
Value = value,
};
var handler = new NumericSpinHandler(spin) { OnChanged = onChanged };
spin.AddChild(handler);
if (onChanged is not null)
{
spin.ValueChanged += handler.HandleValueChanged;
}
spin.Grabbed += handler.HandleGrabbed;
spin.Ungrabbed += handler.HandleUngrabbed;
spin.FocusExited += handler.HandleFocusExited;
return spin;
}
///
/// Creates a panel with a row of labelled controls for editing a vector value.
///
/// The type of the vector/quaternion/plane.
/// A function to retrieve the value of a specific component.
/// An action to invoke when any component value changes.
/// A containing the vector editor controls.
public static VBoxContainer CreateVectorEditor(
StatescriptVariableType type,
Func getComponent,
Action? onChanged)
{
var componentCount = GetVectorComponentCount(type);
var labels = GetVectorComponentLabels(type);
var vBox = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
var row = new HBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
row.AddThemeConstantOverride("separation", 0);
var panelContainer = new PanelContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
panelContainer.AddThemeStyleboxOverride("panel", GetPanelStyle());
vBox.AddChild(panelContainer);
panelContainer.AddChild(row);
var values = new double[componentCount];
var handler = new VectorComponentHandler(values) { OnChanged = onChanged };
vBox.AddChild(handler);
for (var i = 0; i < componentCount; i++)
{
values[i] = getComponent(i);
var spin = new EditorSpinSlider
{
Label = labels[i],
Step = 0.001,
Rounded = false,
EditingInteger = false,
AllowGreater = true,
AllowLesser = true,
Flat = false,
ControlState = EditorSpinSlider.ControlStateEnum.Hide,
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
SizeFlagsStretchRatio = 1,
CustomMinimumSize = new Vector2(71, 0),
Value = values[i],
};
spin.AddThemeColorOverride("label_color", GetComponentColor(i));
var componentHandler = new VectorSpinHandler(handler, i);
spin.AddChild(componentHandler);
spin.ValueChanged += componentHandler.HandleValueChanged;
spin.Grabbed += componentHandler.HandleGrabbed;
spin.Ungrabbed += componentHandler.HandleUngrabbed;
spin.FocusExited += componentHandler.HandleFocusExited;
row.AddChild(spin);
}
return vBox;
}
///
/// Reads a single component from a vector/quaternion/plane variant.
///
/// The variant containing the vector/quaternion/plane value.
/// The type of the vector/quaternion/plane.
/// The index of the component to retrieve.
/// Exception thrown if the provided type is not a vector/quaternion/plane
/// type.
public static double GetVectorComponent(Variant value, StatescriptVariableType type, int index)
{
return type switch
{
StatescriptVariableType.Vector2 => index == 0
? value.AsVector2().X
: value.AsVector2().Y,
StatescriptVariableType.Vector3 => index switch
{
0 => value.AsVector3().X,
1 => value.AsVector3().Y,
_ => value.AsVector3().Z,
},
StatescriptVariableType.Vector4 => index switch
{
0 => value.AsVector4().X,
1 => value.AsVector4().Y,
2 => value.AsVector4().Z,
_ => value.AsVector4().W,
},
StatescriptVariableType.Plane => index switch
{
0 => value.AsPlane().Normal.X,
1 => value.AsPlane().Normal.Y,
2 => value.AsPlane().Normal.Z,
_ => value.AsPlane().D,
},
StatescriptVariableType.Quaternion => index switch
{
0 => value.AsQuaternion().X,
1 => value.AsQuaternion().Y,
2 => value.AsQuaternion().Z,
_ => value.AsQuaternion().W,
},
StatescriptVariableType.Bool => throw new NotImplementedException(),
StatescriptVariableType.Byte => throw new NotImplementedException(),
StatescriptVariableType.SByte => throw new NotImplementedException(),
StatescriptVariableType.Char => throw new NotImplementedException(),
StatescriptVariableType.Decimal => throw new NotImplementedException(),
StatescriptVariableType.Double => throw new NotImplementedException(),
StatescriptVariableType.Float => throw new NotImplementedException(),
StatescriptVariableType.Int => throw new NotImplementedException(),
StatescriptVariableType.UInt => throw new NotImplementedException(),
StatescriptVariableType.Long => throw new NotImplementedException(),
StatescriptVariableType.ULong => throw new NotImplementedException(),
StatescriptVariableType.Short => throw new NotImplementedException(),
StatescriptVariableType.UShort => throw new NotImplementedException(),
_ => 0,
};
}
///
/// Builds a Godot from a component array for the given vector/quaternion/plane type.
///
/// The type of the vector/quaternion/plane.
/// The array of component values.
/// A representing the vector/quaternion/plane.
/// Exception thrown if the provided type is not a vector/quaternion/plane
/// type.
public static Variant BuildVectorVariant(StatescriptVariableType type, double[] values)
{
return type switch
{
StatescriptVariableType.Vector2 => Variant.From(
new Vector2((float)values[0], (float)values[1])),
StatescriptVariableType.Vector3 => Variant.From(
new Vector3(
(float)values[0],
(float)values[1],
(float)values[2])),
StatescriptVariableType.Vector4 => Variant.From(
new Vector4(
(float)values[0],
(float)values[1],
(float)values[2],
(float)values[3])),
StatescriptVariableType.Plane => Variant.From(
new Plane(
new Vector3(
(float)values[0],
(float)values[1],
(float)values[2]),
(float)values[3])),
StatescriptVariableType.Quaternion => Variant.From(
new Quaternion(
(float)values[0],
(float)values[1],
(float)values[2],
(float)values[3])),
StatescriptVariableType.Bool => throw new NotImplementedException(),
StatescriptVariableType.Byte => throw new NotImplementedException(),
StatescriptVariableType.SByte => throw new NotImplementedException(),
StatescriptVariableType.Char => throw new NotImplementedException(),
StatescriptVariableType.Decimal => throw new NotImplementedException(),
StatescriptVariableType.Double => throw new NotImplementedException(),
StatescriptVariableType.Float => throw new NotImplementedException(),
StatescriptVariableType.Int => throw new NotImplementedException(),
StatescriptVariableType.UInt => throw new NotImplementedException(),
StatescriptVariableType.Long => throw new NotImplementedException(),
StatescriptVariableType.ULong => throw new NotImplementedException(),
StatescriptVariableType.Short => throw new NotImplementedException(),
StatescriptVariableType.UShort => throw new NotImplementedException(),
_ => Variant.From(0),
};
}
private static int GetVectorComponentCount(StatescriptVariableType type)
{
return type switch
{
StatescriptVariableType.Vector2 => 2,
StatescriptVariableType.Vector3 => 3,
StatescriptVariableType.Vector4 => 4,
StatescriptVariableType.Plane => 4,
StatescriptVariableType.Quaternion => 4,
StatescriptVariableType.Bool => throw new NotImplementedException(),
StatescriptVariableType.Byte => throw new NotImplementedException(),
StatescriptVariableType.SByte => throw new NotImplementedException(),
StatescriptVariableType.Char => throw new NotImplementedException(),
StatescriptVariableType.Decimal => throw new NotImplementedException(),
StatescriptVariableType.Double => throw new NotImplementedException(),
StatescriptVariableType.Float => throw new NotImplementedException(),
StatescriptVariableType.Int => throw new NotImplementedException(),
StatescriptVariableType.UInt => throw new NotImplementedException(),
StatescriptVariableType.Long => throw new NotImplementedException(),
StatescriptVariableType.ULong => throw new NotImplementedException(),
StatescriptVariableType.Short => throw new NotImplementedException(),
StatescriptVariableType.UShort => throw new NotImplementedException(),
_ => 4,
};
}
private static string[] GetVectorComponentLabels(StatescriptVariableType type)
{
return type switch
{
StatescriptVariableType.Vector2 => ["x", "y"],
StatescriptVariableType.Vector3 => ["x", "y", "z"],
StatescriptVariableType.Plane => ["x", "y", "z", "d"],
StatescriptVariableType.Vector4 => ["x", "y", "z", "w"],
StatescriptVariableType.Quaternion => ["x", "y", "z", "w"],
StatescriptVariableType.Bool => throw new NotImplementedException(),
StatescriptVariableType.Byte => throw new NotImplementedException(),
StatescriptVariableType.SByte => throw new NotImplementedException(),
StatescriptVariableType.Char => throw new NotImplementedException(),
StatescriptVariableType.Decimal => throw new NotImplementedException(),
StatescriptVariableType.Double => throw new NotImplementedException(),
StatescriptVariableType.Float => throw new NotImplementedException(),
StatescriptVariableType.Int => throw new NotImplementedException(),
StatescriptVariableType.UInt => throw new NotImplementedException(),
StatescriptVariableType.Long => throw new NotImplementedException(),
StatescriptVariableType.ULong => throw new NotImplementedException(),
StatescriptVariableType.Short => throw new NotImplementedException(),
StatescriptVariableType.UShort => throw new NotImplementedException(),
_ => ["x", "y", "z", "w"],
};
}
private static Color GetComponentColor(int index)
{
return index switch
{
0 => _axisXColor,
1 => _axisYColor,
2 => _axisZColor,
_ => _axisWColor,
};
}
private static NumericConfig GetNumericConfig(StatescriptVariableType type)
{
return type switch
{
StatescriptVariableType.Byte => new NumericConfig(byte.MinValue, byte.MaxValue, 1, true, false),
StatescriptVariableType.SByte => new NumericConfig(sbyte.MinValue, sbyte.MaxValue, 1, true, false),
StatescriptVariableType.Char => new NumericConfig(char.MinValue, char.MaxValue, 1, true, false),
StatescriptVariableType.Short => new NumericConfig(short.MinValue, short.MaxValue, 1, true, false),
StatescriptVariableType.UShort => new NumericConfig(ushort.MinValue, ushort.MaxValue, 1, true, false),
StatescriptVariableType.Int => new NumericConfig(int.MinValue, int.MaxValue, 1, true, false),
StatescriptVariableType.UInt => new NumericConfig(uint.MinValue, uint.MaxValue, 1, true, false),
// Godot's interface starts acting weird if we try to use the full range of long/ulong, so we clamp to +/-
// 9e18 which should be sufficient for most use cases.
StatescriptVariableType.Long => new NumericConfig(-9e18, 9e18, 1, true, false),
StatescriptVariableType.ULong => new NumericConfig(0, 9e18, 1, true, false),
StatescriptVariableType.Float => new NumericConfig(-1e10, 1e10, 0.001, false, true),
StatescriptVariableType.Double => new NumericConfig(-1e10, 1e10, 0.001, false, true),
StatescriptVariableType.Decimal => new NumericConfig(-1e10, 1e10, 0.001, false, true),
StatescriptVariableType.Bool => throw new NotImplementedException(),
StatescriptVariableType.Vector2 => throw new NotImplementedException(),
StatescriptVariableType.Vector3 => throw new NotImplementedException(),
StatescriptVariableType.Vector4 => throw new NotImplementedException(),
StatescriptVariableType.Plane => throw new NotImplementedException(),
StatescriptVariableType.Quaternion => throw new NotImplementedException(),
_ => new NumericConfig(-1e10, 1e10, 0.001, false, true),
};
}
private readonly record struct NumericConfig(
double MinValue,
double MaxValue,
double Step,
bool IsInteger,
bool AllowBeyondRange);
private static StyleBox GetPanelStyle()
{
if (_cachedPanelStyle is null || !GodotObject.IsInstanceValid(_cachedPanelStyle))
{
Control baseControl = EditorInterface.Singleton.GetBaseControl();
_cachedPanelStyle = (StyleBox)baseControl.GetThemeStylebox("normal", "LineEdit").Duplicate();
_cachedPanelStyle.SetContentMarginAll(0);
}
return _cachedPanelStyle;
}
}
#endif