BreadcrumbDrawer.cs 9 KB
using System;
using System.Collections.Generic;
using UnityEngine;

namespace UnityEditor.Timeline
{
    enum TitleMode
    {
        None,
        DisabledComponent,
        Prefab,
        PrefabOutOfContext,
        Asset,
        GameObject
    }

    struct BreadCrumbTitle
    {
        public string name;
        public TitleMode mode;
    }

    class BreadcrumbDrawer
    {
        static readonly GUIContent s_TextContent = new GUIContent();
        static readonly string k_DisabledComponentText = L10n.Tr("The PlayableDirector is disabled");
        static readonly string k_PrefabOutOfContext = L10n.Tr("Prefab Isolation not enabled. Click to Enable.");

        static readonly GUIStyle k_BreadCrumbLeft;
        static readonly GUIStyle k_BreadCrumbMid;
        static readonly GUIStyle k_BreadCrumbLeftBg;
        static readonly GUIStyle k_BreadCrumbMidBg;
        static readonly GUIStyle k_BreadCrumbMidSelected;
        static readonly GUIStyle k_BreadCrumbMidBgSelected;

        static readonly Texture k_TimelineIcon;

        const string k_Elipsis = "…";

        static BreadcrumbDrawer()
        {
            k_BreadCrumbLeft = "GUIEditor.BreadcrumbLeft";
            k_BreadCrumbMid = "GUIEditor.BreadcrumbMid";
            k_BreadCrumbLeftBg = "GUIEditor.BreadcrumbLeftBackground";
            k_BreadCrumbMidBg = "GUIEditor.BreadcrumbMidBackground";

            k_BreadCrumbMidSelected = k_BreadCrumbMid;
            k_BreadCrumbMidSelected.normal = k_BreadCrumbMidSelected.onNormal;

            k_BreadCrumbMidBgSelected = k_BreadCrumbMidBg;
            k_BreadCrumbMidBgSelected.normal = k_BreadCrumbMidBgSelected.onNormal;
            k_TimelineIcon = EditorGUIUtility.IconContent("TimelineAsset Icon").image;
        }

        static string FitTextInArea(float areaWidth, string text, GUIStyle style)
        {
            var borderWidth = style.border.left + style.border.right;
            var textWidth = style.CalcSize(EditorGUIUtility.TextContent(text)).x;

            if (borderWidth + textWidth < areaWidth)
                return text;

            // Need to truncate the text to fit in the areaWidth
            var textAreaWidth = areaWidth - borderWidth;
            var pixByChar = textWidth / text.Length;
            var charNeeded = (int)Mathf.Floor(textAreaWidth / pixByChar);
            charNeeded -= k_Elipsis.Length;

            if (charNeeded <= 0)
                return k_Elipsis;

            if (charNeeded <= text.Length)
                return k_Elipsis + " " + text.Substring(text.Length - charNeeded);

            return k_Elipsis;
        }

        public static void Draw(float breadcrumbAreaWidth, List<BreadCrumbTitle> labels, Action<int> navigateToBreadcrumbIndex)
        {
            GUILayout.BeginHorizontal();
            {
                var labelWidth = (int)(breadcrumbAreaWidth / labels.Count);

                for (var i = 0; i < labels.Count; i++)
                {
                    var label = labels[i];

                    var style = i == 0 ? k_BreadCrumbLeft : k_BreadCrumbMid;
                    var backgroundStyle = i == 0 ? k_BreadCrumbLeftBg : k_BreadCrumbMidBg;

                    if (i == labels.Count - 1)
                    {
                        if (i > 0) // Only tint last breadcrumb if we are dug-in
                            DrawBreadcrumbAsSelectedSubSequence(labelWidth, label, k_BreadCrumbMidSelected, k_BreadCrumbMidBgSelected);
                        else
                            DrawActiveBreadcrumb(labelWidth, label, style, backgroundStyle);
                    }
                    else
                    {
                        var previousContentColor = GUI.contentColor;
                        GUI.contentColor = new Color(previousContentColor.r,
                            previousContentColor.g,
                            previousContentColor.b,
                            previousContentColor.a * 0.6f);
                        var content = GetTextContent(labelWidth, label, style);
                        var rect = GetBreadcrumbLayoutRect(content, style);

                        if (Event.current.type == EventType.Repaint)
                            backgroundStyle.Draw(rect, GUIContent.none, 0);

                        if (GUI.Button(rect, content, style))
                            navigateToBreadcrumbIndex.Invoke(i);
                        GUI.contentColor = previousContentColor;
                    }
                }
            }
            GUILayout.EndHorizontal();
        }

