StyleGuide Sample 완료

This commit is contained in:
logonkhi
2026-01-13 20:39:45 +09:00
parent c8ff7b503d
commit ee86f93814
47 changed files with 20319 additions and 88 deletions

2
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,2 @@
프로젝트 루트 폴더의 CLAUDE.md를 참조해
답변은 항상 한국어로 작성해

View File

@@ -0,0 +1,578 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace UVC.UIToolkit.Editor
{
/// <summary>
/// MaterialSymbolsOutlinedCodepoints.txt 파일을 파싱하여
/// UTKMaterialIcons.cs 정적 클래스를 자동 생성하는 에디터 도구입니다.
/// </summary>
public class UTKMaterialIconsGenerator : EditorWindow
{
#region EditorPrefs Keys
private const string PrefKeyCodepointsPath = "UTK_MaterialIcons_CodepointsPath";
private const string PrefKeyOutputPath = "UTK_MaterialIcons_OutputPath";
private const string PrefKeyClassName = "UTK_MaterialIcons_ClassName";
private const string PrefKeyNamespace = "UTK_MaterialIcons_Namespace";
private const string PrefKeyFontPath = "UTK_MaterialIcons_FontPath";
#endregion
#region Default Values
private const string DefaultCodepointsPath = "Assets/Resources/Fonts/Icons/MaterialSymbolsOutlinedCodepoints.txt";
private const string DefaultOutputPath = "Assets/Scripts/UVC/UIToolkit/Common/UTKMaterialIcons.cs";
private const string DefaultClassName = "UTKMaterialIcons";
private const string DefaultNamespace = "UVC.UIToolkit";
private const string DefaultFontPath = "Fonts/Icons/MaterialSymbolsOutlined";
#endregion
#region Fields
private string _codepointsPath = DefaultCodepointsPath;
private string _outputPath = DefaultOutputPath;
private string _className = DefaultClassName;
private string _namespace = DefaultNamespace;
private string _fontPath = DefaultFontPath;
#endregion
[MenuItem("Tools/UTK/Material Icons Generator")]
public static void ShowWindow()
{
var window = GetWindow<UTKMaterialIconsGenerator>("Material Icons Generator");
window.minSize = new Vector2(450, 250);
window.LoadSettings();
}
[MenuItem("Tools/UTK/Generate Material Icons Class (Quick)")]
public static void GenerateQuick()
{
var codepointsPath = EditorPrefs.GetString(PrefKeyCodepointsPath, DefaultCodepointsPath);
var outputPath = EditorPrefs.GetString(PrefKeyOutputPath, DefaultOutputPath);
var className = EditorPrefs.GetString(PrefKeyClassName, DefaultClassName);
var namespaceName = EditorPrefs.GetString(PrefKeyNamespace, DefaultNamespace);
var fontPath = EditorPrefs.GetString(PrefKeyFontPath, DefaultFontPath);
Generate(codepointsPath, outputPath, className, namespaceName, fontPath);
}
private void OnEnable()
{
LoadSettings();
}
private void LoadSettings()
{
_codepointsPath = EditorPrefs.GetString(PrefKeyCodepointsPath, DefaultCodepointsPath);
_outputPath = EditorPrefs.GetString(PrefKeyOutputPath, DefaultOutputPath);
_className = EditorPrefs.GetString(PrefKeyClassName, DefaultClassName);
_namespace = EditorPrefs.GetString(PrefKeyNamespace, DefaultNamespace);
_fontPath = EditorPrefs.GetString(PrefKeyFontPath, DefaultFontPath);
}
private void SaveSettings()
{
EditorPrefs.SetString(PrefKeyCodepointsPath, _codepointsPath);
EditorPrefs.SetString(PrefKeyOutputPath, _outputPath);
EditorPrefs.SetString(PrefKeyClassName, _className);
EditorPrefs.SetString(PrefKeyNamespace, _namespace);
EditorPrefs.SetString(PrefKeyFontPath, _fontPath);
}
private void OnGUI()
{
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Material Icons Class Generator", EditorStyles.boldLabel);
EditorGUILayout.Space(5);
EditorGUILayout.HelpBox("Codepoints 파일을 파싱하여 C# 정적 클래스를 생성합니다.", MessageType.Info);
EditorGUILayout.Space(10);
// Codepoints 파일 경로
EditorGUILayout.BeginHorizontal();
_codepointsPath = EditorGUILayout.TextField("Codepoints File", _codepointsPath);
if (GUILayout.Button("...", GUILayout.Width(30)))
{
var path = EditorUtility.OpenFilePanel("Select Codepoints File", "Assets", "txt");
if (!string.IsNullOrEmpty(path))
{
// 상대 경로로 변환
if (path.StartsWith(Application.dataPath))
{
path = "Assets" + path.Substring(Application.dataPath.Length);
}
_codepointsPath = path;
}
}
EditorGUILayout.EndHorizontal();
// 출력 파일 경로
EditorGUILayout.BeginHorizontal();
_outputPath = EditorGUILayout.TextField("Output File", _outputPath);
if (GUILayout.Button("...", GUILayout.Width(30)))
{
var directory = Path.GetDirectoryName(_outputPath) ?? "Assets";
var filename = Path.GetFileName(_outputPath);
var path = EditorUtility.SaveFilePanel("Save Generated Class", directory, filename, "cs");
if (!string.IsNullOrEmpty(path))
{
// 상대 경로로 변환
if (path.StartsWith(Application.dataPath))
{
path = "Assets" + path.Substring(Application.dataPath.Length);
}
_outputPath = path;
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// 클래스명
_className = EditorGUILayout.TextField("Class Name", _className);
// 네임스페이스
_namespace = EditorGUILayout.TextField("Namespace", _namespace);
// 폰트 경로 (Resources 기준)
_fontPath = EditorGUILayout.TextField("Font Path (Resources)", _fontPath);
EditorGUILayout.HelpBox("Resources.Load에 사용할 폰트 경로 (확장자 제외)", MessageType.None);
EditorGUILayout.Space(15);
// 파일 존재 여부 표시
var codepointsExists = File.Exists(_codepointsPath);
var outputExists = File.Exists(_outputPath);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Codepoints File:", GUILayout.Width(100));
EditorGUILayout.LabelField(codepointsExists ? "Found" : "Not Found",
codepointsExists ? EditorStyles.label : EditorStyles.boldLabel);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Output File:", GUILayout.Width(100));
EditorGUILayout.LabelField(outputExists ? "Exists (will overwrite)" : "New file",
EditorStyles.label);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(15);
// 버튼들
EditorGUILayout.BeginHorizontal();
GUI.enabled = codepointsExists;
if (GUILayout.Button("Generate", GUILayout.Height(30)))
{
SaveSettings();
Generate(_codepointsPath, _outputPath, _className, _namespace, _fontPath);
}
GUI.enabled = true;
if (GUILayout.Button("Reset to Defaults", GUILayout.Height(30)))
{
_codepointsPath = DefaultCodepointsPath;
_outputPath = DefaultOutputPath;
_className = DefaultClassName;
_namespace = DefaultNamespace;
_fontPath = DefaultFontPath;
SaveSettings();
}
EditorGUILayout.EndHorizontal();
}
public static void Generate(string codepointsPath, string outputPath, string className, string namespaceName, string fontPath = DefaultFontPath)
{
if (!File.Exists(codepointsPath))
{
Debug.LogError($"Codepoints 파일을 찾을 수 없습니다: {codepointsPath}");
return;
}
var icons = ParseCodepoints(codepointsPath);
if (icons.Count == 0)
{
Debug.LogError("파싱된 아이콘이 없습니다.");
return;
}
var code = GenerateCode(icons, className, namespaceName, fontPath);
// 디렉토리 생성
var directory = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(outputPath, code, Encoding.UTF8);
AssetDatabase.Refresh();
Debug.Log($"{className}.cs 생성 완료: {icons.Count}개 아이콘 ({outputPath})");
}
private static List<(string name, string unicode)> ParseCodepoints(string path)
{
var result = new List<(string, string)>();
var lines = File.ReadAllLines(path);
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line)) continue;
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2)
{
result.Add((parts[0], parts[1]));
}
}
return result;
}
private static string GenerateCode(List<(string name, string unicode)> icons, string className, string namespaceName, string fontPath)
{
var sb = new StringBuilder();
var usedNames = new HashSet<string>();
sb.AppendLine("// <auto-generated>");
sb.AppendLine("// 이 파일은 UTKMaterialIconsGenerator에 의해 자동 생성되었습니다.");
sb.AppendLine("// 직접 수정하지 마세요. Tools > UTK > Material Icons Generator 메뉴로 재생성하세요.");
sb.AppendLine("// </auto-generated>");
sb.AppendLine();
sb.AppendLine("#nullable enable");
sb.AppendLine("using System;");
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine("using System.Threading;");
sb.AppendLine("using Cysharp.Threading.Tasks;");
sb.AppendLine("using UnityEngine;");
sb.AppendLine("using UnityEngine.UIElements;");
sb.AppendLine();
sb.AppendLine($"namespace {namespaceName}");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Material Symbols Outlined 아이콘 유니코드 문자 상수 클래스입니다.");
sb.AppendLine($" /// 총 {icons.Count}개의 아이콘을 포함합니다.");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <example>");
sb.AppendLine(" /// <code>");
sb.AppendLine($" /// // 동기 폰트 로드 (캐싱됨)");
sb.AppendLine($" /// Font font = {className}.LoadFont();");
sb.AppendLine(" /// ");
sb.AppendLine($" /// // 비동기 폰트 로드 (UniTask)");
sb.AppendLine($" /// Font? font = await {className}.LoadFontAsync(cancellationToken);");
sb.AppendLine(" /// ");
sb.AppendLine($" /// // Label에 아이콘 적용");
sb.AppendLine($" /// var label = new Label({className}.Home);");
sb.AppendLine($" /// {className}.ApplyIconStyle(label);");
sb.AppendLine(" /// ");
sb.AppendLine($" /// // 비동기로 아이콘 스타일 적용");
sb.AppendLine($" /// await {className}.ApplyIconStyleAsync(label, cancellationToken);");
sb.AppendLine(" /// ");
sb.AppendLine($" /// // 또는 StyleFontDefinition 직접 사용");
sb.AppendLine($" /// label.style.unityFontDefinition = {className}.GetFontDefinition();");
sb.AppendLine(" /// ");
sb.AppendLine($" /// // 이름으로 아이콘 조회");
sb.AppendLine($" /// string icon = {className}.GetIcon(\"settings\");");
sb.AppendLine(" /// ");
sb.AppendLine($" /// // 아이콘 존재 여부 확인");
sb.AppendLine($" /// if ({className}.HasIcon(\"search\")) {{ }}");
sb.AppendLine(" /// ");
sb.AppendLine($" /// // 전체 아이콘 이름 순회");
sb.AppendLine($" /// foreach (var name in {className}.GetAllIconNames()) {{ }}");
sb.AppendLine(" /// ");
sb.AppendLine($" /// // 아이콘 총 개수");
sb.AppendLine($" /// int count = {className}.Count;");
sb.AppendLine(" /// </code>");
sb.AppendLine(" /// </example>");
sb.AppendLine(" /// <remarks>");
sb.AppendLine(" /// <para><b>UXML에서 사용하기:</b></para>");
sb.AppendLine(" /// <para>UXML에서 Material Icons를 사용하려면 USS에서 폰트를 설정하고, Label의 text에 유니코드 문자를 직접 입력합니다.</para>");
sb.AppendLine(" /// <code>");
sb.AppendLine(" /// /* USS 파일 */");
sb.AppendLine(" /// .material-icon {");
sb.AppendLine(" /// -unity-font: resource('Fonts/Icons/MaterialSymbolsOutlined');");
sb.AppendLine(" /// font-size: 24px;");
sb.AppendLine(" /// }");
sb.AppendLine(" /// </code>");
sb.AppendLine(" /// <code>");
sb.AppendLine(" /// <!-- UXML 파일 -->");
sb.AppendLine(" /// <ui:Label class=\"material-icon\" text=\"&#xe88a;\" /> <!-- home 아이콘 -->");
sb.AppendLine(" /// <ui:Label class=\"material-icon\" text=\"&#xe8b8;\" /> <!-- settings 아이콘 -->");
sb.AppendLine(" /// </code>");
sb.AppendLine(" /// <para><b>C# 코드에서 UXML Label에 아이콘 적용:</b></para>");
sb.AppendLine(" /// <code>");
sb.AppendLine($" /// var iconLabel = root.Q<Label>(\"my-icon\");");
sb.AppendLine($" /// iconLabel.text = {className}.Settings;");
sb.AppendLine($" /// {className}.ApplyIconStyle(iconLabel);");
sb.AppendLine(" /// </code>");
sb.AppendLine(" /// </remarks>");
sb.AppendLine($" public static class {className}");
sb.AppendLine(" {");
sb.AppendLine();
sb.AppendLine(" #region Font");
sb.AppendLine();
sb.AppendLine($" private const string FontResourcePath = \"{fontPath}\";");
sb.AppendLine(" private static Font? _cachedFont;");
sb.AppendLine(" private static StyleFontDefinition? _cachedFontDefinition;");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 아이콘 폰트를 동기로 로드합니다. (캐싱됨)");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <returns>로드된 Font, 실패 시 null</returns>");
sb.AppendLine(" public static Font? LoadFont()");
sb.AppendLine(" {");
sb.AppendLine(" if (_cachedFont == null)");
sb.AppendLine(" {");
sb.AppendLine(" _cachedFont = Resources.Load<Font>(FontResourcePath);");
sb.AppendLine(" }");
sb.AppendLine(" return _cachedFont;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 아이콘 폰트를 비동기로 로드합니다. (캐싱됨)");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"ct\">취소 토큰</param>");
sb.AppendLine(" /// <returns>로드된 Font, 실패 시 null</returns>");
sb.AppendLine(" public static async UniTask<Font?> LoadFontAsync(CancellationToken ct = default)");
sb.AppendLine(" {");
sb.AppendLine(" if (_cachedFont != null)");
sb.AppendLine(" {");
sb.AppendLine(" return _cachedFont;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" var request = Resources.LoadAsync<Font>(FontResourcePath);");
sb.AppendLine(" await request.ToUniTask(cancellationToken: ct);");
sb.AppendLine();
sb.AppendLine(" _cachedFont = request.asset as Font;");
sb.AppendLine(" return _cachedFont;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// UI Toolkit용 StyleFontDefinition을 동기로 반환합니다. (캐싱됨)");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <returns>StyleFontDefinition, 폰트 로드 실패 시 기본값</returns>");
sb.AppendLine(" public static StyleFontDefinition GetFontDefinition()");
sb.AppendLine(" {");
sb.AppendLine(" if (_cachedFontDefinition == null)");
sb.AppendLine(" {");
sb.AppendLine(" var font = LoadFont();");
sb.AppendLine(" if (font != null)");
sb.AppendLine(" {");
sb.AppendLine(" _cachedFontDefinition = new StyleFontDefinition(font);");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine(" return _cachedFontDefinition ?? default;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// UI Toolkit용 StyleFontDefinition을 비동기로 반환합니다. (캐싱됨)");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"ct\">취소 토큰</param>");
sb.AppendLine(" /// <returns>StyleFontDefinition, 폰트 로드 실패 시 기본값</returns>");
sb.AppendLine(" public static async UniTask<StyleFontDefinition> GetFontDefinitionAsync(CancellationToken ct = default)");
sb.AppendLine(" {");
sb.AppendLine(" if (_cachedFontDefinition == null)");
sb.AppendLine(" {");
sb.AppendLine(" var font = await LoadFontAsync(ct);");
sb.AppendLine(" if (font != null)");
sb.AppendLine(" {");
sb.AppendLine(" _cachedFontDefinition = new StyleFontDefinition(font);");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine(" return _cachedFontDefinition ?? default;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// VisualElement에 아이콘 폰트 스타일을 동기로 적용합니다.");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"element\">스타일을 적용할 요소</param>");
sb.AppendLine(" /// <param name=\"fontSize\">폰트 크기 (기본값: 24)</param>");
sb.AppendLine(" public static void ApplyIconStyle(VisualElement element, int fontSize = 24)");
sb.AppendLine(" {");
sb.AppendLine(" element.style.unityFontDefinition = GetFontDefinition();");
sb.AppendLine(" element.style.fontSize = fontSize;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// VisualElement에 아이콘 폰트 스타일을 비동기로 적용합니다.");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"element\">스타일을 적용할 요소</param>");
sb.AppendLine(" /// <param name=\"ct\">취소 토큰</param>");
sb.AppendLine(" /// <param name=\"fontSize\">폰트 크기 (기본값: 24)</param>");
sb.AppendLine(" public static async UniTask ApplyIconStyleAsync(VisualElement element, CancellationToken ct = default, int fontSize = 24)");
sb.AppendLine(" {");
sb.AppendLine(" element.style.unityFontDefinition = await GetFontDefinitionAsync(ct);");
sb.AppendLine(" element.style.fontSize = fontSize;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 폰트 캐시를 클리어합니다.");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static void ClearFontCache()");
sb.AppendLine(" {");
sb.AppendLine(" _cachedFont = null;");
sb.AppendLine(" _cachedFontDefinition = null;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" #endregion");
sb.AppendLine();
// 아이콘 상수 생성
foreach (var (name, unicode) in icons)
{
var fieldName = ConvertToFieldName(name, usedNames);
var character = GetCharacterFromUnicode(unicode);
var escapedChar = EscapeForCSharp(character);
sb.AppendLine($" /// <summary>{name} (U+{unicode.ToUpper()})</summary>");
sb.AppendLine($" public const string {fieldName} = \"{escapedChar}\";");
}
sb.AppendLine();
sb.AppendLine(" #region Lookup");
sb.AppendLine();
// 이름으로 아이콘 조회 딕셔너리
sb.AppendLine(" private static readonly Dictionary<string, string> _iconsByName = new()");
sb.AppendLine(" {");
foreach (var (name, unicode) in icons)
{
var character = GetCharacterFromUnicode(unicode);
var escapedChar = EscapeForCSharp(character);
sb.AppendLine($" [\"{name}\"] = \"{escapedChar}\",");
}
sb.AppendLine(" };");
sb.AppendLine();
// 조회 메서드
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 아이콘 이름으로 유니코드 문자를 조회합니다.");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"iconName\">아이콘 이름 (예: \"home\", \"settings\")</param>");
sb.AppendLine(" /// <returns>아이콘 문자, 없으면 빈 문자열</returns>");
sb.AppendLine(" public static string GetIcon(string iconName)");
sb.AppendLine(" {");
sb.AppendLine(" return _iconsByName.TryGetValue(iconName, out var icon) ? icon : string.Empty;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 아이콘이 존재하는지 확인합니다.");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static bool HasIcon(string iconName) => _iconsByName.ContainsKey(iconName);");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 모든 아이콘 이름 목록을 반환합니다.");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEnumerable<string> GetAllIconNames() => _iconsByName.Keys;");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 전체 아이콘 수를 반환합니다.");
sb.AppendLine(" /// </summary>");
sb.AppendLine($" public static int Count => {icons.Count};");
sb.AppendLine();
sb.AppendLine(" #endregion");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string ConvertToFieldName(string iconName, HashSet<string> usedNames)
{
var sb = new StringBuilder();
var words = iconName.Split('_', StringSplitOptions.RemoveEmptyEntries);
foreach (var word in words)
{
if (word.Length == 0) continue;
// 첫 글자 대문자, 나머지 소문자 (PascalCase)
sb.Append(char.ToUpper(word[0]));
if (word.Length > 1)
{
sb.Append(word.Substring(1).ToLower());
}
}
var result = sb.ToString();
// 숫자로 시작하면 앞에 _ 추가
if (result.Length > 0 && char.IsDigit(result[0]))
{
result = "_" + result;
}
// 빈 문자열이면 기본값
if (string.IsNullOrEmpty(result))
{
result = "_Icon";
}
// 중복 처리
var originalResult = result;
var counter = 2;
while (usedNames.Contains(result))
{
result = $"{originalResult}_{counter}";
counter++;
}
usedNames.Add(result);
return result;
}
private static string GetCharacterFromUnicode(string hexCode)
{
try
{
var codepoint = int.Parse(hexCode, NumberStyles.HexNumber);
return char.ConvertFromUtf32(codepoint);
}
catch
{
return "?";
}
}
private static string EscapeForCSharp(string str)
{
var sb = new StringBuilder();
foreach (var c in str)
{
// 유니코드 이스케이프 시퀀스로 변환
if (c > 127 || c < 32)
{
// Surrogate pair 처리
sb.Append($"\\u{(int)c:X4}");
}
else if (c == '"')
{
sb.Append("\\\"");
}
else if (c == '\\')
{
sb.Append("\\\\");
}
else
{
sb.Append(c);
}
}
return sb.ToString();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -292,6 +292,70 @@
color: var(--color-text-on-primary);
}
/* ===================================
Text Variant (Label/Icon only, no background/border)
=================================== */
/* Text - 배경과 외곽선이 투명하고 텍스트/아이콘만 표시 */
.utk-button--text {
background-color: transparent;
border-color: transparent;
border-width: 0;
color: var(--color-text-primary);
padding-left: var(--space-s);
padding-right: var(--space-s);
min-width: auto;
}
.utk-button--text:hover {
background-color: transparent;
color: var(--color-btn-primary);
}
.utk-button--text:active {
background-color: transparent;
color: var(--color-btn-primary-hover);
}
.utk-button--text .utk-button__text,
.utk-button--text .utk-button__icon {
color: var(--color-text-primary);
}
.utk-button--text:hover .utk-button__text,
.utk-button--text:hover .utk-button__icon {
color: var(--color-btn-primary);
}
.utk-button--text:active .utk-button__text,
.utk-button--text:active .utk-button__icon {
color: var(--color-btn-primary-hover);
}
/* Text Icon Only - 원형 아이콘 버튼 */
.utk-button--text.utk-button--icon-only {
width: var(--size-icon-btn);
height: var(--size-icon-btn);
border-radius: 50%;
padding: 0;
}
.utk-button--text.utk-button--icon-only:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.utk-button--text.utk-button--icon-only:hover .utk-button__icon {
color: var(--color-text-primary);
}
.utk-button--text.utk-button--icon-only:active {
background-color: rgba(255, 255, 255, 0.15);
}
.utk-button--text.utk-button--icon-only:active .utk-button__icon {
color: var(--color-text-primary);
}
/* ===================================
Disabled State
=================================== */
@@ -321,3 +385,53 @@
.utk-button--disabled .utk-button__icon {
color: var(--color-text-disabled);
}
/* Text Variant Disabled - hover/active 시에도 색상 유지 */
.utk-button--text.utk-button--disabled {
background-color: transparent;
border-color: transparent;
}
.utk-button--text.utk-button--disabled:hover {
background-color: transparent;
}
.utk-button--text.utk-button--disabled:hover .utk-button__text,
.utk-button--text.utk-button--disabled:hover .utk-button__icon {
color: var(--color-text-disabled);
}
.utk-button--text.utk-button--disabled:active .utk-button__text,
.utk-button--text.utk-button--disabled:active .utk-button__icon {
color: var(--color-text-disabled);
}
/* ===================================
Image Icon Support
=================================== */
.utk-button__image-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
-unity-background-scale-mode: scale-to-fit;
}
.utk-button--has-image-icon .utk-button__image-icon {
margin-right: var(--space-s);
}
.utk-button--icon-only .utk-button__image-icon {
margin-right: 0;
}
/* Size Variants for Image Icon */
.utk-button--small .utk-button__image-icon {
width: 16px;
height: 16px;
}
.utk-button--large .utk-button__image-icon {
width: 24px;
height: 24px;
}

