#if UGX_BRIDGE || UNITY_EDITOR using System; using System.Collections; using System.Collections.Generic; using Uigenx.Binding; using Uigenx.Protocol; using UnityEngine; using UnityEngine.UIElements; namespace Uigenx.Bridge { /// /// Renders a screen into an offscreen panel and returns a PNG. Requests are queued and served /// one at a time so a burst from the studio cannot allocate several render textures at once. /// public sealed class UgxPreviewHandler : IDisposable { /// Largest preview Unity will render. Bigger requests are clamped and reported. public const int MaxDimension = 4096; /// Pending requests kept while one is rendering. Older requests are dropped first. public const int MaxQueued = 4; /// /// Runtime panels cannot be forced into a pseudo state, so a requested state is applied as /// a class instead. The compiler mirrors every `:hover` rule onto `.ugx-state-hover`. /// public const string StateClassPrefix = "ugx-state-"; readonly UgxBridgeClient _client; readonly MonoBehaviour _runner; readonly Queue _queue = new Queue(MaxQueued); bool _busy; bool _disposed; public UgxPreviewHandler(UgxBridgeClient client, MonoBehaviour runner) { _client = client ?? throw new ArgumentNullException(nameof(client)); _runner = runner ?? throw new ArgumentNullException(nameof(runner)); client.Register(UgxMessageTypes.PreviewRequest, OnRequest); client.Register(UgxMessageTypes.ScreenshotRequest, OnScreenshot); } public void Dispose() { if (_disposed) return; _disposed = true; _client.Unregister(UgxMessageTypes.PreviewRequest); _client.Unregister(UgxMessageTypes.ScreenshotRequest); _queue.Clear(); } void OnRequest(UgxEnvelope envelope) { var body = envelope.BodyAs(); if (string.IsNullOrEmpty(body.Screen)) { _client.SendError(UgxErrorCodes.BadRequest, "preview.request needs a screen", envelope.Id); return; } while (_queue.Count >= MaxQueued) { // The studio is waiting on every requestId it sent, so a drop has to be reported. var dropped = _queue.Dequeue(); _client.SendError(UgxErrorCodes.RenderFailed, "preview queue is full, this request was dropped", dropped.CorrelationId); } _queue.Enqueue(new Request(body, envelope.Id)); if (!_busy) _runner.StartCoroutine(Drain()); } void OnScreenshot(UgxEnvelope envelope) { var body = envelope.BodyAs(); _runner.StartCoroutine(CaptureScreen(body, envelope.Id)); } IEnumerator Drain() { _busy = true; while (!_disposed && _queue.Count > 0) { var request = _queue.Dequeue(); yield return Render(request); } _busy = false; } IEnumerator Render(Request request) { var body = request.Body; int width = Mathf.Clamp(body.Width, 1, MaxDimension); int height = Mathf.Clamp(body.Height, 1, MaxDimension); var registry = UgxScreenRegistry.Active; if (registry == null || !registry.TryGet(body.Screen, out var entry)) { _client.SendError(UgxErrorCodes.RenderFailed, "screen '" + body.Screen + "' is not in the registry", request.CorrelationId); yield break; } var template = UgxSettings.Instance.DefaultPanelSettings; if (template == null) { _client.SendError(UgxErrorCodes.RenderFailed, "no default PanelSettings is configured, cannot render offscreen", request.CorrelationId); yield break; } RenderTexture texture = null; PanelSettings panel = null; GameObject host = null; Texture2D readback = null; UgxScreen screen = null; try { texture = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32) { name = "ugx-preview", hideFlags = HideFlags.HideAndDontSave, }; texture.Create(); panel = UnityEngine.Object.Instantiate(template); panel.name = "ugx-preview-panel"; panel.hideFlags = HideFlags.HideAndDontSave; panel.targetTexture = texture; panel.scaleMode = PanelScaleMode.ConstantPixelSize; panel.clearColor = true; panel.colorClearValue = new Color(0f, 0f, 0f, 0f); host = new GameObject("ugx-preview") { hideFlags = HideFlags.HideAndDontSave }; var document = host.AddComponent(); document.panelSettings = panel; document.visualTreeAsset = entry.Uxml; var root = document.rootVisualElement; foreach (var sheet in entry.Styles) if (sheet != null) root.styleSheets.Add(sheet); var source = new UgxDictionaryDataSource(); source.Merge(ToPlainMap(body.Data)); screen = UgxScreens.Create(body.Screen); screen?.Attach(root, source); screen?.Show(); ApplyStates(root, body.States); } catch (Exception e) { Cleanup(screen, host, panel, texture); _client.SendError(UgxErrorCodes.RenderFailed, e.Message, request.CorrelationId); host = null; } if (host == null) yield break; // Two frames: one for layout, one for the panel to render into the target texture. yield return null; yield return new WaitForEndOfFrame(); try { readback = new Texture2D(width, height, TextureFormat.RGBA32, false) { hideFlags = HideFlags.HideAndDontSave, }; var previous = RenderTexture.active; RenderTexture.active = texture; readback.ReadPixels(new Rect(0, 0, width, height), 0, 0); readback.Apply(false); RenderTexture.active = previous; var result = new UgxPreviewResult { RequestId = body.RequestId, Png = Convert.ToBase64String(readback.EncodeToPNG()), Width = width, Height = height, }; if (body.Layout) CollectLayout(host.GetComponent().rootVisualElement, result.Layout); _client.Send(UgxMessageTypes.PreviewResult, result, re: request.CorrelationId); } catch (Exception e) { _client.SendError(UgxErrorCodes.RenderFailed, e.Message, request.CorrelationId); } finally { if (readback != null) Destroy(readback); Cleanup(screen, host, panel, texture); } } IEnumerator CaptureScreen(UgxScreenshotRequest body, string correlationId) { yield return new WaitForEndOfFrame(); Texture2D shot = null; try { shot = ScreenCapture.CaptureScreenshotAsTexture(); var result = new UgxPreviewResult { RequestId = body.RequestId, Png = Convert.ToBase64String(shot.EncodeToPNG()), Width = shot.width, Height = shot.height, }; _client.Send(UgxMessageTypes.PreviewResult, result, re: correlationId); } catch (Exception e) { _client.SendError(UgxErrorCodes.RenderFailed, e.Message, correlationId); } finally { if (shot != null) Destroy(shot); } } /// Nested preview data arrives as Newtonsoft nodes; the data source wants plain values. static Dictionary ToPlainMap(Dictionary source) { var plain = new Dictionary(source.Count); foreach (var pair in source) plain[pair.Key] = UgxJson.ToPlain(pair.Value); return plain; } static void ApplyStates(VisualElement root, Dictionary> states) { if (states == null) return; foreach (var pair in states) { var element = string.IsNullOrEmpty(pair.Key) ? root : root.Q(pair.Key); if (element == null || pair.Value == null) continue; foreach (var state in pair.Value) element.AddToClassList(StateClassPrefix + state); } } static void CollectLayout(VisualElement root, List into) { if (!string.IsNullOrEmpty(root.name)) { var rect = root.worldBound; into.Add(new UgxLayoutRect { Name = root.name, X = rect.x, Y = rect.y, W = rect.width, H = rect.height }); } for (int i = 0; i < root.childCount; i++) CollectLayout(root[i], into); } static void Cleanup(UgxScreen screen, GameObject host, PanelSettings panel, RenderTexture texture) { screen?.Detach(); if (host != null) Destroy(host); if (panel != null) Destroy(panel); if (texture == null) return; texture.Release(); Destroy(texture); } static void Destroy(UnityEngine.Object target) { if (Application.isPlaying) UnityEngine.Object.Destroy(target); else UnityEngine.Object.DestroyImmediate(target); } readonly struct Request { public readonly UgxPreviewRequest Body; public readonly string CorrelationId; public Request(UgxPreviewRequest body, string correlationId) { Body = body; CorrelationId = correlationId; } } } } #endif