Files
XRLib/Assets/Scripts/UVC/UIToolkit/UTKThemeManager.cs
2026-01-21 20:43:54 +09:00

274 lines
11 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// UIToolkit 테마 관리자
/// 런타임에서 Light/Dark 테마 전환을 지원합니다.
/// </summary>
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<VisualElement> _registeredRoots = new();
private UTKTheme _currentTheme = UTKTheme.Dark;
#endregion
#region Properties
/// <summary>현재 적용된 테마</summary>
public UTKTheme CurrentTheme => _currentTheme;
/// <summary>다크 테마 여부</summary>
public bool IsDarkTheme => _currentTheme == UTKTheme.Dark;
#endregion
#region Events
/// <summary>테마 변경 이벤트</summary>
public event Action<UTKTheme>? OnThemeChanged;
#endregion
#region Initialization
private void LoadStyleSheets()
{
_variablesSheet = Resources.Load<StyleSheet>(VARIABLES_PATH);
_themeDarkSheet = Resources.Load<StyleSheet>(THEME_DARK_PATH);
_themeLightSheet = Resources.Load<StyleSheet>(THEME_LIGHT_PATH);
_componentsSheet = Resources.Load<StyleSheet>(COMPONENTS_PATH);
_defaultStyleSheet = Resources.Load<StyleSheet>(DEFAULT_STYLE_PATH);
if (_variablesSheet == null)
Debug.LogWarning($"[UTKThemeManager] UTKVariables.uss not found at: {VARIABLES_PATH}");
if (_themeDarkSheet == null)
Debug.LogWarning($"[UTKThemeManager] UTKThemeDark.uss not found at: {THEME_DARK_PATH}");
if (_themeLightSheet == null)
Debug.LogWarning($"[UTKThemeManager] UTKThemeLight.uss not found at: {THEME_LIGHT_PATH}");
if (_componentsSheet == null)
Debug.LogWarning($"[UTKThemeManager] UTKComponents.uss not found at: {COMPONENTS_PATH}");
}
#endregion
#region Public Methods
/// <summary>
/// VisualElement 루트에 테마 스타일시트를 등록합니다.
/// UIDocument의 rootVisualElement나 커스텀 VisualElement를 전달하세요.
/// </summary>
/// <param name="root">스타일시트를 적용할 VisualElement</param>
/// <param name="includeDefaultStyle">UTKDefaultStyle.uss 포함 여부 (Unity 기본 요소 스타일)</param>
public void RegisterRoot(VisualElement root, bool includeDefaultStyle = true)
{
if (root == null || _registeredRoots.Contains(root))
return;
_registeredRoots.Add(root);
ApplyThemeToRoot(root, includeDefaultStyle);
}
/// <summary>
/// 등록된 루트에서 스타일시트를 제거합니다.
/// </summary>
/// <param name="root">제거할 VisualElement</param>
public void UnregisterRoot(VisualElement root)
{
if (root == null)
return;
_registeredRoots.Remove(root);
RemoveThemeFromRoot(root);
}
/// <summary>
/// 테마를 전환합니다.
/// </summary>
/// <param name="theme">적용할 테마</param>
public void SetTheme(UTKTheme theme)
{
if (_currentTheme == theme)
return;
_currentTheme = theme;
// 모든 등록된 루트에 테마 적용
foreach (var root in _registeredRoots)
{
ApplyThemeToRoot(root, true);
}
OnThemeChanged?.Invoke(_currentTheme);
Debug.Log($"[UTKThemeManager] Theme changed to: {_currentTheme}");
}
/// <summary>
/// 테마를 토글합니다 (Dark ↔ Light).
/// </summary>
public void ToggleTheme()
{
SetTheme(_currentTheme == UTKTheme.Dark ? UTKTheme.Light : UTKTheme.Dark);
}
/// <summary>
/// 스타일시트만 반환합니다 (수동 적용용).
/// </summary>
/// <returns>현재 테마에 해당하는 스타일시트 배열</returns>
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 };
}
}
/// <summary>
/// 특정 컴포넌트에 필요한 스타일시트만 추가합니다.
/// 이미 루트에 등록된 경우 호출할 필요 없음.
/// </summary>
/// <param name="element">스타일을 적용할 요소</param>
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);
// 현재 테마 추가 (없는 경우에만)
if (currentThemeSheet != null && !root.styleSheets.Contains(currentThemeSheet))
root.styleSheets.Add(currentThemeSheet);
// Components 추가 (없는 경우에만)
if (_componentsSheet != null && !root.styleSheets.Contains(_componentsSheet))
root.styleSheets.Add(_componentsSheet);
// DefaultStyle 추가 (없는 경우에만)
if (includeDefaultStyle && _defaultStyleSheet != null && !root.styleSheets.Contains(_defaultStyleSheet))
root.styleSheets.Add(_defaultStyleSheet);
}
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
/// <summary>
/// 현재 테마의 변수 값을 가져옵니다.
/// VisualElement가 패널에 연결된 후에만 동작합니다.
/// </summary>
public static Color GetColor(VisualElement element, CustomStyleProperty<Color> property, Color fallback)
{
if (element?.customStyle == null)
return fallback;
return element.customStyle.TryGetValue(property, out var color) ? color : fallback;
}
/// <summary>
/// 현재 테마의 float 값을 가져옵니다.
/// </summary>
public static float GetFloat(VisualElement element, CustomStyleProperty<float> property, float fallback)
{
if (element?.customStyle == null)
return fallback;
return element.customStyle.TryGetValue(property, out var value) ? value : fallback;
}
/// <summary>
/// 현재 테마의 int 값을 가져옵니다.
/// </summary>
public static int GetInt(VisualElement element, CustomStyleProperty<int> property, int fallback)
{
if (element?.customStyle == null)
return fallback;
return element.customStyle.TryGetValue(property, out var value) ? value : fallback;
}
#endregion
}
/// <summary>
/// 지원하는 테마 종류
/// </summary>
public enum UTKTheme
{
Dark,
Light
}
}