diff --git a/Editor/Mono/BuildProfile/BuildProfile.cs b/Editor/Mono/BuildProfile/BuildProfile.cs index fe8868c1c6..2b5dbf9d1d 100644 --- a/Editor/Mono/BuildProfile/BuildProfile.cs +++ b/Editor/Mono/BuildProfile/BuildProfile.cs @@ -22,6 +22,12 @@ namespace UnityEditor.Build.Profile [HelpURL("build-profiles-reference")] public sealed partial class BuildProfile : ScriptableObject { + /// + /// Asset Schema Version + /// + [SerializeField] + uint m_AssetVersion = 1; + /// /// Build Target used to fetch module and build profile extension. /// @@ -78,6 +84,17 @@ internal BuildProfilePlatformSettingsBase platformBuildProfile set => m_PlatformBuildProfile = value; } + /// + /// When set, this build profiles used when building. + /// + /// + [SerializeField] private bool m_OverrideGlobalSceneList = false; + internal bool overrideGlobalSceneList + { + get => m_OverrideGlobalSceneList; + set => m_OverrideGlobalSceneList = value; + } + /// /// List of scenes specified in the build profile. /// @@ -97,7 +114,7 @@ public EditorBuildSettingsScene[] scenes m_Scenes = value; CheckSceneListConsistency(); - if (this == BuildProfileContext.activeProfile) + if (this == BuildProfileContext.activeProfile && m_OverrideGlobalSceneList) EditorBuildSettings.SceneListChanged(); } } @@ -202,7 +219,7 @@ void OnEnable() void OnDisable() { - if (IsActiveBuildProfileOrPlatform()) + if (BuildProfileContext.activeProfile == this) EditorUserBuildSettings.SetActiveProfileScriptingDefines(m_ScriptingDefines); var playerSettingsDirty = EditorUtility.IsDirty(m_PlayerSettings); diff --git a/Editor/Mono/BuildTargetDiscovery.bindings.cs b/Editor/Mono/BuildTargetDiscovery.bindings.cs index ac8825c16e..85552ce76b 100644 --- a/Editor/Mono/BuildTargetDiscovery.bindings.cs +++ b/Editor/Mono/BuildTargetDiscovery.bindings.cs @@ -349,7 +349,6 @@ internal static bool DoesBuildTargetSupportSinglePassStereoRendering(BuildTarget s_platform_43, s_platform_45, s_platform_46, - s_platform_47, s_platform_48, }; diff --git a/Editor/Mono/EditorBuildSettings.bindings.cs b/Editor/Mono/EditorBuildSettings.bindings.cs index 52d18935d0..bda07fd7d0 100644 --- a/Editor/Mono/EditorBuildSettings.bindings.cs +++ b/Editor/Mono/EditorBuildSettings.bindings.cs @@ -86,7 +86,8 @@ public static EditorBuildSettingsScene[] scenes { get { - if (BuildProfileContext.activeProfile is not null) + if (BuildProfileContext.activeProfile is not null + && BuildProfileContext.activeProfile.overrideGlobalSceneList) { return BuildProfileContext.activeProfile.scenes; } @@ -95,7 +96,8 @@ public static EditorBuildSettingsScene[] scenes } set { - if (BuildProfileContext.activeProfile is not null) + if (BuildProfileContext.activeProfile is not null + && BuildProfileContext.activeProfile.overrideGlobalSceneList) { BuildProfileContext.activeProfile.scenes = value; } @@ -109,7 +111,9 @@ public static EditorBuildSettingsScene[] scenes [RequiredByNativeCode] static EditorBuildSettingsScene[] GetActiveBuildProfileSceneList() { - if (!EditorUserBuildSettings.isBuildProfileAvailable || BuildProfileContext.activeProfile is null) + if (!EditorUserBuildSettings.isBuildProfileAvailable + || BuildProfileContext.activeProfile is null + || !BuildProfileContext.activeProfile.overrideGlobalSceneList) return null; return BuildProfileContext.activeProfile.scenes; diff --git a/Editor/Mono/EditorWindow.cs b/Editor/Mono/EditorWindow.cs index 91af95b475..ade759998f 100644 --- a/Editor/Mono/EditorWindow.cs +++ b/Editor/Mono/EditorWindow.cs @@ -1081,6 +1081,12 @@ public void Close() if (WindowLayout.IsMaximized(this)) WindowLayout.Unmaximize(this); + // [UUM-58449] If the focused window got closed, reset the IME composition mode to the default value. The normal codepaths may not run since this object is immediately destroyed. + if (focusedWindow == this) + { + GUIUtility.imeCompositionMode = IMECompositionMode.Auto; + } + DockArea da = m_Parent as DockArea; if (da) { diff --git a/Editor/Mono/GI/Lightmapping.bindings.cs b/Editor/Mono/GI/Lightmapping.bindings.cs index d0c549a00d..314a1bf5b6 100644 --- a/Editor/Mono/GI/Lightmapping.bindings.cs +++ b/Editor/Mono/GI/Lightmapping.bindings.cs @@ -100,6 +100,13 @@ public enum GIWorkflowMode Legacy = 2 } + [NativeHeader("Runtime/Graphics/LightmapSettings.h")] + public enum BakeOnSceneLoadMode + { + Never = 0, + IfMissingLightingData = 1, + }; + // Obsolete, please use Actions instead public delegate void OnStartedFunction(); public delegate void OnCompletedFunction(); @@ -447,6 +454,9 @@ public static void Tetrahedralize(Vector3[] positions, out int[] outIndices, out [FreeFunction] public static extern void GetTerrainGIChunks([NotNull] Terrain terrain, ref int numChunksX, ref int numChunksY); + [StaticAccessor("GetLightmapSettings()")] + public static extern BakeOnSceneLoadMode bakeOnSceneLoad { get; set; } + [StaticAccessor("GetLightmapSettings()")] public static extern LightingDataAsset lightingDataAsset { get; set; } diff --git a/Editor/Mono/GUI/EditorStyles.cs b/Editor/Mono/GUI/EditorStyles.cs index 412d882124..341eb3260c 100644 --- a/Editor/Mono/GUI/EditorStyles.cs +++ b/Editor/Mono/GUI/EditorStyles.cs @@ -257,6 +257,9 @@ public sealed class EditorStyles public static GUIStyle inspectorDefaultMargins { get { return s_Current.m_InspectorDefaultMargins; } } private GUIStyle m_InspectorDefaultMargins; + internal static GUIStyle inspectorHorizontalDefaultMargins => s_Current.m_InspectorHorizontalDefaultMargins; + private GUIStyle m_InspectorHorizontalDefaultMargins; + public static GUIStyle inspectorFullWidthMargins { get { return s_Current.m_InspectorFullWidthMargins; } } private GUIStyle m_InspectorFullWidthMargins; @@ -564,6 +567,11 @@ private void InitSharedStyles() padding = new RectOffset(kInspectorPaddingLeft, kInspectorPaddingRight, kInspectorPaddingTop, 0) }; + m_InspectorHorizontalDefaultMargins = new GUIStyle + { + padding = new RectOffset(kInspectorPaddingLeft, kInspectorPaddingRight, 0, 0) + }; + // For the full width margins, use padding from right side in both sides, // though adjust for overdraw by adding one in left side to get even margins. m_InspectorFullWidthMargins = new GUIStyle diff --git a/Editor/Mono/Inspector/Core/ScriptAttributeGUI/PropertyHandler.cs b/Editor/Mono/Inspector/Core/ScriptAttributeGUI/PropertyHandler.cs index 45e89be784..b4d045eeae 100644 --- a/Editor/Mono/Inspector/Core/ScriptAttributeGUI/PropertyHandler.cs +++ b/Editor/Mono/Inspector/Core/ScriptAttributeGUI/PropertyHandler.cs @@ -300,11 +300,7 @@ internal bool OnGUI(Rect position, SerializedProperty property, GUIContent label if (childrenAreExpanded) { SerializedProperty endProperty = prop.GetEndProperty(); - // Children need to be indented - int prevIndent = EditorGUI.indentLevel; - EditorGUI.indentLevel++; - position = EditorGUI.IndentedRect(position); - EditorGUI.indentLevel = prevIndent; + while (prop.NextVisible(childrenAreExpanded) && !SerializedProperty.EqualContents(prop, endProperty)) { if (GUI.isInsideList && prop.depth <= EditorGUI.GetInsideListDepth()) diff --git a/Editor/Mono/Overlays/Overlay.cs b/Editor/Mono/Overlays/Overlay.cs index bbe2447b1d..392d5f5b2c 100644 --- a/Editor/Mono/Overlays/Overlay.cs +++ b/Editor/Mono/Overlays/Overlay.cs @@ -766,6 +766,11 @@ internal void ToggleCollapsedPopup() m_ModalPopup.Focus(); } + public void RefreshPopup() + { + m_ModalPopup?.Refresh(); + } + void ClosePopup() { m_ModalPopup?.RemoveFromHierarchy(); diff --git a/Editor/Mono/Overlays/OverlayPopup.cs b/Editor/Mono/Overlays/OverlayPopup.cs index e56f4d8b7a..3b7b54f3e7 100644 --- a/Editor/Mono/Overlays/OverlayPopup.cs +++ b/Editor/Mono/Overlays/OverlayPopup.cs @@ -33,13 +33,26 @@ class OverlayPopup : VisualElement AddToClassList(Overlay.ussClassName); style.position = Position.Absolute; + Refresh(); + + RegisterCallback(evt => m_CursorIsOverPopup = true); + RegisterCallback(evt => m_CursorIsOverPopup = false); + } + + public void Refresh() + { var root = this.Q("overlay-content"); + + root.Clear(); + root.renderHints = RenderHints.ClipWithScissors; + style.maxHeight = StyleKeyword.Initial; + style.maxWidth = StyleKeyword.Initial; + root.Add(overlay.GetSimpleHeader()); root.Add(overlay.CreatePanelContent()); - RegisterCallback(evt => m_CursorIsOverPopup = true); - RegisterCallback(evt => m_CursorIsOverPopup = false); + root.Focus(); } public static OverlayPopup CreateUnderOverlay(Overlay overlay) diff --git a/Editor/Mono/PlayerSettingsVulkan.bindings.cs b/Editor/Mono/PlayerSettingsVulkan.bindings.cs index 4486f94e1f..44d40568cb 100644 --- a/Editor/Mono/PlayerSettingsVulkan.bindings.cs +++ b/Editor/Mono/PlayerSettingsVulkan.bindings.cs @@ -10,7 +10,27 @@ namespace UnityEditor public partial class PlayerSettings : UnityEngine.Object { public static extern bool vulkanEnableSetSRGBWrite { get; set; } - public static extern UInt32 vulkanNumSwapchainBuffers { get; set; } + + private static extern UInt32 GetVulkanNumSwapchainBuffersImpl(); + private static extern void SetVulkanNumSwapchainBuffersImpl(UInt32 value); + + // NOTE: While in the editor, changing this value can be destructive so we force 3 swapchain buffers while running in the editor. + public static UInt32 vulkanNumSwapchainBuffers + { + get + { + // Must match the value PlayerSettings::kFixedEditorVulkanSwapchainBufferCount in native code, + // explicitly report the current value being used. + const UInt32 kFixedEditorVulkanSwapchainBufferCount = 3; + if (EditorApplication.isPlaying) + return kFixedEditorVulkanSwapchainBufferCount; + else + return GetVulkanNumSwapchainBuffersImpl(); + } + + set => SetVulkanNumSwapchainBuffersImpl(value); + } + public static extern bool vulkanEnableLateAcquireNextImage { get; set; } [Obsolete("Vulkan SW command buffers are deprecated, vulkanUseSWCommandBuffers will be ignored.")] diff --git a/Editor/Mono/SceneModeWindows/LightingWindow.cs b/Editor/Mono/SceneModeWindows/LightingWindow.cs index 4728dd9222..44566460cb 100644 --- a/Editor/Mono/SceneModeWindows/LightingWindow.cs +++ b/Editor/Mono/SceneModeWindows/LightingWindow.cs @@ -39,6 +39,8 @@ static class Styles public static readonly GUIContent progressiveGPUChangeWarning = EditorGUIUtility.TrTextContent("Changing the compute device used by the Progressive GPU Lightmapper requires the editor to be relaunched. Do you want to change device and restart?"); public static readonly GUIContent gpuBakingProfile = EditorGUIUtility.TrTextContent("GPU Baking Profile", "The profile chosen for trading off between performance and memory usage when baking using the GPU."); + public static readonly GUIContent bakeOnSceneLoad = EditorGUIUtility.TrTextContent("Bake On Scene Load", "Whether to automatically generate lighting for Scenes that do not have valid lighting data when first opened."); + public static readonly GUIContent invalidEnvironmentLabel = EditorGUIUtility.TrTextContentWithIcon("Baked environment lighting does not match the current Scene state. Generate Lighting to update this.", MessageType.Warning); public static readonly GUIContent unsupportedDenoisersLabel = EditorGUIUtility.TrTextContentWithIcon("Unsupported denoiser selected", MessageType.Error); @@ -539,6 +541,16 @@ void DrawBakingProfileSelector() } } + void DrawBakeOnLoadSelector() + { + var selected = (Lightmapping.BakeOnSceneLoadMode)EditorGUILayout.EnumPopup(Styles.bakeOnSceneLoad, Lightmapping.bakeOnSceneLoad); + if (selected != Lightmapping.bakeOnSceneLoad) + { + Undo.RecordObject(LightmapEditorSettings.GetLightmapSettings(), "Change Bake On Load Setting"); + Lightmapping.bakeOnSceneLoad = selected; + } + } + void DrawBottomBarGUI(Mode selectedMode) { using (new EditorGUI.DisabledScope(EditorApplication.isPlayingOrWillChangePlaymode)) @@ -552,6 +564,7 @@ void DrawBottomBarGUI(Mode selectedMode) // Bake settings. DrawGPUDeviceSelector(); DrawBakingProfileSelector(); + DrawBakeOnLoadSelector(); { // Bake button if we are not currently baking diff --git a/Editor/Mono/SceneView/SceneView.cs b/Editor/Mono/SceneView/SceneView.cs index b8bb4e8ab1..473a7d700d 100644 --- a/Editor/Mono/SceneView/SceneView.cs +++ b/Editor/Mono/SceneView/SceneView.cs @@ -2459,6 +2459,10 @@ void HandleViewToolCursor(Rect cameraRect) { if (!Tools.viewToolActive || Event.current.type != EventType.Repaint) return; + // In case multiple scene views are opened, we only want to set the cursor for the one being hovered + // Skip if the mouse is over an overlay or an area that should not use a custom cursor + if (mouseOverWindow is SceneView view && (mouseOverWindow != this || !view.sceneViewMotion.viewportsUnderMouse)) + return; var cursor = MouseCursor.Arrow; switch (Tools.viewTool) @@ -3012,13 +3016,6 @@ void HandleMouseCursor() bool repaintView = false; MouseCursor cursor = MouseCursor.Arrow; - //Reset the cursor if the mouse is over an overlay or an area that should not use a custom cursor - if (mouseOverWindow is SceneView view && !view.sceneViewMotion.viewportsUnderMouse) - { - InternalEditorUtility.ResetCursor(); - return; - } - foreach (CursorRect r in s_MouseRects) { if (r.rect.Contains(evt.mousePosition)) diff --git a/Editor/Mono/UIElements/Controls/PropertyField.cs b/Editor/Mono/UIElements/Controls/PropertyField.cs index 76bcdf206d..83536c56f3 100644 --- a/Editor/Mono/UIElements/Controls/PropertyField.cs +++ b/Editor/Mono/UIElements/Controls/PropertyField.cs @@ -152,6 +152,8 @@ public string label /// public static readonly string inspectorElementUssClassName = ussClassName + "__inspector-property"; + internal static readonly string imguiContainerPropertyUssClassName = ussClassName + "__imgui-container-property"; + /// /// PropertyField constructor. /// @@ -326,6 +328,9 @@ void Reset(SerializedProperty newProperty) if (customPropertyGUI == null) { customPropertyGUI = CreatePropertyIMGUIContainer(); + + AddToClassList(imguiContainerPropertyUssClassName); + m_imguiChildField = customPropertyGUI; } else @@ -410,11 +415,11 @@ private void Reset(SerializedPropertyBindEvent evt) private VisualElement CreatePropertyIMGUIContainer() { - GUIContent customLabel = string.IsNullOrEmpty(label) ? null : new GUIContent(label); - var imguiContainer = new IMGUIContainer(() => { var originalWideMode = InspectorElement.SetWideModeForWidth(this); + var originalHierarchyMode = EditorGUIUtility.hierarchyMode; + EditorGUIUtility.hierarchyMode = true; var oldLabelWidth = EditorGUIUtility.labelWidth; try @@ -422,91 +427,95 @@ private VisualElement CreatePropertyIMGUIContainer() if (!serializedProperty.isValid || !serializedObject.isValid) return; - if (m_InspectorElement is InspectorElement inspectorElement) - { - //set the current PropertyHandlerCache to the current editor - ScriptAttributeUtility.propertyHandlerCache = inspectorElement.editor.propertyHandlerCache; - } - - EditorGUI.BeginChangeCheck(); - serializedProperty.serializedObject.Update(); - - if (classList.Contains(inspectorElementUssClassName)) + EditorGUILayout.BeginVertical(EditorStyles.inspectorHorizontalDefaultMargins); { - var spacing = 0f; - - if (m_imguiChildField != null) + if (m_InspectorElement is InspectorElement inspectorElement) { - spacing = m_imguiChildField.worldBound.x - m_InspectorElement.worldBound.x - m_InspectorElement.resolvedStyle.paddingLeft; + //set the current PropertyHandlerCache to the current editor + ScriptAttributeUtility.propertyHandlerCache = inspectorElement.editor.propertyHandlerCache; } - var imguiSpacing = EditorGUI.kLabelWidthMargin - EditorGUI.kLabelWidthPadding; - var contextWidthElement = m_ContextWidthElement ?? m_InspectorElement; - var contextWidth = contextWidthElement.resolvedStyle.width; - var labelWidth = (contextWidth * EditorGUI.kLabelWidthRatio - imguiSpacing - spacing); - var minWidth = EditorGUI.kMinLabelWidth + EditorGUI.kLabelWidthPadding; - var minLabelWidth = Mathf.Max(minWidth - spacing, 0f); + EditorGUI.BeginChangeCheck(); + serializedProperty.serializedObject.Update(); - EditorGUIUtility.labelWidth = Mathf.Max(labelWidth, minLabelWidth); - } - else - { - if (m_FoldoutDepth > 0) - EditorGUI.indentLevel += m_FoldoutDepth; - } + if (classList.Contains(inspectorElementUssClassName)) + { + var spacing = 0f; - // Wait at last minute to call GetHandler, sometimes the handler cache is cleared between calls. - var handler = ScriptAttributeUtility.GetHandler(serializedProperty); - using (var nestingContext = handler.ApplyNestingContext(m_DrawNestingLevel)) - { - // Decorator drawers are already handled on the uitk side - handler.skipDecoratorDrawers = true; + if (m_imguiChildField != null) + { + spacing = m_imguiChildField.worldBound.x - m_InspectorElement.worldBound.x - m_InspectorElement.resolvedStyle.paddingLeft - resolvedStyle.marginLeft; + } - var previousLeftMarginCoord = EditorGUIUtility.leftMarginCoord; + var imguiSpacing = EditorGUI.kLabelWidthMargin; + var contextWidthElement = m_ContextWidthElement ?? m_InspectorElement; + var contextWidth = contextWidthElement.resolvedStyle.width; + var labelWidth = (contextWidth * EditorGUI.kLabelWidthRatio - imguiSpacing - spacing); + var minWidth = EditorGUI.kMinLabelWidth + EditorGUI.kLabelWidthPadding; + var minLabelWidth = Mathf.Max(minWidth - spacing, 0f); - if (m_InspectorElement != null && m_imguiChildField != null) - { - // Set a left margin offset to align the prefab override bar with the property field - var extraLeftMargin = m_InspectorElement.worldBound.x; - EditorGUIUtility.leftMarginCoord = -m_imguiChildField.worldBound.x + extraLeftMargin; + EditorGUIUtility.labelWidth = Mathf.Max(labelWidth, minLabelWidth); } - - if (label == null) + else { - EditorGUILayout.PropertyField(serializedProperty, true); + if (m_FoldoutDepth > 0) + EditorGUI.indentLevel += m_FoldoutDepth; } - else if (label == string.Empty) + + // Wait at last minute to call GetHandler, sometimes the handler cache is cleared between calls. + var handler = ScriptAttributeUtility.GetHandler(serializedProperty); + using (var nestingContext = handler.ApplyNestingContext(m_DrawNestingLevel)) { - EditorGUILayout.PropertyField(serializedProperty, GUIContent.none, true); + // Decorator drawers are already handled on the uitk side + handler.skipDecoratorDrawers = true; + + var previousLeftMarginCoord = EditorGUIUtility.leftMarginCoord; + + if (m_InspectorElement != null && m_imguiChildField != null) + { + // Set a left margin offset to align the prefab override bar with the property field + var extraLeftMargin = m_InspectorElement.worldBound.x; + EditorGUIUtility.leftMarginCoord = -m_imguiChildField.worldBound.x + extraLeftMargin; + } + + if (label == null) + { + EditorGUILayout.PropertyField(serializedProperty, true); + } + else if (label == string.Empty) + { + EditorGUILayout.PropertyField(serializedProperty, GUIContent.none, true); + } + else + { + EditorGUILayout.PropertyField(serializedProperty, new GUIContent(label), true); + } + + if (m_InspectorElement != null && m_imguiChildField != null) + { + // Reset the left margin to the original value + EditorGUIUtility.leftMarginCoord = previousLeftMarginCoord; + } } - else + + if (!classList.Contains(inspectorElementUssClassName)) { - EditorGUILayout.PropertyField(serializedProperty, new GUIContent(label), true); + if (m_FoldoutDepth > 0) + EditorGUI.indentLevel -= m_FoldoutDepth; } - if (m_InspectorElement != null && m_imguiChildField != null) + serializedProperty.serializedObject.ApplyModifiedProperties(); + if (EditorGUI.EndChangeCheck()) { - // Reset the left margin to the original value - EditorGUIUtility.leftMarginCoord = previousLeftMarginCoord; + DispatchPropertyChangedEvent(); } } - - if (!classList.Contains(inspectorElementUssClassName)) - { - if (m_FoldoutDepth > 0) - EditorGUI.indentLevel -= m_FoldoutDepth; - } - - serializedProperty.serializedObject.ApplyModifiedProperties(); - if (EditorGUI.EndChangeCheck()) - { - DispatchPropertyChangedEvent(); - } + EditorGUILayout.EndVertical(); } finally { EditorGUIUtility.wideMode = originalWideMode; - + EditorGUIUtility.hierarchyMode = originalHierarchyMode; if (classList.Contains(inspectorElementUssClassName)) { EditorGUIUtility.labelWidth = oldLabelWidth; diff --git a/Editor/Mono/UIElements/Inspector/InspectorElement.cs b/Editor/Mono/UIElements/Inspector/InspectorElement.cs index f393650e6b..efd78e4689 100644 --- a/Editor/Mono/UIElements/Inspector/InspectorElement.cs +++ b/Editor/Mono/UIElements/Inspector/InspectorElement.cs @@ -179,6 +179,7 @@ public UxmlTraits() bool m_IsOpenForEdit; bool m_InvalidateGUIBlockCache = true; bool m_Rebind; + VisualElement m_ContextWidthElement; /// /// Gets or sets the editor backing this inspector element. @@ -338,6 +339,19 @@ void OnAttachToPanel(AttachToPanelEvent evt) m_Rebind = true; this.Bind(boundObject); } + + var currentElement = parent; + while (currentElement != null) + { + if (!currentElement.ClassListContains(PropertyEditor.s_MainContainerClassName)) + { + currentElement = currentElement.parent; + continue; + } + + m_ContextWidthElement = currentElement; + break; + } } void OnDetachFromPanel(DetachFromPanelEvent evt) @@ -686,7 +700,7 @@ VisualElement CreateInspectorElementUsingIMGUI(Editor targetEditor) EditorGUIUtility.hierarchyMode = true; EditorGUIUtility.comparisonViewMode = comparisonViewMode; - var originalWideMode = SetWideModeForWidth(inspector); + var originalWideMode = SetWideModeForWidth(m_ContextWidthElement ?? inspector); GUIStyle editorWrapper = (targetEditor.UseDefaultMargins() && targetEditor.CanBeExpandedViaAFoldoutWithoutUpdate() ? EditorStyles.inspectorDefaultMargins diff --git a/Editor/Mono/UnityConnect/CloudProjectSettings.cs b/Editor/Mono/UnityConnect/CloudProjectSettings.cs index 5d98a54f7e..2721f5cf49 100644 --- a/Editor/Mono/UnityConnect/CloudProjectSettings.cs +++ b/Editor/Mono/UnityConnect/CloudProjectSettings.cs @@ -3,6 +3,8 @@ // https://unity3d.com/legal/licenses/Unity_Reference_Only_License using System; +using System.Threading; +using System.Threading.Tasks; using UnityEditor.Connect; namespace UnityEditor @@ -40,6 +42,9 @@ public static string accessToken } } + public static Task GetServiceTokenAsync(CancellationToken cancellationToken = default) + => ServiceToken.Instance.GetServiceTokenAsync(accessToken, cancellationToken); + public static void RefreshAccessToken(Action refresh) { UnityConnect.instance.RefreshAccessToken(refresh); diff --git a/Modules/UnityConnectEditor/Common/UnityConnectWebRequestException.cs b/Editor/Mono/UnityConnect/Network/UnityConnectWebRequestException.cs similarity index 97% rename from Modules/UnityConnectEditor/Common/UnityConnectWebRequestException.cs rename to Editor/Mono/UnityConnect/Network/UnityConnectWebRequestException.cs index 142d30783d..f1fd950a6d 100644 --- a/Modules/UnityConnectEditor/Common/UnityConnectWebRequestException.cs +++ b/Editor/Mono/UnityConnect/Network/UnityConnectWebRequestException.cs @@ -2,8 +2,6 @@ // Copyright (c) Unity Technologies. For terms of use, see // https://unity3d.com/legal/licenses/Unity_Reference_Only_License - -using UnityEngine; using System; using System.Collections.Generic; diff --git a/Editor/Mono/UnityConnect/Network/UnityConnectWebRequestUtils.cs b/Editor/Mono/UnityConnect/Network/UnityConnectWebRequestUtils.cs new file mode 100644 index 0000000000..68110b9b34 --- /dev/null +++ b/Editor/Mono/UnityConnect/Network/UnityConnectWebRequestUtils.cs @@ -0,0 +1,83 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; + +namespace UnityEditor.Connect +{ + internal static class UnityConnectWebRequestUtils + { + internal static UnityConnectWebRequestException CreateUnityWebRequestException(UnityWebRequest request, + string message) + => new(L10n.Tr(message)) + { + error = request.error, + method = request.method, + timeout = request.timeout, + url = request.url, + responseHeaders = request.GetResponseHeaders(), + responseCode = request.responseCode, + isHttpError = request.result == UnityWebRequest.Result.ProtocolError, + isNetworkError = request.result == UnityWebRequest.Result.ConnectionError + }; + + /// + /// Used to determine if the UnityWebRequest had an error or error code + /// + internal static bool IsRequestError(UnityWebRequest request) + { + if (!string.IsNullOrEmpty(request.error)) + { + return true; + } + + switch (request.result) + { + case UnityWebRequest.Result.ConnectionError: + case UnityWebRequest.Result.ProtocolError: + case UnityWebRequest.Result.DataProcessingError: + return true; + } + + if (request.responseCode is < 200 or >= 300) + { + return true; + } + + return false; + } + + internal static bool IsUnityWebRequestReadyForJsonExtract(UnityWebRequest unityWebRequest) + { + return !IsRequestError(unityWebRequest) + && !string.IsNullOrEmpty(unityWebRequest.downloadHandler.text); + } + + /// + /// Used to run a UnityWebRequest on the main thread in an awaitable manner, while handling CancellationToken + /// + internal static async Task SendWebRequestAsync( + UnityWebRequest unityWebRequest, + CancellationToken cancellationToken = default) + { + var webRequestTask = AsyncUtils.RunUnityWebRequestOnMainThread(unityWebRequest); + + while (!unityWebRequest.isDone) + { + if (cancellationToken.IsCancellationRequested) + { + unityWebRequest.Abort(); + cancellationToken.ThrowIfCancellationRequested(); + } + + await Task.Yield(); + } + + await webRequestTask; + } + } +} diff --git a/Editor/Mono/UnityConnect/ServiceToken/Caching/GenesisAndServiceTokenCaching.cs b/Editor/Mono/UnityConnect/ServiceToken/Caching/GenesisAndServiceTokenCaching.cs new file mode 100644 index 0000000000..9c4d480e98 --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/Caching/GenesisAndServiceTokenCaching.cs @@ -0,0 +1,62 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; +using System.Collections.Generic; + +namespace UnityEditor.Connect +{ + class GenesisAndServiceTokenCaching : IGenesisAndServiceTokenCaching + { + internal const string CacheKey = "Editor.GatewayTokens.Cache"; + readonly TimeSpan m_RefreshGracePeriod = TimeSpan.FromMinutes(30); + + public Tokens LoadCache() + { + var serializedTokens = SessionState.GetString(CacheKey, string.Empty); + + if (string.IsNullOrEmpty(serializedTokens)) + { + return new Tokens(); + } + + var deserializedTokens = Json.Deserialize(serializedTokens) as Dictionary; + + if (deserializedTokens == null) + { + return new Tokens(); + } + + return new Tokens() + { + GenesisToken = deserializedTokens.GetValueOrDefault(nameof(Tokens.GenesisToken))?.ToString(), + GatewayToken = deserializedTokens.GetValueOrDefault(nameof(Tokens.GatewayToken))?.ToString() + }; + } + + public void SaveCache(Tokens tokens) + { + var serialized = Json.Serialize(tokens); + SessionState.SetString(CacheKey, serialized); + } + + public DateTime GetNextRefreshTime(string gatewayToken) + { + try + { + if (string.IsNullOrEmpty(gatewayToken)) + { + return new DateTime(); + } + + var jwt = JsonWebToken.Decode(gatewayToken); + return jwt.exp - m_RefreshGracePeriod; + } + catch + { + return new DateTime(); + } + } + } +} diff --git a/Editor/Mono/UnityConnect/ServiceToken/Caching/IGenesisAndServiceTokenCaching.cs b/Editor/Mono/UnityConnect/ServiceToken/Caching/IGenesisAndServiceTokenCaching.cs new file mode 100644 index 0000000000..7e758f5c23 --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/Caching/IGenesisAndServiceTokenCaching.cs @@ -0,0 +1,16 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; + +namespace UnityEditor.Connect +{ + interface IGenesisAndServiceTokenCaching + { + public Tokens LoadCache(); + public void SaveCache(Tokens tokens); + public DateTime GetNextRefreshTime(string gatewayToken); + } +} + diff --git a/Editor/Mono/UnityConnect/ServiceToken/Caching/JsonWebToken.cs b/Editor/Mono/UnityConnect/ServiceToken/Caching/JsonWebToken.cs new file mode 100644 index 0000000000..cde2f60271 --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/Caching/JsonWebToken.cs @@ -0,0 +1,58 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; +using System.Collections.Generic; +using System.Text; + +namespace UnityEditor.Connect +{ + readonly struct JsonWebToken + { + static readonly char[] k_JwtSeparator = { '.' }; + static readonly DateTime k_UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + public DateTime exp { get; } + + public JsonWebToken(long exp) + { + this.exp = k_UnixEpoch.AddSeconds(exp); + } + + public override string ToString() + { + return Json.Serialize(this); + } + + public static JsonWebToken Decode(string token) + { + var parts = token.Split(k_JwtSeparator); + if (parts.Length != 3) + { + throw new ArgumentException($"The authentication token is malformed or invalid. " + + $"JWT has an invalid number of sections. Token: '{token}'"); + } + + var payload = parts[1]; + var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload)); + var deserialized = Json.Deserialize(payloadJson) as Dictionary; + return new JsonWebToken(Convert.ToInt64(deserialized.GetValueOrDefault(nameof(exp)))); + } + + static byte[] Base64UrlDecode(string input) + { + var output = input; + output = output.Replace('-', '+'); // 62nd char of encoding + output = output.Replace('_', '/'); // 63rd char of encoding + + var mod4 = input.Length % 4; + if (mod4 > 0) + { + output += new string('=', 4 - mod4); + } + + return Convert.FromBase64String(output); + } + } +} diff --git a/Editor/Mono/UnityConnect/ServiceToken/Caching/Tokens.cs b/Editor/Mono/UnityConnect/ServiceToken/Caching/Tokens.cs new file mode 100644 index 0000000000..1962b4c156 --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/Caching/Tokens.cs @@ -0,0 +1,20 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +namespace UnityEditor.Connect +{ + internal struct Tokens + { + public string GatewayToken; + public string GenesisToken; + + public Tokens() + { + GatewayToken = default; + GenesisToken = default; + } + } +} + + diff --git a/Editor/Mono/UnityConnect/ServiceToken/ConfigurationProvider/CloudEnvironmentConfigProvider.cs b/Editor/Mono/UnityConnect/ServiceToken/ConfigurationProvider/CloudEnvironmentConfigProvider.cs new file mode 100644 index 0000000000..e902c7587d --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/ConfigurationProvider/CloudEnvironmentConfigProvider.cs @@ -0,0 +1,54 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; +using UnityEngine; + +namespace UnityEditor.Connect +{ + class CloudEnvironmentConfigProvider : ICloudEnvironmentConfigProvider + { + internal const string CloudEnvironmentArg = "-cloudEnvironment"; + internal const string StagingEnv = "staging"; + + public bool IsStaging() + { + return GetCloudEnvironment(Environment.GetCommandLineArgs()) == StagingEnv; + } + + internal string GetCloudEnvironment(string[] commandLineArgs) + { + string cloudEnvironmentField = null; + + foreach (var arg in commandLineArgs) + { + if (arg.StartsWith(CloudEnvironmentArg)) + { + cloudEnvironmentField = arg; + break; + } + } + + if (cloudEnvironmentField != null) + { + var cloudEnvironmentIndex = Array.IndexOf(commandLineArgs, cloudEnvironmentField); + + if (cloudEnvironmentField == CloudEnvironmentArg) + { + if (cloudEnvironmentIndex <= commandLineArgs.Length - 2) + { + return commandLineArgs[cloudEnvironmentIndex + 1]; + } + } + else if (cloudEnvironmentField.Contains('=')) + { + var value = cloudEnvironmentField.Substring(cloudEnvironmentField.IndexOf('=') + 1); + return !string.IsNullOrEmpty(value) ? value : null; + } + } + + return null; + } + } +} diff --git a/Editor/Mono/UnityConnect/ServiceToken/ConfigurationProvider/ICloudEnvironmentConfigProvider.cs b/Editor/Mono/UnityConnect/ServiceToken/ConfigurationProvider/ICloudEnvironmentConfigProvider.cs new file mode 100644 index 0000000000..d3552daed4 --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/ConfigurationProvider/ICloudEnvironmentConfigProvider.cs @@ -0,0 +1,11 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +namespace UnityEditor.Connect +{ + interface ICloudEnvironmentConfigProvider + { + bool IsStaging(); + } +} diff --git a/Editor/Mono/UnityConnect/ServiceToken/ServiceToken.cs b/Editor/Mono/UnityConnect/ServiceToken/ServiceToken.cs new file mode 100644 index 0000000000..56a3ed26bc --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/ServiceToken.cs @@ -0,0 +1,73 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace UnityEditor.Connect +{ + class ServiceToken + { + readonly ITokenExchange m_TokenExchange; + readonly IGenesisAndServiceTokenCaching m_GenesisAndServiceTokenCaching; + readonly DateTime m_DateTime; + + internal static ServiceToken Instance => k_LazyInstance.Value; + + static readonly Lazy k_LazyInstance = new Lazy(() => + { + var cloudEnvironmentConfigProvider = new CloudEnvironmentConfigProvider(); + var tokenExchange = new TokenExchange(cloudEnvironmentConfigProvider); + var tokenCaching = new GenesisAndServiceTokenCaching(); + return new ServiceToken(tokenExchange, tokenCaching); + }); + + internal ServiceToken( + ITokenExchange tokenExchange, + IGenesisAndServiceTokenCaching genesisAndServiceTokenCaching) + { + m_TokenExchange = tokenExchange; + m_GenesisAndServiceTokenCaching = genesisAndServiceTokenCaching; + m_DateTime = DateTime.UtcNow; + } + + public async Task GetServiceTokenAsync(string genesisToken, CancellationToken cancellationToken = default) + { + Tokens cachedTokens = new(); + await AsyncUtils.RunNextActionOnMainThread(() => cachedTokens = m_GenesisAndServiceTokenCaching.LoadCache()); + + var nextRefreshTime = m_GenesisAndServiceTokenCaching.GetNextRefreshTime(cachedTokens.GatewayToken); + + if (genesisToken != cachedTokens.GenesisToken || m_DateTime.ToUniversalTime() >= nextRefreshTime) + { + if (!string.IsNullOrEmpty(genesisToken)) + { + try + { + cachedTokens.GatewayToken = + await m_TokenExchange.GetServiceTokenAsync(genesisToken, cancellationToken); + } + catch + { + cachedTokens.GatewayToken = null; + throw; + } + } + else + { + cachedTokens.GatewayToken = null; + } + + cachedTokens.GenesisToken = genesisToken; + } + + await AsyncUtils.RunNextActionOnMainThread(() => m_GenesisAndServiceTokenCaching.SaveCache(cachedTokens)); + return cachedTokens.GatewayToken; + } + } +} + + diff --git a/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/ITokenExchange.cs b/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/ITokenExchange.cs new file mode 100644 index 0000000000..fd22ef6739 --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/ITokenExchange.cs @@ -0,0 +1,14 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System.Threading; +using System.Threading.Tasks; + +namespace UnityEditor.Connect +{ + interface ITokenExchange + { + Task GetServiceTokenAsync(string genesisToken, CancellationToken cancellationToken); + } +} diff --git a/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/Model/TokenExchangeRequest.cs b/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/Model/TokenExchangeRequest.cs new file mode 100644 index 0000000000..6088c26d9e --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/Model/TokenExchangeRequest.cs @@ -0,0 +1,21 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; + +namespace UnityEditor.Connect +{ + [Serializable] + class TokenExchangeRequest + { + public string token; + + public TokenExchangeRequest() {} + + public TokenExchangeRequest(string token) + { + this.token = token; + } + } +} diff --git a/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/Model/TokenExchangeResponse.cs b/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/Model/TokenExchangeResponse.cs new file mode 100644 index 0000000000..af101c3e6b --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/Model/TokenExchangeResponse.cs @@ -0,0 +1,14 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; + +namespace UnityEditor.Connect +{ + [Serializable] + class TokenExchangeResponse + { + public string token; + } +} diff --git a/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/TokenExchange.cs b/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/TokenExchange.cs new file mode 100644 index 0000000000..695a0f4c42 --- /dev/null +++ b/Editor/Mono/UnityConnect/ServiceToken/TokenExchange/TokenExchange.cs @@ -0,0 +1,150 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; + +namespace UnityEditor.Connect +{ + class TokenExchange : ITokenExchange + { + const string k_RequestContentType = "application/json"; + const string k_StagingServicesGatewayTokenExchangeUrl = + "https://staging.services.unity.com/api/auth/v1/genesis-token-exchange/unity"; + const string k_ProductionServicesGatewayTokenExchangeUrl = + "https://services.unity.com/api/auth/v1/genesis-token-exchange/unity"; + + const string k_SerializationFailureMessage = + "Token Exchange failed due to an issue with serialization/deserialization. "; + const string k_WebRequestFailureMessage = + "Token Exchange failed due a failure with the web request."; + const string k_PayloadDeserializationFailureMessage = + k_SerializationFailureMessage + "Payload that failed to deserialize: "; + const string k_KeyMissingSerializationFailureMessage = + k_SerializationFailureMessage + "Deserialized response does not contain the key: "; + + readonly ICloudEnvironmentConfigProvider m_CloudEnvironmentConfigProvider; + + internal TokenExchange(ICloudEnvironmentConfigProvider cloudEnvironmentConfigProvider) + { + m_CloudEnvironmentConfigProvider = cloudEnvironmentConfigProvider; + } + + public async Task GetServiceTokenAsync( + string genesisToken, + CancellationToken cancellationToken = default) + { + var tokenExchangeRequest = new TokenExchangeRequest(genesisToken); + Dictionary deserializedResponse; + + var exchangeResult = await TokenExchangeRequestAsync(tokenExchangeRequest, cancellationToken); + + try + { + deserializedResponse = Json.Deserialize(exchangeResult.ResponseJson) as Dictionary; + } + catch (Exception exception) + { + throw new SerializationException(k_PayloadDeserializationFailureMessage + + $"'{exchangeResult.ResponseJson}'", exception); + } + + if (deserializedResponse is null) + { + throw new SerializationException(k_PayloadDeserializationFailureMessage + + $"'{exchangeResult.ResponseJson}'"); + } + + if (!TokenExchangeResponseContainsTokenKey(deserializedResponse)) + { + throw new SerializationException(k_KeyMissingSerializationFailureMessage + + $"'{nameof(TokenExchangeResponse.token)}'"); + } + + return deserializedResponse[nameof(TokenExchangeResponse.token)].ToString(); + } + + async Task TokenExchangeRequestAsync( + TokenExchangeRequest tokenExchangeRequest, + CancellationToken cancellationToken = default) + { + var jsonPayload = Json.Serialize(tokenExchangeRequest); + var postBytes = Encoding.UTF8.GetBytes(jsonPayload); + var endpoint = GetEndpoint(); + + using (var exchangeRequest = new UnityWebRequest(endpoint, UnityWebRequest.kHttpVerbPOST)) + { + exchangeRequest.uploadHandler = new UploadHandlerRaw(postBytes) {contentType = k_RequestContentType}; + exchangeRequest.downloadHandler = new DownloadHandlerBuffer(); + + await UnityConnectWebRequestUtils.SendWebRequestAsync(exchangeRequest, cancellationToken); + + VerifyTokenExchangeResponse(exchangeRequest); + + return new TokenExchangeResult( + exchangeRequest.result.ToString(), + exchangeRequest.error, + exchangeRequest.responseCode.ToString(), + exchangeRequest.downloadHandler.text); + } + } + + static void VerifyTokenExchangeResponse(UnityWebRequest exchangeRequest) + { + if (UnityConnectWebRequestUtils.IsUnityWebRequestReadyForJsonExtract(exchangeRequest)) + { + return; + } + + throw UnityConnectWebRequestUtils + .CreateUnityWebRequestException(exchangeRequest, k_WebRequestFailureMessage); + } + + string GetEndpoint() + { + string endpoint = k_ProductionServicesGatewayTokenExchangeUrl; + + try + { + if (m_CloudEnvironmentConfigProvider.IsStaging()) + { + endpoint = k_StagingServicesGatewayTokenExchangeUrl; + } + } + catch (Exception e) + { + Debug.LogError("Error while parsing the Unity build command" + + " line environment argument, defaulting environment to production for token" + + $" exchange. Details: '{e}'."); + } + + return endpoint; + } + + bool TokenExchangeResponseContainsTokenKey(Dictionary deserializedResponse) + => deserializedResponse.ContainsKey(nameof(TokenExchangeResponse.token)); + } + + struct TokenExchangeResult + { + public string Result { get; } + public string Error { get; } + public string ResponseCode { get; } + public string ResponseJson { get; } + + public TokenExchangeResult(string result, string error, string responseCode, string responseJson) + { + Result = result; + Error = error; + ResponseCode = responseCode; + ResponseJson = responseJson; + } + } +} diff --git a/Editor/Mono/UnityConnect/Utils/AsyncUtils.cs b/Editor/Mono/UnityConnect/Utils/AsyncUtils.cs new file mode 100644 index 0000000000..2e77d418e1 --- /dev/null +++ b/Editor/Mono/UnityConnect/Utils/AsyncUtils.cs @@ -0,0 +1,79 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; + +namespace UnityEditor.Connect +{ + internal static class AsyncUtils + { + /// + /// Used to run an action on the main thread of Unity + /// + /// Awaitable task that indicates when the action is completed + internal static Task RunNextActionOnMainThread( + Action action, + [CallerFilePath] string file = null, + [CallerMemberName] string caller = null, + [CallerLineNumber] int line = 0) + { + var taskCompletionSource = new TaskCompletionSource(); + EditorApplication.CallbackFunction callback = null; + callback = () => + { + EditorApplication.update -= callback; + try + { + action(); + taskCompletionSource.SetResult(true); + } + catch (Exception e) when (caller != null && file != null && line != 0) + { + taskCompletionSource.SetException(e); + throw new Exception($"Exception thrown from invocation made by '{file}'({line}) by {caller}", e); + } + }; + EditorApplication.update += callback; + return taskCompletionSource.Task; + } + + /// + /// Used to run a UnityWebRequest on the main thread of Unity + /// + /// Awaitable task that indicates when the web request is completed + internal static Task RunUnityWebRequestOnMainThread( + UnityWebRequest request, + [CallerFilePath] string file = null, + [CallerMemberName] string caller = null, + [CallerLineNumber] int line = 0) + { + var taskCompletionSource = new TaskCompletionSource(); + EditorApplication.update += Callback; + return taskCompletionSource.Task; + + void Callback() + { + EditorApplication.update -= Callback; + try + { + request.SendWebRequest().completed += RequestCompleted; + } + catch (Exception e) when (caller != null && file != null && line != 0) + { + taskCompletionSource.SetException(e); + throw new Exception($"Exception thrown from invocation made by '{file}'({line}) by {caller}", e); + } + } + + void RequestCompleted(AsyncOperation _) + { + taskCompletionSource.SetResult(true); + } + } + } +} diff --git a/Modules/BuildPipeline/Editor/Managed/BuildDefines.cs b/Modules/BuildPipeline/Editor/Managed/BuildDefines.cs index 36e5d7afc8..7316dfb812 100644 --- a/Modules/BuildPipeline/Editor/Managed/BuildDefines.cs +++ b/Modules/BuildPipeline/Editor/Managed/BuildDefines.cs @@ -32,6 +32,12 @@ public static string[] GetScriptCompilationDefines(BuildTarget target, string[] [RequiredByNativeCode] public static string[] GetBuildProfileScriptDefines() { + // Profile scripting defines are cached when profile is first activated + // and when the domain is unloaded. Workaround for active build profile + // not reachable when AssetDatabase.IsReadOnly() is true. + if (!EditorUserBuildSettings.isBuildProfileAvailable) + return EditorUserBuildSettings.GetActiveProfileScriptingDefines(); + var profile = EditorUserBuildSettings.activeBuildProfile; if (profile == null) return EditorUserBuildSettings.GetActiveProfileScriptingDefines(); diff --git a/Modules/BuildProfileEditor/ActiveBuildProfilerListener.cs b/Modules/BuildProfileEditor/ActiveBuildProfilerListener.cs new file mode 100644 index 0000000000..b0e1a7cd53 --- /dev/null +++ b/Modules/BuildProfileEditor/ActiveBuildProfilerListener.cs @@ -0,0 +1,40 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; +using UnityEngine; +using UnityEditor; +using UnityEditor.Build; + +namespace UnityEditor.Build.Profile +{ + /// + /// Listener for when the active build target changes. + /// This is used to update the build profile window and other components when the active build target changes. + /// + /// + /// Calls to `EditorUserBuildSettings.SwitchActiveBuildTarget(buildTargetGroup, target)` will trigger this event. + /// The event is called after the build target is changed, and an instance of this class is created when the event occurs. + /// + internal class ActiveBuildTargetListener : IActiveBuildTargetChanged + { + /// + /// The order in which the callback will be called. Lower numbers are called first. + /// + public int callbackOrder => 0; + + /// + /// Called when the active build target changes. + /// + public void OnActiveBuildTargetChanged(BuildTarget previousTarget, BuildTarget newTarget) + { + activeBuildTargetChanged?.Invoke(previousTarget, newTarget); + } + + /// + /// Event that is called when the active build platform changes. + /// + static public event Action activeBuildTargetChanged; + } +} diff --git a/Modules/BuildProfileEditor/BuildProfileEditor.cs b/Modules/BuildProfileEditor/BuildProfileEditor.cs index 1ae36a00d9..62d185d8af 100644 --- a/Modules/BuildProfileEditor/BuildProfileEditor.cs +++ b/Modules/BuildProfileEditor/BuildProfileEditor.cs @@ -28,6 +28,7 @@ internal class BuildProfileEditor : Editor const string k_SceneListFoldoutAddOpenButton = "scene-list-foldout-add-open-button"; const string k_SceneListFoldoutClassicSection = "scene-list-foldout-classic-section"; const string k_SceneListFoldoutClassicButton = "scene-list-foldout-classic-button"; + const string k_SceneListGlobalToggle = "scene-list-global-toggle"; const string k_CompilingWarningHelpBox = "compiling-warning-help-box"; const string k_VirtualTextureWarningHelpBox = "virtual-texture-warning-help-box"; const string k_PlatformBuildWarningsRoot = "platform-build-warning-root"; @@ -279,6 +280,8 @@ void AddSceneList(VisualElement root, BuildProfile profile = null) bool isEnable = isGlobalSceneList || !isClassicPlatform; var sceneListFoldout = root.Q(k_SceneListFoldout); + var globalToggle = root.Q(k_SceneListGlobalToggle); + globalToggle.label = TrText.sceneListOverride; sceneListFoldout.text = TrText.sceneList; m_SceneList = (isGlobalSceneList || isClassicPlatform) ? new BuildProfileSceneList() @@ -303,8 +306,35 @@ void AddSceneList(VisualElement root, BuildProfile profile = null) root.Q(k_SceneListFoldoutClassicSection).Show(); var globalSceneListButton = root.Q - [EditorWindowTitle(title = "Build Settings")] internal class BuildProfileWindow : EditorWindow { const string k_DevOpsUrl = "https://unity.com/products/unity-devops?utm_medium=desktop-app&utm_source=unity-editor-window-menu&utm_content=buildsettings"; @@ -152,6 +151,7 @@ public void CreateGUI() BuildProfileContext.activeProfileChanged -= OnActiveProfileChanged; BuildProfileContext.activeProfileChanged += OnActiveProfileChanged; + ActiveBuildTargetListener.activeBuildTargetChanged += OnActiveBuildTargetChanged; } public void OnDisable() @@ -159,6 +159,7 @@ public void OnDisable() DestroyImmediate(buildProfileEditor); BuildProfileContext.activeProfileChanged -= OnActiveProfileChanged; + ActiveBuildTargetListener.activeBuildTargetChanged -= OnActiveBuildTargetChanged; if (m_BuildProfileDataSource != null) { @@ -536,6 +537,15 @@ void OnActiveProfileChanged(BuildProfile prev, BuildProfile cur) UpdateFormButtonState(m_BuildProfileSelection.Get(0)); } + /// + /// Callback invoked when the active build target changes. + /// + void OnActiveBuildTargetChanged(BuildTarget prev, BuildTarget cur) + { + m_ProfileListViews.Rebuild(); + UpdateFormButtonState(m_BuildProfileSelection.Get(0)); + } + void RebuildBuildProfileEditor(BuildProfile profile) { // Rebuild the BuildProfile inspector, targeting the newly selected BuildProfile. diff --git a/Modules/BuildProfileEditor/Handlers/BuildProfileContextMenu.cs b/Modules/BuildProfileEditor/Handlers/BuildProfileContextMenu.cs index 03dc847676..b1e4cac3a4 100644 --- a/Modules/BuildProfileEditor/Handlers/BuildProfileContextMenu.cs +++ b/Modules/BuildProfileEditor/Handlers/BuildProfileContextMenu.cs @@ -3,6 +3,8 @@ // https://unity3d.com/legal/licenses/Unity_Reference_Only_License using System; +using System.Text; +using System.Collections.Generic; using UnityEditor.Build.Profile.Elements; using UnityEngine.UIElements; @@ -17,8 +19,13 @@ internal class BuildProfileContextMenu static readonly string k_DeleteContinue = L10n.Tr("Continue"); static readonly string k_DeleteCancel = L10n.Tr("Cancel"); - static readonly string k_DeleteTitle = L10n.Tr("Delete Active Build Profile"); - static readonly string k_DeleteMessage = L10n.Tr("This will delete your active build profile and activate the respective platform build profile. This cannot be undone."); + static readonly string k_DeleteActiveProfileTitle = L10n.Tr("Delete Active Build Profile"); + static readonly string k_DeleteActiveProfileMessage = L10n.Tr("This will delete your active build profile and activate the respective platform build profile. This cannot be undone."); + static readonly string k_DeleteProfileTitle = L10n.Tr("Delete Selected Build Profile"); + static readonly string k_DeleteProfileMessage = L10n.Tr("This will delete the selected build profile. This cannot be undone."); + static readonly string k_DeleteMultipleProfilesTitle = L10n.Tr("Delete Selected Build Profiles"); + static readonly string k_DeleteActiveAndOtherProfilesMessage = L10n.Tr("This will delete the selected build profiles, including your active build profile, and activate the respective platform build profile. This cannot be undone."); + static readonly string k_DeleteMultipleProfilesMessage = L10n.Tr("This will delete the selected build profiles. This cannot be undone."); readonly BuildProfileWindowSelection m_ProfileSelection; readonly BuildProfileDataSource m_ProfileDataSource; @@ -132,20 +139,55 @@ void SelectBuildProfileInView(BuildProfile buildProfile, bool isClassic, bool sh m_ProfileSelection.visualElement.SelectBuildProfile(index); } + /// + /// Given a list of build profiles, prompts for user confirmation for deletion. + /// + /// true, if user approves deletion. + bool ShowDeleteSelectedProfilesDialog(List selectedProfiles) + { + var maxPathsToShow = 3; + var paths = new StringBuilder(); + var containsActiveProfile = false; + for (int i = selectedProfiles.Count - 1; i >= 0; --i) + { + if (i < selectedProfiles.Count - maxPathsToShow) + { + paths.Append("...\n"); + break; + } + + var profile = selectedProfiles[i]; + if (BuildProfileContext.activeProfile == profile) + containsActiveProfile = true; + + var path = AssetDatabase.GetAssetPath(profile); + paths.Append(path + "\n"); + } + + var multipleProfiles = selectedProfiles.Count > 1; + var title = multipleProfiles + ? k_DeleteMultipleProfilesTitle + : containsActiveProfile ? k_DeleteActiveProfileTitle : k_DeleteProfileTitle; + var message = paths + "\n" + (multipleProfiles + ? (containsActiveProfile ? k_DeleteActiveAndOtherProfilesMessage : k_DeleteMultipleProfilesMessage) + : (containsActiveProfile ? k_DeleteActiveProfileMessage : k_DeleteProfileMessage)); + + return EditorUtility.DisplayDialog(title, message, k_DeleteContinue, k_DeleteCancel); + } + void HandleDeleteSelectedProfiles() { var selectedProfiles = m_ProfileSelection.GetAll(); + var isDeleteConfirmed = ShowDeleteSelectedProfilesDialog(selectedProfiles); + if (!isDeleteConfirmed) + return; + + var profilesDeleted = false; for (int i = selectedProfiles.Count - 1; i >= 0; --i) { var profile = selectedProfiles[i]; if (BuildProfileContext.activeProfile == profile) { - string path = AssetDatabase.GetAssetPath(profile); - string finalMessage = $"{path}\n\n{k_DeleteMessage}"; - if (!EditorUtility.DisplayDialog(k_DeleteTitle, finalMessage, k_DeleteContinue, k_DeleteCancel)) - { - continue; - } // if we're deleting an active profile, we want to compare the value of its settings that require a restart // to the value of the settings for the platform we'll be activating after we delete the current platform // and show a restart editor prompt if they're different so the settings take effect @@ -158,11 +200,13 @@ void HandleDeleteSelectedProfiles() } m_ProfileDataSource.DeleteAsset(profile); + profilesDeleted = true; } // No need to select a profile after deletion, since the method below // selects the active profile after repaint - m_ProfileWindow.RepaintAndClearSelection(); + if (profilesDeleted) + m_ProfileWindow.RepaintAndClearSelection(); } } } diff --git a/Modules/BuildProfileEditor/TrText.cs b/Modules/BuildProfileEditor/TrText.cs index 1ff51f2242..ff4156f6d5 100644 --- a/Modules/BuildProfileEditor/TrText.cs +++ b/Modules/BuildProfileEditor/TrText.cs @@ -35,6 +35,7 @@ internal class TrText public static readonly string activatePlatform = L10n.Tr("Switch Platform"); public static readonly string sceneList = L10n.Tr("Scene List"); public static readonly string addOpenScenes = L10n.Tr("Add Open Scenes"); + public static readonly string sceneListOverride = L10n.Tr("Override Global Scene List"); public static readonly string openSceneList = L10n.Tr("Open Scene List"); public static readonly string compilingMessage = L10n.Tr("Cannot build player while editor is importing assets or compiling scripts."); public static readonly string invalidVirtualTexturingSettingMessage = L10n.Tr("Cannot build player because Virtual Texturing is enabled, but the target platform or graphics API does not support Virtual Texturing. Go to Player Settings to resolve the incompatibility."); diff --git a/Modules/EditorToolbar/ToolbarElements/PreviewPackagesInUseDropdown.cs b/Modules/EditorToolbar/ToolbarElements/PreviewPackagesInUseDropdown.cs index 3bfe03c644..94b2641425 100644 --- a/Modules/EditorToolbar/ToolbarElements/PreviewPackagesInUseDropdown.cs +++ b/Modules/EditorToolbar/ToolbarElements/PreviewPackagesInUseDropdown.cs @@ -31,6 +31,8 @@ sealed class PreviewPackagesInUseDropdown : ToolbarButton private const string previewIcon = "unity-editor-toolbar-preview-package-in-use__icon"; private const string previewArrowIcon = "unity-icon-arrow-preview-packages-in-use"; + private static readonly string k_ExpPackagesInUseText = L10n.Tr("Experimental Packages In Use"); + public PreviewPackagesInUseDropdown() { m_ApplicationProxy = ServicesContainer.instance.Resolve(); @@ -41,7 +43,9 @@ public PreviewPackagesInUseDropdown() AddToClassList("unity-toolbar-button-preview-packages-in-use"); - AddTextElement(this).text = L10n.Tr("Experimental Packages In Use"); + tooltip = k_ExpPackagesInUseText; + + AddTextElement(this).text = k_ExpPackagesInUseText; AddIconElement(this); AddArrowElement(this); clicked += () => ShowUserMenu(worldBound); diff --git a/Modules/IMGUI/TextSelectingUtilities.cs b/Modules/IMGUI/TextSelectingUtilities.cs index 08020e26e6..7e77f22b5f 100644 --- a/Modules/IMGUI/TextSelectingUtilities.cs +++ b/Modules/IMGUI/TextSelectingUtilities.cs @@ -387,6 +387,15 @@ public void SelectParagraphForward() { ClearCursorPos(); bool wasBehind = cursorIndex < selectIndex; + + if (textHandle.useAdvancedText) + { + int cursorTempIndex = cursorIndex; + textHandle.SelectToNextParagraph(ref cursorTempIndex); + cursorIndex = cursorTempIndex; + return; + } + if (cursorIndex < characterCount) { cursorIndex = IndexOfEndOfLine(cursorIndex + 1); @@ -399,9 +408,19 @@ public void SelectParagraphBackward() { ClearCursorPos(); bool wasInFront = cursorIndex > selectIndex; + + if (textHandle.useAdvancedText) + { + int cursorTempIndex = cursorIndex; + textHandle.SelectToPreviousParagraph(ref cursorTempIndex); + cursorIndex = cursorTempIndex; + return; + } + if (cursorIndex > 1) { cursorIndex = textHandle.LastIndexOf(kNewLineChar, cursorIndex - 2) + 1; + if (wasInFront && cursorIndex < selectIndex) cursorIndex = selectIndex; } @@ -454,6 +473,16 @@ public void SelectCurrentParagraph() ClearCursorPos(); int textLen = characterCount; + if (textHandle.useAdvancedText) + { + int cursorTempIndex = cursorIndex; + int selectTempIndex = selectIndex; + textHandle.SelectCurrentParagraph(ref cursorTempIndex, ref selectTempIndex); + cursorIndex = cursorTempIndex; + selectIndex = selectTempIndex; + return; + } + if (cursorIndex < textLen) cursorIndex = IndexOfEndOfLine(cursorIndex); if (selectIndex != 0) @@ -583,6 +612,14 @@ public void MoveTextEnd() /// Move to the next paragraph public void MoveParagraphForward() { + if (textHandle.useAdvancedText) + { + int cursorTempIndex = cursorIndex; + textHandle.SelectToNextParagraph(ref cursorTempIndex); + cursorIndex = selectIndex = cursorTempIndex; + return; + } + cursorIndex = cursorIndex > selectIndex ? cursorIndex : selectIndex; if (cursorIndex < characterCount) { @@ -593,6 +630,14 @@ public void MoveParagraphForward() /// Move to the previous paragraph public void MoveParagraphBackward() { + if (textHandle.useAdvancedText) + { + int cursorTempIndex = cursorIndex; + textHandle.SelectToPreviousParagraph(ref cursorTempIndex); + cursorIndex = selectIndex = cursorTempIndex; + return; + } + cursorIndex = cursorIndex < selectIndex ? cursorIndex : selectIndex; if (cursorIndex > 1) { @@ -760,8 +805,16 @@ public void SelectToPosition(Vector2 cursorPosition) } // paragraph else { - if (p <= m_DblClickInitPosStart) + if ((!textHandle.useAdvancedText && p <= m_DblClickInitPosStart) || (textHandle.useAdvancedText && p < m_DblClickInitPosStart)) { + if (textHandle.useAdvancedText) + { + int selectTempIndex = p; + textHandle.SelectToStartOfParagraph(ref selectTempIndex); + selectIndex = selectTempIndex; + return; + } + if (p > 0) cursorIndex = textHandle.LastIndexOf(kNewLineChar, Mathf.Max(0, p - 1)) + 1; else @@ -771,6 +824,14 @@ public void SelectToPosition(Vector2 cursorPosition) } else if (p >= m_DblClickInitPosEnd) { + if (textHandle.useAdvancedText) + { + int cursorTempIndex = p; + textHandle.SelectToEndOfParagraph(ref cursorTempIndex); + cursorIndex = cursorTempIndex; + return; + } + if (p < characterCount) { cursorIndex = IndexOfEndOfLine(p); @@ -782,8 +843,16 @@ public void SelectToPosition(Vector2 cursorPosition) } else { - cursorIndex = m_DblClickInitPosStart; - selectIndex = m_DblClickInitPosEnd; + if (textHandle.useAdvancedText) + { + cursorIndex = m_DblClickInitPosEnd; + selectIndex = m_DblClickInitPosStart; + } + else + { + cursorIndex = m_DblClickInitPosStart; + selectIndex = m_DblClickInitPosEnd; + } } } } diff --git a/Modules/Marshalling/MarshallingTests.bindings.cs b/Modules/Marshalling/MarshallingTests.bindings.cs index 5b50ced44b..6a8019eb19 100644 --- a/Modules/Marshalling/MarshallingTests.bindings.cs +++ b/Modules/Marshalling/MarshallingTests.bindings.cs @@ -901,6 +901,13 @@ internal class ValueTypeSpanTests [NativeThrows] public static extern void ParameterCharReadOnlySpan(ReadOnlySpan param); [NativeThrows] public static extern void ParameterEnumReadOnlySpan(ReadOnlySpan param); [NativeThrows] public static extern void ParameterBlittableCornerCaseStructReadOnlySpan(ReadOnlySpan param); + public static extern Span ReturnsArrayRefWritableAsSpan(int val1, int val2, int val3); + public static extern Span ReturnsCoreVectorRefAsSpan(int val1, int val2, int val3); + public static extern Span ReturnsScriptingSpanAsSpan(int val1, int val2, int val3); + public static extern ReadOnlySpan ReturnsArrayRefWritableAsReadOnlySpan(int val1, int val2, int val3); + public static extern ReadOnlySpan ReturnsCoreVectorRefAsReadOnlySpan(int val1, int val2, int val3); + public static extern ReadOnlySpan ReturnsArrayRefAsReadOnlySpan(int val1, int val2, int val3); + public static extern ReadOnlySpan ReturnsScriptingReadOnlySpanAsSpan(int val1, int val2, int val3); } // -------------------------------------------------------------------- diff --git a/Modules/PackageManagerUI/Editor/External/SemVersionExtension.cs b/Modules/PackageManagerUI/Editor/External/SemVersionExtension.cs index 55df745938..98eafd5d32 100644 --- a/Modules/PackageManagerUI/Editor/External/SemVersionExtension.cs +++ b/Modules/PackageManagerUI/Editor/External/SemVersionExtension.cs @@ -26,13 +26,13 @@ public static bool IsEqualOrPatchOf(this SemVersion version, SemVersion? olderVe return version.Major == olderVersion?.Major && version.Minor == olderVersion?.Minor && version >= olderVersion; } - // The implementation here matches with the experimental version tagging check implemented in Package Validation Suite (US0017 CorrectPackageVersionTags) - public static bool IsExperimental(this SemVersion version) + // The implementation to check for Experimental packages here matches with the experimental version tagging check implemented in Package Validation Suite (US0017 CorrectPackageVersionTags) + public static PackageTag GetExpOrPreOrReleaseTag(this SemVersion version) { if (string.IsNullOrEmpty(version.Prerelease)) - return false; + return PackageTag.Release; var match = k_ExpRegex.Match(version.Prerelease); - return match.Success && match.Groups["iteration"].Success && match.Groups["feature"].Value.Length <= 10; + return match.Success && match.Groups["iteration"].Success && match.Groups["feature"].Value.Length <= 10 ? PackageTag.Experimental : PackageTag.PreRelease; } } } diff --git a/Modules/PackageManagerUI/Editor/Services/Pages/PageManager.cs b/Modules/PackageManagerUI/Editor/Services/Pages/PageManager.cs index 9b1645fed7..3424492f12 100644 --- a/Modules/PackageManagerUI/Editor/Services/Pages/PageManager.cs +++ b/Modules/PackageManagerUI/Editor/Services/Pages/PageManager.cs @@ -243,8 +243,6 @@ public IPage FindPage(IList packages) if (packages.All(p => page.ShouldInclude(p))) return page; - if (!m_SettingsProxy.enablePreReleasePackages && packages.Any(p => p.versions.primary.version?.Prerelease.StartsWith("pre.") == true)) - Debug.Log(L10n.Tr("You must check \"Show Pre-release Package Versions\" in Project Settings > Package Manager in order to see this package.")); return null; } diff --git a/Modules/PackageManagerUI/Editor/Services/Upm/UpmClient.cs b/Modules/PackageManagerUI/Editor/Services/Upm/UpmClient.cs index 55ddb71bf7..a0837f8c2c 100644 --- a/Modules/PackageManagerUI/Editor/Services/Upm/UpmClient.cs +++ b/Modules/PackageManagerUI/Editor/Services/Upm/UpmClient.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using UnityEditor.PackageManager.Requests; +using UnityEditor.Scripting.ScriptCompilation; using UnityEngine; namespace UnityEditor.PackageManager.UI.Internal @@ -110,7 +111,7 @@ public UpmClient(IUpmCache upmCache, public bool IsAnyExperimentalPackagesInUse() { - return PackageInfo.GetAllRegisteredPackages().Any(info => (info.version.Contains("-preview") || info.version.Contains("-exp.") || info.version.StartsWith("0.")) && IsUnityPackage(info)); + return PackageInfo.GetAllRegisteredPackages().Any(info => SemVersionParser.TryParse(info.version, out var parsedVersion) && parsedVersion?.GetExpOrPreOrReleaseTag() == PackageTag.Experimental); } public void OnBeforeSerialize() diff --git a/Modules/PackageManagerUI/Editor/Services/Upm/UpmPackageVersion.cs b/Modules/PackageManagerUI/Editor/Services/Upm/UpmPackageVersion.cs index 241decdad3..53d0c8be13 100644 --- a/Modules/PackageManagerUI/Editor/Services/Upm/UpmPackageVersion.cs +++ b/Modules/PackageManagerUI/Editor/Services/Upm/UpmPackageVersion.cs @@ -228,13 +228,10 @@ private void RefreshTags(PackageInfo packageInfo) if (!HasTag(PackageTag.InstalledFromPath) && packageInfo.versions.deprecated.Contains(m_VersionString)) m_Tag |= PackageTag.Deprecated; - if (isInvalidSemVerInManifest) + if (isInvalidSemVerInManifest || m_Version == null) return; - if (m_Version?.IsExperimental() == true) - m_Tag |= PackageTag.Experimental; - else - m_Tag |= string.IsNullOrEmpty(m_Version?.Prerelease) ? PackageTag.Release : PackageTag.PreRelease; + m_Tag |= m_Version.Value.GetExpOrPreOrReleaseTag(); } public override string GetDescriptor(bool isFirstLetterCapitalized = false) diff --git a/Modules/PackageManagerUI/Editor/UI/PackageDetailsTabs/PackageDetailsVersionsTab.cs b/Modules/PackageManagerUI/Editor/UI/PackageDetailsTabs/PackageDetailsVersionsTab.cs index 76cc9550e3..0170c82699 100644 --- a/Modules/PackageManagerUI/Editor/UI/PackageDetailsTabs/PackageDetailsVersionsTab.cs +++ b/Modules/PackageManagerUI/Editor/UI/PackageDetailsTabs/PackageDetailsVersionsTab.cs @@ -111,8 +111,7 @@ protected override void RefreshContent(IPackageVersion version) return; } - var seeVersionsToolbar = versions.numUnloadedVersions > 0 && (m_SettingsProxy.seeAllPackageVersions || m_Version.availableRegistry != RegistryType.UnityRegistry || m_Version.package.versions.installed?.HasTag(PackageTag.Experimental) == true); - UIUtils.SetElementDisplay(m_VersionsToolbar, seeVersionsToolbar); + UIUtils.SetElementDisplay(m_VersionsToolbar, versions.numUnloadedVersions > 0); UIUtils.SetElementDisplay(m_LoadingLabel, false); var primaryVersion = m_Version.package?.versions.primary; diff --git a/Modules/PackageManagerUI/Editor/UI/PackageManagerWindowRoot.cs b/Modules/PackageManagerUI/Editor/UI/PackageManagerWindowRoot.cs index 9ff2cdd56f..3fccba18ff 100644 --- a/Modules/PackageManagerUI/Editor/UI/PackageManagerWindowRoot.cs +++ b/Modules/PackageManagerUI/Editor/UI/PackageManagerWindowRoot.cs @@ -302,18 +302,18 @@ private bool TryApplyPackageAndPageSelection(PackageAndPageSelectionArgs args, b // of this function to after the `My Assets` logic is done so that we don't break `My Assets` and the Entitlement Error checker. if (!m_PageRefreshHandler.IsInitialFetchingDone(m_PageManager.activePage)) return false; - + if (string.IsNullOrEmpty(args.packageToSelect)) { m_PageManager.activePage = args.page; return true; } - + m_PackageDatabase.GetPackageAndVersionByIdOrName(args.packageToSelect, out var package, out var version, true); if (package == null && failIfPackageIsNotFoundInDatabase) return false; - + m_PageManager.activePage = args.page; if (package != null) { @@ -380,11 +380,10 @@ public void SelectPackageAndPage(string packageToSelect = null, string pageId = var packageToSelectSplit = packageToSelect.Split('@'); var versionString = packageToSelectSplit.Length == 2 ? packageToSelectSplit[1] : string.Empty; - // Package is not found in PackageDatabase but we can determine if it's a preview package or not with it's version string. - SemVersionParser.TryParse(versionString, out var semVersion); - if (!m_SettingsProxy.enablePreReleasePackages && semVersion.HasValue && (semVersion.Value.Major == 0 || semVersion.Value.Prerelease.StartsWith("preview"))) + // Package is not found in PackageDatabase, but we can determine if it's a prerelease package or not with its version string. + if (!m_SettingsProxy.enablePreReleasePackages && SemVersionParser.TryParse(versionString, out var semVersion) && semVersion?.GetExpOrPreOrReleaseTag() == PackageTag.PreRelease) { - Debug.Log("You must check \"Enable Preview Packages\" in Project Settings > Package Manager in order to see this package."); + Debug.Log("You must check \"Enable Pre-release Package Versions\" in Project Settings > Package Manager in order to see this package."); args.packageToSelect = null; } args.page = m_PageManager.activePage; diff --git a/Modules/QuickSearch/Editor/SearchService.cs b/Modules/QuickSearch/Editor/SearchService.cs index 9f084a08e8..7cfe6a2d79 100644 --- a/Modules/QuickSearch/Editor/SearchService.cs +++ b/Modules/QuickSearch/Editor/SearchService.cs @@ -38,7 +38,7 @@ public class SearchActionsProviderAttribute : Attribute public static class SearchService { private const int k_MaxFetchTimeMs = 50; - private const int k_MaxSessionTimeMs = 60000; + private const int k_MaxSessionTimeMs = SearchSession.k_InfiniteSession; static SearchProvider s_SearchServiceProvider; /// diff --git a/Modules/QuickSearch/Editor/SearchSession.cs b/Modules/QuickSearch/Editor/SearchSession.cs index 72920eec3f..25784bc6a6 100644 --- a/Modules/QuickSearch/Editor/SearchSession.cs +++ b/Modules/QuickSearch/Editor/SearchSession.cs @@ -200,11 +200,12 @@ class SearchSession : BaseAsyncIEnumerableHandler /// This event is used to know when a search has finished fetching items. /// public event Action sessionEnded; + public const int k_InfiniteSession = -1; private SearchSessionContext m_Context; private SearchProvider m_Provider; private Stopwatch m_SessionTimer = new Stopwatch(); - private const long k_DefaultSessionTimeOut = 10000; + private const long k_DefaultSessionTimeOut = k_InfiniteSession; private long m_SessionTimeOut = k_DefaultSessionTimeOut; /// @@ -280,7 +281,7 @@ public override void Update(List newItems) m_SessionTimer.Restart(); } - if (m_SessionTimer.ElapsedMilliseconds > m_SessionTimeOut) + if (m_SessionTimeOut > 0 && m_SessionTimer.ElapsedMilliseconds > m_SessionTimeOut) { // Do this before stopping to get target IEnumerator var timeOutError = BuildSessionContextTimeOutError(); diff --git a/Modules/QuickSearch/Editor/SearchUtils.cs b/Modules/QuickSearch/Editor/SearchUtils.cs index 6da7e00741..0a86561552 100644 --- a/Modules/QuickSearch/Editor/SearchUtils.cs +++ b/Modules/QuickSearch/Editor/SearchUtils.cs @@ -1500,6 +1500,7 @@ internal static ISearchView OpenWithContextualProviders(params string[] provider return OpenWithContextualProviders("", providerIds); } + [Flags] internal enum OpenWithContextualProvidersFlags { None = 0, diff --git a/Modules/QuickSearch/Editor/UITK/SearchView.cs b/Modules/QuickSearch/Editor/UITK/SearchView.cs index 064355092f..e2b8a28b9b 100644 --- a/Modules/QuickSearch/Editor/UITK/SearchView.cs +++ b/Modules/QuickSearch/Editor/UITK/SearchView.cs @@ -740,7 +740,7 @@ private void NotifySyncSearch(string groupId, UnityEditor.SearchService.SearchSe syncViewId = typeof(SceneSearchEngine).FullName; break; } - UnityEditor.SearchService.SearchService.NotifySyncSearchChanged(evt, syncViewId, context.searchText); + UnityEditor.SearchService.SearchService.NotifySyncSearchChanged(evt, syncViewId, context.searchQuery); } public void SetupColumns(IList fields) diff --git a/Modules/TextCoreTextEngine/Managed/AssemblyInfo.cs b/Modules/TextCoreTextEngine/Managed/AssemblyInfo.cs index 950726187e..66dddeeb84 100644 --- a/Modules/TextCoreTextEngine/Managed/AssemblyInfo.cs +++ b/Modules/TextCoreTextEngine/Managed/AssemblyInfo.cs @@ -34,3 +34,4 @@ [assembly: InternalsVisibleTo("Unity.TextCore.Editor.Tests")] [assembly: InternalsVisibleTo("Unity.TextMeshPro.Editor")] [assembly: InternalsVisibleTo("Unity.TextMeshPro.Editor.Tests")] +[assembly: InternalsVisibleTo("Unity.UIElements.EditorTests")] diff --git a/Modules/TextCoreTextEngine/Managed/NativeTextInfo.cs b/Modules/TextCoreTextEngine/Managed/NativeTextInfo.cs index 08f6db045a..2f05cca2ad 100644 --- a/Modules/TextCoreTextEngine/Managed/NativeTextInfo.cs +++ b/Modules/TextCoreTextEngine/Managed/NativeTextInfo.cs @@ -7,6 +7,7 @@ namespace UnityEngine.TextCore.Text { + [VisibleToOtherModules("UnityEngine.UIElementsModule", "UnityEngine.IMGUIModule")] [StructLayout(LayoutKind.Sequential)] [NativeHeader("Modules/TextCoreTextEngine/Native/TextInfo.h")] diff --git a/Modules/TextCoreTextEngine/Managed/TextAssets/FontAsset.cs b/Modules/TextCoreTextEngine/Managed/TextAssets/FontAsset.cs index 43b08a1a4d..4df9b7ecde 100644 --- a/Modules/TextCoreTextEngine/Managed/TextAssets/FontAsset.cs +++ b/Modules/TextCoreTextEngine/Managed/TextAssets/FontAsset.cs @@ -956,6 +956,7 @@ private void OnValidate() // We need to update those lists to native in case they changed UpdateFallbacks(); UpdateWeightFallbacks(); + UpdateFaceInfo(); } } diff --git a/Modules/TextCoreTextEngine/Managed/TextAssets/FontAsset/NativeFontAsset.cs b/Modules/TextCoreTextEngine/Managed/TextAssets/FontAsset/NativeFontAsset.cs index 7ee23f9dd5..71f97c9dd3 100644 --- a/Modules/TextCoreTextEngine/Managed/TextAssets/FontAsset/NativeFontAsset.cs +++ b/Modules/TextCoreTextEngine/Managed/TextAssets/FontAsset/NativeFontAsset.cs @@ -44,6 +44,11 @@ internal void UpdateWeightFallbacks() UpdateWeightFallbacks(nativeFontAsset, weights.Item1, weights.Item2); } + internal void UpdateFaceInfo() + { + UpdateFaceInfo(nativeFontAsset, faceInfo); + } + internal IntPtr[] GetFallbacks() { List fallbackList = new List(); @@ -177,6 +182,7 @@ private bool HasRecursionInternal(FontAsset fontAsset) private static extern void UpdateWeightFallbacks(IntPtr ptr, IntPtr[] regularFallbacks, IntPtr[] italicFallbacks); private static extern IntPtr Create(FaceInfo faceInfo, Font sourceFontFile, Font sourceFont_EditorRef, string sourceFontFilePath, int fontInstanceID, IntPtr[] fallbacks, IntPtr[] weightFallbacks, IntPtr[] italicFallbacks); + private static extern void UpdateFaceInfo(IntPtr ptr, FaceInfo faceInfo); [FreeFunction("FontAsset::Destroy")] private static extern void Destroy(IntPtr ptr); diff --git a/Modules/TextCoreTextEngine/Managed/TextGenerator/NativeTextGenerationSettings.bindings.cs b/Modules/TextCoreTextEngine/Managed/TextGenerator/NativeTextGenerationSettings.bindings.cs index 5c138ebf66..771a828471 100644 --- a/Modules/TextCoreTextEngine/Managed/TextGenerator/NativeTextGenerationSettings.bindings.cs +++ b/Modules/TextCoreTextEngine/Managed/TextGenerator/NativeTextGenerationSettings.bindings.cs @@ -7,6 +7,7 @@ using UnityEngine.Bindings; using UnityEngine.Scripting; using UnityEngine.TextCore.Text; +using System.Collections.Generic; namespace UnityEngine.TextCore { @@ -18,28 +19,68 @@ internal struct NativeTextGenerationSettings { public IntPtr fontAsset; public IntPtr[] globalFontAssetFallbacks; - public string text; // TODO: use RenderedText instead of string here + public string text; // Contains the parsed text, meaning the rich text tags have been removed. public int screenWidth; // Encoded in Fixed Point. public int screenHeight; // Encoded in Fixed Point. - public int fontSize; // Encoded in Fixed Point. public WhiteSpace wordWrap; public LanguageDirection languageDirection; - + public int vertexPadding; // Encoded in Fixed Point. [VisibleToOtherModules("UnityEngine.UIElementsModule")] internal HorizontalAlignment horizontalAlignment; [VisibleToOtherModules("UnityEngine.UIElementsModule")] internal VerticalAlignment verticalAlignment; - public Color32 color; + public int fontSize; // Encoded in Fixed Point. public FontStyles fontStyle; public TextFontWeight fontWeight; - public int vertexPadding; // Encoded in Fixed Point. + + public TextSpan[] textSpans; + public Color32 color; + + public bool hasLink => textSpans != null && Array.Exists(textSpans, span => span.linkID != -1); + + public readonly TextSpan CreateTextSpan() + { + return new TextSpan() + { + fontAsset = this.fontAsset, + fontSize = this.fontSize, + color = this.color, + fontStyle = this.fontStyle, + fontWeight = this.fontWeight, + linkID = -1 + }; + } + + // Used by automated tests + public string GetTextSpanContent(int spanIndex) + { + if (string.IsNullOrEmpty(text)) + { + throw new InvalidOperationException("The text property is null or empty."); + } + + if (textSpans == null || spanIndex < 0 || spanIndex >= textSpans.Length) + { + throw new ArgumentOutOfRangeException(nameof(spanIndex), "Invalid span index."); + } + + TextSpan span = textSpans[spanIndex]; + + if (span.startIndex < 0 || span.startIndex >= text.Length || span.startIndex + span.length > text.Length) + { + throw new ArgumentOutOfRangeException(nameof(spanIndex), "Invalid startIndex or length for the current text."); + } + + return text.Substring(span.startIndex, span.length); + } public static NativeTextGenerationSettings Default => new () { fontStyle = FontStyles.Normal, - fontWeight = TextFontWeight.Regular + fontWeight = TextFontWeight.Regular, + color = Color.black, }; // Used by automated tests @@ -59,28 +100,73 @@ internal NativeTextGenerationSettings(NativeTextGenerationSettings tgs) fontWeight = tgs.fontWeight; languageDirection = tgs.languageDirection; vertexPadding = tgs.vertexPadding; + textSpans = tgs.textSpans != null ? (TextSpan[])tgs.textSpans.Clone() : null; } public override string ToString() { string fallbacksString = globalFontAssetFallbacks != null - ? $"{string.Join(", ", globalFontAssetFallbacks)}" - : "null"; + ? $"{string.Join(", ", globalFontAssetFallbacks)}" + : "null"; + + string textSpansString = "null"; + if (textSpans != null) + { + System.Text.StringBuilder sb = new System.Text.StringBuilder(); + sb.Append("["); + for (int i = 0; i < textSpans.Length; i++) + { + if (i > 0) + { + sb.Append(", "); + } + sb.Append(textSpans[i].ToString()); + } + sb.Append("]"); + textSpansString = sb.ToString(); + } return $"{nameof(fontAsset)}: {fontAsset}\n" + - $"{nameof(globalFontAssetFallbacks)}: {fallbacksString}\n" + - $"{nameof(text)}: {text}\n" + - $"{nameof(screenWidth)}: {screenWidth}\n" + - $"{nameof(screenHeight)}: {screenHeight}\n" + - $"{nameof(fontSize)}: {fontSize}\n" + - $"{nameof(wordWrap)}: {wordWrap}\n" + - $"{nameof(languageDirection)}: {languageDirection}\n" + - $"{nameof(horizontalAlignment)}: {horizontalAlignment}\n" + - $"{nameof(verticalAlignment)}: {verticalAlignment}\n" + - $"{nameof(color)}: {color}\n" + - $"{nameof(fontStyle)}: {fontStyle}\n" + - $"{nameof(fontWeight)}: {fontWeight}\n" + - $"{nameof(vertexPadding)}: {vertexPadding}"; + $"{nameof(globalFontAssetFallbacks)}: {fallbacksString}\n" + + $"{nameof(text)}: {text}\n" + + $"{nameof(screenWidth)}: {screenWidth}\n" + + $"{nameof(screenHeight)}: {screenHeight}\n" + + $"{nameof(fontSize)}: {fontSize}\n" + + $"{nameof(wordWrap)}: {wordWrap}\n" + + $"{nameof(languageDirection)}: {languageDirection}\n" + + $"{nameof(horizontalAlignment)}: {horizontalAlignment}\n" + + $"{nameof(verticalAlignment)}: {verticalAlignment}\n" + + $"{nameof(color)}: {color}\n" + + $"{nameof(fontStyle)}: {fontStyle}\n" + + $"{nameof(fontWeight)}: {fontWeight}\n" + + $"{nameof(vertexPadding)}: {vertexPadding}\n" + + $"{nameof(textSpans)}: {textSpansString}"; + } + } + + [VisibleToOtherModules( "UnityEngine.UIElementsModule")] + [StructLayout(LayoutKind.Sequential)] + internal struct TextSpan + { + public int startIndex; + public int length; + public IntPtr fontAsset; + public int fontSize; // Encoded in Fixed Point. + public Color32 color; + public FontStyles fontStyle; + public TextFontWeight fontWeight; + public int linkID; + + public override string ToString() + { + return $"{nameof(color)}: {color}\n" + + $"{nameof(fontStyle)}: {fontStyle}\n" + + $"{nameof(fontWeight)}: {fontWeight}\n" + + $"{nameof(linkID)}: {linkID}\n" + + $"{nameof(fontSize)}: {fontSize}\n" + + $"{nameof(fontAsset)}: {fontAsset}" + + $"{nameof(startIndex)}: {startIndex}\n" + + $"{nameof(length)}: {length}"; } } diff --git a/Modules/TextCoreTextEngine/Managed/TextGenerator/RichTextTagParser.cs b/Modules/TextCoreTextEngine/Managed/TextGenerator/RichTextTagParser.cs new file mode 100644 index 0000000000..9d7b29092f --- /dev/null +++ b/Modules/TextCoreTextEngine/Managed/TextGenerator/RichTextTagParser.cs @@ -0,0 +1,679 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System.Collections.Generic; +using System; +using System.Text; +using UnityEngine.Bindings; + +#nullable enable + +namespace UnityEngine.TextCore +{ + + [VisibleToOtherModules("UnityEngine.UIElementsModule")] + internal static class RichTextTagParser + { + public enum TagType + { + Hyperlink, + Align, + AllCaps, + Alpha, + Bold, + Br, + Color, + CSpace, + Font, + FontWeight, + Italic, + Indent, + LineHeight, + LineIndent, + Link, + Lowercase, + Mark, + Mspace, + NoBr, + NoParse, + Strikethrough, + Size, + SmallCaps, + Space, + Sprite, + Style, + Subscript, + Superscript, + Underline, + Uppercase, + Unknown // Not a real tag, used to indicate an error + + //gradient: margin, pos, rotate , width, voffset will not be supported + } + + internal record TagTypeInfo + { + internal TagTypeInfo(TagType tagType, string name, TagValueType valueType = TagValueType.None, TagUnitType unitType = TagUnitType.Pixels) + { + TagType = tagType; + this.name = name; + this.valueType = valueType; + this.unitType = unitType; + } + + public TagType TagType; + public string name; + public TagValueType valueType; + public TagUnitType unitType; + } + + internal readonly static TagTypeInfo[] TagsInfo = + { + new TagTypeInfo(TagType.Hyperlink, "a"), + new TagTypeInfo(TagType.Align,"align"), //"left", "center", "right", "justified", "flush" + new TagTypeInfo(TagType.AllCaps, "allcaps"), //none + new TagTypeInfo(TagType.Alpha, "alpha"), //FF CC AA 88 66 44 22 00 + new TagTypeInfo(TagType.Bold,"b" ), + new TagTypeInfo(TagType.Br,"br"), + new TagTypeInfo(TagType.Color,"color",TagValueType.ColorValue), //Red Dark Green <#0000FF>Blue Semitransparent Red + new TagTypeInfo(TagType.CSpace,"cspace" ), //Spacing is just as important as timing. + new TagTypeInfo(TagType.Font,"font"), //a different font? or just a different material? + new TagTypeInfo(TagType.FontWeight,"font-weight"), //Thin + new TagTypeInfo(TagType.Italic,"i"), + new TagTypeInfo(TagType.Indent,"indent"), // pixels, font units, or percentages. + new TagTypeInfo(TagType.LineHeight,"line-height"), //pixels, font units, or percentages. Rather cozy. + new TagTypeInfo(TagType.LineIndent,"line-indent" ), //pixels, font units, or percentages. + new TagTypeInfo(TagType.Link, "link"), //my link + new TagTypeInfo(TagType.Lowercase,"lowercase"),//none + new TagTypeInfo(TagType.Mark,"mark" ), // + new TagTypeInfo(TagType.Mspace,"mspace" ), // monospace : pixels or font units. + new TagTypeInfo(TagType.NoBr,"nobr"), // none + new TagTypeInfo(TagType.NoParse,"noparse"), // none + new TagTypeInfo(TagType.Strikethrough,"s"), //striketrhough + new TagTypeInfo(TagType.Size,"size"), // // pixels, font units, or percentage. Pixel adjustments can be absolute (5px, 10px, and so on) or relative (+1 or -1, for example). Relative sizes are based on the original font size, so they're not cumulative. + new TagTypeInfo(TagType.SmallCaps,"smallcaps" ),//none + new TagTypeInfo(TagType.Space,"space" ), //pixels or font units. + new TagTypeInfo(TagType.Sprite,"sprite"), + // , or by name . + //tint=1 attribute to the tag tints the sprite with the TextMesh Pro object's Vertex Color. You can choose a different color by adding a color attribute to the tag (color=#FFFFFF). + new TagTypeInfo ( TagType.Style,"style"), //Styles + new TagTypeInfo ( TagType.Subscript,"sub" ), + new TagTypeInfo ( TagType.Superscript,"sup" ), + new TagTypeInfo ( TagType.Underline,"u"), + new TagTypeInfo ( TagType.Uppercase,"uppercase"),//none + // page + + }; + + internal enum TagValueType + { + None = 0x0, + NumericalValue = 0x1, + StringValue = 0x2, + ColorValue = 0x4, + } + + internal enum TagUnitType + { + Pixels = 0x0, + FontUnits = 0x1, + Percentage = 0x2, + } + + //TODO : change this for an union when development is over to save memory + // we possibly could remove the type check on getter unless in debug mode + //[StructLayout(LayoutKind.Explicit)] + internal record TagValue + { + internal TagValue(float value) + { + type = TagValueType.NumericalValue; + m_numericalValue = value; + } + + internal TagValue(Color value) + { + type = TagValueType.ColorValue; + m_colorValue = value; + } + + internal TagValue(string value) + { + type = TagValueType.StringValue; + m_stringValue = value; + } + + //[FieldOffset(0)] + internal TagValueType type; + + //[FieldOffset(4)] + //private TagUnitType unit; + + //[FieldOffset(8)] + private string? m_stringValue; + + //[FieldOffset(8)] + private float m_numericalValue; + + //[FieldOffset(8)] + private Color m_colorValue; + + + internal string? StringValue + { + get + { + if (type != TagValueType.StringValue) + throw new InvalidOperationException("Not a string value"); + return m_stringValue; + } + } + + internal float NumericalValue + { + get + { + if (type != TagValueType.NumericalValue) + throw new InvalidOperationException("Not a numerical value"); + return m_numericalValue; + } + } + + internal Color ColorValue + { + get + { + if (type != TagValueType.ColorValue) + throw new InvalidOperationException("Not a color value"); + return m_colorValue; + } + } + + + } + + + internal struct Tag + { + public TagType tagType; + public bool isClosing; + public int start; //position of the '<' character + public int end; //position of the '>' character + public TagValue? value; //could be replaced by a nullable struct? + } + + public struct Segment + { + public List? tags; + public int start; + public int end; + } + + internal record ParseError + { + internal ParseError(string message, int position) + { + this.message = message; + this.position = position; + } + public readonly int position; + public readonly string message; + } + + static bool tagMatch(ReadOnlySpan tagCandidate, string tagName) + { + return tagCandidate.StartsWith(tagName.AsSpan()) && (tagCandidate.Length == tagName.Length || (!char.IsLetter(tagCandidate[tagName.Length]) && tagCandidate[tagName.Length] != '-')); + } + + //Return true if there is a match + static bool SpanToEnum(ReadOnlySpan tagCandidate, out TagType tagType, out string? error, out ReadOnlySpan attribute) + { + for (int i = 0; i < TagsInfo.Length; i++) + { + string tagName = TagsInfo[i].name; + if (tagMatch(tagCandidate, tagName)) + { + tagType = TagsInfo[i].TagType; + error = null; + attribute = tagCandidate.Slice(tagName.Length);//Support only one attribute for now + return true; + } + } + + //Special case for color where there is no tag, just the attribute. + if(tagCandidate.Length > 4 &&tagCandidate[0] == '#') + { + tagType = TagType.Color; + error = null; + attribute = tagCandidate; + return true; + } + + error = "Unknown tag: " + tagCandidate.ToString(); + tagType = TagType.Unknown; + attribute = null; + return false; + } + + + internal static List FindTags(string inputStr, List? errors = null) + { + var input = inputStr.ToCharArray(); + var result = new List(); + int pos = 0; + + while (true) + { + var start = Array.IndexOf(input, '<', pos); + if (start == -1) // no tag + break; + + var end = Array.IndexOf(input, '>', start); + if (end == -1) + break; + + bool isClosing = (input.Length > start + 1 && input[start + 1] == '/'); + + if (end == start + 1) + { + errors?.Add(new("Empty tag", start)); + pos = end + 1; + continue; + } + + pos = end + 1; + + if (!isClosing) + { + var span = input.AsSpan(start + 1, end - start - 1); + if (SpanToEnum(span, out TagType tagType, out string? error, out var atributeSection)) + { + // TODO Manual parsing of color need to be moved elsewhere + TagValue? value = null; + if (tagType == TagType.Color) + { + if (atributeSection.Length >= 2 && atributeSection[0] == '=') + atributeSection = atributeSection.Slice(1); // we should probably have a better way to do this + + if (atributeSection.Length >= 4 && atributeSection[0] == '"' && atributeSection[atributeSection.Length - 1] == '"') + { + ColorUtility.TryParseHtmlString(atributeSection.Slice(1, atributeSection.Length - 2).ToString(), out Color color); + value = new TagValue(color); + } + else + { + ColorUtility.TryParseHtmlString(atributeSection.ToString(), out Color color); + value = new TagValue(color); + } + + if (value is null) + { + errors?.Add(new("Invalid color value", start)); + pos = start + 1; //malformed tag, skip the '<' character + continue; + } + } + + if (tagType == TagType.Link || tagType == TagType.Hyperlink) + { + if (tagType == TagType.Hyperlink && atributeSection.StartsWith(" href=")) + atributeSection = atributeSection.Slice(" href=".Length); + + // strip the = for . The lenght need to be checked so that it is greater than 0 + if (atributeSection.Length >= 1 && atributeSection[0] == '=') + atributeSection = atributeSection.Slice(1); // we should probably have a better way to do this + + // strip the quotes for both and + // The length need to be checked so that it is greater than 0 + // Quotes are not mandatory for link tag and for url it isn't problematic if they aren't there unless there is a <> in the url. + // We would need to stop parsing from the beginning of the quote until the second one (a bit like for the noparse tags) for supporting <> character in url + if (atributeSection.Length >= 2 && atributeSection[0] == '"' && atributeSection[atributeSection.Length - 1] == '"') + { + value = new TagValue(atributeSection.Slice(1, atributeSection.Length - 2).ToString()); + } + else + { + value = new TagValue(atributeSection.ToString()); + } + } + + result.Add(new Tag { tagType = tagType, start = start, end = end, isClosing = isClosing, value = value }); + + if (tagType == TagType.NoParse) + { + //Not uisng the real loop to skip all "malformed tags" errors. + if ((start = input.AsSpan(pos).IndexOf("")) == -1) + { + break; // no closing noparse tag, no need to cleanup + } + start += pos; //The start index was relative to the span, we need to make it relative to the input + end = start + "".Length; + result.Add(new Tag { tagType = TagType.NoParse, start = start, end = end, isClosing = true }); + pos = end + 1; + } + } + else + { + if (error is not null) + errors?.Add(new(error, start)); + + pos = start + 1; //malformed tag, skip the '<' character + } + } + else + { + if (SpanToEnum(input.AsSpan(start + 2, end - start - 2), out TagType tagType, out string? error, out var _)) + { + result.Add(new Tag { tagType = tagType, start = start, end = end, isClosing = isClosing }); + } + else + { + if (error is not null) + errors?.Add(new(error, start)); + + pos = start + 1; //malformed tag, skip the '<' character + } + } + + + } + + return result; + } + + + // Return a list of tags that will be applied to the text + // previousTags can be empty to skip the allocation of a new list + + internal static List PickResultingTags(List allTags, string input, int atPosition, List? applicableTags = null) + { + if (applicableTags == null) + { + applicableTags = new List(); + } + else + { + applicableTags.Clear(); + } + + int startingPos = 0; //Assument the starting position is always 0 as we do not backup the stack infos.. + // TODO incremental procesing, will have to review methods parameter and structure... + // TOOD clean the checks belos + + Debug.Assert(string.IsNullOrEmpty(input) || (atPosition < input.Length && atPosition >= 0), "Invalid position"); + Debug.Assert(startingPos <= atPosition && startingPos >= 0, "Invalid starting position"); + + int previousTagPosition = 0; + foreach(var tag in allTags) + { + Debug.Assert(tag.start >= previousTagPosition, "Tags are not sorted"); + previousTagPosition = tag.end+1; + } + + foreach(var tag in applicableTags) + { + Debug.Assert(tag.end <= startingPos, "Tag end pass the point where we should start parsing"); + Debug.Assert(allTags.Contains(tag)); + } + Span parents = stackalloc int?[allTags.Count]; + Span lastTagOfType = stackalloc int?[TagsInfo.Length]; + + int i = -1; + foreach (var tag in allTags) + { + i++; + if (tag.end < startingPos) + { + continue; + } + + if (tag.tagType == TagType.NoParse) + { + continue; + } + + if (tag.start > atPosition) + { + break; // we are done. + } + + + if (tag.isClosing) + { + if (lastTagOfType[(int)tag.tagType].HasValue) + { + if (parents[i].HasValue) + { + lastTagOfType[(int)tag.tagType] = parents[i]; + } + else + lastTagOfType[(int)tag.tagType] = null; + } + } + else + { + //New tag, set as last tag open and nest under parent if there was one already + + var currentLastTagIndex = lastTagOfType[(int)tag.tagType]; + if (currentLastTagIndex.HasValue) + { + parents[i] = currentLastTagIndex; + } + + lastTagOfType[(int)tag.tagType] = i; + + } + + } + + //The order in the resulting list is important: we cannot iterate only adding lastTagOfType + int currentTagIndex = 0; + foreach (var tag in allTags) + { + var lastTag = lastTagOfType[(int)tag.tagType]; + if (lastTag.HasValue && currentTagIndex == lastTag.Value) + applicableTags.Add(tag); + + currentTagIndex++; + } + + return applicableTags; + } + + + //Return a list of text setgment that will share the same text generation settings between two tags + internal static Segment[] GenerateSegments(string input, List tags) + { + List segments = new List(); + int afterPreviousTagEnd = 0; + for (int i = 0; i < tags.Count; i++) + { + Debug.Assert(tags[i].start >= afterPreviousTagEnd); + //If the tag is consecutive, no segment is generated + if (tags[i].start > afterPreviousTagEnd) + { + segments.Add(new Segment { start = afterPreviousTagEnd, end = tags[i].start - 1 }); // tags[i].start-1 wont be negative because afterPreviousTagEnd start at 0 and we are greater + } + afterPreviousTagEnd = tags[i].end + 1; + } + + //Check if there is a segment after the last tag + if (afterPreviousTagEnd < input.Length) + { + segments.Add(new Segment { start = afterPreviousTagEnd, end = input.Length - 1 }); + } + + //This is ugly, need to be able to modify a reference in a loop later + return segments.ToArray(); + } + + internal static void ApplyStateToSegment(string input, List tags, Segment[] segments) + { + + //tmp list = new List(); + for (int i = 0; i < segments.Length; i++) + { + segments[i].tags = PickResultingTags(tags, input, segments[i].start); + // TODO change to the non alloc version + //segments[i].state.tags = PickResultingTags(tags, input, segments[i].start, i>0?segments[i-1].start :0, list).Copy? ; + // + } + + } + + static private int AddLink(TagType type, string value, List<(int, TagType, string)> links) + { + foreach (var (index, listType, listValue) in links) + { + if (type == listType && value == listValue) + return index; + } + + int nextIndex = links.Count; + links.Add((nextIndex, type, value)); + + return nextIndex; + } + + static TextSpan CreateTextSpan(Segment segment, ref NativeTextGenerationSettings tgs, List<(int, TagType, string)> links, Color hyperlinkColor ) + { + var textSpan = tgs.CreateTextSpan(); + + if (segment.tags is null) + return textSpan; + + for (int i = 0; i < segment.tags.Count; i++) + { + switch (segment.tags[i].tagType) + { + //Font Style + case TagType.Bold: + textSpan.fontWeight = TextCore.Text.TextFontWeight.Bold; + break; + case TagType.Italic: + textSpan.fontStyle |= TextCore.Text.FontStyles.Italic; + break; + case TagType.Underline: + textSpan.fontStyle |= TextCore.Text.FontStyles.Underline; + break; + case TagType.Strikethrough: + textSpan.fontStyle |= TextCore.Text.FontStyles.Strikethrough; + break; + case TagType.Subscript: + textSpan.fontStyle |= TextCore.Text.FontStyles.Subscript; + break; + case TagType.Superscript: + textSpan.fontStyle |= TextCore.Text.FontStyles.Superscript; + break; + case TagType.AllCaps: + case TagType.Uppercase: + textSpan.fontStyle |= TextCore.Text.FontStyles.UpperCase; + break; + case TagType.SmallCaps: + case TagType.Lowercase: + textSpan.fontStyle |= TextCore.Text.FontStyles.LowerCase; + break; + + //Color and appeareance + case TagType.Color: + textSpan.color = segment.tags[i].value!.ColorValue; + break; + case TagType.Alpha: + //TODO tgs.color.a = segment.state.tags[i].value.NumericalValue; + break; + case TagType.Mark: + textSpan.fontStyle |= TextCore.Text.FontStyles.Highlight; + break; + + //Asset required + case TagType.Style: + //TODO : Add support for style + break; + case TagType.Font: + //TODO : Add support for font + break; + case TagType.Hyperlink: + textSpan.linkID = AddLink(TagType.Hyperlink, segment.tags[i].value?.StringValue ?? "", links); + textSpan.color = hyperlinkColor; + textSpan.fontStyle |= TextCore.Text.FontStyles.Underline; + break; + case TagType.Link: + textSpan.linkID = AddLink(TagType.Link, segment.tags[i].value?.StringValue ?? "", links); + break; + case TagType.Sprite: + //TODO : Add support for sprite + break; + + //Layout/Positioning + case TagType.Size: + textSpan.fontSize = (int)(segment.tags[i].value!.NumericalValue/64f); + break; + case TagType.CSpace: + //TODO : Add support for cspace + break; + case TagType.Br: + //TODO : Add support for br + break; + case TagType.Mspace: + //TODO : Add support for mspace + break; + case TagType.LineIndent: + //TODO : Add support for lineindent + break; + case TagType.Space: + //TODO : Add support for space + break; + case TagType.NoBr: + //TODO : Add support for nobr + break; + case TagType.Align: + //TODO : Add support for align + break; + case TagType.LineHeight: + //TODO : Add support for lineheight + break; + + + case TagType.NoParse://Noparse should not be reach here/Should be trimmed + case TagType.Unknown: + throw new InvalidOperationException("Invalid tag type" + segment.tags[i].tagType); + + } + } + + + return textSpan; + } + + [VisibleToOtherModules("UnityEngine.UIElementsModule")] + internal static void CreateTextGenerationSettingsArray(ref NativeTextGenerationSettings tgs, List<(int, TagType, string)> links, Color hyperlinkColor) + { + links.Clear(); + + + var tags = FindTags(tgs.text); + var segments = GenerateSegments(tgs.text, tags); + ApplyStateToSegment(tgs.text, tags, segments); + + var parsedTextBuilder = new StringBuilder(tgs.text.Length); + tgs.textSpans = new TextSpan[segments.Length]; + int parsedIndex = 0; + + for (int i = 0; i < segments.Length; i++) + { + var segment = segments[i]; + string segmentText = tgs.text.Substring(segment.start, segment.end + 1 - segment.start); + + var textSpan = CreateTextSpan(segment, ref tgs,links, hyperlinkColor ); + textSpan.startIndex = parsedIndex; + textSpan.length = segmentText.Length; + tgs.textSpans[i] = textSpan; + parsedTextBuilder.Append(segmentText); + parsedIndex += segmentText.Length; + } + + tgs.text = parsedTextBuilder.ToString(); + } + } +} diff --git a/Modules/TextCoreTextEngine/Managed/TextGenerator/TextLib.bindings.cs b/Modules/TextCoreTextEngine/Managed/TextGenerator/TextLib.bindings.cs index 11616dfdc7..ff1506f795 100644 --- a/Modules/TextCoreTextEngine/Managed/TextGenerator/TextLib.bindings.cs +++ b/Modules/TextCoreTextEngine/Managed/TextGenerator/TextLib.bindings.cs @@ -3,6 +3,7 @@ // https://unity3d.com/legal/licenses/Unity_Reference_Only_License using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using UnityEngine.Bindings; using UnityEngine.Scripting; @@ -27,6 +28,7 @@ internal TextLib() [VisibleToOtherModules("UnityEngine.UIElementsModule")] internal NativeTextInfo GenerateText(NativeTextGenerationSettings settings, IntPtr textGenerationInfo) { + Debug.Assert((settings.fontStyle & FontStyles.Bold) == 0);// Bold need to be set by the fontWeight only. var textInfo = GenerateTextInternal(settings, textGenerationInfo); foreach (ref var meshInfo in textInfo.meshInfos.AsSpan()) @@ -62,10 +64,12 @@ internal NativeTextInfo GenerateText(NativeTextGenerationSettings settings, IntP bottomRightUV.x = topRightUV.x; bottomRightUV.y = bottomLeftUV.y; - textElementInfo.bottomLeft.uv0 = bottomLeftUV; - textElementInfo.topLeft.uv0 = topLeftUV; - textElementInfo.topRight.uv0 = topRightUV; - textElementInfo.bottomRight.uv0 = bottomRightUV; + // The native code is not yet aware of the atlas, and glyphs for the underline+strikethrough have their UV manually eddited to + // be stretched without side effect. We need to combine the position in the atlas with the expected position in the source glyph. + textElementInfo.bottomLeft.uv0 = topRightUV * textElementInfo.bottomLeft.uv0 + bottomLeftUV * (new Vector2(1, 1) - textElementInfo.bottomLeft.uv0); + textElementInfo.topLeft.uv0 = topRightUV * textElementInfo.topLeft.uv0 + bottomLeftUV * (new Vector2(1, 1) - textElementInfo.topLeft.uv0); ; + textElementInfo.topRight.uv0 = topRightUV * textElementInfo.topRight.uv0 + bottomLeftUV * (new Vector2(1, 1) - textElementInfo.topRight.uv0); ; + textElementInfo.bottomRight.uv0 = topRightUV * textElementInfo.bottomRight.uv0 + bottomLeftUV * (new Vector2(1, 1) - textElementInfo.bottomRight.uv0); ; } } return textInfo; @@ -78,6 +82,10 @@ internal NativeTextInfo GenerateText(NativeTextGenerationSettings settings, IntP [NativeMethod(Name = "TextLib::MeasureText")] internal extern Vector2 MeasureText(NativeTextGenerationSettings settings, IntPtr textGenerationInfo); + [VisibleToOtherModules("UnityEngine.UIElementsModule")] + [NativeMethod(Name = "TextLib::FindIntersectingLink")] + static internal extern int FindIntersectingLink(Vector2 point, IntPtr textGenerationInfo); + internal static class BindingsMarshaller { public static IntPtr ConvertToNative(TextLib textLib) => textLib.m_Ptr; @@ -134,10 +142,9 @@ internal static bool GetICUdata(Span data, int maxSize) [VisibleToOtherModules("UnityEngine.UIElementsModule")] internal static class TextGenerationInfo { - internal static extern IntPtr Create(); + public static extern IntPtr Create(); [FreeFunction("TextGenerationInfo::Destroy")] - [VisibleToOtherModules("UnityEngine.UIElementsModule")] - internal static extern void Destroy(IntPtr ptr); + public static extern void Destroy(IntPtr ptr); } } diff --git a/Modules/TextCoreTextEngine/Managed/TextGenerator/TextSelectionService.bindings.cs b/Modules/TextCoreTextEngine/Managed/TextGenerator/TextSelectionService.bindings.cs index b1babeb8b4..3374ef37d4 100644 --- a/Modules/TextCoreTextEngine/Managed/TextGenerator/TextSelectionService.bindings.cs +++ b/Modules/TextCoreTextEngine/Managed/TextGenerator/TextSelectionService.bindings.cs @@ -58,5 +58,20 @@ internal class TextSelectionService [NativeMethod(Name = "TextSelectionService::GetLineNumberFromLogicalIndex")] internal static extern int GetLineNumber(IntPtr textGenerationInfo, int logicalIndex); + + [NativeMethod(Name = "TextSelectionService::SelectToPreviousParagraph")] + internal static extern void SelectToPreviousParagraph(IntPtr textGenerationInfo, ref int cursorIndex); + + [NativeMethod(Name = "TextSelectionService::SelectToStartOfParagraph")] + internal static extern void SelectToStartOfParagraph(IntPtr textGenerationInfo, ref int cursorIndex); + + [NativeMethod(Name = "TextSelectionService::SelectToEndOfParagraph")] + internal static extern void SelectToEndOfParagraph(IntPtr textGenerationInfo, ref int cursorIndex); + + [NativeMethod(Name = "TextSelectionService::SelectToNextParagraph")] + internal static extern void SelectToNextParagraph(IntPtr textGenerationInfo, ref int cursorIndex); + + [NativeMethod(Name = "TextSelectionService::SelectCurrentParagraph")] + internal static extern void SelectCurrentParagraph(IntPtr textGenerationInfo, ref int cursorIndex, ref int selectIndex); } } diff --git a/Modules/TextCoreTextEngine/Managed/TextHandle.cs b/Modules/TextCoreTextEngine/Managed/TextHandle.cs index 3d3213c535..152d374554 100644 --- a/Modules/TextCoreTextEngine/Managed/TextHandle.cs +++ b/Modules/TextCoreTextEngine/Managed/TextHandle.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; using Unity.Jobs.LowLevel.Unsafe; using UnityEngine.Bindings; namespace UnityEngine.TextCore.Text { + [DebuggerDisplay("{settings.text}")] [VisibleToOtherModules("UnityEngine.IMGUIModule", "UnityEngine.UIElementsModule")] internal partial class TextHandle { @@ -181,7 +183,7 @@ public void RemoveTextInfoFromPermanentCache() { s_PermanentCache.RemoveTextInfoFromCache(this); } - + } public static void UpdateCurrentFrame() @@ -465,7 +467,7 @@ public LineInfo GetLineInfoFromCharacterIndex(int index) Debug.LogError("Cannot use GetLineInfoFromCharacterIndex while using Advanced Text"); return new LineInfo(); } - + return textInfo.GetLineInfoFromCharacterIndex(index); } @@ -602,6 +604,56 @@ public void SelectCurrentWord(int index, ref int cursorIndex, ref int selectInde TextSelectionService.SelectCurrentWord(textGenerationInfo, index, ref cursorIndex, ref selectIndex); } + public void SelectCurrentParagraph(ref int cursorIndex, ref int selectIndex) + { + if (!useAdvancedText) + { + Debug.LogError("Cannot use SelectCurrentParagraph while using Standard Text"); + return; + } + TextSelectionService.SelectCurrentParagraph(textGenerationInfo, ref cursorIndex, ref selectIndex); + } + + public void SelectToPreviousParagraph(ref int cursorIndex) + { + if (!useAdvancedText) + { + Debug.LogError("Cannot use SelectToPreviousParagraph while using Standard Text"); + return; + } + TextSelectionService.SelectToPreviousParagraph(textGenerationInfo, ref cursorIndex); + } + + public void SelectToNextParagraph(ref int cursorIndex) + { + if (!useAdvancedText) + { + Debug.LogError("Cannot use SelectToNextParagraph while using Standard Text"); + return; + } + TextSelectionService.SelectToNextParagraph(textGenerationInfo, ref cursorIndex); + } + + public void SelectToStartOfParagraph(ref int cursorIndex) + { + if (!useAdvancedText) + { + Debug.LogError("Cannot use SelectToStartOfParagraph while using Standard Text"); + return; + } + TextSelectionService.SelectToStartOfParagraph(textGenerationInfo, ref cursorIndex); + } + + public void SelectToEndOfParagraph(ref int cursorIndex) + { + if (!useAdvancedText) + { + Debug.LogError("Cannot use SelectToEndOfParagraph while using Standard Text"); + return; + } + TextSelectionService.SelectToEndOfParagraph(textGenerationInfo, ref cursorIndex); + } + internal virtual bool IsAdvancedTextEnabledForElement() { return false; } } } diff --git a/Modules/UIElements/Core/Controls/Slider.cs b/Modules/UIElements/Core/Controls/Slider.cs index f2bc2796a3..d7c5adbe2e 100644 --- a/Modules/UIElements/Core/Controls/Slider.cs +++ b/Modules/UIElements/Core/Controls/Slider.cs @@ -51,8 +51,6 @@ public class Slider : BaseSlider public override void Deserialize(object obj) { - base.Deserialize(obj); - var e = (Slider)obj; if (ShouldWriteAttributeValue(lowValue_UxmlAttributeFlags)) e.lowValue = lowValue; @@ -66,6 +64,9 @@ public override void Deserialize(object obj) e.showInputField = showInputField; if (ShouldWriteAttributeValue(inverted_UxmlAttributeFlags)) e.inverted = inverted; + + // We need to apply the lowValue and highValue before the value to avoid incorrect clamping. + base.Deserialize(obj); } } diff --git a/Modules/UIElements/Core/Panel.cs b/Modules/UIElements/Core/Panel.cs index 0d6aa0d358..34d31dafbc 100644 --- a/Modules/UIElements/Core/Panel.cs +++ b/Modules/UIElements/Core/Panel.cs @@ -1356,6 +1356,8 @@ internal override IVisualTreeUpdater GetUpdater(VisualTreeUpdatePhase phase) { return m_VisualTreeUpdater.GetUpdater(phase); } + + internal virtual Color HyperlinkColor => Color.blue; } internal abstract class BaseRuntimePanel : Panel @@ -1581,6 +1583,7 @@ internal void PointerEntersPanel(int pointerId, Vector2 position) { PointerDeviceState.SavePointerPosition(pointerId, position, this, contextType); } + } internal interface IRuntimePanelComponent diff --git a/Modules/UIElements/Core/Renderer/UIRUtility.cs b/Modules/UIElements/Core/Renderer/UIRUtility.cs index ca50407dda..87dd48242a 100644 --- a/Modules/UIElements/Core/Renderer/UIRUtility.cs +++ b/Modules/UIElements/Core/Renderer/UIRUtility.cs @@ -93,7 +93,7 @@ internal static void ComputeTransformMatrix(VisualElement ve, VisualElement ance k_ComputeTransformMatrixMarker.Begin(); ve.GetPivotedMatrixWithLayout(out result); - VisualElement currentAncestor = ve.parent; + VisualElement currentAncestor = ve.hierarchy.parent; if ((currentAncestor == null) || (ancestor == currentAncestor)) { k_ComputeTransformMatrixMarker.End(); @@ -112,7 +112,7 @@ internal static void ComputeTransformMatrix(VisualElement ve, VisualElement ance else VisualElement.MultiplyMatrix34(ref ancestorMatrix, ref temp, out result); - currentAncestor = currentAncestor.parent; + currentAncestor = currentAncestor.hierarchy.parent; destIsTemp = !destIsTemp; } while ((currentAncestor != null) && (ancestor != currentAncestor)); diff --git a/Modules/UIElements/Core/Text/ATGTextEventHandler.cs b/Modules/UIElements/Core/Text/ATGTextEventHandler.cs new file mode 100644 index 0000000000..6af9abb611 --- /dev/null +++ b/Modules/UIElements/Core/Text/ATGTextEventHandler.cs @@ -0,0 +1,263 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; +using UnityEngine.TextCore.Text; +using UnityEngine.UIElements; + +namespace UnityEngine.UIElements +{ + class ATGTextEventHandler + { + TextElement m_TextElement; + + public ATGTextEventHandler(TextElement textElement) + { + Debug.Assert(textElement.uitkTextHandle.useAdvancedText); + m_TextElement = textElement; + } + + EventCallback m_LinkTagOnPointerDown; + EventCallback m_LinkTagOnPointerUp; + EventCallback m_LinkTagOnPointerMove; + EventCallback m_LinkTagOnPointerOut; + + EventCallback m_HyperlinkOnPointerUp; + EventCallback m_HyperlinkOnPointerMove; + EventCallback m_HyperlinkOnPointerOver; + EventCallback m_HyperlinkOnPointerOut; + + bool HasAllocatedLinkCallbacks() + { + return m_LinkTagOnPointerDown != null; + } + + void AllocateLinkCallbacks() + { + if (HasAllocatedLinkCallbacks()) + return; + + m_LinkTagOnPointerDown = LinkTagOnPointerDown; + m_LinkTagOnPointerUp = LinkTagOnPointerUp; + m_LinkTagOnPointerMove = LinkTagOnPointerMove; + m_LinkTagOnPointerOut = LinkTagOnPointerOut; + } + + bool HasAllocatedHyperlinkCallbacks() + { + return m_HyperlinkOnPointerUp != null; + } + + void AllocateHyperlinkCallbacks() + { + if (HasAllocatedHyperlinkCallbacks()) + return; + + m_HyperlinkOnPointerUp = HyperlinkOnPointerUp; + m_HyperlinkOnPointerMove = HyperlinkOnPointerMove; + m_HyperlinkOnPointerOver = HyperlinkOnPointerOver; + m_HyperlinkOnPointerOut = HyperlinkOnPointerOut; + } + + void HyperlinkOnPointerUp(PointerUpEvent pue) + { + var pos = pue.localPosition - new Vector3(m_TextElement.contentRect.min.x, m_TextElement.contentRect.min.y); + var(type, link) = m_TextElement.uitkTextHandle.ATGFindIntersectingLink(pos); + if (link == null || type!= TextCore.RichTextTagParser.TagType.Hyperlink) + return; + + if (Uri.IsWellFormedUriString(link, UriKind.Absolute)) + Application.OpenURL(link); + } + + internal bool isOverridingCursor; + + void HyperlinkOnPointerOver(PointerOverEvent _) + { + isOverridingCursor = false; + } + + void HyperlinkOnPointerMove(PointerMoveEvent pme) + { + var pos = pme.localPosition - new Vector3(m_TextElement.contentRect.min.x, m_TextElement.contentRect.min.y); + var (type, link) = m_TextElement.uitkTextHandle.ATGFindIntersectingLink(pos); + + var cursorManager = (m_TextElement.panel as BaseVisualElementPanel)?.cursorManager; + if (link != null && type == TextCore.RichTextTagParser.TagType.Hyperlink) + { + + if (!isOverridingCursor) + { + isOverridingCursor = true; + + // defaultCursorId maps to the UnityEditor.MouseCursor enum where 4 is the link cursor. + cursorManager?.SetCursor(new Cursor { defaultCursorId = 4 }); + } + + return; + } + + if (isOverridingCursor) + { + cursorManager?.SetCursor(m_TextElement.computedStyle.cursor); + isOverridingCursor = false; + } + } + + void HyperlinkOnPointerOut(PointerOutEvent evt) + { + isOverridingCursor = false; + } + + + void LinkTagOnPointerDown(PointerDownEvent pde) + { + var pos = pde.localPosition - new Vector3(m_TextElement.contentRect.min.x, m_TextElement.contentRect.min.y); + // Convert UITK pos to ATG pos + var (type, link) = m_TextElement.uitkTextHandle.ATGFindIntersectingLink(pos); + if (link == null || type != TextCore.RichTextTagParser.TagType.Link) + return; + + using (var e = Experimental.PointerDownLinkTagEvent.GetPooled(pde, link, "test" /* TODO we have no way of gettting the hilighted text*/ )) + { + e.elementTarget = m_TextElement; + m_TextElement.SendEvent(e); + } + } + + void LinkTagOnPointerUp(PointerUpEvent pue) + { + var pos = pue.localPosition - new Vector3(m_TextElement.contentRect.min.x, m_TextElement.contentRect.min.y); + var (type, link) = m_TextElement.uitkTextHandle.ATGFindIntersectingLink(pos); + if (link == null || type != TextCore.RichTextTagParser.TagType.Link) + return; + + using (var e = Experimental.PointerUpLinkTagEvent.GetPooled(pue, link, "test" /* TODO we have no way of gettting the hilighted text*/ )) + { + e.elementTarget = m_TextElement; + m_TextElement.SendEvent(e); + } + } + + // Used in automated test + internal int currentLinkIDHash = -1; + + void LinkTagOnPointerMove(PointerMoveEvent pme) + { + var pos = pme.localPosition - new Vector3(m_TextElement.contentRect.min.x, m_TextElement.contentRect.min.y); + // Convert UITK pos to ATG pos + var (type, link) = m_TextElement.uitkTextHandle.ATGFindIntersectingLink(pos); + + if (link != null && type == TextCore.RichTextTagParser.TagType.Link) + { + // PointerOver + if (currentLinkIDHash == -1) + { + currentLinkIDHash = 0; // Placeholder for link.hashCode + using (var e = Experimental.PointerOverLinkTagEvent.GetPooled(pme, link, "test" /* TODO we have no way of gettting the hilighted text*/ )) + { + e.elementTarget = m_TextElement; + m_TextElement.SendEvent(e); + } + + return; + } + + // PointerMove + if (currentLinkIDHash == 0) // Placeholder for link.hashCode + { + using (var e = Experimental.PointerMoveLinkTagEvent.GetPooled(pme, link, "test" /* TODO we have no way of gettting the hilighted text*/ )) + { + e.elementTarget = m_TextElement; + m_TextElement.SendEvent(e); + } + + return; + } + } + + // PointerOut + if (currentLinkIDHash != -1) + { + currentLinkIDHash = -1; + using (var e = Experimental.PointerOutLinkTagEvent.GetPooled(pme, string.Empty)) + { + e.elementTarget = m_TextElement; + m_TextElement.SendEvent(e); + } + } + } + + void LinkTagOnPointerOut(PointerOutEvent poe) + { + if (currentLinkIDHash != -1) + { + using (var e = Experimental.PointerOutLinkTagEvent.GetPooled(poe, string.Empty)) + { + e.elementTarget = m_TextElement; + m_TextElement.SendEvent(e); + } + + currentLinkIDHash = -1; + } + } + + internal void RegisterLinkTagCallbacks() + { + if (m_TextElement?.panel == null) + return; + + AllocateLinkCallbacks(); + m_TextElement.RegisterCallback(m_LinkTagOnPointerDown, TrickleDown.TrickleDown); + m_TextElement.RegisterCallback(m_LinkTagOnPointerUp, TrickleDown.TrickleDown); + m_TextElement.RegisterCallback(m_LinkTagOnPointerMove, TrickleDown.TrickleDown); + m_TextElement.RegisterCallback(m_LinkTagOnPointerOut, TrickleDown.TrickleDown); + } + + internal void UnRegisterLinkTagCallbacks() + { + if (HasAllocatedLinkCallbacks()) + { + m_TextElement.UnregisterCallback(m_LinkTagOnPointerDown, TrickleDown.TrickleDown); + m_TextElement.UnregisterCallback(m_LinkTagOnPointerUp, TrickleDown.TrickleDown); + m_TextElement.UnregisterCallback(m_LinkTagOnPointerMove, TrickleDown.TrickleDown); + m_TextElement.UnregisterCallback(m_LinkTagOnPointerOut, TrickleDown.TrickleDown); + } + } + + internal void RegisterHyperlinkCallbacks() + { + if (m_TextElement?.panel == null) + return; + + AllocateHyperlinkCallbacks(); + m_TextElement.RegisterCallback(m_HyperlinkOnPointerUp, TrickleDown.TrickleDown); + + // Switching the cursor to the Link cursor has been disable at runtime until OS cursor support is available at runtime. + if (m_TextElement.panel.contextType == ContextType.Editor) + { + m_TextElement.RegisterCallback(m_HyperlinkOnPointerMove, TrickleDown.TrickleDown); + m_TextElement.RegisterCallback(m_HyperlinkOnPointerOver, TrickleDown.TrickleDown); + m_TextElement.RegisterCallback(m_HyperlinkOnPointerOut, TrickleDown.TrickleDown); + } + } + + internal void UnRegisterHyperlinkCallbacks() + { + if (m_TextElement?.panel == null) + return; + + if (HasAllocatedHyperlinkCallbacks()) + { + m_TextElement.UnregisterCallback(m_HyperlinkOnPointerUp, TrickleDown.TrickleDown); + if (m_TextElement.panel.contextType == ContextType.Editor) + { + m_TextElement.UnregisterCallback(m_HyperlinkOnPointerMove, TrickleDown.TrickleDown); + m_TextElement.UnregisterCallback(m_HyperlinkOnPointerOver, TrickleDown.TrickleDown); + m_TextElement.UnregisterCallback(m_HyperlinkOnPointerOut, TrickleDown.TrickleDown); + } + } + } + } +} diff --git a/Modules/UIElements/Core/Text/ATGTextHandle.cs b/Modules/UIElements/Core/Text/ATGTextHandle.cs index 336b8bda93..e30f4ef63a 100644 --- a/Modules/UIElements/Core/Text/ATGTextHandle.cs +++ b/Modules/UIElements/Core/Text/ATGTextHandle.cs @@ -5,12 +5,21 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using UnityEngine.TextCore; using UnityEngine.TextCore.Text; +using static UnityEngine.TextCore.RichTextTagParser; namespace UnityEngine.UIElements { internal partial class UITKTextHandle { + ATGTextEventHandler m_ATGTextEventHandler; + // int LinkID: The identifier for the link. + // TagType: Specifies the type of tag (either Hyperlink or Link). + // string Attribute: For Hyperlink, this is the 'href' attribute; for Link, it's the associated attribute. + List<(int, TagType, string)> m_Links;//Not clearing links would result in a leak of strings and enum, but no class, so no consideration for clearing the list at the moment + private List<(int, TagType, string)> Links => m_Links ??= new(); + public void ComputeNativeTextSize(in RenderedText textToMeasure, float width, float height) { ConvertUssToNativeTextGenerationSettings(); @@ -18,7 +27,18 @@ public void ComputeNativeTextSize(in RenderedText textToMeasure, float width, fl nativeSettings.screenWidth = float.IsNaN(width) ? Int32.MaxValue : (int)(width * 64.0f); nativeSettings.screenHeight = float.IsNaN(height) ? Int32.MaxValue : (int)(height * 64.0f); - preferredSize = TextLib.MeasureText(nativeSettings, textGenerationInfo); + if (m_TextElement.enableRichText && !String.IsNullOrEmpty(nativeSettings.text)) + { + Color hyperlinkColor = (m_TextElement.panel as Panel)?.HyperlinkColor ?? Color.blue; + RichTextTagParser.CreateTextGenerationSettingsArray(ref nativeSettings, Links, hyperlinkColor); + } + else + nativeSettings.textSpans = null; + + // Passing a zero pointer instead of the cached textGenerationInfo because it is possible that calling the measure will not + // change the size, there will therefore be no full layout of the glyph and the textGenerationInfo will not have the final + // glyph position populated and it breaks TextLib.FindIntersectingLink + preferredSize = TextLib.MeasureText(nativeSettings, IntPtr.Zero); } public NativeTextInfo UpdateNative(ref bool success) @@ -30,7 +50,87 @@ public NativeTextInfo UpdateNative(ref bool success) } success = true; - return TextLib.GenerateText(nativeSettings, textGenerationInfo); + + if (m_TextElement.enableRichText && !String.IsNullOrEmpty(nativeSettings.text)) + { + Color hyperlinkColor = (m_TextElement.panel as Panel)?.HyperlinkColor ?? Color.blue; + RichTextTagParser.CreateTextGenerationSettingsArray(ref nativeSettings, Links, hyperlinkColor); + } + else + nativeSettings.textSpans = null; + + if ((nativeSettings.hasLink) && textGenerationInfo == IntPtr.Zero) + { + textGenerationInfo = TextGenerationInfo.Create(); + m_ATGTextEventHandler ??= new ATGTextEventHandler(m_TextElement); + } + var textInfo = TextLib.GenerateText(nativeSettings, textGenerationInfo); + UpdateATGTextEventHandler(nativeSettings); + + return textInfo; + } + + private (bool, bool) hasLinkAndHyperlink() + { + bool hasLink = false; + bool hasHyperlink = false; + + if (m_Links != null) // Using member variable to not allocate if unused + { + foreach (var (_, type, _) in Links) + { + hasLink = hasLink || type == TagType.Link; + hasHyperlink = hasHyperlink || type == TagType.Hyperlink; + + if (hasLink && hasHyperlink) + break; + } + } + return (hasLink, hasHyperlink); + } + + internal (TagType, string) ATGFindIntersectingLink(Vector2 point) + { + + //This should probably be public, but it would require exposing TagType + Debug.Assert(useAdvancedText); + if (textGenerationInfo == IntPtr.Zero) + { + Debug.LogError("TextGenerationInfo pointer is null."); + return(TagType.Unknown, null); + } + + int id = TextLib.FindIntersectingLink(point, textGenerationInfo); + + if (id == -1) + return (TagType.Unknown, null); + + return (m_Links[id].Item2, m_Links[id].Item3); + } + + private void UpdateATGTextEventHandler(NativeTextGenerationSettings setting) + { + if (m_ATGTextEventHandler == null) + return; + + var (hasLink, hasHyperlink) = hasLinkAndHyperlink(); + if (hasLink) + { + m_ATGTextEventHandler.RegisterLinkTagCallbacks(); + } + else + { + m_ATGTextEventHandler.UnRegisterLinkTagCallbacks(); + } + + if (hasHyperlink) + { + m_ATGTextEventHandler.RegisterHyperlinkCallbacks(); + } + else + { + m_ATGTextEventHandler.UnRegisterHyperlinkCallbacks(); + } } internal bool ConvertUssToNativeTextGenerationSettings() @@ -89,7 +189,12 @@ internal bool ConvertUssToNativeTextGenerationSettings() } } nativeSettings.globalFontAssetFallbacks = globalFontAssetFallbacks.ToArray(); - nativeSettings.fontStyle = TextGeneratorUtilities.LegacyStyleToNewStyle(style.unityFontStyleAndWeight); + + //Bold is not part of the font style in css and in text native, but it is in textCore/Uitk + var sourcefontStyle = TextGeneratorUtilities.LegacyStyleToNewStyle(style.unityFontStyleAndWeight); + nativeSettings.fontStyle = sourcefontStyle & ~FontStyles.Bold; + //Backward compatibility with text core + nativeSettings.fontWeight = (sourcefontStyle & FontStyles.Bold) == FontStyles.Bold ? TextFontWeight.Bold : TextFontWeight.Regular; // The screenRect in TextCore is not properly implemented with regards to the offset part, so zero it out for now and we will add it ourselves later var size = m_TextElement.contentRect.size; diff --git a/Modules/UIElements/Core/Text/UITKTextHandle.cs b/Modules/UIElements/Core/Text/UITKTextHandle.cs index abd9926e9f..5567784dd5 100644 --- a/Modules/UIElements/Core/Text/UITKTextHandle.cs +++ b/Modules/UIElements/Core/Text/UITKTextHandle.cs @@ -7,6 +7,8 @@ using System.Runtime.CompilerServices; using Unity.Jobs.LowLevel.Unsafe; using UnityEngine.TextCore.Text; +using UnityEngine.UIElements.Internal; +using UnityEngine.UIElements.UIR; namespace UnityEngine.UIElements { diff --git a/Modules/UIElementsEditor/EditorPanel.cs b/Modules/UIElementsEditor/EditorPanel.cs index e3e86fc327..3e672f0b26 100644 --- a/Modules/UIElementsEditor/EditorPanel.cs +++ b/Modules/UIElementsEditor/EditorPanel.cs @@ -59,7 +59,7 @@ public static void InitEditorUpdater(BaseVisualElementPanel panel, VisualTreeUpd EditorWindow editorWindow => GetBackingScaleFactor(editorWindow?.m_Parent), IEditorWindowModel ewm => GetBackingScaleFactor(ewm.window), _ => null, - } ; + }; } private void CheckPanelScaling() @@ -72,11 +72,11 @@ private void CheckPanelScaling() var windowScaling = GetBackingScaleFactor(); if (windowScaling == null || windowScaling.Value == -1) { - Debug.Assert(windowScaling != null, "got -1 here!!" ); - // if we have -1, we were able to get to a GuiView, but the native call returned -1 because there is no containerWindow - // if the windowScaling == null we were simply not able to get to a GuiView - // in both cases, we want to update the scaling like the old behavior. - pixelsPerPoint = GUIUtility.pixelsPerPoint; + Debug.Assert(windowScaling != null, "got -1 here!!"); + // if we have -1, we were able to get to a GuiView, but the native call returned -1 because there is no containerWindow + // if the windowScaling == null we were simply not able to get to a GuiView + // in both cases, we want to update the scaling like the old behavior. + pixelsPerPoint = GUIUtility.pixelsPerPoint; } else { @@ -100,5 +100,15 @@ public override void Render() CheckPanelScaling(); base.Render(); } + + internal override Color HyperlinkColor + { + get + { + ColorUtility.TryParseHtmlString(EditorGUIUtility.GetHyperlinkColorForSkin(), out Color color); + return color; + } + } + } } diff --git a/Projects/CSharp/UnityEditor.csproj b/Projects/CSharp/UnityEditor.csproj index 4d2d15eac2..0b9f3589b3 100644 --- a/Projects/CSharp/UnityEditor.csproj +++ b/Projects/CSharp/UnityEditor.csproj @@ -3927,12 +3927,54 @@ Editor\Mono\UnityConnect\CoppaCompliance.cs + + Editor\Mono\UnityConnect\Network\UnityConnectWebRequestException.cs + + + Editor\Mono\UnityConnect\Network\UnityConnectWebRequestUtils.cs + + + Editor\Mono\UnityConnect\ServiceToken\Caching\GenesisAndServiceTokenCaching.cs + + + Editor\Mono\UnityConnect\ServiceToken\Caching\IGenesisAndServiceTokenCaching.cs + + + Editor\Mono\UnityConnect\ServiceToken\Caching\JsonWebToken.cs + + + Editor\Mono\UnityConnect\ServiceToken\Caching\Tokens.cs + + + Editor\Mono\UnityConnect\ServiceToken\ConfigurationProvider\CloudEnvironmentConfigProvider.cs + + + Editor\Mono\UnityConnect\ServiceToken\ConfigurationProvider\ICloudEnvironmentConfigProvider.cs + + + Editor\Mono\UnityConnect\ServiceToken\ServiceToken.cs + + + Editor\Mono\UnityConnect\ServiceToken\TokenExchange\ITokenExchange.cs + + + Editor\Mono\UnityConnect\ServiceToken\TokenExchange\Model\TokenExchangeRequest.cs + + + Editor\Mono\UnityConnect\ServiceToken\TokenExchange\Model\TokenExchangeResponse.cs + + + Editor\Mono\UnityConnect\ServiceToken\TokenExchange\TokenExchange.cs + Editor\Mono\UnityConnect\Services\EditorProjectAccess.bindings.cs Editor\Mono\UnityConnect\UnityConnect.bindings.cs + + Editor\Mono\UnityConnect\Utils\AsyncUtils.cs + Editor\Mono\UnityStats.bindings.cs @@ -4434,6 +4476,9 @@ Modules\BuildPipeline\Editor\Ucbp\BuildPipelineContext.bindings.cs + + Modules\BuildProfileEditor\ActiveBuildProfilerListener.cs + Modules\BuildProfileEditor\AssemblyInfo.cs @@ -9378,9 +9423,6 @@ Modules\UnityConnectEditor\Common\UIElementsNotificationSubscriber.cs - - Modules\UnityConnectEditor\Common\UnityConnectWebRequestException.cs - Modules\UnityConnectEditor\ProjectSettings\AdsProjectSettings.cs diff --git a/Projects/CSharp/UnityEngine.csproj b/Projects/CSharp/UnityEngine.csproj index 9bb863c417..7ee1aadd59 100644 --- a/Projects/CSharp/UnityEngine.csproj +++ b/Projects/CSharp/UnityEngine.csproj @@ -1452,6 +1452,9 @@ Modules\TextCoreTextEngine\Managed\TextGenerator\NativeTextGenerationSettings.bindings.cs + + Modules\TextCoreTextEngine\Managed\TextGenerator\RichTextTagParser.cs + Modules\TextCoreTextEngine\Managed\TextGenerator\TextGenerationSettings.cs @@ -2844,6 +2847,9 @@ Modules\UIElements\Core\TextShadow.cs + + Modules\UIElements\Core\Text\ATGTextEventHandler.cs + Modules\UIElements\Core\Text\ATGTextHandle.cs diff --git a/README.md b/README.md index 7a583118e3..7ef41e1fbd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## Unity 6000.0.21f1 C# reference source code +## Unity 6000.0.22f1 C# reference source code The C# part of the Unity engine and editor source code. May be used for reference purposes only. diff --git a/Runtime/Export/Graphics/GraphicsFormatUtility.bindings.cs b/Runtime/Export/Graphics/GraphicsFormatUtility.bindings.cs index 40e27c1c30..60c1661c11 100644 --- a/Runtime/Export/Graphics/GraphicsFormatUtility.bindings.cs +++ b/Runtime/Export/Graphics/GraphicsFormatUtility.bindings.cs @@ -56,9 +56,9 @@ public static GraphicsFormat GetGraphicsFormat(RenderTextureFormat format, Rende [FreeFunction(IsThreadSafe = true)] extern private static GraphicsFormat GetDepthStencilFormatFromBitsLegacy_Native(int minimumDepthBits); - internal static GraphicsFormat GetDepthStencilFormat(int minimumDepthBits) + public static GraphicsFormat GetDepthStencilFormat(int depthBits) { - return GetDepthStencilFormatFromBitsLegacy_Native(minimumDepthBits); + return GetDepthStencilFormatFromBitsLegacy_Native(depthBits); } [FreeFunction(IsThreadSafe = true)] diff --git a/Runtime/Export/Graphics/Texture.cs b/Runtime/Export/Graphics/Texture.cs index 7a49a83c90..0d1b469319 100644 --- a/Runtime/Export/Graphics/Texture.cs +++ b/Runtime/Export/Graphics/Texture.cs @@ -21,7 +21,7 @@ public struct RenderTextureDescriptor public int volumeDepth { get; set; } public int mipCount { get; set; } - private GraphicsFormat _graphicsFormat;// { get; set; } + private GraphicsFormat _graphicsFormat; public GraphicsFormat graphicsFormat { @@ -30,9 +30,7 @@ public GraphicsFormat graphicsFormat set { _graphicsFormat = value; - SetOrClearRenderTextureCreationFlag(GraphicsFormatUtility.IsSRGBFormat(value), RenderTextureCreationFlags.SRGB); - //To avoid that the order of setting a property changes the end result, we need to update the depthbufferbits because the setter depends on the colorFormat. - depthBufferBits = depthBufferBits; + SetOrClearRenderTextureCreationFlag(GraphicsFormatUtility.IsSRGBFormat(value), RenderTextureCreationFlags.SRGB); } } @@ -55,16 +53,10 @@ public RenderTextureFormat colorFormat } set { - if (value == RenderTextureFormat.Shadowmap) - { - shadowSamplingMode = ShadowSamplingMode.CompareDepths; - // 'shadowSamplingMode' must be set immediately as we otherwise are unable to track the fact - // that a Shadowmap was requested. (+ see comment below regarding setting 'graphicsFormat' as well) - } - // Update 'graphicsFormat' last because it also updates 'depthBufferBits', which relies on the 'colorFormat', - // which itself relies on the 'shadowSamplingMode' to make the distinction between RTF.Depth/RTF.Shadowmap. + shadowSamplingMode = RenderTexture.GetShadowSamplingModeForFormat(value); GraphicsFormat requestedFormat = GraphicsFormatUtility.GetGraphicsFormat(value, sRGB); graphicsFormat = SystemInfo.GetCompatibleFormat(requestedFormat, GraphicsFormatUsage.Render); + depthStencilFormat = RenderTexture.GetDepthStencilFormatLegacy(depthBufferBits, shadowSamplingMode); } } @@ -83,10 +75,12 @@ public int depthBufferBits { get { return GraphicsFormatUtility.GetDepthBits(depthStencilFormat); } //Ideally we deprecate the setter but keeping it for now because its a very commonly used api - //It is very bad practice to use the colorFormat property here because that makes the result depend on the order of setting the properties + //It is very bad practice to use the shadowSamplingMode property here because that makes the result depend on the order of setting the properties //However, it's the best what we can do to make sure this is functionally correct. - //We now need to set depthBufferBits after we set graphicsFormat, see that property. - set { depthStencilFormat = RenderTexture.GetDepthStencilFormatLegacy(value, colorFormat, true); } + //depthBufferBits and colorFormat are legacy APIs that can be used togther in any order to set a combination of the (modern) fields graphicsFormat, dephtStencilFormat and shadowSamplingMode. + //The use of these legacy APIs should not be combined with setting the modern fields directly, the order can change the results. + //There should be no "magic" when setting the modern fields, the desc will contain what the users sets, even if the combination is not valid (ie a depthStencilFormat with stencil and shadowSamplingMode CompareDepths). + set { depthStencilFormat = RenderTexture.GetDepthStencilFormatLegacy(value, shadowSamplingMode); } } public Rendering.TextureDimension dimension { get; set; } diff --git a/Runtime/Export/iOS/iOSDevice.cs b/Runtime/Export/iOS/iOSDevice.cs index be2d3a5394..718d949d7f 100644 --- a/Runtime/Export/iOS/iOSDevice.cs +++ b/Runtime/Export/iOS/iOSDevice.cs @@ -93,6 +93,10 @@ public enum DeviceGeneration iPhone15Plus = 80, iPhone15Pro = 81, iPhone15ProMax = 82, + iPhone16 = 83, + iPhone16Plus = 84, + iPhone16Pro = 85, + iPhone16ProMax = 86, iPhoneUnknown = 10001, iPadUnknown = 10002,