using System; using System.Collections.Generic; using System.Reflection; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; namespace DanieleMarotta.RiversongCodeShowcase { [GameSystemGroup(typeof(UISystemGroup))] [InitializeAfter(typeof(UIInitializationSystem))] public class DayNightUIThemeSystem : GameSystem, IInitializable, IDisposable, IUpdatable { [InjectService] private GameConfig _config; [InjectService] private UIService _uiService; [InjectService] private World _world; private VisualElement _root; private RuntimeThemeSheet _runtimeThemeSheet; private float _lastAppliedNightBlend = -1; private bool _refreshClassEnabled; public DayNightUIThemeSystem(IServiceLocator serviceLocator) : base(serviceLocator) { } public UniTask InitializeAsync() { _root = _uiService.UIRoot.RootVisualElement; _runtimeThemeSheet = RuntimeThemeSheet.TryCreate(_root, _config.UI.Theme); ApplyTheme(true); return UniTask.CompletedTask; } public void Update() { ApplyTheme(); } public void Dispose() { if (_root == null || _runtimeThemeSheet == null) return; _runtimeThemeSheet.Apply(0); ToggleRefreshClass(); _lastAppliedNightBlend = 0; } private void ApplyTheme(bool force = false) { if (_root == null || _config.UI.Theme == null || _runtimeThemeSheet == null) return; var nightBlend = _world.TimeState.NightBlend; if (!force && !Application.isEditor && Mathf.Approximately(nightBlend, _lastAppliedNightBlend)) return; _runtimeThemeSheet.Apply(nightBlend); ToggleRefreshClass(); _lastAppliedNightBlend = nightBlend; } private void ToggleRefreshClass() { _refreshClassEnabled = !_refreshClassEnabled; _root.EnableInClassList(RuntimeThemeSheet.RefreshClassName, _refreshClassEnabled); } private sealed class RuntimeThemeSheet { public const string RefreshClassName = "__day-night-theme-refresh"; private readonly StyleSheet _styleSheet; private readonly RuntimePropertyBinding[] _bindings; private RuntimeThemeSheet(StyleSheet styleSheet, RuntimePropertyBinding[] bindings) { _styleSheet = styleSheet; _bindings = bindings; } public static RuntimeThemeSheet TryCreate(VisualElement root, DayNightUITheme theme) { if (root == null || theme == null) return null; var styleSheets = StyleSheetReflection.GetStyleSheets(root); if (styleSheets == null) return null; foreach (var styleSheet in styleSheets) { if (!StyleSheetReflection.TryCreateBindings(styleSheet, theme.OrderedProperties, out var bindings)) continue; return new RuntimeThemeSheet(styleSheet, bindings); } return null; } public void Apply(float nightBlend) { foreach (var binding in _bindings) binding.Apply(nightBlend); _styleSheet.contentHash = unchecked(_styleSheet.contentHash + 1); } } private sealed class RuntimePropertyBinding { private readonly DayNightUITheme.ThemeColorProperty _themeProperty; private readonly object _manipulator; private readonly object[] _setColorArguments = new object[2]; public RuntimePropertyBinding(DayNightUITheme.ThemeColorProperty themeProperty, object manipulator) { _themeProperty = themeProperty; _manipulator = manipulator; } public void Apply(float nightBlend) { _setColorArguments[0] = 0; _setColorArguments[1] = Color.Lerp(_themeProperty.DayColor, _themeProperty.NightColor, nightBlend); StyleSheetReflection.SetColor(_manipulator, _setColorArguments); } } private static class StyleSheetReflection { private static readonly FieldInfo VisualElementStyleSheetListField = typeof(VisualElement).GetField("styleSheetList", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly FieldInfo StyleSheetRulesField = typeof(StyleSheet).GetField("m_Rules", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly Type StyleRuleType = StyleSheetRulesField?.FieldType.GetElementType(); private static readonly MethodInfo StyleRuleGetPropertiesMethod = StyleRuleType?.GetMethod("get_properties", BindingFlags.Instance | BindingFlags.Public); private static readonly Type StylePropertyType = StyleRuleGetPropertiesMethod?.ReturnType.GetElementType(); private static readonly MethodInfo StylePropertyGetNameMethod = StylePropertyType?.GetMethod("get_name", BindingFlags.Instance | BindingFlags.Public); private static readonly MethodInfo StylePropertyGetManipulatorMethod = StylePropertyType?.GetMethod("GetManipulator", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly Type ManipulatorType = StylePropertyGetManipulatorMethod?.ReturnType; private static readonly MethodInfo ManipulatorSetColorMethod = ManipulatorType?.GetMethod( "SetColor", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(int), typeof(Color) }, null); public static List GetStyleSheets(VisualElement root) { return VisualElementStyleSheetListField?.GetValue(root) as List; } public static void SetColor(object manipulator, object[] arguments) { ManipulatorSetColorMethod?.Invoke(manipulator, arguments); } public static bool TryCreateBindings(StyleSheet styleSheet, IReadOnlyList themeProperties, out RuntimePropertyBinding[] bindings) { bindings = null; if (styleSheet == null || themeProperties == null) return false; if (StyleSheetRulesField == null || StyleRuleGetPropertiesMethod == null || StylePropertyGetNameMethod == null || StylePropertyGetManipulatorMethod == null || ManipulatorSetColorMethod == null) return false; var propertiesByName = new Dictionary(themeProperties.Count, StringComparer.Ordinal); var rules = StyleSheetRulesField.GetValue(styleSheet) as Array; if (rules == null) return false; foreach (var rule in rules) { if (rule == null) continue; var styleProperties = StyleRuleGetPropertiesMethod.Invoke(rule, null) as Array; if (styleProperties == null) continue; foreach (var styleProperty in styleProperties) { if (styleProperty == null) continue; var propertyName = StylePropertyGetNameMethod.Invoke(styleProperty, null) as string; if (string.IsNullOrEmpty(propertyName)) continue; if (propertiesByName.ContainsKey(propertyName)) continue; propertiesByName[propertyName] = styleProperty; } } var resolvedBindings = new RuntimePropertyBinding[themeProperties.Count]; for (var i = 0; i < themeProperties.Count; i++) { var themeProperty = themeProperties[i]; if (!propertiesByName.TryGetValue(themeProperty.Name, out var styleProperty)) return false; var manipulator = StylePropertyGetManipulatorMethod.Invoke(styleProperty, new object[] { styleSheet }); resolvedBindings[i] = new RuntimePropertyBinding(themeProperty, manipulator); } bindings = resolvedBindings; return true; } } } }