View File

@@ -136,3 +136,85 @@
.utk-label--right .utk-label__text {
-unity-text-align: middle-right;
}
/* ===================================
Icon Styles
=================================== */
.utk-label--has-icon {
align-items: center;
}
.utk-label__icon {
margin: 0;
padding: 0;
-unity-text-align: middle-center;
flex-shrink: 0;
-unity-font-definition: resource('Fonts/Icons/MaterialSymbolsOutlined');
font-size: 16px;
color: var(--color-text-primary);
}
/* Icon Color Variants */
.utk-label--primary .utk-label__icon {
color: var(--color-text-primary);
}
.utk-label--secondary .utk-label__icon {
color: var(--color-text-secondary);
}
.utk-label--disabled .utk-label__icon {
color: var(--color-text-disabled);
}
.utk-label--success .utk-label__icon {
color: var(--color-state-success);
}
.utk-label--warning .utk-label__icon {
color: var(--color-state-warning);
}
.utk-label--error .utk-label__icon {
color: var(--color-state-error);
}
.utk-label--info .utk-label__icon {
color: var(--color-state-info);
}
.utk-label__image-icon {
flex-shrink: 0;
-unity-background-scale-mode: scale-to-fit;
-unity-background-image-tint-color: var(--color-text-primary);
}
/* Image Icon Color Variants */
.utk-label--primary .utk-label__image-icon {
-unity-background-image-tint-color: var(--color-text-primary);
}
.utk-label--secondary .utk-label__image-icon {
-unity-background-image-tint-color: var(--color-text-secondary);
}
.utk-label--disabled .utk-label__image-icon {
-unity-background-image-tint-color: var(--color-text-disabled);
}
.utk-label--success .utk-label__image-icon {
-unity-background-image-tint-color: var(--color-state-success);
}
.utk-label--warning .utk-label__image-icon {
-unity-background-image-tint-color: var(--color-state-warning);
}
.utk-label--error .utk-label__image-icon {
-unity-background-image-tint-color: var(--color-state-error);
}
.utk-label--info .utk-label__image-icon {
-unity-background-image-tint-color: var(--color-state-info);
}

View File

@@ -9,7 +9,10 @@
border-radius: var(--radius-m);
border-width: var(--border-width);
border-color: var(--color-border);
padding: var(--space-l);
padding-top: var(--space-s);
padding-left: var(--space-l);
padding-right: var(--space-l);
padding-bottom: var(--space-l);
width: 320px;
}
@@ -37,7 +40,9 @@
background-color: transparent;
border-width: 0;
color: var(--color-text-secondary);
border-radius: var(--radius-s);
border-radius: var(--radius-full);
margin: 0;
padding: 10px;
}
.utk-color-picker__close-btn:hover {
@@ -222,32 +227,11 @@
margin-top: 15px;
}
/* UTKButton은 자체 스타일을 가지므로 마진/크기만 설정 */
.utk-color-picker__cancel-btn {
margin-right: var(--space-m);
width: var(--size-btn-min-width);
height: var(--size-btn-height);
font-size: var(--font-size-body2);
background-color: var(--color-btn-normal);
color: var(--color-text-primary);
border-width: var(--border-width);
border-color: var(--color-border);
border-radius: var(--radius-s);
}
.utk-color-picker__cancel-btn:hover {
background-color: var(--color-btn-hover);
}
.utk-color-picker__confirm-btn {
width: var(--size-btn-min-width);
height: var(--size-btn-height);
font-size: var(--font-size-body2);
background-color: var(--color-btn-primary);
color: var(--color-text-on-primary);
border-width: 0;
border-radius: var(--radius-s);
}
.utk-color-picker__confirm-btn:hover {
background-color: var(--color-btn-primary-hover);
/* UTKButton Primary 스타일 사용 */
}

View File

