기본 Styleguide 완료

This commit is contained in:
logonkhi
2026-01-21 20:43:54 +09:00
parent fd8f8c6de0
commit e00953de52
122 changed files with 3310 additions and 709 deletions

View File

@@ -0,0 +1,323 @@
#nullable enable
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 코드 샘플을 표시하는 컴포넌트.
/// 텍스트 선택 및 복사가 가능하며, 복사 버튼을 제공합니다.
/// </summary>
/// <remarks>
/// <para><b>CodeBlock(코드 블록)이란?</b></para>
/// <para>
/// CodeBlock은 코드 샘플을 읽기 쉽게 표시하는 컴포넌트입니다.
/// 고정폭 폰트, 배경색, 복사 버튼 등으로 코드를 강조하여 표시합니다.
/// 문서, 튜토리얼, 샘플 앱 등에서 코드 예제를 보여줄 때 사용합니다.
/// </para>
///
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item><description>고정폭 폰트로 코드 가독성 향상</description></item>
/// <item><description>텍스트 선택 및 복사 가능</description></item>
/// <item><description>복사 버튼으로 원클릭 복사</description></item>
/// <item><description>제목 표시 (예: "C#", "UXML", "JSON")</description></item>
/// </list>
///
/// <para><b>UXML에서 코드 설정 방법:</b></para>
/// <list type="number">
/// <item><description>code 속성 사용: <c>code="var x = 1;\nvar y = 2;"</c> (줄바꿈은 \n)</description></item>
/// <item><description>텍스트 콘텐츠 사용: 태그 사이에 직접 코드 작성 (줄바꿈 유지)</description></item>
/// </list>
///
/// <para><b>실제 활용 예시:</b></para>
/// <list type="bullet">
/// <item><description>API 문서 - 함수 사용법 예제</description></item>
/// <item><description>튜토리얼 - 단계별 코드 설명</description></item>
/// <item><description>StyleGuide 샘플 - C#/UXML 코드 예시</description></item>
/// <item><description>오류 로그 - 스택 트레이스 표시</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>UXML에서 사용 (code 속성):</b></para>
/// <code>
/// <utk:UTKCodeBlock title="C#" code="var x = 1;&#10;var y = 2;" />
/// </code>
/// <para><b>UXML에서 사용 (텍스트 콘텐츠):</b></para>
/// <code>
/// <utk:UTKCodeBlock title="C#">
/// var x = 1;
/// var y = 2;
/// </utk:UTKCodeBlock>
/// </code>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// var codeBlock = new UTKCodeBlock
/// {
/// Title = "C#",
/// Code = "var player = new Player();\nplayer.Move(10, 0);"
/// };
///
/// // 여러 코드 블록 컨테이너 생성
/// var container = UTKCodeBlock.CreateCodeSampleContainer(
/// csharpCode: "var btn = new UTKButton();",
/// uxmlCode: "<utk:UTKButton text=\"Click\" />"
/// );
/// </code>
/// </example>
[UxmlElement]
public partial class UTKCodeBlock : VisualElement
{
private static class UssClasses
{
public const string Root = "utk-code-block";
public const string Header = "utk-code-block__header";
public const string Title = "utk-code-block__title";
public const string CopyButton = "utk-code-block__copy-btn";
public const string Content = "utk-code-block__content";
public const string Code = "utk-code-block__code";
}
private Label? _titleLabel;
private UTKButton? _copyButton;
private TextField? _codeField;
private string _title = "";
private string _code = "";
private bool _codeSetFromAttribute;
/// <summary>
/// 코드 블록 제목 (예: "C# Code", "UXML")
/// </summary>
[UxmlAttribute("title")]
public string Title
{
get => _title;
set
{
_title = value;
if (_titleLabel != null)
{
_titleLabel.text = value;
_titleLabel.style.display = string.IsNullOrEmpty(value) ? DisplayStyle.None : DisplayStyle.Flex;
}
}
}
/// <summary>
/// 표시할 코드
/// UXML에서 줄바꿈은 \n 문자열로 표현하거나, 텍스트 콘텐츠로 직접 입력할 수 있습니다.
/// </summary>
[UxmlAttribute("code")]
public string Code
{
get => _code;
set
{
// UXML에서 "\n" 문자열 리터럴이 들어올 수 있으므로 실제 줄바꿈으로 변환
_code = (value ?? "").Replace("\\n", "\n");
_codeSetFromAttribute = !string.IsNullOrEmpty(_code);
if (_codeField != null)
{
_codeField.value = _code;
UpdateCodeFieldHeight();
}
}
}
/// <summary>
/// 텍스트 콘텐츠로 코드를 설정합니다.
/// code 속성이 설정되지 않은 경우에만 적용됩니다.
/// </summary>
public void SetCodeFromContent(string content)
{
if (_codeSetFromAttribute) return;
// 앞뒤 빈 줄 제거하고 공통 들여쓰기 제거
_code = TrimAndNormalizeIndent(content);
if (_codeField != null)
{
_codeField.value = _code;
UpdateCodeFieldHeight();
}
}
/// <summary>
/// 텍스트의 앞뒤 빈 줄을 제거하고 공통 들여쓰기를 정규화합니다.
/// </summary>
private static string TrimAndNormalizeIndent(string text)
{
if (string.IsNullOrEmpty(text)) return "";
var lines = text.Split('\n');
// 앞뒤 빈 줄 제거
int startIndex = 0;
int endIndex = lines.Length - 1;
while (startIndex <= endIndex && string.IsNullOrWhiteSpace(lines[startIndex]))
startIndex++;
while (endIndex >= startIndex && string.IsNullOrWhiteSpace(lines[endIndex]))
endIndex--;
if (startIndex > endIndex) return "";
// 공통 들여쓰기 찾기
int minIndent = int.MaxValue;
for (int i = startIndex; i <= endIndex; i++)
{
var line = lines[i];
if (string.IsNullOrWhiteSpace(line)) continue;
int indent = 0;
foreach (char c in line)
{
if (c == ' ') indent++;
else if (c == '\t') indent += 4;
else break;
}
minIndent = Mathf.Min(minIndent, indent);
}
if (minIndent == int.MaxValue) minIndent = 0;
// 공통 들여쓰기 제거하고 결과 생성
var result = new System.Text.StringBuilder();
for (int i = startIndex; i <= endIndex; i++)
{
var line = lines[i];
if (string.IsNullOrWhiteSpace(line))
{
result.AppendLine();
}
else
{
int removed = 0;
int charIndex = 0;
while (removed < minIndent && charIndex < line.Length)
{
if (line[charIndex] == ' ') { removed++; charIndex++; }
else if (line[charIndex] == '\t') { removed += 4; charIndex++; }
else break;
}
result.AppendLine(line[charIndex..]);
}
}
return result.ToString().TrimEnd('\r', '\n');
}
private void UpdateCodeFieldHeight()
{
if (_codeField == null) return;
// 줄 수에 따라 높이 조정
var lineCount = string.IsNullOrEmpty(_code) ? 1 : _code.Split('\n').Length;
var height = Mathf.Max(40, lineCount * 18 + 24);
_codeField.style.height = height;
}
public UTKCodeBlock()
{
AddToClassList(UssClasses.Root);
// Header
var header = new VisualElement();
header.AddToClassList(UssClasses.Header);
Add(header);
_titleLabel = new Label();
_titleLabel.AddToClassList(UssClasses.Title);
header.Add(_titleLabel);
_copyButton = new UTKButton("Copy", UTKMaterialIcons.ContentCopy, UTKButton.ButtonVariant.Text, 12);
_copyButton.AddToClassList(UssClasses.CopyButton);
_copyButton.OnClicked += OnCopyClicked;
header.Add(_copyButton);
// Content
var content = new VisualElement();
content.AddToClassList(UssClasses.Content);
Add(content);
// TextField를 사용하여 텍스트 선택 가능하게
_codeField = new TextField();
_codeField.AddToClassList(UssClasses.Code);
_codeField.multiline = true;
_codeField.isReadOnly = true;
content.Add(_codeField);
// 초기값 적용 (UXML에서 속성이 먼저 설정된 경우)
if (!string.IsNullOrEmpty(_title))
{
_titleLabel.text = _title;
}
if (!string.IsNullOrEmpty(_code))
{
_codeField.value = _code;
UpdateCodeFieldHeight();
}
// UXML 텍스트 콘텐츠 처리를 위한 콜백
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
}
private void OnAttachToPanel(AttachToPanelEvent evt)
{
// UXML에서 텍스트 콘텐츠가 있으면 처리
// Unity는 텍스트 콘텐츠를 Label 자식으로 추가하므로 확인
schedule.Execute(() =>
{
if (_codeSetFromAttribute) return;
// 자식 Label에서 텍스트 추출 시도
var textLabel = this.Q<Label>(className: null);
if (textLabel != null && textLabel != _titleLabel && !string.IsNullOrEmpty(textLabel.text))
{
SetCodeFromContent(textLabel.text);
textLabel.RemoveFromHierarchy();
}
});
}
private void OnCopyClicked()
{
if (!string.IsNullOrEmpty(_code))
{
GUIUtility.systemCopyBuffer = _code;
UTKToast.Show("코드가 클립보드에 복사되었습니다.");
}
}
/// <summary>
/// C# 코드와 UXML 코드를 함께 표시하는 컨테이너를 생성합니다.
/// </summary>
public static VisualElement CreateCodeSampleContainer(string? csharpCode, string? uxmlCode)
{
var container = new VisualElement();
container.AddToClassList("utk-code-sample-container");
if (!string.IsNullOrEmpty(csharpCode))
{
var csharpBlock = new UTKCodeBlock
{
Title = "C#",
Code = csharpCode
};
container.Add(csharpBlock);
}
if (!string.IsNullOrEmpty(uxmlCode))
{
var uxmlBlock = new UTKCodeBlock
{
Title = "UXML",
Code = uxmlCode
};
container.Add(uxmlBlock);
}
return container;
}
}
}

