diff --git a/Silksong.ModMenu/Elements/ColorInput.cs b/Silksong.ModMenu/Elements/ColorInput.cs
new file mode 100644
index 0000000..3d8b7c1
--- /dev/null
+++ b/Silksong.ModMenu/Elements/ColorInput.cs
@@ -0,0 +1,129 @@
+using Silksong.ModMenu.Internal;
+using Silksong.ModMenu.Models;
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace Silksong.ModMenu.Elements;
+
+///
+/// Selectable element that accepts input in 3, 6, or 8 character hex strings.
+/// Includes a preview swatch beside the hex code.
+///
+public class ColorInput : TextInput
+{
+ ///
+ /// The relative size of the Swatch to the choice text when the input was first created.
+ /// Used to automatically resize the Swatch when is called.
+ ///
+ private readonly float swatchSizeMultiplier;
+
+ ///
+ /// Construct a color input with no description.
+ ///
+ public ColorInput(LocalizedText label)
+ : this(label, "") { }
+
+ ///
+ /// Construct a color input with a description.
+ ///
+ public ColorInput(LocalizedText label, LocalizedText description)
+ : base(label, TextModels.ForHexColors(), description)
+ {
+ Container.name = $"{label.Canonical} Color Input";
+ InputField.contentType = InputField.ContentType.Custom;
+ InputField.onValidateInput = HexValidation;
+ ApplyDefaultColors = true;
+ Format = InputFormat.RGBA;
+
+ Swatch = MenuPrefabs.Get().NewColorSwatch().RectTransform;
+ Swatch.SetParent(InputField.textComponent.transform, false);
+ Swatch.gameObject.SetActive(true);
+ SwatchFill = Swatch.Find("Fill").GetComponent();
+ SwatchOutline = Swatch.Find("Outline").GetComponent();
+ InvalidValueIndicator = Swatch.Find("Invalid Indicator").GetComponent();
+
+ swatchSizeMultiplier = Swatch.rect.height / InputField.textComponent.preferredHeight;
+
+ OnTextValueChanged += _ =>
+ {
+ SwatchFill.color = Value;
+ State = TextModel.IsTextValid ? ElementState.DEFAULT : ElementState.INVALID;
+ InvalidValueIndicator.enabled = !TextModel.IsTextValid;
+ };
+
+ Value = Color.clear;
+ }
+
+ ///
+ /// Whether or not this input accepts 8-character RGBA strings.
+ ///
+ public InputFormat Format
+ {
+ get => field;
+ set
+ {
+ field = value;
+ InputField.characterLimit = (int)field;
+
+ // force the field to re-clamp the length of the current text value
+ string val = InputField.text;
+ InputField.SetTextWithoutNotify("");
+ InputField.text = val;
+ }
+ }
+
+ ///
+ /// Semantic input formats for s.
+ ///
+ public enum InputFormat
+ {
+ ///
+ /// The input accepts at most 6-character RGB hex codes.
+ ///
+ RGB = 6,
+
+ ///
+ /// The input accepts at most 8-character RGBA hex codes.
+ ///
+ RGBA = 8,
+ }
+
+ ///
+ /// The unity component that controls the size and position of the preview swatch.
+ ///
+ public readonly RectTransform Swatch;
+
+ ///
+ /// The unity component for the filled area of the preview swatch.
+ ///
+ public readonly Image SwatchFill;
+
+ ///
+ /// The unity component for the outline around the preview swatch.
+ ///
+ public readonly Image SwatchOutline;
+
+ ///
+ /// The unity component for the symbol that appears in the preview swatch
+ /// to visually indicate an invalid value.
+ ///
+ public readonly Text InvalidValueIndicator;
+
+ ///
+ ///
+ /// This will also adjust the size and position of the .
+ ///
+ public override void SetFontSizes(FontSizes fontSizes)
+ {
+ base.SetFontSizes(fontSizes);
+ float size = InputField.textComponent.preferredHeight * swatchSizeMultiplier;
+ Swatch.sizeDelta = Vector2.one * size;
+ Swatch.anchoredPosition = Swatch.anchoredPosition with { x = -0.5f * size };
+ }
+
+ ///
+ /// validation for hex codes; only accepts characters a-fA-F0-7.
+ ///
+ static char HexValidation(string input, int index, char addedChar) =>
+ $"{addedChar}".TryParseHex(out _) ? char.ToUpper(addedChar) : '0';
+}
diff --git a/Silksong.ModMenu/Internal/CustomInputField.cs b/Silksong.ModMenu/Internal/CustomInputField.cs
index f55a599..fd08d75 100644
--- a/Silksong.ModMenu/Internal/CustomInputField.cs
+++ b/Silksong.ModMenu/Internal/CustomInputField.cs
@@ -23,16 +23,17 @@ internal class CustomInputField : InputField
{
base.Awake();
textRect = textComponent.gameObject.GetComponent();
+ textComponent.alignment = TextAnchor.MiddleRight;
+ textRect.anchorMin = textRect.anchorMin with { x = 1 };
+ textRect.anchorMax = textRect.anchorMax with { x = 1 };
+ textRect.pivot = textRect.pivot with { x = 1 };
+ textRect.anchoredPosition = Vector2.zero;
}
private void Update()
{
- if (textRect == null)
- return;
-
- var width = Mathf.Max(200, preferredWidth);
- textRect.offsetMin = new(-width, 0);
- textRect.sizeDelta = new(width, 0);
+ if (textRect)
+ textRect.sizeDelta = new(Mathf.Max(200, preferredWidth), 0);
}
private bool AllSelected() =>
diff --git a/Silksong.ModMenu/Internal/GameObjectUtil.cs b/Silksong.ModMenu/Internal/GameObjectUtil.cs
index 3ea20d9..72aedb4 100644
--- a/Silksong.ModMenu/Internal/GameObjectUtil.cs
+++ b/Silksong.ModMenu/Internal/GameObjectUtil.cs
@@ -4,14 +4,25 @@ namespace Silksong.ModMenu.Internal;
internal static class GameObjectUtil
{
- internal static void DestroyAllChildren(this GameObject self)
+ extension(GameObject self)
{
- for (int i = self.transform.childCount - 1; i >= 0; i--)
+ internal void DestroyAllChildren()
{
- var obj = self.transform.GetChild(i).gameObject;
- obj.transform.SetParent(null);
- Object.Destroy(obj);
+ for (int i = self.transform.childCount - 1; i >= 0; i--)
+ {
+ var obj = self.transform.GetChild(i).gameObject;
+ obj.transform.SetParent(null);
+ Object.Destroy(obj);
+ }
}
+
+ ///
+ /// Casts the object's transform to a .
+ ///
+ ///
+ /// If the object doesn't actually have a RectTransform.
+ ///
+ internal RectTransform RectTransform => (RectTransform)self.transform;
}
private class InactiveScope : System.IDisposable
diff --git a/Silksong.ModMenu/Internal/MenuPrefabs.cs b/Silksong.ModMenu/Internal/MenuPrefabs.cs
index 7312708..ff4e78b 100644
--- a/Silksong.ModMenu/Internal/MenuPrefabs.cs
+++ b/Silksong.ModMenu/Internal/MenuPrefabs.cs
@@ -30,6 +30,7 @@ internal static MenuPrefabs Get() =>
private readonly GameObject textInputTemplate;
private readonly GameObject sliderTemplate;
private readonly GameObject scrollPaneTemplate;
+ private readonly GameObject colorSwatchTemplate;
private MenuPrefabs(UIManager uiManager)
{
@@ -163,6 +164,55 @@ private MenuPrefabs(UIManager uiManager)
sliderChild.FindChild("MasterVolValue")!.name = "Value";
scrollPaneTemplate = ConstructScrollPanePrefab(uiManager);
+
+ {
+ colorSwatchTemplate = new GameObject("Color Swatch") { layer = (int)PhysLayers.UI };
+ colorSwatchTemplate.SetActive(false);
+ Object.DontDestroyOnLoad(colorSwatchTemplate);
+ var swatchRT = colorSwatchTemplate.AddComponent();
+ swatchRT.sizeDelta = Vector2.one * 70;
+ swatchRT.anchorMax = swatchRT.anchorMin = new Vector2(0, 0.5f);
+ swatchRT.pivot = new Vector2(1, 0.5f);
+ swatchRT.anchoredPosition = new Vector2(-0.5f * swatchRT.sizeDelta.x, 0);
+
+ Transform journalIcon = GameCameras.instance.hudCamera.transform.Find(
+ "In-game/Inventory/Journal/Enemy List Parent/Enemy List/Template Journal Entry"
+ );
+
+ var fill = new GameObject("Fill") { layer = (int)PhysLayers.UI };
+ fill.transform.SetParentReset(swatchRT);
+ var imgF = fill.AddComponent();
+ imgF.sprite = journalIcon.Find("Mask").GetComponent().sprite;
+ imgF.preserveAspect = true;
+ fill.RectTransform.FitToParent();
+
+ var line = new GameObject("Outline") { layer = (int)PhysLayers.UI };
+ line.transform.SetParentReset(swatchRT);
+ var imgL = line.AddComponent();
+ imgL.sprite = journalIcon.Find("Standard Frame").GetComponent().sprite;
+ imgL.preserveAspect = true;
+ line.AddComponent().effectColor = Color.white with { a = 0.5f };
+ line.RectTransform.FitToParent();
+
+ var indicator = new GameObject("Invalid Indicator") { layer = (int)PhysLayers.UI };
+ indicator.transform.SetParentReset(swatchRT);
+ var text = indicator.AddComponent();
+ text.enabled = false;
+ text.text = "?";
+ text.font = textLabelTemplate.GetComponent().font;
+ text.alignByGeometry = true;
+ text.alignment = TextAnchor.MiddleCenter;
+ text.horizontalOverflow = HorizontalWrapMode.Overflow;
+ text.verticalOverflow = VerticalWrapMode.Truncate;
+ text.resizeTextForBestFit = true;
+ text.resizeTextMaxSize = 100;
+ text.resizeTextMinSize = 10;
+ text.fontSize = 0;
+ var indicatorRT = indicator.RectTransform;
+ indicatorRT.sizeDelta = Vector2.zero;
+ indicatorRT.anchorMax = new Vector2(0.52f, 0.8f);
+ indicatorRT.anchorMin = new Vector2(0.52f, 0.2f);
+ }
}
internal GameObject NewCustomMenu(LocalizedText title)
@@ -205,6 +255,8 @@ internal GameObject NewTextInputContainer(out CustomInputField customInputField)
return obj;
}
+ internal GameObject NewColorSwatch() => Object.Instantiate(colorSwatchTemplate);
+
internal GameObject NewSliderContainer(out Slider slider)
{
var obj = Object.Instantiate(sliderTemplate);
diff --git a/Silksong.ModMenu/Internal/StringUtil.cs b/Silksong.ModMenu/Internal/StringUtil.cs
index c164cc7..95ddf5f 100644
--- a/Silksong.ModMenu/Internal/StringUtil.cs
+++ b/Silksong.ModMenu/Internal/StringUtil.cs
@@ -1,4 +1,5 @@
-using System.Text;
+using System.Globalization;
+using System.Text;
namespace Silksong.ModMenu.Internal;
@@ -50,4 +51,7 @@ internal static string UnCamelCase(this string self)
return sb.ToString();
}
+
+ internal static bool TryParseHex(this string self, out byte n) =>
+ byte.TryParse(self, NumberStyles.HexNumber, null, out n);
}
diff --git a/Silksong.ModMenu/Models/TextModels.cs b/Silksong.ModMenu/Models/TextModels.cs
index b96cee4..6e91719 100644
--- a/Silksong.ModMenu/Models/TextModels.cs
+++ b/Silksong.ModMenu/Models/TextModels.cs
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using Silksong.ModMenu.Internal;
+using UnityEngine;
namespace Silksong.ModMenu.Models;
@@ -64,4 +67,55 @@ bool Check(T value)
return Check;
}
+
+ ///
+ /// An ITextModel which parses 3, 6, or 8 character hex strings to and from s.
+ ///
+ public static ParserTextModel ForHexColors() =>
+ new(HexParser, HexUnparser, INVALID_COLOR);
+
+ private static readonly Color INVALID_COLOR = new(-1, -1, -1, -1);
+
+ private static bool HexParser(string x, out Color c)
+ {
+ if (x.Length == 8 && x[6..8].TryParseHex(out byte a)) { }
+ else
+ a = byte.MaxValue;
+
+ if (
+ (x.Length == 6 || x.Length == 8)
+ && x[0..2].TryParseHex(out byte r)
+ && x[2..4].TryParseHex(out byte g)
+ && x[4..6].TryParseHex(out byte b)
+ )
+ {
+ c = new Color32(r, g, b, a);
+ return true;
+ }
+ else if (
+ x.Length == 3
+ && $"{x[0]}{x[0]}".TryParseHex(out r)
+ && $"{x[1]}{x[1]}".TryParseHex(out g)
+ && $"{x[2]}{x[2]}".TryParseHex(out b)
+ )
+ {
+ c = new Color32(r, g, b, a);
+ return true;
+ }
+
+ c = INVALID_COLOR;
+ return false;
+ }
+
+ private static bool HexUnparser(Color c, out string x)
+ {
+ if (!Enumerable.Range(0, 3).Any(i => c[i] < 0 || 1 < c[i]))
+ {
+ Color32 c32 = c;
+ x = $"{c32.r:X2}{c32.g:X2}{c32.b:X2}{c32.a:X2}";
+ return true;
+ }
+ x = "###";
+ return false;
+ }
}
diff --git a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs
index 254c45f..71416a0 100644
--- a/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs
+++ b/Silksong.ModMenu/Plugin/ConfigEntryFactory.cs
@@ -36,6 +36,7 @@ public delegate bool MenuElementGenerator(
GenerateIntElement,
GenerateFloatElement,
GenerateStringElement,
+ GenerateColorElement,
];
///
@@ -143,10 +144,20 @@ public virtual bool GenerateEntryButton(
LocalizedText name,
BaseUnityPlugin plugin,
[MaybeNullWhen(false)] out SelectableElement selectableElement
+ ) => GenerateEntryButton(name, plugin.Config, out _, out selectableElement);
+
+ ///
+ /// Generate a button which opens a sub-menu for this ConfigFile.
+ ///
+ public virtual bool GenerateEntryButton(
+ LocalizedText name,
+ ConfigFile config,
+ [MaybeNullWhen(false)] out AbstractMenuScreen menuScreen,
+ [MaybeNullWhen(false)] out SelectableElement selectableElement
)
{
TreeNode elementsTree = new();
- foreach (var entry in plugin.Config.OrderBy(e => e.Key.Key))
+ foreach (var entry in config.OrderBy(e => e.Key.Key))
{
if (GenerateMenuElement(entry.Value, out var element))
{
@@ -168,6 +179,7 @@ public virtual bool GenerateEntryButton(
);
if (elementsTree.Value.TotalElements == 0)
{
+ menuScreen = default;
selectableElement = default;
return false;
}
@@ -176,7 +188,8 @@ public virtual bool GenerateEntryButton(
List subpageNames = [];
FindFirstNonEmptyChild(ref elementsTree, subpageNames);
- var menu = BuildSubtreeScreen(name, subpageNames, elementsTree);
+ AbstractMenuScreen menu = BuildSubtreeScreen(name, subpageNames, elementsTree);
+ menuScreen = menu;
selectableElement = new TextButton(name)
{
OnSubmit = () => MenuScreenNavigation.Show(menu),
@@ -419,6 +432,33 @@ public static bool GenerateStringElement(
return true;
}
+ ///
+ /// Generate a text element for a color.
+ ///
+ public static bool GenerateColorElement(
+ ConfigEntryBase entry,
+ [MaybeNullWhen(false)] out MenuElement menuElement
+ )
+ {
+ if (entry is not ConfigEntry colorEntry)
+ {
+ menuElement = default;
+ return false;
+ }
+
+ ColorInput color = new(entry.LabelName(), entry.DescriptionLine())
+ {
+ Format =
+ (entry.Description.AcceptableValues is RGBColorValues)
+ ? ColorInput.InputFormat.RGB
+ : ColorInput.InputFormat.RGBA,
+ };
+ color.SynchronizeWith(colorEntry);
+
+ menuElement = color;
+ return true;
+ }
+
private record ElementTreeNode
{
public readonly List<(string path, MenuElement element)> Elements = [];
diff --git a/Silksong.ModMenu/Plugin/RGBColorValues.cs b/Silksong.ModMenu/Plugin/RGBColorValues.cs
new file mode 100644
index 0000000..6da283d
--- /dev/null
+++ b/Silksong.ModMenu/Plugin/RGBColorValues.cs
@@ -0,0 +1,24 @@
+using BepInEx.Configuration;
+using UnityEngine;
+
+namespace Silksong.ModMenu.Plugin;
+
+///
+/// Clamps a setting to always be at 100% opacity.
+///
+///
+/// Automatically generated s for a setting with this restriction
+/// will automatically have their format set to .
+///
+public class RGBColorValues() : AcceptableValueBase(typeof(Color))
+{
+ ///
+ public override object Clamp(object value) => (value is Color c ? c : default) with { a = 1 };
+
+ ///
+ public override bool IsValid(object value) => value is Color c && c.a == 1;
+
+ ///
+ public override string ToDescriptionString() =>
+ $"# Acceptable values: color codes from 000000FF to FFFFFFFF";
+}
diff --git a/Silksong.ModMenuTesting/BaseProxyPluginTest.cs b/Silksong.ModMenuTesting/BaseProxyPluginTest.cs
new file mode 100644
index 0000000..c35b82b
--- /dev/null
+++ b/Silksong.ModMenuTesting/BaseProxyPluginTest.cs
@@ -0,0 +1,38 @@
+using BepInEx.Configuration;
+using Silksong.ModMenu.Plugin;
+using Silksong.ModMenu.Screens;
+using System;
+using System.IO;
+
+namespace Silksong.ModMenuTesting;
+
+internal abstract class BaseProxyPluginTest : ModMenuTest
+{
+ private ConfigFile config;
+
+ public BaseProxyPluginTest(string pluginId)
+ {
+ string configFolder = Path.GetDirectoryName(GetType().Assembly.Location);
+ config = new(
+ Path.Combine(configFolder, pluginId + ".cfg"),
+ true
+ );
+
+ Setup(config);
+ }
+
+ protected virtual ConfigEntryFactory GetFactory() => new();
+
+ protected abstract void Setup(ConfigFile config);
+
+ internal override AbstractMenuScreen BuildMenuScreen()
+ {
+ ConfigEntryFactory factory = GetFactory();
+ if (!factory.GenerateEntryButton(Name, config, out AbstractMenuScreen? screen, out _))
+ {
+ throw new ArgumentException($"No menu created for {Name}");
+ }
+
+ return screen;
+ }
+}
diff --git a/Silksong.ModMenuTesting/ModMenuNestedTestingPlugin.cs b/Silksong.ModMenuTesting/ModMenuNestedTestingPlugin.cs
deleted file mode 100644
index b57eb68..0000000
--- a/Silksong.ModMenuTesting/ModMenuNestedTestingPlugin.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using BepInEx;
-using Silksong.ModMenu.Elements;
-using Silksong.ModMenu.Plugin;
-
-namespace Silksong.ModMenuTesting;
-
-// This test requires its own ConfigFile so it's not part of the general testing framework.
-[BepInAutoPlugin(id: "org.silksong_modding.modmenunestedtesting")]
-public partial class ModMenuNestedTestingPlugin : BaseUnityPlugin, IModMenuNestedMenu
-{
- private void Awake()
- {
- Config.Bind(new("Main", "Int1"), 1, new("Root Integer 1"));
- Config.Bind(new("Main", "Int2"), 2, new("Root Integer 2"));
- // Place this in Main instead of its config path.
- Config.Bind(
- new("Other.Group", "Int3"),
- 3,
- new("Other Integer 3", tags: new ConfigEntrySubgroup(["Main", "Int3"]))
- );
-
- // We should generte a single 'Sub' button for the three below
- Config.Bind(new("Main.Sub", "Int4"), 4, new("Sub Integer 4"));
- Config.Bind(new("Main.Sub", "Int5"), 5, new("Sub Integer 5"));
- // This gets flattened into the 'Sub' menu because there are not enough 'Sub.Sub' elements.
- Config.Bind(new("Main.Sub.Sub", "Int6"), 6, new("Sub Sub Integer 6"));
-
- // Test
- Config.Bind(new("Main.B.C", "Int7"), 7, new("ABC Integer 7"));
- Config.Bind(new("Main.B.C", "Int8"), 8, new("ABC Integer 8"));
- }
-
- public LocalizedText ModMenuName() => "Mod Menu Nested Testing";
-}
diff --git a/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs b/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs
new file mode 100644
index 0000000..269f17c
--- /dev/null
+++ b/Silksong.ModMenuTesting/Tests/ModMenuAutoTestingPlugin.cs
@@ -0,0 +1,37 @@
+using BepInEx.Configuration;
+using UnityEngine;
+
+namespace Silksong.ModMenuTesting;
+
+internal class ModMenuAutoTestingPlugin() : BaseProxyPluginTest("org.silksong_modding.modmenuautotesting")
+{
+ internal override string Name => "Mod Menu Auto Testing";
+
+ public enum TestEnum { EnumOne, EnumTwo, EnumThree }
+
+ protected override void Setup(ConfigFile config)
+ {
+ config.Bind("Unity Types", "KeyCode Option", KeyCode.A);
+ config.Bind("Unity Types", "Color Option", Color.green);
+ config.Bind("Unity Types", "Vector2 Option", Vector2.one); // not done
+ config.Bind("Unity Types", "Vector3 Option", Vector3.one); // not done
+ config.Bind("Unity Types", "Vector4 Option", Vector4.one); // not done
+ config.Bind("Unity Types", "Quaternion Option", Quaternion.identity); // not done
+
+
+ config.Bind("Value Types", "String Option", "value");
+ config.Bind("Value Types", "Enum Option", TestEnum.EnumOne);
+ config.Bind("Value Types", "Bool Option", true);
+ config.Bind("Value Types", "Byte Option", (byte)0); // not done
+ config.Bind("Value Types", "SByte Option", (sbyte)0); // not done
+ config.Bind("Value Types", "Short Option", (short)0); // not done
+ config.Bind("Value Types", "UShort Option", (ushort)0); // not done
+ config.Bind("Value Types", "Int Option", 0);
+ config.Bind("Value Types", "UInt Option", 0u); // not done
+ config.Bind("Value Types", "Long Option", 0L); // not done
+ config.Bind("Value Types", "ULong Option", 0UL); // not done
+ config.Bind("Value Types", "Float Option", 0.0f);
+ config.Bind("Value Types", "Double Option", 0.0d); // not done
+ config.Bind("Value Types", "Decimal Option", 0.0m); // not done
+ }
+}
\ No newline at end of file
diff --git a/Silksong.ModMenuTesting/Tests/ModMenuNestedTestingPlugin.cs b/Silksong.ModMenuTesting/Tests/ModMenuNestedTestingPlugin.cs
new file mode 100644
index 0000000..291746a
--- /dev/null
+++ b/Silksong.ModMenuTesting/Tests/ModMenuNestedTestingPlugin.cs
@@ -0,0 +1,36 @@
+using BepInEx.Configuration;
+using Silksong.ModMenu.Plugin;
+
+namespace Silksong.ModMenuTesting;
+
+internal class ModMenuNestedTestingPlugin() : BaseProxyPluginTest("org.silksong_modding.modmenunestedtesting")
+{
+ protected override void Setup(ConfigFile config)
+ {
+ config.Bind(new("Main", "Int1"), 1, new("Root Integer 1"));
+ config.Bind(new("Main", "Int2"), 2, new("Root Integer 2"));
+ // Place this in Main instead of its config path.
+ config.Bind(
+ new("Other.Group", "Int3"),
+ 3,
+ new("Other Integer 3", tags: new ConfigEntrySubgroup(["Main", "Int3"]))
+ );
+
+ // We should generte a single 'Sub' button for the three below
+ config.Bind(new("Main.Sub", "Int4"), 4, new("Sub Integer 4"));
+ config.Bind(new("Main.Sub", "Int5"), 5, new("Sub Integer 5"));
+ // This gets flattened into the 'Sub' menu because there are not enough 'Sub.Sub' elements.
+ config.Bind(new("Main.Sub.Sub", "Int6"), 6, new("Sub Sub Integer 6"));
+
+ // Test
+ config.Bind(new("Main.B.C", "Int7"), 7, new("ABC Integer 7"));
+ config.Bind(new("Main.B.C", "Int8"), 8, new("ABC Integer 8"));
+ }
+
+ internal override string Name => "Mod Menu Nested Testing";
+
+ protected override ConfigEntryFactory GetFactory()
+ {
+ return new() { GenerateSubgroups = true };
+ }
+}
diff --git a/Silksong.ModMenuTesting/Tests/StandardElementsTest.cs b/Silksong.ModMenuTesting/Tests/StandardElementsTest.cs
index 9fbde99..a228d49 100644
--- a/Silksong.ModMenuTesting/Tests/StandardElementsTest.cs
+++ b/Silksong.ModMenuTesting/Tests/StandardElementsTest.cs
@@ -1,9 +1,7 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using Silksong.ModMenu.Elements;
+using Silksong.ModMenu.Elements;
using Silksong.ModMenu.Models;
using Silksong.ModMenu.Screens;
+using System.Collections.Generic;
namespace Silksong.ModMenuTesting.Tests;
@@ -65,6 +63,12 @@ internal static IEnumerable CreateUnboundElements()
textModel.OnValueChanged += s => Log($"Text model -> {s}");
yield return stringInput;
}
+
+ {
+ ColorInput colorInput = new("Colour RGBA", "You can input a colour here");
+ colorInput.Model.OnValueChanged += c => Log($"Colour model -> {c}");
+ yield return colorInput;
+ }
}
internal override AbstractMenuScreen BuildMenuScreen()