using UnityEditor; using UnityEngine; using UnityTcp.Editor.Helpers; namespace UnityTcp.Editor.Windows { public class TcpBridgeControlWindow : EditorWindow { private enum BridgeState { NotStarted, Starting, Started } private double lastStatusUpdate; private const double StatusUpdateInterval = 0.5; private BridgeState currentState = BridgeState.NotStarted; private bool isStarting = false; private double startingTime = 0; private const double StartingTimeout = 3.0; // Window title icon private Texture2D titleIcon; // Client icons private Texture2D codelyIcon; private Texture2D vscodeIcon; private Texture2D visualStudioIcon; private Texture2D jetbrainsIcon; private Texture2D cliIcon; private Texture2D unityEditorIcon; private Texture2D tuanjieIcon; // Fonts private Font firaMono; private Font inter; // Button textures private Texture2D connectTex; private Texture2D connectHoverTex; private Texture2D connectPressedTex; private Texture2D connectingTex; private Texture2D disconnectTex; private Texture2D disconnectHoverTex; private Texture2D disconnectPressedTex; // Connection tracking private bool connectedToCLI; private bool connectedToVSCode; private bool connectedToVisualStudio; private bool connectedToJetBrains; private bool connectedToUnityEditor; private bool connectedToTuanjie; private static readonly Color GreenColor = new Color(0x01 / 255f, 0xA7 / 255f, 0x7F / 255f); private static readonly Color GrayColor = new Color(0.5f, 0.5f, 0.5f); private static readonly Color SubtextColor = new Color(0x9C / 255f, 0xA3 / 255f, 0xAF / 255f); // ── Scale ──────────────────────────────────────────────────────────────── private const float ScaleFactor = 2 / 1.5f; // ── Base sizes (ScaleFactor = 1) ───────────────────────────────────────── private const float BaseWindowWidth = 430f; private const float BaseLeftMargin = 25f; private const float BaseTopPadding = 40f; private const float BaseBottomPadding = 40f; private const float BaseCodelyIconSize = 40f; private const float BaseSpaceAfterIcon = 25f; private const int BaseTitleFontSize = 20; private const float BaseSpaceAfterTitle = 15f; private const int BaseBodyFontSize = 17; private const float BaseSpaceAfterBody = 30f; private const int BaseStatusFontSize = 12; private const float BaseSpaceBeforeStatus = 30f; private const float BaseMinWindowHeight = 80f; private const int BaseConnectedFontSize = 16; private const float BaseConnectedItemSpacing = 6f; private const float BaseClientIconSize = 24f; private const float BaseClientIconLabelGap = 6f; private const int BaseClientLabelFontSize = 13; private const float BaseClientLabelHeight = 18f; private const float BaseClientItemSpacing = 12f; private const float BaseConnectBtnW = 159f; private const float BaseConnectBtnH = 40f; // ── Scaled values ───────────────────────────────────────────────────────── private static float WindowWidth => BaseWindowWidth * ScaleFactor; private static float LeftMargin => BaseLeftMargin * ScaleFactor; private static float TopPadding => BaseTopPadding * ScaleFactor; private static float BottomPadding => BaseBottomPadding * ScaleFactor; private static float CodelyIconSize => BaseCodelyIconSize * ScaleFactor; private static float SpaceAfterIcon => BaseSpaceAfterIcon * ScaleFactor; private static int TitleFontSize => Mathf.RoundToInt(BaseTitleFontSize * ScaleFactor); private static float SpaceAfterTitle => BaseSpaceAfterTitle * ScaleFactor; private static int BodyFontSize => Mathf.RoundToInt(BaseBodyFontSize * ScaleFactor); private static float SpaceAfterBody => BaseSpaceAfterBody * ScaleFactor; private static int StatusFontSize => Mathf.RoundToInt(BaseStatusFontSize * ScaleFactor); private static float SpaceBeforeStatus => BaseSpaceBeforeStatus * ScaleFactor; private static float MinWindowHeight => BaseMinWindowHeight * ScaleFactor; private static int ConnectedFontSize => Mathf.RoundToInt(BaseConnectedFontSize * ScaleFactor); private static float ConnectedItemSpacing => BaseConnectedItemSpacing * ScaleFactor; private static float ClientIconSize => BaseClientIconSize * ScaleFactor; private static float ClientIconLabelGap => BaseClientIconLabelGap * ScaleFactor; private static int ClientLabelFontSize => Mathf.RoundToInt(BaseClientLabelFontSize * ScaleFactor); private static float ClientLabelHeight => BaseClientLabelHeight * ScaleFactor; private static float ClientItemSpacing => BaseClientItemSpacing * ScaleFactor; private static float ConnectBtnW => BaseConnectBtnW * ScaleFactor; private static float ConnectBtnH => BaseConnectBtnH * ScaleFactor; [MenuItem("Window/Codely Bridge", priority = 1001)] public static void ShowWindow() { var window = GetWindow("Codely Bridge"); window.minSize = new Vector2(WindowWidth, MinWindowHeight); window.maxSize = new Vector2(WindowWidth, 2000f); window.Show(); } private void OnEnable() { EditorPrefs.SetBool("UnityTcp.DebugLogs", false); LoadIcons(); UpdateStatus(); UnityTcpBridge.OnClientPlatformsChanged += OnClientPlatformsChanged; } private void OnDisable() { UnityTcpBridge.OnClientPlatformsChanged -= OnClientPlatformsChanged; } private void OnClientPlatformsChanged() => UpdateConnectionStatus(); private static Texture2D Load(string path) => AssetDatabase.LoadAssetAtPath(path); private void LoadIcons() { const string b = "Packages/cn.tuanjie.codely.bridge/Editor/Icons/"; titleIcon = Load(b + "title_icon.png"); codelyIcon = Load(b + "codely.png"); vscodeIcon = Load(b + "vscode.png"); visualStudioIcon = Load(b + "visualstudio.png"); jetbrainsIcon = Load(b + "jetbrains.png"); cliIcon = Load(b + "cli.png"); unityEditorIcon = Load(b + "unity_editor.png"); tuanjieIcon = Load(b + "tuanjie.png"); connectTex = Load(b + "connect.png"); connectHoverTex = Load(b + "connect_hover.png"); connectPressedTex = Load(b + "connect_pressed.png"); connectingTex = Load(b + "connecting.png"); disconnectTex = Load(b + "disconnect.png"); disconnectHoverTex = Load(b + "disconnect_hover.png"); disconnectPressedTex = Load(b + "disconnect_pressed.png"); firaMono = AssetDatabase.LoadAssetAtPath("Packages/cn.tuanjie.codely.bridge/Editor/fonts/FiraMono-Regular.ttf"); inter = AssetDatabase.LoadAssetAtPath("Packages/cn.tuanjie.codely.bridge/Editor/fonts/Inter.ttc"); if (titleIcon != null) titleContent = new GUIContent(" Codely Bridge", titleIcon); } private void OnGUI() { if (EditorApplication.timeSinceStartup - lastStatusUpdate > StatusUpdateInterval) { UpdateStatus(); lastStatusUpdate = EditorApplication.timeSinceStartup; } DrawUI(); } private static readonly Color BgColor = new Color(0.18f, 0.18f, 0.18f); private void DrawUI() { EditorGUI.DrawRect(new Rect(0, 0, position.width, position.height), BgColor); bool started = currentState == BridgeState.Started; // ── Top padding ────────────────────────────────────────────────────── GUILayout.Space(TopPadding); // ── Codely icon ────────────────────────────────────────────────────── GUILayout.BeginHorizontal(); GUILayout.Space(LeftMargin); if (codelyIcon != null) GUILayout.Box(codelyIcon, GUIStyle.none, GUILayout.Width(CodelyIconSize), GUILayout.Height(CodelyIconSize)); GUILayout.EndHorizontal(); GUILayout.Space(SpaceAfterIcon); // ── Title ──────────────────────────────────────────────────────────── GUILayout.BeginHorizontal(); GUILayout.Space(LeftMargin); var titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = TitleFontSize, wordWrap = false, font = inter }; if (started) { titleStyle.normal.textColor = GreenColor; titleStyle.hover.textColor = GreenColor; } else { titleStyle.normal.textColor = Color.white; titleStyle.hover.textColor = Color.white; } GUILayout.Label( started ? "Codely bridge is ready \u2713" : "Connect to Codely Agent", titleStyle); GUILayout.EndHorizontal(); GUILayout.Space(SpaceAfterTitle); // ── Body (Form 1: client list | Form 2: text + status) ─────────────── bool hasClients = started && (connectedToCLI || connectedToVSCode || connectedToVisualStudio || connectedToJetBrains || connectedToUnityEditor || connectedToTuanjie); GUILayout.BeginHorizontal(); GUILayout.Space(LeftMargin); GUILayout.BeginVertical(); if (hasClients) DrawClientListBody(); else DrawNoClientBody(); GUILayout.EndVertical(); GUILayout.EndHorizontal(); GUILayout.Space(SpaceAfterBody); // ── Action button ──────────────────────────────────────────────────── Texture2D btnNormal = null, btnPressed = null, btnHover = null; bool btnEnabled = true; System.Action btnAction = null; switch (currentState) { case BridgeState.NotStarted: btnNormal = connectTex; btnPressed = connectPressedTex; btnHover = connectHoverTex; btnAction = StartBridge; break; case BridgeState.Starting: btnNormal = connectingTex; btnPressed = connectingTex; btnHover = connectingTex; btnEnabled = false; break; case BridgeState.Started: btnNormal = disconnectTex; btnPressed = disconnectPressedTex; btnHover = disconnectHoverTex; btnAction = StopBridge; break; } GUILayout.BeginHorizontal(); GUILayout.Space(LeftMargin); if (btnNormal != null) { float btnW = ConnectBtnW; float btnH = ConnectBtnH; var style = new GUIStyle(GUIStyle.none); style.normal.background = btnNormal; style.active.background = btnPressed ?? btnNormal; style.hover.background = btnHover ?? btnNormal; if (!btnEnabled) GUI.enabled = false; if (GUILayout.Button(GUIContent.none, style, GUILayout.Width(btnW), GUILayout.Height(btnH)) && btnAction != null) btnAction(); if (!btnEnabled) GUI.enabled = true; } else { string fallback = currentState == BridgeState.NotStarted ? "Connect \u2192" : currentState == BridgeState.Starting ? "Connecting..." : "Disconnect"; if (!btnEnabled) GUI.enabled = false; if (GUILayout.Button(fallback, GUILayout.Width(ConnectBtnW), GUILayout.Height(ConnectBtnH)) && btnAction != null) btnAction(); if (!btnEnabled) GUI.enabled = true; } GUILayout.EndHorizontal(); // ── Bottom padding ─────────────────────────────────────────────────── GUILayout.Space(BottomPadding); // ── Auto-resize height to fit content ──────────────────────────────── if (Event.current.type == EventType.Repaint) { float targetH = Mathf.Max(MinWindowHeight, GUILayoutUtility.GetLastRect().yMax); if (Mathf.Abs(position.height - targetH) > 1f) { minSize = new Vector2(WindowWidth, targetH); maxSize = new Vector2(WindowWidth, targetH); } } } // Form 1 – bridge started and at least one client is connected private void DrawClientListBody() { var labelStyle = new GUIStyle(EditorStyles.label) { fontSize = ConnectedFontSize, font = inter }; labelStyle.normal.textColor = SubtextColor; labelStyle.hover.textColor = SubtextColor; GUILayout.BeginHorizontal(); // Left column: "Connected to :" label, top-aligned float labelW = labelStyle.CalcSize(new GUIContent("Connected to :")).x + 4f; GUILayout.Label("Connected to :", labelStyle, GUILayout.Width(labelW)); // Right column: all clients stacked vertically GUILayout.BeginVertical(); if (connectedToVSCode) DrawClient(vscodeIcon, "VS Code"); if (connectedToVisualStudio) DrawClient(visualStudioIcon, "Visual Studio"); if (connectedToJetBrains) DrawClient(jetbrainsIcon, "JetBrains"); if (connectedToCLI) DrawClient(cliIcon, "CLI"); if (connectedToUnityEditor) DrawClient(unityEditorIcon, "Unity Editor"); if (connectedToTuanjie) DrawClient(tuanjieIcon, "Tuanjie"); GUILayout.EndVertical(); GUILayout.EndHorizontal(); } // Form 2 – not started, starting, or started but no clients yet private void DrawNoClientBody() { string bodyText = currentState == BridgeState.Started ? "Waiting for clients..." : "Start Codely Bridge to connect to Codely Agents."; var bodyStyle = new GUIStyle(EditorStyles.label) { fontSize = BodyFontSize, wordWrap = true, font = inter }; bodyStyle.normal.textColor = SubtextColor; bodyStyle.hover.textColor = SubtextColor; GUILayout.Label(bodyText, bodyStyle); GUILayout.Space(SpaceBeforeStatus); string dots = new string('.', (int)(EditorApplication.timeSinceStartup * 2) % 3 + 1); string statusText = currentState == BridgeState.Starting ? $"\u2022 Status: Connecting{dots}" : currentState == BridgeState.Started ? "\u2022 Status: Waiting for clients..." : "\u2022 Status: Disconnected"; GUILayout.Label( statusText, new GUIStyle(EditorStyles.label) { fontSize = StatusFontSize, font = firaMono, normal = { textColor = GrayColor }, hover = { textColor = GrayColor } }); } private void DrawClient(Texture2D icon, string label) { GUILayout.BeginHorizontal(); if (icon != null) GUILayout.Box(icon, GUIStyle.none, GUILayout.Width(ClientIconSize), GUILayout.Height(ClientIconSize)); else GUILayout.Space(ClientIconSize); GUILayout.Space(ClientIconLabelGap); GUILayout.Label( label, new GUIStyle(EditorStyles.label) { fontSize = ClientLabelFontSize, font = inter, alignment = TextAnchor.MiddleLeft, normal = { textColor = Color.white }, hover = { textColor = Color.white } }, GUILayout.Height(ClientLabelHeight)); GUILayout.EndHorizontal(); GUILayout.Space(ClientItemSpacing); } private void StartBridge() { try { UnityTcpBridge.Start(); isStarting = true; startingTime = EditorApplication.timeSinceStartup; currentState = BridgeState.Starting; TcpLog.Info("Codely Bridge started via Control Window"); } catch (System.Exception ex) { TcpLog.Error($"Failed to start Codely Bridge: {ex.Message}"); currentState = BridgeState.NotStarted; isStarting = false; } } private void StopBridge() { try { UnityTcpBridge.Stop(true); TcpLog.Info("Codely Bridge manually stopped via Control Window"); currentState = BridgeState.NotStarted; isStarting = false; ResetConnections(); } catch (System.Exception ex) { TcpLog.Error($"Failed to stop Codely Bridge: {ex.Message}"); } } private void ResetConnections() { connectedToCLI = connectedToVSCode = connectedToVisualStudio = connectedToJetBrains = connectedToUnityEditor = connectedToTuanjie = false; } private void UpdateStatus() { bool isRunning = UnityTcpBridge.IsRunning; if (isStarting && isRunning) { currentState = BridgeState.Started; isStarting = false; } else if (isStarting && !isRunning) { if (EditorApplication.timeSinceStartup - startingTime > StartingTimeout) { currentState = BridgeState.NotStarted; isStarting = false; } } else if (!isStarting && isRunning && currentState != BridgeState.Started) { currentState = BridgeState.Started; } else if (!isRunning && currentState == BridgeState.Started) { currentState = BridgeState.NotStarted; ResetConnections(); } if (currentState == BridgeState.Started) UpdateConnectionStatus(); } private void UpdateConnectionStatus() { var clients = UnityTcpBridge.GetConnectedClients(); ResetConnections(); foreach (var platform in clients.Values) { var p = platform.ToLower(); if (p.Contains("cli")) connectedToCLI = true; else if (p.Contains("vscode") || p.Contains("vs code")) connectedToVSCode = true; else if (p.Contains("visualstudio") || p.Contains("visual studio")) connectedToVisualStudio = true; else if (p.Contains("jetbrains") || p.Contains("intellij") || p.Contains("rider")) connectedToJetBrains = true; else if (p.Contains("tuanjie") || p.Contains("codely") || p.Contains("agent")) connectedToTuanjie = true; else if (p.Contains("unity")) connectedToUnityEditor = true; } } private void OnInspectorUpdate() => Repaint(); } }