Editor/Settings.cs
#if DEBUG

namespace Sandbox.Reactivity.Editor;

[CodeGenerator(CodeGeneratorFlags.WrapPropertyGet | CodeGeneratorFlags.Static,
	"Sandbox.Reactivity.Editor.SettingAttribute.Get")]
[CodeGenerator(CodeGeneratorFlags.WrapPropertySet | CodeGeneratorFlags.Static,
	"Sandbox.Reactivity.Editor.SettingAttribute.Set")]
[AttributeUsage(AttributeTargets.Property)]
internal class SettingAttribute : Attribute
{
	private static readonly Dictionary<string, object?> Cached = [];

	private string? _key;

	private string GetKey(int memberIdent)
	{
		if (_key == null)
		{
			var property = EditorTypeLibrary.GetMemberByIdent(memberIdent);
			_key = $"{property.DeclaringType.FullName}.{property.Name}";
		}

		return _key;
	}

	// ReSharper disable once UnusedMember.Global
	internal static T Get<T>(in WrappedPropertyGet<T> wrapped)
	{
		if (wrapped.GetAttribute<SettingAttribute>() is not { } setting)
		{
			throw new InvalidOperationException();
		}

		var key = setting.GetKey(wrapped.MemberIdent);
		return Cached.TryGetValue(key, out var value) ? (T)value! : ProjectCookie.Get(key, wrapped.Value);
	}

	// ReSharper disable once UnusedMember.Global
	internal static void Set<T>(in WrappedPropertySet<T> wrapped)
	{
		if (wrapped.GetAttribute<SettingAttribute>() is not { } setting)
		{
			throw new InvalidOperationException();
		}

		var key = setting.GetKey(wrapped.MemberIdent);

		Cached[key] = wrapped.Value;
		ProjectCookie.Set(key, wrapped.Value);
	}
}

internal static class Settings<T>
	where T : notnull
{
	private static IEnumerable<PropertyDescription> All =>
		EditorTypeLibrary.GetType<T>().Properties.Where(x => x.IsStatic && x.HasAttribute<SettingAttribute>());

	public static void PopulateMenu(
		global::Editor.Menu menu,
		Func<IEnumerable<PropertyDescription>, IEnumerable<PropertyDescription>>? filter = null,
		Action? onSettingChanged = null
	)
	{
		var settings = filter?.Invoke(All) ?? All;
		var groups = settings.GroupBy(x => x.GetDisplayInfo().Group);
		var first = true;

		foreach (var group in groups)
		{
			var properties = group.ToList();

			if (properties.Count == 0)
			{
				continue;
			}

			if (first)
			{
				first = false;
			}

			menu.AddSeparator();

			foreach (var property in properties)
			{
				if (property.GetValue(null) is not bool value)
				{
					continue;
				}

				var display = property.GetDisplayInfo();
				var option = new Option
				{
					Text = display.Name ?? property.Name,
					Icon = display.Icon,
					StatusTip = display.Description,
					ToolTip = display.Description,
					Checkable = true,
					Checked = value,
					Toggled = newValue => property.SetValue(null, newValue),
					Triggered = onSettingChanged,
				};

				menu.AddOption(option);
			}
		}
	}

	public static void OpenContextMenu(
		Widget? parent,
		Func<IEnumerable<PropertyDescription>, IEnumerable<PropertyDescription>>? filter = null
	)
	{
		var menu = new ContextMenu(parent);
		PopulateMenu(menu, filter);

		menu.OpenAtCursor();
	}
}

#endif