UIToolkit 기본 UI 개발 중

This commit is contained in:
logonkhi
2026-01-08 20:15:57 +09:00
parent ef4e86820c
commit 71831dd4c3
319 changed files with 28283 additions and 761 deletions

View File

@@ -0,0 +1,104 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 접을 수 있는 섹션 컴포넌트.
/// Unity Foldout을 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
[UxmlElement]
public partial class UTKFoldout : Foldout, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Common/UTKFoldout";
#endregion
#region Fields
private bool _disposed;
#endregion
#region Events
/// <summary>펼침/접힘 상태 변경 이벤트</summary>
public event Action<bool>? OnValueChanged;
#endregion
#region Properties
/// <summary>펼침 상태</summary>
public bool IsExpanded
{
get => value;
set => this.value = value;
}
#endregion
#region Constructor
public UTKFoldout() : base()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
SetupStyles();
SetupEvents();
SubscribeToThemeChanges();
}
public UTKFoldout(string title, bool expanded = true) : this()
{
text = title;
value = expanded;
}
#endregion
#region Setup
private void SetupStyles()
{
AddToClassList("utk-foldout");
}
private void SetupEvents()
{
this.RegisterValueChangedCallback(OnFoldoutValueChanged);
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region Event Handlers
private void OnFoldoutValueChanged(ChangeEvent<bool> evt)
{
OnValueChanged?.Invoke(evt.newValue);
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
OnValueChanged = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 91209cef71c1caa46abfbcd3fc625650

View File

@@ -0,0 +1,114 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 도움말 박스 컴포넌트.
/// Unity HelpBox를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
[UxmlElement]
public partial class UTKHelpBox : HelpBox, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Common/UTKHelpBox";
#endregion
#region Fields
private bool _disposed;
#endregion
#region Properties
/// <summary>메시지 텍스트</summary>
public string Message
{
get => text;
set => text = value;
}
/// <summary>메시지 타입</summary>
public new HelpBoxMessageType messageType
{
get => base.messageType;
set
{
base.messageType = value;
UpdateMessageTypeClass();
}
}
#endregion
#region Constructor
public UTKHelpBox() : base()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
SetupStyles();
SubscribeToThemeChanges();
}
public UTKHelpBox(string message, HelpBoxMessageType type = HelpBoxMessageType.Info) : this()
{
text = message;
messageType = type;
}
#endregion
#region Setup
private void SetupStyles()
{
AddToClassList("utk-helpbox");
UpdateMessageTypeClass();
}
private void UpdateMessageTypeClass()
{
RemoveFromClassList("utk-helpbox--info");
RemoveFromClassList("utk-helpbox--warning");
RemoveFromClassList("utk-helpbox--error");
RemoveFromClassList("utk-helpbox--none");
var typeClass = messageType switch
{
HelpBoxMessageType.Warning => "utk-helpbox--warning",
HelpBoxMessageType.Error => "utk-helpbox--error",
HelpBoxMessageType.None => "utk-helpbox--none",
_ => "utk-helpbox--info"
};
AddToClassList(typeClass);
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c202715bb407dd64ab19c99f0e9674a0

View File

@@ -0,0 +1,75 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 스크롤 뷰 컴포넌트.
/// Unity ScrollView를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
[UxmlElement]
public partial class UTKScrollView : ScrollView, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Common/UTKScrollView";
#endregion
#region Fields
private bool _disposed;
#endregion
#region Constructor
public UTKScrollView() : base()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
SetupStyles();
SubscribeToThemeChanges();
}
public UTKScrollView(ScrollViewMode mode) : this()
{
this.mode = mode;
}
#endregion
#region Setup
private void SetupStyles()
{
AddToClassList("utk-scrollview");
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d911858474fd4ae49a5306eb5366528c

View File

@@ -0,0 +1,56 @@
#nullable enable
using UnityEngine.UIElements;
namespace UVC.UIToolkit.Common
{
/// <summary>
/// VisualElement에 대한 툴팁 확장 메서드
/// </summary>
public static class UTKTooltipExtensions
{
/// <summary>
/// VisualElement에 툴팁을 설정합니다.
/// UTKTooltipManager가 초기화되어 있어야 합니다.
/// </summary>
/// <param name="element">대상 요소</param>
/// <param name="tooltip">툴팁 텍스트 또는 다국어 키</param>
/// <returns>체이닝을 위한 원본 요소</returns>
public static T SetTooltip<T>(this T element, string tooltip) where T : VisualElement
{
if (UTKTooltipManager.Instance.IsInitialized)
{
UTKTooltipManager.Instance.AttachTooltip(element, tooltip);
}
return element;
}
/// <summary>
/// VisualElement에서 툴팁을 제거합니다.
/// </summary>
/// <param name="element">대상 요소</param>
/// <returns>체이닝을 위한 원본 요소</returns>
public static T ClearTooltip<T>(this T element) where T : VisualElement
{
if (UTKTooltipManager.Instance.IsInitialized)
{
UTKTooltipManager.Instance.DetachTooltip(element);
}
return element;
}
/// <summary>
/// VisualElement의 툴팁을 업데이트합니다.
/// </summary>
/// <param name="element">대상 요소</param>
/// <param name="tooltip">새 툴팁 텍스트</param>
/// <returns>체이닝을 위한 원본 요소</returns>
public static T UpdateTooltip<T>(this T element, string tooltip) where T : VisualElement
{
if (UTKTooltipManager.Instance.IsInitialized)
{
UTKTooltipManager.Instance.UpdateTooltip(element, tooltip);
}
return element;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 67c6909173307404b849ffdc51eabae6

View File

@@ -0,0 +1,413 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Locale;
namespace UVC.UIToolkit.Common
{
/// <summary>
/// UIToolkit 기반 툴팁 매니저
/// VisualElement에 마우스 오버 시 툴팁 표시
/// </summary>
public class UTKTooltipManager : IDisposable
{
#region Singleton
private static UTKTooltipManager? _instance;
public static UTKTooltipManager Instance => _instance ??= new UTKTooltipManager();
protected UTKTooltipManager() { }
#endregion
#region Constants
private const string UXML_PATH = "UIToolkit/Common/UTKTooltip";
private const string USS_PATH = "UIToolkit/Common/UTKTooltip";
private const int SHOW_DELAY_MS = 500;
private const int POSITION_OFFSET = 10;
#endregion
#region Fields
private VisualElement? _root;
private VisualElement? _tooltipContainer;
private Label? _tooltipLabel;
private bool _isInitialized;
private bool _isVisible;
private bool _disposed;
private CancellationTokenSource? _showDelayCts;
private readonly Dictionary<VisualElement, string> _tooltipRegistry = new();
private readonly Dictionary<VisualElement, EventCallback<PointerEnterEvent>> _enterCallbacks = new();
private readonly Dictionary<VisualElement, EventCallback<PointerLeaveEvent>> _leaveCallbacks = new();
private readonly Dictionary<VisualElement, EventCallback<PointerMoveEvent>> _moveCallbacks = new();
#endregion
#region Properties
public bool IsInitialized => _isInitialized;
public bool IsVisible => _isVisible;
#endregion
#region Initialization
/// <summary>
/// 툴팁 매니저를 초기화합니다.
/// </summary>
/// <param name="root">VisualElement 트리의 루트</param>
public void Initialize(VisualElement root)
{
if (_isInitialized)
{
Debug.LogWarning("[UTKTooltipManager] Already initialized.");
return;
}
_root = root;
// UXML 로드 시도
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
if (visualTree != null)
{
_tooltipContainer = visualTree.Instantiate();
_tooltipContainer.name = "utk-tooltip-container";
_tooltipLabel = _tooltipContainer.Q<Label>("tooltip-label");
}
else
{
// UXML 없으면 코드로 생성
CreateTooltipUI();
}
if (_tooltipContainer != null)
{
_tooltipContainer.style.position = Position.Absolute;
_tooltipContainer.style.display = DisplayStyle.None;
_tooltipContainer.pickingMode = PickingMode.Ignore;
_root.Add(_tooltipContainer);
}
// 테마 변경 이벤트 구독
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
_isInitialized = true;
}
/// <summary>
/// 테마 변경 시 호출
/// </summary>
private void OnThemeChanged(UTKTheme theme)
{
if (_tooltipContainer != null)
{
UTKThemeManager.Instance.ApplyThemeToElement(_tooltipContainer);
}
}
/// <summary>
/// 코드로 툴팁 UI 생성
/// </summary>
private void CreateTooltipUI()
{
_tooltipContainer = new VisualElement
{
name = "utk-tooltip-container",
pickingMode = PickingMode.Ignore
};
// 테마 적용
UTKThemeManager.Instance.ApplyThemeToElement(_tooltipContainer);
// USS 스타일시트 로드
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
_tooltipContainer.styleSheets.Add(uss);
}
// USS 클래스로 스타일 적용
_tooltipContainer.AddToClassList("utk-tooltip-container");
_tooltipLabel = new Label
{
name = "tooltip-label",
pickingMode = PickingMode.Ignore
};
_tooltipLabel.AddToClassList("utk-tooltip-label");
_tooltipContainer.Add(_tooltipLabel);
}
#endregion
#region Public Methods
/// <summary>
/// 툴팁을 즉시 표시합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="position">화면 좌표</param>
public void Show(string text, Vector2 position)
{
if (!_isInitialized || _tooltipContainer == null || _tooltipLabel == null)
return;
// 다국어 처리
string displayText = GetLocalizedText(text);
if (string.IsNullOrEmpty(displayText))
{
Hide();
return;
}
_tooltipLabel.text = displayText;
_tooltipContainer.style.display = DisplayStyle.Flex;
_isVisible = true;
// 다음 프레임에 위치 조정 (레이아웃 계산 후)
_tooltipContainer.schedule.Execute(() => AdjustPosition(position));
}
/// <summary>
/// 지연 후 툴팁을 표시합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="position">화면 좌표</param>
/// <param name="delayMs">지연 시간 (밀리초)</param>
public async UniTaskVoid ShowDelayed(string text, Vector2 position, int delayMs = SHOW_DELAY_MS)
{
CancelShowDelay();
_showDelayCts = new CancellationTokenSource();
try
{
await UniTask.Delay(delayMs, cancellationToken: _showDelayCts.Token);
Show(text, position);
}
catch (OperationCanceledException)
{
// 취소됨 - 무시
}
}
/// <summary>
/// 툴팁을 숨깁니다.
/// </summary>
public void Hide()
{
CancelShowDelay();
if (_tooltipContainer != null)
{
_tooltipContainer.style.display = DisplayStyle.None;
}
_isVisible = false;
}
/// <summary>
/// VisualElement에 툴팁을 연결합니다.
/// </summary>
/// <param name="element">대상 요소</param>
/// <param name="tooltip">툴팁 텍스트 또는 다국어 키</param>
public void AttachTooltip(VisualElement element, string tooltip)
{
if (element == null || string.IsNullOrEmpty(tooltip))
return;
// 기존 등록 제거
DetachTooltip(element);
_tooltipRegistry[element] = tooltip;
// 이벤트 콜백 생성 및 등록
EventCallback<PointerEnterEvent> enterCallback = evt =>
{
if (_tooltipRegistry.TryGetValue(element, out var text))
{
ShowDelayed(text, evt.position).Forget();
}
};
EventCallback<PointerLeaveEvent> leaveCallback = _ => Hide();
EventCallback<PointerMoveEvent> moveCallback = evt =>
{
if (_isVisible)
{
AdjustPosition(evt.position);
}
};
element.RegisterCallback(enterCallback);
element.RegisterCallback(leaveCallback);
element.RegisterCallback(moveCallback);
_enterCallbacks[element] = enterCallback;
_leaveCallbacks[element] = leaveCallback;
_moveCallbacks[element] = moveCallback;
}
/// <summary>
/// VisualElement에서 툴팁을 제거합니다.
/// </summary>
/// <param name="element">대상 요소</param>
public void DetachTooltip(VisualElement element)
{
if (element == null)
return;
_tooltipRegistry.Remove(element);
if (_enterCallbacks.TryGetValue(element, out var enterCallback))
{
element.UnregisterCallback(enterCallback);
_enterCallbacks.Remove(element);
}
if (_leaveCallbacks.TryGetValue(element, out var leaveCallback))
{
element.UnregisterCallback(leaveCallback);
_leaveCallbacks.Remove(element);
}
if (_moveCallbacks.TryGetValue(element, out var moveCallback))
{
element.UnregisterCallback(moveCallback);
_moveCallbacks.Remove(element);
}
}
/// <summary>
/// 등록된 툴팁 텍스트를 업데이트합니다.
/// </summary>
/// <param name="element">대상 요소</param>
/// <param name="tooltip">새 툴팁 텍스트</param>
public void UpdateTooltip(VisualElement element, string tooltip)
{
if (element == null)
return;
if (string.IsNullOrEmpty(tooltip))
{
DetachTooltip(element);
}
else if (_tooltipRegistry.ContainsKey(element))
{
_tooltipRegistry[element] = tooltip;
}
else
{
AttachTooltip(element, tooltip);
}
}
#endregion
#region Private Methods
/// <summary>
/// 지연 표시 취소
/// </summary>
private void CancelShowDelay()
{
_showDelayCts?.Cancel();
_showDelayCts?.Dispose();
_showDelayCts = null;
}
/// <summary>
/// 다국어 텍스트 가져오기
/// </summary>
private string GetLocalizedText(string keyOrText)
{
if (string.IsNullOrEmpty(keyOrText))
return string.Empty;
// LocalizationManager 연동
if (LocalizationManager.Instance != null)
{
string localized = LocalizationManager.Instance.GetString(keyOrText);
if (!string.IsNullOrEmpty(localized) && localized != keyOrText)
{
return localized;
}
}
return keyOrText;
}
/// <summary>
/// 화면 경계 내에서 위치 조정
/// </summary>
private void AdjustPosition(Vector2 mousePosition)
{
if (_tooltipContainer == null || _root == null)
return;
var tooltipSize = new Vector2(
_tooltipContainer.resolvedStyle.width,
_tooltipContainer.resolvedStyle.height
);
var rootSize = new Vector2(
_root.resolvedStyle.width,
_root.resolvedStyle.height
);
// 기본 위치: 마우스 오른쪽 아래
float x = mousePosition.x + POSITION_OFFSET;
float y = mousePosition.y + POSITION_OFFSET;
// 오른쪽 경계 체크
if (x + tooltipSize.x > rootSize.x)
{
x = mousePosition.x - tooltipSize.x - POSITION_OFFSET;
}
// 아래쪽 경계 체크
if (y + tooltipSize.y > rootSize.y)
{
y = mousePosition.y - tooltipSize.y - POSITION_OFFSET;
}
// 왼쪽/위쪽 경계 체크
x = Mathf.Max(0, x);
y = Mathf.Max(0, y);
_tooltipContainer.style.left = x;
_tooltipContainer.style.top = y;
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
// 테마 이벤트 구독 해제
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
CancelShowDelay();
// 모든 등록된 요소에서 이벤트 해제
foreach (var element in new List<VisualElement>(_tooltipRegistry.Keys))
{
DetachTooltip(element);
}
_tooltipRegistry.Clear();
_enterCallbacks.Clear();
_leaveCallbacks.Clear();
_moveCallbacks.Clear();
// UI 제거
_tooltipContainer?.RemoveFromHierarchy();
_tooltipContainer = null;
_tooltipLabel = null;
_root = null;
_isInitialized = false;
_isVisible = false;
_instance = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3e9f40c91d7a6e74286d1173edb7a120