#nullable enable using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// UIToolkit 테마 관리자. /// 런타임에서 Light/Dark 테마 전환을 지원하며, 모든 UTK 컴포넌트에 일관된 스타일을 적용합니다. /// /// /// 테마 매니저란? /// /// UTKThemeManager는 애플리케이션 전체의 UI 테마(색상, 스타일)를 중앙에서 관리합니다. /// 싱글톤 패턴으로 구현되어 어디서든 UTKThemeManager.Instance로 접근 가능합니다. /// /// /// 스타일시트 로드 순서: /// /// UTKVariables.uss - 레이아웃 변수 (spacing, radius, font-size) /// UTKThemeDark/Light.uss - 테마별 색상 변수 /// UTKComponents.uss - 공통 컴포넌트 스타일 /// UTKDefaultStyle.uss - Unity 기본 요소 오버라이드 (선택) /// /// /// 주요 기능: /// /// RegisterRoot() - UIDocument 루트에 테마 등록 /// SetTheme() - 테마 변경 (Dark/Light) /// ToggleTheme() - 테마 토글 /// ApplyThemeToElement() - 개별 요소에 테마 적용 /// OnThemeChanged - 테마 변경 이벤트 구독 /// /// /// 사용 시점: /// /// UIDocument 초기화 시 RegisterRoot() 호출 /// UTK 컴포넌트 내부에서 자동으로 ApplyThemeToElement() 호출 /// 설정 화면에서 테마 전환 버튼 클릭 시 ToggleTheme() 호출 /// /// /// /// 기본 사용법 - UIDocument에 테마 등록: /// /// public class MyUIController : MonoBehaviour /// { /// [SerializeField] private UIDocument _uiDocument; /// /// void Start() /// { /// // UIDocument 루트에 테마 스타일시트 등록 /// var root = _uiDocument.rootVisualElement; /// UTKThemeManager.Instance.RegisterRoot(root); /// } /// /// void OnDestroy() /// { /// // 정리 (선택사항) /// var root = _uiDocument.rootVisualElement; /// UTKThemeManager.Instance.UnregisterRoot(root); /// } /// } /// /// /// 테마 전환: /// /// // 특정 테마로 변경 /// UTKThemeManager.Instance.SetTheme(UTKTheme.Light); /// /// // 토글 (Dark ↔ Light) /// UTKThemeManager.Instance.ToggleTheme(); /// /// // 현재 테마 확인 /// if (UTKThemeManager.Instance.IsDarkTheme) /// { /// Debug.Log("다크 테마 사용 중"); /// } /// /// /// 테마 변경 이벤트 구독: /// /// void OnEnable() /// { /// UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; /// } /// /// void OnDisable() /// { /// UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; /// } /// /// private void OnThemeChanged(UTKTheme newTheme) /// { /// Debug.Log($"테마 변경됨: {newTheme}"); /// // 테마에 따른 추가 로직 (아이콘 변경 등) /// } /// /// /// 커스텀 스타일 값 가져오기: /// /// // USS 변수에서 색상 값 읽기 /// var primaryColor = new CustomStyleProperty("--color-btn-primary"); /// var color = UTKThemeManager.GetColor(myElement, primaryColor, Color.blue); /// /// // USS 변수에서 float 값 읽기 /// var spacing = new CustomStyleProperty("--space-m"); /// var value = UTKThemeManager.GetFloat(myElement, spacing, 8f); /// /// /// UTK 컴포넌트에서 사용 (내부 구현): /// /// [UxmlElement] /// public partial class MyCustomComponent : VisualElement, IDisposable /// { /// private bool _disposed; /// /// public MyCustomComponent() /// { /// // 컴포넌트에 테마 스타일 적용 /// UTKThemeManager.Instance.ApplyThemeToElement(this); /// /// // 테마 변경 구독 (Attach/Detach 쌍으로 관리) /// SubscribeToThemeChanges(); /// } /// /// private void SubscribeToThemeChanges() /// { /// UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; /// RegisterCallback(OnAttachToPanelForTheme); /// RegisterCallback(OnDetachFromPanelForTheme); /// } /// /// private void OnAttachToPanelForTheme(AttachToPanelEvent evt) /// { /// UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; /// UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; /// UTKThemeManager.Instance.ApplyThemeToElement(this); /// } /// /// private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt) /// { /// UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; /// } /// /// private void OnThemeChanged(UTKTheme theme) /// { /// UTKThemeManager.Instance.ApplyThemeToElement(this); /// } /// /// public void Dispose() /// { /// if (_disposed) return; /// _disposed = true; /// UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; /// UnregisterCallback(OnAttachToPanelForTheme); /// UnregisterCallback(OnDetachFromPanelForTheme); /// } /// } /// /// /// 테마 전환 버튼 구현: /// /// var themeToggle = new UTKButton("테마 전환", UTKButtonVariant.Secondary); /// themeToggle.clicked += () => /// { /// UTKThemeManager.Instance.ToggleTheme(); /// var icon = UTKThemeManager.Instance.IsDarkTheme /// ? UTKMaterialIcons.DarkMode /// : UTKMaterialIcons.LightMode; /// themeToggle.SetMaterialIcon(icon); /// }; /// /// public class UTKThemeManager { #region Singleton private static UTKThemeManager? _instance; public static UTKThemeManager Instance => _instance ??= new UTKThemeManager(); private UTKThemeManager() { LoadStyleSheets(); } #endregion #region Constants private const string VARIABLES_PATH = "UIToolkit/Style/UTKVariables"; private const string THEME_DARK_PATH = "UIToolkit/Style/UTKThemeDark"; private const string THEME_LIGHT_PATH = "UIToolkit/Style/UTKThemeLight"; private const string COMPONENTS_PATH = "UIToolkit/Style/UTKComponents"; private const string DEFAULT_STYLE_PATH = "UIToolkit/Style/UTKDefaultStyle"; #endregion #region Fields private StyleSheet? _variablesSheet; private StyleSheet? _themeDarkSheet; private StyleSheet? _themeLightSheet; private StyleSheet? _componentsSheet; private StyleSheet? _defaultStyleSheet; private readonly HashSet _registeredRoots = new(); private UTKTheme _currentTheme = UTKTheme.Dark; #endregion #region Properties /// 현재 적용된 테마 public UTKTheme CurrentTheme => _currentTheme; /// 다크 테마 여부 public bool IsDarkTheme => _currentTheme == UTKTheme.Dark; #endregion #region Events /// 테마 변경 이벤트 public event Action? OnThemeChanged; #endregion #region Initialization private void LoadStyleSheets() { _variablesSheet = Resources.Load(VARIABLES_PATH); _themeDarkSheet = Resources.Load(THEME_DARK_PATH); _themeLightSheet = Resources.Load(THEME_LIGHT_PATH); _componentsSheet = Resources.Load(COMPONENTS_PATH); _defaultStyleSheet = Resources.Load(DEFAULT_STYLE_PATH); } #endregion #region Public Methods /// /// VisualElement 루트에 테마 스타일시트를 등록합니다. /// UIDocument의 rootVisualElement나 커스텀 VisualElement를 전달하세요. /// /// 스타일시트를 적용할 VisualElement /// UTKDefaultStyle.uss 포함 여부 (Unity 기본 요소 스타일) public void RegisterRoot(VisualElement root, bool includeDefaultStyle = true) { if (root == null || _registeredRoots.Contains(root)) return; _registeredRoots.Add(root); ApplyThemeToRoot(root, includeDefaultStyle); } /// /// 등록된 루트에서 스타일시트를 제거합니다. /// /// 제거할 VisualElement public void UnregisterRoot(VisualElement root) { if (root == null) return; _registeredRoots.Remove(root); RemoveThemeFromRoot(root); } /// /// 테마를 전환합니다. /// /// 적용할 테마 public void SetTheme(UTKTheme theme) { if (_currentTheme == theme) return; _currentTheme = theme; // 모든 등록된 루트에 테마 적용 foreach (var root in _registeredRoots) { ApplyThemeToRoot(root, true); } OnThemeChanged?.Invoke(_currentTheme); } /// /// 테마를 토글합니다 (Dark ↔ Light). /// public void ToggleTheme() { SetTheme(_currentTheme == UTKTheme.Dark ? UTKTheme.Light : UTKTheme.Dark); } /// /// 스타일시트만 반환합니다 (수동 적용용). /// /// 현재 테마에 해당하는 스타일시트 배열 public StyleSheet?[] GetStyleSheets(bool includeDefaultStyle = true) { var currentThemeSheet = _currentTheme == UTKTheme.Dark ? _themeDarkSheet : _themeLightSheet; if (includeDefaultStyle) { return new[] { _variablesSheet, currentThemeSheet, _componentsSheet, _defaultStyleSheet }; } else { return new[] { _variablesSheet, currentThemeSheet, _componentsSheet }; } } /// /// 특정 컴포넌트에 필요한 스타일시트만 추가합니다. /// 이미 루트에 등록된 경우 호출할 필요 없음. /// /// 스타일을 적용할 요소 public void ApplyThemeToElement(VisualElement element) { if (element == null) return; var currentThemeSheet = _currentTheme == UTKTheme.Dark ? _themeDarkSheet : _themeLightSheet; var otherThemeSheet = _currentTheme == UTKTheme.Dark ? _themeLightSheet : _themeDarkSheet; // 이전 테마 제거 if (otherThemeSheet != null && element.styleSheets.Contains(otherThemeSheet)) element.styleSheets.Remove(otherThemeSheet); // 현재 테마가 이미 있다면 제거 후 다시 추가 (순서 보장) if (currentThemeSheet != null && element.styleSheets.Contains(currentThemeSheet)) element.styleSheets.Remove(currentThemeSheet); if (_variablesSheet != null && !element.styleSheets.Contains(_variablesSheet)) element.styleSheets.Add(_variablesSheet); // 현재 테마 적용 if (currentThemeSheet != null) element.styleSheets.Add(currentThemeSheet); if (_componentsSheet != null && !element.styleSheets.Contains(_componentsSheet)) element.styleSheets.Add(_componentsSheet); } #endregion #region Private Methods private void ApplyThemeToRoot(VisualElement root, bool includeDefaultStyle) { var currentThemeSheet = _currentTheme == UTKTheme.Dark ? _themeDarkSheet : _themeLightSheet; var otherThemeSheet = _currentTheme == UTKTheme.Dark ? _themeLightSheet : _themeDarkSheet; // 이전 테마 제거 if (otherThemeSheet != null && root.styleSheets.Contains(otherThemeSheet)) root.styleSheets.Remove(otherThemeSheet); // Variables 추가 (없는 경우에만) if (_variablesSheet != null && !root.styleSheets.Contains(_variablesSheet)) root.styleSheets.Add(_variablesSheet); else Debug.Log($"[UTKThemeManager] Variables 스킵 - null: {_variablesSheet == null}, contains: {_variablesSheet != null && root.styleSheets.Contains(_variablesSheet)}"); // 현재 테마 추가 (없는 경우에만) if (currentThemeSheet != null && !root.styleSheets.Contains(currentThemeSheet)) root.styleSheets.Add(currentThemeSheet); // Components 추가 (없는 경우에만) if (_componentsSheet != null && !root.styleSheets.Contains(_componentsSheet)) root.styleSheets.Add(_componentsSheet); else Debug.Log($"[UTKThemeManager] Components 스킵 - null: {_componentsSheet == null}, contains: {_componentsSheet != null && root.styleSheets.Contains(_componentsSheet)}"); // DefaultStyle 추가 (없는 경우에만) if (includeDefaultStyle && _defaultStyleSheet != null && !root.styleSheets.Contains(_defaultStyleSheet)) root.styleSheets.Add(_defaultStyleSheet); else Debug.Log($"[UTKThemeManager] DefaultStyle 스킵 - null: {_defaultStyleSheet == null}, includeDefaultStyle: {includeDefaultStyle}, contains: {_defaultStyleSheet != null && root.styleSheets.Contains(_defaultStyleSheet)}"); Debug.Log($"[UTKThemeManager] ApplyThemeToRoot 완료 - styleSheets.count(after): {root.styleSheets.count}"); } private void RemoveThemeFromRoot(VisualElement root) { if (_variablesSheet != null && root.styleSheets.Contains(_variablesSheet)) root.styleSheets.Remove(_variablesSheet); if (_themeDarkSheet != null && root.styleSheets.Contains(_themeDarkSheet)) root.styleSheets.Remove(_themeDarkSheet); if (_themeLightSheet != null && root.styleSheets.Contains(_themeLightSheet)) root.styleSheets.Remove(_themeLightSheet); if (_componentsSheet != null && root.styleSheets.Contains(_componentsSheet)) root.styleSheets.Remove(_componentsSheet); if (_defaultStyleSheet != null && root.styleSheets.Contains(_defaultStyleSheet)) root.styleSheets.Remove(_defaultStyleSheet); } #endregion #region Static Helpers /// /// 현재 테마의 변수 값을 가져옵니다. /// VisualElement가 패널에 연결된 후에만 동작합니다. /// public static Color GetColor(VisualElement element, CustomStyleProperty property, Color fallback) { if (element?.customStyle == null) return fallback; return element.customStyle.TryGetValue(property, out var color) ? color : fallback; } /// /// 현재 테마의 float 값을 가져옵니다. /// public static float GetFloat(VisualElement element, CustomStyleProperty property, float fallback) { if (element?.customStyle == null) return fallback; return element.customStyle.TryGetValue(property, out var value) ? value : fallback; } /// /// 현재 테마의 int 값을 가져옵니다. /// public static int GetInt(VisualElement element, CustomStyleProperty property, int fallback) { if (element?.customStyle == null) return fallback; return element.customStyle.TryGetValue(property, out var value) ? value : fallback; } #endregion } /// /// 지원하는 테마 종류. /// /// /// 각 테마는 해당 USS 파일의 색상 변수를 사용합니다: /// /// Dark - UTKThemeDark.uss (어두운 배경, 밝은 텍스트) /// Light - UTKThemeLight.uss (밝은 배경, 어두운 텍스트) /// /// public enum UTKTheme { /// 다크 테마 (기본값) Dark, /// 라이트 테마 Light } }