        static GUIContent GetTextContent(int width, BreadCrumbTitle text, GUIStyle style)
        {
            s_TextContent.tooltip = string.Empty;
            s_TextContent.image = null;
            if (text.mode == TitleMode.DisabledComponent)
            {
                s_TextContent.tooltip = k_DisabledComponentText;
                s_TextContent.image = EditorGUIUtility.GetHelpIcon(MessageType.Warning);
            }
            else if (text.mode == TitleMode.Prefab)
                s_TextContent.image = PrefabUtility.GameObjectStyles.prefabIcon;
            else if (text.mode == TitleMode.GameObject)
                s_TextContent.image = PrefabUtility.GameObjectStyles.gameObjectIcon;
            else if (text.mode == TitleMode.Asset)
                s_TextContent.image = k_TimelineIcon;
            else if (text.mode == TitleMode.PrefabOutOfContext)
            {
                s_TextContent.image = PrefabUtility.GameObjectStyles.prefabIcon;
                if (!TimelineWindow.instance.locked)
                    s_TextContent.tooltip = k_PrefabOutOfContext;
            }

            if (s_TextContent.image != null)
                width = Math.Max(0, width - s_TextContent.image.width);
            s_TextContent.text = FitTextInArea(width, text.name, style);

            return s_TextContent;
        }

        static void DrawBreadcrumbAsSelectedSubSequence(int width, BreadCrumbTitle label, GUIStyle style, GUIStyle backgroundStyle)
        {
            var rect = DrawActiveBreadcrumb(width, label, style, backgroundStyle);
            const float underlineThickness = 2.0f;
            const float underlineVerticalOffset = 0.0f;
            var underlineHorizontalOffset = backgroundStyle.border.right * 0.333f;
            var underlineRect = Rect.MinMaxRect(
                rect.xMin - underlineHorizontalOffset,
                rect.yMax - underlineThickness - underlineVerticalOffset,
                rect.xMax - underlineHorizontalOffset,
                rect.yMax - underlineVerticalOffset);

            EditorGUI.DrawRect(underlineRect, DirectorStyles.Instance.customSkin.colorSubSequenceDurationLine);
        }

        static Rect GetBreadcrumbLayoutRect(GUIContent content, GUIStyle style)
        {
            // the image makes the button far too big compared to non-image versions
            var image = content.image;
            content.image = null;
            var size = style.CalcSizeWithConstraints(content, Vector2.zero);
            content.image = image;
            if (image != null)
                size.x += size.y; // assumes square image, constrained by height

            return GUILayoutUtility.GetRect(content, style, GUILayout.MaxWidth(size.x));
        }

        static Rect DrawActiveBreadcrumb(int width, BreadCrumbTitle label, GUIStyle style, GUIStyle backgroundStyle)
        {
            var content = GetTextContent(width, label, style);
            var rect = GetBreadcrumbLayoutRect(content, style);

            if (Event.current.type == EventType.Repaint)
            {
                backgroundStyle.Draw(rect, GUIContent.none, 0);
            }

            if (GUI.Button(rect, content, style))
            {
                UnityEngine.Object target = TimelineEditor.inspectedDirector;
                if (target == null)
                    target = TimelineEditor.inspectedAsset;
                if (target != null)
                {
                    bool ping = true;
                    if (label.mode == TitleMode.PrefabOutOfContext)
                    {
                        var gameObject = PrefabUtility.GetRootGameObject(target);
                        if (gameObject != null)
                        {
                            target = gameObject;  // ping the prefab root if it's locked.
                            if (!TimelineWindow.instance.locked)
                            {
                                var assetPath = AssetDatabase.GetAssetPath(gameObject);
                                if (!string.IsNullOrEmpty(assetPath))
                                {
                                    var stage = UnityEditor.Experimental.SceneManagement.PrefabStageUtility.OpenPrefab(assetPath);
                                    if (stage != null)
                                        ping = false;
                                }
                            }
                        }
                    }

                    if (ping)
                    {
                        EditorGUIUtility.PingObject(target);
                    }
                }
            }

            return rect;
        }
    }
}