#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