View File

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

View File

@@ -8,7 +8,36 @@ namespace UVC.UIToolkit
/// <summary>
/// 접을 수 있는 섹션 컴포넌트.
/// Unity Foldout을 래핑하여 커스텀 스타일을 적용합니다.
/// 헤더 클릭으로 내용을 펼치거나 접을 수 있습니다.
/// </summary>
/// <remarks>
/// <para><b>Foldout(폴드아웃)이란?</b></para>
/// <para>
/// Foldout은 헤더를 클릭하여 내용을 펼치거나 접을 수 있는 컨테이너입니다.
/// 아코디언(Accordion)이라고도 불리며, 많은 내용을 정리할 때 유용합니다.
/// Unity Inspector에서 컴포넌트 섹션을 접는 것과 같은 동작입니다.
/// </para>
///
/// <para><b>Foldout vs Panel 차이:</b></para>
/// <list type="bullet">
/// <item><description><c>Foldout</c> - 간단한 접기/펼치기, 토글 화살표 표시</description></item>
/// <item><description><c>Panel</c> - 헤더/푸터/액션 등 풍부한 기능, 더 많은 커스터마이징</description></item>
/// </list>
///
/// <para><b>주요 속성:</b></para>
/// <list type="bullet">
/// <item><description><c>text</c> - 헤더에 표시되는 제목</description></item>
/// <item><description><c>value</c> / <c>IsExpanded</c> - 펼침 상태 (true=펼침, false=접힘)</description></item>
/// </list>
///
/// <para><b>실제 활용 예시:</b></para>
/// <list type="bullet">
/// <item><description>설정 페이지 - 고급 설정 숨기기</description></item>
/// <item><description>FAQ - 질문 클릭 시 답변 표시</description></item>
/// <item><description>인스펙터 - 컴포넌트 섹션 접기</description></item>
/// <item><description>필터 패널 - 상세 필터 옵션 숨기기</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>

