using System; using System.Collections; using System.Collections.Generic; using System.Text; using Unity.UIWidgets.async; using Unity.UIWidgets.engine; using Unity.UIWidgets.foundation; using Unity.UIWidgets.ui; using UnityEngine; using UnityEngine.Networking; using Codec = Unity.UIWidgets.ui.Codec; using Locale = Unity.UIWidgets.ui.Locale; using Object = UnityEngine.Object; using Path = System.IO.Path; using TextDirection = Unity.UIWidgets.ui.TextDirection; namespace Unity.UIWidgets.painting { public static partial class painting_ { internal delegate void _KeyAndErrorHandlerCallback(T key, Action handleError); internal delegate Future _AsyncKeyErrorHandler(T key, Exception exception); } public class ImageConfiguration : IEquatable { public ImageConfiguration( AssetBundle bundle = null, float? devicePixelRatio = null, Locale locale = null, Size size = null, RuntimePlatform? platform = null ) { this.bundle = bundle; this.devicePixelRatio = devicePixelRatio; this.locale = locale; this.size = size; this.platform = platform; } public ImageConfiguration copyWith( AssetBundle bundle = null, float? devicePixelRatio = null, Locale locale = null, Size size = null, RuntimePlatform? platform = null ) { return new ImageConfiguration( bundle: bundle ? bundle : this.bundle, devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, locale: locale ?? this.locale, size: size ?? this.size, platform: platform ?? this.platform ); } public readonly AssetBundle bundle; public readonly float? devicePixelRatio; public readonly Locale locale; public readonly TextDirection textDirection; public readonly Size size; public readonly RuntimePlatform? platform; public static readonly ImageConfiguration empty = new ImageConfiguration(); public bool Equals(ImageConfiguration other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return Equals(bundle, other.bundle) && devicePixelRatio.Equals(other.devicePixelRatio) && Equals(locale, other.locale) && Equals(size, other.size) && platform == other.platform; } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((ImageConfiguration) obj); } public override int GetHashCode() { unchecked { var hashCode = (bundle != null ? bundle.GetHashCode() : 0); hashCode = (hashCode * 397) ^ devicePixelRatio.GetHashCode(); hashCode = (hashCode * 397) ^ (locale != null ? locale.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (size != null ? size.GetHashCode() : 0); hashCode = (hashCode * 397) ^ platform.GetHashCode(); return hashCode; } } public static bool operator ==(ImageConfiguration left, ImageConfiguration right) { return Equals(left, right); } public static bool operator !=(ImageConfiguration left, ImageConfiguration right) { return !Equals(left, right); } public override string ToString() { var result = new StringBuilder(); result.Append("ImageConfiguration("); bool hasArguments = false; if (bundle != null) { if (hasArguments) { result.Append(", "); } result.Append($"bundle: {bundle}"); hasArguments = true; } if (devicePixelRatio != null) { if (hasArguments) { result.Append(", "); } result.Append($"devicePixelRatio: {devicePixelRatio:F1}"); hasArguments = true; } if (locale != null) { if (hasArguments) { result.Append(", "); } result.Append($"locale: {locale}"); hasArguments = true; } if (size != null) { if (hasArguments) { result.Append(", "); } result.Append($"size: {size}"); hasArguments = true; } if (platform != null) { if (hasArguments) { result.Append(", "); } result.Append($"platform: {platform}"); hasArguments = true; } result.Append(")"); return result.ToString(); } } public delegate Future DecoderCallback(byte[] bytes, int? cacheWidth = 0, int? cacheHeight = 0); public abstract class ImageProvider { public abstract ImageStream resolve(ImageConfiguration configuration); public static bool operator ==(ImageProvider left, ImageProvider right) { return Equals(left, right); } public static bool operator !=(ImageProvider left, ImageProvider right) { return !Equals(left, right); } } public abstract class ImageProvider : ImageProvider { public override ImageStream resolve(ImageConfiguration configuration) { D.assert(configuration != null); ImageStream stream = new ImageStream(); _createErrorHandlerAndKey( configuration, (T successKey, Action errorHandler) => { resolveStreamForKey(configuration, stream, successKey, (Exception e) => errorHandler(e)); }, (T key, Exception exception) => { Timer.run(() => { _ErrorImageCompleter imageCompleter = new _ErrorImageCompleter(); stream.setCompleter(imageCompleter); InformationCollector collector = null; D.assert(() => { IEnumerable infoCollector() { yield return new DiagnosticsProperty("Image provider", this); yield return new DiagnosticsProperty("Image configuration", configuration); yield return new DiagnosticsProperty("Image key", key, defaultValue: null); } collector = infoCollector; return true; }); imageCompleter.setError( exception: exception, stack: exception.StackTrace, context: new ErrorDescription("while resolving an image"), silent: true, // could be a network error or whatnot informationCollector: collector ); return null; }); return null; } ); return stream; } public ImageStream createStream(ImageConfiguration configuration) { return new ImageStream(); } public virtual void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) { if (stream.completer != null) { ImageStreamCompleter completerEdge = PaintingBinding.instance.imageCache.putIfAbsent( key, () => stream.completer, onError: handleError ); D.assert(Equals(completerEdge, stream.completer)); return; } ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent( key, () => load(key, ui_.instantiateImageCodec), onError: handleError ); if (completer != null) { stream.setCompleter(completer); } } public Future evict(ImageCache cache = null, ImageConfiguration configuration = null) { configuration = configuration ?? ImageConfiguration.empty; cache = cache ?? PaintingBinding.instance.imageCache; return obtainKey(configuration).then(key => cache.evict(key)).to(); } public abstract ImageStreamCompleter load(T assetBundleImageKey, DecoderCallback decode); public abstract Future obtainKey(ImageConfiguration configuration); Future obtainCacheStatus( ImageConfiguration configuration, ImageErrorListener handleError = null ) { D.assert(configuration != null); Completer completer = Completer.create(); _createErrorHandlerAndKey( configuration, (T key, Action innerHandleError) => { completer.complete(FutureOr.value(PaintingBinding.instance.imageCache.statusForKey(key))); }, (T key, Exception exception) => { if (handleError != null) { handleError(exception); } else { InformationCollector collector = null; D.assert(() => { IEnumerable infoCollector() { yield return new DiagnosticsProperty("Image provider", this); yield return new DiagnosticsProperty("Image configuration", configuration); yield return new DiagnosticsProperty("Image key", key, defaultValue: null); } collector = infoCollector; return true; }); UIWidgetsError.onError(new UIWidgetsErrorDetails( context: new ErrorDescription("while checking the cache location of an image"), informationCollector: collector, exception: exception )); completer.complete(); } return Future.value(); } ); return completer.future.to(); } private void _createErrorHandlerAndKey( ImageConfiguration configuration, painting_._KeyAndErrorHandlerCallback successCallback, painting_._AsyncKeyErrorHandler errorCallback ) { T obtainedKey = default; bool didError = false; Action handleError = (Exception exception) => { if (didError) { return; } if (!didError) { errorCallback(obtainedKey, exception); } didError = true; }; Zone dangerZone = Zone.current.fork( specification: new ZoneSpecification( handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, Exception error) => { handleError(error); } ) ); dangerZone.runGuarded(() => { Future key; try { key = obtainKey(configuration); } catch (Exception error) { handleError(error); return null; } key.then_((T reusltKey) => { obtainedKey = reusltKey; try { successCallback(reusltKey, handleError); } catch (Exception error) { handleError(error); } }).catchError(handleError); return null; }); } } public class AssetBundleImageKey : IEquatable { public AssetBundleImageKey( AssetBundle bundle, string name, float scale ) { D.assert(name != null); D.assert(scale >= 0.0); this.bundle = bundle; this.name = name; this.scale = scale; } public readonly AssetBundle bundle; public readonly string name; public readonly float scale; public bool Equals(AssetBundleImageKey other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return Equals(bundle, other.bundle) && string.Equals(name, other.name) && scale.Equals(other.scale); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((AssetBundleImageKey) obj); } public override int GetHashCode() { unchecked { var hashCode = (bundle != null ? bundle.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (name != null ? name.GetHashCode() : 0); hashCode = (hashCode * 397) ^ scale.GetHashCode(); return hashCode; } } public static bool operator ==(AssetBundleImageKey left, AssetBundleImageKey right) { return Equals(left, right); } public static bool operator !=(AssetBundleImageKey left, AssetBundleImageKey right) { return !Equals(left, right); } public override string ToString() { return $"{GetType()}(bundle: {bundle}, name: \"{name}\", scale: {scale})"; } } public abstract class AssetBundleImageProvider : ImageProvider { protected AssetBundleImageProvider() { } public override ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) { IEnumerable infoCollector() { yield return new DiagnosticsProperty("Image provider", this); yield return new DiagnosticsProperty("Image key", key); } return new MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: key.scale, informationCollector: infoCollector ); } Future _loadAsync(AssetBundleImageKey key, DecoderCallback decode) { Object data; // Hot reload/restart could change whether an asset bundle or key in a // bundle are available, or if it is a network backed bundle. try { data = key.bundle.LoadAsset(key.name); } catch (Exception e) { PaintingBinding.instance.imageCache.evict(key); throw e; } if (data != null && data is Texture2D textureData) { return decode(textureData.EncodeToPNG()); } else { PaintingBinding.instance.imageCache.evict(key); throw new Exception("Unable to read data"); } } IEnumerator _loadAssetAsync(AssetBundleImageKey key) { if (key.bundle == null) { ResourceRequest request = Resources.LoadAsync(key.name); if (request.asset) { yield return request.asset; } else { yield return request; yield return request.asset; } } else { AssetBundleRequest request = key.bundle.LoadAssetAsync(key.name); if (request.asset) { yield return request.asset; } else { yield return request.asset; } } } } internal class _SizeAwareCacheKey : IEquatable<_SizeAwareCacheKey> { internal _SizeAwareCacheKey(object providerCacheKey, int width, int height) { this.providerCacheKey = providerCacheKey; this.width = width; this.height = height; } public readonly object providerCacheKey; public readonly int width; public readonly int height; public static bool operator ==(_SizeAwareCacheKey left, object right) { return Equals(left, right); } public static bool operator !=(_SizeAwareCacheKey left, object right) { return !Equals(left, right); } public bool Equals(_SizeAwareCacheKey other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return Equals(providerCacheKey, other.providerCacheKey) && width == other.width && height == other.height; } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((_SizeAwareCacheKey) obj); } public override int GetHashCode() { unchecked { var hashCode = (providerCacheKey != null ? providerCacheKey.GetHashCode() : 0); hashCode = (hashCode * 397) ^ width; hashCode = (hashCode * 397) ^ height; return hashCode; } } } internal class ResizeImage : ImageProvider<_SizeAwareCacheKey> { public ResizeImage( ImageProvider imageProvider, int width = 0, int height = 0 ) { this.imageProvider = imageProvider; this.width = width; this.height = height; } public readonly ImageProvider imageProvider; public readonly int width; public readonly int height; public static ImageProvider resizeIfNeeded(int? cacheWidth, int? cacheHeight, ImageProvider provider) { if (cacheWidth != null || cacheHeight != null) { return new ResizeImage((ImageProvider) provider, width: cacheWidth.Value, height: cacheHeight.Value); } return provider; } public override ImageStreamCompleter load(_SizeAwareCacheKey assetBundleImageKey, DecoderCallback decode) { Future decodeResize(byte[] bytes, int? cacheWidth = 0, int? cacheHeight = 0) { D.assert( cacheWidth == null && cacheHeight == null, () => "ResizeImage cannot be composed with another ImageProvider that applies cacheWidth or cacheHeight." ); return decode(bytes, cacheWidth: width, cacheHeight: height); } return imageProvider.load(assetBundleImageKey.providerCacheKey, decodeResize); } public override Future<_SizeAwareCacheKey> obtainKey(ImageConfiguration configuration) { Completer completer = null; SynchronousFuture<_SizeAwareCacheKey> result = null; imageProvider.obtainKey(configuration).then((object key) => { // TODO: completer is always null? if (completer == null) { result = new SynchronousFuture<_SizeAwareCacheKey>(new _SizeAwareCacheKey(key, width, height)); } else { completer.complete(FutureOr.value(new _SizeAwareCacheKey(key, width, height))); } }); if (result != null) { return result; } completer = Completer.create(); return completer.future.to<_SizeAwareCacheKey>(); } } public class NetworkImage : ImageProvider, IEquatable { public NetworkImage(string url, float scale = 1.0f, IDictionary headers = null) { D.assert(url != null); this.url = url; this.scale = scale; this.headers = headers; } public readonly string url; public readonly float scale; public readonly IDictionary headers; public override Future obtainKey(ImageConfiguration configuration) { return new SynchronousFuture(this); } public override ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) { IEnumerable infoCollector() { yield return new ErrorDescription($"url: {url}"); } return new MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: key.scale, informationCollector: infoCollector ); } Future _loadAsync(NetworkImage key, DecoderCallback decode) { var completer = Completer.create(); var isolate = Isolate.current; var panel = UIWidgetsPanelWrapper.current.window; if (panel.isActive()) { panel.startCoroutine(_loadCoroutine(key.url, completer, isolate)); return completer.future.to().then_(data => { if (data != null && data.Length > 0) { return decode(data); } throw new Exception("not loaded"); }).to(); } return new Future(Future.create(() => FutureOr.value(null))); } IEnumerator _loadCoroutine(string key, Completer completer, Isolate isolate) { var url = new Uri(key); using (var www = UnityWebRequest.Get(url)) { if (headers != null) { foreach (var header in headers) { www.SetRequestHeader(header.Key, header.Value); } } yield return www.SendWebRequest(); if (www.isNetworkError || www.isHttpError) { completer.completeError(new Exception($"Failed to load from url \"{url}\": {www.error}")); yield break; } var data = www.downloadHandler.data; using (Isolate.getScope(isolate)) { completer.complete(data); } } } IEnumerator _loadBytes(NetworkImage key) { D.assert(key == this); var uri = new Uri(key.url); if (uri.LocalPath.EndsWith(".gif")) { using (var www = UnityWebRequest.Get(uri)) { if (headers != null) { foreach (var header in headers) { www.SetRequestHeader(header.Key, header.Value); } } yield return www.SendWebRequest(); if (www.isNetworkError || www.isHttpError) { throw new Exception($"Failed to load from url \"{uri}\": {www.error}"); } var data = www.downloadHandler.data; yield return data; } yield break; } using (var www = UnityWebRequestTexture.GetTexture(uri)) { if (headers != null) { foreach (var header in headers) { www.SetRequestHeader(header.Key, header.Value); } } yield return www.SendWebRequest(); if (www.isNetworkError || www.isHttpError) { throw new Exception($"Failed to load from url \"{uri}\": {www.error}"); } var data = ((DownloadHandlerTexture) www.downloadHandler).texture; yield return data; } } public bool Equals(NetworkImage other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return string.Equals(url, other.url) && scale.Equals(other.scale); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((NetworkImage) obj); } public override int GetHashCode() { unchecked { return ((url != null ? url.GetHashCode() : 0) * 397) ^ scale.GetHashCode(); } } public static bool operator ==(NetworkImage left, NetworkImage right) { return Equals(left, right); } public static bool operator !=(NetworkImage left, NetworkImage right) { return !Equals(left, right); } public override string ToString() { return $"runtimeType(\"{url}\", scale: {scale})"; } } public class FileImage : ImageProvider, IEquatable { public FileImage(string file, float scale = 1.0f) { D.assert(file != null); this.file = file; this.scale = scale; } public readonly string file; public readonly float scale; public override Future obtainKey(ImageConfiguration configuration) { return new SynchronousFuture(this); } public override ImageStreamCompleter load(FileImage key, DecoderCallback decode) { IEnumerable infoCollector() { yield return new ErrorDescription($"Path: {file}"); } return new MultiFrameImageStreamCompleter(_loadAsync(key, decode), scale: key.scale, informationCollector: infoCollector); } Future _loadAsync(FileImage key, DecoderCallback decode) { #if UNITY_ANDROID && !UNITY_EDITOR var path = Path.Combine(Application.streamingAssetsPath, key.file); #else var path = "file://" + Path.Combine(Application.streamingAssetsPath, key.file); #endif using(var unpackerWWW = UnityWebRequest.Get(path)) { unpackerWWW.SendWebRequest(); while (!unpackerWWW.isDone) { } // This will block in the webplayer. if (unpackerWWW.isNetworkError || unpackerWWW.isHttpError) { throw new Exception($"Failed to get file \"{path}\": {unpackerWWW.error}"); } var data = unpackerWWW.downloadHandler.data; if (data.Length > 0) { return decode(data); } throw new Exception("not loaded"); } } IEnumerator _loadBytes(FileImage key) { D.assert(key == this); var uri = "file://" + key.file; if (uri.EndsWith(".gif")) { using (var www = UnityWebRequest.Get(uri)) { yield return www.SendWebRequest(); if (www.isNetworkError || www.isHttpError) { throw new Exception($"Failed to get file \"{uri}\": {www.error}"); } var data = www.downloadHandler.data; yield return data; } yield break; } using (var www = UnityWebRequestTexture.GetTexture(uri)) { yield return www.SendWebRequest(); if (www.isNetworkError || www.isHttpError) { throw new Exception($"Failed to get file \"{uri}\": {www.error}"); } var data = ((DownloadHandlerTexture) www.downloadHandler).texture; yield return data; } } public bool Equals(FileImage other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return string.Equals(file, other.file) && scale.Equals(other.scale); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((FileImage) obj); } public override int GetHashCode() { unchecked { return ((file != null ? file.GetHashCode() : 0) * 397) ^ scale.GetHashCode(); } } public static bool operator ==(FileImage left, FileImage right) { return Equals(left, right); } public static bool operator !=(FileImage left, FileImage right) { return !Equals(left, right); } public override string ToString() { return $"{foundation_.objectRuntimeType(this, "FileImage")}({file}, scale: {scale})"; } } public class MemoryImage : ImageProvider, IEquatable { public MemoryImage(byte[] bytes, float scale = 1.0f) { D.assert(bytes != null); this.bytes = bytes; this.scale = scale; } public readonly byte[] bytes; public readonly float scale; public override Future obtainKey(ImageConfiguration configuration) { return new SynchronousFuture(this); //Future.value(FutureOr.value(this)).to(); } public override ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) { return new MultiFrameImageStreamCompleter( _loadAsync(key, decode), scale: key.scale); } Future _loadAsync(MemoryImage key, DecoderCallback decode) { D.assert(key == this); return decode(bytes); } public bool Equals(MemoryImage other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return Equals(bytes, other.bytes) && scale.Equals(other.scale); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((MemoryImage) obj); } public override int GetHashCode() { unchecked { return ((bytes != null ? bytes.GetHashCode() : 0) * 397) ^ scale.GetHashCode(); } } public static bool operator ==(MemoryImage left, MemoryImage right) { return Equals(left, right); } public static bool operator !=(MemoryImage left, MemoryImage right) { return !Equals(left, right); } public override string ToString() { return $"{foundation_.objectRuntimeType(this, "MemoryImage")}({foundation_.describeIdentity(bytes)}), scale: {scale}"; } } public class ExactAssetImage : AssetBundleImageProvider, IEquatable { public ExactAssetImage( string assetName, float scale = 1.0f, AssetBundle bundle = null ) { D.assert(assetName != null); this.assetName = assetName; this.scale = scale; this.bundle = bundle; } public readonly string assetName; public readonly float scale; public readonly AssetBundle bundle; public override Future obtainKey(ImageConfiguration configuration) { return new SynchronousFuture(new AssetBundleImageKey( bundle: bundle ? bundle : configuration.bundle, name: assetName, scale: scale )); } public bool Equals(ExactAssetImage other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return string.Equals(assetName, other.assetName) && scale.Equals(other.scale) && Equals(bundle, other.bundle); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((ExactAssetImage) obj); } public override int GetHashCode() { unchecked { var hashCode = (assetName != null ? assetName.GetHashCode() : 0); hashCode = (hashCode * 397) ^ scale.GetHashCode(); hashCode = (hashCode * 397) ^ (bundle != null ? bundle.GetHashCode() : 0); return hashCode; } } public static bool operator ==(ExactAssetImage left, ExactAssetImage right) { return Equals(left, right); } public static bool operator !=(ExactAssetImage left, ExactAssetImage right) { return !Equals(left, right); } public override string ToString() { return $"{foundation_.objectRuntimeType(this, "ExactAssetImage")}(name: \"{assetName}\", scale: {scale}, bundle: {bundle})"; } } internal class _ErrorImageCompleter : ImageStreamCompleter { internal _ErrorImageCompleter() { } public void setError( DiagnosticsNode context, Exception exception, string stack, InformationCollector informationCollector, bool silent = false ) { reportError( context: context, exception: exception, informationCollector: informationCollector, silent: silent ); } } public class NetworkImageLoadException : Exception { NetworkImageLoadException(int statusCode, Uri uri) { D.assert(uri != null); D.assert(statusCode != null); this.statusCode = statusCode; this.uri = uri; _message = $"HTTP request failed, statusCode: {statusCode}, {uri}"; } public readonly int statusCode; readonly string _message; public readonly Uri uri; public override string ToString() => _message; } }