Files
riversong-code-showcase/Source/Riversong/Game/UI/DayNightUITheme.cs
2026-05-21 16:04:49 +02:00

293 lines
9.7 KiB
C#

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(@"^(?<name>--[a-z0-9-]+)\s*:\s*(?<value>#[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<ThemeColorProperty> Properties = new();
internal IReadOnlyList<ThemeColorProperty> OrderedProperties => Properties;
private void OnEnable()
{
EnsureProperties();
}
private void OnValidate()
{
EnsureProperties();
}
private void EnsureProperties()
{
Properties ??= new List<ThemeColorProperty>();
var definitions = GetThemePropertyDefinitions();
var existingProperties = Properties;
var existingPropertiesByName = CreatePropertiesByName(existingProperties);
var definitionNames = CreateDefinitionNames(definitions);
var orderedProperties = new List<ThemeColorProperty>(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<string, ThemeColorProperty> CreatePropertiesByName(IEnumerable<ThemeColorProperty> properties)
{
var propertiesByName = new Dictionary<string, ThemeColorProperty>(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<string> CreateDefinitionNames(IEnumerable<ThemePropertyDefinition> definitions)
{
var names = new HashSet<string>(StringComparer.Ordinal);
foreach (var definition in definitions) names.Add(definition.Name);
return names;
}
private static ThemeColorProperty TryGetProperty(Dictionary<string, ThemeColorProperty> propertiesByName, string propertyName)
{
return propertiesByName.GetValueOrDefault(propertyName);
}
private static ThemeColorProperty TryGetSameIndexFallbackProperty(IReadOnlyList<ThemeColorProperty> existingProperties, HashSet<string> 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<ThemePropertyDefinition> GetThemePropertyDefinitions()
{
return TryReadThemePropertyDefinitionsFromCommonUss();
}
private static List<ThemePropertyDefinition> TryReadThemePropertyDefinitionsFromCommonUss()
{
var path = GetCommonUssFullPath();
if (!File.Exists(path)) return new List<ThemePropertyDefinition>();
var definitions = new List<ThemePropertyDefinition>();
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
}
}