View File

@@ -8,7 +8,38 @@ namespace UVC.UIToolkit
/// <summary>
/// 도움말 박스 컴포넌트.
/// Unity HelpBox를 래핑하여 커스텀 스타일을 적용합니다.
/// 정보, 경고, 오류 메시지를 아이콘과 함께 표시합니다.
/// </summary>
/// <remarks>
/// <para><b>HelpBox(도움말 박스)란?</b></para>
/// <para>
/// HelpBox는 사용자에게 중요한 정보, 경고, 오류 메시지를 눈에 띄게 표시하는 컴포넌트입니다.
/// 메시지 유형에 따라 아이콘과 배경색이 달라져 시각적으로 구분됩니다.
/// Unity Inspector에서 볼 수 있는 노란색/빨간색 경고 박스와 같은 역할입니다.
/// </para>
///
/// <para><b>메시지 유형 (HelpBoxMessageType):</b></para>
/// <list type="bullet">
/// <item><description><c>None</c> - 아이콘 없이 텍스트만 표시</description></item>
/// <item><description><c>Info</c> - 정보 아이콘 (파란색) - 일반 안내 메시지</description></item>
/// <item><description><c>Warning</c> - 경고 아이콘 (노란색) - 주의 필요한 사항</description></item>
/// <item><description><c>Error</c> - 오류 아이콘 (빨간색) - 에러, 문제 상황</description></item>
/// </list>
///
/// <para><b>HelpBox vs Alert 차이:</b></para>
/// <list type="bullet">
/// <item><description><c>HelpBox</c> - 화면에 고정 표시, 닫기 불가, 인라인 메시지</description></item>
/// <item><description><c>Alert</c> - 모달 팝업, 버튼으로 닫기, 사용자 확인 필요</description></item>
/// </list>
///
/// <para><b>실제 활용 예시:</b></para>
/// <list type="bullet">
/// <item><description>폼 유효성 검사 - 입력 오류 메시지</description></item>
/// <item><description>설정 페이지 - "이 설정은 재시작 후 적용됩니다" 안내</description></item>
/// <item><description>에디터 - "필수 필드가 비어 있습니다" 경고</description></item>
/// <item><description>튜토리얼 - 사용법 힌트 표시</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>

