using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using Sirenix.OdinInspector; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace DanieleMarotta.RiversongCodeShowcase { [CreateAssetMenu(fileName = "DayNightUITheme", menuName = "Riversong Code Showcase/Day Night UI Theme")] public class DayNightUITheme : ScriptableObject { private const string CommonUssAssetPath = "_Project/GameAssets/UI/Common.uss"; private const string CommonUssEditorAssetPath = "Assets/_Project/GameAssets/UI/Common.uss"; private static readonly Regex RootColorVariableRegex = new(@"^(?--[a-z0-9-]+)\s*:\s*(?#[0-9A-Fa-f]{6,8})\s*;$", RegexOptions.Compiled); [TableList(AlwaysExpanded = true, ShowIndexLabels = true, HideToolbar = true)] [ListDrawerSettings(DefaultExpandedState = true, DraggableItems = false, HideAddButton = true, HideRemoveButton = true, ShowFoldout = false)] public List Properties = new(); internal IReadOnlyList OrderedProperties => Properties; private void OnEnable() { EnsureProperties(); } private void OnValidate() { EnsureProperties(); } private void EnsureProperties() { Properties ??= new List(); var definitions = GetThemePropertyDefinitions(); var existingProperties = Properties; var existingPropertiesByName = CreatePropertiesByName(existingProperties); var definitionNames = CreateDefinitionNames(definitions); var orderedProperties = new List(definitions.Count); for (var i = 0; i < definitions.Count; i++) { var definition = definitions[i]; var property = TryGetProperty(existingPropertiesByName, definition.Name) ?? TryGetSameIndexFallbackProperty(existingProperties, definitionNames, i); property ??= new ThemeColorProperty(definition.Name, Color.white, Color.white); property.Name = definition.Name; orderedProperties.Add(property); } Properties = orderedProperties; } private static Dictionary CreatePropertiesByName(IEnumerable properties) { var propertiesByName = new Dictionary(StringComparer.Ordinal); foreach (var property in properties) { if (property == null || string.IsNullOrWhiteSpace(property.Name)) continue; propertiesByName.TryAdd(property.Name, property); } return propertiesByName; } private static HashSet CreateDefinitionNames(IEnumerable definitions) { var names = new HashSet(StringComparer.Ordinal); foreach (var definition in definitions) names.Add(definition.Name); return names; } private static ThemeColorProperty TryGetProperty(Dictionary propertiesByName, string propertyName) { return propertiesByName.GetValueOrDefault(propertyName); } private static ThemeColorProperty TryGetSameIndexFallbackProperty(IReadOnlyList existingProperties, HashSet definitionNames, int index) { if (index >= existingProperties.Count) return null; var property = existingProperties[index]; if (property == null) return null; return string.IsNullOrWhiteSpace(property.Name) || !definitionNames.Contains(property.Name) ? property : null; } private static List GetThemePropertyDefinitions() { return TryReadThemePropertyDefinitionsFromCommonUss(); } private static List TryReadThemePropertyDefinitionsFromCommonUss() { var path = GetCommonUssFullPath(); if (!File.Exists(path)) return new List(); var definitions = new List(); var lines = File.ReadAllLines(path); var inRootBlock = false; foreach (var rawLine in lines) { var line = rawLine.Trim(); if (!inRootBlock) { if (line == ":root {") inRootBlock = true; continue; } if (line == "}") break; var match = RootColorVariableRegex.Match(line); if (!match.Success) continue; var name = match.Groups["name"].Value; var dayColor = FromHex(match.Groups["value"].Value); var nightColor = GetDefaultNightColor(name, dayColor); definitions.Add(new ThemePropertyDefinition(name, dayColor, nightColor)); } return definitions; } private static Color GetDefaultNightColor(string name, Color dayColor) { return name switch { "--wood-color" => FromHex("#424C54"), _ => dayColor }; } private static Color FromHex(string hex) { if (ColorUtility.TryParseHtmlString(hex, out var color)) return color; throw new ArgumentException($"Invalid color value: {hex}", nameof(hex)); } private static string GetCommonUssFullPath() { return Path.Combine(Application.dataPath, CommonUssAssetPath.Replace('/', Path.DirectorySeparatorChar)); } private static string ToHex(Color color) { var color32 = (Color32)color; return color32.a == byte.MaxValue ? $"#{color32.r:X2}{color32.g:X2}{color32.b:X2}" : $"#{color32.r:X2}{color32.g:X2}{color32.b:X2}{color32.a:X2}"; } private readonly struct ThemePropertyDefinition { public readonly string Name; public readonly Color DayColor; public readonly Color NightColor; public ThemePropertyDefinition(string name, Color dayColor, Color nightColor) { Name = name; DayColor = dayColor; NightColor = nightColor; } } [Serializable] public class ThemeColorProperty { [ReadOnly] public string Name; public Color DayColor = Color.white; public Color NightColor = Color.white; public ThemeColorProperty() { } public ThemeColorProperty(string name, Color dayColor, Color nightColor) { Name = name; DayColor = dayColor; NightColor = nightColor; } } #if UNITY_EDITOR [HorizontalGroup("Sync")] [Button("Copy Day Colors From USS", ButtonSizes.Large)] [GUIColor("cyan")] private void CopyDayColorsFromUss() { EnsureProperties(); var definitions = TryReadThemePropertyDefinitionsFromCommonUss(); if (definitions.Count == 0) { Debug.LogWarning($"Could not read any theme properties from '{CommonUssEditorAssetPath}'.", this); return; } var propertiesByName = CreatePropertiesByName(Properties); foreach (var definition in definitions) { if (!propertiesByName.TryGetValue(definition.Name, out var property)) continue; property.DayColor = definition.DayColor; } EditorUtility.SetDirty(this); AssetDatabase.SaveAssetIfDirty(this); } [HorizontalGroup("Sync")] [Button("Apply Day Colors To USS", ButtonSizes.Large)] [GUIColor("cyan")] private void ApplyDayColorsToUss() { EnsureProperties(); var path = GetCommonUssFullPath(); if (!File.Exists(path)) { Debug.LogWarning($"Could not find '{CommonUssEditorAssetPath}'.", this); return; } var propertiesByName = CreatePropertiesByName(Properties); var lines = File.ReadAllLines(path); var updatedAnyLine = false; var inRootBlock = false; for (var i = 0; i < lines.Length; i++) { var rawLine = lines[i]; var trimmedLine = rawLine.Trim(); if (!inRootBlock) { if (trimmedLine == ":root {") inRootBlock = true; continue; } if (trimmedLine == "}") break; var match = RootColorVariableRegex.Match(trimmedLine); if (!match.Success) continue; var propertyName = match.Groups["name"].Value; if (!propertiesByName.TryGetValue(propertyName, out var property)) continue; var indentationLength = rawLine.Length - rawLine.TrimStart().Length; var indentation = rawLine.Substring(0, indentationLength); lines[i] = $"{indentation}{propertyName}: {ToHex(property.DayColor)};"; updatedAnyLine = true; } if (!updatedAnyLine) { Debug.LogWarning($"No matching ':root' theme color properties were updated in '{CommonUssEditorAssetPath}'.", this); return; } File.WriteAllLines(path, lines); AssetDatabase.ImportAsset(CommonUssEditorAssetPath); } #endif } }