Editor/SuiPreviewLauncher.cs
using System;
using System.IO;
using System.Threading.Tasks;
using Editor;
using Sandbox;
using SboxUiDesigner.Runtime;
namespace SboxUiDesigner.EditorUi;
/// <summary>
/// Editor-side glue for the "Test in Play" button. Sequence:
///
/// 1. Compile the .sui to the preview cache (Code/_sui_preview/...)
/// — generates a real PanelComponent type via the engine's hot-reload.
/// 2. Poll TypeLibrary for the generated type until it's loaded (or timeout).
/// 3. Set <see cref="SuiPreviewState.PendingTypeFullName"/> so the preview
/// scene's <see cref="SuiPreviewMount"/> knows what to mount.
/// 4. Open the preview stage scene (player + ground + light + Mount).
/// 5. <c>EditorScene.Play(session)</c> — engine enters real Play mode.
///
/// No window minimize/restore (sbox editor doesn't expose that API publicly).
/// No Stop polling either — when the user stops play, they're back in the
/// preview stage scene; if they want their previous scene, they re-open it.
/// </summary>
public static class SuiPreviewLauncher
{
/// <summary>
/// Addon-relative path to the bundled preview stage scene. The AssetSystem
/// indexes Library assets under their addon-local path (without the
/// `Libraries/<addon>/Assets/` prefix), confirmed via engine log:
/// "On-demand recompile of asset sui_preview/preview_stage.scene"
/// </summary>
public const string PreviewStageScenePath = "sui_preview/preview_stage.scene";
/// <summary>
/// Fire-and-forget entry point — wires from the toolbar button click.
/// </summary>
public static void Launch( SuiDocument document )
{
_ = LaunchAsync( document );
}
private static async Task LaunchAsync( SuiDocument document )
{
if ( document == null )
{
Log.Warning( "[SuiPreviewLauncher] No document — open a .sui first." );
return;
}
var session = SceneEditorSession.Active;
if ( session == null )
{
Log.Warning( "[SuiPreviewLauncher] No active SceneEditorSession — can't enter Play." );
return;
}
if ( session.IsPlaying )
{
Log.Warning( "[SuiPreviewLauncher] Already in Play. Stop the current play first." );
return;
}
// 1. Compile to preview cache.
var write = SuiPreviewCacheWriter.Write( document );
if ( !write.Ok )
{
Log.Warning( $"[SuiPreviewLauncher] Compile failed: {string.Join( "; ", write.Errors )}" );
return;
}
Log.Info( $"[SuiPreviewLauncher] Compiled '{write.TypeFullName}' (razorChanged={write.RazorChanged}, scssChanged={write.ScssChanged})" );
// 2. Wait for hot-reload to make the generated type visible. Poll TypeLibrary.
// Engine usually picks up new .razor in ~200-800 ms; we cap at 6 s.
const int maxAttempts = 60;
const int delayMs = 100;
Sandbox.TypeDescription typeDesc = null;
for ( int i = 0; i < maxAttempts; i++ )
{
typeDesc = TypeLibrary.GetType( write.TypeFullName );
if ( typeDesc != null ) break;
await Task.Delay( delayMs );
}
if ( typeDesc == null )
{
Log.Warning( $"[SuiPreviewLauncher] Timed out waiting for type '{write.TypeFullName}' to compile (≈{maxAttempts * delayMs / 1000.0}s). The .razor was written; check the engine console for compile errors." );
return;
}
Log.Info( $"[SuiPreviewLauncher] Type loaded: {typeDesc.FullName}" );
// 3. Set the handoff state.
SuiPreviewState.Set( write.TypeFullName );
// 4. Open the preview stage scene in the editor session.
var stageAsset = AssetSystem.FindByPath( PreviewStageScenePath );
if ( stageAsset == null )
{
Log.Warning( $"[SuiPreviewLauncher] Preview stage not found at '{PreviewStageScenePath}'. Did the addon's Assets/sui_preview/preview_stage.scene get installed?" );
SuiPreviewState.Clear();
return;
}
try
{
stageAsset.OpenInEditor();
Log.Info( $"[SuiPreviewLauncher] Opened preview stage scene." );
}
catch ( Exception ex )
{
Log.Warning( $"[SuiPreviewLauncher] Failed to open preview stage: {ex.Message}" );
SuiPreviewState.Clear();
return;
}
// 5. Re-fetch session — opening a new scene may have created a new session.
session = SceneEditorSession.Active;
if ( session == null )
{
Log.Warning( "[SuiPreviewLauncher] Active session went null after scene load." );
SuiPreviewState.Clear();
return;
}
EditorScene.Play( session );
Log.Info( "[SuiPreviewLauncher] EditorScene.Play() invoked. UI should mount in OnAwake of SuiPreviewMount." );
}
}