using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEditor; #if UNITY_2019_1_OR_NEWER using UnityEngine.UIElements; #else using UnityEngine.Experimental.UIElements; #endif namespace Unity.Tutorials.Core.Editor { using static Localization; /// /// Manages masking and highlighting. /// internal static class MaskingManager { /// /// Master control for masking and highlighting. /// public static UserSetting MaskingEnabled = new UserSetting("IET.MaskingEnabled", Tr("Enable Masking and Highlighting"), true, Tr("Master control for Masking and Highlighting")); /// /// Delay, in seconds, before the highlight starts pulsating. /// public static float HighlightAnimationDelay { get; set; } /// /// Speed of the highligh pulsation. /// public static float HighlightAnimationSpeed { get; set; } internal static bool IsMasked(GUIViewProxy view, List rects) { rects.Clear(); MaskViewData maskViewData; if (s_UnmaskedViews.TryGetValue(view, out maskViewData)) { var rectList = maskViewData.rects; rects.AddRange(rectList); return false; } return true; } internal static bool IsHighlighted(GUIViewProxy view, List rects) { rects.Clear(); MaskViewData maskViewData; if (!s_HighlightedViews.TryGetValue(view, out maskViewData)) return false; var rectList = maskViewData.rects; rects.AddRange(rectList); return true; } static GUIViewProxyComparer s_GUIViewProxyComparer = new GUIViewProxyComparer(); static readonly Dictionary s_UnmaskedViews = new Dictionary(s_GUIViewProxyComparer); static readonly Dictionary s_HighlightedViews = new Dictionary(s_GUIViewProxyComparer); static readonly List s_Masks = new List(); static readonly List s_Highlighters = new List(); static double s_LastHighlightTime; static MaskingManager() { EditorApplication.update += delegate { // do not animate unless enough time has passed since masking was last applied var t = EditorApplication.timeSinceStartup - s_LastHighlightTime - HighlightAnimationDelay; if (t < 0d) return; var baseBorderWidth = 4.2f; var borderWidthAmplitude = 2.1f; var animatedBorderWidth = Mathf.Cos((float)t * HighlightAnimationSpeed) * borderWidthAmplitude + baseBorderWidth; foreach (var highlighter in s_Highlighters) { if (highlighter == null) continue; highlighter.style.borderLeftWidth = animatedBorderWidth; highlighter.style.borderRightWidth = animatedBorderWidth; highlighter.style.borderTopWidth = animatedBorderWidth; highlighter.style.borderBottomWidth = animatedBorderWidth; } foreach (var view in s_HighlightedViews) { if (view.Key.IsValid) view.Key.Repaint(); } }; } /// /// Unmasks all views. /// public static void Unmask() { foreach (var mask in s_Masks) { if (mask != null && mask.parent != null) mask.parent.Remove(mask); } s_Masks.Clear(); foreach (var highlighter in s_Highlighters) { if (highlighter != null && highlighter.parent != null) highlighter.parent.Remove(highlighter); } s_Highlighters.Clear(); } static void CopyMaskData(UnmaskedView.MaskData maskData, Dictionary viewsAndResources) { viewsAndResources.Clear(); foreach (var unmaskedView in maskData.m_MaskData) { if (unmaskedView.Key == null) continue; var maskViewData = unmaskedView.Value; var unmaskedRegions = maskViewData.rects == null ? new List(1) : maskViewData.rects.ToList(); if (unmaskedRegions.Count == 0) unmaskedRegions.Add(new Rect(0f, 0f, unmaskedView.Key.Position.width, unmaskedView.Key.Position.height)); viewsAndResources[unmaskedView.Key] = new MaskViewData { maskType = maskViewData.maskType, rects = unmaskedRegions, EditorWindowType = maskViewData.EditorWindowType }; } } /// /// Adds a mask for a view. /// /// /// static void AddMaskToView(GUIViewProxy view, VisualElement child) { // Since 2019.3(?), we must suppress input to the elements behind masks. // TODO Doesn't suppress everything, e.g. tooltips are shown still. child.RegisterCallback((e) => e.StopPropagation()); child.RegisterCallback((e) => e.StopPropagation()); child.RegisterCallback((e) => e.StopPropagation()); child.RegisterCallback((e) => e.StopPropagation()); child.RegisterCallback((e) => e.StopPropagation()); child.RegisterCallback((e) => e.StopPropagation()); child.RegisterCallback((e) => e.StopPropagation()); child.RegisterCallback((e) => e.StopPropagation()); child.RegisterCallback((e) => e.StopPropagation()); if (view.IsDockedToEditor()) { UIElementsHelper.GetVisualTree(view).Add(child); } else { var viewVisualElement = UIElementsHelper.GetVisualTree(view); Debug.Assert( viewVisualElement.Children().Count() == 2 && viewVisualElement.Children().Count(viewChild => viewChild is IMGUIContainer) == 1, "Could not find the expected VisualElement structure" ); foreach (var visualElement in viewVisualElement.Children()) { if (!(visualElement is IMGUIContainer)) { visualElement.Add(child); break; } } } } /// /// Applies masking. /// /// /// /// /// /// /// public static void Mask( UnmaskedView.MaskData unmaskedViewsAndRegionsMaskData, Color maskColor, UnmaskedView.MaskData highlightedRegionsMaskData, Color highlightColor, Color blockedInteractionsColor, float highlightThickness ) { Unmask(); CopyMaskData(unmaskedViewsAndRegionsMaskData, s_UnmaskedViews); CopyMaskData(highlightedRegionsMaskData, s_HighlightedViews); List views = new List(); GUIViewDebuggerHelperProxy.GetViews(views); foreach (var view in views) { if (!view.IsValid) continue; MaskViewData maskViewData; var viewRect = new Rect(0, 0, view.Position.width, view.Position.height); // mask everything except the unmasked view rects if (s_UnmaskedViews.TryGetValue(view, out maskViewData)) { // Beginning from 2021.2 the layout of floating/undocked EditorWindows has changed a bit and now contains // an offset caused by the tab area which we need to take into account. EditorWindow parentWindow = null; if (maskViewData.EditorWindowType != null) parentWindow = FindOpenEditorWindowInstance(maskViewData.EditorWindowType); List rects = maskViewData.rects; var maskedRects = GetNegativeSpaceRects(viewRect, rects); for (var i = 0; i < maskedRects.Count; ++i) { var rect = maskedRects[i]; if (parentWindow != null && !parentWindow.IsDocked()) { // In theory we could have an X offset also but it seems highgly unlikely. rect.y -= parentWindow.rootVisualElement.layout.y; } var mask = new VisualElement(); mask.style.backgroundColor = maskColor; mask.SetLayout(rect); AddMaskToView(view, mask); s_Masks.Add(mask); } if (maskViewData.maskType == MaskType.BlockInteractions) { foreach (var rect in rects) { var mask = new VisualElement(); mask.style.backgroundColor = blockedInteractionsColor; mask.SetLayout(rect); AddMaskToView(view, mask); s_Masks.Add(mask); } } } else // mask the whole view { var mask = new VisualElement(); mask.style.backgroundColor = maskColor; mask.SetLayout(viewRect); AddMaskToView(view, mask); s_Masks.Add(mask); } if (s_HighlightedViews.TryGetValue(view, out maskViewData)) { var rects = maskViewData.rects; // unclip highlight to apply as "outer stroke" if it is being applied to some control(s) in the view var unclip = rects.Count > 1 || rects[0] != viewRect; var borderRadius = 5.0f; foreach (var rect in rects) { var highlighter = new VisualElement(); #if UNITY_2019_3_OR_NEWER highlighter.style.borderLeftColor = highlightColor; highlighter.style.borderRightColor = highlightColor; highlighter.style.borderTopColor = highlightColor; highlighter.style.borderBottomColor = highlightColor; #else highlighter.style.borderColor = highlightColor; #endif highlighter.style.borderLeftWidth = highlightThickness; highlighter.style.borderRightWidth = highlightThickness; highlighter.style.borderTopWidth = highlightThickness; highlighter.style.borderBottomWidth = highlightThickness; highlighter.style.borderBottomLeftRadius = borderRadius; highlighter.style.borderTopLeftRadius = borderRadius; highlighter.style.borderBottomRightRadius = borderRadius; highlighter.style.borderTopRightRadius = borderRadius; highlighter.pickingMode = PickingMode.Ignore; var layout = rect; if (unclip) { layout.xMin -= highlightThickness; layout.xMax += highlightThickness; layout.yMin -= highlightThickness; layout.yMax += highlightThickness; } highlighter.SetLayout(layout); UIElementsHelper.GetVisualTree(view).Add(highlighter); s_Highlighters.Add(highlighter); } } } s_LastHighlightTime = EditorApplication.timeSinceStartup; } static EditorWindow FindOpenEditorWindowInstance(System.Type type) => Resources.FindObjectsOfTypeAll(type).FirstOrDefault() as EditorWindow; static readonly HashSet s_YCoords = new HashSet(); static readonly HashSet s_XCoords = new HashSet(); static readonly List s_SortedYCoords = new List(); static readonly List s_SortedXCoords = new List(); internal static List GetNegativeSpaceRects(Rect viewRect, List positiveSpaceRects) { //TODO maybe its okay to round to int? s_YCoords.Clear(); s_XCoords.Clear(); for (int i = 0; i < positiveSpaceRects.Count; i++) { var hole = positiveSpaceRects[i]; s_YCoords.Add(hole.y); s_YCoords.Add(hole.yMax); s_XCoords.Add(hole.x); s_XCoords.Add(hole.xMax); } s_YCoords.Add(0); s_YCoords.Add(viewRect.height); s_XCoords.Add(0); s_XCoords.Add(viewRect.width); s_SortedYCoords.Clear(); s_SortedXCoords.Clear(); s_SortedYCoords.AddRange(s_YCoords); s_SortedXCoords.AddRange(s_XCoords); s_SortedYCoords.Sort(); s_SortedXCoords.Sort(); var filledRects = new List(); for (var i = 1; i < s_SortedYCoords.Count; ++i) { var minY = s_SortedYCoords[i - 1]; var maxY = s_SortedYCoords[i]; var midY = (maxY + minY) / 2; var workingRect = new Rect(s_SortedXCoords[0], minY, 0, (maxY - minY)); for (var j = 1; j < s_SortedXCoords.Count; ++j) { var minX = s_SortedXCoords[j - 1]; var maxX = s_SortedXCoords[j]; var midX = (maxX + minX) / 2; var potentialHole = positiveSpaceRects.Find((hole) => { return hole.Contains(new Vector2(midX, midY)); }); var cellIsHole = potentialHole.width > 0 && potentialHole.height > 0; if (cellIsHole) { if (workingRect.width > 0 && workingRect.height > 0) filledRects.Add(workingRect); workingRect.x = maxX; workingRect.xMax = maxX; } else { workingRect.xMax = maxX; } } if (workingRect.width > 0 && workingRect.height > 0) filledRects.Add(workingRect); } return filledRects; } } }