View File

@@ -0,0 +1,492 @@
#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.Networking;
namespace UVC.UIToolkit
{
/// <summary>
/// 이미지 컴포넌트.
/// Resources 폴더 내부 경로 또는 HTTP/HTTPS URL에서 이미지를 로드하여 표시합니다.
/// </summary>
/// <remarks>
/// <para><b>UTKImage란?</b></para>
/// <para>
/// UTKImage는 UXML에서 직접 사용할 수 있는 이미지 표시 컴포넌트입니다.
/// <c>src</c> 속성에 경로를 지정하면 자동으로 이미지를 로드하여 표시합니다.
/// 내부 리소스(Resources 폴더)와 외부 URL(HTTP/HTTPS) 모두 지원합니다.
/// </para>
///
/// <para><b>지원하는 이미지 소스:</b></para>
/// <list type="bullet">
/// <item><description><b>내부 리소스</b> - Assets/Resources 폴더 내 이미지 경로 (확장자 제외)</description></item>
/// <item><description><b>외부 URL</b> - http:// 또는 https://로 시작하는 이미지 URL</description></item>
/// </list>
///
/// <para><b>주요 속성:</b></para>
/// <list type="bullet">
/// <item><description><c>src</c> - 이미지 소스 경로 (Resources 경로 또는 HTTP URL)</description></item>
/// <item><description><c>scale-mode</c> - 이미지 스케일 모드 (StretchToFill, ScaleAndCrop, ScaleToFit)</description></item>
/// <item><description><c>tint-color</c> - 이미지 틴트 색상</description></item>
/// </list>
///
/// <para><b>이미지 스케일 모드:</b></para>
/// <list type="bullet">
/// <item><description><c>StretchToFill</c> - 요소 크기에 맞게 늘림 (비율 무시)</description></item>
/// <item><description><c>ScaleAndCrop</c> - 비율 유지하며 요소를 채움 (잘릴 수 있음)</description></item>
/// <item><description><c>ScaleToFit</c> - 비율 유지하며 요소 안에 맞춤 (여백 발생 가능)</description></item>
/// </list>
///
/// <para><b>실제 활용 예시:</b></para>
/// <list type="bullet">
/// <item><description>프로필 이미지, 아바타</description></item>
/// <item><description>썸네일, 미리보기 이미지</description></item>
/// <item><description>배너, 광고 이미지</description></item>
/// <item><description>제품 이미지, 갤러리</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 내부 리소스 이미지 로드
/// var image = new UTKImage();
/// image.Src = "UIToolkit/Images/icon_setting_22";
/// image.style.width = 100;
/// image.style.height = 100;
///
/// // 외부 URL 이미지 로드
/// var webImage = new UTKImage();
/// webImage.Src = "https://example.com/image.png";
/// webImage.ScaleMode = ScaleMode.ScaleToFit;
///
/// // 생성자에서 소스 지정
/// var profileImage = new UTKImage("https://api.example.com/avatar/123");
/// profileImage.style.width = 64;
/// profileImage.style.height = 64;
/// profileImage.style.borderTopLeftRadius = 32;
/// profileImage.style.borderTopRightRadius = 32;
/// profileImage.style.borderBottomLeftRadius = 32;
/// profileImage.style.borderBottomRightRadius = 32;
///
/// // 이미지 로드 이벤트 핸들링
/// image.OnImageLoaded += (texture) => Debug.Log("이미지 로드 완료");
/// image.OnImageFailed += (error) => Debug.LogError($"로드 실패: {error}");
///
/// // 틴트 색상 적용
/// image.TintColor = Color.red;
///
/// // 비동기 로드
/// await image.LoadAsync("https://example.com/image.png", cancellationToken);
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// &lt;ui:UXML xmlns:utk="UVC.UIToolkit"&gt;
/// &lt;!-- 내부 리소스 이미지 --&gt;
/// &lt;utk:UTKImage src="UIToolkit/Images/icon_setting_22"
/// style="width: 48px; height: 48px;" /&gt;
///
/// &lt;!-- 외부 URL 이미지 --&gt;
/// &lt;utk:UTKImage src="https://example.com/image.png"
/// style="width: 200px; height: 150px;"
/// scale-mode="ScaleToFit" /&gt;
///
/// &lt;!-- 둥근 프로필 이미지 --&gt;
/// &lt;utk:UTKImage src="https://api.example.com/avatar"
/// class="profile-avatar" /&gt;
///
/// &lt;!-- 틴트 색상 적용 --&gt;
/// &lt;utk:UTKImage src="UIToolkit/Images/icon_home"
/// tint-color="#FF5500" /&gt;
/// &lt;/ui:UXML&gt;
/// </code>
/// </example>
[UxmlElement]
public partial class UTKImage : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Common/UTKImage";
#endregion
#region Fields
private bool _disposed;
private string _src = "";
private ScaleMode _scaleMode = ScaleMode.ScaleToFit;
private Color _tintColor = Color.white;
private Texture2D? _loadedTexture;
private CancellationTokenSource? _loadCts;
private bool _isExternalTexture;
#endregion
#region Events
/// <summary>이미지 로드 완료 이벤트</summary>
public event Action<Texture2D>? OnImageLoaded;
/// <summary>이미지 로드 실패 이벤트</summary>
public event Action<string>? OnImageFailed;
#endregion
#region Properties
/// <summary>
/// 이미지 소스 경로.
/// Resources 폴더 내 경로(확장자 제외) 또는 HTTP/HTTPS URL을 지정합니다.
/// </summary>
/// <example>
/// <code>
/// // 내부 리소스
/// image.Src = "UIToolkit/Images/icon_setting_22";
///
/// // 외부 URL
/// image.Src = "https://example.com/image.png";
/// </code>
/// </example>
[UxmlAttribute("src")]
public string Src
{
get => _src;
set
{
if (_src == value) return;
_src = value;
LoadImageFromSource();
}
}
/// <summary>이미지 스케일 모드</summary>
[UxmlAttribute("scale-mode")]
public ScaleMode ScaleMode
{
get => _scaleMode;
set
{
_scaleMode = value;
UpdateBackgroundScaleMode();
}
}
/// <summary>이미지 틴트 색상</summary>
[UxmlAttribute("tint-color")]
public Color TintColor
{
get => _tintColor;
set
{
_tintColor = value;
style.unityBackgroundImageTintColor = value;
}
}
/// <summary>현재 로드된 텍스처</summary>
public Texture2D? LoadedTexture => _loadedTexture;
/// <summary>이미지 로드 중 여부</summary>
public bool IsLoading => _loadCts != null && !_loadCts.IsCancellationRequested;
#endregion
#region Constructor
public UTKImage()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
SetupStyles();
SubscribeToThemeChanges();
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
}
/// <summary>
/// 소스를 지정하여 이미지를 생성합니다.
/// </summary>
/// <param name="src">이미지 소스 경로 (Resources 경로 또는 HTTP URL)</param>
public UTKImage(string src) : this()
{
_src = src;
}
/// <summary>
/// Texture2D로 직접 이미지를 생성합니다.
/// </summary>
/// <param name="texture">표시할 텍스처</param>
public UTKImage(Texture2D texture) : this()
{
SetTexture(texture);
}
#endregion
#region Setup
private void SetupStyles()
{
AddToClassList("utk-image");
style.unityBackgroundScaleMode = _scaleMode;
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
private void OnAttachToPanel(AttachToPanelEvent evt)
{
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
// UXML에서 설정된 src가 있으면 로드
if (!string.IsNullOrEmpty(_src))
{
LoadImageFromSource();
}
}
#endregion
#region Image Loading
private void LoadImageFromSource()
{
if (string.IsNullOrEmpty(_src))
{
ClearImage();
return;
}
// 이전 로드 취소
CancelCurrentLoad();
if (IsExternalUrl(_src))
{
LoadFromUrlAsync(_src).Forget();
}
else
{
LoadFromResources(_src);
}
}
/// <summary>
/// Resources 폴더에서 이미지를 동기로 로드합니다.
/// </summary>
/// <param name="resourcePath">Resources 폴더 내 경로 (확장자 제외)</param>
public void LoadFromResources(string resourcePath)
{
var texture = Resources.Load<Texture2D>(resourcePath);
if (texture != null)
{
_isExternalTexture = false;
ApplyTexture(texture);
OnImageLoaded?.Invoke(texture);
}
else
{
var errorMsg = $"리소스를 찾을 수 없습니다: {resourcePath}";
Debug.LogWarning($"[UTKImage] {errorMsg}");
OnImageFailed?.Invoke(errorMsg);
}
}
/// <summary>
/// Resources 폴더에서 이미지를 비동기로 로드합니다.
/// </summary>
/// <param name="resourcePath">Resources 폴더 내 경로 (확장자 제외)</param>
/// <param name="ct">취소 토큰</param>
public async UniTask LoadFromResourcesAsync(string resourcePath, CancellationToken ct = default)
{
CancelCurrentLoad();
_loadCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
try
{
var request = Resources.LoadAsync<Texture2D>(resourcePath);
await request.ToUniTask(cancellationToken: _loadCts.Token);
var texture = request.asset as Texture2D;
if (texture != null)
{
_isExternalTexture = false;
ApplyTexture(texture);
OnImageLoaded?.Invoke(texture);
}
else
{
var errorMsg = $"리소스를 찾을 수 없습니다: {resourcePath}";
Debug.LogWarning($"[UTKImage] {errorMsg}");
OnImageFailed?.Invoke(errorMsg);
}
}
catch (OperationCanceledException)
{
// 취소됨
}
finally
{
_loadCts?.Dispose();
_loadCts = null;
}
}
/// <summary>
/// 외부 URL에서 이미지를 비동기로 로드합니다.
/// </summary>
/// <param name="url">이미지 URL (http:// 또는 https://)</param>
/// <param name="ct">취소 토큰</param>
public async UniTask LoadFromUrlAsync(string url, CancellationToken ct = default)
{
CancelCurrentLoad();
_loadCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
try
{
AddToClassList("utk-image--loading");
using var request = UnityWebRequestTexture.GetTexture(url);
await request.SendWebRequest().ToUniTask(cancellationToken: _loadCts.Token);
if (request.result == UnityWebRequest.Result.Success)
{
var texture = DownloadHandlerTexture.GetContent(request);
if (texture != null)
{
// 이전 외부 텍스처 정리
CleanupExternalTexture();
_isExternalTexture = true;
ApplyTexture(texture);
OnImageLoaded?.Invoke(texture);
}
}
else
{
var errorMsg = $"이미지 로드 실패: {request.error}";
Debug.LogWarning($"[UTKImage] {errorMsg}");
OnImageFailed?.Invoke(errorMsg);
}
}
catch (OperationCanceledException)
{
// 취소됨
}
catch (Exception ex)
{
var errorMsg = $"이미지 로드 중 오류: {ex.Message}";
Debug.LogError($"[UTKImage] {errorMsg}");
OnImageFailed?.Invoke(errorMsg);
}
finally
{
RemoveFromClassList("utk-image--loading");
_loadCts?.Dispose();
_loadCts = null;
}
}
/// <summary>
/// 소스에서 이미지를 비동기로 로드합니다.
/// Resources 경로 또는 HTTP URL을 자동으로 감지합니다.
/// </summary>
/// <param name="source">이미지 소스 (Resources 경로 또는 HTTP URL)</param>
/// <param name="ct">취소 토큰</param>
public async UniTask LoadAsync(string source, CancellationToken ct = default)
{
_src = source;
if (IsExternalUrl(source))
{
await LoadFromUrlAsync(source, ct);
}
else
{
await LoadFromResourcesAsync(source, ct);
}
}
/// <summary>
/// Texture2D를 직접 설정합니다.
/// </summary>
/// <param name="texture">표시할 텍스처</param>
public void SetTexture(Texture2D texture)
{
CleanupExternalTexture();
_isExternalTexture = false;
ApplyTexture(texture);
}
/// <summary>
/// 이미지를 제거합니다.
/// </summary>
public void ClearImage()
{
CleanupExternalTexture();
_loadedTexture = null;
style.backgroundImage = StyleKeyword.None;
}
#endregion
#region Private Methods
private void ApplyTexture(Texture2D texture)
{
_loadedTexture = texture;
style.backgroundImage = new StyleBackground(texture);
style.unityBackgroundScaleMode = _scaleMode;
style.unityBackgroundImageTintColor = _tintColor;
}
private void UpdateBackgroundScaleMode()
{
style.unityBackgroundScaleMode = _scaleMode;
}
private void CancelCurrentLoad()
{
if (_loadCts != null)
{
_loadCts.Cancel();
_loadCts.Dispose();
_loadCts = null;
}
}
private void CleanupExternalTexture()
{
if (_isExternalTexture && _loadedTexture != null)
{
UnityEngine.Object.Destroy(_loadedTexture);
_loadedTexture = null;
_isExternalTexture = false;
}
}
private static bool IsExternalUrl(string source)
{
return source.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
source.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
CancelCurrentLoad();
CleanupExternalTexture();
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
OnImageLoaded = null;
OnImageFailed = null;
}
#endregion
}
}

