445 lines
18 KiB
C#
445 lines
18 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace UVC.UIToolkit
|
|
{
|
|
/// <summary>
|
|
/// UIToolkit 테마 관리자.
|
|
/// 런타임에서 Light/Dark 테마 전환을 지원하며, 모든 UTK 컴포넌트에 일관된 스타일을 적용합니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para><b>테마 매니저란?</b></para>
|
|
/// <para>
|
|
/// UTKThemeManager는 애플리케이션 전체의 UI 테마(색상, 스타일)를 중앙에서 관리합니다.
|
|
/// 싱글톤 패턴으로 구현되어 어디서든 <c>UTKThemeManager.Instance</c>로 접근 가능합니다.
|
|
/// </para>
|
|
///
|
|
/// <para><b>스타일시트 로드 순서:</b></para>
|
|
/// <list type="number">
|
|
/// <item><description>UTKVariables.uss - 레이아웃 변수 (spacing, radius, font-size)</description></item>
|
|
/// <item><description>UTKThemeDark/Light.uss - 테마별 색상 변수</description></item>
|
|
/// <item><description>UTKComponents.uss - 공통 컴포넌트 스타일</description></item>
|
|
/// <item><description>UTKDefaultStyle.uss - Unity 기본 요소 오버라이드 (선택)</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>주요 기능:</b></para>
|
|
/// <list type="bullet">
|
|
/// <item><description><c>RegisterRoot()</c> - UIDocument 루트에 테마 등록</description></item>
|
|
/// <item><description><c>SetTheme()</c> - 테마 변경 (Dark/Light)</description></item>
|
|
/// <item><description><c>ToggleTheme()</c> - 테마 토글</description></item>
|
|
/// <item><description><c>ApplyThemeToElement()</c> - 개별 요소에 테마 적용</description></item>
|
|
/// <item><description><c>OnThemeChanged</c> - 테마 변경 이벤트 구독</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>사용 시점:</b></para>
|
|
/// <list type="bullet">
|
|
/// <item><description>UIDocument 초기화 시 <c>RegisterRoot()</c> 호출</description></item>
|
|
/// <item><description>UTK 컴포넌트 내부에서 자동으로 <c>ApplyThemeToElement()</c> 호출</description></item>
|
|
/// <item><description>설정 화면에서 테마 전환 버튼 클릭 시 <c>ToggleTheme()</c> 호출</description></item>
|
|
/// </list>
|
|
/// </remarks>
|
|
/// <example>
|
|
/// <para><b>기본 사용법 - UIDocument에 테마 등록:</b></para>
|
|
/// <code>
|
|
/// 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);
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
///
|
|
/// <para><b>테마 전환:</b></para>
|
|
/// <code>
|
|
/// // 특정 테마로 변경
|
|
/// UTKThemeManager.Instance.SetTheme(UTKTheme.Light);
|
|
///
|
|
/// // 토글 (Dark ↔ Light)
|
|
/// UTKThemeManager.Instance.ToggleTheme();
|
|
///
|
|
/// // 현재 테마 확인
|
|
/// if (UTKThemeManager.Instance.IsDarkTheme)
|
|
/// {
|
|
/// Debug.Log("다크 테마 사용 중");
|
|
/// }
|
|
/// </code>
|
|
///
|
|
/// <para><b>테마 변경 이벤트 구독:</b></para>
|
|
/// <code>
|
|
/// void OnEnable()
|
|
/// {
|
|
/// UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
|
/// }
|
|
///
|
|
/// void OnDisable()
|
|
/// {
|
|
/// UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
|
/// }
|
|
///
|
|
/// private void OnThemeChanged(UTKTheme newTheme)
|
|
/// {
|
|
/// Debug.Log($"테마 변경됨: {newTheme}");
|
|
/// // 테마에 따른 추가 로직 (아이콘 변경 등)
|
|
/// }
|
|
/// </code>
|
|
///
|
|
/// <para><b>커스텀 스타일 값 가져오기:</b></para>
|
|
/// <code>
|
|
/// // USS 변수에서 색상 값 읽기
|
|
/// var primaryColor = new CustomStyleProperty<Color>("--color-btn-primary");
|
|
/// var color = UTKThemeManager.GetColor(myElement, primaryColor, Color.blue);
|
|
///
|
|
/// // USS 변수에서 float 값 읽기
|
|
/// var spacing = new CustomStyleProperty<float>("--space-m");
|
|
/// var value = UTKThemeManager.GetFloat(myElement, spacing, 8f);
|
|
/// </code>
|
|
///
|
|
/// <para><b>UTK 컴포넌트에서 사용 (내부 구현):</b></para>
|
|
/// <code>
|
|
/// [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<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
|
/// RegisterCallback<DetachFromPanelEvent>(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<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
|
/// UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
///
|
|
/// <para><b>테마 전환 버튼 구현:</b></para>
|
|
/// <code>
|
|
/// var themeToggle = new UTKButton("테마 전환", UTKButtonVariant.Secondary);
|
|
/// themeToggle.clicked += () =>
|
|
/// {
|
|
/// UTKThemeManager.Instance.ToggleTheme();
|
|
/// var icon = UTKThemeManager.Instance.IsDarkTheme
|
|
/// ? UTKMaterialIcons.DarkMode
|
|
/// : UTKMaterialIcons.LightMode;
|
|
/// themeToggle.SetMaterialIcon(icon);
|
|
/// };
|
|
/// </code>
|
|
/// </example>
|
|
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);
|
|
}
|
|
#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);
|
|
}
|
|
|
|
/// <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);
|
|
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
|
|
/// <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>
|
|
/// <remarks>
|
|
/// <para>각 테마는 해당 USS 파일의 색상 변수를 사용합니다:</para>
|
|
/// <list type="bullet">
|
|
/// <item><description><c>Dark</c> - UTKThemeDark.uss (어두운 배경, 밝은 텍스트)</description></item>
|
|
/// <item><description><c>Light</c> - UTKThemeLight.uss (밝은 배경, 어두운 텍스트)</description></item>
|
|
/// </list>
|
|
/// </remarks>
|
|
public enum UTKTheme
|
|
{
|
|
/// <summary>다크 테마 (기본값)</summary>
|
|
Dark,
|
|
/// <summary>라이트 테마</summary>
|
|
Light
|
|
}
|
|
}
|