@@ -1,9 +1,9 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:utk="UVC.UIToolkit" editor-extension-mode="False">
<Style src="project://database/Assets/Resources/UIToolkit/Modal/UTKColorPicker.uss?fileID=7433441132597879392&amp;guid=7ae6523e61765d84caaec92e84dcc017&amp;type=3#UTKColorPicker" />
<ui:VisualElement name="root" class="utk-color-picker">
<ui:VisualElement name="header" class="utk-color-picker__header">
<ui:Label name="title" text="Color Picker" class="utk-color-picker__title" />
<ui:Button name="close-btn" text="" class="utk-color-picker__close-btn" />
<ui:Button name="close-btn" text="" class="utk-color-picker__close-btn material-icon" />
</ui:VisualElement>
<ui:VisualElement name="color-area" class="utk-color-picker__color-area">
<ui:VisualElement name="sv-box" class="utk-color-picker__sv-box">
@@ -44,8 +44,8 @@
<ui:TextField name="hex-field" class="utk-color-picker__hex-field" />
</ui:VisualElement>
<ui:VisualElement name="button-row" class="utk-color-picker__button-row">
<ui:Button name="cancel-btn" text="Cancel" class="utk-color-picker__cancel-btn" />
<ui:Button name="confirm-btn" text="OK" class="utk-color-picker__confirm-btn" />
<utk:UTKButton name="cancel-btn" text="Cancel" variant="Normal" class="utk-color-picker__cancel-btn" />
<utk:UTKButton name="confirm-btn" text="OK" variant="Primary" class="utk-color-picker__confirm-btn" />
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -18,7 +18,10 @@
border-radius: var(--radius-l);
border-width: var(--border-width);
border-color: var(--color-border);
padding: var(--space-l);
padding-top: var(--space-s);
padding-left: var(--space-l);
padding-right: var(--space-l);
padding-bottom: var(--space-l);
min-width: 300px;
}
@@ -31,6 +34,7 @@
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-l);
cursor: move;
}
.utk-date-picker__title {
@@ -42,16 +46,18 @@
.utk-date-picker__close-btn {
width: var(--size-icon-btn);
height: var(--size-icon-btn);
font-size: var(--font-size-body2);
background-color: transparent;
border-width: 0;
color: var(--color-text-secondary);
font-size: var(--font-size-body2);
border-radius: var(--radius-full);
margin: 0;
padding: 10px;
}
.utk-date-picker__close-btn:hover {
color: var(--color-text-primary);
background-color: rgba(255, 255, 255, 0.1);
border-radius: var(--radius-m);
}
/* ===================================
@@ -239,8 +245,10 @@
=================================== */
.utk-date-picker__range-info {
margin-top: var(--space-m);
padding: var(--space-s) var(--space-m);
margin-top: var(--space-l);
padding-top: var(--space-l);
border-top-width: var(--border-width);
border-top-color: var(--color-border);
background-color: var(--color-bg-secondary);
border-radius: var(--radius-s);
font-size: var(--font-size-body2);
@@ -255,7 +263,7 @@
.utk-date-picker__time-row {
flex-direction: row;
align-items: center;
justify-content: flex-start;
justify-content: center;
margin-top: var(--space-l);
padding-top: var(--space-l);
border-top-width: var(--border-width);

View File

@@ -661,3 +661,9 @@ SetupDraggerEvents() 메서드처럼 코드로 MouseEnterEvent/MouseLeaveEvent
}
/* USS 파일 */
.material-icon {
-unity-font-definition: resource('Fonts/Icons/MaterialSymbolsOutlined');
font-size: 24px;
-unity-text-align: middle-center;
}

View File

@@ -124,6 +124,61 @@ namespace UVC.Sample.UIToolkit
openPickerAsyncBtn.style.marginBottom = 10;
container.Add(openPickerAsyncBtn);
// Alpha 프리셋 버튼들
var alphaPresetLabel = new Label("Alpha Presets:");
alphaPresetLabel.style.color = Color.white;
alphaPresetLabel.style.marginTop = 10;
alphaPresetLabel.style.marginBottom = 5;
container.Add(alphaPresetLabel);
var alphaPresetRow = new VisualElement();
alphaPresetRow.style.flexDirection = FlexDirection.Row;
alphaPresetRow.style.marginBottom = 10;
// Alpha 값이 다른 프리셋 버튼들
float[] alphaValues = { 1.0f, 0.75f, 0.5f, 0.25f };
foreach (var alpha in alphaValues)
{
var alphaBtn = new Button(() => SetColorWithAlpha(alpha));
alphaBtn.style.width = 50;
alphaBtn.style.height = 28;
alphaBtn.style.marginRight = 5;
alphaBtn.style.backgroundColor = new Color(0.3f, 0.3f, 0.3f);
alphaBtn.style.borderTopLeftRadius = 4;
alphaBtn.style.borderTopRightRadius = 4;
alphaBtn.style.borderBottomLeftRadius = 4;
alphaBtn.style.borderBottomRightRadius = 4;
alphaBtn.text = $"{(int)(alpha * 100)}%";
alphaBtn.style.fontSize = 11;
alphaPresetRow.Add(alphaBtn);
}
container.Add(alphaPresetRow);
// Alpha 활성화/비활성화 직접 호출 버튼들
var alphaControlLabel = new Label("Alpha Control Examples:");
alphaControlLabel.style.color = Color.white;
alphaControlLabel.style.marginTop = 5;
alphaControlLabel.style.marginBottom = 5;
container.Add(alphaControlLabel);
var alphaControlRow = new VisualElement();
alphaControlRow.style.flexDirection = FlexDirection.Row;
alphaControlRow.style.marginBottom = 10;
var withAlphaBtn = new Button(OpenColorPickerWithAlpha) { text = "With Alpha" };
withAlphaBtn.style.height = 28;
withAlphaBtn.style.flexGrow = 1;
withAlphaBtn.style.marginRight = 5;
alphaControlRow.Add(withAlphaBtn);
var withoutAlphaBtn = new Button(OpenColorPickerWithoutAlpha) { text = "Without Alpha" };
withoutAlphaBtn.style.height = 28;
withoutAlphaBtn.style.flexGrow = 1;
alphaControlRow.Add(withoutAlphaBtn);
container.Add(alphaControlRow);
// 프리셋 색상 버튼들
var presetLabel = new Label("Preset Colors:");
presetLabel.style.color = Color.white;
@@ -169,6 +224,58 @@ namespace UVC.Sample.UIToolkit
_currentPicker.OnColorSelected += OnColorSelected;
}
/// <summary>
/// Alpha 채널 활성화 상태로 컬러 피커 열기
/// </summary>
private void OpenColorPickerWithAlpha()
{
if (_root == null || _currentPicker != null) return;
// useAlpha = true로 명시적 호출
_currentPicker = UTKColorPicker.Show(_root, _currentColor, "Select Color (Alpha ON)", useAlpha: true);
_currentPicker.OnColorChanged += OnColorChanged;
_currentPicker.OnColorSelected += OnColorSelected;
Debug.Log("[Sample] ColorPicker opened with Alpha channel enabled");
}
/// <summary>
/// Alpha 채널 비활성화 상태로 컬러 피커 열기
/// </summary>
private void OpenColorPickerWithoutAlpha()
{
if (_root == null || _currentPicker != null) return;
// useAlpha = false로 명시적 호출
_currentPicker = UTKColorPicker.Show(_root, _currentColor, "Select Color (Alpha OFF)", useAlpha: false);
_currentPicker.OnColorChanged += OnColorChanged;
_currentPicker.OnColorSelected += OnColorSelected;
Debug.Log("[Sample] ColorPicker opened without Alpha channel");
}
/// <summary>
/// 현재 색상의 Alpha 값을 변경
/// </summary>
private void SetColorWithAlpha(float alpha)
{
_currentColor = new Color(_currentColor.r, _currentColor.g, _currentColor.b, alpha);
if (_colorPreview != null)
{
_colorPreview.style.backgroundColor = _currentColor;
}
if (_colorLabel != null)
{
_colorLabel.text = ColorToHex(_currentColor);
}
Debug.Log($"[Sample] Alpha set to {(int)(alpha * 100)}% - Color: {ColorToHex(_currentColor)}");
}
private async UniTaskVoid OpenColorPickerAsync()
{
if (_root == null) return;

View File

@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
@@ -48,6 +49,7 @@ public class UTKStyleGuideSample : MonoBehaviour
// 카테고리별 컨트롤 정의
private static readonly Dictionary<string, string[]> ControlCategories = new()
{
["Icon"] = new[] { "MaterialSymbolsOutlined", "UTKImageIcons" },
["Button"] = new[] { "UTKButton", "UTKCheckBox", "UTKToggle", "UTKRadioButton", "UTKToggleButtonGroup" },
["Input"] = new[] { "UTKInputField", "UTKIntegerField", "UTKLongField", "UTKFloatField", "UTKDoubleField", "UTKVector2Field", "UTKVector3Field", "UTKVector4Field", "UTKRectField", "UTKBoundsField", "UTKNumberStepper" },
["Slider"] = new[] { "UTKSlider", "UTKMinMaxSlider", "UTKProgressBar" },
@@ -57,7 +59,7 @@ public class UTKStyleGuideSample : MonoBehaviour
["Card"] = new[] { "UTKCard", "UTKPanel" },
["Tab"] = new[] { "UTKTabView" },
["Modal"] = new[] { "UTKAlert", "UTKToast", "UTKTooltip" },
["Picker"] = new[] { "UTKColorPicker", "UTKDatePicker" }
["Picker"] = new[] { "UTKColorPicker", "UTKDatePicker" },
};
// UI 요소들
@@ -73,6 +75,8 @@ public class UTKStyleGuideSample : MonoBehaviour
uiDocument ??= GetComponent<UIDocument>();
_root = uiDocument.rootVisualElement;
UTKToast.SetRoot(_root);
// 테마 매니저에 루트 등록
UTKThemeManager.Instance.RegisterRoot(_root);
@@ -344,6 +348,12 @@ public class UTKStyleGuideSample : MonoBehaviour
case "UTKDatePicker":
CreateDatePickerPreview(container);
break;
case "MaterialSymbolsOutlined":
CreateIconPreview(container);
break;
case "UTKImageIcons":
CreateImageIconPreview(container);
break;
default:
container.Add(new Label($"Preview for {controlName} not implemented"));
break;
@@ -370,15 +380,33 @@ public class UTKStyleGuideSample : MonoBehaviour
// Icon Only
var row3 = CreateRow(container, "Icon Only");
row3.Add(new UTKButton("", "✚", UTKButton.ButtonVariant.Primary) { IconOnly = true });
row3.Add(new UTKButton("", "✎", UTKButton.ButtonVariant.Normal) { IconOnly = true });
row3.Add(new UTKButton("", "✖", UTKButton.ButtonVariant.Danger) { IconOnly = true });
row3.Add(new UTKButton("", "⚙", UTKButton.ButtonVariant.OutlinePrimary) { IconOnly = true });
row3.Add(new UTKButton("", UTKMaterialIcons.PlusOne, UTKButton.ButtonVariant.Primary) { IconOnly = true });
row3.Add(new UTKButton("", UTKMaterialIcons.Edit, UTKButton.ButtonVariant.Normal) { IconOnly = true });
row3.Add(new UTKButton("", UTKMaterialIcons.Close, UTKButton.ButtonVariant.Danger) { IconOnly = true });
row3.Add(new UTKButton("", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.OutlinePrimary) { IconOnly = true });
row3.Add(new UTKButton("", UTKMaterialIcons.Cancel, UTKButton.ButtonVariant.Normal) { IconOnly = true });
// Ghost
var row4 = CreateRow(container, "Ghost");
row4.Add(new UTKButton("Ghost", "", UTKButton.ButtonVariant.Ghost));
row4.Add(new UTKButton("", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Ghost) { IconOnly = true });
// Text (배경/외곽선 없이 텍스트/아이콘만)
var row5 = CreateRow(container, "Text (Label/Icon Only)");
row5.Add(new UTKButton("Text Only", "", UTKButton.ButtonVariant.Text));
row5.Add(new UTKButton("With Icon", UTKMaterialIcons.FlashOn, UTKButton.ButtonVariant.Text));
row5.Add(new UTKButton("Link Style", "", UTKButton.ButtonVariant.Text));
// Text Icon Only (원형 아이콘 버튼 - UTKColorPicker 닫기 버튼 스타일)
var row6 = CreateRow(container, "Text Icon Only (Circle)");
row6.Add(new UTKButton("", UTKMaterialIcons.Close, UTKButton.ButtonVariant.Text, 12) { IconOnly = true });
row6.Add(new UTKButton("", UTKMaterialIcons.Check, UTKButton.ButtonVariant.Text, 12) { IconOnly = true });
row6.Add(new UTKButton("", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Text, 12) { IconOnly = true });
row6.Add(new UTKButton("", UTKMaterialIcons.Edit, UTKButton.ButtonVariant.Text, 12) { IconOnly = true });
row6.Add(new UTKButton("", UTKMaterialIcons.Search, UTKButton.ButtonVariant.Text, 12) { IconOnly = true });
// Disabled
var row4 = CreateRow(container, "Disabled");
row4.Add(new UTKButton("Disabled", "", UTKButton.ButtonVariant.Primary) { IsEnabled = false });
row4.Add(new UTKButton("Disabled", "", UTKButton.ButtonVariant.Normal) { IsEnabled = false });
var row7 = CreateRow(container, "Disabled");
row7.Add(new UTKButton("Disabled", "", UTKButton.ButtonVariant.Primary) { IsEnabled = false });
row7.Add(new UTKButton("Disabled", "", UTKButton.ButtonVariant.Normal) { IsEnabled = false });
row7.Add(new UTKButton("Disabled", "", UTKButton.ButtonVariant.Text) { IsEnabled = false });
}
private void CreateCheckBoxPreview(VisualElement container)
@@ -718,14 +746,68 @@ public class UTKStyleGuideSample : MonoBehaviour
private void CreateLabelPreview(VisualElement container)
{
AddDescription(container, "다양한 크기 라벨 컴포넌트");
AddDescription(container, "다양한 크기와 아이콘을 지원하는 라벨 컴포넌트");
container.Add(new UTKLabel("H1 Heading", UTKLabel.LabelSize.H1));
container.Add(new UTKLabel("H2 Heading", UTKLabel.LabelSize.H2));
container.Add(new UTKLabel("H3 Heading", UTKLabel.LabelSize.H3));
container.Add(new UTKLabel("Body1 Text", UTKLabel.LabelSize.Body1));
container.Add(new UTKLabel("Body2 Text", UTKLabel.LabelSize.Body2));
container.Add(new UTKLabel("Caption Text", UTKLabel.LabelSize.Caption));
// 텍스트 크기
var row1 = CreateRow(container, "Text Sizes");
row1.style.flexDirection = FlexDirection.Column;
row1.style.alignItems = Align.FlexStart;
row1.Add(new UTKLabel("H1 Heading", UTKLabel.LabelSize.H1));
row1.Add(new UTKLabel("H2 Heading", UTKLabel.LabelSize.H2));
row1.Add(new UTKLabel("H3 Heading", UTKLabel.LabelSize.H3));
row1.Add(new UTKLabel("Body1 Text", UTKLabel.LabelSize.Body1));
row1.Add(new UTKLabel("Body2 Text", UTKLabel.LabelSize.Body2));
row1.Add(new UTKLabel("Caption Text", UTKLabel.LabelSize.Caption));
// Material Icon + 텍스트
var row2 = CreateRow(container, "Material Icon + Text");
row2.Add(new UTKLabel("Settings", UTKMaterialIcons.Settings));
row2.Add(new UTKLabel("Home", UTKMaterialIcons.Home));
row2.Add(new UTKLabel("Search", UTKMaterialIcons.Search));
row2.Add(new UTKLabel("Edit", UTKMaterialIcons.Edit));
// Material Icon 오른쪽 배치
var row3 = CreateRow(container, "Icon Right");
row3.Add(new UTKLabel("Next", UTKMaterialIcons.ArrowForward, UTKLabel.IconPosition.Right));
row3.Add(new UTKLabel("Download", UTKMaterialIcons.Download, UTKLabel.IconPosition.Right));
row3.Add(new UTKLabel("External", UTKMaterialIcons.OpenInNew, UTKLabel.IconPosition.Right));
// Image Icon + 텍스트
var row4 = CreateRow(container, "Image Icon + Text");
row4.Add(new UTKLabel("Close", UTKImageIcons.BtnClose16, isImageIcon: true));
row4.Add(new UTKLabel("Settings", UTKImageIcons.IconSetting22, isImageIcon: true));
// Material Icon만
var row5 = CreateRow(container, "Material Icon Only");
row5.Add(new UTKLabel(UTKMaterialIcons.Home, 24));
row5.Add(new UTKLabel(UTKMaterialIcons.Settings, 24));
row5.Add(new UTKLabel(UTKMaterialIcons.Search, 24));
row5.Add(new UTKLabel(UTKMaterialIcons.Edit, 24));
row5.Add(new UTKLabel(UTKMaterialIcons.Delete, 24));
// Image Icon만
var row6 = CreateRow(container, "Image Icon Only");
row6.Add(new UTKLabel(UTKImageIcons.BtnClose22, isImageIcon: true, iconSize: 22));
row6.Add(new UTKLabel(UTKImageIcons.IconSetting22, isImageIcon: true, iconSize: 22));
// 메서드로 아이콘 설정
var row7 = CreateRow(container, "Set Icon via Method");
var label1 = new UTKLabel("Dynamic Icon", UTKLabel.LabelSize.Body1);
label1.SetMaterialIcon(UTKMaterialIcons.Star);
row7.Add(label1);
var label2 = new UTKLabel("Image Icon", UTKLabel.LabelSize.Body1);
label2.SetImageIconByName("icon_setting_22");
row7.Add(label2);
// 텍스트 변형
var row8 = CreateRow(container, "Variants");
row8.Add(new UTKLabel("Primary", UTKLabel.LabelSize.Body1) { Variant = UTKLabel.LabelVariant.Primary });
row8.Add(new UTKLabel("Secondary", UTKLabel.LabelSize.Body1) { Variant = UTKLabel.LabelVariant.Secondary });
row8.Add(new UTKLabel("Success", UTKLabel.LabelSize.Body1) { Variant = UTKLabel.LabelVariant.Success });
row8.Add(new UTKLabel("Warning", UTKLabel.LabelSize.Body1) { Variant = UTKLabel.LabelVariant.Warning });
row8.Add(new UTKLabel("Error", UTKLabel.LabelSize.Body1) { Variant = UTKLabel.LabelVariant.Error });
row8.Add(new UTKLabel("Disabled", UTKLabel.LabelSize.Body1) { Variant = UTKLabel.LabelVariant.Disabled });
}
private void CreateHelpBoxPreview(VisualElement container)
@@ -1172,8 +1254,6 @@ public class UTKStyleGuideSample : MonoBehaviour
var row = CreateRow(container, "Toasts");
UTKToast.SetRoot(_root);
var infoBtn = new UTKButton("Info Toast", "", UTKButton.ButtonVariant.Primary);
infoBtn.OnClicked += () => UTKToast.Show("This is an info toast!This is an info toast!This is an info toast!This is an info toast!This is an info toast!");
row.Add(infoBtn);
@@ -1315,6 +1395,8 @@ public class UTKStyleGuideSample : MonoBehaviour
AddDescription(container, "날짜 선택 컴포넌트 (버튼 클릭으로 모달 표시)");
UTKDatePicker.SetDayNames(new[] { "일", "월", "화", "수", "목", "금", "토" });
// 현재 선택된 날짜 표시
var previewRow = CreateRow(container, "Current Date");
_dateLabel = new Label($"Selected: {_selectedDate:yyyy-MM-dd}");
@@ -1435,6 +1517,472 @@ public class UTKStyleGuideSample : MonoBehaviour
#endregion
#region Icon Previews
private List<string>? _iconNameList;
private List<string>? _filteredIconNameList;
private ListView? _iconListView;
private Label? _iconCountLabel;
// 그리드 레이아웃 상수
private const int IconItemWidth = 80;
private const int IconItemHeight = 80;
private const int IconItemMargin = 4;
private const int IconsPerRow = 10; // 한 행에 표시할 아이콘 수
private void CreateIconPreview(VisualElement container)
{
AddDescription(container, $"Material Symbols Outlined 아이콘 폰트 (UTKMaterialIcons 사용, ListView 가상화 적용)");
// UTKMaterialIcons에서 폰트 로드
var font = UTKMaterialIcons.LoadFont();
if (font == null)
{
container.Add(new Label("Error: UTKMaterialIcons 폰트를 로드할 수 없습니다."));
return;
}
// UTKMaterialIcons에서 모든 아이콘 이름 가져오기
_iconNameList = UTKMaterialIcons.GetAllIconNames().ToList();
_filteredIconNameList = _iconNameList;
// 아이콘 개수 표시
_iconCountLabel = new Label($"총 {UTKMaterialIcons.Count}개의 아이콘 (가상화 적용)");
_iconCountLabel.style.marginBottom = 10;
_iconCountLabel.style.color = new Color(0.6f, 0.6f, 0.6f);
container.Add(_iconCountLabel);
// 검색 필드
var searchRow = CreateRow(container, "Search");
var searchField = new UTKInputField("", "Search icons...");
searchField.style.width = 300;
searchRow.Add(searchField);
// 행 데이터 생성 (아이콘 이름을 행 단위로 그룹화)
var rowData = CreateIconRowData(_filteredIconNameList);
// ListView 생성 (가상화 적용)
_iconListView = new ListView();
_iconListView.style.flexGrow = 1;
_iconListView.style.maxHeight = 500;
_iconListView.fixedItemHeight = IconItemHeight + IconItemMargin;
_iconListView.itemsSource = rowData;
_iconListView.makeItem = MakeIconRow;
_iconListView.bindItem = BindIconRow;
_iconListView.selectionType = SelectionType.None;
_iconListView.virtualizationMethod = CollectionVirtualizationMethod.FixedHeight;
_iconListView.AddToClassList("utk-icon-listview");
container.Add(_iconListView);
// 검색 기능 (Enter 키 또는 포커스 해제 시 검색)
void PerformSearch(string searchValue)
{
if (_iconNameList == null || _iconListView == null || _iconCountLabel == null) return;
_filteredIconNameList = string.IsNullOrEmpty(searchValue)
? _iconNameList
: _iconNameList.FindAll(name => name.Contains(searchValue, StringComparison.OrdinalIgnoreCase));
var newRowData = CreateIconRowData(_filteredIconNameList);
_iconListView.itemsSource = newRowData;
_iconListView.Rebuild();
// 필터링 결과 개수 업데이트
_iconCountLabel.text = string.IsNullOrEmpty(searchValue)
? $"총 {UTKMaterialIcons.Count}개의 아이콘 (가상화 적용)"
: $"{_filteredIconNameList.Count}개 / {UTKMaterialIcons.Count}개 아이콘 (가상화 적용)";
}
// Enter 키로 검색
searchField.OnSubmit += PerformSearch;
// 포커스 해제 시 검색
searchField.OnBlurred += () => PerformSearch(searchField.Value);
}
/// <summary>
/// 아이콘 이름 목록을 행 단위로 그룹화합니다.
/// </summary>
private List<List<string>> CreateIconRowData(List<string> iconNames)
{
var rows = new List<List<string>>();
for (int i = 0; i < iconNames.Count; i += IconsPerRow)
{
var row = new List<string>();
for (int j = 0; j < IconsPerRow && i + j < iconNames.Count; j++)
{
row.Add(iconNames[i + j]);
}
rows.Add(row);
}
return rows;
}
/// <summary>
/// ListView 행 아이템을 생성합니다.
/// </summary>
private VisualElement MakeIconRow()
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.height = IconItemHeight;
// 행에 아이콘 슬롯 미리 생성
for (int i = 0; i < IconsPerRow; i++)
{
var iconSlot = CreateIconSlot();
row.Add(iconSlot);
}
return row;
}
/// <summary>
/// 아이콘 슬롯 (재사용 가능한 컨테이너)을 생성합니다.
/// </summary>
private VisualElement CreateIconSlot()
{
var item = new VisualElement();
item.name = "icon-slot";
item.style.width = IconItemWidth;
item.style.height = IconItemHeight;
item.style.marginRight = IconItemMargin;
item.style.alignItems = Align.Center;
item.style.justifyContent = Justify.Center;
item.style.backgroundColor = new Color(0.15f, 0.15f, 0.15f, 0.5f);
item.style.borderTopLeftRadius = 4;
item.style.borderTopRightRadius = 4;
item.style.borderBottomLeftRadius = 4;
item.style.borderBottomRightRadius = 4;
// 호버 효과
item.RegisterCallback<MouseEnterEvent>(_ =>
{
if (item.style.display != DisplayStyle.None)
item.style.backgroundColor = new Color(0.25f, 0.25f, 0.25f, 0.8f);
});
item.RegisterCallback<MouseLeaveEvent>(_ =>
{
if (item.style.display != DisplayStyle.None)
item.style.backgroundColor = new Color(0.15f, 0.15f, 0.15f, 0.5f);
});
// 아이콘 라벨 (UTKMaterialIcons.ApplyIconStyle 사용)
var iconLabel = new Label();
iconLabel.name = "icon-label";
UTKMaterialIcons.ApplyIconStyle(iconLabel, 28);
iconLabel.style.color = Color.white;
iconLabel.style.marginBottom = 4;
item.Add(iconLabel);
// 아이콘 이름
var nameLabel = new Label();
nameLabel.name = "name-label";
nameLabel.style.fontSize = 8;
nameLabel.style.color = new Color(0.7f, 0.7f, 0.7f);
nameLabel.style.whiteSpace = WhiteSpace.NoWrap;
nameLabel.style.overflow = Overflow.Hidden;
nameLabel.style.textOverflow = TextOverflow.Ellipsis;
nameLabel.style.maxWidth = IconItemWidth - 4;
nameLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
item.Add(nameLabel);
return item;
}
/// <summary>
/// ListView 행에 데이터를 바인딩합니다.
/// </summary>
private void BindIconRow(VisualElement row, int index)
{
if (_iconListView?.itemsSource is not List<List<string>> rowData) return;
if (index < 0 || index >= rowData.Count) return;
var iconNames = rowData[index];
var slots = row.Query<VisualElement>("icon-slot").ToList();
for (int i = 0; i < slots.Count; i++)
{
var slot = slots[i];
if (i < iconNames.Count)
{
var iconName = iconNames[i];
var iconChar = UTKMaterialIcons.GetIcon(iconName);
slot.style.display = DisplayStyle.Flex;
// 아이콘 라벨 업데이트
var iconLabel = slot.Q<Label>("icon-label");
if (iconLabel != null)
iconLabel.text = iconChar;
// 이름 라벨 업데이트
var nameLabel = slot.Q<Label>("name-label");
if (nameLabel != null)
nameLabel.text = iconName;
// 툴팁 갱신
slot.tooltip = iconName;
// 클릭 이벤트 재등록 (userData로 아이콘 이름 저장)
slot.userData = iconName;
slot.UnregisterCallback<ClickEvent>(OnIconSlotClicked);
slot.RegisterCallback<ClickEvent>(OnIconSlotClicked);
}
else
{
// 빈 슬롯 숨기기
slot.style.display = DisplayStyle.None;
}
}
}
/// <summary>
/// 아이콘 슬롯 클릭 이벤트 핸들러
/// </summary>
private void OnIconSlotClicked(ClickEvent evt)
{
if (evt.currentTarget is VisualElement slot && slot.userData is string iconName)
{
var iconChar = UTKMaterialIcons.GetIcon(iconName);
GUIUtility.systemCopyBuffer = iconChar;
UTKToast.Show($"'{iconName}' 아이콘이 클립보드에 복사되었습니다.");
}
}
#endregion
#region Image Icon Previews
private List<string>? _imageIconNameList;
private List<string>? _filteredImageIconNameList;
private ListView? _imageIconListView;
private Label? _imageIconCountLabel;
// 이미지 아이콘 그리드 레이아웃 상수
private const int ImageIconItemWidth = 100;
private const int ImageIconItemHeight = 100;
private const int ImageIconItemMargin = 8;
private const int ImageIconsPerRow = 8;
private void CreateImageIconPreview(VisualElement container)
{
AddDescription(container, $"이미지 기반 아이콘 (UTKImageIcons 사용, ListView 가상화 적용)");
// UTKImageIcons에서 모든 아이콘 이름 가져오기
_imageIconNameList = UTKImageIcons.GetAllIconNames().ToList();
_filteredImageIconNameList = _imageIconNameList;
// 아이콘 개수 표시
_imageIconCountLabel = new Label($"총 {UTKImageIcons.Count}개의 이미지 아이콘 (가상화 적용)");
_imageIconCountLabel.style.marginBottom = 10;
_imageIconCountLabel.style.color = new Color(0.6f, 0.6f, 0.6f);
container.Add(_imageIconCountLabel);
// 검색 필드
var searchRow = CreateRow(container, "Search");
var searchField = new UTKInputField("", "Search image icons...");
searchField.style.width = 300;
searchRow.Add(searchField);
// 행 데이터 생성 (아이콘 이름을 행 단위로 그룹화)
var rowData = CreateImageIconRowData(_filteredImageIconNameList);
// ListView 생성 (가상화 적용)
_imageIconListView = new ListView();
_imageIconListView.style.flexGrow = 1;
_imageIconListView.style.maxHeight = 500;
_imageIconListView.fixedItemHeight = ImageIconItemHeight + ImageIconItemMargin;
_imageIconListView.itemsSource = rowData;
_imageIconListView.makeItem = MakeImageIconRow;
_imageIconListView.bindItem = BindImageIconRow;
_imageIconListView.selectionType = SelectionType.None;
_imageIconListView.virtualizationMethod = CollectionVirtualizationMethod.FixedHeight;
_imageIconListView.AddToClassList("utk-image-icon-listview");
container.Add(_imageIconListView);
// 검색 기능
void PerformSearch(string searchValue)
{
if (_imageIconNameList == null || _imageIconListView == null || _imageIconCountLabel == null) return;
_filteredImageIconNameList = string.IsNullOrEmpty(searchValue)
? _imageIconNameList
: _imageIconNameList.FindAll(name => name.Contains(searchValue, StringComparison.OrdinalIgnoreCase));
var newRowData = CreateImageIconRowData(_filteredImageIconNameList);
_imageIconListView.itemsSource = newRowData;
_imageIconListView.Rebuild();
_imageIconCountLabel.text = string.IsNullOrEmpty(searchValue)
? $"총 {UTKImageIcons.Count}개의 이미지 아이콘 (가상화 적용)"
: $"{_filteredImageIconNameList.Count}개 / {UTKImageIcons.Count}개 이미지 아이콘 (가상화 적용)";
}
searchField.OnSubmit += PerformSearch;
searchField.OnBlurred += () => PerformSearch(searchField.Value);
}
/// <summary>
/// 이미지 아이콘 이름 목록을 행 단위로 그룹화합니다.
/// </summary>
private List<List<string>> CreateImageIconRowData(List<string> iconNames)
{
var rows = new List<List<string>>();
for (int i = 0; i < iconNames.Count; i += ImageIconsPerRow)
{
var row = new List<string>();
for (int j = 0; j < ImageIconsPerRow && i + j < iconNames.Count; j++)
{
row.Add(iconNames[i + j]);
}
rows.Add(row);
}
return rows;
}
/// <summary>
/// 이미지 아이콘 ListView 행 아이템을 생성합니다.
/// </summary>
private VisualElement MakeImageIconRow()
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.height = ImageIconItemHeight;
for (int i = 0; i < ImageIconsPerRow; i++)
{
var iconSlot = CreateImageIconSlot();
row.Add(iconSlot);
}
return row;
}
/// <summary>
/// 이미지 아이콘 슬롯을 생성합니다.
/// </summary>
private VisualElement CreateImageIconSlot()
{
var item = new VisualElement();
item.name = "image-icon-slot";
item.style.width = ImageIconItemWidth;
item.style.height = ImageIconItemHeight;
item.style.marginRight = ImageIconItemMargin;
item.style.alignItems = Align.Center;
item.style.justifyContent = Justify.Center;
item.style.backgroundColor = new Color(0.15f, 0.15f, 0.15f, 0.5f);
item.style.borderTopLeftRadius = 4;
item.style.borderTopRightRadius = 4;
item.style.borderBottomLeftRadius = 4;
item.style.borderBottomRightRadius = 4;
// 호버 효과
item.RegisterCallback<MouseEnterEvent>(_ =>
{
if (item.style.display != DisplayStyle.None)
item.style.backgroundColor = new Color(0.25f, 0.25f, 0.25f, 0.8f);
});
item.RegisterCallback<MouseLeaveEvent>(_ =>
{
if (item.style.display != DisplayStyle.None)
item.style.backgroundColor = new Color(0.15f, 0.15f, 0.15f, 0.5f);
});
// 이미지 아이콘
var iconImage = new VisualElement();
iconImage.name = "icon-image";
iconImage.style.width = 32;
iconImage.style.height = 32;
iconImage.style.marginBottom = 4;
item.Add(iconImage);
// 아이콘 이름
var nameLabel = new Label();
nameLabel.name = "name-label";
nameLabel.style.fontSize = 8;
nameLabel.style.color = new Color(0.7f, 0.7f, 0.7f);
nameLabel.style.whiteSpace = WhiteSpace.NoWrap;
nameLabel.style.overflow = Overflow.Hidden;
nameLabel.style.textOverflow = TextOverflow.Ellipsis;
nameLabel.style.maxWidth = ImageIconItemWidth - 4;
nameLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
item.Add(nameLabel);
return item;
}
/// <summary>
/// 이미지 아이콘 ListView 행에 데이터를 바인딩합니다.
/// </summary>
private void BindImageIconRow(VisualElement row, int index)
{
if (_imageIconListView?.itemsSource is not List<List<string>> rowData) return;
if (index < 0 || index >= rowData.Count) return;
var iconNames = rowData[index];
var slots = row.Query<VisualElement>("image-icon-slot").ToList();
for (int i = 0; i < slots.Count; i++)
{
var slot = slots[i];
if (i < iconNames.Count)
{
var iconName = iconNames[i];
var iconPath = UTKImageIcons.GetPath(iconName);
slot.style.display = DisplayStyle.Flex;
// 이미지 아이콘 로드 및 표시
var iconImage = slot.Q<VisualElement>("icon-image");
if (iconImage != null)
{
var texture = UTKImageIcons.LoadTextureByName(iconName);
if (texture != null)
{
iconImage.style.backgroundImage = new StyleBackground(texture);
}
else
{
iconImage.style.backgroundImage = StyleKeyword.None;
}
}
// 이름 라벨 업데이트
var nameLabel = slot.Q<Label>("name-label");
if (nameLabel != null)
nameLabel.text = iconName;
// 툴팁 갱신
slot.tooltip = $"{iconName}\n{iconPath}";
// 클릭 이벤트 재등록
slot.userData = iconName;
slot.UnregisterCallback<ClickEvent>(OnImageIconSlotClicked);
slot.RegisterCallback<ClickEvent>(OnImageIconSlotClicked);
}
else
{
slot.style.display = DisplayStyle.None;
}
}
}
/// <summary>
/// 이미지 아이콘 슬롯 클릭 이벤트 핸들러
/// </summary>
private void OnImageIconSlotClicked(ClickEvent evt)
{
if (evt.currentTarget is VisualElement slot && slot.userData is string iconName)
{
var iconCode = $"UTKImageIcons.LoadSpriteByName(\"{iconName}\")";
GUIUtility.systemCopyBuffer = iconCode;
UTKToast.Show($"'{iconName}' 코드가 클립보드에 복사되었습니다.\n{iconCode}");
}
}
#endregion
#region Helper Methods
private void AddDescription(VisualElement container, string text)

