#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.Networking;
namespace UVC.UIToolkit
{
///
/// 이미지 컴포넌트.
/// Resources 폴더 내부 경로 또는 HTTP/HTTPS URL에서 이미지를 로드하여 표시합니다.
///
///
/// UTKImage란?
///
/// UTKImage는 UXML에서 직접 사용할 수 있는 이미지 표시 컴포넌트입니다.
/// src 속성에 경로를 지정하면 자동으로 이미지를 로드하여 표시합니다.
/// 내부 리소스(Resources 폴더)와 외부 URL(HTTP/HTTPS) 모두 지원합니다.
///
///
/// 지원하는 이미지 소스:
///
/// - 내부 리소스 - Assets/Resources 폴더 내 이미지 경로 (확장자 제외)
/// - 외부 URL - http:// 또는 https://로 시작하는 이미지 URL
///
///
/// 주요 속성:
///
/// - src - 이미지 소스 경로 (Resources 경로 또는 HTTP URL)
/// - scale-mode - 이미지 스케일 모드 (StretchToFill, ScaleAndCrop, ScaleToFit)
/// - tint-color - 이미지 틴트 색상
///
///
/// 이미지 스케일 모드:
///
/// - StretchToFill - 요소 크기에 맞게 늘림 (비율 무시)
/// - ScaleAndCrop - 비율 유지하며 요소를 채움 (잘릴 수 있음)
/// - ScaleToFit - 비율 유지하며 요소 안에 맞춤 (여백 발생 가능)
///
///
/// 실제 활용 예시:
///
/// - 프로필 이미지, 아바타
/// - 썸네일, 미리보기 이미지
/// - 배너, 광고 이미지
/// - 제품 이미지, 갤러리
///
///
///
/// C# 코드에서 사용:
///
/// // 내부 리소스 이미지 로드
/// 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);
///
/// UXML에서 사용:
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
[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
/// 이미지 로드 완료 이벤트
public event Action? OnImageLoaded;
/// 이미지 로드 실패 이벤트
public event Action? OnImageFailed;
#endregion
#region Properties
///
/// 이미지 소스 경로.
/// Resources 폴더 내 경로(확장자 제외) 또는 HTTP/HTTPS URL을 지정합니다.
///
///
///
/// // 내부 리소스
/// image.Src = "UIToolkit/Images/icon_setting_22";
///
/// // 외부 URL
/// image.Src = "https://example.com/image.png";
///
///
[UxmlAttribute("src")]
public string Src
{
get => _src;
set
{
if (_src == value) return;
_src = value;
LoadImageFromSource();
}
}
/// 이미지 스케일 모드
[UxmlAttribute("scale-mode")]
public ScaleMode ScaleMode
{
get => _scaleMode;
set
{
_scaleMode = value;
UpdateBackgroundScaleMode();
}
}
/// 이미지 틴트 색상
[UxmlAttribute("tint-color")]
public Color TintColor
{
get => _tintColor;
set
{
_tintColor = value;
style.unityBackgroundImageTintColor = value;
}
}
/// 현재 로드된 텍스처
public Texture2D? LoadedTexture => _loadedTexture;
/// 이미지 로드 중 여부
public bool IsLoading => _loadCts != null && !_loadCts.IsCancellationRequested;
#endregion
#region Constructor
public UTKImage()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
SetupStyles();
SubscribeToThemeChanges();
RegisterCallback(OnAttachToPanel);
}
///
/// 소스를 지정하여 이미지를 생성합니다.
///
/// 이미지 소스 경로 (Resources 경로 또는 HTTP URL)
public UTKImage(string src) : this()
{
_src = src;
}
///
/// Texture2D로 직접 이미지를 생성합니다.
///
/// 표시할 텍스처
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(OnAttachToPanelForTheme);
RegisterCallback(OnDetachFromPanelForTheme);
}
private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt)
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
private void OnAttachToPanel(AttachToPanelEvent evt)
{
UnregisterCallback(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);
}
}
///
/// Resources 폴더에서 이미지를 동기로 로드합니다.
///
/// Resources 폴더 내 경로 (확장자 제외)
public void LoadFromResources(string resourcePath)
{
var texture = Resources.Load(resourcePath);
if (texture != null)
{
_isExternalTexture = false;
ApplyTexture(texture);
OnImageLoaded?.Invoke(texture);
}
else
{
var errorMsg = $"리소스를 찾을 수 없습니다: {resourcePath}";
Debug.LogWarning($"[UTKImage] {errorMsg}");
OnImageFailed?.Invoke(errorMsg);
}
}
///
/// Resources 폴더에서 이미지를 비동기로 로드합니다.
///
/// Resources 폴더 내 경로 (확장자 제외)
/// 취소 토큰
public async UniTask LoadFromResourcesAsync(string resourcePath, CancellationToken ct = default)
{
CancelCurrentLoad();
_loadCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
try
{
var request = Resources.LoadAsync(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;
}
}
///
/// 외부 URL에서 이미지를 비동기로 로드합니다.
///
/// 이미지 URL (http:// 또는 https://)
/// 취소 토큰
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;
}
}
///
/// 소스에서 이미지를 비동기로 로드합니다.
/// Resources 경로 또는 HTTP URL을 자동으로 감지합니다.
///
/// 이미지 소스 (Resources 경로 또는 HTTP URL)
/// 취소 토큰
public async UniTask LoadAsync(string source, CancellationToken ct = default)
{
_src = source;
if (IsExternalUrl(source))
{
await LoadFromUrlAsync(source, ct);
}
else
{
await LoadFromResourcesAsync(source, ct);
}
}
///
/// Texture2D를 직접 설정합니다.
///
/// 표시할 텍스처
public void SetTexture(Texture2D texture)
{
CleanupExternalTexture();
_isExternalTexture = false;
ApplyTexture(texture);
}
///
/// 이미지를 제거합니다.
///
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;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
OnImageLoaded = null;
OnImageFailed = null;
}
#endregion
}
}