228 lines
8.5 KiB
C#
228 lines
8.5 KiB
C#
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<StyleSheet> GetStyleSheets(VisualElement root)
|
|
{
|
|
return VisualElementStyleSheetListField?.GetValue(root) as List<StyleSheet>;
|
|
}
|
|
|
|
public static void SetColor(object manipulator, object[] arguments)
|
|
{
|
|
ManipulatorSetColorMethod?.Invoke(manipulator, arguments);
|
|
}
|
|
|
|
public static bool TryCreateBindings(StyleSheet styleSheet, IReadOnlyList<DayNightUITheme.ThemeColorProperty> 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<string, object>(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;
|
|
}
|
|
}
|
|
}
|
|
} |