View File

@@ -1,5 +1,7 @@
#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
@@ -10,6 +12,104 @@ namespace UVC.UIToolkit
/// 텍스트와 아이콘을 동시에 표시하거나, 아이콘만 표시할 수 있습니다.
/// 배경 색상, 외곽선 굵기 등을 설정할 수 있습니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 버튼 생성
/// var btn = new UTKButton("확인");
/// btn.OnClicked += () => Debug.Log("클릭됨!");
///
/// // 텍스트와 Material Icon이 있는 버튼
/// var saveBtn = new UTKButton("저장", UTKMaterialIcons.Save, UTKButton.ButtonVariant.Primary);
///
/// // 텍스트와 아이콘 (크기 지정)
/// var largeIconBtn = new UTKButton("설정", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Primary, 28);
/// var smallImageBtn = new UTKButton("닫기", UTKImageIcons.BtnClose22, UTKButton.ButtonVariant.Danger, 20);
///
/// // Material Icon 설정
/// // 텍스트와 아이콘 (크기 지정)
/// var largeIconBtn = new UTKButton("설정", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Primary, 28);
/// var smallImageBtn = new UTKButton("닫기", UTKImageIcons.BtnClose22, UTKButton.ButtonVariant.Danger, 20);
///
/// // Text 버튼 스타일에서 아이콘 크기 지정
/// var textSmallIcon = new UTKButton("Small", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Text, 16);
/// var textMediumIcon = new UTKButton("Medium", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Text, 24);
/// var textLargeIcon = new UTKButton("Large", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Text, 32);
///
/// // Material Icon 설정
/// var imgBtn = new UTKButton("닫기");
/// imgBtn.SetImageIcon(UTKImageIcons.BtnClose22);
/// imgBtn.SetImageIconByName("icon_setting_22");
///
/// // 비동기 아이콘 설정
/// await btn.SetMaterialIconAsync(UTKMaterialIcons.Search);
/// await btn.SetImageIconAsync(UTKImageIcons.IconSetting22);
///
/// // 아이콘만 표시하는 버튼
/// var iconOnlyBtn = new UTKButton { IconOnly = true };
/// iconOnlyBtn.SetMaterialIcon(UTKMaterialIcons.Close);
///
/// // 버튼 스타일 변형
/// btn.Variant = UTKButton.ButtonVariant.Primary;
/// btn.Variant = UTKButton.ButtonVariant.Danger;
/// btn.Variant = UTKButton.ButtonVariant.Ghost;
///
/// // 버튼 크기
/// btn.Size = UTKButton.ButtonSize.Small;
/// btn.Size = UTKButton.ButtonSize.Large;
///
/// // 주의: 생성자에서 icon 파라미터로 아이콘을 전달하면 다음 순서로 타입이 감지됩니다.
/// // 1. UTKMaterialIcons에서 먼저 검사 (Material Symbols Outlined 아이콘)
/// // 2. UTKImageIcons에서 검사 (이미지 기반 아이콘)
/// // 3. 둘 다 아니면 텍스트로 처리됨
///
/// // 아이콘 제거
/// btn.ClearIcon();
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <!-- 네임스페이스 선언 -->
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
///
/// <!-- 기본 버튼 -->
/// <utk:UTKButton Text="확인" />
///
/// <!-- Material Icon 버튼 (유니코드 직접 입력) -->
/// <utk:UTKButton Text="설정" Icon="&#xe8b8;" />
///
/// <!-- 버튼 변형 -->
/// <utk:UTKButton Text="저장" Variant="Primary" />
/// <utk:UTKButton Text="삭제" Variant="Danger" />
/// <utk:UTKButton Text="취소" Variant="Ghost" />
/// <utk:UTKButton Text="링크" Variant="Text" />
///
/// <!-- 외곽선 변형 -->
/// <utk:UTKButton Text="확인" Variant="OutlinePrimary" />
/// <utk:UTKButton Text="삭제" Variant="OutlineDanger" />
///
/// <!-- 크기 변형 -->
/// <utk:UTKButton Text="작은 버튼" Size="Small" />
/// <utk:UTKButton Text="큰 버튼" Size="Large" />
///
/// <!-- 아이콘만 표시 -->
/// <utk:UTKButton Icon="&#xe5cd;" IconOnly="true" />
/// <utk:UTKButton Text="" Icon="Close" Variant="Text" IconSize="12" />
///
/// <!-- 비활성화 -->
/// <utk:UTKButton Text="비활성화" IsEnabled="false" />
///
/// <!-- 외곽선 굵기 -->
/// <utk:UTKButton Text="두꺼운 외곽선" BorderWidth="2" />
///
/// </ui:UXML>
/// </code>
/// <para><b>UXML에서 로드 후 C#에서 아이콘 설정:</b></para>
/// <code>
/// var root = GetComponent<UIDocument>().rootVisualElement;
/// var btn = root.Q<UTKButton>("my-button");
/// btn.SetMaterialIcon(UTKMaterialIcons.Settings);
/// </code>
/// </example>
[UxmlElement]
public partial class UTKButton : VisualElement, IDisposable
{
@@ -21,6 +121,7 @@ namespace UVC.UIToolkit
private bool _disposed;
private Label? _iconLabel;
private Label? _textLabel;
private VisualElement? _imageIcon;
private string _text = "";
private string _icon = "";
@@ -145,7 +246,9 @@ namespace UVC.UIToolkit
Danger,
OutlineNormal,
OutlinePrimary,
OutlineDanger
OutlineDanger,
/// <summary>배경과 외곽선이 투명하고 텍스트/아이콘만 표시</summary>
Text
}
public enum ButtonSize
@@ -170,15 +273,52 @@ namespace UVC.UIToolkit
CreateUI();
SetupEvents();
SubscribeToThemeChanges();
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
RegisterCallback<AttachToPanelEvent>(_ =>
{
UpdateContent();
UpdateVariant();
UpdateSize();
});
}
public UTKButton(string text, string icon = "", ButtonVariant variant = ButtonVariant.Normal) : this()
/// <summary>
/// 텍스트, 아이콘, 변형, 크기를 설정하여 버튼을 생성합니다.
/// </summary>
/// <param name="text">버튼에 표시할 텍스트</param>
/// <param name="icon">아이콘 이름 또는 경로 (Material Icon 또는 Image Icon)</param>
/// <param name="variant">버튼 스타일 변형</param>
/// <param name="iconSize">아이콘 크기 (생략 시 기본값 사용)</param>
/// <remarks>
/// 아이콘 타입 감지 순서:
/// 1. UTKMaterialIcons 검사 (Material Symbols Outlined 아이콘)
/// 2. UTKImageIcons 검사 (이미지 기반 아이콘)
/// 3. 둘 다 아니면 텍스트로 처리됨
/// </remarks>
public UTKButton(string text, string icon = "", ButtonVariant variant = ButtonVariant.Normal, int? iconSize = null) : this()
{
_text = text;
_icon = icon;
_variant = variant;
UpdateContent();
UpdateVariant();
// 아이콘 타입 자동 감지 및 적용
if (!string.IsNullOrEmpty(icon))
{
// 1순위: UTKMaterialIcons에 해당하는지 확인
if (UTKMaterialIcons.GetIcon(icon) != null)
{
SetMaterialIcon(icon, iconSize);
}
// 2순위: UTKImageIcons에 해당하는지 확인
else if (!string.IsNullOrEmpty(UTKImageIcons.GetPath(icon)))
{
SetImageIcon(icon, iconSize);
}
// 3순위: 둘 다 아니면 현재 로직 유지 (UpdateContent에서 이미 처리됨)
}
}
#endregion
@@ -187,6 +327,7 @@ namespace UVC.UIToolkit
{
AddToClassList("utk-button");
focusable = true;
pickingMode = PickingMode.Position;
_iconLabel = new Label
{
@@ -281,6 +422,7 @@ namespace UVC.UIToolkit
RemoveFromClassList("utk-button--outline-normal");
RemoveFromClassList("utk-button--outline-primary");
RemoveFromClassList("utk-button--outline-danger");
RemoveFromClassList("utk-button--text");
var variantClass = _variant switch
{
@@ -291,6 +433,7 @@ namespace UVC.UIToolkit
ButtonVariant.OutlineNormal => "utk-button--outline-normal",
ButtonVariant.OutlinePrimary => "utk-button--outline-primary",
ButtonVariant.OutlineDanger => "utk-button--outline-danger",
ButtonVariant.Text => "utk-button--text",
_ => "utk-button--normal"
};
AddToClassList(variantClass);
@@ -339,6 +482,179 @@ namespace UVC.UIToolkit
}
#endregion
#region Icon Methods
/// <summary>
/// Material Icon을 설정합니다.
/// </summary>
/// <param name="icon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings)</param>
/// <param name="fontSize">아이콘 폰트 크기 (null이면 버튼 크기에 맞춤)</param>
public void SetMaterialIcon(string icon, int? fontSize = null)
{
ClearImageIcon();
Icon = icon;
if (_iconLabel != null)
{
UTKMaterialIcons.ApplyIconStyle(_iconLabel, fontSize ?? GetDefaultIconSize());
}
}
/// <summary>
/// Material Icon을 비동기로 설정합니다.
/// </summary>
/// <param name="icon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings)</param>
/// <param name="ct">취소 토큰</param>
/// <param name="fontSize">아이콘 폰트 크기 (null이면 버튼 크기에 맞춤)</param>
public async UniTask SetMaterialIconAsync(string icon, CancellationToken ct = default, int? fontSize = null)
{
ClearImageIcon();
Icon = icon;
if (_iconLabel != null)
{
await UTKMaterialIcons.ApplyIconStyleAsync(_iconLabel, ct, fontSize ?? GetDefaultIconSize());
}
}
/// <summary>
/// 아이콘 이름으로 Material Icon을 설정합니다.
/// </summary>
/// <param name="iconName">아이콘 이름 (예: "settings", "home")</param>
/// <param name="fontSize">아이콘 폰트 크기 (null이면 버튼 크기에 맞춤)</param>
public void SetMaterialIconByName(string iconName, int? fontSize = null)
{
var iconChar = UTKMaterialIcons.GetIcon(iconName);
if (!string.IsNullOrEmpty(iconChar))
{
SetMaterialIcon(iconChar, fontSize);
}
else
{
Debug.LogWarning($"[UTKButton] Material icon '{iconName}'을(를) 찾을 수 없습니다.");
}
}
/// <summary>
/// Image Icon을 설정합니다.
/// </summary>
/// <param name="resourcePath">이미지 리소스 경로 (예: UTKImageIcons.IconSetting22)</param>
/// <param name="iconSize">아이콘 크기 (null이면 버튼 크기에 맞춤)</param>
public void SetImageIcon(string resourcePath, int? iconSize = null)
{
var texture = UTKImageIcons.LoadTexture(resourcePath);
if (texture != null)
{
ApplyImageIcon(texture, iconSize);
}
else
{
Debug.LogWarning($"[UTKButton] Image icon '{resourcePath}'을(를) 로드할 수 없습니다.");
}
}
/// <summary>
/// Image Icon을 비동기로 설정합니다.
/// </summary>
/// <param name="resourcePath">이미지 리소스 경로 (예: UTKImageIcons.IconSetting22)</param>
/// <param name="ct">취소 토큰</param>
/// <param name="iconSize">아이콘 크기 (null이면 버튼 크기에 맞춤)</param>
public async UniTask SetImageIconAsync(string resourcePath, CancellationToken ct = default, int? iconSize = null)
{
var texture = await UTKImageIcons.LoadTextureAsync(resourcePath, ct);
if (texture != null)
{
ApplyImageIcon(texture, iconSize);
}
else
{
Debug.LogWarning($"[UTKButton] Image icon '{resourcePath}'을(를) 로드할 수 없습니다.");
}
}
/// <summary>
/// 아이콘 이름으로 Image Icon을 설정합니다.
/// </summary>
/// <param name="iconName">아이콘 이름 (예: "icon_setting_22", "btn_close_16")</param>
/// <param name="iconSize">아이콘 크기 (null이면 버튼 크기에 맞춤)</param>
public void SetImageIconByName(string iconName, int? iconSize = null)
{
var texture = UTKImageIcons.LoadTextureByName(iconName);
if (texture != null)
{
ApplyImageIcon(texture, iconSize);
}
else
{
Debug.LogWarning($"[UTKButton] Image icon '{iconName}'을(를) 찾을 수 없습니다.");
}
}
/// <summary>
/// 모든 아이콘을 제거합니다.
/// </summary>
public void ClearIcon()
{
Icon = "";
ClearImageIcon();
if (_iconLabel != null)
{
_iconLabel.style.display = DisplayStyle.None;
}
}
private void ApplyImageIcon(Texture2D texture, int? iconSize)
{
// 기존 아이콘 Label 숨기기
Icon = "";
if (_iconLabel != null)
{
_iconLabel.style.display = DisplayStyle.None;
}
// 이미지 아이콘용 VisualElement 생성 또는 재사용
if (_imageIcon == null)
{
_imageIcon = new VisualElement
{
name = "image-icon",
pickingMode = PickingMode.Ignore
};
_imageIcon.AddToClassList("utk-button__image-icon");
Insert(0, _imageIcon);
}
var size = iconSize ?? GetDefaultIconSize();
_imageIcon.style.width = size;
_imageIcon.style.height = size;
_imageIcon.style.backgroundImage = new StyleBackground(texture);
_imageIcon.style.display = DisplayStyle.Flex;
EnableInClassList("utk-button--has-image-icon", true);
}
private void ClearImageIcon()
{
if (_imageIcon != null)
{
_imageIcon.style.display = DisplayStyle.None;
}
EnableInClassList("utk-button--has-image-icon", false);
}
private int GetDefaultIconSize()
{
return _size switch
{
ButtonSize.Small => 16,
ButtonSize.Large => 24,
_ => 20 // Medium
};
}
#endregion
#region IDisposable
public void Dispose()
{

View File

@@ -2,6 +2,7 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit.Common;
namespace UVC.UIToolkit
{
@@ -9,12 +10,37 @@ namespace UVC.UIToolkit
/// 체크박스 컴포넌트.
/// 선택/해제 상태를 토글할 수 있습니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 체크박스
/// var checkbox = new UTKCheckBox();
/// checkbox.Text = "약관에 동의합니다";
/// checkbox.OnValueChanged += (isChecked) => Debug.Log($"체크: {isChecked}");
///
/// // 상태 설정
/// checkbox.IsChecked = true;
/// checkbox.IsIndeterminate = true; // 부분 선택 상태
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 체크박스 -->
/// <utk:UTKCheckBox Text="이메일 수신 동의" />
///
/// <!-- 기본값 체크됨 -->
/// <utk:UTKCheckBox Text="자동 로그인" IsChecked="true" />
///
/// <!-- 비활성화 -->
/// <utk:UTKCheckBox Text="필수 동의" IsEnabled="false" IsChecked="true" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKCheckBox : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Button/UTKCheckBox";
private const string CHECK_ICON = "✓";
#endregion
#region Fields
@@ -95,6 +121,13 @@ namespace UVC.UIToolkit
CreateUI();
SetupEvents();
SubscribeToThemeChanges();
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
RegisterCallback<AttachToPanelEvent>(_ =>
{
if (_label != null) _label.text = _text;
UpdateState();
});
}
public UTKCheckBox(string text, bool isChecked = false) : this()
@@ -113,8 +146,9 @@ namespace UVC.UIToolkit
_checkBox = new VisualElement { name = "checkbox" };
_checkBox.AddToClassList("utk-checkbox__box");
_checkIcon = new Label { name = "check-icon", text = CHECK_ICON };
_checkIcon = new Label { name = "check-icon", text = UTKMaterialIcons.Check };
_checkIcon.AddToClassList("utk-checkbox__icon");
UTKMaterialIcons.ApplyIconStyle(_checkIcon, 14);
_checkBox.Add(_checkIcon);
Add(_checkBox);
@@ -200,7 +234,7 @@ namespace UVC.UIToolkit
if (_checkIcon != null)
{
_checkIcon.text = _isIndeterminate ? "" : CHECK_ICON;
_checkIcon.text = _isIndeterminate ? UTKMaterialIcons.Remove : UTKMaterialIcons.Check;
}
}
#endregion

View File

@@ -9,6 +9,33 @@ namespace UVC.UIToolkit
/// 라디오 버튼 컴포넌트.
/// Unity RadioButton을 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 라디오 버튼 그룹
/// var group = new RadioButtonGroup();
/// var radio1 = new UTKRadioButton { Label = "옵션 1" };
/// var radio2 = new UTKRadioButton { Label = "옵션 2" };
/// var radio3 = new UTKRadioButton { Label = "옵션 3" };
/// group.Add(radio1);
/// group.Add(radio2);
/// group.Add(radio3);
///
/// radio1.OnValueChanged += (isSelected) => {
/// if (isSelected) Debug.Log("옵션 1 선택됨");
/// };
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <ui:RadioButtonGroup>
/// <utk:UTKRadioButton label="소형" />
/// <utk:UTKRadioButton label="중형" value="true" />
/// <utk:UTKRadioButton label="대형" />
/// </ui:RadioButtonGroup>
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKRadioButton : RadioButton, IDisposable
{

View File

@@ -9,6 +9,29 @@ namespace UVC.UIToolkit
/// 토글 스위치 컴포넌트.
/// Unity Toggle을 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 토글
/// var toggle = new UTKToggle();
/// toggle.Label = "알림 받기";
/// toggle.IsOn = true;
/// toggle.OnValueChanged += (isOn) => Debug.Log($"토글: {isOn}");
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 토글 -->
/// <utk:UTKToggle label="다크 모드" />
///
/// <!-- 기본값 켜짐 -->
/// <utk:UTKToggle label="자동 저장" value="true" />
///
/// <!-- 비활성화 -->
/// <utk:UTKToggle label="프리미엄 기능" IsEnabled="false" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKToggle : Toggle, IDisposable
{

View File

@@ -10,6 +10,37 @@ namespace UVC.UIToolkit
/// 토글 버튼 그룹 컴포넌트.
/// Unity ToggleButtonGroup을 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 토글 버튼 그룹 생성
/// var group = new UTKToggleButtonGroup();
/// group.Add(new Button { text = "왼쪽" });
/// group.Add(new Button { text = "가운데" });
/// group.Add(new Button { text = "오른쪽" });
///
/// // 단일 선택 모드
/// group.allowEmptySelection = false;
///
/// // 다중 선택 모드
/// group.isMultipleSelection = true;
///
/// // 선택 변경 이벤트
/// group.OnSelectionChanged += (indices) => {
/// Debug.Log($"선택됨: {string.Join(", ", indices)}");
/// };
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKToggleButtonGroup allow-empty-selection="false">
/// <ui:Button text="왼쪽" />
/// <ui:Button text="가운데" />
/// <ui:Button text="오른쪽" />
/// </utk:UTKToggleButtonGroup>
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKToggleButtonGroup : ToggleButtonGroup, IDisposable
{

View File

@@ -9,6 +9,44 @@ namespace UVC.UIToolkit
/// 카드 컴포넌트.
/// 콘텐츠를 카드 형태로 표시합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 카드
/// var card = new UTKCard();
/// card.Title = "카드 제목";
/// card.Subtitle = "부제목";
/// card.Variant = UTKCard.CardVariant.Elevated;
///
/// // 이미지 설정
/// card.SetImage(myTexture);
///
/// // 콘텐츠 추가
/// card.AddContent(new Label("카드 내용"));
///
/// // 액션 버튼 추가
/// card.AddActionButton("자세히", () => Debug.Log("클릭"));
///
/// // 클릭 가능 카드
/// card.IsClickable = true;
/// card.OnClicked += () => Debug.Log("카드 클릭");
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 카드 -->
/// <utk:UTKCard Title="제목" Subtitle="부제목" Variant="Elevated">
/// <ui:Label text="카드 내용" />
/// </utk:UTKCard>
///
/// <!-- 클릭 가능 카드 -->
/// <utk:UTKCard Title="클릭해보세요" IsClickable="true" />
///
/// <!-- 외곽선 카드 -->
/// <utk:UTKCard Title="외곽선" Variant="Outlined" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKCard : VisualElement, IDisposable
{
@@ -138,6 +176,23 @@ namespace UVC.UIToolkit
CreateUI();
SetupEvents();
SubscribeToThemeChanges();
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
RegisterCallback<AttachToPanelEvent>(_ =>
{
if (_titleLabel != null)
{
_titleLabel.text = _title;
_titleLabel.style.display = string.IsNullOrEmpty(_title) ? DisplayStyle.None : DisplayStyle.Flex;
}
if (_subtitleLabel != null)
{
_subtitleLabel.text = _subtitle;
_subtitleLabel.style.display = string.IsNullOrEmpty(_subtitle) ? DisplayStyle.None : DisplayStyle.Flex;
}
UpdateVariant();
UpdateHeaderVisibility();
});
}
public UTKCard(string title, string subtitle = "") : this()

View File

@@ -9,6 +9,38 @@ namespace UVC.UIToolkit
/// 접을 수 있는 섹션 컴포넌트.
/// Unity Foldout을 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 폴드아웃
/// var foldout = new UTKFoldout("고급 설정", expanded: false);
/// foldout.Add(new Label("옵션 1"));
/// foldout.Add(new Label("옵션 2"));
///
/// // 상태 변경 이벤트
/// foldout.OnValueChanged += (isExpanded) => {
/// Debug.Log(isExpanded ? "펼쳐짐" : "접힘");
/// };
///
/// // 프로그래밍 방식으로 상태 제어
/// foldout.IsExpanded = true;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 폴드아웃 -->
/// <utk:UTKFoldout text="설정" value="true">
/// <ui:Label text="내용 1" />
/// <ui:Label text="내용 2" />
/// </utk:UTKFoldout>
///
/// <!-- 접힌 상태 -->
/// <utk:UTKFoldout text="고급 옵션" value="false">
/// <ui:Label text="숨겨진 내용" />
/// </utk:UTKFoldout>
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKFoldout : Foldout, IDisposable
{

View File

@@ -9,6 +9,44 @@ namespace UVC.UIToolkit
/// 도움말 박스 컴포넌트.
/// Unity HelpBox를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 정보 메시지
/// var infoBox = new UTKHelpBox()
/// {
/// Message = "이 기능은 베타 버전입니다.",
/// messageType = HelpBoxMessageType.Info
/// };
///
/// // 경고 메시지
/// var warningBox = new UTKHelpBox()
/// {
/// Message = "주의: 되돌릴 수 없습니다.",
/// messageType = HelpBoxMessageType.Warning
/// };
///
/// // 오류 메시지
/// var errorBox = new UTKHelpBox()
/// {
/// Message = "오류: 파일을 찾을 수 없습니다.",
/// messageType = HelpBoxMessageType.Error
/// };
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 정보 -->
/// <utk:UTKHelpBox text="정보 메시지입니다." message-type="Info" />
///
/// <!-- 경고 -->
/// <utk:UTKHelpBox text="경고 메시지입니다." message-type="Warning" />
///
/// <!-- 오류 -->
/// <utk:UTKHelpBox text="오류 메시지입니다." message-type="Error" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKHelpBox : HelpBox, IDisposable
{

View File

@@ -0,0 +1,359 @@
// <auto-generated>
// 이 파일은 UTKImageIconsGenerator에 의해 자동 생성되었습니다.
// 직접 수정하지 마세요. Tools > UTK > Image Icons Generator 메뉴로 재생성하세요.
// Source: Assets/Resources/UIToolkit/Images
// </auto-generated>
#nullable enable
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace UVC.UIToolkit
{
/// <summary>
/// 이미지 아이콘 리소스 경로 상수 클래스입니다.
/// 총 41개의 아이콘을 포함합니다.
/// </summary>
/// <example>
/// <code>
/// // 상수로 리소스 경로 사용
/// string path = UTKImageIcons.Home;
///
/// // 동기 Sprite 로드 (캐싱됨)
/// Sprite sprite = UTKImageIcons.LoadSprite(UTKImageIcons.Home);
///
/// // 비동기 Sprite 로드 (UniTask, 캐싱됨)
/// Sprite? sprite = await UTKImageIcons.LoadSpriteAsync(UTKImageIcons.Home, cancellationToken);
///
/// // 동기 Texture2D 로드 (캐싱됨)
/// Texture2D texture = UTKImageIcons.LoadTexture(UTKImageIcons.Settings);
///
/// // 비동기 Texture2D 로드 (UniTask, 캐싱됨)
/// Texture2D? texture = await UTKImageIcons.LoadTextureAsync(UTKImageIcons.Settings, cancellationToken);
///
/// // 이름으로 Sprite 로드
/// Sprite icon = UTKImageIcons.LoadSpriteByName("home");
///
/// // 이름으로 비동기 Sprite 로드
/// Sprite? icon = await UTKImageIcons.LoadSpriteByNameAsync("home", cancellationToken);
///
/// // 이름으로 경로 조회
/// string iconPath = UTKImageIcons.GetPath("settings");
///
/// // 아이콘 존재 여부 확인
/// if (UTKImageIcons.HasIcon("search")) { }
///
/// // 전체 아이콘 이름 순회
/// foreach (var name in UTKImageIcons.GetAllIconNames()) { }
///
/// // 캐시 클리어
/// UTKImageIcons.ClearCache();
/// </code>
/// </example>
/// <remarks>
/// <para><b>UXML에서 사용하기:</b></para>
/// <para>UXML에서 이미지 아이콘을 사용하려면 USS에서 background-image를 설정합니다.</para>
/// <code>
/// /* USS 파일 */
/// .my-icon {
/// width: 24px;
/// height: 24px;
/// background-image: resource('UIToolkit/Images/icon_setting_22');
/// }
/// </code>
/// <code>
/// <!-- UXML 파일 -->
/// <ui:VisualElement class="my-icon" />
/// </code>
/// <para><b>C# 코드에서 UXML 요소에 이미지 적용:</b></para>
/// <code>
/// var iconElement = root.Q<VisualElement>("my-icon");
/// var texture = UTKImageIcons.LoadTextureByName("icon_setting_22");
/// iconElement.style.backgroundImage = new StyleBackground(texture);
/// </code>
/// <para><b>Image 요소에서 사용:</b></para>
/// <code>
/// var image = root.Q<Image>("my-image");
/// image.sprite = UTKImageIcons.LoadSpriteByName("btn_close_16");
/// </code>
/// </remarks>
public static class UTKImageIcons
{
/// <summary>btn_cancel_64.png</summary>
public const string BtnCancel64 = "UIToolkit/Images/btn_cancel_64";
/// <summary>btn_close_16.png</summary>
public const string BtnClose16 = "UIToolkit/Images/btn_close_16";
/// <summary>btn_close_22.png</summary>
public const string BtnClose22 = "UIToolkit/Images/btn_close_22";
/// <summary>cursor_arc_32.png</summary>
public const string CursorArc32 = "UIToolkit/Images/cursor_arc_32";
/// <summary>cursor_ask_32.png</summary>
public const string CursorAsk32 = "UIToolkit/Images/cursor_ask_32";
/// <summary>cursor_context_menu_32.png</summary>
public const string CursorContextMenu32 = "UIToolkit/Images/cursor_context_menu_32";
/// <summary>cursor_copy_32.png</summary>
public const string CursorCopy32 = "UIToolkit/Images/cursor_copy_32";
/// <summary>cursor_default_black_32.png</summary>
public const string CursorDefaultBlack32 = "UIToolkit/Images/cursor_default_black_32";
/// <summary>cursor_default_white_32.png</summary>
public const string CursorDefaultWhite32 = "UIToolkit/Images/cursor_default_white_32";
/// <summary>cursor_export_32.png</summary>
public const string CursorExport32 = "UIToolkit/Images/cursor_export_32";
/// <summary>cursor_grabbing_32.png</summary>
public const string CursorGrabbing32 = "UIToolkit/Images/cursor_grabbing_32";
/// <summary>cursor_hand_32.png</summary>
public const string CursorHand32 = "UIToolkit/Images/cursor_hand_32";
/// <summary>cursor_link_32.png</summary>
public const string CursorLink32 = "UIToolkit/Images/cursor_link_32";
/// <summary>cursor_move_32.png</summary>
public const string CursorMove32 = "UIToolkit/Images/cursor_move_32";
/// <summary>cursor_node_32.png</summary>
public const string CursorNode32 = "UIToolkit/Images/cursor_node_32";
/// <summary>cursor_point_white_32.png</summary>
public const string CursorPointWhite32 = "UIToolkit/Images/cursor_point_white_32";
/// <summary>cursor_resize_col_32.png</summary>
public const string CursorResizeCol32 = "UIToolkit/Images/cursor_resize_col_32";
/// <summary>cursor_resize_down_32.png</summary>
public const string CursorResizeDown32 = "UIToolkit/Images/cursor_resize_down_32";
/// <summary>cursor_resize_h_32.png</summary>
public const string CursorResizeH32 = "UIToolkit/Images/cursor_resize_h_32";
/// <summary>cursor_resize_left_32.png</summary>
public const string CursorResizeLeft32 = "UIToolkit/Images/cursor_resize_left_32";
/// <summary>cursor_resize_right_32.png</summary>
public const string CursorResizeRight32 = "UIToolkit/Images/cursor_resize_right_32";
/// <summary>cursor_resize_row_32.png</summary>
public const string CursorResizeRow32 = "UIToolkit/Images/cursor_resize_row_32";
/// <summary>cursor_resize_tlbr_32.png</summary>
public const string CursorResizeTlbr32 = "UIToolkit/Images/cursor_resize_tlbr_32";
/// <summary>cursor_resize_trbl_32.png</summary>
public const string CursorResizeTrbl32 = "UIToolkit/Images/cursor_resize_trbl_32";
/// <summary>cursor_resize_up_32.png</summary>
public const string CursorResizeUp32 = "UIToolkit/Images/cursor_resize_up_32";
/// <summary>cursor_resize_v_32.png</summary>
public const string CursorResizeV32 = "UIToolkit/Images/cursor_resize_v_32";
/// <summary>cursor_rotate_bottom_left_32.png</summary>
public const string CursorRotateBottomLeft32 = "UIToolkit/Images/cursor_rotate_bottom_left_32";
/// <summary>cursor_rotate_bottom_right_32.png</summary>
public const string CursorRotateBottomRight32 = "UIToolkit/Images/cursor_rotate_bottom_right_32";
/// <summary>cursor_rotate_top_left_32.png</summary>
public const string CursorRotateTopLeft32 = "UIToolkit/Images/cursor_rotate_top_left_32";
/// <summary>cursor_rotate_top_right_32.png</summary>
public const string CursorRotateTopRight32 = "UIToolkit/Images/cursor_rotate_top_right_32";
/// <summary>cursor_select_32.png</summary>
public const string CursorSelect32 = "UIToolkit/Images/cursor_select_32";
/// <summary>cursor_wait_32.png</summary>
public const string CursorWait32 = "UIToolkit/Images/cursor_wait_32";
/// <summary>cursor_zoom_in_32.png</summary>
public const string CursorZoomIn32 = "UIToolkit/Images/cursor_zoom_in_32";
/// <summary>cursor_zoom_out_32.png</summary>
public const string CursorZoomOut32 = "UIToolkit/Images/cursor_zoom_out_32";
/// <summary>icon_down_20x16.png</summary>
public const string IconDown20x16 = "UIToolkit/Images/icon_down_20x16";
/// <summary>icon_down_22.png</summary>
public const string IconDown22 = "UIToolkit/Images/icon_down_22";
/// <summary>icon_eye_22x16.png</summary>
public const string IconEye22x16 = "UIToolkit/Images/icon_eye_22x16";
/// <summary>icon_eye_close_22x16.png</summary>
public const string IconEyeClose22x16 = "UIToolkit/Images/icon_eye_close_22x16";
/// <summary>icon_right_22.png</summary>
public const string IconRight22 = "UIToolkit/Images/icon_right_22";
/// <summary>icon_search_22x16.png</summary>
public const string IconSearch22x16 = "UIToolkit/Images/icon_search_22x16";
/// <summary>icon_setting_22.png</summary>
public const string IconSetting22 = "UIToolkit/Images/icon_setting_22";
#region Lookup & Load
private static readonly Dictionary<string, string> _pathsByName = new()
{
["btn_cancel_64"] = "UIToolkit/Images/btn_cancel_64",
["btn_close_16"] = "UIToolkit/Images/btn_close_16",
["btn_close_22"] = "UIToolkit/Images/btn_close_22",
["cursor_arc_32"] = "UIToolkit/Images/cursor_arc_32",
["cursor_ask_32"] = "UIToolkit/Images/cursor_ask_32",
["cursor_context_menu_32"] = "UIToolkit/Images/cursor_context_menu_32",
["cursor_copy_32"] = "UIToolkit/Images/cursor_copy_32",
["cursor_default_black_32"] = "UIToolkit/Images/cursor_default_black_32",
["cursor_default_white_32"] = "UIToolkit/Images/cursor_default_white_32",
["cursor_export_32"] = "UIToolkit/Images/cursor_export_32",
["cursor_grabbing_32"] = "UIToolkit/Images/cursor_grabbing_32",
["cursor_hand_32"] = "UIToolkit/Images/cursor_hand_32",
["cursor_link_32"] = "UIToolkit/Images/cursor_link_32",
["cursor_move_32"] = "UIToolkit/Images/cursor_move_32",
["cursor_node_32"] = "UIToolkit/Images/cursor_node_32",
["cursor_point_white_32"] = "UIToolkit/Images/cursor_point_white_32",
["cursor_resize_col_32"] = "UIToolkit/Images/cursor_resize_col_32",
["cursor_resize_down_32"] = "UIToolkit/Images/cursor_resize_down_32",
["cursor_resize_h_32"] = "UIToolkit/Images/cursor_resize_h_32",
["cursor_resize_left_32"] = "UIToolkit/Images/cursor_resize_left_32",
["cursor_resize_right_32"] = "UIToolkit/Images/cursor_resize_right_32",
["cursor_resize_row_32"] = "UIToolkit/Images/cursor_resize_row_32",
["cursor_resize_tlbr_32"] = "UIToolkit/Images/cursor_resize_tlbr_32",
["cursor_resize_trbl_32"] = "UIToolkit/Images/cursor_resize_trbl_32",
["cursor_resize_up_32"] = "UIToolkit/Images/cursor_resize_up_32",
["cursor_resize_v_32"] = "UIToolkit/Images/cursor_resize_v_32",
["cursor_rotate_bottom_left_32"] = "UIToolkit/Images/cursor_rotate_bottom_left_32",
["cursor_rotate_bottom_right_32"] = "UIToolkit/Images/cursor_rotate_bottom_right_32",
["cursor_rotate_top_left_32"] = "UIToolkit/Images/cursor_rotate_top_left_32",
["cursor_rotate_top_right_32"] = "UIToolkit/Images/cursor_rotate_top_right_32",
["cursor_select_32"] = "UIToolkit/Images/cursor_select_32",
["cursor_wait_32"] = "UIToolkit/Images/cursor_wait_32",
["cursor_zoom_in_32"] = "UIToolkit/Images/cursor_zoom_in_32",
["cursor_zoom_out_32"] = "UIToolkit/Images/cursor_zoom_out_32",
["icon_down_20x16"] = "UIToolkit/Images/icon_down_20x16",
["icon_down_22"] = "UIToolkit/Images/icon_down_22",
["icon_eye_22x16"] = "UIToolkit/Images/icon_eye_22x16",
["icon_eye_close_22x16"] = "UIToolkit/Images/icon_eye_close_22x16",
["icon_right_22"] = "UIToolkit/Images/icon_right_22",
["icon_search_22x16"] = "UIToolkit/Images/icon_search_22x16",
["icon_setting_22"] = "UIToolkit/Images/icon_setting_22",
};
private static readonly Dictionary<string, Sprite?> _spriteCache = new();
private static readonly Dictionary<string, Texture2D?> _textureCache = new();
/// <summary>
/// 아이콘 이름으로 리소스 경로를 조회합니다.
/// </summary>
/// <param name="iconName">아이콘 파일명 (확장자 제외)</param>
/// <returns>리소스 경로, 없으면 빈 문자열</returns>
public static string GetPath(string iconName)
{
return _pathsByName.TryGetValue(iconName, out var path) ? path : string.Empty;
}
/// <summary>
/// 아이콘이 존재하는지 확인합니다.
/// </summary>
public static bool HasIcon(string iconName) => _pathsByName.ContainsKey(iconName);
/// <summary>
/// 모든 아이콘 이름 목록을 반환합니다.
/// </summary>
public static IEnumerable<string> GetAllIconNames() => _pathsByName.Keys;
/// <summary>
/// 전체 아이콘 수를 반환합니다.
/// </summary>
public static int Count => 41;
/// <summary>
/// 리소스 경로로 Sprite를 동기로 로드합니다. (캐싱됨)
/// </summary>
/// <param name="resourcePath">리소스 경로 (예: Icons/Home)</param>
public static Sprite? LoadSprite(string resourcePath)
{
if (_spriteCache.TryGetValue(resourcePath, out var cached))
return cached;
var sprite = Resources.Load<Sprite>(resourcePath);
_spriteCache[resourcePath] = sprite;
return sprite;
}
/// <summary>
/// 리소스 경로로 Sprite를 비동기로 로드합니다. (캐싱됨)
/// </summary>
/// <param name="resourcePath">리소스 경로 (예: Icons/Home)</param>
/// <param name="ct">취소 토큰</param>
public static async UniTask<Sprite?> LoadSpriteAsync(string resourcePath, CancellationToken ct = default)
{
if (_spriteCache.TryGetValue(resourcePath, out var cached))
return cached;
var request = Resources.LoadAsync<Sprite>(resourcePath);
await request.ToUniTask(cancellationToken: ct);
var sprite = request.asset as Sprite;
_spriteCache[resourcePath] = sprite;
return sprite;
}
/// <summary>
/// 리소스 경로로 Texture2D를 동기로 로드합니다. (캐싱됨)
/// </summary>
/// <param name="resourcePath">리소스 경로 (예: Icons/Home)</param>
public static Texture2D? LoadTexture(string resourcePath)
{
if (_textureCache.TryGetValue(resourcePath, out var cached))
return cached;
var texture = Resources.Load<Texture2D>(resourcePath);
_textureCache[resourcePath] = texture;
return texture;
}
/// <summary>
/// 리소스 경로로 Texture2D를 비동기로 로드합니다. (캐싱됨)
/// </summary>
/// <param name="resourcePath">리소스 경로 (예: Icons/Home)</param>
/// <param name="ct">취소 토큰</param>
public static async UniTask<Texture2D?> LoadTextureAsync(string resourcePath, CancellationToken ct = default)
{
if (_textureCache.TryGetValue(resourcePath, out var cached))
return cached;
var request = Resources.LoadAsync<Texture2D>(resourcePath);
await request.ToUniTask(cancellationToken: ct);
var texture = request.asset as Texture2D;
_textureCache[resourcePath] = texture;
return texture;
}
/// <summary>
/// 아이콘 이름으로 Sprite를 동기로 로드합니다. (캐싱됨)
/// </summary>
/// <param name="iconName">아이콘 파일명 (확장자 제외)</param>
public static Sprite? LoadSpriteByName(string iconName)
{
var path = GetPath(iconName);
return string.IsNullOrEmpty(path) ? null : LoadSprite(path);
}
/// <summary>
/// 아이콘 이름으로 Sprite를 비동기로 로드합니다. (캐싱됨)
/// </summary>
/// <param name="iconName">아이콘 파일명 (확장자 제외)</param>
/// <param name="ct">취소 토큰</param>
public static async UniTask<Sprite?> LoadSpriteByNameAsync(string iconName, CancellationToken ct = default)
{
var path = GetPath(iconName);
return string.IsNullOrEmpty(path) ? null : await LoadSpriteAsync(path, ct);
}
/// <summary>
/// 아이콘 이름으로 Texture2D를 동기로 로드합니다. (캐싱됨)
/// </summary>
/// <param name="iconName">아이콘 파일명 (확장자 제외)</param>
public static Texture2D? LoadTextureByName(string iconName)
{
var path = GetPath(iconName);
return string.IsNullOrEmpty(path) ? null : LoadTexture(path);
}
/// <summary>
/// 아이콘 이름으로 Texture2D를 비동기로 로드합니다. (캐싱됨)
/// </summary>
/// <param name="iconName">아이콘 파일명 (확장자 제외)</param>
/// <param name="ct">취소 토큰</param>
public static async UniTask<Texture2D?> LoadTextureByNameAsync(string iconName, CancellationToken ct = default)
{
var path = GetPath(iconName);
return string.IsNullOrEmpty(path) ? null : await LoadTextureAsync(path, ct);
}
/// <summary>
/// 캐시를 클리어합니다.
/// </summary>
public static void ClearCache()
{
_spriteCache.Clear();
_textureCache.Clear();
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,37 @@ namespace UVC.UIToolkit
/// 스크롤 뷰 컴포넌트.
/// Unity ScrollView를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 스크롤 뷰
/// var scrollView = new UTKScrollView();
/// scrollView.Add(new Label("내용 1"));
/// scrollView.Add(new Label("내용 2"));
/// scrollView.Add(new Label("내용 3"));
///
/// // 스크롤 모드 설정
/// var verticalScroll = new UTKScrollView(ScrollViewMode.Vertical);
/// var horizontalScroll = new UTKScrollView(ScrollViewMode.Horizontal);
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 스크롤 뷰 -->
/// <utk:UTKScrollView style="height: 200px;">
/// <ui:Label text="항목 1" />
/// <ui:Label text="항목 2" />
/// <ui:Label text="항목 3" />
/// </utk:UTKScrollView>
///
/// <!-- 수직 스크롤만 -->
/// <utk:UTKScrollView mode="Vertical" />
///
/// <!-- 수평 스크롤만 -->
/// <utk:UTKScrollView mode="Horizontal" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKScrollView : ScrollView, IDisposable
{

View File

@@ -10,6 +10,33 @@ namespace UVC.UIToolkit
/// 드롭다운 메뉴 컴포넌트.
/// Unity DropdownField를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 드롭다운
/// var dropdown = new UTKDropdown();
/// dropdown.Label = "국가 선택";
/// dropdown.SetChoices(new List<string> { "한국", "미국", "일본" });
/// dropdown.OnSelectionChanged += (index, value) => Debug.Log($"선택: {value}");
///
/// // 기본값 설정
/// dropdown.value = "한국";
/// dropdown.index = 0;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 드롭다운 -->
/// <utk:UTKDropdown label="정렬" choices="이름,날짜,크기" />
///
/// <!-- 기본값 지정 -->
/// <utk:UTKDropdown label="언어" choices="한국어,English,日本語" index="0" />
///
/// <!-- 비활성화 -->
/// <utk:UTKDropdown label="선택" IsEnabled="false" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKDropdown : DropdownField, IDisposable
{

View File

@@ -9,6 +9,38 @@ namespace UVC.UIToolkit
/// Enum 선택 드롭다운 컴포넌트.
/// Unity EnumField를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // Enum 정의
/// public enum MyOption { Option1, Option2, Option3 }
///
/// // 기본 Enum 드롭다운
/// var enumDropdown = new UTKEnumDropDown();
/// enumDropdown.label = "옵션 선택";
/// enumDropdown.Init(MyOption.Option1);
///
/// // 값 변경 이벤트
/// enumDropdown.OnValueChanged += (value) => {
/// Debug.Log($"선택됨: {value}");
/// };
///
/// // 현재 값 가져오기
/// var current = (MyOption)enumDropdown.Value;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- UXML에서는 C#에서 Init() 호출 필요 -->
/// <utk:UTKEnumDropDown name="my-enum" label="옵션" />
/// </ui:UXML>
/// </code>
/// <para><b>UXML 로드 후 초기화:</b></para>
/// <code>
/// var enumField = root.Q<UTKEnumDropDown>("my-enum");
/// enumField.Init(MyOption.Option1);
/// </code>
/// </example>
[UxmlElement]
public partial class UTKEnumDropDown : EnumField, IDisposable
{

View File

@@ -9,6 +9,27 @@ namespace UVC.UIToolkit
/// 실수 입력 필드 컴포넌트.
/// Unity FloatField를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 실수 필드
/// var floatField = new UTKFloatField();
/// floatField.label = "가격";
/// floatField.value = 99.99f;
///
/// // 값 변경 이벤트
/// floatField.OnValueChanged += (value) => Debug.Log($"가격: {value}");
///
/// // 현재 값 접근
/// float current = floatField.Value;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKFloatField label="가격" value="99.99" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKFloatField : FloatField, IDisposable
{

View File

@@ -9,6 +9,47 @@ namespace UVC.UIToolkit
/// 입력 필드 컴포넌트.
/// Unity TextField를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 입력 필드
/// var input = new UTKInputField();
/// input.Label = "이름";
/// input.Placeholder = "이름을 입력하세요";
/// input.OnValueChanged += (value) => Debug.Log($"입력값: {value}");
///
/// // 비밀번호 입력 필드
/// var password = new UTKInputField();
/// password.Label = "비밀번호";
/// password.isPasswordField = true;
///
/// // 검증 오류 표시
/// input.SetError("이름은 필수입니다.");
/// input.ClearError();
///
/// // 변형 스타일
/// input.Variant = UTKInputField.InputFieldVariant.Outlined;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 입력 필드 -->
/// <utk:UTKInputField label="이름" />
///
/// <!-- 플레이스홀더 -->
/// <utk:UTKInputField label="이메일" Placeholder="example@email.com" />
///
/// <!-- 비밀번호 필드 -->
/// <utk:UTKInputField label="비밀번호" is-password-field="true" />
///
/// <!-- 여러 줄 입력 -->
/// <utk:UTKInputField label="설명" multiline="true" />
///
/// <!-- 비활성화 -->
/// <utk:UTKInputField label="읽기전용" IsEnabled="false" value="수정 불가" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKInputField : TextField, IDisposable
{
@@ -133,6 +174,12 @@ namespace UVC.UIToolkit
SetupStyles();
SetupEvents();
SubscribeToThemeChanges();
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
RegisterCallback<AttachToPanelEvent>(_ =>
{
UpdateVariant();
});
}
public UTKInputField(string label, string placeholder = "") : this()

View File

@@ -9,6 +9,27 @@ namespace UVC.UIToolkit
/// 정수 입력 필드 컴포넌트.
/// Unity IntegerField를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 정수 필드
/// var intField = new UTKIntegerField();
/// intField.label = "수량";
/// intField.value = 10;
///
/// // 값 변경 이벤트
/// intField.OnValueChanged += (value) => Debug.Log($"수량: {value}");
///
/// // 현재 값 접근
/// int current = intField.Value;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKIntegerField label="수량" value="10" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKIntegerField : IntegerField, IDisposable
{

View File

@@ -2,6 +2,7 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit.Common;
namespace UVC.UIToolkit.Input
{
@@ -217,15 +218,17 @@ namespace UVC.UIToolkit.Input
buttonContainer.AddToClassList("utk-number-stepper__buttons");
// Up Button
_upButton = new Button { name = "stepper-up", text = "\u25B2" }; // ▲
_upButton = new Button { name = "stepper-up", text = UTKMaterialIcons.KeyboardArrowUp };
_upButton.AddToClassList("utk-number-stepper__btn");
_upButton.AddToClassList("utk-number-stepper__btn--up");
UTKMaterialIcons.ApplyIconStyle(_upButton, 14);
buttonContainer.Add(_upButton);
// Down Button
_downButton = new Button { name = "stepper-down", text = "\u25BC" }; // ▼
_downButton = new Button { name = "stepper-down", text = UTKMaterialIcons.KeyboardArrowDown };
_downButton.AddToClassList("utk-number-stepper__btn");
_downButton.AddToClassList("utk-number-stepper__btn--down");
UTKMaterialIcons.ApplyIconStyle(_downButton, 14);
buttonContainer.Add(_downButton);
Add(buttonContainer);

View File

@@ -9,6 +9,29 @@ namespace UVC.UIToolkit
/// Vector3 입력 필드 컴포넌트.
/// Unity Vector3Field를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 Vector3 필드
/// var vec3Field = new UTKVector3Field();
/// vec3Field.label = "위치";
/// vec3Field.Value = new Vector3(1, 2, 3);
///
/// // 값 변경 이벤트
/// vec3Field.OnValueChanged += (vec) => Debug.Log($"위치: {vec}");
///
/// // 라벨 커스터마이징
/// vec3Field.XLabel = "X위치";
/// vec3Field.YLabel = "Y위치";
/// vec3Field.ZLabel = "Z위치";
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKVector3Field label="위치" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKVector3Field : Vector3Field, IDisposable
{

View File

@@ -1,5 +1,7 @@
#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
@@ -8,7 +10,72 @@ namespace UVC.UIToolkit
/// <summary>
/// 텍스트 라벨 컴포넌트.
/// 다양한 스타일과 크기의 텍스트를 표시합니다.
/// Material Icon 또는 Image Icon을 텍스트와 함께 또는 단독으로 표시할 수 있습니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 라벨
/// var label = new UTKLabel();
/// label.Text = "안녕하세요";
/// label.Size = UTKLabel.LabelSize.Body1;
/// label.Variant = UTKLabel.LabelVariant.Primary;
///
/// // 제목 스타일
/// var title = new UTKLabel();
/// title.Text = "제목";
/// title.Size = UTKLabel.LabelSize.Heading1;
/// title.IsBold = true;
///
/// // 스타일 적용
/// label.IsItalic = true;
/// label.TextAlign = UTKLabel.TextAlign.Center;
///
/// // Material Icon과 텍스트 함께 사용
/// var iconLabel = new UTKLabel("설정", UTKMaterialIcons.Settings);
///
/// // Image Icon과 텍스트 함께 사용
/// var imgLabel = new UTKLabel("닫기", UTKImageIcons.BtnClose16, isImageIcon: true);
///
/// // Material Icon만 사용
/// var iconOnly = new UTKLabel(UTKMaterialIcons.Home);
///
/// // Image Icon만 사용
/// var imgOnly = new UTKLabel(UTKImageIcons.IconSetting22, isImageIcon: true);
///
/// // 메서드로 아이콘 설정
/// label.SetMaterialIcon(UTKMaterialIcons.Search);
/// label.SetImageIcon(UTKImageIcons.IconSetting22);
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 라벨 -->
/// <utk:UTKLabel Text="일반 텍스트" />
///
/// <!-- 제목 -->
/// <utk:UTKLabel Text="제목" Size="H1" IsBold="true" />
///
/// <!-- 보조 텍스트 -->
/// <utk:UTKLabel Text="설명" Size="Caption" Variant="Secondary" />
///
/// <!-- Material Icon과 텍스트 -->
/// <utk:UTKLabel Text="설정" MaterialIcon="settings" />
///
/// <!-- Image Icon과 텍스트 -->
/// <utk:UTKLabel Text="닫기" ImageIcon="btn_close_16" />
///
/// <!-- 아이콘만 (Material) -->
/// <utk:UTKLabel MaterialIcon="home" IconSize="24" />
///
/// <!-- 아이콘만 (Image) -->
/// <utk:UTKLabel ImageIcon="icon_setting_22" IconSize="22" />
///
/// <!-- 아이콘 오른쪽 배치 -->
/// <utk:UTKLabel Text="다음" MaterialIcon="arrow_forward" IconPlacement="Right" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKLabel : VisualElement, IDisposable
{
@@ -19,6 +86,8 @@ namespace UVC.UIToolkit
#region Fields
private bool _disposed;
private Label? _label;
private Label? _iconLabel;
private VisualElement? _imageIcon;
private string _text = "";
private LabelSize _size = LabelSize.Body1;
@@ -27,6 +96,11 @@ namespace UVC.UIToolkit
private bool _isItalic;
private TextAlign _textAlign = TextAlign.Left;
private bool _isSelectable;
private IconPosition _iconPosition = IconPosition.Left;
private int _iconSize;
private int _gap = 4;
private string _materialIconName = "";
private string _imageIconName = "";
#endregion
#region Properties
@@ -116,6 +190,80 @@ namespace UVC.UIToolkit
}
}
}
/// <summary>아이콘 위치 (텍스트 기준 왼쪽/오른쪽)</summary>
[UxmlAttribute]
public IconPosition IconPlacement
{
get => _iconPosition;
set
{
_iconPosition = value;
UpdateIconPosition();
}
}
/// <summary>아이콘 크기 (0이면 텍스트 크기에 맞춤)</summary>
[UxmlAttribute]
public int IconSize
{
get => _iconSize;
set
{
_iconSize = value;
UpdateIconSize();
}
}
/// <summary>아이콘과 텍스트 사이 간격 (px)</summary>
[UxmlAttribute]
public int Gap
{
get => _gap;
set
{
_gap = value;
UpdateGap();
}
}
/// <summary>Material Icon 이름 (예: "settings", "home")</summary>
[UxmlAttribute]
public string MaterialIcon
{
get => _materialIconName;
set
{
_materialIconName = value;
if (!string.IsNullOrEmpty(value))
{
SetMaterialIconByName(value);
}
else
{
ClearIcon();
}
}
}
/// <summary>Image Icon 이름 (예: "icon_setting_22", "btn_close_16")</summary>
[UxmlAttribute]
public string ImageIcon
{
get => _imageIconName;
set
{
_imageIconName = value;
if (!string.IsNullOrEmpty(value))
{
SetImageIconByName(value);
}
else
{
ClearIcon();
}
}
}
#endregion
#region Enums
@@ -149,6 +297,12 @@ namespace UVC.UIToolkit
Center,
Right
}
public enum IconPosition
{
Left,
Right
}
#endregion
#region Constructor
@@ -164,13 +318,95 @@ namespace UVC.UIToolkit
CreateUI();
SubscribeToThemeChanges();
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
RegisterCallback<AttachToPanelEvent>(_ =>
{
if (_label != null) _label.text = _text;
UpdateSize();
UpdateVariant();
UpdateTextAlign();
});
}
/// <summary>
/// 텍스트와 크기를 지정하여 라벨을 생성합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="size">텍스트 크기</param>
public UTKLabel(string text, LabelSize size = LabelSize.Body1) : this()
{
Text = text;
Size = size;
}
/// <summary>
/// Material Icon과 텍스트를 함께 표시하는 라벨을 생성합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="materialIcon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings)</param>
/// <param name="iconPosition">아이콘 위치 (기본: 왼쪽)</param>
/// <param name="size">텍스트 크기</param>
public UTKLabel(string text, string materialIcon, IconPosition iconPosition = IconPosition.Left, LabelSize size = LabelSize.Body1) : this()
{
Text = text;
Size = size;
IconPlacement = iconPosition;
SetMaterialIcon(materialIcon);
}
/// <summary>
/// Image Icon과 텍스트를 함께 표시하는 라벨을 생성합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="imageIconPath">Image Icon 리소스 경로 (예: UTKImageIcons.IconSetting22)</param>
/// <param name="isImageIcon">true로 설정해야 합니다 (Image Icon 생성자 구분용)</param>
/// <param name="iconPosition">아이콘 위치 (기본: 왼쪽)</param>
/// <param name="size">텍스트 크기</param>
public UTKLabel(string text, string imageIconPath, bool isImageIcon, IconPosition iconPosition = IconPosition.Left, LabelSize size = LabelSize.Body1) : this()
{
Text = text;
Size = size;
IconPlacement = iconPosition;
if (isImageIcon)
{
SetImageIcon(imageIconPath);
}
else
{
SetMaterialIcon(imageIconPath);
}
}
/// <summary>
/// Material Icon만 표시하는 라벨을 생성합니다.
/// </summary>
/// <param name="materialIcon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Home)</param>
/// <param name="iconSize">아이콘 크기 (0이면 기본 크기)</param>
public UTKLabel(string materialIcon, int iconSize = 0) : this()
{
IconSize = iconSize;
SetMaterialIcon(materialIcon);
}
/// <summary>
/// Image Icon만 표시하는 라벨을 생성합니다.
/// </summary>
/// <param name="imageIconPath">Image Icon 리소스 경로 (예: UTKImageIcons.IconSetting22)</param>
/// <param name="isImageIcon">true로 설정해야 합니다 (Image Icon 생성자 구분용)</param>
/// <param name="iconSize">아이콘 크기 (0이면 기본 크기)</param>
public UTKLabel(string imageIconPath, bool isImageIcon, int iconSize = 0) : this()
{
IconSize = iconSize;
if (isImageIcon)
{
SetImageIcon(imageIconPath);
}
else
{
SetMaterialIcon(imageIconPath);
}
}
#endregion
#region UI Creation
@@ -267,6 +503,266 @@ namespace UVC.UIToolkit
};
AddToClassList(alignClass);
}
private void UpdateIconPosition()
{
if (_iconLabel != null)
{
_iconLabel.SendToBack();
if (_iconPosition == IconPosition.Right)
{
_iconLabel.BringToFront();
}
}
if (_imageIcon != null)
{
_imageIcon.SendToBack();
if (_iconPosition == IconPosition.Right)
{
_imageIcon.BringToFront();
}
}
}
private void UpdateIconSize()
{
var size = GetEffectiveIconSize();
if (_iconLabel != null && _iconLabel.style.display == DisplayStyle.Flex)
{
_iconLabel.style.fontSize = size;
}
if (_imageIcon != null && _imageIcon.style.display == DisplayStyle.Flex)
{
_imageIcon.style.width = size;
_imageIcon.style.height = size;
}
}
private void UpdateGap()
{
// 아이콘과 텍스트 사이의 간격을 적용
if (_iconLabel != null)
{
_iconLabel.style.marginRight = _iconPosition == IconPosition.Left ? _gap : 0;
_iconLabel.style.marginLeft = _iconPosition == IconPosition.Right ? _gap : 0;
}
if (_imageIcon != null)
{
_imageIcon.style.marginRight = _iconPosition == IconPosition.Left ? _gap : 0;
_imageIcon.style.marginLeft = _iconPosition == IconPosition.Right ? _gap : 0;
}
}
private int GetEffectiveIconSize()
{
if (_iconSize > 0) return _iconSize;
return _size switch
{
LabelSize.H1 => 28,
LabelSize.H2 => 24,
LabelSize.H3 => 20,
LabelSize.Body1 => 16,
LabelSize.Body2 => 14,
LabelSize.Label1 => 14,
LabelSize.Label2 => 12,
LabelSize.Label3 => 12,
LabelSize.Caption => 12,
_ => 16
};
}
#endregion
#region Icon Methods
/// <summary>
/// Material Icon을 설정합니다.
/// </summary>
/// <param name="icon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings)</param>
/// <param name="fontSize">아이콘 폰트 크기 (null이면 텍스트 크기에 맞춤)</param>
public void SetMaterialIcon(string icon, int? fontSize = null)
{
ClearImageIcon();
EnsureIconLabel();
if (_iconLabel != null)
{
_iconLabel.text = icon;
_iconLabel.style.display = DisplayStyle.Flex;
UTKMaterialIcons.ApplyIconStyle(_iconLabel, fontSize ?? GetEffectiveIconSize());
}
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();
}
/// <summary>
/// Material Icon을 비동기로 설정합니다.
/// </summary>
/// <param name="icon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings)</param>
/// <param name="ct">취소 토큰</param>
/// <param name="fontSize">아이콘 폰트 크기 (null이면 텍스트 크기에 맞춤)</param>
public async UniTask SetMaterialIconAsync(string icon, CancellationToken ct = default, int? fontSize = null)
{
ClearImageIcon();
EnsureIconLabel();
if (_iconLabel != null)
{
_iconLabel.text = icon;
_iconLabel.style.display = DisplayStyle.Flex;
await UTKMaterialIcons.ApplyIconStyleAsync(_iconLabel, ct, fontSize ?? GetEffectiveIconSize());
}
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();
}
/// <summary>
/// 아이콘 이름으로 Material Icon을 설정합니다.
/// </summary>
/// <param name="iconName">아이콘 이름 (예: "settings", "home")</param>
/// <param name="fontSize">아이콘 폰트 크기 (null이면 텍스트 크기에 맞춤)</param>
public void SetMaterialIconByName(string iconName, int? fontSize = null)
{
var iconChar = UTKMaterialIcons.GetIcon(iconName);
if (!string.IsNullOrEmpty(iconChar))
{
SetMaterialIcon(iconChar, fontSize);
}
else
{
Debug.LogWarning($"[UTKLabel] Material icon '{iconName}'을(를) 찾을 수 없습니다.");
}
}
/// <summary>
/// Image Icon을 설정합니다.
/// </summary>
/// <param name="resourcePath">이미지 리소스 경로 (예: UTKImageIcons.IconSetting22)</param>
/// <param name="iconSize">아이콘 크기 (null이면 텍스트 크기에 맞춤)</param>
public void SetImageIcon(string resourcePath, int? iconSize = null)
{
var texture = UTKImageIcons.LoadTexture(resourcePath);
if (texture != null)
{
ApplyImageIcon(texture, iconSize);
}
else
{
Debug.LogWarning($"[UTKLabel] Image icon '{resourcePath}'을(를) 로드할 수 없습니다.");
}
}
/// <summary>
/// Image Icon을 비동기로 설정합니다.
/// </summary>
/// <param name="resourcePath">이미지 리소스 경로 (예: UTKImageIcons.IconSetting22)</param>
/// <param name="ct">취소 토큰</param>
/// <param name="iconSize">아이콘 크기 (null이면 텍스트 크기에 맞춤)</param>
public async UniTask SetImageIconAsync(string resourcePath, CancellationToken ct = default, int? iconSize = null)
{
var texture = await UTKImageIcons.LoadTextureAsync(resourcePath, ct);
if (texture != null)
{
ApplyImageIcon(texture, iconSize);
}
else
{
Debug.LogWarning($"[UTKLabel] Image icon '{resourcePath}'을(를) 로드할 수 없습니다.");
}
}
/// <summary>
/// 아이콘 이름으로 Image Icon을 설정합니다.
/// </summary>
/// <param name="iconName">아이콘 이름 (예: "icon_setting_22", "btn_close_16")</param>
/// <param name="iconSize">아이콘 크기 (null이면 텍스트 크기에 맞춤)</param>
public void SetImageIconByName(string iconName, int? iconSize = null)
{
var texture = UTKImageIcons.LoadTextureByName(iconName);
if (texture != null)
{
ApplyImageIcon(texture, iconSize);
}
else
{
Debug.LogWarning($"[UTKLabel] Image icon '{iconName}'을(를) 찾을 수 없습니다.");
}
}
/// <summary>
/// 모든 아이콘을 제거합니다.
/// </summary>
public void ClearIcon()
{
ClearMaterialIcon();
ClearImageIcon();
EnableInClassList("utk-label--has-icon", false);
}
private void EnsureIconLabel()
{
if (_iconLabel != null) return;
_iconLabel = new Label
{
name = "icon",
pickingMode = PickingMode.Ignore
};
_iconLabel.AddToClassList("utk-label__icon");
_iconLabel.style.display = DisplayStyle.None;
Insert(0, _iconLabel);
}
private void ApplyImageIcon(Texture2D texture, int? iconSize)
{
ClearMaterialIcon();
if (_imageIcon == null)
{
_imageIcon = new VisualElement
{
name = "image-icon",
pickingMode = PickingMode.Ignore
};
_imageIcon.AddToClassList("utk-label__image-icon");
Insert(0, _imageIcon);
}
var size = iconSize ?? GetEffectiveIconSize();
_imageIcon.style.width = size;
_imageIcon.style.height = size;
_imageIcon.style.backgroundImage = new StyleBackground(texture);
_imageIcon.style.display = DisplayStyle.Flex;
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();
}
private void ClearMaterialIcon()
{
if (_iconLabel != null)
{
_iconLabel.text = "";
_iconLabel.style.display = DisplayStyle.None;
}
}
private void ClearImageIcon()
{
if (_imageIcon != null)
{
_imageIcon.style.backgroundImage = StyleKeyword.None;
_imageIcon.style.display = DisplayStyle.None;
}
}
#endregion
#region IDisposable

View File

@@ -1388,8 +1388,8 @@ namespace UVC.UIToolkit.List
// 검색어가 없으면 원본 데이터 복원
if (string.IsNullOrEmpty(query))
{
_treeView.SetRootItems<UTKComponentListItemDataBase>(_rootData);
_treeView.Rebuild();
_treeView?.SetRootItems<UTKComponentListItemDataBase>(_rootData);
_treeView?.Rebuild();
ExpandByData(_originalRoots);
return;
}
@@ -1399,8 +1399,8 @@ namespace UVC.UIToolkit.List
var filteredWrappers = FilterTree(qLower);
// 필터링된 결과로 TreeView 갱신
_treeView.SetRootItems<UTKComponentListItemDataBase>(filteredWrappers);
_treeView.Rebuild();
_treeView?.SetRootItems<UTKComponentListItemDataBase>(filteredWrappers);
_treeView?.Rebuild();
// 검색 결과 모두 펼치기
ExpandAll(filteredWrappers);

View File

@@ -10,6 +10,33 @@ namespace UVC.UIToolkit
/// 리스트 뷰 컴포넌트.
/// Unity ListView를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 리스트 뷰 생성
/// var listView = new UTKListView();
///
/// // 데이터 소스 설정
/// var items = new List<string> { "항목 1", "항목 2", "항목 3" };
/// listView.itemsSource = items;
///
/// // 아이템 렌더링
/// listView.makeItem = () => new Label();
/// listView.bindItem = (element, index) => {
/// (element as Label).text = items[index];
/// };
///
/// // 선택 이벤트
/// listView.OnItemSelected += (index) => Debug.Log($"선택: {items[index]}");
/// listView.OnItemDoubleClicked += (index) => Debug.Log($"더블클릭: {items[index]}");
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKListView fixed-item-height="30" selection-type="Single" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKListView : ListView, IDisposable
{

View File

@@ -10,6 +10,39 @@ namespace UVC.UIToolkit
/// 트리 뷰 컴포넌트.
/// Unity TreeView를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 트리 뷰 생성
/// var treeView = new UTKTreeView();
///
/// // 데이터 구조 설정
/// var rootItems = new List<TreeViewItemData<string>> {
/// new TreeViewItemData<string>(0, "루트 1", new List<TreeViewItemData<string>> {
/// new TreeViewItemData<string>(1, "자식 1-1"),
/// new TreeViewItemData<string>(2, "자식 1-2")
/// }),
/// new TreeViewItemData<string>(3, "루트 2")
/// };
/// treeView.SetRootItems(rootItems);
///
/// // 아이템 렌더링
/// treeView.makeItem = () => new Label();
/// treeView.bindItem = (element, index) => {
/// var item = treeView.GetItemDataForIndex<string>(index);
/// (element as Label).text = item;
/// };
///
/// // 선택 이벤트
/// treeView.OnItemSelected += (index) => Debug.Log($"선택: {index}");
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKTreeView fixed-item-height="24" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKTreeView : TreeView, IDisposable
{

View File

@@ -11,6 +11,34 @@ namespace UVC.UIToolkit
/// Alert 팝업 컴포넌트.
/// 사용자에게 중요한 정보를 알립니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 초기화 (root 설정 필요)
/// UTKAlert.Initialize(rootVisualElement);
///
/// // 기본 알림
/// await UTKAlert.Show("알림", "작업이 완료되었습니다.");
///
/// // 확인 대화상자
/// bool confirmed = await UTKAlert.ShowConfirm("삭제 확인", "정말 삭제하시겠습니까?");
/// if (confirmed) {
/// // 삭제 실행
/// }
///
/// // 타입별 알림
/// await UTKAlert.ShowSuccess("성공", "저장되었습니다.");
/// await UTKAlert.ShowError("오류", "파일을 찾을 수 없습니다.");
/// await UTKAlert.ShowWarning("경고", "변경사항이 저장되지 않았습니다.");
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- Alert는 주로 C# 코드로 동적 생성합니다 -->
/// <utk:UTKAlert Title="알림" Message="메시지" AlertType="Info" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKAlert : VisualElement, IDisposable
{

View File

@@ -4,6 +4,7 @@ using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit.Common;
namespace UVC.UIToolkit.Modal
{
@@ -217,6 +218,9 @@ namespace UVC.UIToolkit.Modal
picker._blocker = UTKModalBlocker.Show(parent, 0.5f, false);
picker._blocker.OnBlockerClicked += picker.Cancel;
// 위치 계산 전까지 숨김 (깜빡임 방지)
picker.style.visibility = Visibility.Hidden;
// 피커 추가
parent.Add(picker);
@@ -316,8 +320,9 @@ namespace UVC.UIToolkit.Modal
_titleLabel = new Label("Color Picker") { name = "title" };
_titleLabel.AddToClassList("utk-color-picker__title");
_closeButton = new Button { name = "close-btn", text = "\u2715" }; // ✕
_closeButton = new Button { name = "close-btn", text = UTKMaterialIcons.Close };
_closeButton.AddToClassList("utk-color-picker__close-btn");
UTKMaterialIcons.ApplyIconStyle(_closeButton, 18);
_header.Add(_titleLabel);
_header.Add(_closeButton);
@@ -453,6 +458,7 @@ namespace UVC.UIToolkit.Modal
_cancelButton ??= this.Q<UTKButton>("cancel-btn");
_confirmButton ??= this.Q<UTKButton>("confirm-btn");
_alphaRow ??= this.Q<VisualElement>("row-a");
_header ??= this.Q<VisualElement>("header");
}
private void SetupEvents()
@@ -1004,6 +1010,9 @@ namespace UVC.UIToolkit.Modal
private void CenterOnScreen()
{
// position absolute 강제 적용
style.position = Position.Absolute;
schedule.Execute(() =>
{
var parent = this.parent;
@@ -1014,8 +1023,20 @@ namespace UVC.UIToolkit.Modal
float selfWidth = resolvedStyle.width;
float selfHeight = resolvedStyle.height;
// NaN 체크
if (float.IsNaN(parentWidth) || float.IsNaN(parentHeight) ||
float.IsNaN(selfWidth) || float.IsNaN(selfHeight))
{
// 다음 프레임에 다시 시도
schedule.Execute(() => CenterOnScreen());
return;
}
style.left = (parentWidth - selfWidth) / 2;
style.top = (parentHeight - selfHeight) / 2;
// 위치 계산 완료 후 표시 (깜빡임 방지)
style.visibility = Visibility.Visible;
});
}

View File

@@ -5,6 +5,7 @@ using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Locale;
using UVC.UIToolkit.Common;
using UVC.UIToolkit.Input;
namespace UVC.UIToolkit.Modal
@@ -58,6 +59,16 @@ namespace UVC.UIToolkit.Modal
/// Debug.Log("Cancelled");
/// }
///
/// // 요일 이름 커스터마이징 (static - 모든 인스턴스에 적용)
/// UTKDatePicker.SetDayNames(new[] { "일", "월", "화", "수", "목", "금", "토" });
/// UTKDatePicker.SetDayNames(new[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" });
///
/// // 요일 이름 로컬라이제이션 키 설정
/// UTKDatePicker.SetDayNameKeys(new[] { "day_sun", "day_mon", "day_tue", "day_wed", "day_thu", "day_fri", "day_sat" });
///
/// // 요일 이름 기본값으로 초기화
/// UTKDatePicker.ResetDayNames();
///
/// // 인스턴스 직접 생성
/// var datePicker = new UTKDatePicker();
/// datePicker.SetDate(DateTime.Today);
@@ -90,7 +101,12 @@ namespace UVC.UIToolkit.Modal
private const string UXML_PATH = "UIToolkit/Modal/UTKDatePicker";
private const string USS_PATH = "UIToolkit/Modal/UTKDatePicker";
private const int DAYS_IN_GRID = 42; // 6 rows x 7 columns
private static readonly string[] DAY_NAME_KEYS = { "day_sun", "day_mon", "day_tue", "day_wed", "day_thu", "day_fri", "day_sat" };
private static readonly string[] DEFAULT_DAY_NAME_KEYS = { "day_sun", "day_mon", "day_tue", "day_wed", "day_thu", "day_fri", "day_sat" };
#endregion
#region Static Fields
private static string[] s_dayNameKeys = DEFAULT_DAY_NAME_KEYS;
private static string[]? s_customDayNames;
#endregion
#region Fields
@@ -124,6 +140,12 @@ namespace UVC.UIToolkit.Modal
private UTKNumberStepper? _minuteStepper;
private UTKButton? _cancelButton;
private UTKButton? _confirmButton;
private VisualElement? _header;
// 드래그 관련 필드
private bool _isDragging;
private Vector2 _dragStartPosition;
private Vector2 _dragStartMousePosition;
#endregion
#region Events
@@ -204,6 +226,9 @@ namespace UVC.UIToolkit.Modal
picker._blocker = UTKModalBlocker.Show(parent, 0.5f, false);
picker._blocker.OnBlockerClicked += picker.Close;
// 위치 계산 전까지 숨김 (깜빡임 방지)
picker.style.visibility = Visibility.Hidden;
// 피커 추가
parent.Add(picker);
@@ -302,6 +327,9 @@ namespace UVC.UIToolkit.Modal
picker._blocker = UTKModalBlocker.Show(parent, 0.5f, false);
picker._blocker.OnBlockerClicked += picker.Close;
// 위치 계산 전까지 숨김 (깜빡임 방지)
picker.style.visibility = Visibility.Hidden;
// 피커 추가
parent.Add(picker);
@@ -399,6 +427,60 @@ namespace UVC.UIToolkit.Modal
public void PreviousMonth() => NavigateMonth(-1);
public void NextMonth() => NavigateMonth(1);
/// <summary>
/// 요일 이름을 직접 설정합니다. (일, 월, 화, 수, 목, 금, 토 순서로 7개)
/// 설정 후 새로 생성되는 피커부터 적용됩니다.
/// </summary>
/// <param name="dayNames">요일 이름 배열 (7개)</param>
/// <example>
/// <code>
/// UTKDatePicker.SetDayNames(new[] { "일", "월", "화", "수", "목", "금", "토" });
/// UTKDatePicker.SetDayNames(new[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" });
/// </code>
/// </example>
public static void SetDayNames(string[] dayNames)
{
if (dayNames == null || dayNames.Length != 7)
{
Debug.LogWarning("SetDayNames requires exactly 7 day names (Sun, Mon, Tue, Wed, Thu, Fri, Sat)");
return;
}
s_customDayNames = dayNames;
}
/// <summary>
/// 요일 이름의 로컬라이제이션 키를 설정합니다. (일, 월, 화, 수, 목, 금, 토 순서로 7개)
/// 설정 후 새로 생성되는 피커부터 적용됩니다.
/// </summary>
/// <param name="dayNameKeys">로컬라이제이션 키 배열 (7개)</param>
/// <example>
/// <code>
/// UTKDatePicker.SetDayNameKeys(new[] { "day_sun", "day_mon", "day_tue", "day_wed", "day_thu", "day_fri", "day_sat" });
/// </code>
/// </example>
public static void SetDayNameKeys(string[] dayNameKeys)
{
if (dayNameKeys == null || dayNameKeys.Length != 7)
{
Debug.LogWarning("SetDayNameKeys requires exactly 7 localization keys");
return;
}
s_customDayNames = null; // 커스텀 이름 초기화
s_dayNameKeys = dayNameKeys;
}
/// <summary>
/// 요일 이름을 기본값(로컬라이제이션)으로 초기화합니다.
/// 설정 후 새로 생성되는 피커부터 적용됩니다.
/// </summary>
public static void ResetDayNames()
{
s_customDayNames = null;
s_dayNameKeys = DEFAULT_DAY_NAME_KEYS;
}
public void Close()
{
OnClosed?.Invoke();
@@ -420,8 +502,9 @@ namespace UVC.UIToolkit.Modal
_titleLabel = new Label("Select Date") { name = "title" };
_titleLabel.AddToClassList("utk-date-picker__title");
_closeButton = new Button { name = "close-btn", text = "\u2715" }; // X
_closeButton = new Button { name = "close-btn", text = UTKMaterialIcons.Close };
_closeButton.AddToClassList("utk-date-picker__close-btn");
UTKMaterialIcons.ApplyIconStyle(_closeButton, UTKStyleGuide.FontSizeBody2);
header.Add(_titleLabel);
header.Add(_closeButton);
@@ -434,10 +517,12 @@ namespace UVC.UIToolkit.Modal
var leftNav = new VisualElement();
leftNav.AddToClassList("utk-date-picker__nav-group");
_prevYearButton = CreateNavButton("prev-year", "\u00AB"); // <<
_prevYearButton = CreateNavButton("prev-year", UTKMaterialIcons.FirstPage);
_prevYearButton.AddToClassList("utk-date-picker__nav-btn--prev-year");
UTKMaterialIcons.ApplyIconStyle(_prevYearButton, 18);
_prevMonthButton = CreateNavButton("prev-month", "\u2039"); // <
_prevMonthButton = CreateNavButton("prev-month", UTKMaterialIcons.ChevronLeft);
UTKMaterialIcons.ApplyIconStyle(_prevMonthButton, 18);
leftNav.Add(_prevYearButton);
leftNav.Add(_prevMonthButton);
@@ -448,10 +533,12 @@ namespace UVC.UIToolkit.Modal
var rightNav = new VisualElement();
rightNav.AddToClassList("utk-date-picker__nav-group");
_nextMonthButton = CreateNavButton("next-month", "\u203A"); // >
_nextMonthButton = CreateNavButton("next-month", UTKMaterialIcons.ChevronRight);
UTKMaterialIcons.ApplyIconStyle(_nextMonthButton, 18);
_nextYearButton = CreateNavButton("next-year", "\u00BB"); // >>
_nextYearButton = CreateNavButton("next-year", UTKMaterialIcons.LastPage);
_nextYearButton.AddToClassList("utk-date-picker__nav-btn--next-year");
UTKMaterialIcons.ApplyIconStyle(_nextYearButton, 18);
rightNav.Add(_nextMonthButton);
rightNav.Add(_nextYearButton);
@@ -465,9 +552,9 @@ namespace UVC.UIToolkit.Modal
_dayNamesRow = new VisualElement { name = "day-names" };
_dayNamesRow.AddToClassList("utk-date-picker__day-names");
for (int i = 0; i < DAY_NAME_KEYS.Length; i++)
for (int i = 0; i < s_dayNameKeys.Length; i++)
{
var dayText = LocalizationManager.Instance.GetString(DAY_NAME_KEYS[i]);
var dayText = GetDayName(i);
var label = new Label(dayText) { name = $"day-name-{i}" };
label.AddToClassList("utk-date-picker__day-name");
@@ -503,7 +590,7 @@ namespace UVC.UIToolkit.Modal
_timeRow = new VisualElement { name = "time-row" };
_timeRow.AddToClassList("utk-date-picker__time-row");
var timeLabel = new Label("시간:");
var timeLabel = new Label("Time:");
timeLabel.AddToClassList("utk-date-picker__time-label");
_hourStepper = new UTKNumberStepper(0, 23, 0, 1) { name = "hour-stepper" };
@@ -557,6 +644,7 @@ namespace UVC.UIToolkit.Modal
private void QueryElements()
{
_header ??= this.Q<VisualElement>("header");
_titleLabel ??= this.Q<Label>("title");
_closeButton ??= this.Q<Button>("close-btn");
_yearMonthLabel ??= this.Q<Label>("year-month");
@@ -599,10 +687,43 @@ namespace UVC.UIToolkit.Modal
_nextMonthButton?.RegisterCallback<ClickEvent>(_ => NextMonth());
_nextYearButton?.RegisterCallback<ClickEvent>(_ => NextYear());
// 헤더 드래그 이벤트
_header?.RegisterCallback<PointerDownEvent>(OnHeaderPointerDown);
_header?.RegisterCallback<PointerMoveEvent>(OnHeaderPointerMove);
_header?.RegisterCallback<PointerUpEvent>(OnHeaderPointerUp);
// 언어 변경 이벤트 구독
LocalizationManager.Instance.OnLanguageChanged += OnLanguageChanged;
}
private void OnHeaderPointerDown(PointerDownEvent evt)
{
if (evt.target == _closeButton) return;
_isDragging = true;
_dragStartMousePosition = evt.position;
_dragStartPosition = new Vector2(resolvedStyle.left, resolvedStyle.top);
_header?.CapturePointer(evt.pointerId);
evt.StopPropagation();
}
private void OnHeaderPointerMove(PointerMoveEvent evt)
{
if (!_isDragging) return;
Vector2 delta = (Vector2)evt.position - _dragStartMousePosition;
style.left = _dragStartPosition.x + delta.x;
style.top = _dragStartPosition.y + delta.y;
}
private void OnHeaderPointerUp(PointerUpEvent evt)
{
if (!_isDragging) return;
_isDragging = false;
_header?.ReleasePointer(evt.pointerId);
}
private void OnLanguageChanged(string newLanguage)
{
UpdateDayNameLabels();
@@ -612,15 +733,28 @@ namespace UVC.UIToolkit.Modal
{
if (_dayNamesRow == null) return;
for (int i = 0; i < DAY_NAME_KEYS.Length; i++)
for (int i = 0; i < s_dayNameKeys.Length; i++)
{
var label = _dayNamesRow.Q<Label>($"day-name-{i}");
if (label != null)
{
label.text = LocalizationManager.Instance.GetString(DAY_NAME_KEYS[i]);
label.text = GetDayName(i);
}
}
}
/// <summary>
/// 인덱스에 해당하는 요일 이름을 반환합니다.
/// 커스텀 이름이 설정되어 있으면 커스텀 이름을, 아니면 로컬라이제이션 키를 사용합니다.
/// </summary>
private string GetDayName(int index)
{
if (s_customDayNames != null && index < s_customDayNames.Length)
{
return s_customDayNames[index];
}
return LocalizationManager.Instance.GetString(s_dayNameKeys[index]);
}
#endregion
#region Private Methods - Logic
@@ -860,6 +994,9 @@ namespace UVC.UIToolkit.Modal
private void CenterOnScreen()
{
// position absolute 강제 적용
style.position = Position.Absolute;
schedule.Execute(() =>
{
var parent = this.parent;
@@ -870,8 +1007,20 @@ namespace UVC.UIToolkit.Modal
float selfWidth = resolvedStyle.width;
float selfHeight = resolvedStyle.height;
// NaN 체크
if (float.IsNaN(parentWidth) || float.IsNaN(parentHeight) ||
float.IsNaN(selfWidth) || float.IsNaN(selfHeight))
{
// 다음 프레임에 다시 시도
schedule.Execute(() => CenterOnScreen());
return;
}
style.left = (parentWidth - selfWidth) / 2;
style.top = (parentHeight - selfHeight) / 2;
// 위치 계산 완료 후 표시 (깜빡임 방지)
style.visibility = Visibility.Visible;
});
}
@@ -890,10 +1039,10 @@ namespace UVC.UIToolkit.Modal
string startText = _rangeStartDate?.ToString("yyyy-MM-dd") ?? "---";
string endText = _rangeEndDate?.ToString("yyyy-MM-dd") ?? "---";
string stateIndicator = _rangeState == RangeSelectionState.SelectingStart ? "" : " ";
string endStateIndicator = _rangeState == RangeSelectionState.SelectingEnd ? "" : " ";
string stateIndicator = _rangeState == RangeSelectionState.SelectingStart ? "" : " ";
string endStateIndicator = _rangeState == RangeSelectionState.SelectingEnd ? "" : " ";
_rangeInfoLabel.text = $"{stateIndicator}시작: {startText} {endStateIndicator}종료: {endText}";
_rangeInfoLabel.text = $"{stateIndicator}Start: {startText} {endStateIndicator}End: {endText}";
}
/// <summary>

View File

@@ -10,6 +10,35 @@ namespace UVC.UIToolkit
/// 모달 창 컴포넌트.
/// 사용자 정의 콘텐츠를 포함할 수 있는 모달 대화상자입니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 모달 생성 및 표시
/// var modal = new UTKModal();
/// modal.Title = "설정";
/// modal.Size = UTKModal.ModalSize.Medium;
/// modal.OnClosed += () => Debug.Log("모달 닫힘");
///
/// // 콘텐츠 추가
/// var content = new Label("모달 내용");
/// modal.AddContent(content);
///
/// // 푸터 버튼 추가
/// modal.AddFooterButton("확인", UTKButton.ButtonVariant.Primary, () => modal.Close());
/// modal.AddFooterButton("취소", UTKButton.ButtonVariant.Normal, () => modal.Close());
///
/// // 표시
/// modal.Show(rootElement);
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKModal Title="설정" Size="Medium" ShowCloseButton="true">
/// <ui:Label text="모달 내용을 여기에 추가하세요" />
/// </utk:UTKModal>
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKModal : VisualElement, IDisposable
{

View File

@@ -3,6 +3,7 @@ using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit.Common;
namespace UVC.UIToolkit
{
@@ -10,6 +11,37 @@ namespace UVC.UIToolkit
/// 알림 창 컴포넌트.
/// 화면 모서리에 표시되는 알림 메시지입니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 초기화 (root 설정 필요)
/// UTKNotification.Initialize(rootVisualElement);
///
/// // 기본 알림
/// UTKNotification.Show("알림", "새로운 메시지가 있습니다.");
///
/// // 타입별 알림
/// UTKNotification.ShowSuccess("성공", "저장되었습니다.");
/// UTKNotification.ShowError("오류", "실패했습니다.");
/// UTKNotification.ShowWarning("경고", "주의가 필요합니다.");
///
/// // 위치 설정
/// UTKNotification.Show("알림", "메시지", position: NotificationPosition.BottomRight);
///
/// // 액션 버튼 있는 알림
/// UTKNotification.Show("알림", "메시지", actions: new[] {
/// ("확인", () => Debug.Log("확인")),
/// ("취소", () => Debug.Log("취소"))
/// });
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- Notification은 주로 C# 코드로 동적 생성합니다 -->
/// <utk:UTKNotification Title="알림" Message="메시지" Type="Info" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKNotification : VisualElement, IDisposable
{
@@ -129,6 +161,14 @@ namespace UVC.UIToolkit
CreateUI();
SubscribeToThemeChanges();
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
RegisterCallback<AttachToPanelEvent>(_ =>
{
if (_titleLabel != null) _titleLabel.text = _title;
if (_messageLabel != null) _messageLabel.text = _message;
UpdateType();
});
}
public UTKNotification(string title, string message, NotificationType type = NotificationType.Info) : this()
@@ -210,14 +250,16 @@ namespace UVC.UIToolkit
_iconLabel = new Label { name = "icon" };
_iconLabel.AddToClassList("utk-notification__icon");
UTKMaterialIcons.ApplyIconStyle(_iconLabel, 20);
_header.Add(_iconLabel);
_titleLabel = new Label { name = "title" };
_titleLabel.AddToClassList("utk-notification__title");
_header.Add(_titleLabel);
_closeButton = new Button { name = "close-btn", text = "✕" };
_closeButton = new Button { name = "close-btn", text = UTKMaterialIcons.Close };
_closeButton.AddToClassList("utk-notification__close-btn");
UTKMaterialIcons.ApplyIconStyle(_closeButton, 16);
_closeButton.RegisterCallback<ClickEvent>(_ => Close());
_header.Add(_closeButton);
@@ -270,10 +312,10 @@ namespace UVC.UIToolkit
{
_iconLabel.text = _type switch
{
NotificationType.Success => "✓",
NotificationType.Warning => "⚠",
NotificationType.Error => "✕",
_ => ""
NotificationType.Success => UTKMaterialIcons.CheckCircle,
NotificationType.Warning => UTKMaterialIcons.Warning,
NotificationType.Error => UTKMaterialIcons.Error,
_ => UTKMaterialIcons.Info
};
}
}

View File

@@ -9,6 +9,46 @@ namespace UVC.UIToolkit
/// 패널 컨테이너 컴포넌트.
/// 콘텐츠를 그룹화하고 시각적으로 구분합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 패널
/// var panel = new UTKPanel();
/// panel.Title = "설정";
/// panel.AddContent(new Label("패널 내용"));
///
/// // 접을 수 있는 패널
/// panel.IsCollapsible = true;
/// panel.IsCollapsed = false;
///
/// // 헤더 액션 버튼 추가
/// panel.AddHeaderAction(UTKMaterialIcons.Settings, () => Debug.Log("설정"));
///
/// // 푸터 표시
/// panel.ShowFooter = true;
/// panel.AddFooterContent(new Label("푸터 내용"));
///
/// // 변형 스타일
/// panel.Variant = UTKPanel.PanelVariant.Elevated;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 패널 -->
/// <utk:UTKPanel Title="제목">
/// <ui:Label text="패널 내용" />
/// </utk:UTKPanel>
///
/// <!-- 접을 수 있는 패널 -->
/// <utk:UTKPanel Title="고급 설정" IsCollapsible="true">
/// <ui:Label text="내용" />
/// </utk:UTKPanel>
///
/// <!-- 외곽선 스타일 -->
/// <utk:UTKPanel Title="외곽선" Variant="Outlined" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKPanel : VisualElement, IDisposable
{

View File

@@ -10,6 +10,35 @@ namespace UVC.UIToolkit
/// 토스트 알림 컴포넌트.
/// 화면 하단에 일시적으로 표시되는 알림입니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 초기화 (root 설정 필요)
/// UTKToast.Initialize(rootVisualElement);
///
/// // 기본 토스트
/// UTKToast.Show("저장되었습니다.");
///
/// // 타입별 토스트
/// UTKToast.ShowSuccess("성공적으로 완료되었습니다.");
/// UTKToast.ShowError("오류가 발생했습니다.");
/// UTKToast.ShowWarning("주의가 필요합니다.");
/// UTKToast.ShowInfo("정보 메시지");
///
/// // 지속시간 설정 (ms)
/// UTKToast.Show("잠시 표시", duration: 2000);
///
/// // 닫기 버튼 표시
/// UTKToast.Show("메시지", showCloseButton: true);
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 토스트는 주로 C# 코드로 동적 생성합니다 -->
/// <utk:UTKToast Message="저장됨" Type="Success" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKToast : VisualElement, IDisposable
{

View File

@@ -9,6 +9,29 @@ namespace UVC.UIToolkit
/// 최소-최대 범위 슬라이더 컴포넌트.
/// Unity MinMaxSlider를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 범위 슬라이더
/// var slider = new UTKMinMaxSlider();
/// slider.label = "가격 범위";
/// slider.lowLimit = 0;
/// slider.highLimit = 1000;
/// slider.MinValue = 100;
/// slider.MaxValue = 500;
///
/// // 값 변경 이벤트
/// slider.OnValueChanged += (range) => {
/// Debug.Log($"범위: {range.x} ~ {range.y}");
/// };
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKMinMaxSlider label="범위" low-limit="0" high-limit="100" min-value="20" max-value="80" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKMinMaxSlider : MinMaxSlider, IDisposable
{

View File

@@ -9,6 +9,40 @@ namespace UVC.UIToolkit
/// 프로그레스 바 컴포넌트.
/// Unity ProgressBar를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 프로그레스 바
/// var progressBar = new UTKProgressBar();
/// progressBar.title = "다운로드 중...";
/// progressBar.MinValue = 0;
/// progressBar.MaxValue = 100;
/// progressBar.Value = 50;
///
/// // 값 표시 설정
/// progressBar.ShowValue = true;
/// progressBar.ShowPercentage = true;
///
/// // 무한 로딩 (불확정 상태)
/// progressBar.IsIndeterminate = true;
///
/// // 변형 스타일
/// progressBar.Variant = UTKProgressBar.ProgressBarVariant.Success;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 프로그레스 바 -->
/// <utk:UTKProgressBar title="진행률" low-value="0" high-value="100" value="30" />
///
/// <!-- 퍼센트 표시 -->
/// <utk:UTKProgressBar ShowPercentage="true" value="75" />
///
/// <!-- 성공 스타일 -->
/// <utk:UTKProgressBar Variant="Success" value="100" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKProgressBar : ProgressBar, IDisposable
{

View File

@@ -9,6 +9,31 @@ namespace UVC.UIToolkit
/// 슬라이더 컴포넌트.
/// Unity Slider를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 슬라이더
/// var slider = new UTKSlider();
/// slider.label = "볼륨";
/// slider.lowValue = 0;
/// slider.highValue = 100;
/// slider.value = 50;
/// slider.OnValueChanged += (value) => Debug.Log($"볼륨: {value}");
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 슬라이더 -->
/// <utk:UTKSlider label="밝기" low-value="0" high-value="100" value="50" />
///
/// <!-- 값 표시 -->
/// <utk:UTKSlider label="투명도" low-value="0" high-value="1" value="0.5" show-input-field="true" />
///
/// <!-- 비활성화 -->
/// <utk:UTKSlider label="잠금" IsEnabled="false" value="75" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKSlider : Slider, IDisposable
{

View File

@@ -9,6 +9,36 @@ namespace UVC.UIToolkit
/// 탭 컴포넌트.
/// Unity Tab을 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // UTKTabView와 함께 사용
/// var tabView = new UTKTabView();
///
/// var tab1 = new UTKTab();
/// tab1.Text = "일반";
/// tab1.Add(new Label("일반 탭 내용"));
/// tabView.Add(tab1);
///
/// var tab2 = new UTKTab();
/// tab2.Text = "고급";
/// tab2.IsEnabled = false; // 비활성화
/// tabView.Add(tab2);
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKTabView>
/// <utk:UTKTab label="일반">
/// <ui:Label text="일반 내용" />
/// </utk:UTKTab>
/// <utk:UTKTab label="고급">
/// <ui:Label text="고급 내용" />
/// </utk:UTKTab>
/// </utk:UTKTabView>
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKTab : Tab, IDisposable
{

View File

@@ -10,6 +10,39 @@ namespace UVC.UIToolkit
/// 탭 뷰 컴포넌트.
/// Unity TabView를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 탭 뷰 생성
/// var tabView = new UTKTabView();
///
/// // 탭 추가
/// var tab1 = tabView.AddTab("일반", UTKMaterialIcons.Settings);
/// tab1.Add(new Label("일반 설정 내용"));
///
/// var tab2 = tabView.AddTab("고급", UTKMaterialIcons.Build);
/// tab2.Add(new Label("고급 설정 내용"));
///
/// // 탭 변경 이벤트
/// tabView.OnTabChanged += (index, tab) => Debug.Log($"탭 {index} 선택됨");
///
/// // 탭 선택
/// tabView.SelectedIndex = 0;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <utk:UTKTabView>
/// <utk:UTKTab label="일반">
/// <ui:Label text="일반 탭 내용" />
/// </utk:UTKTab>
/// <utk:UTKTab label="고급">
/// <ui:Label text="고급 탭 내용" />
/// </utk:UTKTab>
/// </utk:UTKTabView>
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKTabView : TabView, IDisposable
{

View File

@@ -233,7 +233,24 @@ UI Toolkit 개발 시 다음 위치의 스타일 리소스를 참조하세요:
---
## 8) 주석 원칙 (C# XML)
## 8) 아이콘 사용 가이드 (Icons)
### 아이콘 사용 우선순위
UI 아이콘 적용 시 다음 순서를 반드시 준수해야 합니다.
1. **1순위 (Material Icons)**: `UTKMaterialIcons` 클래스 확인
- 폰트 기반 아이콘(Unicode)을 우선 사용합니다.
- 예: `UTKButton.SetMaterialIcon(UTKMaterialIcons.Home)`
2. **2순위 (Image Icons)**: `UTKImageIcons` 클래스 사용
- 필요한 아이콘이 `UTKMaterialIcons`에 없는 경우에만 `UTKImageIcons`를 사용합니다.
- 예: `UTKButton.SetImageIcon(UTKImageIcons.CustomIcon)`
> **권장**: 일관된 UI 스타일과 메모리 효율을 위해 가능한 Material Icons 사용을 권장합니다.
---
## 9) 주석 원칙 (C# XML)
```csharp
/// <summary>
@@ -251,7 +268,7 @@ public async UniTask<UserData?> LoadUserAsync(string userId, CancellationToken c
---
## 9) 디자인 패턴 요약
## 10) 디자인 패턴 요약
| 패턴 | 사용 시점 |
|------|-----------|
@@ -266,7 +283,7 @@ public async UniTask<UserData?> LoadUserAsync(string userId, CancellationToken c
---
## 10) View 기본 패턴
## 11) View 기본 패턴
```csharp
#nullable enable
@@ -307,7 +324,7 @@ public abstract class UIViewBase : IDisposable
---
## 11) Unity Nullable 주의
## 12) Unity Nullable 주의
```csharp
// Unity Object는 == null 오버로드됨
@@ -321,7 +338,7 @@ if (ReferenceEquals(obj, null)) { }
---
## 12) 품질 자동화
## 13) 품질 자동화
### .editorconfig 권장
```ini