Editor/GlobalChatDock.cs

Editor dock widget that hosts the Global Chat Razor surface inside the editor layout. It creates and manages a GlobalChatWindow, attaches a native host for embedding, synchronizes state on editor frames, handles open/hide/resize and cleans up on destruction.

Native Interop
using Sandbox;
using System;
using System.Linq;

namespace Editor.GlobalChat;

/// <summary>Places the Razor chat surface in the editor's normal dock layout.</summary>
[Dock( "Editor", "Global Chat", "forum", DockArea.Bottom )]
public sealed class GlobalChatDock : Widget
{
	internal GlobalChatWindow ChatWindow { get; private set; }
	ChatDockNativeHost nativeHost;
	internal bool SurfaceAttached => ChatWindow is { IsOpen: true } && nativeHost is not null && nativeHost.IsAttached;
	internal IntPtr NativeHandle => nativeHost?.Handle ?? IntPtr.Zero;
	internal static DockWidget Dock => EditorWindow.IsValid() && EditorWindow.DockManager.IsValid()
		? EditorWindow.DockManager.FindDockWidget( "Global Chat" ) : null;

	public GlobalChatDock( Widget parent ) : base( parent )
	{
		MinimumSize = new Vector2( 540, 420 );
		FocusMode = FocusMode.TabOrClick;
		OnPaintOverride = SuppressParentPaint;
	}

	// A named callback survives hotload when constructor fields change.
	static bool SuppressParentPaint()
	{
		return true;
	}

	internal static void Open()
	{
		// Restore the library registration if project startup cleared the dock types.
		if ( !EditorWindow.DockManager.DockTypes.Any( info => info.Title == "Global Chat" ) )
			EditorWindow.DockManager.RegisterDockType( new DockManager.DockInfo { Title = "Global Chat", Icon = "forum", Area = DockArea.Bottom, CreateAction = CreateDock } );
		EditorWindow.DockManager.SetDockState( "Global Chat", true );
		var dock = Dock ?? throw new InvalidOperationException( "The editor has not registered the Global Chat dock." );
		dock.MinimumSizeFromContent = true;
		dock.SetAsCurrentTab();
		var host = dock.Widget as GlobalChatDock ?? throw new InvalidOperationException( "Global Chat has an unexpected dock host." );
		host.EnsureWindow();
		host.Synchronize();
		host.Focus();
	}

	static Widget CreateDock()
	{
		return new GlobalChatDock( EditorWindow );
	}

	internal static void Hide( GlobalChatWindow window )
	{
		var dock = Dock;
		if ( dock is not null && dock.Widget is GlobalChatDock host && host.ChatWindow == window ) dock.ToggleView( false );
	}

	void EnsureWindow()
	{
		if ( ChatWindow is { IsOpen: true } ) return;
		ChatWindow = GlobalChatWindow.CreateDocked();
		try { nativeHost = new ChatDockNativeHost( ChatWindow, this ); }
		catch { ChatWindow.Close(); ChatWindow = null; throw; }
	}

	[EditorEvent.Frame]
	internal void Synchronize()
	{
		// Rebind existing docks as well as docks made after a code reload.
		OnPaintOverride = SuppressParentPaint;
		var dock = EditorWindow.DockManager.FindDockWidget( this );
		if ( dock is null || dock.IsClosed )
		{
			nativeHost?.Dispose();
			ChatWindow?.CloseSurface();
			ChatWindow = null;
			nativeHost = null;
			return;
		}
		dock.MinimumSizeFromContent = true;
		if ( !Visible ) { ChatWindow?.Hide(); return; }
		EnsureWindow();
		nativeHost.UpdateBounds();
		if ( !ChatWindow.IsVisible ) ChatWindow.Show();
	}

	internal static void ResizeFloating( Vector2 size )
	{
		var dock = Dock;
		if ( dock is null || !dock.IsFloating ) throw new InvalidOperationException( "Float Global Chat before requesting a capture size, or omit width and height to capture the dock area." );
		var host = (GlobalChatDock)dock.Widget;
		var floatingWindow = host.GetWindow();
		floatingWindow.Size += size - host.Size;
		host.Synchronize();
	}

	public override void OnDestroyed()
	{
		// The native widget is already gone. Release chat without querying its dock.
		nativeHost?.Dispose();
		ChatWindow?.CloseSurface();
		ChatWindow = null;
		nativeHost = null;
		base.OnDestroyed();
	}
}