View File

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

View File

@@ -65,7 +65,7 @@ namespace UVC.UIToolkit
/// </code>
/// <code>
/// <!-- UXML 파일 -->
/// <ui:VisualElement class="my-icon" />
/// <VisualElement class="my-icon" />
/// </code>
/// <para><b>C# 코드에서 UXML 요소에 이미지 적용:</b></para>
/// <code>

View File

@@ -8,7 +8,38 @@ namespace UVC.UIToolkit
/// <summary>
/// 스크롤 뷰 컴포넌트.
/// Unity ScrollView를 래핑하여 커스텀 스타일을 적용합니다.
/// 내용이 컨테이너보다 클 때 스크롤하여 볼 수 있습니다.
/// </summary>
/// <remarks>
/// <para><b>ScrollView(스크롤 뷰)란?</b></para>
/// <para>
/// ScrollView는 내용이 표시 영역보다 클 때 스크롤하여 나머지 내용을 볼 수 있게 하는 컨테이너입니다.
/// 긴 목록, 대화 내용, 문서 등 많은 콘텐츠를 제한된 공간에 표시할 때 사용합니다.
/// </para>
///
/// <para><b>스크롤 모드 (ScrollViewMode):</b></para>
/// <list type="bullet">
/// <item><description><c>Vertical</c> - 세로 스크롤만 (기본값, 대부분의 목록)</description></item>
/// <item><description><c>Horizontal</c> - 가로 스크롤만 (가로 갤러리, 타임라인)</description></item>
/// <item><description><c>VerticalAndHorizontal</c> - 양방향 스크롤 (큰 이미지, 스프레드시트)</description></item>
/// </list>
///
/// <para><b>주요 속성:</b></para>
/// <list type="bullet">
/// <item><description><c>mode</c> - 스크롤 방향 설정</description></item>
/// <item><description><c>scrollOffset</c> - 현재 스크롤 위치</description></item>
/// <item><description><c>verticalScrollerVisibility</c> - 세로 스크롤바 표시 설정</description></item>
/// <item><description><c>horizontalScrollerVisibility</c> - 가로 스크롤바 표시 설정</description></item>
/// </list>
///
/// <para><b>실제 활용 예시:</b></para>
/// <list type="bullet">
/// <item><description>채팅 창 - 메시지 목록 스크롤</description></item>
/// <item><description>설정 페이지 - 많은 옵션 스크롤</description></item>
/// <item><description>갤러리 - 이미지 목록 스크롤</description></item>
/// <item><description>코드 에디터 - 긴 코드 스크롤</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>

