#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 } }