View File

@@ -10,9 +10,69 @@ using UVC.Locale;
namespace UVC.UIToolkit
{
/// <summary>
/// UIToolkit 기반 툴팁 매니저
/// VisualElement에 마우스 오버 시 툴팁 표시
/// UIToolkit 기반 툴팁 매니저.
/// VisualElement에 마우스 오버 시 툴팁 표시하는 싱글톤 관리자입니다.
/// </summary>
/// <remarks>
/// <para><b>Tooltip(툴팁)이란?</b></para>
/// <para>
/// Tooltip은 UI 요소에 마우스를 올렸을 때 나타나는 작은 설명 텍스트입니다.
/// 버튼이나 아이콘의 기능을 설명하거나 추가 정보를 제공할 때 사용합니다.
/// 일반적으로 잠시 후(500ms) 나타나고, 마우스가 벗어나면 사라집니다.
/// </para>
///
/// <para><b>싱글톤 패턴:</b></para>
/// <para>
/// UTKTooltipManager는 싱글톤으로 구현되어 있습니다.
/// <c>UTKTooltipManager.Instance</c>로 접근하며, 앱 전체에서 하나의 툴팁 UI를 공유합니다.
/// 사용 전에 반드시 <c>Initialize(root)</c>를 호출해야 합니다.
/// </para>
///
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item><description>지연 표시 - 마우스 오버 후 일정 시간 뒤에 표시</description></item>
/// <item><description>마우스 따라가기 - 마우스 이동에 따라 위치 업데이트</description></item>
/// <item><description>화면 경계 처리 - 화면 밖으로 나가지 않도록 자동 조정</description></item>
/// <item><description>다국어 지원 - 로컬라이제이션 키 자동 변환</description></item>
/// </list>
///
/// <para><b>주요 메서드:</b></para>
/// <list type="bullet">
/// <item><description><c>Initialize(root)</c> - 초기화 (루트 요소 지정)</description></item>
/// <item><description><c>AttachTooltip(element, text)</c> - 요소에 툴팁 연결</description></item>
/// <item><description><c>DetachTooltip(element)</c> - 툴팁 제거</description></item>
/// <item><description><c>Show(text, position)</c> - 즉시 표시</description></item>
/// <item><description><c>Hide()</c> - 숨기기</description></item>
/// </list>
///
/// <para><b>실제 활용 예시:</b></para>
/// <list type="bullet">
/// <item><description>아이콘 버튼 - "저장", "삭제", "설정" 등 기능 설명</description></item>
/// <item><description>복잡한 옵션 - 설정 항목의 상세 설명</description></item>
/// <item><description>잘린 텍스트 - 전체 내용 표시</description></item>
/// <item><description>단축키 안내 - "Ctrl+S" 등 키보드 힌트</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 1. 초기화 (앱 시작 시 한 번)
/// UTKTooltipManager.Instance.Initialize(rootVisualElement);
///
/// // 2. 버튼에 툴팁 연결
/// var saveButton = new UTKButton("", UTKMaterialIcons.Save);
/// UTKTooltipManager.Instance.AttachTooltip(saveButton, "저장 (Ctrl+S)");
///
/// // 3. 다국어 키로 툴팁 연결
/// UTKTooltipManager.Instance.AttachTooltip(settingsButton, "tooltip_settings");
///
/// // 4. 툴팁 업데이트
/// UTKTooltipManager.Instance.UpdateTooltip(button, "새로운 설명");
///
/// // 5. 툴팁 제거
/// UTKTooltipManager.Instance.DetachTooltip(button);
/// </code>
/// </example>
public class UTKTooltipManager : IDisposable
{
#region Singleton