diff --git a/Editor/UX/Controller/UXBindingEditor.cs b/Editor/UX/Controller/UXBindingEditor.cs index 6bf74a1..a0b4d7c 100644 --- a/Editor/UX/Controller/UXBindingEditor.cs +++ b/Editor/UX/Controller/UXBindingEditor.cs @@ -8,27 +8,138 @@ namespace UnityEngine.UI [CustomEditor(typeof(UXBinding))] public sealed class UXBindingEditor : UnityEditor.Editor { + private readonly struct AddRuleOption + { + public readonly string ControllerId; + public readonly string ControllerName; + public readonly UXBindingProperty Property; + public readonly string PropertyName; + + public AddRuleOption(string controllerId, string controllerName, UXBindingProperty property, string propertyName) + { + ControllerId = controllerId; + ControllerName = controllerName; + Property = property; + PropertyName = propertyName; + } + } + + private sealed class AddRulePopup : PopupWindowContent + { + private readonly UXBindingEditor _editor; + private readonly UXBinding _binding; + private readonly List _options; + private readonly bool _showControllerName; + private string _search = string.Empty; + private Vector2 _scroll; + + public AddRulePopup(UXBindingEditor editor, UXBinding binding, List options, bool showControllerName) + { + _editor = editor; + _binding = binding; + _options = new List(options); + _showControllerName = showControllerName; + } + + public override Vector2 GetWindowSize() + { + return new Vector2(360f, 320f); + } + + public override void OnGUI(Rect rect) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("Add Rule", EditorStyles.boldLabel); + EditorGUILayout.EndVertical(); + + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + _search = GUILayout.TextField(_search, GUI.skin.FindStyle("ToolbarSearchTextField") ?? EditorStyles.toolbarSearchField, GUILayout.ExpandWidth(true)); + if (GUILayout.Button(string.Empty, GUI.skin.FindStyle("ToolbarSearchCancelButton") ?? EditorStyles.toolbarButton, GUILayout.Width(18f))) + { + _search = string.Empty; + GUI.FocusControl(null); + } + EditorGUILayout.EndHorizontal(); + + _scroll = EditorGUILayout.BeginScrollView(_scroll); + for (int i = 0; i < _options.Count; i++) + { + AddRuleOption option = _options[i]; + if (!IsMatch(option, _search)) + { + continue; + } + + string label = _showControllerName ? $"{option.ControllerName} / {option.PropertyName}" : option.PropertyName; + if (GUILayout.Button(label, EditorStyles.miniButton)) + { + _editor.AddEntry(_binding, option.ControllerId, option.Property); + editorWindow.Close(); + } + } + EditorGUILayout.EndScrollView(); + } + + private static bool IsMatch(AddRuleOption option, string search) + { + if (string.IsNullOrEmpty(search)) + { + return true; + } + + return option.ControllerName.IndexOf(search, System.StringComparison.OrdinalIgnoreCase) >= 0 || + option.PropertyName.IndexOf(search, System.StringComparison.OrdinalIgnoreCase) >= 0; + } + } + private SerializedProperty _controllerProp; private SerializedProperty _entriesProp; private readonly Dictionary _foldouts = new Dictionary(); private readonly List _supportedProperties = new List(); + private string[] _controllerNames = System.Array.Empty(); + private string[] _indexNames = System.Array.Empty(); + private GUIStyle _pillOn; + private GUIStyle _pillOff; + private readonly List _addRuleOptions = new List(); + private GUIContent _addRuleContent; + private GUIContent _autoBindContent; + private GUIContent _captureContent; + private GUIContent _resetContent; + private GUIContent _expandAllContent; + private GUIContent _collapseAllContent; + private GUIContent _upContent; + private GUIContent _downContent; + private GUIContent _deleteContent; private void OnEnable() { _controllerProp = serializedObject.FindProperty("_controller"); _entriesProp = serializedObject.FindProperty("_entries"); + InitializeContents(); + } + + private void InitializeContents() + { + _addRuleContent = EditorGUIUtility.IconContent("Toolbar Plus", "Add binding rule"); + _autoBindContent = EditorGUIUtility.IconContent("d_Prefab Icon", "Auto bind parent UXController"); + _captureContent = EditorGUIUtility.IconContent("d_SaveAs", "Capture defaults"); + _resetContent = EditorGUIUtility.IconContent("d_Refresh", "Reset to defaults"); + _expandAllContent = EditorGUIUtility.IconContent("d_scrollup", "Expand all rules"); + _collapseAllContent = EditorGUIUtility.IconContent("d_scrolldown", "Collapse all rules"); + _upContent = EditorGUIUtility.IconContent("d_scrollup", "Move up"); + _downContent = EditorGUIUtility.IconContent("d_scrolldown", "Move down"); + _deleteContent = EditorGUIUtility.IconContent("TreeEditor.Trash", "Delete rule"); } public override void OnInspectorGUI() { serializedObject.Update(); + EnsureStyles(); var binding = (UXBinding)target; DrawHeader(binding); EditorGUILayout.Space(6f); - DrawControllerField(binding); - EditorGUILayout.Space(6f); DrawEntries(binding); serializedObject.ApplyModifiedProperties(); @@ -37,39 +148,11 @@ namespace UnityEngine.UI private void DrawHeader(UXBinding binding) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField("UX Binding", EditorStyles.boldLabel); - EditorGUILayout.LabelField($"Target: {binding.gameObject.name}", EditorStyles.miniLabel); - EditorGUILayout.LabelField($"Rules: {_entriesProp.arraySize}", EditorStyles.miniLabel); - EditorGUILayout.BeginHorizontal(); - if (GUILayout.Button("Add Rule")) - { - AddEntry(binding); - } - - if (GUILayout.Button("Capture Defaults")) - { - binding.CaptureDefaults(); - EditorUtility.SetDirty(binding); - } - - if (GUILayout.Button("Reset To Defaults")) - { - binding.ResetToDefaults(); - EditorUtility.SetDirty(binding); - SceneView.RepaintAll(); - } - EditorGUILayout.EndHorizontal(); - EditorGUILayout.EndVertical(); - } - - private void DrawControllerField(UXBinding binding) - { - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField("Controller", EditorStyles.boldLabel); + EditorGUILayout.LabelField("UX Binding", EditorStyles.boldLabel, GUILayout.Width(82f)); EditorGUI.BeginChangeCheck(); - UXController newController = (UXController)EditorGUILayout.ObjectField("Reference", binding.Controller, typeof(UXController), true); + UXController newController = (UXController)EditorGUILayout.ObjectField(binding.Controller, typeof(UXController), true); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(binding, "Change UX Binding Controller"); @@ -78,13 +161,40 @@ namespace UnityEngine.UI EditorUtility.SetDirty(binding); } + if (binding.Controller != null) + { + EditorGUILayout.LabelField(binding.Controller.ControllerCount.ToString(), EditorStyles.miniLabel, GUILayout.Width(18f)); + } + + GUILayout.FlexibleSpace(); + + if (GUILayout.Button(_addRuleContent, EditorStyles.miniButtonLeft, GUILayout.Width(28f))) + { + ShowAddRuleMenu(binding, GUILayoutUtility.GetLastRect()); + } + + if (GUILayout.Button(_autoBindContent, EditorStyles.miniButtonMid, GUILayout.Width(28f))) + { + AutoBindController(binding); + } + + if (GUILayout.Button(_captureContent, EditorStyles.miniButtonMid, GUILayout.Width(28f))) + { + binding.CaptureDefaults(); + EditorUtility.SetDirty(binding); + } + + if (GUILayout.Button(_resetContent, EditorStyles.miniButtonRight, GUILayout.Width(28f))) + { + binding.ResetToDefaults(); + EditorUtility.SetDirty(binding); + SceneView.RepaintAll(); + } + EditorGUILayout.EndHorizontal(); + if (binding.Controller == null) { - EditorGUILayout.HelpBox("Assign a UXController on this object or one of its parents.", MessageType.Warning); - } - else - { - EditorGUILayout.LabelField($"Bound To: {binding.Controller.name}", EditorStyles.miniLabel); + EditorGUILayout.HelpBox("Assign a UXController or use Auto Bind.", MessageType.Warning); } EditorGUILayout.EndVertical(); @@ -112,27 +222,39 @@ namespace UnityEngine.UI SerializedProperty entryProp = _entriesProp.GetArrayElementAtIndex(index); SerializedProperty controllerIdProp = entryProp.FindPropertyRelative("_controllerId"); SerializedProperty controllerIndexProp = entryProp.FindPropertyRelative("_controllerIndex"); + SerializedProperty controllerIndexMaskProp = entryProp.FindPropertyRelative("_controllerIndexMask"); SerializedProperty propertyProp = entryProp.FindPropertyRelative("_property"); SerializedProperty valueProp = entryProp.FindPropertyRelative("_value"); + SerializedProperty indexedValuesProp = entryProp.FindPropertyRelative("_indexedValues"); SerializedProperty fallbackModeProp = entryProp.FindPropertyRelative("_fallbackMode"); SerializedProperty fallbackValueProp = entryProp.FindPropertyRelative("_fallbackValue"); UXBindingProperty property = (UXBindingProperty)propertyProp.enumValueIndex; bool expanded = _foldouts.ContainsKey(index) && _foldouts[index]; string label = UXBindingPropertyUtility.GetMetadata(property).DisplayName; + bool propertySupported = _supportedProperties.Contains(property); + bool controllerResolved = TryGetControllerName(binding.Controller, controllerIdProp.stringValue, out string controllerName); EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); expanded = EditorGUILayout.Foldout(expanded, $"[{index}] {label}", true); GUILayout.FlexibleSpace(); - if (GUILayout.Button("Preview", EditorStyles.miniButtonLeft, GUILayout.Width(60f))) + EditorGUI.BeginDisabledGroup(index == 0); + if (GUILayout.Button(_upContent, EditorStyles.miniButtonLeft, GUILayout.Width(24f))) { - binding.PreviewEntry(index); - SceneView.RepaintAll(); + MoveEntry(index, index - 1); } + EditorGUI.EndDisabledGroup(); - if (GUILayout.Button("X", EditorStyles.miniButtonRight, GUILayout.Width(24f))) + EditorGUI.BeginDisabledGroup(index >= _entriesProp.arraySize - 1); + if (GUILayout.Button(_downContent, EditorStyles.miniButtonMid, GUILayout.Width(24f))) + { + MoveEntry(index, index + 1); + } + EditorGUI.EndDisabledGroup(); + + if (GUILayout.Button(_deleteContent, EditorStyles.miniButtonRight, GUILayout.Width(24f))) { DeleteEntry(binding, index); } @@ -141,39 +263,75 @@ namespace UnityEngine.UI if (expanded) { - DrawControllerSelector(binding, controllerIdProp, controllerIndexProp); - DrawPropertySelector(entryProp, binding.gameObject, propertyProp); - - property = (UXBindingProperty)propertyProp.enumValueIndex; UXBindingPropertyMetadata metadata = UXBindingPropertyUtility.GetMetadata(property); + bool indexChanged = DrawControllerSelector(binding, controllerIdProp, controllerIndexProp, controllerIndexMaskProp, property == UXBindingProperty.GameObjectActive); - EditorGUILayout.Space(2f); - EditorGUILayout.LabelField("Value", EditorStyles.boldLabel); - DrawValueField(valueProp, metadata, "Matched Value"); - - EditorGUILayout.BeginHorizontal(); - if (GUILayout.Button("Use Current")) + if (property == UXBindingProperty.GameObjectActive) { - binding.CaptureEntryValue(index); - EditorUtility.SetDirty(binding); - } - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(2f); - EditorGUILayout.PropertyField(fallbackModeProp, new GUIContent("Fallback")); - UXBindingFallbackMode fallbackMode = (UXBindingFallbackMode)fallbackModeProp.enumValueIndex; - if (fallbackMode == UXBindingFallbackMode.UseCustomValue) - { - DrawValueField(fallbackValueProp, metadata, "Fallback Value"); - - if (GUILayout.Button("Use Current As Fallback")) + DrawGameObjectActiveHint(controllerIndexMaskProp.intValue); + ForceGameObjectActiveValues(valueProp, fallbackModeProp, fallbackValueProp); + if (indexChanged) { - binding.CaptureEntryFallbackValue(index); + serializedObject.ApplyModifiedProperties(); + binding.ApplyEntryValue(index, GetFirstSelectedIndex(controllerIndexMaskProp.intValue)); EditorUtility.SetDirty(binding); + SceneView.RepaintAll(); + } + } + else + { + int selectedIndex = GetFirstSelectedIndex(controllerIndexMaskProp.intValue); + if (indexChanged) + { + serializedObject.ApplyModifiedProperties(); + binding.ApplyEntryValue(index, selectedIndex); + EditorUtility.SetDirty(binding); + SceneView.RepaintAll(); + } + + EditorGUILayout.Space(2f); + EditorGUILayout.LabelField("Value", EditorStyles.boldLabel); + SerializedProperty selectedValueProp = GetIndexedValueProperty(indexedValuesProp, valueProp, selectedIndex); + + EditorGUI.BeginChangeCheck(); + DrawValueField(selectedValueProp, metadata, $"Index {selectedIndex} Value"); + if (EditorGUI.EndChangeCheck()) + { + serializedObject.ApplyModifiedProperties(); + binding.ApplyEntryValue(index, selectedIndex); + EditorUtility.SetDirty(binding); + SceneView.RepaintAll(); + } + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Use Current")) + { + binding.CaptureEntryValue(index, selectedIndex); + EditorUtility.SetDirty(binding); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(2f); + EditorGUILayout.PropertyField(fallbackModeProp, new GUIContent("Fallback")); + UXBindingFallbackMode fallbackMode = (UXBindingFallbackMode)fallbackModeProp.enumValueIndex; + if (fallbackMode == UXBindingFallbackMode.UseCustomValue) + { + DrawValueField(fallbackValueProp, metadata, "Fallback Value"); + + if (GUILayout.Button("Use Current As Fallback")) + { + binding.CaptureEntryFallbackValue(index); + EditorUtility.SetDirty(binding); + } } } - if (!_supportedProperties.Contains(property)) + if (!controllerResolved) + { + EditorGUILayout.HelpBox("Controller reference is missing or points to a deleted controller definition.", MessageType.Error); + } + + if (!propertySupported) { EditorGUILayout.HelpBox("This property is not supported by the components on the current GameObject.", MessageType.Error); } @@ -182,22 +340,25 @@ namespace UnityEngine.UI EditorGUILayout.EndVertical(); } - private void DrawControllerSelector(UXBinding binding, SerializedProperty controllerIdProp, SerializedProperty controllerIndexProp) + private bool DrawControllerSelector(UXBinding binding, SerializedProperty controllerIdProp, SerializedProperty controllerIndexProp, SerializedProperty controllerIndexMaskProp, bool multiSelect) { UXController controller = binding.Controller; if (controller == null || controller.Controllers.Count == 0) { EditorGUILayout.HelpBox("Create a controller definition first.", MessageType.Info); - return; + return false; } - string[] names = new string[controller.Controllers.Count]; + bool changed = false; + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Controller", EditorStyles.miniLabel, GUILayout.Width(64f)); + EnsureStringArray(ref _controllerNames, controller.Controllers.Count); int selectedController = 0; for (int i = 0; i < controller.Controllers.Count; i++) { UXController.ControllerDefinition definition = controller.Controllers[i]; - names[i] = definition.Name; + _controllerNames[i] = definition.Name; if (definition.Id == controllerIdProp.stringValue) { selectedController = i; @@ -205,59 +366,23 @@ namespace UnityEngine.UI } EditorGUI.BeginChangeCheck(); - selectedController = EditorGUILayout.Popup("Controller", selectedController, names); + selectedController = EditorGUILayout.Popup(selectedController, _controllerNames, GUILayout.MinWidth(90f)); if (EditorGUI.EndChangeCheck()) { controllerIdProp.stringValue = controller.Controllers[selectedController].Id; controllerIndexProp.intValue = 0; + controllerIndexMaskProp.intValue = 1; + changed = true; } UXController.ControllerDefinition selectedDefinition = controller.Controllers[selectedController]; int maxIndex = Mathf.Max(1, selectedDefinition.Length); controllerIndexProp.intValue = Mathf.Clamp(controllerIndexProp.intValue, 0, maxIndex - 1); + controllerIndexMaskProp.intValue = ClampMask(controllerIndexMaskProp.intValue, maxIndex, controllerIndexProp.intValue); - string[] indexNames = new string[maxIndex]; - for (int i = 0; i < maxIndex; i++) - { - indexNames[i] = i.ToString(); - } - - controllerIndexProp.intValue = EditorGUILayout.Popup("Index", controllerIndexProp.intValue, indexNames); - } - - private void DrawPropertySelector(SerializedProperty entryProp, GameObject targetObject, SerializedProperty propertyProp) - { - var options = new List(_supportedProperties); - UXBindingProperty current = (UXBindingProperty)propertyProp.enumValueIndex; - if (!options.Contains(current)) - { - options.Add(current); - } - - string[] displayNames = new string[options.Count]; - int selectedIndex = 0; - for (int i = 0; i < options.Count; i++) - { - UXBindingProperty option = options[i]; - UXBindingPropertyMetadata metadata = UXBindingPropertyUtility.GetMetadata(option); - bool supported = UXBindingPropertyUtility.IsSupported(targetObject, option); - displayNames[i] = supported ? metadata.DisplayName : $"{metadata.DisplayName} (Unsupported)"; - if (option == current) - { - selectedIndex = i; - } - } - - EditorGUI.BeginChangeCheck(); - selectedIndex = EditorGUILayout.Popup("Property", selectedIndex, displayNames); - if (EditorGUI.EndChangeCheck()) - { - propertyProp.enumValueIndex = (int)options[selectedIndex]; - entryProp.FindPropertyRelative("_hasCapturedDefault").boolValue = false; - entryProp.FindPropertyRelative("_capturedProperty").enumValueIndex = propertyProp.enumValueIndex; - ResetValue(entryProp.FindPropertyRelative("_capturedDefault")); - ApplyDefaultFallbackForProperty(entryProp, (UXBindingProperty)propertyProp.enumValueIndex); - } + changed |= DrawIndexMask(controllerIndexMaskProp, controllerIndexProp, maxIndex, multiSelect); + EditorGUILayout.EndHorizontal(); + return changed; } private void DrawValueField(SerializedProperty valueProp, UXBindingPropertyMetadata metadata, string label) @@ -314,7 +439,7 @@ namespace UnityEngine.UI } } - private void AddEntry(UXBinding binding) + private void AddEntry(UXBinding binding, string controllerId, UXBindingProperty property) { int index = _entriesProp.arraySize; _entriesProp.InsertArrayElementAtIndex(index); @@ -322,31 +447,73 @@ namespace UnityEngine.UI SerializedProperty entryProp = _entriesProp.GetArrayElementAtIndex(index); SerializedProperty controllerIdProp = entryProp.FindPropertyRelative("_controllerId"); SerializedProperty controllerIndexProp = entryProp.FindPropertyRelative("_controllerIndex"); + SerializedProperty controllerIndexMaskProp = entryProp.FindPropertyRelative("_controllerIndexMask"); SerializedProperty propertyProp = entryProp.FindPropertyRelative("_property"); SerializedProperty fallbackModeProp = entryProp.FindPropertyRelative("_fallbackMode"); SerializedProperty valueProp = entryProp.FindPropertyRelative("_value"); + SerializedProperty indexedValuesProp = entryProp.FindPropertyRelative("_indexedValues"); SerializedProperty fallbackValueProp = entryProp.FindPropertyRelative("_fallbackValue"); SerializedProperty capturedDefaultProp = entryProp.FindPropertyRelative("_capturedDefault"); SerializedProperty hasCapturedDefaultProp = entryProp.FindPropertyRelative("_hasCapturedDefault"); SerializedProperty capturedPropertyProp = entryProp.FindPropertyRelative("_capturedProperty"); - controllerIdProp.stringValue = string.Empty; + controllerIdProp.stringValue = controllerId; controllerIndexProp.intValue = 0; - propertyProp.enumValueIndex = (int)UXBindingProperty.GameObjectActive; + controllerIndexMaskProp.intValue = 1; + propertyProp.enumValueIndex = (int)property; fallbackModeProp.enumValueIndex = (int)UXBindingFallbackMode.RestoreCapturedDefault; ResetValue(valueProp); + indexedValuesProp.ClearArray(); ResetValue(fallbackValueProp); ResetValue(capturedDefaultProp); hasCapturedDefaultProp.boolValue = false; - capturedPropertyProp.enumValueIndex = (int)UXBindingProperty.GameObjectActive; - ApplyDefaultFallbackForProperty(entryProp, UXBindingProperty.GameObjectActive); - - if (binding.Controller != null && binding.Controller.Controllers.Count > 0) - { - controllerIdProp.stringValue = binding.Controller.Controllers[0].Id; - } + capturedPropertyProp.enumValueIndex = (int)property; + ApplyDefaultFallbackForProperty(entryProp, property); _foldouts[index] = true; + serializedObject.ApplyModifiedProperties(); + EditorUtility.SetDirty(binding); + } + + private void ShowAddRuleMenu(UXBinding binding, Rect activatorRect) + { + if (binding.Controller == null || binding.Controller.Controllers.Count == 0) + { + EditorUtility.DisplayDialog("Add UX Binding Rule", "Assign a UXController before adding rules.", "OK"); + return; + } + + UXBindingPropertyUtility.GetSupportedProperties(binding.gameObject, _supportedProperties); + _addRuleOptions.Clear(); + + for (int controllerIndex = 0; controllerIndex < binding.Controller.Controllers.Count; controllerIndex++) + { + UXController.ControllerDefinition controller = binding.Controller.Controllers[controllerIndex]; + if (controller == null) + { + continue; + } + + for (int propertyIndex = 0; propertyIndex < _supportedProperties.Count; propertyIndex++) + { + UXBindingProperty property = _supportedProperties[propertyIndex]; + if (HasRule(controller.Id, property)) + { + continue; + } + + UXBindingPropertyMetadata metadata = UXBindingPropertyUtility.GetMetadata(property); + _addRuleOptions.Add(new AddRuleOption(controller.Id, controller.Name, property, metadata.DisplayName)); + } + } + + if (_addRuleOptions.Count == 0) + { + EditorUtility.DisplayDialog("Add UX Binding Rule", "All supported states already exist.", "OK"); + return; + } + + PopupWindow.Show(activatorRect, new AddRulePopup(this, binding, _addRuleOptions, binding.Controller.Controllers.Count > 1)); } private static void ResetValue(SerializedProperty valueProp) @@ -367,6 +534,15 @@ namespace UnityEngine.UI return; } + if (!EditorUtility.DisplayDialog( + "Delete UX Binding Rule", + $"Delete binding rule {index}? This cannot be undone outside Unity Undo.", + "Delete", + "Cancel")) + { + return; + } + Undo.RecordObject(binding, "Delete UX Binding Rule"); _entriesProp.DeleteArrayElementAtIndex(index); CleanupFoldouts(index); @@ -375,6 +551,22 @@ namespace UnityEngine.UI GUIUtility.ExitGUI(); } + private void MoveEntry(int fromIndex, int toIndex) + { + if (fromIndex < 0 || toIndex < 0 || fromIndex >= _entriesProp.arraySize || toIndex >= _entriesProp.arraySize) + { + return; + } + + _entriesProp.MoveArrayElement(fromIndex, toIndex); + bool fromExpanded = _foldouts.ContainsKey(fromIndex) && _foldouts[fromIndex]; + bool toExpanded = _foldouts.ContainsKey(toIndex) && _foldouts[toIndex]; + _foldouts[fromIndex] = toExpanded; + _foldouts[toIndex] = fromExpanded; + serializedObject.ApplyModifiedProperties(); + GUIUtility.ExitGUI(); + } + private void CleanupFoldouts(int removedIndex) { _foldouts.Remove(removedIndex); @@ -411,6 +603,230 @@ namespace UnityEngine.UI ResetValue(fallbackValueProp); } } + + private void AutoBindController(UXBinding binding) + { + UXController controller = binding.GetComponentInParent(); + if (controller == null) + { + EditorUtility.DisplayDialog("Auto Bind UX Controller", "No UXController found in parents.", "OK"); + return; + } + + Undo.RecordObject(binding, "Auto Bind UX Controller"); + binding.SetController(controller); + _controllerProp.objectReferenceValue = controller; + serializedObject.ApplyModifiedProperties(); + EditorUtility.SetDirty(binding); + } + + private void EnsureStyles() + { + if (_pillOn != null) + { + return; + } + + _pillOn = new GUIStyle(EditorStyles.miniButton) + { + fontStyle = FontStyle.Bold, + fixedHeight = 18f, + margin = new RectOffset(1, 1, 1, 1), + padding = new RectOffset(2, 2, 1, 1) + }; + _pillOff = new GUIStyle(EditorStyles.miniButton) + { + fixedHeight = 18f, + margin = new RectOffset(1, 1, 1, 1), + padding = new RectOffset(2, 2, 1, 1) + }; + } + + private bool DrawIndexMask(SerializedProperty maskProp, SerializedProperty indexProp, int length, bool multiSelect) + { + EditorGUILayout.LabelField(multiSelect ? "Active" : "Index", EditorStyles.miniLabel, GUILayout.Width(38f)); + int mask = maskProp.intValue; + int originalMask = mask; + for (int i = 0; i < length; i++) + { + int bit = UXBinding.BindingEntry.IndexToMask(i); + bool selected = (mask & bit) != 0; + bool nextSelected = GUILayout.Toggle(selected, i.ToString(), selected ? _pillOn : _pillOff, GUILayout.Width(26f)); + if (nextSelected != selected) + { + if (multiSelect) + { + if (nextSelected) + { + mask |= bit; + } + else + { + mask &= ~bit; + } + } + else + { + mask = bit; + } + } + } + + if (mask == 0) + { + mask = UXBinding.BindingEntry.IndexToMask(Mathf.Clamp(indexProp.intValue, 0, length - 1)); + } + + maskProp.intValue = ClampMask(mask, length, indexProp.intValue); + indexProp.intValue = GetFirstSelectedIndex(maskProp.intValue); + return maskProp.intValue != originalMask; + } + + private static SerializedProperty GetIndexedValueProperty(SerializedProperty indexedValuesProp, SerializedProperty fallbackValueProp, int selectedIndex) + { + for (int i = 0; i < indexedValuesProp.arraySize; i++) + { + SerializedProperty indexedValueProp = indexedValuesProp.GetArrayElementAtIndex(i); + if (indexedValueProp.FindPropertyRelative("_index").intValue == selectedIndex) + { + return indexedValueProp.FindPropertyRelative("_value"); + } + } + + int nextIndex = indexedValuesProp.arraySize; + indexedValuesProp.InsertArrayElementAtIndex(nextIndex); + SerializedProperty nextValueProp = indexedValuesProp.GetArrayElementAtIndex(nextIndex); + nextValueProp.FindPropertyRelative("_index").intValue = selectedIndex; + CopyValue(fallbackValueProp, nextValueProp.FindPropertyRelative("_value")); + return nextValueProp.FindPropertyRelative("_value"); + } + + private static void CopyValue(SerializedProperty source, SerializedProperty destination) + { + destination.FindPropertyRelative("_boolValue").boolValue = source.FindPropertyRelative("_boolValue").boolValue; + destination.FindPropertyRelative("_floatValue").floatValue = source.FindPropertyRelative("_floatValue").floatValue; + destination.FindPropertyRelative("_stringValue").stringValue = source.FindPropertyRelative("_stringValue").stringValue; + destination.FindPropertyRelative("_colorValue").colorValue = source.FindPropertyRelative("_colorValue").colorValue; + destination.FindPropertyRelative("_vector2Value").vector2Value = source.FindPropertyRelative("_vector2Value").vector2Value; + destination.FindPropertyRelative("_vector3Value").vector3Value = source.FindPropertyRelative("_vector3Value").vector3Value; + destination.FindPropertyRelative("_objectValue").objectReferenceValue = source.FindPropertyRelative("_objectValue").objectReferenceValue; + } + + private static void DrawGameObjectActiveHint(int mask) + { + EditorGUILayout.LabelField($"Visible: {BuildIndexLabel(mask)} Hidden: others", EditorStyles.miniLabel); + } + + private static void ForceGameObjectActiveValues(SerializedProperty valueProp, SerializedProperty fallbackModeProp, SerializedProperty fallbackValueProp) + { + valueProp.FindPropertyRelative("_boolValue").boolValue = true; + fallbackModeProp.enumValueIndex = (int)UXBindingFallbackMode.UseCustomValue; + fallbackValueProp.FindPropertyRelative("_boolValue").boolValue = false; + } + + private bool HasRule(string controllerId, UXBindingProperty property) + { + for (int i = 0; i < _entriesProp.arraySize; i++) + { + SerializedProperty entry = _entriesProp.GetArrayElementAtIndex(i); + if (entry.FindPropertyRelative("_controllerId").stringValue == controllerId && + entry.FindPropertyRelative("_property").enumValueIndex == (int)property) + { + return true; + } + } + + return false; + } + + private void SetAllFoldouts(bool expanded) + { + for (int i = 0; i < _entriesProp.arraySize; i++) + { + _foldouts[i] = expanded; + } + } + + private static bool TryGetControllerName(UXController controller, string controllerId, out string controllerName) + { + controllerName = string.Empty; + if (controller == null || string.IsNullOrEmpty(controllerId)) + { + return false; + } + + for (int i = 0; i < controller.Controllers.Count; i++) + { + UXController.ControllerDefinition definition = controller.Controllers[i]; + if (definition != null && definition.Id == controllerId) + { + controllerName = definition.Name; + return true; + } + } + + return false; + } + + private static string BuildIndexLabel(int mask) + { + if (mask == 0) + { + return "0"; + } + + string label = string.Empty; + for (int i = 0; i < 31; i++) + { + if ((mask & UXBinding.BindingEntry.IndexToMask(i)) == 0) + { + continue; + } + + label = string.IsNullOrEmpty(label) ? i.ToString() : $"{label},{i}"; + } + + return label; + } + + private static int ClampMask(int mask, int length, int fallbackIndex) + { + int validMask = 0; + int max = Mathf.Min(length, 31); + for (int i = 0; i < max; i++) + { + validMask |= UXBinding.BindingEntry.IndexToMask(i); + } + + mask &= validMask; + if (mask == 0) + { + mask = UXBinding.BindingEntry.IndexToMask(Mathf.Clamp(fallbackIndex, 0, max - 1)); + } + + return mask; + } + + private static int GetFirstSelectedIndex(int mask) + { + for (int i = 0; i < 31; i++) + { + if ((mask & UXBinding.BindingEntry.IndexToMask(i)) != 0) + { + return i; + } + } + + return 0; + } + + private static void EnsureStringArray(ref string[] array, int length) + { + if (array.Length != length) + { + array = new string[length]; + } + } } } #endif diff --git a/Editor/UX/Controller/UXControllerEditor.cs b/Editor/UX/Controller/UXControllerEditor.cs index d4e2a90..143c1b8 100644 --- a/Editor/UX/Controller/UXControllerEditor.cs +++ b/Editor/UX/Controller/UXControllerEditor.cs @@ -77,6 +77,7 @@ namespace UnityEngine.UI SerializedProperty idProp = entryProp.FindPropertyRelative("_id"); SerializedProperty nameProp = entryProp.FindPropertyRelative("_name"); SerializedProperty lengthProp = entryProp.FindPropertyRelative("_length"); + SerializedProperty defaultIndexProp = entryProp.FindPropertyRelative("_defaultIndex"); SerializedProperty descriptionProp = entryProp.FindPropertyRelative("_description"); bool expanded = _foldouts.ContainsKey(index) && _foldouts[index]; @@ -110,6 +111,7 @@ namespace UnityEngine.UI { EditorGUILayout.PropertyField(nameProp, new GUIContent("Name")); lengthProp.intValue = Mathf.Max(1, EditorGUILayout.IntField("Length", Mathf.Max(1, lengthProp.intValue))); + defaultIndexProp.intValue = Mathf.Clamp(EditorGUILayout.IntField("Default Index", defaultIndexProp.intValue), 0, lengthProp.intValue - 1); EditorGUILayout.PropertyField(descriptionProp, new GUIContent("Description")); EditorGUI.BeginDisabledGroup(true); @@ -181,6 +183,7 @@ namespace UnityEngine.UI entryProp.FindPropertyRelative("_id").stringValue = string.Empty; entryProp.FindPropertyRelative("_name").stringValue = $"Controller {index + 1}"; entryProp.FindPropertyRelative("_length").intValue = 2; + entryProp.FindPropertyRelative("_defaultIndex").intValue = 0; entryProp.FindPropertyRelative("_description").stringValue = string.Empty; _foldouts[index] = true; @@ -195,6 +198,15 @@ namespace UnityEngine.UI SerializedProperty entryProp = _controllersProp.GetArrayElementAtIndex(index); string deletedControllerId = entryProp.FindPropertyRelative("_id").stringValue; + int referenceCount = CountBindingReferences(controller, deletedControllerId); + if (referenceCount > 0 && !EditorUtility.DisplayDialog( + "Delete UX Controller", + $"This controller is referenced by {referenceCount} binding rule(s). Delete and clear those references?", + "Delete", + "Cancel")) + { + return; + } Undo.RecordObject(controller, "Delete UX Controller"); _controllersProp.DeleteArrayElementAtIndex(index); @@ -261,6 +273,37 @@ namespace UnityEngine.UI } } } + + private static int CountBindingReferences(UXController controller, string controllerId) + { + if (controller == null || string.IsNullOrEmpty(controllerId)) + { + return 0; + } + + int count = 0; + IReadOnlyList bindings = controller.Bindings; + for (int i = 0; i < bindings.Count; i++) + { + UXBinding binding = bindings[i]; + if (binding == null) + { + continue; + } + + IReadOnlyList entries = binding.Entries; + for (int entryIndex = 0; entryIndex < entries.Count; entryIndex++) + { + UXBinding.BindingEntry entry = entries[entryIndex]; + if (entry != null && entry.ControllerId == controllerId) + { + count++; + } + } + } + + return count; + } } } #endif diff --git a/Editor/UX/Controller/UXControllerSceneOverlayWindow.cs b/Editor/UX/Controller/UXControllerSceneOverlayWindow.cs index 32cb194..be7cc31 100644 --- a/Editor/UX/Controller/UXControllerSceneOverlayWindow.cs +++ b/Editor/UX/Controller/UXControllerSceneOverlayWindow.cs @@ -8,29 +8,39 @@ namespace UnityEngine.UI public sealed class UXControllerSceneOverlayWindow : EditorWindow { private const string OverlayEnabledKey = "AlicizaX.UI.UXControllerSceneOverlay.Enabled"; - private const string OverlayAutoFocusKey = "AlicizaX.UI.UXControllerSceneOverlay.AutoFocus"; - private const float OverlayWidth = 360f; - private const float OverlayMargin = 12f; + private const string MenuPath = "Window/UX/Controller Scene Overlay"; + private const float OverlayWidth = 340f; + private const float OverlayMargin = 10f; private static bool s_overlayEnabled; - private static bool s_autoFocusSceneView; private static Vector2 s_scrollPosition; private static bool s_registered; + private static readonly List Controllers = new List(); + private static GUIStyle s_indexOnStyle; + private static GUIStyle s_indexOffStyle; - [MenuItem("Window/UX/Controller Scene Overlay")] - public static void ShowWindow() + [MenuItem(MenuPath)] + public static void ToggleOverlay() { - var window = GetWindow("UX Controller Overlay"); - window.minSize = new Vector2(320f, 140f); - window.Show(); + s_overlayEnabled = !s_overlayEnabled; + EditorPrefs.SetBool(OverlayEnabledKey, s_overlayEnabled); + Menu.SetChecked(MenuPath, s_overlayEnabled); EnsureRegistered(); + SceneView.RepaintAll(); + } + + [MenuItem(MenuPath, true)] + private static bool ToggleOverlayValidate() + { + Menu.SetChecked(MenuPath, s_overlayEnabled); + return true; } [InitializeOnLoadMethod] private static void Initialize() { s_overlayEnabled = EditorPrefs.GetBool(OverlayEnabledKey, false); - s_autoFocusSceneView = EditorPrefs.GetBool(OverlayAutoFocusKey, false); + Menu.SetChecked(MenuPath, s_overlayEnabled); EnsureRegistered(); } @@ -45,37 +55,6 @@ namespace UnityEngine.UI s_registered = true; } - private void OnGUI() - { - EditorGUILayout.LabelField("Scene View Overlay", EditorStyles.boldLabel); - EditorGUILayout.HelpBox( - "When enabled, the Scene View will display all UXController components in the current loaded scenes and let you preview them directly.", - MessageType.Info); - - EditorGUI.BeginChangeCheck(); - bool overlayEnabled = EditorGUILayout.Toggle("Enable Overlay", s_overlayEnabled); - bool autoFocus = EditorGUILayout.Toggle("Focus SceneView On Open", s_autoFocusSceneView); - if (EditorGUI.EndChangeCheck()) - { - s_overlayEnabled = overlayEnabled; - s_autoFocusSceneView = autoFocus; - EditorPrefs.SetBool(OverlayEnabledKey, s_overlayEnabled); - EditorPrefs.SetBool(OverlayAutoFocusKey, s_autoFocusSceneView); - SceneView.RepaintAll(); - } - - EditorGUILayout.Space(8f); - if (GUILayout.Button("Repaint Scene View")) - { - SceneView.RepaintAll(); - } - - if (GUILayout.Button("Open Scene View")) - { - SceneView.FocusWindowIfItsOpen(); - } - } - private static void OnSceneGui(SceneView sceneView) { if (!s_overlayEnabled) @@ -83,51 +62,44 @@ namespace UnityEngine.UI return; } - if (s_autoFocusSceneView && sceneView != null) - { - s_autoFocusSceneView = false; - EditorPrefs.SetBool(OverlayAutoFocusKey, false); - sceneView.Focus(); - } - - List controllers = GetSceneControllers(); + GetSceneControllers(Controllers); Handles.BeginGUI(); - float height = Mathf.Max(160f, sceneView.position.height - OverlayMargin * 2f); + float height = Mathf.Min(Mathf.Max(180f, sceneView.position.height - OverlayMargin * 2f), 520f); Rect area = new Rect( sceneView.position.width - OverlayWidth - OverlayMargin, OverlayMargin, OverlayWidth, height); - GUILayout.BeginArea(area, EditorStyles.helpBox); - DrawOverlayContent(controllers); + GUILayout.BeginArea(area, GUI.skin.window); + DrawOverlayContent(Controllers); GUILayout.EndArea(); Handles.EndGUI(); } private static void DrawOverlayContent(List controllers) { + EnsureStyles(); + EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField("UX Controllers", EditorStyles.boldLabel); + EditorGUILayout.LabelField("UX Controller Preview", EditorStyles.boldLabel); + EditorGUILayout.LabelField(controllers.Count.ToString(), EditorStyles.miniBoldLabel, GUILayout.Width(24f)); GUILayout.FlexibleSpace(); - if (GUILayout.Button("Hide", GUILayout.Width(64f))) + if (GUILayout.Button("X", EditorStyles.miniButton, GUILayout.Width(22f))) { s_overlayEnabled = false; EditorPrefs.SetBool(OverlayEnabledKey, s_overlayEnabled); - SceneView.RepaintAll(); - } - if (GUILayout.Button("Refresh", GUILayout.Width(64f))) - { + Menu.SetChecked(MenuPath, false); SceneView.RepaintAll(); } EditorGUILayout.EndHorizontal(); - EditorGUILayout.Space(4f); + EditorGUILayout.Space(2f); if (controllers.Count == 0) { - EditorGUILayout.HelpBox("No UXController found in the current loaded scenes.", MessageType.Info); + EditorGUILayout.HelpBox("No UXController in loaded scenes.", MessageType.Info); return; } @@ -152,16 +124,17 @@ namespace UnityEngine.UI EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField($"{index + 1}. {controller.gameObject.name}", EditorStyles.boldLabel); + EditorGUILayout.LabelField(controller.gameObject.name, EditorStyles.boldLabel); + EditorGUILayout.LabelField($"C:{controller.ControllerCount} B:{controller.Bindings.Count}", EditorStyles.miniLabel, GUILayout.Width(72f)); GUILayout.FlexibleSpace(); - if (GUILayout.Button("Select", GUILayout.Width(52f))) + if (GUILayout.Button("Ping", EditorStyles.miniButtonLeft, GUILayout.Width(42f))) { Selection.activeGameObject = controller.gameObject; EditorGUIUtility.PingObject(controller.gameObject); } - if (GUILayout.Button("Reset", GUILayout.Width(52f))) + if (GUILayout.Button("Reset", EditorStyles.miniButtonRight, GUILayout.Width(44f))) { controller.ResetAllControllers(); EditorUtility.SetDirty(controller); @@ -170,7 +143,6 @@ namespace UnityEngine.UI EditorGUILayout.EndHorizontal(); EditorGUILayout.LabelField(GetHierarchyPath(controller.transform), EditorStyles.miniLabel); - EditorGUILayout.LabelField($"Bindings: {controller.Bindings.Count}", EditorStyles.miniLabel); for (int controllerIndex = 0; controllerIndex < controller.ControllerCount; controllerIndex++) { @@ -188,38 +160,33 @@ namespace UnityEngine.UI private static void DrawDefinitionPreview(UXController controller, UXController.ControllerDefinition definition) { - EditorGUILayout.Space(3f); - EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField(definition.Name, EditorStyles.boldLabel); + int currentIndex = Mathf.Max(0, definition.SelectedIndex); + int length = Mathf.Max(1, definition.Length); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(definition.Name, EditorStyles.miniBoldLabel, GUILayout.Width(96f)); + for (int i = 0; i < length; i++) + { + bool selected = i == currentIndex; + GUIStyle style = selected ? s_indexOnStyle : s_indexOffStyle; + if (GUILayout.Toggle(selected, i.ToString(), style, GUILayout.Width(26f)) != selected) + { + controller.SetControllerIndex(definition.Id, i); + EditorUtility.SetDirty(controller); + SceneView.RepaintAll(); + } + } + EditorGUILayout.EndHorizontal(); if (!string.IsNullOrWhiteSpace(definition.Description)) { EditorGUILayout.LabelField(definition.Description, EditorStyles.miniLabel); } - - int currentIndex = Mathf.Max(0, definition.SelectedIndex); - int length = Mathf.Max(1, definition.Length); - string[] options = new string[length]; - for (int i = 0; i < length; i++) - { - options[i] = i.ToString(); - } - - EditorGUI.BeginChangeCheck(); - int newIndex = GUILayout.SelectionGrid(currentIndex, options, Mathf.Min(length, 5)); - if (EditorGUI.EndChangeCheck()) - { - controller.SetControllerIndex(definition.Id, newIndex); - EditorUtility.SetDirty(controller); - SceneView.RepaintAll(); - } - - EditorGUILayout.EndVertical(); } - private static List GetSceneControllers() + private static void GetSceneControllers(List results) { - var results = new List(); + results.Clear(); UXController[] allControllers = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < allControllers.Length; i++) @@ -248,8 +215,35 @@ namespace UnityEngine.UI results.Add(controller); } - results.Sort((left, right) => string.CompareOrdinal(GetHierarchyPath(left.transform), GetHierarchyPath(right.transform))); - return results; + results.Sort(CompareControllers); + } + + private static int CompareControllers(UXController left, UXController right) + { + return string.CompareOrdinal(GetHierarchyPath(left.transform), GetHierarchyPath(right.transform)); + } + + private static void EnsureStyles() + { + if (s_indexOnStyle != null) + { + return; + } + + s_indexOnStyle = new GUIStyle(EditorStyles.miniButton) + { + fontStyle = FontStyle.Bold, + fixedHeight = 18f, + margin = new RectOffset(1, 1, 1, 1), + padding = new RectOffset(2, 2, 1, 1) + }; + + s_indexOffStyle = new GUIStyle(EditorStyles.miniButton) + { + fixedHeight = 18f, + margin = new RectOffset(1, 1, 1, 1), + padding = new RectOffset(2, 2, 1, 1) + }; } private static string GetHierarchyPath(Transform target) diff --git a/Editor/UX/Group/UXGroupEditor.cs b/Editor/UX/Group/UXGroupEditor.cs index 674eb28..1144bf0 100644 --- a/Editor/UX/Group/UXGroupEditor.cs +++ b/Editor/UX/Group/UXGroupEditor.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; +using UnityEditor; using UnityEditorInternal; using UnityEngine; using UnityEngine.UI; -using UnityEditor; namespace UnityEditor.UI { @@ -12,23 +11,23 @@ namespace UnityEditor.UI private SerializedProperty m_Toggles; private SerializedProperty m_AllowSwitchOff; private SerializedProperty m_DefaultToggle; - private UXGroup _target; - private ReorderableList _reorderableList; + private UXGroup m_Target; + private ReorderableList m_ReorderableList; private void OnEnable() { - _target = (UXGroup)target; + m_Target = (UXGroup)target; m_Toggles = serializedObject.FindProperty("m_Toggles"); m_AllowSwitchOff = serializedObject.FindProperty("m_AllowSwitchOff"); m_DefaultToggle = serializedObject.FindProperty("m_DefaultToggle"); - _reorderableList = new ReorderableList(serializedObject, m_Toggles, true, true, true, true) + m_ReorderableList = new ReorderableList(serializedObject, m_Toggles, true, true, true, true) { drawHeaderCallback = DrawHeader, drawElementCallback = DrawElement, + onAddCallback = OnAddList, onRemoveCallback = OnRemoveList, - onChangedCallback = OnChanged, - displayAdd = false, + onChangedCallback = OnChanged }; } @@ -37,160 +36,166 @@ namespace UnityEditor.UI serializedObject.Update(); EditorGUILayout.PropertyField(m_AllowSwitchOff); - - // Default selector: only show toggles that are currently in the group's m_Toggles list DrawDefaultToggleSelector(); + DrawTools(); bool isPlaying = Application.isPlaying || EditorApplication.isPlaying; + m_ReorderableList.draggable = !isPlaying; + m_ReorderableList.displayAdd = !isPlaying; + m_ReorderableList.displayRemove = !isPlaying; - _reorderableList.draggable = !isPlaying; - _reorderableList.displayAdd = !isPlaying; - _reorderableList.displayRemove = !isPlaying; - - bool prevEnabled = GUI.enabled; - if (isPlaying) GUI.enabled = false; - - _reorderableList.DoLayoutList(); - - GUI.enabled = prevEnabled; + bool previousEnabled = GUI.enabled; + GUI.enabled = !isPlaying; + m_ReorderableList.DoLayoutList(); + GUI.enabled = previousEnabled; serializedObject.ApplyModifiedProperties(); - // 在编辑器下尽量实时同步状态 if (!Application.isPlaying) - { - // 保证序列化数据写入后同步 group 状态 - _target.EnsureValidState(); - } + m_Target.EnsureValidState(); } private void DrawDefaultToggleSelector() { - // Build a list of current toggles (non-null) - List toggles = new List(); + int toggleCount = CountValidToggles(); + bool requireDefault = !m_AllowSwitchOff.boolValue && toggleCount > 0; + int optionOffset = requireDefault ? 0 : 1; + string[] options = new string[toggleCount + optionOffset]; + UXToggle[] toggles = new UXToggle[toggleCount]; + if (!requireDefault) + options[0] = ""; + + int write = 0; for (int i = 0; i < m_Toggles.arraySize; i++) { - var elem = m_Toggles.GetArrayElementAtIndex(i); - var t = elem.objectReferenceValue as UXToggle; - if (t != null) - toggles.Add(t); + UXToggle toggle = m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue as UXToggle; + if (toggle == null) + continue; + + toggles[write] = toggle; + options[write + optionOffset] = "[" + i + "] " + toggle.name; + write++; } - // Prepare options array with a "None" entry - string[] options = new string[toggles.Count + 1]; - options[0] = ""; - for (int i = 0; i < toggles.Count; i++) - { - options[i + 1] = string.Format("[{0}] {1}", i, toggles[i] != null ? toggles[i].name : "Null"); - } - - // Determine current index UXToggle currentDefault = m_DefaultToggle.objectReferenceValue as UXToggle; - int currentIndex = 0; - if (currentDefault != null) + int currentIndex = requireDefault ? -1 : 0; + for (int i = 0; i < toggles.Length; i++) { - int found = toggles.IndexOf(currentDefault); - if (found >= 0) - currentIndex = found + 1; // +1 because 0 is - else + if (toggles[i] == currentDefault) { - // Current default is not in the list -> clear it - m_DefaultToggle.objectReferenceValue = null; - serializedObject.ApplyModifiedProperties(); - currentIndex = 0; + currentIndex = i + optionOffset; + break; } } + if (requireDefault && currentIndex < 0) + { + currentDefault = GetSelectedToggle(toggles); + if (currentDefault == null) + currentDefault = toggles[0]; + + m_DefaultToggle.objectReferenceValue = currentDefault; + for (int i = 0; i < toggles.Length; i++) + { + if (toggles[i] == currentDefault) + { + currentIndex = i; + break; + } + } + } + else if (!requireDefault && currentDefault != null && currentIndex == 0) + { + m_DefaultToggle.objectReferenceValue = null; + } + EditorGUI.BeginChangeCheck(); int newIndex = EditorGUILayout.Popup("Default Toggle", currentIndex, options); if (EditorGUI.EndChangeCheck()) { - UXToggle newDefault = null; - if (newIndex > 0) - newDefault = toggles[newIndex - 1]; - + UXToggle newDefault = newIndex >= optionOffset ? toggles[newIndex - optionOffset] : null; m_DefaultToggle.objectReferenceValue = newDefault; serializedObject.ApplyModifiedProperties(); - // 如果选择了一个非空默认项,则在 allowSwitchOff == false 时将其设为选中状态(并通知组) if (newDefault != null) { - // 确保该 toggle 在 group 中(理论上应该如此) - if (!_target.ContainsToggle(newDefault)) - { - _target.RegisterToggle(newDefault); - } - - if (!_target.allowSwitchOff) - { - // 通过 SerializedObject 修改 UXToggle 的 m_IsOn,避免触发不必要的回调 - SerializedObject so = new SerializedObject(newDefault); - var isOnProp = so.FindProperty("m_IsOn"); - isOnProp.boolValue = true; - so.ApplyModifiedProperties(); - - // 通知组同步其余项 - _target.NotifyToggleOn(newDefault); - } - } - else - { - // 选择 None:不自动切换状态。但如果组不允许 all-off,则会在 EnsureValidState 中被处理 + AssignGroup(newDefault, m_Target); } } } + private static UXToggle GetSelectedToggle(UXToggle[] toggles) + { + for (int i = 0; i < toggles.Length; i++) + { + UXToggle toggle = toggles[i]; + if (toggle != null && toggle.isOn) + return toggle; + } + + return null; + } + + private void DrawTools() + { + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Collect Children")) + { + CollectChildren(); + } + + if (GUILayout.Button("Clean Nulls")) + { + CleanNulls(); + } + + if (GUILayout.Button("Sort By Hierarchy")) + { + SortByHierarchy(); + } + + EditorGUILayout.EndHorizontal(); + } + private void DrawHeader(Rect rect) { EditorGUI.LabelField(rect, "Toggles", EditorStyles.boldLabel); } - // 记录旧的引用用于侦测变化 - private UXToggle previousRef; - private void DrawElement(Rect rect, int index, bool isActive, bool isFocused) { SerializedProperty element = m_Toggles.GetArrayElementAtIndex(index); + UXToggle oldToggle = element.objectReferenceValue as UXToggle; rect.y += 2; Rect fieldRect = new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight); + string label = oldToggle != null ? "[" + index + "] " + oldToggle.name : "[" + index + "] Null"; - UXToggle oldButton = element.objectReferenceValue as UXToggle; - - string label = $"[{index}] {(oldButton != null ? oldButton.name : "Null")}"; + bool duplicate = oldToggle != null && HasDuplicate(oldToggle, index); + bool wrongGroup = oldToggle != null && oldToggle.group != null && oldToggle.group != m_Target; + if (duplicate || wrongGroup) + EditorGUI.DrawRect(fieldRect, new Color(1f, 0.55f, 0f, 0.2f)); EditorGUI.BeginChangeCheck(); - - var newRef = EditorGUI.ObjectField(fieldRect, label, oldButton, typeof(UXToggle), true) as UXToggle; - + UXToggle newToggle = EditorGUI.ObjectField(fieldRect, label, oldToggle, typeof(UXToggle), true) as UXToggle; if (EditorGUI.EndChangeCheck()) { - // 先处理 Remove(旧值存在且不同) - if (oldButton != null && oldButton != newRef) - { - OnRemove(oldButton); - } + if (oldToggle != null && oldToggle != newToggle) + AssignGroup(oldToggle, null); - // 再处理 Add(新值非空) - if (newRef != null && oldButton != newRef) - { - OnAdd(newRef); - } + if (newToggle != null && oldToggle != newToggle) + AssignGroup(newToggle, m_Target); - // 最后把引用写回去 - element.objectReferenceValue = newRef; + element.objectReferenceValue = newToggle; serializedObject.ApplyModifiedProperties(); + m_Target.EnsureValidState(); } } private void OnAddList(ReorderableList list) { - int newIndex = m_Toggles.arraySize; m_Toggles.arraySize++; - serializedObject.ApplyModifiedProperties(); - - var newElem = m_Toggles.GetArrayElementAtIndex(newIndex); - newElem.objectReferenceValue = null; + m_Toggles.GetArrayElementAtIndex(m_Toggles.arraySize - 1).objectReferenceValue = null; serializedObject.ApplyModifiedProperties(); } @@ -199,72 +204,122 @@ namespace UnityEditor.UI if (list.index < 0 || list.index >= m_Toggles.arraySize) return; - var oldButton = m_Toggles.GetArrayElementAtIndex(list.index).objectReferenceValue as UXToggle; - if (oldButton) - { - OnRemove(oldButton); - } + UXToggle oldToggle = m_Toggles.GetArrayElementAtIndex(list.index).objectReferenceValue as UXToggle; + if (oldToggle != null) + AssignGroup(oldToggle, null); m_Toggles.DeleteArrayElementAtIndex(list.index); serializedObject.ApplyModifiedProperties(); + m_Target.EnsureValidState(); } private void OnChanged(ReorderableList list) { serializedObject.ApplyModifiedProperties(); - - // 编辑器变动后同步 group 状态和默认项 if (!Application.isPlaying) - _target.EnsureValidState(); + m_Target.EnsureValidState(); } - // ======================== - // 自动调用的新增方法 - // ======================== - - private void OnAdd(UXToggle toggle) + private void CollectChildren() { - if (toggle == null) - return; - - SerializedObject so = new SerializedObject(toggle); - var groupProp = so.FindProperty("m_Group"); - groupProp.objectReferenceValue = target; - so.ApplyModifiedProperties(); - - UXGroup group = (UXGroup)target; - group.RegisterToggle(toggle); - - // 添加后尽量同步组状态(处理默认项或冲突) - if (!Application.isPlaying) - group.EnsureValidState(); - } - - private void OnRemove(UXToggle toggle) - { - if (toggle == null) - return; - - SerializedObject so = new SerializedObject(toggle); - var groupProp = so.FindProperty("m_Group"); - - UXGroup group = groupProp.objectReferenceValue as UXGroup; - if (group != null) - group.UnregisterToggle(toggle); - - groupProp.objectReferenceValue = null; - - so.ApplyModifiedProperties(); - - // 如果移除的正好是默认项,则清空默认选项 - if (m_DefaultToggle != null && m_DefaultToggle.objectReferenceValue == toggle) + UXToggle[] toggles = m_Target.GetComponentsInChildren(true); + m_Toggles.arraySize = toggles.Length; + for (int i = 0; i < toggles.Length; i++) { - m_DefaultToggle.objectReferenceValue = null; - serializedObject.ApplyModifiedProperties(); + m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue = toggles[i]; + AssignGroup(toggles[i], m_Target); } - if (!Application.isPlaying && _target != null) - _target.EnsureValidState(); + serializedObject.ApplyModifiedProperties(); + m_Target.EnsureValidState(); + EditorUtility.SetDirty(m_Target); + } + + private void CleanNulls() + { + for (int i = m_Toggles.arraySize - 1; i >= 0; i--) + { + if (m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue == null) + m_Toggles.DeleteArrayElementAtIndex(i); + } + + serializedObject.ApplyModifiedProperties(); + m_Target.EnsureValidState(); + EditorUtility.SetDirty(m_Target); + } + + private void SortByHierarchy() + { + int count = CountValidToggles(); + UXToggle[] toggles = new UXToggle[count]; + int write = 0; + for (int i = 0; i < m_Toggles.arraySize; i++) + { + UXToggle toggle = m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue as UXToggle; + if (toggle != null) + { + toggles[write] = toggle; + write++; + } + } + + System.Array.Sort(toggles, CompareHierarchyIndex); + m_Toggles.arraySize = toggles.Length; + for (int i = 0; i < toggles.Length; i++) + m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue = toggles[i]; + + serializedObject.ApplyModifiedProperties(); + m_Target.EnsureValidState(); + EditorUtility.SetDirty(m_Target); + } + + private int CountValidToggles() + { + int count = 0; + for (int i = 0; i < m_Toggles.arraySize; i++) + { + if (m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue != null) + count++; + } + + return count; + } + + private bool HasDuplicate(UXToggle toggle, int selfIndex) + { + for (int i = 0; i < m_Toggles.arraySize; i++) + { + if (i != selfIndex && m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue == toggle) + return true; + } + + return false; + } + + private static int CompareHierarchyIndex(UXToggle left, UXToggle right) + { + int leftIndex = left.transform.GetSiblingIndex(); + int rightIndex = right.transform.GetSiblingIndex(); + if (leftIndex < rightIndex) + return -1; + + if (leftIndex > rightIndex) + return 1; + + return 0; + } + + private static void AssignGroup(UXToggle toggle, UXGroup group) + { + if (toggle == null) + return; + + SerializedObject serializedToggle = new SerializedObject(toggle); + SerializedProperty groupProperty = serializedToggle.FindProperty("m_Group"); + groupProperty.objectReferenceValue = group; + serializedToggle.ApplyModifiedProperties(); + toggle.group = group; + EditorUtility.SetDirty(toggle); } } } diff --git a/Editor/UX/Group/UXGroupEditor.cs.meta b/Editor/UX/Group/UXGroupEditor.cs.meta index 70a0f1a..9bf8b7f 100644 --- a/Editor/UX/Group/UXGroupEditor.cs.meta +++ b/Editor/UX/Group/UXGroupEditor.cs.meta @@ -1,3 +1,11 @@ fileFormatVersion: 2 guid: 292ca921cd4242d4be9f76bef9bfc08f -timeCreated: 1760343702 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UX/Hotkey/HotkeyComponentEditor.cs b/Editor/UX/Hotkey/HotkeyComponentEditor.cs index ae4ab23..73a557a 100644 --- a/Editor/UX/Hotkey/HotkeyComponentEditor.cs +++ b/Editor/UX/Hotkey/HotkeyComponentEditor.cs @@ -11,10 +11,12 @@ namespace UnityEditor.UI private SerializedProperty _hotkeyAction; private SerializedProperty _hotkeyPressType; private SerializedProperty _component; + private SerializedProperty _holder; private void OnEnable() { _component = serializedObject.FindProperty("_component"); + _holder = serializedObject.FindProperty("_holder"); _hotkeyAction = serializedObject.FindProperty("_hotkeyAction"); _hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType"); } @@ -30,7 +32,7 @@ namespace UnityEditor.UI MessageType.Info ); - if (hotkeyComponent.GetComponentInParent(true) == null) + if (_holder.objectReferenceValue == null) { EditorGUILayout.HelpBox( "No UIHolderObjectBase was found in parents. This hotkey will not register at runtime.", @@ -46,6 +48,16 @@ namespace UnityEditor.UI _component.objectReferenceValue = submitHandler; } } + else if (_component.objectReferenceValue is not ISubmitHandler) + { + EditorGUILayout.HelpBox("Submit target must implement ISubmitHandler. The invalid reference will be cleared.", MessageType.Error); + _component.objectReferenceValue = null; + } + + if (_hotkeyAction.objectReferenceValue == null) + { + EditorGUILayout.HelpBox("Input Action is required. This hotkey will not register at runtime.", MessageType.Error); + } using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { @@ -53,6 +65,7 @@ namespace UnityEditor.UI EditorGUI.BeginDisabledGroup(true); EditorGUILayout.PropertyField(_component, new GUIContent("Component")); + EditorGUILayout.PropertyField(_holder, new GUIContent("Holder")); EditorGUI.EndDisabledGroup(); EditorGUILayout.PropertyField(_hotkeyAction, new GUIContent("Input Action")); diff --git a/Editor/UX/Image/UXImageEditor.cs b/Editor/UX/Image/UXImageEditor.cs index 869ba44..e3da771 100644 --- a/Editor/UX/Image/UXImageEditor.cs +++ b/Editor/UX/Image/UXImageEditor.cs @@ -4,7 +4,6 @@ using UnityEditor; using UnityEngine; using System.IO; using UnityEditorInternal; -using System.Linq; using UnityEditor.AnimatedValues; namespace UnityEngine.UI @@ -289,15 +288,6 @@ namespace UnityEngine.UI if (EditorGUI.EndChangeCheck()) { m_ColorType.intValue = (int)type; - if ((int)type == 1 && m_Type.intValue != 0) - { - m_Type.intValue = 0; - } - - if ((int)type == 1 && m_FlipMode.intValue != 0) - { - m_FlipMode.intValue = 0; - } } GUILayout.BeginHorizontal(); @@ -399,15 +389,7 @@ namespace UnityEngine.UI protected void TypeGUI() { - EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(m_Type, m_SpriteTypeContent); - if (EditorGUI.EndChangeCheck()) - { - if (m_Type.intValue != 0 && m_ColorType.intValue == 1) - { - m_ColorType.intValue = 0; - } - } ++EditorGUI.indentLevel; { @@ -415,7 +397,17 @@ namespace UnityEngine.UI bool showSlicedOrTiled = (!m_Type.hasMultipleDifferentValues && (typeEnum == Image.Type.Sliced || typeEnum == Image.Type.Tiled)); if (showSlicedOrTiled && targets.Length > 1) - showSlicedOrTiled = targets.Select(obj => obj as Image).All(img => img.hasBorder); + { + for (int i = 0; i < targets.Length; i++) + { + Image targetImage = targets[i] as Image; + if (targetImage == null || !targetImage.hasBorder) + { + showSlicedOrTiled = false; + break; + } + } + } m_ShowSlicedOrTiled.target = showSlicedOrTiled; m_ShowSliced.target = (showSlicedOrTiled && !m_Type.hasMultipleDifferentValues && typeEnum == Image.Type.Sliced); @@ -517,15 +509,7 @@ namespace UnityEngine.UI "None", "Horizontal", "Vertical", "FourCorner" }; - EditorGUI.BeginChangeCheck(); m_FlipMode.intValue = EnumPopupLayoutEx(m_FlipModeContent.text, typeof(UXImage.FlipMode), m_FlipMode.intValue, labels); - if (EditorGUI.EndChangeCheck()) - { - if (m_FlipMode.intValue != 0 && m_ColorType.intValue == 1) - { - m_ColorType.intValue = 0; - } - } //EditorGUILayout.PropertyField(m_FlipMode, m_FlipModeContent); UXImage.FlipMode flipmodeEnum = (UXImage.FlipMode)m_FlipMode.enumValueIndex; @@ -805,8 +789,12 @@ namespace UnityEngine.UI GUILayout.Space(EditorGUIUtility.labelWidth); if (GUILayout.Button(m_CorrectButtonContent, EditorStyles.miniButton)) { - foreach (Graphic graphic in targets.Select(obj => obj as Graphic)) + for (int i = 0; i < targets.Length; i++) { + Graphic graphic = targets[i] as Graphic; + if (graphic == null) + continue; + Undo.RecordObject(graphic.rectTransform, "Set Native Size"); graphic.SetNativeSize(); EditorUtility.SetDirty(graphic); @@ -819,10 +807,40 @@ namespace UnityEngine.UI EditorGUILayout.EndFadeGroup(); } + + private static readonly Dictionary s_EnumValues = new Dictionary(); + private static readonly Dictionary s_EnumNames = new Dictionary(); + + private static int[] GetEnumValues(Type type) + { + if (!s_EnumValues.TryGetValue(type, out int[] values)) + { + Array rawValues = Enum.GetValues(type); + values = new int[rawValues.Length]; + for (int i = 0; i < rawValues.Length; i++) + { + values[i] = (int)rawValues.GetValue(i); + } + s_EnumValues.Add(type, values); + } + + return values; + } + + private static string[] GetEnumNames(Type type) + { + if (!s_EnumNames.TryGetValue(type, out string[] names)) + { + names = Enum.GetNames(type); + s_EnumNames.Add(type, names); + } + + return names; + } public int EnumPopupLayoutEx(string label, Type type, int enumValueIndex, string[] labels) { - int[] ints = (int[])Enum.GetValues(type); - string[] strings = Enum.GetNames(type); + int[] ints = GetEnumValues(type); + string[] strings = GetEnumNames(type); if (labels.Length != ints.Length) { return EditorGUILayout.IntPopup(label, enumValueIndex, strings, ints); diff --git a/Editor/UX/Navigation.meta b/Editor/UX/Navigation.meta new file mode 100644 index 0000000..94790eb --- /dev/null +++ b/Editor/UX/Navigation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 55d4289f6579aec4eaeeeda649282af4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UX/Navigation/UXNavigationScopeEditor.cs b/Editor/UX/Navigation/UXNavigationScopeEditor.cs new file mode 100644 index 0000000..6df99a5 --- /dev/null +++ b/Editor/UX/Navigation/UXNavigationScopeEditor.cs @@ -0,0 +1,202 @@ +#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; + +namespace AlicizaX.UI.Extension.Editor +{ + [CustomEditor(typeof(UXNavigationScope))] + public sealed class UXNavigationScopeEditor : UnityEditor.Editor + { + private SerializedProperty _defaultSelectable; + private SerializedProperty _bakedSelectables; + private SerializedProperty _runtimeSelectableCapacity; + private SerializedProperty _rememberLastSelection; + private SerializedProperty _requireSelectionWhenGamepad; + private SerializedProperty _blockLowerScopes; + private SerializedProperty _autoSelectFirstAvailable; + + private void OnEnable() + { + _defaultSelectable = serializedObject.FindProperty("_defaultSelectable"); + _bakedSelectables = serializedObject.FindProperty("_bakedSelectables"); + _runtimeSelectableCapacity = serializedObject.FindProperty("_runtimeSelectableCapacity"); + _rememberLastSelection = serializedObject.FindProperty("_rememberLastSelection"); + _requireSelectionWhenGamepad = serializedObject.FindProperty("_requireSelectionWhenGamepad"); + _blockLowerScopes = serializedObject.FindProperty("_blockLowerScopes"); + _autoSelectFirstAvailable = serializedObject.FindProperty("_autoSelectFirstAvailable"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.PropertyField(_defaultSelectable); + EditorGUILayout.PropertyField(_runtimeSelectableCapacity); + EditorGUILayout.PropertyField(_rememberLastSelection); + EditorGUILayout.PropertyField(_requireSelectionWhenGamepad); + EditorGUILayout.PropertyField(_blockLowerScopes); + EditorGUILayout.PropertyField(_autoSelectFirstAvailable); + + EditorGUILayout.Space(); + DrawBakeTools(); + + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(_bakedSelectables, true); + + EditorGUILayout.Space(); + DrawDiagnostics(); + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawBakeTools() + { + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button("收集子 Selectable")) + { + BakeSelectables(); + } + + if (GUILayout.Button("清理空引用")) + { + RemoveNullEntries(); + } + + if (GUILayout.Button("按层级排序")) + { + SortBakedSelectables(); + } + } + } + + private void DrawDiagnostics() + { + UXNavigationScope scope = (UXNavigationScope)target; + EditorGUILayout.LabelField("诊断", EditorStyles.boldLabel); + EditorGUILayout.LabelField("烘焙控件数", _bakedSelectables.arraySize.ToString()); + EditorGUILayout.LabelField("Runtime 动态控件数", Application.isPlaying ? scope.RuntimeSelectableCount.ToString() : "仅 Play Mode"); + EditorGUILayout.LabelField("被 Skip", scope.GetComponentInParent(true) != null ? "是" : "否"); + EditorGUILayout.LabelField("当前 Suppressed", Application.isPlaying && scope.NavigationSuppressed ? "是" : "否"); + ValidateReferences(scope); + } + + private void ValidateReferences(UXNavigationScope scope) + { + Selectable defaultSelectable = _defaultSelectable.objectReferenceValue as Selectable; + if (defaultSelectable != null && defaultSelectable.GetComponentInParent(true) != scope) + { + EditorGUILayout.HelpBox("默认选中控件不属于当前 UXNavigationScope。", MessageType.Error); + } + + for (int i = 0; i < _bakedSelectables.arraySize; i++) + { + Selectable selectable = _bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue as Selectable; + if (selectable == null) + { + EditorGUILayout.HelpBox("烘焙列表存在空引用。", MessageType.Warning); + continue; + } + + if (selectable.GetComponentInParent(true) != scope) + { + EditorGUILayout.HelpBox("烘焙列表存在跨 Scope 引用。", MessageType.Error); + return; + } + } + } + + private void BakeSelectables() + { + UXNavigationScope scope = (UXNavigationScope)target; + Selectable[] allSelectables = scope.GetComponentsInChildren(true); + List ownedSelectables = new List(allSelectables.Length); + for (int i = 0; i < allSelectables.Length; i++) + { + Selectable selectable = allSelectables[i]; + if (selectable != null && selectable.GetComponentInParent(true) == scope) + { + ownedSelectables.Add(selectable); + } + } + + Undo.RecordObject(scope, "Bake UX Navigation Selectables"); + _bakedSelectables.arraySize = ownedSelectables.Count; + for (int i = 0; i < ownedSelectables.Count; i++) + { + _bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue = ownedSelectables[i]; + } + + serializedObject.ApplyModifiedProperties(); + EditorUtility.SetDirty(scope); + } + + private void RemoveNullEntries() + { + UXNavigationScope scope = (UXNavigationScope)target; + Undo.RecordObject(scope, "Clean UX Navigation Selectables"); + for (int i = _bakedSelectables.arraySize - 1; i >= 0; i--) + { + if (_bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue == null) + { + _bakedSelectables.DeleteArrayElementAtIndex(i); + } + } + + serializedObject.ApplyModifiedProperties(); + EditorUtility.SetDirty(scope); + } + + private void SortBakedSelectables() + { + UXNavigationScope scope = (UXNavigationScope)target; + List selectables = new List(_bakedSelectables.arraySize); + for (int i = 0; i < _bakedSelectables.arraySize; i++) + { + Selectable selectable = _bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue as Selectable; + if (selectable != null) + { + selectables.Add(selectable); + } + } + + selectables.Sort(CompareSiblingPath); + Undo.RecordObject(scope, "Sort UX Navigation Selectables"); + _bakedSelectables.arraySize = selectables.Count; + for (int i = 0; i < selectables.Count; i++) + { + _bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue = selectables[i]; + } + + serializedObject.ApplyModifiedProperties(); + EditorUtility.SetDirty(scope); + } + + private static int CompareSiblingPath(Selectable left, Selectable right) + { + if (left == right) + { + return 0; + } + + string leftPath = GetSiblingPath(left.transform); + string rightPath = GetSiblingPath(right.transform); + return string.CompareOrdinal(leftPath, rightPath); + } + + private static string GetSiblingPath(Transform transform) + { + if (transform == null) + { + return string.Empty; + } + + return transform.parent == null + ? transform.GetSiblingIndex().ToString("D4") + : GetSiblingPath(transform.parent) + "/" + transform.GetSiblingIndex().ToString("D4"); + } + } +} +#endif diff --git a/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs.meta b/Editor/UX/Navigation/UXNavigationScopeEditor.cs.meta similarity index 83% rename from Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs.meta rename to Editor/UX/Navigation/UXNavigationScopeEditor.cs.meta index 97da493..e808ea5 100644 --- a/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs.meta +++ b/Editor/UX/Navigation/UXNavigationScopeEditor.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 0268df3dd46bb194fa4ae7ec48be7702 +guid: 7cc921173c16d4b4ba1fcf1fcaa80479 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Editor/UX/Toggle/UXToggleEditor.cs b/Editor/UX/Toggle/UXToggleEditor.cs index 366de99..ba408b4 100644 --- a/Editor/UX/Toggle/UXToggleEditor.cs +++ b/Editor/UX/Toggle/UXToggleEditor.cs @@ -1,7 +1,7 @@ +using UnityEditor; using UnityEditor.DrawUtils; using UnityEditor.Extensions; using UnityEditor.SceneManagement; -using UnityEditor; using UnityEngine; using UnityEngine.UI; @@ -11,11 +11,11 @@ namespace UnityEditor.UI [CanEditMultipleObjects] internal class UXToggleEditor : UXSelectableEditor { - SerializedProperty m_OnValueChangedProperty; - SerializedProperty m_TransitionProperty; - SerializedProperty m_GraphicProperty; - SerializedProperty m_GroupProperty; - SerializedProperty m_IsOnProperty; + private SerializedProperty m_OnValueChangedProperty; + private SerializedProperty m_TransitionProperty; + private SerializedProperty m_GraphicProperty; + private SerializedProperty m_GroupProperty; + private SerializedProperty m_IsOnProperty; private SerializedProperty hoverAudioClip; private SerializedProperty clickAudioClip; @@ -48,10 +48,8 @@ namespace UnityEditor.UI private void DrawEventTab() { EditorGUILayout.Space(); - serializedObject.Update(); EditorGUILayout.PropertyField(m_OnValueChangedProperty); - serializedObject.ApplyModifiedProperties(); } @@ -60,100 +58,88 @@ namespace UnityEditor.UI GUILayoutHelper.DrawProperty(hoverAudioClip, customSkin, "Hover Sound", "Play", () => { if (hoverAudioClip.objectReferenceValue != null) - { PlayAudio((AudioClip)hoverAudioClip.objectReferenceValue); - } }); + GUILayoutHelper.DrawProperty(clickAudioClip, customSkin, "Click Sound", "Play", () => { if (clickAudioClip.objectReferenceValue != null) - { PlayAudio((AudioClip)clickAudioClip.objectReferenceValue); - } }); } private void PlayAudio(AudioClip clip) { if (clip != null) - { ExtensionHelper.PreviewAudioClip(clip); - } } private void DrawImageTab() { EditorGUILayout.Space(); - serializedObject.Update(); + UXToggle toggle = serializedObject.targetObject as UXToggle; - EditorGUI.BeginChangeCheck(); - GUILayoutHelper.DrawProperty(m_IsOnProperty, customSkin, "Is On"); - if (EditorGUI.EndChangeCheck()) - { - if (!Application.isPlaying) - EditorSceneManager.MarkSceneDirty(toggle.gameObject.scene); - - UXGroup group = m_GroupProperty.objectReferenceValue as UXGroup; - - bool newIsOn = m_IsOnProperty.boolValue; - bool oldIsOn = toggle.isOn; - - // 编辑器下:如果属于某组且不允许 all-off,且当前正是被选中的项,则禁止通过 Inspector 将其关闭 - if (!Application.isPlaying && group != null && !group.allowSwitchOff && oldIsOn && !newIsOn) - { - Debug.LogWarning($"Cannot turn off toggle '{toggle.name}' because its group '{group.name}' does not allow all toggles to be off.", toggle); - // 恢复 Inspector 中的显示为 true - m_IsOnProperty.boolValue = true; - serializedObject.ApplyModifiedProperties(); - } - else - { - // 使用属性赋值以保证视觉刷新(PlayEffect 会在 setter 被调用) - toggle.isOn = newIsOn; - - if (group != null && group.isActiveAndEnabled && toggle.IsActive()) - { - if (toggle.isOn || (!group.AnyTogglesOn() && !group.allowSwitchOff)) - { - toggle.isOn = true; - group.NotifyToggleOn(toggle); - } - } - } - } + DrawIsOn(toggle); GUILayoutHelper.DrawProperty(m_TransitionProperty, customSkin, "Transition"); GUILayoutHelper.DrawProperty(m_GraphicProperty, customSkin, "Graphic"); - EditorGUI.BeginChangeCheck(); - GUILayoutHelper.DrawProperty(m_GroupProperty, customSkin, "UXGroup", (oldValue, newValue) => - { - UXToggle self = target as UXToggle; - if (oldValue != null) - { - oldValue.UnregisterToggle(self); - } - - if (newValue != null) - { - newValue.RegisterToggle(self); - } - }); - - if (EditorGUI.EndChangeCheck()) - { - if (!Application.isPlaying) - EditorSceneManager.MarkSceneDirty(toggle.gameObject.scene); - - UXGroup group = m_GroupProperty.objectReferenceValue as UXGroup; - - // Use the property setter to ensure consistent registration/unregistration - toggle.group = group; - } + DrawGroup(toggle); EditorGUILayout.Space(); - serializedObject.ApplyModifiedProperties(); } + + private void DrawIsOn(UXToggle toggle) + { + EditorGUI.BeginChangeCheck(); + GUILayoutHelper.DrawProperty(m_IsOnProperty, customSkin, "Is On"); + if (!EditorGUI.EndChangeCheck()) + return; + + if (!Application.isPlaying) + EditorSceneManager.MarkSceneDirty(toggle.gameObject.scene); + + UXGroup group = m_GroupProperty.objectReferenceValue as UXGroup; + bool newIsOn = m_IsOnProperty.boolValue; + bool oldIsOn = toggle.isOn; + + if (!Application.isPlaying && group != null && !group.allowSwitchOff && oldIsOn && !newIsOn) + { + Debug.LogWarning("Cannot turn off the selected toggle because its group does not allow all toggles to be off.", toggle); + m_IsOnProperty.boolValue = true; + serializedObject.ApplyModifiedProperties(); + return; + } + + toggle.isOn = newIsOn; + } + + private void DrawGroup(UXToggle toggle) + { + EditorGUI.BeginChangeCheck(); + GUILayoutHelper.DrawProperty(m_GroupProperty, customSkin, "UXGroup", OnGroupChanged); + if (!EditorGUI.EndChangeCheck()) + return; + + if (!Application.isPlaying) + EditorSceneManager.MarkSceneDirty(toggle.gameObject.scene); + + UXGroup group = m_GroupProperty.objectReferenceValue as UXGroup; + toggle.group = group; + } + + private void OnGroupChanged(UXGroup oldValue, UXGroup newValue) + { + UXToggle toggle = target as UXToggle; + if (toggle == null) + return; + + if (oldValue != null) + oldValue.UnregisterToggle(toggle); + + if (newValue != null) + newValue.RegisterToggle(toggle); + } } } diff --git a/Editor/UX/Toggle/UXToggleEditor.cs.meta b/Editor/UX/Toggle/UXToggleEditor.cs.meta index 2fadedd..86f08e2 100644 --- a/Editor/UX/Toggle/UXToggleEditor.cs.meta +++ b/Editor/UX/Toggle/UXToggleEditor.cs.meta @@ -1,3 +1,11 @@ fileFormatVersion: 2 guid: 9301ee465f2c46d08b2fade637710625 -timeCreated: 1766136386 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Interaction/ItemInteractionProxy.cs b/Runtime/RecyclerView/Interaction/ItemInteractionProxy.cs index 98235cc..e931216 100644 --- a/Runtime/RecyclerView/Interaction/ItemInteractionProxy.cs +++ b/Runtime/RecyclerView/Interaction/ItemInteractionProxy.cs @@ -106,6 +106,9 @@ namespace AlicizaX.UI public void OnSelect(BaseEventData eventData) { #if UX_NAVIGATION +#if INPUTSYSTEM_SUPPORT + UXNavigationRuntime.NotifySelection(gameObject); +#endif if ((flags & ItemInteractionFlags.Select) != 0) { host?.HandleSelect(eventData); diff --git a/Runtime/RecyclerView/Navigation/RecyclerNavigationBridge.cs b/Runtime/RecyclerView/Navigation/RecyclerNavigationBridge.cs index edf65ee..d564654 100644 --- a/Runtime/RecyclerView/Navigation/RecyclerNavigationBridge.cs +++ b/Runtime/RecyclerView/Navigation/RecyclerNavigationBridge.cs @@ -29,6 +29,9 @@ namespace AlicizaX.UI public override void OnSelect(BaseEventData eventData) { base.OnSelect(eventData); +#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION + UXNavigationRuntime.NotifySelection(gameObject); +#endif TryEnter(defaultEntryDirection); } diff --git a/Runtime/UXComponent/Button/UXButton.cs b/Runtime/UXComponent/Button/UXButton.cs index dc99195..ba8c61d 100644 --- a/Runtime/UXComponent/Button/UXButton.cs +++ b/Runtime/UXComponent/Button/UXButton.cs @@ -37,6 +37,9 @@ namespace UnityEngine.UI public override void OnSelect(BaseEventData eventData) { base.OnSelect(eventData); +#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION + UXNavigationRuntime.NotifySelection(gameObject); +#endif if (eventData is PointerEventData) return; PlayAudio(hoverAudioClip); diff --git a/Runtime/UXComponent/Controller/UXBinding.cs b/Runtime/UXComponent/Controller/UXBinding.cs index 82221cd..efe6d11 100644 --- a/Runtime/UXComponent/Controller/UXBinding.cs +++ b/Runtime/UXComponent/Controller/UXBinding.cs @@ -115,10 +115,27 @@ namespace UnityEngine.UI [Serializable] public sealed class BindingEntry { + [Serializable] + public sealed class IndexedValue + { + [SerializeField] private int _index; + [SerializeField] private UXBindingValue _value = new UXBindingValue(); + + public int Index + { + get => Mathf.Max(0, _index); + set => _index = Mathf.Max(0, value); + } + + public UXBindingValue Value => _value; + } + [SerializeField] private string _controllerId = string.Empty; [SerializeField] private int _controllerIndex; + [SerializeField] private int _controllerIndexMask = 1; [SerializeField] private UXBindingProperty _property = UXBindingProperty.GameObjectActive; [SerializeField] private UXBindingValue _value = new UXBindingValue(); + [SerializeField] private List _indexedValues = new List(); [SerializeField] private UXBindingFallbackMode _fallbackMode = UXBindingFallbackMode.RestoreCapturedDefault; [SerializeField] private UXBindingValue _fallbackValue = new UXBindingValue(); [HideInInspector] [SerializeField] private UXBindingValue _capturedDefault = new UXBindingValue(); @@ -134,7 +151,17 @@ namespace UnityEngine.UI public int ControllerIndex { get => Mathf.Max(0, _controllerIndex); - set => _controllerIndex = Mathf.Max(0, value); + set + { + _controllerIndex = Mathf.Max(0, value); + _controllerIndexMask = IndexToMask(_controllerIndex); + } + } + + public int ControllerIndexMask + { + get => NormalizeMask(_controllerIndexMask, _controllerIndex); + set => _controllerIndexMask = value; } public UXBindingProperty Property @@ -144,6 +171,7 @@ namespace UnityEngine.UI } public UXBindingValue Value => _value; + public List IndexedValues => _indexedValues; public UXBindingFallbackMode FallbackMode { @@ -156,6 +184,13 @@ namespace UnityEngine.UI internal void Normalize() { + _controllerIndex = Mathf.Max(0, _controllerIndex); + _controllerIndexMask = NormalizeMask(_controllerIndexMask, _controllerIndex); + if (_indexedValues.Count == 0) + { + EnsureIndexedValue(_controllerIndex).Value.CopyFrom(_value); + } + if (_property != UXBindingProperty.GameObjectActive) { return; @@ -168,36 +203,86 @@ namespace UnityEngine.UI } } - internal void CaptureDefault(GameObject target) + [NonSerialized] private int _runtimeControllerSlot = -1; + [NonSerialized] private UXBindingResolvedTarget _runtimeTarget; + [NonSerialized] private bool _runtimeSupported; + + internal int RuntimeControllerSlot => _runtimeControllerSlot; + internal bool RuntimeSupported => _runtimeSupported; + + internal void BuildRuntime(GameObject target, UXController controller) { - if (target == null || !UXBindingPropertyUtility.IsSupported(target, _property)) + _runtimeControllerSlot = -1; + _runtimeSupported = UXBindingPropertyUtility.Resolve(target, _property, out _runtimeTarget); + + if (controller != null && !string.IsNullOrEmpty(_controllerId)) { + controller.TryGetControllerSlot(_controllerId, out _runtimeControllerSlot); + } + + if (!_runtimeSupported) + { + _hasCapturedDefault = false; + _capturedProperty = _property; + return; + } + + if (!_hasCapturedDefault || _capturedProperty != _property) + { + CaptureDefault(in _runtimeTarget); + } + } + + internal void CaptureDefault(GameObject target) + { + if (!UXBindingPropertyUtility.Resolve(target, _property, out UXBindingResolvedTarget resolvedTarget)) + { + _hasCapturedDefault = false; + _capturedProperty = _property; + return; + } + + CaptureDefault(in resolvedTarget); + } + + private void CaptureDefault(in UXBindingResolvedTarget target) + { + if (!UXBindingPropertyUtility.CaptureValue(in target, _property, _capturedDefault)) + { + _hasCapturedDefault = false; + _capturedProperty = _property; return; } - UXBindingPropertyUtility.CaptureValue(target, _property, _capturedDefault); _capturedProperty = _property; _hasCapturedDefault = true; } internal void CaptureCurrentAsValue(GameObject target) { - if (target == null || !UXBindingPropertyUtility.IsSupported(target, _property)) + CaptureCurrentAsValue(target, _controllerIndex); + } + + internal void CaptureCurrentAsValue(GameObject target, int selectedIndex) + { + UXBindingValue value = GetMutableValue(selectedIndex); + if (!UXBindingPropertyUtility.CaptureValue(target, _property, value)) { return; } - UXBindingPropertyUtility.CaptureValue(target, _property, _value); + if (selectedIndex == _controllerIndex) + { + _value.CopyFrom(value); + } } internal void CaptureCurrentAsFallback(GameObject target) { - if (target == null || !UXBindingPropertyUtility.IsSupported(target, _property)) + if (!UXBindingPropertyUtility.CaptureValue(target, _property, _fallbackValue)) { return; } - - UXBindingPropertyUtility.CaptureValue(target, _property, _fallbackValue); } internal void ResetToCapturedDefault(GameObject target) @@ -215,26 +300,29 @@ namespace UnityEngine.UI UXBindingPropertyUtility.ApplyValue(target, _property, _capturedDefault); } - internal void Apply(GameObject target, string controllerId, int selectedIndex) + internal void ApplyRuntime(int selectedIndex) { - if (target == null || !string.Equals(_controllerId, controllerId, StringComparison.Ordinal)) + if (!_runtimeSupported) { return; } if (!_hasCapturedDefault || _capturedProperty != _property) { - CaptureDefault(target); + CaptureDefault(in _runtimeTarget); } - if (!UXBindingPropertyUtility.IsSupported(target, _property)) + if (_property == UXBindingProperty.GameObjectActive) { - return; + if (IsSelectedIndexMatched(selectedIndex)) + { + UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, _value); + return; + } } - - if (selectedIndex == _controllerIndex) + else if (TryGetValue(selectedIndex, out UXBindingValue indexedValue)) { - UXBindingPropertyUtility.ApplyValue(target, _property, _value); + UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, indexedValue); return; } @@ -245,14 +333,90 @@ namespace UnityEngine.UI case UXBindingFallbackMode.RestoreCapturedDefault: if (_hasCapturedDefault) { - UXBindingPropertyUtility.ApplyValue(target, _property, _capturedDefault); + UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, _capturedDefault); } return; case UXBindingFallbackMode.UseCustomValue: - UXBindingPropertyUtility.ApplyValue(target, _property, _fallbackValue); + UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, _fallbackValue); return; } } + + internal bool IsSelectedIndexMatched(int selectedIndex) + { + if (selectedIndex < 0 || selectedIndex >= 31) + { + return false; + } + + return (ControllerIndexMask & IndexToMask(selectedIndex)) != 0; + } + + internal bool TryGetValue(int selectedIndex, out UXBindingValue value) + { + for (int i = 0; i < _indexedValues.Count; i++) + { + IndexedValue indexedValue = _indexedValues[i]; + if (indexedValue != null && indexedValue.Index == selectedIndex) + { + value = indexedValue.Value; + return true; + } + } + + if (selectedIndex == _controllerIndex) + { + value = _value; + return true; + } + + value = null; + return false; + } + + internal UXBindingValue GetMutableValue(int selectedIndex) + { + return EnsureIndexedValue(selectedIndex).Value; + } + + private IndexedValue EnsureIndexedValue(int selectedIndex) + { + selectedIndex = Mathf.Max(0, selectedIndex); + for (int i = 0; i < _indexedValues.Count; i++) + { + IndexedValue indexedValue = _indexedValues[i]; + if (indexedValue != null && indexedValue.Index == selectedIndex) + { + return indexedValue; + } + } + + IndexedValue nextValue = new IndexedValue(); + nextValue.Index = selectedIndex; + nextValue.Value.CopyFrom(_value); + _indexedValues.Add(nextValue); + return nextValue; + } + + internal static int IndexToMask(int index) + { + if (index < 0) + { + return 1; + } + + if (index >= 31) + { + return 1 << 30; + } + + return 1 << index; + } + + private static int NormalizeMask(int mask, int fallbackIndex) + { + return mask != 0 ? mask : IndexToMask(fallbackIndex); + } } [SerializeField] private UXController _controller; @@ -261,7 +425,8 @@ namespace UnityEngine.UI private bool _initialized; public UXController Controller => _controller; - public List Entries => _entries; + public IReadOnlyList Entries => _entries; + internal int RuntimeEntryCount => _entries.Count; public void Initialize() { @@ -274,7 +439,34 @@ namespace UnityEngine.UI NormalizeEntries(); EnsureControllerReference(); RegisterToController(); - CaptureDefaults(); + BuildRuntimeEntries(); + } + + internal void RebuildRuntime(UXController controller) + { + _controller = controller; + NormalizeEntries(); + BuildRuntimeEntries(); + } + + internal int GetRuntimeControllerSlot(int entryIndex) + { + return entryIndex >= 0 && entryIndex < _entries.Count && _entries[entryIndex] != null + ? _entries[entryIndex].RuntimeControllerSlot + : -1; + } + + internal bool IsRuntimeEntrySupported(int entryIndex) + { + return entryIndex >= 0 && entryIndex < _entries.Count && _entries[entryIndex] != null && _entries[entryIndex].RuntimeSupported; + } + + internal void ApplyRuntimeEntry(int entryIndex, int selectedIndex) + { + if (entryIndex >= 0 && entryIndex < _entries.Count && _entries[entryIndex] != null) + { + _entries[entryIndex].ApplyRuntime(selectedIndex); + } } public void SetController(UXController controller) @@ -291,6 +483,7 @@ namespace UnityEngine.UI _controller = controller; RegisterToController(); + BuildRuntimeEntries(); } public void CaptureDefaults() @@ -328,7 +521,20 @@ namespace UnityEngine.UI return; } - _controller.SetControllerIndex(entry.ControllerId, entry.ControllerIndex); + _controller.SetControllerIndex(entry.ControllerId, GetFirstSelectedIndex(entry.ControllerIndexMask)); + } + + private static int GetFirstSelectedIndex(int mask) + { + for (int i = 0; i < 31; i++) + { + if ((mask & BindingEntry.IndexToMask(i)) != 0) + { + return i; + } + } + + return 0; } public void CaptureEntryValue(int entryIndex) @@ -338,7 +544,28 @@ namespace UnityEngine.UI return; } - _entries[entryIndex].CaptureCurrentAsValue(gameObject); + _entries[entryIndex].CaptureCurrentAsValue(gameObject, GetFirstSelectedIndex(_entries[entryIndex].ControllerIndexMask)); + } + + public void CaptureEntryValue(int entryIndex, int selectedIndex) + { + if (entryIndex < 0 || entryIndex >= _entries.Count || _entries[entryIndex] == null) + { + return; + } + + _entries[entryIndex].CaptureCurrentAsValue(gameObject, selectedIndex); + } + + public void ApplyEntryValue(int entryIndex, int selectedIndex) + { + if (entryIndex < 0 || entryIndex >= _entries.Count || _entries[entryIndex] == null) + { + return; + } + + _entries[entryIndex].BuildRuntime(gameObject, _controller); + _entries[entryIndex].ApplyRuntime(selectedIndex); } public void CaptureEntryFallbackValue(int entryIndex) @@ -351,18 +578,6 @@ namespace UnityEngine.UI _entries[entryIndex].CaptureCurrentAsFallback(gameObject); } - internal void OnControllerChanged(string controllerId, int selectedIndex) - { - for (int i = 0; i < _entries.Count; i++) - { - BindingEntry entry = _entries[i]; - if (entry != null) - { - entry.Apply(gameObject, controllerId, selectedIndex); - } - } - } - private void Reset() { EnsureControllerReference(); @@ -405,6 +620,17 @@ namespace UnityEngine.UI } } + private void BuildRuntimeEntries() + { + for (int i = 0; i < _entries.Count; i++) + { + if (_entries[i] != null) + { + _entries[i].BuildRuntime(gameObject, _controller); + } + } + } + private void NormalizeEntries() { for (int i = 0; i < _entries.Count; i++) diff --git a/Runtime/UXComponent/Controller/UXBindingPropertyUtility.cs b/Runtime/UXComponent/Controller/UXBindingPropertyUtility.cs index a8958dc..78cd196 100644 --- a/Runtime/UXComponent/Controller/UXBindingPropertyUtility.cs +++ b/Runtime/UXComponent/Controller/UXBindingPropertyUtility.cs @@ -24,6 +24,18 @@ namespace UnityEngine.UI public Type ObjectReferenceType { get; } } + public struct UXBindingResolvedTarget + { + public GameObject GameObject; + public Transform Transform; + public CanvasGroup CanvasGroup; + public Graphic Graphic; + public Image Image; + public Text Text; + public TextMeshProUGUI TmpText; + public RectTransform RectTransform; + } + public static class UXBindingPropertyUtility { private static readonly UXBindingPropertyMetadata[] Metadata = @@ -46,6 +58,12 @@ namespace UnityEngine.UI public static UXBindingPropertyMetadata GetMetadata(UXBindingProperty property) { + int index = (int)property; + if ((uint)index < (uint)Metadata.Length && Metadata[index].Property == property) + { + return Metadata[index]; + } + for (int i = 0; i < Metadata.Length; i++) { if (Metadata[i].Property == property) @@ -57,13 +75,17 @@ namespace UnityEngine.UI return Metadata[0]; } - public static bool IsSupported(GameObject target, UXBindingProperty property) + public static bool Resolve(GameObject target, UXBindingProperty property, out UXBindingResolvedTarget resolvedTarget) { + resolvedTarget = default; if (target == null) { return false; } + resolvedTarget.GameObject = target; + resolvedTarget.Transform = target.transform; + switch (property) { case UXBindingProperty.GameObjectActive: @@ -73,22 +95,32 @@ namespace UnityEngine.UI case UXBindingProperty.CanvasGroupAlpha: case UXBindingProperty.CanvasGroupInteractable: case UXBindingProperty.CanvasGroupBlocksRaycasts: - return target.GetComponent() != null; + return target.TryGetComponent(out resolvedTarget.CanvasGroup); case UXBindingProperty.GraphicColor: case UXBindingProperty.GraphicMaterial: - return target.GetComponent() != null; + return target.TryGetComponent(out resolvedTarget.Graphic); case UXBindingProperty.ImageSprite: - return target.GetComponent() != null; + return target.TryGetComponent(out resolvedTarget.Image); case UXBindingProperty.TextContent: case UXBindingProperty.TextColor: - return target.GetComponent() != null || target.GetComponent() != null; + if (target.TryGetComponent(out resolvedTarget.Text)) + { + return true; + } + + return target.TryGetComponent(out resolvedTarget.TmpText); case UXBindingProperty.RectTransformAnchoredPosition: - return target.GetComponent() != null; + return target.TryGetComponent(out resolvedTarget.RectTransform); default: return false; } } + public static bool IsSupported(GameObject target, UXBindingProperty property) + { + return Resolve(target, property, out _); + } + public static void GetSupportedProperties(GameObject target, List output) { output.Clear(); @@ -107,127 +139,185 @@ namespace UnityEngine.UI } } - public static void CaptureValue(GameObject target, UXBindingProperty property, UXBindingValue destination) + public static bool CaptureValue(GameObject target, UXBindingProperty property, UXBindingValue destination) { - if (target == null || destination == null) + if (!Resolve(target, property, out UXBindingResolvedTarget resolvedTarget)) { - return; + return false; + } + + return CaptureValue(in resolvedTarget, property, destination); + } + + public static bool CaptureValue(in UXBindingResolvedTarget target, UXBindingProperty property, UXBindingValue destination) + { + if (destination == null || target.GameObject == null) + { + return false; } switch (property) { case UXBindingProperty.GameObjectActive: - destination.BoolValue = target.activeSelf; - return; + destination.BoolValue = target.GameObject.activeSelf; + return true; case UXBindingProperty.CanvasGroupAlpha: - destination.FloatValue = target.GetComponent().alpha; - return; + if (target.CanvasGroup == null) return false; + destination.FloatValue = target.CanvasGroup.alpha; + return true; case UXBindingProperty.CanvasGroupInteractable: - destination.BoolValue = target.GetComponent().interactable; - return; + if (target.CanvasGroup == null) return false; + destination.BoolValue = target.CanvasGroup.interactable; + return true; case UXBindingProperty.CanvasGroupBlocksRaycasts: - destination.BoolValue = target.GetComponent().blocksRaycasts; - return; + if (target.CanvasGroup == null) return false; + destination.BoolValue = target.CanvasGroup.blocksRaycasts; + return true; case UXBindingProperty.GraphicColor: - destination.ColorValue = target.GetComponent().color; - return; + if (target.Graphic == null) return false; + destination.ColorValue = target.Graphic.color; + return true; case UXBindingProperty.GraphicMaterial: - destination.ObjectValue = target.GetComponent().material; - return; + if (target.Graphic == null) return false; + destination.ObjectValue = target.Graphic.defaultMaterial; + return true; case UXBindingProperty.ImageSprite: - destination.ObjectValue = target.GetComponent().sprite; - return; + if (target.Image == null) return false; + destination.ObjectValue = target.Image.sprite; + return true; case UXBindingProperty.TextContent: - if (target.TryGetComponent(out Text text)) + if (target.Text != null) { - destination.StringValue = text.text; + destination.StringValue = target.Text.text; + return true; } - else if (target.TryGetComponent(out TextMeshProUGUI tmp)) + + if (target.TmpText != null) { - destination.StringValue = tmp.text; + destination.StringValue = target.TmpText.text; + return true; } - return; + + return false; case UXBindingProperty.TextColor: - if (target.TryGetComponent(out Text legacyText)) + if (target.Text != null) { - destination.ColorValue = legacyText.color; + destination.ColorValue = target.Text.color; + return true; } - else if (target.TryGetComponent(out TextMeshProUGUI tmpText)) + + if (target.TmpText != null) { - destination.ColorValue = tmpText.color; + destination.ColorValue = target.TmpText.color; + return true; } - return; + + return false; case UXBindingProperty.RectTransformAnchoredPosition: - destination.Vector2Value = target.GetComponent().anchoredPosition; - return; + if (target.RectTransform == null) return false; + destination.Vector2Value = target.RectTransform.anchoredPosition; + return true; case UXBindingProperty.TransformLocalScale: - destination.Vector3Value = target.transform.localScale; - return; + if (target.Transform == null) return false; + destination.Vector3Value = target.Transform.localScale; + return true; case UXBindingProperty.TransformLocalEulerAngles: - destination.Vector3Value = target.transform.localEulerAngles; - return; + if (target.Transform == null) return false; + destination.Vector3Value = target.Transform.localEulerAngles; + return true; + default: + return false; } } - public static void ApplyValue(GameObject target, UXBindingProperty property, UXBindingValue value) + public static bool ApplyValue(GameObject target, UXBindingProperty property, UXBindingValue value) { - if (target == null || value == null) + if (!Resolve(target, property, out UXBindingResolvedTarget resolvedTarget)) { - return; + return false; + } + + return ApplyValue(in resolvedTarget, property, value); + } + + public static bool ApplyValue(in UXBindingResolvedTarget target, UXBindingProperty property, UXBindingValue value) + { + if (value == null || target.GameObject == null) + { + return false; } switch (property) { case UXBindingProperty.GameObjectActive: - target.SetActive(value.BoolValue); - return; + target.GameObject.SetActive(value.BoolValue); + return true; case UXBindingProperty.CanvasGroupAlpha: - target.GetComponent().alpha = value.FloatValue; - return; + if (target.CanvasGroup == null) return false; + target.CanvasGroup.alpha = value.FloatValue; + return true; case UXBindingProperty.CanvasGroupInteractable: - target.GetComponent().interactable = value.BoolValue; - return; + if (target.CanvasGroup == null) return false; + target.CanvasGroup.interactable = value.BoolValue; + return true; case UXBindingProperty.CanvasGroupBlocksRaycasts: - target.GetComponent().blocksRaycasts = value.BoolValue; - return; + if (target.CanvasGroup == null) return false; + target.CanvasGroup.blocksRaycasts = value.BoolValue; + return true; case UXBindingProperty.GraphicColor: - target.GetComponent().color = value.ColorValue; - return; + if (target.Graphic == null) return false; + target.Graphic.color = value.ColorValue; + return true; case UXBindingProperty.GraphicMaterial: - target.GetComponent().material = value.ObjectValue as Material; - return; + if (target.Graphic == null) return false; + target.Graphic.material = value.ObjectValue as Material; + return true; case UXBindingProperty.ImageSprite: - target.GetComponent().sprite = value.ObjectValue as Sprite; - return; + if (target.Image == null) return false; + target.Image.sprite = value.ObjectValue as Sprite; + return true; case UXBindingProperty.TextContent: - if (target.TryGetComponent(out Text text)) + if (target.Text != null) { - text.text = value.StringValue; + target.Text.text = value.StringValue; + return true; } - else if (target.TryGetComponent(out TextMeshProUGUI tmp)) + + if (target.TmpText != null) { - tmp.text = value.StringValue; + target.TmpText.text = value.StringValue; + return true; } - return; + + return false; case UXBindingProperty.TextColor: - if (target.TryGetComponent(out Text legacyText)) + if (target.Text != null) { - legacyText.color = value.ColorValue; + target.Text.color = value.ColorValue; + return true; } - else if (target.TryGetComponent(out TextMeshProUGUI tmpText)) + + if (target.TmpText != null) { - tmpText.color = value.ColorValue; + target.TmpText.color = value.ColorValue; + return true; } - return; + + return false; case UXBindingProperty.RectTransformAnchoredPosition: - target.GetComponent().anchoredPosition = value.Vector2Value; - return; + if (target.RectTransform == null) return false; + target.RectTransform.anchoredPosition = value.Vector2Value; + return true; case UXBindingProperty.TransformLocalScale: - target.transform.localScale = value.Vector3Value; - return; + if (target.Transform == null) return false; + target.Transform.localScale = value.Vector3Value; + return true; case UXBindingProperty.TransformLocalEulerAngles: - target.transform.localEulerAngles = value.Vector3Value; - return; + if (target.Transform == null) return false; + target.Transform.localEulerAngles = value.Vector3Value; + return true; + default: + return false; } } } diff --git a/Runtime/UXComponent/Controller/UXController.cs b/Runtime/UXComponent/Controller/UXController.cs index 414e05e..4409ac4 100644 --- a/Runtime/UXComponent/Controller/UXController.cs +++ b/Runtime/UXComponent/Controller/UXController.cs @@ -17,6 +17,7 @@ namespace UnityEngine.UI [SerializeField] private string _id = string.Empty; [SerializeField] private string _name = "Controller"; [SerializeField] private int _length = 2; + [SerializeField] private int _defaultIndex; [SerializeField] private string _description = string.Empty; [NonSerialized] private int _selectedIndex = -1; [NonSerialized] private UXController _owner; @@ -41,6 +42,12 @@ namespace UnityEngine.UI set => _description = value; } + public int DefaultIndex + { + get => Mathf.Clamp(_defaultIndex, 0, Length - 1); + set => _defaultIndex = Mathf.Clamp(value, 0, Length - 1); + } + public int SelectedIndex { get => _selectedIndex; @@ -80,6 +87,15 @@ namespace UnityEngine.UI private readonly Dictionary _controllerIdMap = new Dictionary(); private readonly Dictionary _controllerNameMap = new Dictionary(); + private RuntimeBindingEntry[][] _runtimeEntriesByController = Array.Empty(); + private int[] _runtimeEntryCounts = Array.Empty(); + private bool _runtimeReady; + + private struct RuntimeBindingEntry + { + public UXBinding Binding; + public int EntryIndex; + } public IReadOnlyList Controllers { @@ -111,6 +127,18 @@ namespace UnityEngine.UI return false; } + internal bool TryGetControllerSlot(string controllerId, out int slot) + { + slot = -1; + if (string.IsNullOrEmpty(controllerId)) + { + return false; + } + + EnsureInitialized(); + return _controllerIdMap.TryGetValue(controllerId, out slot); + } + public bool TryGetControllerByName(string controllerName, out ControllerDefinition controller) { controller = null; @@ -190,7 +218,11 @@ namespace UnityEngine.UI for (int i = 0; i < _controllers.Count; i++) { - SetControllerIndexInternal(_controllers[i], 0, true); + ControllerDefinition controller = _controllers[i]; + if (controller != null) + { + SetControllerIndexInternal(controller, controller.DefaultIndex, true); + } } } @@ -209,6 +241,12 @@ namespace UnityEngine.UI if (!_bindings.Contains(binding)) { _bindings.Add(binding); + _runtimeReady = false; + if (Application.isPlaying) + { + RebuildRuntimeEntries(); + ApplyCurrentStateToBinding(binding); + } #if UNITY_EDITOR if (!Application.isPlaying) { @@ -226,6 +264,7 @@ namespace UnityEngine.UI } _bindings.Remove(binding); + _runtimeReady = false; #if UNITY_EDITOR if (!Application.isPlaying) { @@ -252,10 +291,11 @@ namespace UnityEngine.UI { if (_bindings[i] != null) { - _bindings[i].Initialize(); + _bindings[i].RebuildRuntime(this); } } + RebuildRuntimeEntries(); ResetAllControllers(); } @@ -278,7 +318,9 @@ namespace UnityEngine.UI _controllerIdMap.Clear(); _controllerNameMap.Clear(); +#if UNITY_EDITOR var usedNames = new HashSet(StringComparer.Ordinal); +#endif for (int i = 0; i < _controllers.Count; i++) { @@ -291,7 +333,9 @@ namespace UnityEngine.UI controller.EnsureId(); controller.SetOwner(this); controller.SetSelectedIndexSilently(Mathf.Clamp(controller.SelectedIndex, -1, controller.Length - 1)); + controller.DefaultIndex = controller.DefaultIndex; +#if UNITY_EDITOR if (string.IsNullOrWhiteSpace(controller.Name)) { controller.Name = $"Controller{i + 1}"; @@ -302,10 +346,13 @@ namespace UnityEngine.UI controller.Name = $"{controller.Name}_{i + 1}"; usedNames.Add(controller.Name); } +#endif _controllerIdMap[controller.Id] = i; _controllerNameMap[controller.Name] = i; } + + _runtimeReady = false; } private void CleanupBindings() @@ -333,18 +380,112 @@ namespace UnityEngine.UI } controller.SetSelectedIndexSilently(selectedIndex); - NotifyBindings(controller.Id, selectedIndex); + if (TryGetControllerSlot(controller.Id, out int slot)) + { + NotifyBindings(slot, selectedIndex); + } return true; } - private void NotifyBindings(string controllerId, int selectedIndex) + private void NotifyBindings(int controllerSlot, int selectedIndex) { - for (int i = 0; i < _bindings.Count; i++) + if (!_runtimeReady) { - UXBinding binding = _bindings[i]; + RebuildRuntimeEntries(); + } + + if ((uint)controllerSlot >= (uint)_runtimeEntriesByController.Length) + { + return; + } + + RuntimeBindingEntry[] entries = _runtimeEntriesByController[controllerSlot]; + int count = _runtimeEntryCounts[controllerSlot]; + for (int i = 0; i < count; i++) + { + UXBinding binding = entries[i].Binding; if (binding != null) { - binding.OnControllerChanged(controllerId, selectedIndex); + binding.ApplyRuntimeEntry(entries[i].EntryIndex, selectedIndex); + } + } + } + + private void RebuildRuntimeEntries() + { + int controllerCount = _controllers.Count; + if (_runtimeEntriesByController.Length != controllerCount) + { + _runtimeEntriesByController = new RuntimeBindingEntry[controllerCount][]; + _runtimeEntryCounts = new int[controllerCount]; + } + + for (int i = 0; i < controllerCount; i++) + { + _runtimeEntryCounts[i] = 0; + } + + for (int bindingIndex = 0; bindingIndex < _bindings.Count; bindingIndex++) + { + UXBinding binding = _bindings[bindingIndex]; + if (binding == null) + { + continue; + } + + binding.RebuildRuntime(this); + + for (int entryIndex = 0; entryIndex < binding.RuntimeEntryCount; entryIndex++) + { + int controllerSlot = binding.GetRuntimeControllerSlot(entryIndex); + if ((uint)controllerSlot >= (uint)controllerCount || !binding.IsRuntimeEntrySupported(entryIndex)) + { + continue; + } + + int nextIndex = _runtimeEntryCounts[controllerSlot]; + RuntimeBindingEntry[] entries = _runtimeEntriesByController[controllerSlot]; + if (entries == null || nextIndex >= entries.Length) + { + int nextLength = entries == null ? 4 : entries.Length << 1; + RuntimeBindingEntry[] nextEntries = new RuntimeBindingEntry[nextLength]; + if (entries != null) + { + Array.Copy(entries, nextEntries, entries.Length); + } + + entries = nextEntries; + _runtimeEntriesByController[controllerSlot] = entries; + } + + entries[nextIndex].Binding = binding; + entries[nextIndex].EntryIndex = entryIndex; + _runtimeEntryCounts[controllerSlot] = nextIndex + 1; + } + } + + _runtimeReady = true; + } + + private void ApplyCurrentStateToBinding(UXBinding binding) + { + if (binding == null) + { + return; + } + + for (int entryIndex = 0; entryIndex < binding.RuntimeEntryCount; entryIndex++) + { + int controllerSlot = binding.GetRuntimeControllerSlot(entryIndex); + if ((uint)controllerSlot >= (uint)_controllers.Count) + { + continue; + } + + ControllerDefinition controller = _controllers[controllerSlot]; + if (controller != null) + { + binding.ApplyRuntimeEntry(entryIndex, controller.SelectedIndex); } } } diff --git a/Runtime/UXComponent/Group/UXGroup.cs b/Runtime/UXComponent/Group/UXGroup.cs index 07ba2d3..d624028 100644 --- a/Runtime/UXComponent/Group/UXGroup.cs +++ b/Runtime/UXComponent/Group/UXGroup.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine.EventSystems; using UnityEngine; +using UnityEngine.EventSystems; namespace UnityEngine.UI { @@ -12,22 +9,34 @@ namespace UnityEngine.UI { [SerializeField] private bool m_AllowSwitchOff = false; - public bool allowSwitchOff - { - get { return m_AllowSwitchOff; } - set { m_AllowSwitchOff = value; } - } - [SerializeField] - private List m_Toggles = new List(); + private UXToggle[] m_Toggles = new UXToggle[0]; [SerializeField] private UXToggle m_DefaultToggle; + private int m_ToggleCount; + private UXToggle m_CurrentToggle; + private int m_CurrentIndex = -1; + + public bool allowSwitchOff + { + get { return m_AllowSwitchOff; } + set + { + m_AllowSwitchOff = value; + EnsureValidState(); + } + } + public UXToggle defaultToggle { get { return m_DefaultToggle; } - set { m_DefaultToggle = value; EnsureValidState(); } + set + { + m_DefaultToggle = ContainsToggle(value) ? value : null; + EnsureValidState(); + } } protected UXGroup() @@ -46,157 +55,90 @@ namespace UnityEngine.UI base.OnEnable(); } - private void ValidateToggleIsInGroup(UXToggle toggle) - { - if (toggle == null || !m_Toggles.Contains(toggle)) - throw new ArgumentException(string.Format("UXToggle {0} is not part of ToggleGroup {1}", new object[] { toggle, this })); - } - public void NotifyToggleOn(UXToggle toggle, bool sendCallback = true) { - ValidateToggleIsInGroup(toggle); - for (var i = 0; i < m_Toggles.Count; i++) + EnsureStorage(); + int index = IndexOfToggle(toggle); + if (index < 0) + return; + + m_CurrentToggle = toggle; + m_CurrentIndex = index; + + for (int i = 0; i < m_ToggleCount; i++) { - if (m_Toggles[i] == toggle) + UXToggle item = m_Toggles[i]; + if (item == null || item == toggle) continue; if (sendCallback) - m_Toggles[i].isOn = false; + item.isOn = false; else - m_Toggles[i].SetIsOnWithoutNotify(false); + item.SetIsOnWithoutNotify(false); } } public void UnregisterToggle(UXToggle toggle) { - if (toggle == null) + EnsureStorage(); + int index = IndexOfToggle(toggle); + if (index < 0) return; - if (m_Toggles.Contains(toggle)) - m_Toggles.Remove(toggle); + RemoveAt(index); if (m_DefaultToggle == toggle) - { m_DefaultToggle = null; + + if (m_CurrentToggle == toggle) + { + m_CurrentToggle = null; + m_CurrentIndex = -1; } + + EnsureSingleSelection(); } public void RegisterToggle(UXToggle toggle) { - if (toggle == null) + EnsureStorage(); + if (toggle == null || ContainsToggle(toggle)) return; - if (!m_Toggles.Contains(toggle)) - m_Toggles.Add(toggle); + EnsureCapacity(m_ToggleCount + 1); + m_Toggles[m_ToggleCount] = toggle; + m_ToggleCount++; - - if (!allowSwitchOff) - { - - var firstActive = GetFirstActiveToggle(); - if (firstActive != null && firstActive != toggle && toggle.isOn) - { - toggle.SetIsOnWithoutNotify(false); - } - else if (firstActive == null) - { - if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle)) - { - var dt = m_DefaultToggle; - if (dt != null && dt != toggle) - { - dt.isOn = true; - NotifyToggleOn(dt); - } - else if (dt == toggle) - { - toggle.isOn = true; - NotifyToggleOn(toggle); - } - } - } - } + if (toggle.isOn) + NotifyToggleOn(toggle); + else + EnsureSingleSelection(); } - public bool ContainsToggle(UXToggle toggle) { - return m_Toggles != null && m_Toggles.Contains(toggle); + EnsureStorage(); + return IndexOfToggle(toggle) >= 0; } public void EnsureValidState() { - if (m_Toggles == null) - m_Toggles = new List(); - - m_Toggles.RemoveAll(x => x == null); - - if (!allowSwitchOff && !AnyTogglesOn() && m_Toggles.Count != 0) - { - UXToggle toSelect = null; - if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle)) - { - toSelect = m_DefaultToggle; - } - else - { - toSelect = m_Toggles[0]; - } - - if (toSelect != null) - { - toSelect.isOn = true; - NotifyToggleOn(toSelect); - } - } - - IEnumerable activeToggles = ActiveToggles(); - - if (activeToggles.Count() > 1) - { - UXToggle firstActive = GetFirstActiveToggle(); - - foreach (UXToggle toggle in activeToggles) - { - if (toggle == firstActive) - { - continue; - } - - toggle.isOn = false; - } - } - - for (int i = 0; i < m_Toggles.Count; i++) - { - var t = m_Toggles[i]; - if (t == null) - continue; - - if (t.group != this) - { - t.group = this; - } - } + EnsureStorage(); + CompactNulls(); + SyncToggleGroups(); + EnsureDefaultToggle(); + EnsureSingleSelection(); } public bool AnyTogglesOn() { - return m_Toggles.Find(x => x != null && x.isOn) != null; - } - - public IEnumerable ActiveToggles() - { - return m_Toggles.Where(x => x != null && x.isOn); + return FindFirstActiveIndex() >= 0; } public UXToggle GetFirstActiveToggle() { - if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle) && m_DefaultToggle.isOn) - return m_DefaultToggle; - - IEnumerable activeToggles = ActiveToggles(); - return activeToggles.Count() > 0 ? activeToggles.First() : null; + int index = FindFirstActiveIndex(); + return index >= 0 ? m_Toggles[index] : null; } public void SetAllTogglesOff(bool sendCallback = true) @@ -204,19 +146,20 @@ namespace UnityEngine.UI bool oldAllowSwitchOff = m_AllowSwitchOff; m_AllowSwitchOff = true; - if (sendCallback) + for (int i = 0; i < m_ToggleCount; i++) { - for (var i = 0; i < m_Toggles.Count; i++) - if (m_Toggles[i] != null) - m_Toggles[i].isOn = false; - } - else - { - for (var i = 0; i < m_Toggles.Count; i++) - if (m_Toggles[i] != null) - m_Toggles[i].SetIsOnWithoutNotify(false); + UXToggle toggle = m_Toggles[i]; + if (toggle == null) + continue; + + if (sendCallback) + toggle.isOn = false; + else + toggle.SetIsOnWithoutNotify(false); } + m_CurrentToggle = null; + m_CurrentIndex = -1; m_AllowSwitchOff = oldAllowSwitchOff; } @@ -225,47 +168,229 @@ namespace UnityEngine.UI SelectAdjacent(true); } - public void Preview() + public void Previous() { SelectAdjacent(false); } - private void SelectAdjacent(bool forward) + internal UXToggle GetToggleAt(int index) { - if (m_Toggles == null || m_Toggles.Count == 0) + return index >= 0 && index < m_ToggleCount ? m_Toggles[index] : null; + } + + internal int ToggleCount + { + get { return m_ToggleCount; } + } + + private void EnsureStorage() + { + if (m_Toggles == null) + m_Toggles = new UXToggle[0]; + + if (m_ToggleCount == 0) + { + int count = 0; + for (int i = 0; i < m_Toggles.Length; i++) + { + if (m_Toggles[i] != null) + count = i + 1; + } + + m_ToggleCount = count; + } + + if (m_ToggleCount > m_Toggles.Length) + m_ToggleCount = m_Toggles.Length; + } + + private void CompactNulls() + { + int write = 0; + for (int read = 0; read < m_Toggles.Length; read++) + { + UXToggle toggle = m_Toggles[read]; + if (toggle == null) + continue; + + m_Toggles[write] = toggle; + write++; + } + + for (int i = write; i < m_Toggles.Length; i++) + m_Toggles[i] = null; + + m_ToggleCount = write; + } + + private void SyncToggleGroups() + { + for (int i = 0; i < m_ToggleCount; i++) + { + UXToggle toggle = m_Toggles[i]; + if (toggle != null && toggle.group != this) + toggle.SetToggleGroupInternal(this, true); + } + } + + private void EnsureDefaultToggle() + { + if (m_ToggleCount == 0) + { + m_DefaultToggle = null; + return; + } + + if (m_DefaultToggle != null && IndexOfToggle(m_DefaultToggle) >= 0) return; - UXToggle current = GetFirstActiveToggle(); - int currentIndex = current != null ? m_Toggles.IndexOf(current) : -1; + if (!m_AllowSwitchOff) + m_DefaultToggle = m_Toggles[0]; + else + m_DefaultToggle = null; + } - int idx = currentIndex; - if (idx == -1 && !forward) - idx = 0; - - for (int step = 0; step < m_Toggles.Count; step++) + private void EnsureSingleSelection() + { + int selectedIndex = FindSelectedIndex(); + if (selectedIndex < 0 && !m_AllowSwitchOff && m_ToggleCount > 0) { - if (forward) + selectedIndex = GetDefaultIndex(); + if (selectedIndex < 0) + selectedIndex = 0; + + UXToggle toggle = m_Toggles[selectedIndex]; + if (toggle != null) + toggle.isOn = true; + } + + if (selectedIndex >= 0) + { + m_CurrentToggle = m_Toggles[selectedIndex]; + m_CurrentIndex = selectedIndex; + + for (int i = 0; i < m_ToggleCount; i++) { - idx = (idx + 1) % m_Toggles.Count; - } - else - { - idx = (idx - 1 + m_Toggles.Count) % m_Toggles.Count; + UXToggle toggle = m_Toggles[i]; + if (toggle != null && i != selectedIndex && toggle.isOn) + toggle.SetIsOnWithoutNotify(false); } + } + else + { + m_CurrentToggle = null; + m_CurrentIndex = -1; + } + } - UXToggle t = m_Toggles[idx]; - if (t == null) + private int FindSelectedIndex() + { + int defaultIndex = GetDefaultIndex(); + if (defaultIndex >= 0 && m_Toggles[defaultIndex].isOn) + return defaultIndex; + + return FindFirstActiveIndex(); + } + + private int FindFirstActiveIndex() + { + for (int i = 0; i < m_ToggleCount; i++) + { + UXToggle toggle = m_Toggles[i]; + if (toggle != null && toggle.isOn) + return i; + } + + return -1; + } + + private int GetDefaultIndex() + { + if (m_DefaultToggle == null) + return -1; + + return IndexOfToggle(m_DefaultToggle); + } + + private int IndexOfToggle(UXToggle toggle) + { + if (toggle == null || m_Toggles == null) + return -1; + + for (int i = 0; i < m_ToggleCount; i++) + { + if (m_Toggles[i] == toggle) + return i; + } + + return -1; + } + + private void EnsureCapacity(int capacity) + { + if (m_Toggles == null) + m_Toggles = new UXToggle[capacity < 4 ? 4 : capacity]; + + if (m_Toggles.Length >= capacity) + return; + + int newCapacity = m_Toggles.Length == 0 ? 4 : m_Toggles.Length * 2; + while (newCapacity < capacity) + newCapacity *= 2; + + UXToggle[] newToggles = new UXToggle[newCapacity]; + for (int i = 0; i < m_ToggleCount; i++) + newToggles[i] = m_Toggles[i]; + + m_Toggles = newToggles; + } + + private void RemoveAt(int index) + { + int lastIndex = m_ToggleCount - 1; + for (int i = index; i < lastIndex; i++) + m_Toggles[i] = m_Toggles[i + 1]; + + if (lastIndex >= 0) + m_Toggles[lastIndex] = null; + + m_ToggleCount = lastIndex; + } + + private void SelectAdjacent(bool forward) + { + if (m_ToggleCount == 0) + return; + + int index = ResolveCurrentIndex(); + if (index < 0) + index = forward ? -1 : 0; + + for (int step = 0; step < m_ToggleCount; step++) + { + index = forward ? index + 1 : index - 1; + if (index >= m_ToggleCount) + index = 0; + else if (index < 0) + index = m_ToggleCount - 1; + + UXToggle toggle = m_Toggles[index]; + if (toggle == null || !toggle.IsActive() || !toggle.IsInteractable()) continue; - if (!t.IsActive()) - continue; - - if (!t.IsInteractable()) - continue; - - t.isOn = true; + toggle.isOn = true; return; } } + + private int ResolveCurrentIndex() + { + if (m_CurrentIndex >= 0 && m_CurrentIndex < m_ToggleCount && m_Toggles[m_CurrentIndex] == m_CurrentToggle) + return m_CurrentIndex; + + m_CurrentIndex = FindFirstActiveIndex(); + m_CurrentToggle = m_CurrentIndex >= 0 ? m_Toggles[m_CurrentIndex] : null; + return m_CurrentIndex; + } } } diff --git a/Runtime/UXComponent/Group/UXGroup.cs.meta b/Runtime/UXComponent/Group/UXGroup.cs.meta index fb0d956..7041095 100644 --- a/Runtime/UXComponent/Group/UXGroup.cs.meta +++ b/Runtime/UXComponent/Group/UXGroup.cs.meta @@ -5,7 +5,7 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {fileID: 2800000, guid: 42b2d97a2cb439b4395c6dca63357d89, type: 3} + icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: diff --git a/Runtime/UXComponent/Group/UXToggle.cs b/Runtime/UXComponent/Group/UXToggle.cs index 268118b..8f8b086 100644 --- a/Runtime/UXComponent/Group/UXToggle.cs +++ b/Runtime/UXComponent/Group/UXToggle.cs @@ -51,7 +51,7 @@ namespace UnityEngine.UI { #if UNITY_EDITOR if (executing == CanvasUpdate.Prelayout) - onValueChanged.Invoke(m_IsOn); + PlayEffect(true); #endif } @@ -66,13 +66,24 @@ namespace UnityEngine.UI protected override void OnDestroy() { if (m_Group != null) - m_Group.EnsureValidState(); + m_Group.UnregisterToggle(this); base.OnDestroy(); } + protected override void OnDisable() + { + if (m_Group != null) + m_Group.UnregisterToggle(this); + + base.OnDisable(); + } + protected override void OnEnable() { base.OnEnable(); + if (m_Group != null) + SetToggleGroup(m_Group, false); + PlayEffect(true); } @@ -92,7 +103,13 @@ namespace UnityEngine.UI } - private void SetToggleGroup(UXGroup newGroup, bool setMemberValue) + internal void SetToggleGroupInternal(UXGroup newGroup, bool setMemberValue) + { + if (setMemberValue) + m_Group = newGroup; + } + + protected virtual void SetToggleGroup(UXGroup newGroup, bool setMemberValue) { if (m_Group == newGroup) { @@ -155,11 +172,13 @@ namespace UnityEngine.UI base.DoStateTransition(state, instant); } - void Set(bool value, bool sendCallback = true) + protected virtual void Set(bool value, bool sendCallback = true) { if (m_IsOn == value) return; + OnBeforeValueChanged(value); + m_IsOn = value; if (m_Group != null && m_Group.isActiveAndEnabled && IsActive()) { @@ -182,20 +201,33 @@ namespace UnityEngine.UI var stateToApply = m_IsOn ? Selectable.SelectionState.Selected : currentSelectionState; DoStateTransition(stateToApply, instant); + OnAfterValueChanged(m_IsOn); } + protected virtual void OnBeforeValueChanged(bool value) + { + } - private void PlayEffect(bool instant) + protected virtual void OnAfterValueChanged(bool value) + { + } + + protected virtual void PlayEffect(bool instant) { if (graphic == null) return; + float alpha = m_IsOn ? 1f : 0f; + #if UNITY_EDITOR if (!Application.isPlaying) - graphic.canvasRenderer.SetAlpha(m_IsOn ? 1f : 0f); + graphic.canvasRenderer.SetAlpha(alpha); else #endif - graphic.CrossFadeAlpha(m_IsOn ? 1f : 0f, instant ? 0f : 0.1f, true); + if (instant) + graphic.canvasRenderer.SetAlpha(alpha); + else + graphic.CrossFadeAlpha(alpha, 0.1f, true); } @@ -230,6 +262,9 @@ namespace UnityEngine.UI public override void OnSelect(BaseEventData eventData) { base.OnSelect(eventData); +#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION + UXNavigationRuntime.NotifySelection(gameObject); +#endif if (eventData is PointerEventData) return; PlayAudio(hoverAudioClip); diff --git a/Runtime/UXComponent/Hotkey/HotkeyComponent.cs b/Runtime/UXComponent/Hotkey/HotkeyComponent.cs index 0d17b96..b47e38c 100644 --- a/Runtime/UXComponent/Hotkey/HotkeyComponent.cs +++ b/Runtime/UXComponent/Hotkey/HotkeyComponent.cs @@ -1,4 +1,5 @@ #if INPUTSYSTEM_SUPPORT +using AlicizaX.UI.Runtime; using UnityEngine.EventSystems; using UnityEngine.InputSystem; @@ -8,29 +9,42 @@ namespace UnityEngine.UI public sealed class HotkeyComponent : MonoBehaviour, IHotkeyTrigger { [SerializeField] private Component _component; + [SerializeField] private UIHolderObjectBase _holder; [SerializeField] private InputActionReference _hotkeyAction; [SerializeField] private EHotkeyPressType _hotkeyPressType = EHotkeyPressType.Performed; + private ISubmitHandler _submitHandler; + private BaseEventData _eventData; + private EventSystem _eventSystem; + public InputActionReference HotkeyAction { get => _hotkeyAction; set => _hotkeyAction = value; } - EHotkeyPressType IHotkeyTrigger.HotkeyPressType - { - get => _hotkeyPressType; - set => _hotkeyPressType = value; - } + public EHotkeyPressType HotkeyPressType => _hotkeyPressType; + public UIHolderObjectBase HotkeyHolder => _holder; private void Reset() { AutoAssignTarget(); + AutoAssignHolder(); + } + + private void Awake() + { + AutoAssignTarget(); + AutoAssignHolder(); + CacheTarget(); + CacheEventData(); } private void OnEnable() { AutoAssignTarget(); + AutoAssignHolder(); + CacheTarget(); ((IHotkeyTrigger)this).BindHotKey(); } @@ -39,36 +53,54 @@ namespace UnityEngine.UI ((IHotkeyTrigger)this).UnBindHotKey(); } + private void OnApplicationFocus(bool hasFocus) + { + if (hasFocus) + { + CacheEventData(); + } + } + private void OnDestroy() { ((IHotkeyTrigger)this).UnBindHotKey(); + _submitHandler = null; + _eventData = null; + _eventSystem = null; } #if UNITY_EDITOR private void OnValidate() { AutoAssignTarget(); + AutoAssignHolder(); + CacheTarget(); + + if (_component != null && _submitHandler == null) + { + _component = null; + } } #endif - void IHotkeyTrigger.HotkeyActionTrigger() + public void HotkeyActionTrigger() { - if (!isActiveAndEnabled || _component == null) + if (!isActiveAndEnabled || _submitHandler == null) { return; } - if (_component is ISubmitHandler) + if (_eventData == null) { - ExecuteEvents.Execute( - _component.gameObject, - new BaseEventData(EventSystem.current), - ExecuteEvents.submitHandler - ); return; } - Debug.LogWarning($"{nameof(HotkeyComponent)} target must implement {nameof(ISubmitHandler)}: {_component.name}", this); + if (!ReferenceEquals(_eventSystem, EventSystem.current)) + { + return; + } + + _submitHandler.OnSubmit(_eventData); } private void AutoAssignTarget() @@ -83,6 +115,27 @@ namespace UnityEngine.UI _component = submitHandler; } } + + private void AutoAssignHolder() + { + if (_holder != null && _holder.IsValid()) + { + return; + } + + _holder = GetComponentInParent(true); + } + + private void CacheTarget() + { + _submitHandler = _component as ISubmitHandler; + } + + private void CacheEventData() + { + _eventSystem = EventSystem.current; + _eventData = new BaseEventData(_eventSystem); + } } } #endif diff --git a/Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs b/Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs index 3241970..822cd04 100644 --- a/Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs +++ b/Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs @@ -1,13 +1,15 @@ #if INPUTSYSTEM_SUPPORT +using AlicizaX.UI.Runtime; using UnityEngine.InputSystem; namespace UnityEngine.UI { public interface IHotkeyTrigger { - public InputActionReference HotkeyAction { get; } - internal EHotkeyPressType HotkeyPressType { get; set; } - internal void HotkeyActionTrigger(); + InputActionReference HotkeyAction { get; } + EHotkeyPressType HotkeyPressType { get; } + UIHolderObjectBase HotkeyHolder { get; } + void HotkeyActionTrigger(); } } diff --git a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs index 0cd1921..263db04 100644 --- a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs +++ b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs @@ -1,6 +1,5 @@ #if INPUTSYSTEM_SUPPORT using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; using AlicizaX.UI.Runtime; using UnityEngine; @@ -8,7 +7,7 @@ using UnityEngine.InputSystem; namespace UnityEngine.UI { - internal enum EHotkeyPressType : byte + public enum EHotkeyPressType : byte { Started = 0, Performed = 1 @@ -17,13 +16,219 @@ namespace UnityEngine.UI internal readonly struct HotkeyRegistration { public readonly IHotkeyTrigger Trigger; - public readonly EHotkeyPressType PressType; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HotkeyRegistration(IHotkeyTrigger trigger, EHotkeyPressType pressType) + public HotkeyRegistration(IHotkeyTrigger trigger) { Trigger = trigger; - PressType = pressType; + } + } + + internal sealed class HotkeyActionRegistrations + { + public readonly HotkeyRegistrationList StartedRegistrations = new(); + public readonly HotkeyRegistrationList PerformedRegistrations = new(); + + public bool IsEmpty => StartedRegistrations.Count == 0 && PerformedRegistrations.Count == 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HotkeyRegistrationList GetRegistrations(EHotkeyPressType pressType) + { + return pressType == EHotkeyPressType.Started ? StartedRegistrations : PerformedRegistrations; + } + } + + internal sealed class HotkeyRegistrationList + { + private HotkeyRegistration[] _items = Array.Empty(); + + public int Count { get; private set; } + + public HotkeyRegistration this[int index] + { + get => _items[index]; + set => _items[index] = value; + } + + public void Add(HotkeyRegistration registration) + { + if (Count == _items.Length) + { + int newLength = _items.Length == 0 ? 2 : _items.Length << 1; + Array.Resize(ref _items, newLength); + } + + _items[Count++] = registration; + } + + public void RemoveLast() + { + Count--; + _items[Count] = default; + } + + public void Clear() + { + for (int i = 0; i < Count; i++) + { + _items[i] = default; + } + + Count = 0; + } + } + + internal sealed class ReferenceMap where TKey : class + { + private int[] _buckets = Array.Empty(); + private int[] _next = Array.Empty(); + private TKey[] _keys = Array.Empty(); + private TValue[] _values = Array.Empty(); + private int _slotCount; + + public int Count { get; private set; } + public int SlotCount => _slotCount; + + public bool TryGetValue(TKey key, out TValue value) + { + if (key != null && _buckets.Length > 0) + { + int bucket = GetBucket(key, _buckets.Length); + for (int i = _buckets[bucket] - 1; i >= 0; i = _next[i]) + { + if (ReferenceEquals(_keys[i], key)) + { + value = _values[i]; + return true; + } + } + } + + value = default; + return false; + } + + public void Set(TKey key, TValue value) + { + if (key == null) + { + return; + } + + EnsureCapacity(_slotCount + 1); + int bucket = GetBucket(key, _buckets.Length); + for (int i = _buckets[bucket] - 1; i >= 0; i = _next[i]) + { + if (ReferenceEquals(_keys[i], key)) + { + _values[i] = value; + return; + } + } + + int index = _slotCount++; + _keys[index] = key; + _values[index] = value; + _next[index] = _buckets[bucket] - 1; + _buckets[bucket] = index + 1; + Count++; + } + + public bool Remove(TKey key) + { + if (key == null || _buckets.Length == 0) + { + return false; + } + + int bucket = GetBucket(key, _buckets.Length); + int previous = -1; + for (int i = _buckets[bucket] - 1; i >= 0; i = _next[i]) + { + if (!ReferenceEquals(_keys[i], key)) + { + previous = i; + continue; + } + + if (previous < 0) + { + _buckets[bucket] = _next[i] + 1; + } + else + { + _next[previous] = _next[i]; + } + + _keys[i] = null; + _values[i] = default; + _next[i] = -1; + Count--; + return true; + } + + return false; + } + + public void Clear() + { + Array.Clear(_buckets, 0, _buckets.Length); + Array.Clear(_next, 0, _next.Length); + Array.Clear(_keys, 0, _keys.Length); + Array.Clear(_values, 0, _values.Length); + _slotCount = 0; + Count = 0; + } + + public bool TryGetValueAtSlot(int slot, out TValue value) + { + if ((uint)slot < (uint)_slotCount && _keys[slot] != null) + { + value = _values[slot]; + return true; + } + + value = default; + return false; + } + + private void EnsureCapacity(int capacity) + { + if (_buckets.Length >= capacity) + { + return; + } + + int newLength = _buckets.Length == 0 ? 8 : _buckets.Length << 1; + while (newLength < capacity) + { + newLength <<= 1; + } + + TKey[] oldKeys = _keys; + TValue[] oldValues = _values; + int oldSlotCount = _slotCount; + + _buckets = new int[newLength]; + _next = new int[newLength]; + _keys = new TKey[newLength]; + _values = new TValue[newLength]; + _slotCount = 0; + Count = 0; + + for (int i = 0; i < oldSlotCount; i++) + { + if (oldKeys[i] != null) + { + Set(oldKeys[i], oldValues[i]); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBucket(TKey key, int length) + { + return (RuntimeHelpers.GetHashCode(key) & int.MaxValue) & (length - 1); } } @@ -33,23 +238,17 @@ namespace UnityEngine.UI { Holder = holder; HierarchyDepth = GetHierarchyDepth(holder.transform); - ParentHolder = FindParentHolder(holder); - BlocksLowerScopes = ParentHolder == null; + ParentHolder = UXHotkeyRegisterManager.FindParentHolder(holder); } public readonly UIHolderObjectBase Holder; public readonly UIHolderObjectBase ParentHolder; public readonly int HierarchyDepth; - public readonly bool BlocksLowerScopes; - public readonly Dictionary> RegistrationsByAction = new(StringComparer.Ordinal); + public readonly ReferenceMap RegistrationsByAction = new(); public bool LifecycleActive; public ulong ActivationSerial; - public Action OnBeforeShow; - public Action OnBeforeClosed; - public Action OnDestroy; - private Canvas _canvas; public Canvas Canvas @@ -65,6 +264,21 @@ namespace UnityEngine.UI } } + public void OnBeforeShowHandler() + { + UXHotkeyRegisterManager.ActivateScope(Holder); + } + + public void OnBeforeClosedHandler() + { + UXHotkeyRegisterManager.DeactivateScope(Holder); + } + + public void OnDestroyHandler() + { + UXHotkeyRegisterManager.DestroyScope(Holder); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetHierarchyDepth(Transform current) { @@ -77,9 +291,191 @@ namespace UnityEngine.UI return depth; } + } + + internal sealed class ActionRegistrationBucket + { + public InputAction Action; + public int StartedCount; + public int PerformedCount; + public bool WasEnabledBeforeHotkey; + public bool EnabledByHotkeySystem; + + public int TotalCount => StartedCount + PerformedCount; + } + + internal readonly struct TriggerRegistration + { + public readonly InputAction Action; + public readonly UIHolderObjectBase Holder; + public readonly EHotkeyPressType PressType; + public readonly int Index; + public readonly IHotkeyTrigger Trigger; + + public TriggerRegistration(InputAction action, UIHolderObjectBase holder, EHotkeyPressType pressType, int index, IHotkeyTrigger trigger) + { + Action = action; + Holder = holder; + PressType = pressType; + Index = index; + Trigger = trigger; + } + } + + internal static class UXHotkeyRegisterManager + { + private static readonly ReferenceMap _actions = new(); + private static readonly ReferenceMap _triggerMap = new(); + private static readonly ReferenceMap _scopes = new(); + private static readonly ReferenceMap _ancestorHolders = new(); + private static IHotkeyTrigger[] _destroyScopeTriggers = Array.Empty(); + private static int _destroyScopeTriggerCount; + private static readonly Action _startedHandler = OnActionStarted; + private static readonly Action _performedHandler = OnActionPerformed; + + private static ulong _serialCounter; + private static HotkeyScope _topLeafScope; + private static bool _isDestroyingScope; + +#if UNITY_EDITOR + [UnityEditor.Callbacks.DidReloadScripts] + internal static void ClearHotkeyRegistry() + { + IHotkeyTrigger[] triggers = new IHotkeyTrigger[_triggerMap.Count]; + int index = 0; + for (int i = 0; i < _triggerMap.SlotCount; i++) + { + if (_triggerMap.TryGetValueAtSlot(i, out var registration)) + { + triggers[index++] = registration.Trigger; + } + } + + for (int i = 0; i < triggers.Length; i++) + { + UnregisterHotkey(triggers[i]); + } + + _actions.Clear(); + _triggerMap.Clear(); + _scopes.Clear(); + _ancestorHolders.Clear(); + _destroyScopeTriggerCount = 0; + _serialCounter = 0; + _topLeafScope = null; + _isDestroyingScope = false; + RebuildTopLeafScopeImmediate(); + } +#endif [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static UIHolderObjectBase FindParentHolder(UIHolderObjectBase holder) + internal static void RegisterHotkey(IHotkeyTrigger trigger, UIHolderObjectBase holder, InputActionReference action, EHotkeyPressType pressType) + { + if (trigger == null || holder == null || action == null || action.action == null) + { + return; + } + + UnregisterHotkey(trigger); + + InputAction inputAction = action.action; + ActionRegistrationBucket bucket = GetOrCreateBucket(inputAction); + + HotkeyScope scope = GetOrCreateScope(holder); + int index = AddScopeRegistration(scope, inputAction, trigger, pressType); + AdjustBucketSubscription(bucket, pressType, true); + + if (scope.LifecycleActive) + { + scope.ActivationSerial = ++_serialCounter; + RebuildTopLeafScopeImmediate(); + } + + _triggerMap.Set(trigger, new TriggerRegistration(inputAction, holder, pressType, index, trigger)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void UnregisterHotkey(IHotkeyTrigger trigger) + { + if (trigger == null || !_triggerMap.TryGetValue(trigger, out var triggerRegistration)) + { + return; + } + + if (!_scopes.TryGetValue(triggerRegistration.Holder, out var scope)) + { +#if UNITY_EDITOR || DEVELOPMENT_BUILD + Debug.LogError("Hotkey registry is inconsistent: scope missing during unregister."); +#endif + return; + } + + if (!RemoveScopeRegistration(scope, triggerRegistration.Action, triggerRegistration.PressType, triggerRegistration.Index, trigger)) + { +#if UNITY_EDITOR || DEVELOPMENT_BUILD + Debug.LogError("Hotkey registry is inconsistent: trigger slot missing during unregister."); +#endif + return; + } + + _triggerMap.Remove(trigger); + if (_actions.TryGetValue(triggerRegistration.Action, out var bucket)) + { + RemoveActionRegistration(bucket, triggerRegistration.PressType, triggerRegistration.Action); + } + + ReleaseScopeIfEmpty(scope); + } + + internal static void ActivateScope(UIHolderObjectBase holder) + { + if (_scopes.TryGetValue(holder, out var scope)) + { + scope.LifecycleActive = true; + scope.ActivationSerial = ++_serialCounter; + RebuildTopLeafScopeImmediate(); + } + } + + internal static void DeactivateScope(UIHolderObjectBase holder) + { + if (_scopes.TryGetValue(holder, out var scope)) + { + scope.LifecycleActive = false; + RebuildTopLeafScopeImmediate(); + } + } + + internal static void DestroyScope(UIHolderObjectBase holder) + { + if (holder == null || _isDestroyingScope || !_scopes.TryGetValue(holder, out var scope)) + { + return; + } + + _isDestroyingScope = true; + _destroyScopeTriggerCount = 0; + for (int i = 0; i < scope.RegistrationsByAction.SlotCount; i++) + { + if (scope.RegistrationsByAction.TryGetValueAtSlot(i, out var actionRegistrations)) + { + CollectTriggers(actionRegistrations.StartedRegistrations); + CollectTriggers(actionRegistrations.PerformedRegistrations); + } + } + + for (int i = 0; i < _destroyScopeTriggerCount; i++) + { + UnregisterHotkey(_destroyScopeTriggers[i]); + } + + Array.Clear(_destroyScopeTriggers, 0, _destroyScopeTriggerCount); + _destroyScopeTriggerCount = 0; + DetachScope(scope); + _isDestroyingScope = false; + } + + internal static UIHolderObjectBase FindParentHolder(UIHolderObjectBase holder) { if (holder == null) { @@ -99,131 +495,29 @@ namespace UnityEngine.UI return null; } - } - - internal sealed class ActionRegistrationBucket - { - public InputActionReference ActionReference; - public Action StartedHandler; - public Action PerformedHandler; - public int StartedCount; - public int PerformedCount; - - public int TotalCount => StartedCount + PerformedCount; - } - - internal readonly struct TriggerRegistration - { - public readonly string ActionId; - public readonly UIHolderObjectBase Holder; - public readonly EHotkeyPressType PressType; - - public TriggerRegistration(string actionId, UIHolderObjectBase holder, EHotkeyPressType pressType) - { - ActionId = actionId; - Holder = holder; - PressType = pressType; - } - } - - internal static class UXHotkeyRegisterManager - { - private static readonly Dictionary _actions = new(StringComparer.Ordinal); - private static readonly Dictionary _triggerMap = new(); - private static readonly Dictionary _scopes = new(); - private static readonly List _leafScopes = new(); - private static readonly HashSet _ancestorHolders = new(); - - private static ulong _serialCounter; - -#if UNITY_EDITOR - [UnityEditor.Callbacks.DidReloadScripts] - internal static void ClearHotkeyRegistry() - { - IHotkeyTrigger[] triggers = new IHotkeyTrigger[_triggerMap.Count]; - int index = 0; - foreach (var kvp in _triggerMap) - { - triggers[index++] = kvp.Key; - } - - for (int i = 0; i < triggers.Length; i++) - { - UnregisterHotkey(triggers[i]); - } - - _actions.Clear(); - _triggerMap.Clear(); - _scopes.Clear(); - _leafScopes.Clear(); - _ancestorHolders.Clear(); - _serialCounter = 0; - } -#endif [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void RegisterHotkey(IHotkeyTrigger trigger, UIHolderObjectBase holder, InputActionReference action, EHotkeyPressType pressType) + private static void OnActionStarted(InputAction.CallbackContext context) { - if (trigger == null || holder == null || action == null || action.action == null) - { - return; - } - - UnregisterHotkey(trigger); - - string actionId = action.action.id.ToString(); - HotkeyScope scope = GetOrCreateScope(holder); - ActionRegistrationBucket bucket = GetOrCreateBucket(actionId, action); - HotkeyRegistration registration = new HotkeyRegistration(trigger, pressType); - - AdjustBucketSubscription(bucket, pressType, true); - AddScopeRegistration(scope, actionId, registration); - - if (scope.LifecycleActive) - { - scope.ActivationSerial = ++_serialCounter; - } - - _triggerMap[trigger] = new TriggerRegistration(actionId, holder, pressType); + Dispatch(context.action, EHotkeyPressType.Started); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void UnregisterHotkey(IHotkeyTrigger trigger) + private static void OnActionPerformed(InputAction.CallbackContext context) { - if (trigger == null || !_triggerMap.TryGetValue(trigger, out var triggerRegistration)) - { - return; - } - - if (_actions.TryGetValue(triggerRegistration.ActionId, out var bucket)) - { - RemoveActionRegistration(bucket, triggerRegistration.PressType, triggerRegistration.ActionId); - } - - if (_scopes.TryGetValue(triggerRegistration.Holder, out var scope)) - { - RemoveScopeRegistration(scope, triggerRegistration.ActionId, trigger); - ReleaseScopeIfEmpty(scope); - } - - _triggerMap.Remove(trigger); + Dispatch(context.action, EHotkeyPressType.Performed); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ActionRegistrationBucket GetOrCreateBucket(string actionId, InputActionReference action) + private static ActionRegistrationBucket GetOrCreateBucket(InputAction inputAction) { - if (_actions.TryGetValue(actionId, out var bucket)) + if (_actions.TryGetValue(inputAction, out var bucket)) { return bucket; } - bucket = new ActionRegistrationBucket - { - ActionReference = action, - StartedHandler = _ => Dispatch(actionId, EHotkeyPressType.Started), - PerformedHandler = _ => Dispatch(actionId, EHotkeyPressType.Performed) - }; - _actions[actionId] = bucket; + bucket = new ActionRegistrationBucket { Action = inputAction }; + _actions.Set(inputAction, bucket); return bucket; } @@ -236,68 +530,19 @@ namespace UnityEngine.UI scope = new HotkeyScope(holder) { - LifecycleActive = IsHolderVisible(holder), ActivationSerial = ++_serialCounter }; + scope.LifecycleActive = IsScopeVisible(scope); - scope.OnBeforeShow = () => ActivateScope(holder); - scope.OnBeforeClosed = () => DeactivateScope(holder); - scope.OnDestroy = () => DestroyScope(holder); + holder.OnWindowBeforeShowEvent += scope.OnBeforeShowHandler; + holder.OnWindowBeforeClosedEvent += scope.OnBeforeClosedHandler; + holder.OnWindowDestroyEvent += scope.OnDestroyHandler; - holder.OnWindowBeforeShowEvent += scope.OnBeforeShow; - holder.OnWindowBeforeClosedEvent += scope.OnBeforeClosed; - holder.OnWindowDestroyEvent += scope.OnDestroy; - - _scopes[holder] = scope; + _scopes.Set(holder, scope); + RebuildTopLeafScopeImmediate(); return scope; } - private static void ActivateScope(UIHolderObjectBase holder) - { - if (_scopes.TryGetValue(holder, out var scope)) - { - scope.LifecycleActive = true; - scope.ActivationSerial = ++_serialCounter; - } - } - - private static void DeactivateScope(UIHolderObjectBase holder) - { - if (_scopes.TryGetValue(holder, out var scope)) - { - scope.LifecycleActive = false; - } - } - - private static void DestroyScope(UIHolderObjectBase holder) - { - if (holder == null || !_scopes.TryGetValue(holder, out var scope)) - { - return; - } - - List triggers = null; - foreach (var pair in scope.RegistrationsByAction) - { - List registrations = pair.Value; - for (int i = 0; i < registrations.Count; i++) - { - triggers ??= new List(registrations.Count); - triggers.Add(registrations[i].Trigger); - } - } - - if (triggers != null) - { - for (int i = 0; i < triggers.Count; i++) - { - UnregisterHotkey(triggers[i]); - } - } - - DetachScope(scope); - } - private static void DetachScope(HotkeyScope scope) { if (scope == null || scope.Holder == null) @@ -305,10 +550,11 @@ namespace UnityEngine.UI return; } - scope.Holder.OnWindowBeforeShowEvent -= scope.OnBeforeShow; - scope.Holder.OnWindowBeforeClosedEvent -= scope.OnBeforeClosed; - scope.Holder.OnWindowDestroyEvent -= scope.OnDestroy; + scope.Holder.OnWindowBeforeShowEvent -= scope.OnBeforeShowHandler; + scope.Holder.OnWindowBeforeClosedEvent -= scope.OnBeforeClosedHandler; + scope.Holder.OnWindowDestroyEvent -= scope.OnDestroyHandler; _scopes.Remove(scope.Holder); + RebuildTopLeafScopeImmediate(); } private static void ReleaseScopeIfEmpty(HotkeyScope scope) @@ -319,56 +565,90 @@ namespace UnityEngine.UI } } - private static void AddScopeRegistration(HotkeyScope scope, string actionId, HotkeyRegistration registration) + private static int AddScopeRegistration(HotkeyScope scope, InputAction action, IHotkeyTrigger trigger, EHotkeyPressType pressType) { - if (!scope.RegistrationsByAction.TryGetValue(actionId, out var registrations)) + if (!scope.RegistrationsByAction.TryGetValue(action, out var actionRegistrations)) { - registrations = new List(); - scope.RegistrationsByAction[actionId] = registrations; + actionRegistrations = new HotkeyActionRegistrations(); + scope.RegistrationsByAction.Set(action, actionRegistrations); } - registrations.Add(registration); + HotkeyRegistrationList registrations = actionRegistrations.GetRegistrations(pressType); + int index = registrations.Count; + registrations.Add(new HotkeyRegistration(trigger)); + return index; } - private static void RemoveScopeRegistration(HotkeyScope scope, string actionId, IHotkeyTrigger trigger) + private static bool RemoveScopeRegistration(HotkeyScope scope, InputAction action, EHotkeyPressType pressType, int index, IHotkeyTrigger expectedTrigger) { - if (!scope.RegistrationsByAction.TryGetValue(actionId, out var registrations)) + if (!scope.RegistrationsByAction.TryGetValue(action, out var actionRegistrations)) + { + return false; + } + + HotkeyRegistrationList registrations = actionRegistrations.GetRegistrations(pressType); + if ((uint)index >= (uint)registrations.Count) + { + return false; + } + + int lastIndex = registrations.Count - 1; + HotkeyRegistration removedRegistration = registrations[index]; + if (!ReferenceEquals(removedRegistration.Trigger, expectedTrigger)) + { + return false; + } + + if (index != lastIndex) + { + HotkeyRegistration movedRegistration = registrations[lastIndex]; + registrations[index] = movedRegistration; + UpdateTriggerIndex(movedRegistration.Trigger, index); + } + + registrations.RemoveLast(); + + if (actionRegistrations.IsEmpty) + { + scope.RegistrationsByAction.Remove(action); + } + + return true; + } + + private static void UpdateTriggerIndex(IHotkeyTrigger trigger, int index) + { + if (trigger == null || !_triggerMap.TryGetValue(trigger, out var registration)) { return; } - for (int i = registrations.Count - 1; i >= 0; i--) - { - if (ReferenceEquals(registrations[i].Trigger, trigger)) - { - registrations.RemoveAt(i); - break; - } - } - - if (registrations.Count == 0) - { - scope.RegistrationsByAction.Remove(actionId); - } + _triggerMap.Set(trigger, new TriggerRegistration(registration.Action, registration.Holder, registration.PressType, index, trigger)); } - private static void RemoveActionRegistration(ActionRegistrationBucket bucket, EHotkeyPressType pressType, string actionId) + private static void RemoveActionRegistration(ActionRegistrationBucket bucket, EHotkeyPressType pressType, InputAction action) { AdjustBucketSubscription(bucket, pressType, false); if (bucket.TotalCount == 0) { - _actions.Remove(actionId); + _actions.Remove(action); } } private static void AdjustBucketSubscription(ActionRegistrationBucket bucket, EHotkeyPressType pressType, bool add) { - InputAction inputAction = bucket.ActionReference != null ? bucket.ActionReference.action : null; + InputAction inputAction = bucket.Action; if (inputAction == null) { return; } + int countBefore = bucket.TotalCount; + if (add && countBefore == 0) + { + bucket.WasEnabledBeforeHotkey = inputAction.enabled; + } + switch (pressType) { case EHotkeyPressType.Started: @@ -376,7 +656,7 @@ namespace UnityEngine.UI { if (bucket.StartedCount == 0) { - inputAction.started += bucket.StartedHandler; + inputAction.started += _startedHandler; } bucket.StartedCount++; @@ -386,7 +666,7 @@ namespace UnityEngine.UI bucket.StartedCount--; if (bucket.StartedCount == 0) { - inputAction.started -= bucket.StartedHandler; + inputAction.started -= _startedHandler; } } @@ -396,7 +676,7 @@ namespace UnityEngine.UI { if (bucket.PerformedCount == 0) { - inputAction.performed += bucket.PerformedHandler; + inputAction.performed += _performedHandler; } bucket.PerformedCount++; @@ -406,41 +686,43 @@ namespace UnityEngine.UI bucket.PerformedCount--; if (bucket.PerformedCount == 0) { - inputAction.performed -= bucket.PerformedHandler; + inputAction.performed -= _performedHandler; } } break; } - if (bucket.TotalCount > 0) + if (add && countBefore == 0 && !bucket.WasEnabledBeforeHotkey) { inputAction.Enable(); + bucket.EnabledByHotkeySystem = true; } - else + else if (!add && bucket.TotalCount == 0 && bucket.EnabledByHotkeySystem && !bucket.WasEnabledBeforeHotkey) { inputAction.Disable(); + bucket.EnabledByHotkeySystem = false; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Dispatch(string actionId, EHotkeyPressType pressType) + private static void Dispatch(InputAction action, EHotkeyPressType pressType) { - HotkeyScope leafScope = GetTopLeafScope(); + HotkeyScope leafScope = _topLeafScope; if (leafScope == null) { return; } - TryDispatchToScopeChain(leafScope, actionId, pressType); + TryDispatchToScopeChain(leafScope, action, pressType); } - private static bool TryDispatchToScopeChain(HotkeyScope leafScope, string actionId, EHotkeyPressType pressType) + private static bool TryDispatchToScopeChain(HotkeyScope leafScope, InputAction action, EHotkeyPressType pressType) { HotkeyScope current = leafScope; while (current != null) { - if (TryGetLatestRegistration(current, actionId, pressType, out var registration)) + if (TryGetLatestRegistration(current, action, pressType, out var registration)) { registration.Trigger?.HotkeyActionTrigger(); return true; @@ -455,18 +737,15 @@ namespace UnityEngine.UI return false; } - private static bool TryGetLatestRegistration(HotkeyScope scope, string actionId, EHotkeyPressType pressType, out HotkeyRegistration registration) + private static bool TryGetLatestRegistration(HotkeyScope scope, InputAction action, EHotkeyPressType pressType, out HotkeyRegistration registration) { - if (scope.RegistrationsByAction.TryGetValue(actionId, out var registrations)) + if (scope.RegistrationsByAction.TryGetValue(action, out var actionRegistrations)) { - for (int i = registrations.Count - 1; i >= 0; i--) + HotkeyRegistrationList registrations = actionRegistrations.GetRegistrations(pressType); + if (registrations.Count > 0) { - HotkeyRegistration candidate = registrations[i]; - if (candidate.PressType == pressType && candidate.Trigger != null) - { - registration = candidate; - return true; - } + registration = registrations[registrations.Count - 1]; + return registration.Trigger != null; } } @@ -474,16 +753,25 @@ namespace UnityEngine.UI return false; } - private static HotkeyScope GetTopLeafScope() + private static void RebuildTopLeafScopeImmediate() + { + _topLeafScope = RebuildTopLeafScope(); + } + + private static HotkeyScope RebuildTopLeafScope() { - _leafScopes.Clear(); _ancestorHolders.Clear(); - foreach (var scope in _scopes.Values) + for (int i = 0; i < _scopes.SlotCount; i++) { + if (!_scopes.TryGetValueAtSlot(i, out var scope)) + { + continue; + } + if (!IsScopeActive(scope) #if UX_NAVIGATION - || !UXNavigationRuntime.IsHolderWithinTopScope(scope.Holder) + || !UXNavigationRuntime.IsHolderWithinTopScope(scope.Holder) #endif ) { @@ -493,7 +781,7 @@ namespace UnityEngine.UI UIHolderObjectBase parentHolder = scope.ParentHolder; while (parentHolder != null) { - _ancestorHolders.Add(parentHolder); + _ancestorHolders.Set(parentHolder, parentHolder); if (_scopes.TryGetValue(parentHolder, out var parentScope)) { parentHolder = parentScope.ParentHolder; @@ -505,25 +793,26 @@ namespace UnityEngine.UI } } - foreach (var scope in _scopes.Values) + HotkeyScope bestScope = null; + for (int i = 0; i < _scopes.SlotCount; i++) { + if (!_scopes.TryGetValueAtSlot(i, out var scope)) + { + continue; + } + if (IsScopeActive(scope) #if UX_NAVIGATION && UXNavigationRuntime.IsHolderWithinTopScope(scope.Holder) #endif - && !_ancestorHolders.Contains(scope.Holder)) + && !_ancestorHolders.TryGetValue(scope.Holder, out _) + && (bestScope == null || CompareScopePriority(scope, bestScope) < 0)) { - _leafScopes.Add(scope); + bestScope = scope; } } - if (_leafScopes.Count == 0) - { - return null; - } - - _leafScopes.Sort(CompareScopePriority); - return _leafScopes[0]; + return bestScope; } private static bool IsScopeActive(HotkeyScope scope) @@ -548,14 +837,15 @@ namespace UnityEngine.UI return canvas != null && canvas.gameObject.layer == UIComponent.UIShowLayer; } - private static bool IsHolderVisible(UIHolderObjectBase holder) + private static bool IsScopeVisible(HotkeyScope scope) { + UIHolderObjectBase holder = scope.Holder; if (holder == null || !holder.gameObject.activeInHierarchy) { return false; } - Canvas canvas = holder.GetComponent(); + Canvas canvas = scope.Canvas; return canvas != null && canvas.gameObject.layer == UIComponent.UIShowLayer; } @@ -578,25 +868,23 @@ namespace UnityEngine.UI return right.ActivationSerial.CompareTo(left.ActivationSerial); } - private static UIHolderObjectBase FindParentHolder(UIHolderObjectBase holder) + private static void CollectTriggers(HotkeyRegistrationList registrations) { - if (holder == null) + for (int i = 0; i < registrations.Count; i++) { - return null; + AddDestroyScopeTrigger(registrations[i].Trigger); + } + } + + private static void AddDestroyScopeTrigger(IHotkeyTrigger trigger) + { + if (_destroyScopeTriggerCount == _destroyScopeTriggers.Length) + { + int newLength = _destroyScopeTriggers.Length == 0 ? 8 : _destroyScopeTriggers.Length << 1; + Array.Resize(ref _destroyScopeTriggers, newLength); } - Transform current = holder.transform.parent; - while (current != null) - { - if (current.TryGetComponent(out var parentHolder)) - { - return parentHolder; - } - - current = current.parent; - } - - return null; + _destroyScopeTriggers[_destroyScopeTriggerCount++] = trigger; } #if UNITY_EDITOR @@ -610,7 +898,7 @@ namespace UnityEngine.UI namespace UnityEngine.UI { - public static class UXHotkeyHotkeyExtension + public static class UXHotkeyExtension { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void BindHotKey(this IHotkeyTrigger trigger) @@ -620,15 +908,12 @@ namespace UnityEngine.UI return; } - if (trigger is not Component component) - { - return; - } - - UIHolderObjectBase holder = component.GetComponentInParent(true); + UIHolderObjectBase holder = trigger.HotkeyHolder; if (holder == null) { - Debug.LogWarning($"{nameof(HotkeyComponent)} could not find a {nameof(UIHolderObjectBase)} owner.", component); +#if UNITY_EDITOR || DEVELOPMENT_BUILD + Debug.LogWarning("Hotkey trigger could not find a UIHolderObjectBase owner."); +#endif return; } diff --git a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs.meta b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs.meta index 9064b0e..9f714e3 100644 --- a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs.meta +++ b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs.meta @@ -1,3 +1,11 @@ fileFormatVersion: 2 guid: 0aa77908962c48199d63710fa15b8c37 -timeCreated: 1754555268 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/Image/UXImage.cs b/Runtime/UXComponent/Image/UXImage.cs index 32b2aff..3cadd29 100644 --- a/Runtime/UXComponent/Image/UXImage.cs +++ b/Runtime/UXComponent/Image/UXImage.cs @@ -1,10 +1,7 @@ - -using System.Collections.Generic; using System; -using System.Linq; + namespace UnityEngine.UI { - public class UXImage : Image { public enum ColorType @@ -12,37 +9,58 @@ namespace UnityEngine.UI Solid_Color, Gradient_Color } + [SerializeField] public ColorType m_ColorType = ColorType.Solid_Color; - [SerializeField] - private Gradient m_GradientColor = new Gradient() + [SerializeField] private Gradient m_GradientColor = new Gradient() { - colorKeys = new GradientColorKey[2] { - // Add your colour and specify the stop point + colorKeys = new GradientColorKey[2] + { + // Add your colour and specify the stop point new GradientColorKey(new Color(0, 0, 0), 0), new GradientColorKey(new Color(1, 1, 1), 1) }, // This sets the alpha to 1 at both ends of the gradient - alphaKeys = new GradientAlphaKey[2] { + alphaKeys = new GradientAlphaKey[2] + { new GradientAlphaKey(1, 0), new GradientAlphaKey(1, 1) } }; + public Gradient gradient { get { return m_GradientColor; } - set { m_GradientColor = value; SetVerticesDirty(); } + set + { + if (m_GradientColor == value) + return; + + m_GradientColor = value; + CacheGradientKeys(); + SetVerticesDirty(); + } } + public enum GradientDirection { Vertical, Horizontal } + [SerializeField] private GradientDirection m_Direction = GradientDirection.Vertical; + public GradientDirection Direction { get { return m_Direction; } - set { m_Direction = Direction; } + set + { + if (m_Direction == value) + return; + + m_Direction = value; + SetVerticesDirty(); + } } //这个用于标记属于哪个镜像区域 @@ -89,7 +107,6 @@ namespace UnityEngine.UI Left = 0, Middle = 1, Right = 2, - } public enum FlipEdgeVertical @@ -111,10 +128,34 @@ namespace UnityEngine.UI public FlipMode m_OriginFlipMode = FlipMode.None; public FlipMode m_FlipMode = FlipMode.None; - public FlipMode flipMode { get { return m_FlipMode; } set { SetVerticesDirty(); } } + + public FlipMode flipMode + { + get { return m_FlipMode; } + set + { + if (m_FlipMode == value) + return; + + m_FlipMode = value; + SetVerticesDirty(); + } + } public bool m_FlipWithCopy = true; - public bool flipWithCopy { get { return m_FlipWithCopy; } set { SetVerticesDirty(); } } + + public bool flipWithCopy + { + get { return m_FlipWithCopy; } + set + { + if (m_FlipWithCopy == value) + return; + + m_FlipWithCopy = value; + SetVerticesDirty(); + } + } public FlipEdge flipEdge { @@ -124,35 +165,113 @@ namespace UnityEngine.UI { return (FlipEdge)(int)m_FlipEdgeHorizontal; } + if (m_FlipMode == FlipMode.Vertical) { return (FlipEdge)(int)m_FlipEdgeVertical; } - return FlipEdge.None; + return FlipEdge.None; + } + set + { + if (m_FlipMode == FlipMode.Horziontal) + { + FlipEdgeHorizontal next = value == FlipEdge.HorzMiddle ? FlipEdgeHorizontal.Middle : (FlipEdgeHorizontal)(int)value; + if (m_FlipEdgeHorizontal == next) + return; + + m_FlipEdgeHorizontal = next; + SetVerticesDirty(); + } + else if (m_FlipMode == FlipMode.Vertical) + { + FlipEdgeVertical next = value == FlipEdge.VertMiddle ? FlipEdgeVertical.Middle : (FlipEdgeVertical)(int)value; + if (m_FlipEdgeVertical == next) + return; + + m_FlipEdgeVertical = next; + SetVerticesDirty(); + } } - set { SetVerticesDirty(); } } public FlipEdgeHorizontal m_FlipEdgeHorizontal = FlipEdgeHorizontal.Right; - public FlipEdgeHorizontal flipEdgeHorizontal { get { return m_FlipEdgeHorizontal; } set { SetVerticesDirty(); } } + + public FlipEdgeHorizontal flipEdgeHorizontal + { + get { return m_FlipEdgeHorizontal; } + set + { + if (m_FlipEdgeHorizontal == value) + return; + + m_FlipEdgeHorizontal = value; + SetVerticesDirty(); + } + } public FlipEdgeVertical m_FlipEdgeVertical = FlipEdgeVertical.Down; - public FlipEdgeVertical flipEdgeVertical { get { return m_FlipEdgeVertical; } set { SetVerticesDirty(); } } + + public FlipEdgeVertical flipEdgeVertical + { + get { return m_FlipEdgeVertical; } + set + { + if (m_FlipEdgeVertical == value) + return; + + m_FlipEdgeVertical = value; + SetVerticesDirty(); + } + } public FlipFillCenter m_FlipFillCenter = FlipFillCenter.LeftBottom; - public FlipFillCenter flipFillCenter { get { return m_FlipFillCenter; } set { SetVerticesDirty(); } } + + public FlipFillCenter flipFillCenter + { + get { return m_FlipFillCenter; } + set + { + if (m_FlipFillCenter == value) + return; + + m_FlipFillCenter = value; + SetVerticesDirty(); + } + } - [SerializeField] - public FlipDirection m_FlipDirection = FlipDirection.FourCorner; - public FlipDirection flipDirection { get { return m_FlipDirection; } set { SetVerticesDirty(); } } + [SerializeField] public FlipDirection m_FlipDirection = FlipDirection.FourCorner; - static Rect rect = new Rect(); - static UIVertex vert = new UIVertex(); + public FlipDirection flipDirection + { + get { return m_FlipDirection; } + set + { + if (m_FlipDirection == value) + return; + + m_FlipDirection = value; + SetVerticesDirty(); + } + } + + private Rect m_WorkRect; + private UIVertex m_WorkVert; private static readonly Vector4 s_DefaultTangent = new Vector4(1.0f, 0.0f, 0.0f, -1.0f); private static readonly Vector3 s_DefaultNormal = Vector3.back; + private const string ProfilerPopulateMesh = "UXImage.OnPopulateMesh"; + private const string ProfilerGenerateSimpleSprite = "UXImage.GenerateSimpleSprite"; + private const string ProfilerGenerateSprite = "UXImage.GenerateSprite"; + private GradientColorKey[] m_CachedColorKeys; + private GradientAlphaKey[] m_CachedAlphaKeys; + private readonly Vector2[] m_VertScratch = new Vector2[4]; + private readonly Vector2[] m_UvScratch = new Vector2[4]; + private readonly Vector3[] m_Xy = new Vector3[4]; + private readonly Vector3[] m_Uv = new Vector3[4]; + private readonly Vector3[] m_Uv1 = new Vector3[4]; /// Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top. @@ -167,10 +286,10 @@ namespace UnityEngine.UI int spriteH = Mathf.RoundToInt(size.y); var v = new Vector4( - padding.x / spriteW, - padding.y / spriteH, - (spriteW - padding.z) / spriteW, - (spriteH - padding.w) / spriteH); + padding.x / spriteW, + padding.y / spriteH, + (spriteW - padding.z) / spriteW, + (spriteH - padding.w) / spriteH); if (shouldPreserveAspect && size.sqrMagnitude > 0.0f) { @@ -178,11 +297,11 @@ namespace UnityEngine.UI } v = new Vector4( - r.x + r.width * v.x, - r.y + r.height * v.y, - r.x + r.width * v.z, - r.y + r.height * v.w - ); + r.x + r.width * v.x, + r.y + r.height * v.y, + r.x + r.width * v.z, + r.y + r.height * v.w + ); return v; } @@ -194,18 +313,20 @@ namespace UnityEngine.UI RectTransform trans = transform as RectTransform; trans.sizeDelta = new Vector2(trans.sizeDelta.x * 2, trans.sizeDelta.y); } + if (flipMode == FlipMode.Vertical && flipWithCopy) { RectTransform trans = transform as RectTransform; trans.sizeDelta = new Vector2(trans.sizeDelta.x, trans.sizeDelta.y * 2); } + if (flipMode == FlipMode.FourCorner) { RectTransform trans = transform as RectTransform; trans.sizeDelta = new Vector2(trans.sizeDelta.x * 2, trans.sizeDelta.y * 2); } - } + protected void OnPopulateMesh1(VertexHelper toFill) { //ResizeByFlip(); @@ -225,22 +346,33 @@ namespace UnityEngine.UI //} //else { - UnityEngine.Profiling.Profiler.BeginSample(GetType().Name + ".OnPopulateMesh:" + type); +#if UNITY_EDITOR || DEVELOPMENT_BUILD + UnityEngine.Profiling.Profiler.BeginSample(ProfilerPopulateMesh); +#endif switch (type) { case Type.Simple: if (!useSpriteMesh) { - UnityEngine.Profiling.Profiler.BeginSample(GetType().Name + ".OnPopulateMesh:" + type + ", GenerateSimpleSprite"); +#if UNITY_EDITOR || DEVELOPMENT_BUILD + UnityEngine.Profiling.Profiler.BeginSample(ProfilerGenerateSimpleSprite); +#endif GenerateSimpleSprite(toFill, preserveAspect); +#if UNITY_EDITOR || DEVELOPMENT_BUILD UnityEngine.Profiling.Profiler.EndSample(); +#endif } else { - UnityEngine.Profiling.Profiler.BeginSample(GetType().Name + ".OnPopulateMesh:" + type + ", GenerateSprite"); +#if UNITY_EDITOR || DEVELOPMENT_BUILD + UnityEngine.Profiling.Profiler.BeginSample(ProfilerGenerateSprite); +#endif GenerateSprite(toFill, preserveAspect); +#if UNITY_EDITOR || DEVELOPMENT_BUILD UnityEngine.Profiling.Profiler.EndSample(); +#endif } + break; case Type.Sliced: GenerateSlicedSprite(toFill); @@ -252,10 +384,12 @@ namespace UnityEngine.UI GenerateFilledSprite(toFill, preserveAspect); break; } +#if UNITY_EDITOR || DEVELOPMENT_BUILD UnityEngine.Profiling.Profiler.EndSample(); +#endif } - rect = GetDrawPixelAdjustedRect(); + m_WorkRect = GetDrawPixelAdjustedRect(); if (flipMode == FlipMode.Horziontal) { @@ -269,7 +403,7 @@ namespace UnityEngine.UI } else { - RemapImage(toFill, FlipMode.Horziontal, 0, toFill.currentVertCount, rect.xMin, rect.xMax, rect.xMax, rect.xMin); + RemapImage(toFill, FlipMode.Horziontal, 0, toFill.currentVertCount, m_WorkRect.xMin, m_WorkRect.xMax, m_WorkRect.xMax, m_WorkRect.xMin); } } @@ -284,20 +418,20 @@ namespace UnityEngine.UI } else { - RemapImage(toFill, FlipMode.Vertical, 0, toFill.currentVertCount, rect.xMin, rect.xMax, rect.xMax, rect.xMin); + RemapImage(toFill, FlipMode.Vertical, 0, toFill.currentVertCount, m_WorkRect.xMin, m_WorkRect.xMax, m_WorkRect.xMax, m_WorkRect.xMin); } } if (flipMode == FlipMode.FourCorner) { - //先水平拷贝 + //先水平拷�? CopyImage(toFill); Rect src = GetPixelRectByFlipDirection(FlipMode.FourCorner, true, flipEdge, flipFillCenter); Rect target = GetCopyRectByFlipCenter(src, FlipMode.Horziontal, flipFillCenter); //Rect target = new Rect(src.position + new Vector2(src.width, 0), src.size); RemapImage(toFill, FlipMode.Horziontal, toFill.currentVertCount / 2, toFill.currentVertCount, src.xMin, src.xMax, target.xMax, target.xMin); - //再垂直拷贝 + //再垂直拷�? CopyImage(toFill); Rect src1 = GetPixelRectByFlipDirection(FlipMode.FourCorner, true, flipEdge, flipFillCenter); Rect target1 = GetCopyRectByFlipCenter(src1, FlipMode.Vertical, flipFillCenter); @@ -334,6 +468,7 @@ namespace UnityEngine.UI } #region Flip + private FlipPart GetFlipPart(int index, int vertCount) { switch (flipDirection) @@ -361,6 +496,7 @@ namespace UnityEngine.UI return FlipPart.Part4; } } + return FlipPart.Part1; } @@ -371,9 +507,10 @@ namespace UnityEngine.UI for (int i = 0; i < count; ++i) { - toFill.PopulateUIVertex(ref vert, i % count); - toFill.AddVert(vert); + toFill.PopulateUIVertex(ref m_WorkVert, i % count); + toFill.AddVert(m_WorkVert); } + for (int i = count; i < 2 * count - 2; i += 4) { toFill.AddTriangle(i, i + 1, i + 2); @@ -385,11 +522,12 @@ namespace UnityEngine.UI { for (int i = indexMin; i < indexMax; i++) { - toFill.PopulateUIVertex(ref vert, i); - RemapVertex(ref vert, flipMode, Min1, Max1, Min2, Max2); - toFill.SetUIVertex(vert, i); + toFill.PopulateUIVertex(ref m_WorkVert, i); + RemapVertex(ref m_WorkVert, flipMode, Min1, Max1, Min2, Max2); + toFill.SetUIVertex(m_WorkVert, i); } } + public void RemapVertex(ref UIVertex vertex, FlipMode flipMode, float Min1, float Max1, float Min2, float Max2) { Vector2 position = vertex.position; @@ -400,12 +538,14 @@ namespace UnityEngine.UI { vertex.position = new Vector2(position.x * k + b, position.y); } + //垂直方向,上方图像方向不变,下方图像翻转 if (flipMode == FlipMode.Vertical) { vertex.position = new Vector2(position.x, position.y * k + b); } } + Rect GetDrawPixelAdjustedRect() { Rect rect = GetPixelAdjustedRect(); @@ -426,8 +566,8 @@ namespace UnityEngine.UI /// /// 修改原来的Rect - /// 如果 flipMode == Horziontal 或 Vertical, 根据copy, flipEdge来修改原本的Rect - /// 如果 flipMode == FourCorner, 根据flipFillCenter 来修改 + /// 如果 flipMode == Horziontal �?Vertical, 根据copy, flipEdge来修改原本的Rect + /// 如果 flipMode == FourCorner, 根据flipFillCenter 来修�? /// /// Image原始Rect private Rect ModifyRectByFlipDirection(Rect rect, FlipMode flipMode, bool copy, FlipEdge flipEdge, FlipFillCenter fillCenter) @@ -440,6 +580,7 @@ namespace UnityEngine.UI { rect = new Rect(rect.center.x, rect.yMin, rect.width / 2, rect.height); } + if (flipEdge == FlipEdge.Right) { rect = new Rect(rect.xMin, rect.yMin, rect.width / 2, rect.height); @@ -451,11 +592,11 @@ namespace UnityEngine.UI { if (copy) { - if (flipEdge == FlipEdge.Up) { rect = new Rect(rect.xMin, rect.yMin, rect.width, rect.height / 2); } + if (flipEdge == FlipEdge.Down) { rect = new Rect(rect.xMin, rect.center.y, rect.width, rect.height / 2); @@ -492,7 +633,7 @@ namespace UnityEngine.UI /// /// 获取Copy出来的Rect /// 这里Src应该是Modify过的原Rect - /// 只处理flipMode == Horziontal 或 Vertical的情况 + /// 只处理flipMode == Horziontal �?Vertical的情�? /// /// /// @@ -512,10 +653,12 @@ namespace UnityEngine.UI { target = new Rect(src.position - new Vector2(src.width, 0), src.size); } + if (flipEdge == FlipEdge.HorzMiddle) { target = src; } + if (flipEdge == FlipEdge.Right) { target = new Rect(src.position + new Vector2(src.width, 0), src.size); @@ -528,10 +671,12 @@ namespace UnityEngine.UI { target = new Rect(src.position + new Vector2(0, src.height), src.size); } + if (flipEdge == FlipEdge.VertMiddle) { target = src; } + if (flipEdge == FlipEdge.Down) { target = new Rect(src.position - new Vector2(0, src.height), src.size); @@ -542,7 +687,7 @@ namespace UnityEngine.UI } /// - /// 获取Copy出来的Rect, flipMode == FourCorner时专用 + /// 获取Copy出来的Rect�?flipMode == FourCorner时专�? /// flipMode == FourCorner时会转换成两次Copy /// 分别调用GetCopyRectByFlipDirection /// 这里Src应该是Modify过的原Rect @@ -577,6 +722,7 @@ namespace UnityEngine.UI target = GetCopyRectByFlipDirection(src, FlipMode.Vertical, FlipEdge.Down); } } + return target; } @@ -610,559 +756,169 @@ namespace UnityEngine.UI #endregion - #region gradient - public class Vert2D //描述alphakey和colorkey的类 - { - public Vector3 position = default; - public Color color = Color.white; - public Vector2 uv0 = default; - public int type;//1=alphakey,2=colorkey, 0=两端 - public Vert2D(int type) - { - this.type = type; - } - public Vert2D(UIVertex vertex) - { - type = 0; - } - public int CompareToVertically(Vert2D vert) - { - if (this.uv0.y > vert.uv0.y) - return 1; - else if (this.uv0.y == vert.uv0.y) - { - if (this.uv0.x <= vert.uv0.x) - { - return -1; - } - else return 1; - } - else - return -1; - } - public int CompareToHorizontally(Vert2D vert) - { - if (this.uv0.x > vert.uv0.x) - return 1; - else if (this.uv0.x == vert.uv0.x) - { - if (this.uv0.y <= vert.uv0.y) - { - return -1; - } - else return 1; - } - else - return -1; - } - // public void SetColor(byte r, byte g, byte b){ - // this.color.r = r; - // this.color.g = g; - // this.color.b = b; - // } - public void SetColor(Color col) - { - this.color.r = col.r; - this.color.g = col.g; - this.color.b = col.b; - } - public void SetAlpha(float a) - { - this.color.a = a; - } - public void SetUV(float u, float v) - { - this.uv0.x = u; - this.uv0.y = v; - } - public void SetPosition(Vector3 pos) - { - this.position.x = pos.x; - this.position.y = pos.y; - this.position.z = pos.z; - } - } - public class Comparer : IEqualityComparer - { - public bool Equals(Vert2D x, Vert2D y) - { - //这里定义比较的逻辑 - return x.uv0 == y.uv0; - } - - public int GetHashCode(Vert2D obj) - { - //返回字段的HashCode,只有HashCode相同才会去比较 - return obj.uv0.GetHashCode(); - } - } - - void PositionLerp(List vlist, Vector2 minpos, Vector2 maxpos) - { - if (m_Direction == GradientDirection.Horizontal) - { - for (int i = 0; i < vlist.Count; i++) - { - Vector3 temppos = Vector3.Lerp(minpos, maxpos, vlist[i].uv0.x); - if (vlist[i].uv0.y == 1) - { - temppos.y = maxpos.y; - } - else - { - temppos.y = minpos.y; - } - vlist[i].SetPosition(temppos); - } - } - else if (m_Direction == GradientDirection.Vertical) - { - for (int i = 0; i < vlist.Count; i++) - { - Vector3 temppos = Vector3.Lerp(minpos, maxpos, vlist[i].uv0.y); - if (vlist[i].uv0.x == 1) - { - temppos.x = maxpos.x; - } - else - { - temppos.x = minpos.x; - } - vlist[i].SetPosition(temppos); - } - } - } - void UVShrink(List vlist, Vector2 uvmin, Vector2 uvmax) - { - foreach (Vert2D v in vlist) - { - v.uv0.x = (uvmax.x - uvmin.x) * v.uv0.x + uvmin.x; - v.uv0.y = (uvmax.y - uvmin.y) * v.uv0.y + uvmin.y; - } - } protected override void OnPopulateMesh(VertexHelper toFill) { - if (m_ColorType == ColorType.Solid_Color) + OnPopulateMesh1(toFill); + + if (m_ColorType == ColorType.Gradient_Color) { - OnPopulateMesh1(toFill); - } - else if (m_ColorType == ColorType.Gradient_Color) - { - //根据Gradient,新建mesh - GradientAlphaKey[] alpha = m_GradientColor.alphaKeys; - GradientColorKey[] colorkey = m_GradientColor.colorKeys; - List vertList = new List(); - - if (m_Direction == GradientDirection.Vertical) - { - //垂直分割 - Rect rect = GetPixelAdjustedRect(); - Vert2D v0 = new Vert2D(0); - v0.SetColor(colorkey[colorkey.Length - 1].color); - v0.SetAlpha(alpha[alpha.Length - 1].alpha); - v0.SetUV(0, 0); - Vector3 vec = new Vector3(rect.x, rect.y, 0); - v0.SetPosition(vec); - vertList.Add(v0); - - Vert2D v1 = new Vert2D(0); - v1.SetColor(colorkey[colorkey.Length - 1].color); - v1.SetAlpha(alpha[alpha.Length - 1].alpha); - v1.SetUV(1, 0); - vec = new Vector3(rect.x + rect.width, rect.y, 0); - v1.SetPosition(vec); - vertList.Add(v1); - - Vert2D v2 = new Vert2D(0); - v2.SetColor(colorkey[0].color); - v2.SetAlpha(alpha[0].alpha); - v2.SetUV(1, 1); - vec = new Vector3(rect.x + rect.width, rect.y + rect.height, 0); - v2.SetPosition(vec); - vertList.Add(v2); - - Vert2D v3 = new Vert2D(0); - v3.SetColor(colorkey[0].color); - v3.SetAlpha(alpha[0].alpha); - v3.SetUV(0, 1); - vec = new Vector3(rect.x, rect.y + rect.height, 0); - v3.SetPosition(vec); - vertList.Add(v3); - - for (int i = 0; i < alpha.Length; i++) - { - if (alpha[i].time == 0 || alpha[i].time == 1) - { - continue; - } - Vert2D v = new Vert2D(1); - v.SetAlpha(alpha[i].alpha); - v.SetUV(0, 1 - alpha[i].time); - vertList.Add(v); - Vert2D v_ = new Vert2D(1); - v_.SetAlpha(alpha[i].alpha); - v_.SetUV(1, 1 - alpha[i].time); - vertList.Add(v_); - } - for (int i = 0; i < colorkey.Length; i++) - { - if (colorkey[i].time == 0 || colorkey[i].time == 1) - { - continue; - } - Vert2D v = new Vert2D(2); - v.SetColor(colorkey[i].color); - v.SetUV(1, 1 - colorkey[i].time); - vertList.Add(v); - Vert2D v_ = new Vert2D(2); - v_.SetColor(colorkey[i].color); - v_.SetUV(0, 1 - colorkey[i].time); - vertList.Add(v_); - } - //排序,addvert - vertList.Sort((x, y) => - { - return x.CompareToVertically(y); - }); - - //给未填充数据的点填充数据 - int nowtype = vertList[1].type; - int lasttypepos = 0; - int nexttypepos = 1; - for (int i = 1; i < vertList.Count - 1; i++) - { - nowtype = vertList[i].type; - if (i >= nexttypepos) - { - //去找下一个type - lasttypepos = i - 1; - for (int j = i + 1; j < vertList.Count; j++) - { - if (vertList[j].type != nowtype) - { - nexttypepos = j; - break; - } - } - } - //插值更新color和alpha - if (nowtype == 1) - { - //是alpha点,要更新color - Color col; - #if UNITY_2022_2_OR_NEWER - if (m_GradientColor.mode == GradientMode.Blend || m_GradientColor.mode == GradientMode.PerceptualBlend) - #else - if (m_GradientColor.mode == GradientMode.Blend) - #endif - { - - if ((vertList[nexttypepos].uv0.y - vertList[lasttypepos].uv0.y) == 0) - { - col = new Color(vertList[lasttypepos].color.r, vertList[lasttypepos].color.g, vertList[lasttypepos].color.b, vertList[lasttypepos].color.a); - } - else - { - col = Color.Lerp(vertList[lasttypepos].color, vertList[nexttypepos].color, - (vertList[i].uv0.y - vertList[lasttypepos].uv0.y) / (vertList[nexttypepos].uv0.y - vertList[lasttypepos].uv0.y)); - } - } - else - { - col = vertList[nexttypepos].color; - } - vertList[i].SetColor(col); - } - else if (nowtype == 2) - { - Color col; - #if UNITY_2022_2_OR_NEWER - if (m_GradientColor.mode == GradientMode.Blend || m_GradientColor.mode == GradientMode.PerceptualBlend) - #else - if (m_GradientColor.mode == GradientMode.Blend) - #endif - { - if ((vertList[nexttypepos].uv0.y - vertList[lasttypepos].uv0.y) == 0) - { - col = new Color(vertList[lasttypepos].color.r, vertList[lasttypepos].color.g, vertList[lasttypepos].color.b, vertList[lasttypepos].color.a); - } - else - { - col = Color.Lerp(vertList[lasttypepos].color, vertList[nexttypepos].color, - (vertList[i].uv0.y - vertList[lasttypepos].uv0.y) / (vertList[nexttypepos].uv0.y - vertList[lasttypepos].uv0.y)); - } - } - else - { - col = vertList[nexttypepos].color; - } - vertList[i].SetAlpha(col.a); - } - } - //去重 - vertList = vertList.Distinct(new Comparer()).ToList(); - PositionLerp(vertList, new Vector2(rect.x, rect.y), new Vector2(rect.x + rect.width, rect.y + rect.height)); - - - } - else if (m_Direction == GradientDirection.Horizontal) - { - //水平分割 - Rect rect = GetPixelAdjustedRect(); - Vert2D v0 = new Vert2D(0); - v0.SetColor(colorkey[0].color); - v0.SetAlpha(alpha[0].alpha); - v0.SetUV(0, 0); - Vector3 vec = new Vector3(rect.x, rect.y, 0); - v0.SetPosition(vec); - vertList.Add(v0); - - Vert2D v3 = new Vert2D(0); - v3.SetColor(colorkey[0].color); - v3.SetAlpha(alpha[0].alpha); - v3.SetUV(0, 1); - vec = new Vector3(rect.x, rect.y + rect.height, 0); - v3.SetPosition(vec); - vertList.Add(v3); - - Vert2D v2 = new Vert2D(0); - v2.SetColor(colorkey[colorkey.Length - 1].color); - v2.SetAlpha(alpha[alpha.Length - 1].alpha); - v2.SetUV(1, 1); - vec = new Vector3(rect.x + rect.width, rect.y + rect.height, 0); - v2.SetPosition(vec); - vertList.Add(v2); - - Vert2D v1 = new Vert2D(0); - v1.SetColor(colorkey[colorkey.Length - 1].color); - v1.SetAlpha(alpha[alpha.Length - 1].alpha); - v1.SetUV(1, 0); - vec = new Vector3(rect.x + rect.width, rect.y, 0); - v1.SetPosition(vec); - vertList.Add(v1); - - - for (int i = 0; i < alpha.Length; i++) - { - if (alpha[i].time == 0 || alpha[i].time == 1) - { - continue; - } - Vert2D v = new Vert2D(1); - v.SetAlpha(alpha[i].alpha); - v.SetUV(alpha[i].time, 0); - vertList.Add(v); - Vert2D v_ = new Vert2D(1); - v_.SetAlpha(alpha[i].alpha); - v_.SetUV(alpha[i].time, 1); - vertList.Add(v_); - } - for (int i = 0; i < colorkey.Length; i++) - { - if (colorkey[i].time == 0 || colorkey[i].time == 1) - { - continue; - } - Vert2D v = new Vert2D(2); - v.SetColor(colorkey[i].color); - v.SetUV(colorkey[i].time, 1); - vertList.Add(v); - Vert2D v_ = new Vert2D(2); - v_.SetColor(colorkey[i].color); - v_.SetUV(colorkey[i].time, 0); - vertList.Add(v_); - } - //排序,addvert - vertList.Sort((x, y) => - { - return x.CompareToHorizontally(y); - }); - - int nowtype = vertList[1].type; - int lasttypepos = 0; - int nexttypepos = 1; - for (int i = 1; i < vertList.Count - 1; i++) - { - nowtype = vertList[i].type; - if (i >= nexttypepos) - { - //去找下一个type - lasttypepos = i - 1; - for (int j = i + 1; j < vertList.Count; j++) - { - if (vertList[j].type != nowtype) - { - nexttypepos = j; - break; - } - } - - } - //插值 - if (nowtype == 1) - { - //是alpha点,要更新color - Color col; - #if UNITY_2022_2_OR_NEWER - if (m_GradientColor.mode == GradientMode.Blend || m_GradientColor.mode == GradientMode.PerceptualBlend) - #else - if (m_GradientColor.mode == GradientMode.Blend) - #endif - { - if ((vertList[nexttypepos].uv0.x - vertList[lasttypepos].uv0.x) == 0) - { - col = new Color(vertList[lasttypepos].color.r, vertList[lasttypepos].color.g, vertList[lasttypepos].color.b, vertList[lasttypepos].color.a); - } - else - { - col = Color.Lerp(vertList[lasttypepos].color, vertList[nexttypepos].color, - (vertList[i].uv0.x - vertList[lasttypepos].uv0.x) / (vertList[nexttypepos].uv0.x - vertList[lasttypepos].uv0.x)); - } - } - else - { - col = vertList[nexttypepos].color; - } - vertList[i].SetColor(col); - } - else if (nowtype == 2) - { - Color col; - #if UNITY_2022_2_OR_NEWER - if (m_GradientColor.mode == GradientMode.Blend || m_GradientColor.mode == GradientMode.PerceptualBlend) - #else - if (m_GradientColor.mode == GradientMode.Blend) - #endif - { - if ((vertList[nexttypepos].uv0.x - vertList[lasttypepos].uv0.x) == 0) - { - col = new Color(vertList[lasttypepos].color.r, vertList[lasttypepos].color.g, vertList[lasttypepos].color.b, vertList[lasttypepos].color.a); - } - else - { - col = Color.Lerp(vertList[lasttypepos].color, vertList[nexttypepos].color, - (vertList[i].uv0.x - vertList[lasttypepos].uv0.x) / (vertList[nexttypepos].uv0.x - vertList[lasttypepos].uv0.x)); - } - } - else - { - col = vertList[nexttypepos].color; - } - vertList[i].SetAlpha(col.a); - } - } - vertList = vertList.Distinct(new Comparer()).ToList(); - PositionLerp(vertList, new Vector2(rect.x, rect.y), new Vector2(rect.x + rect.width, rect.y + rect.height)); - } - - //重写四个函数 - - switch (type) - { - case Type.Simple: - //因为UseSpriteMesh和渐变色加点冲突,因此放弃使用 - if (!useSpriteMesh) - { - GenerateSimpleSprite(toFill, vertList, preserveAspect); - } - else - GenerateSprite(toFill, preserveAspect); - break; - case Type.Sliced: - GenerateSlicedSprite(toFill, vertList); - break; - // case Type.Tiled: - // GenerateTiledSprite(toFill); - // break; - // case Type.Filled: - // GenerateFilledSprite(toFill, preserveAspect); - // break; - } - // rect = GetDrawPixelAdjustedRect(); - - // if (flipMode == FlipMode.Horziontal) - // { - // if (flipWithCopy) - // { - // CopyImage(toFill); - // Rect src = GetPixelRectByFlipDirection(FlipMode.Horziontal, flipWithCopy, flipEdge, flipFillCenter); - // Rect target = GetCopyRectByFlipDirection(src, FlipMode.Horziontal, flipEdge); - // //Rect target = new Rect(src.position - new Vector2(src.width, 0), src.size); - // RemapImage(toFill, FlipMode.Horziontal, toFill.currentVertCount / 2, toFill.currentVertCount, src.xMin, src.xMax, target.xMax, target.xMin); - // } - // else - // { - // RemapImage(toFill, FlipMode.Horziontal, 0, toFill.currentVertCount, rect.xMin, rect.xMax, rect.xMax, rect.xMin); - // } - // } - - // if (flipMode == FlipMode.Vertical) - // { - // if (flipWithCopy) - // { - // CopyImage(toFill); - // Rect src = GetPixelRectByFlipDirection(FlipMode.Vertical, flipWithCopy, flipEdge, flipFillCenter); - // Rect target = GetCopyRectByFlipDirection(src, FlipMode.Vertical, flipEdge); - // RemapImage(toFill, FlipMode.Vertical, toFill.currentVertCount / 2, toFill.currentVertCount, src.yMin, src.yMax, target.yMax, target.yMin); - // } - // else - // { - // RemapImage(toFill, FlipMode.Vertical, 0, toFill.currentVertCount, rect.xMin, rect.xMax, rect.xMax, rect.xMin); - // } - // } - - // if (flipMode == FlipMode.FourCorner) - // { - // //先水平拷贝 - // CopyImage(toFill); - // Rect src = GetPixelRectByFlipDirection(FlipMode.FourCorner, true, flipEdge, flipFillCenter); - // Rect target = GetCopyRectByFlipCenter(src, FlipMode.Horziontal, flipFillCenter); - // //Rect target = new Rect(src.position + new Vector2(src.width, 0), src.size); - // RemapImage(toFill, FlipMode.Horziontal, toFill.currentVertCount / 2, toFill.currentVertCount, src.xMin, src.xMax, target.xMax, target.xMin); - - // //再垂直拷贝 - // CopyImage(toFill); - // Rect src1 = GetPixelRectByFlipDirection(FlipMode.FourCorner, true, flipEdge, flipFillCenter); - // Rect target1 = GetCopyRectByFlipCenter(src1, FlipMode.Vertical, flipFillCenter); - // RemapImage(toFill, FlipMode.Vertical, toFill.currentVertCount / 2, toFill.currentVertCount, src1.yMin, src1.yMax, target1.yMax, target1.yMin); - // } - - // m_OriginFlipMode = m_FlipMode; - //将color设置为white,函数结束将原color设置回去 - - // rect = GetPixelAdjustedRect(); - // for (int i = 0; i < toFill.currentVertCount; ++i) - // { - - // toFill.PopulateUIVertex(ref vert, i); - - // //SetGradientColor(ref vert, rect, flipDirection, GetFlipPart(i, toFill.currentVertCount)); - // toFill.SetUIVertex(vert, i); - // } - - //根据gradient的值,赋color - + ApplyGradientColor(toFill); } } - void GenerateSimpleSprite(VertexHelper vh, List vertlist, bool lPreserveAspect) + private void CacheGradientKeys() { - if (overrideSprite == null) + if (m_GradientColor == null) { - vh.Clear(); - AddQuads(vh, vertlist); + m_CachedColorKeys = null; + m_CachedAlphaKeys = null; return; } - Vector4 v = GetDrawingDimensions(lPreserveAspect); - var uv = (overrideSprite != null) ? Sprites.DataUtility.GetOuterUV(overrideSprite) : Vector4.zero; - PositionLerp(vertlist, new Vector2(v.x, v.y), new Vector2(v.z, v.w)); - UVShrink(vertlist, new Vector2(uv.x, uv.y), new Vector2(uv.z, uv.w)); - vh.Clear(); - AddQuads(vh, vertlist); + + m_CachedColorKeys = m_GradientColor.colorKeys; + m_CachedAlphaKeys = m_GradientColor.alphaKeys; } + protected override void OnEnable() + { + base.OnEnable(); + CacheGradientKeys(); + } +#if UNITY_EDITOR + protected override void OnValidate() + { + base.OnValidate(); + CacheGradientKeys(); + } +#endif + private void ApplyGradientColor(VertexHelper toFill) + { + int count = toFill.currentVertCount; + if (count == 0) + return; + + if (m_CachedColorKeys == null || m_CachedAlphaKeys == null) + CacheGradientKeys(); + + Rect bounds = GetGradientBounds(toFill, count); + float inverseSize = m_Direction == GradientDirection.Horizontal ? bounds.width : bounds.height; + if (Mathf.Approximately(inverseSize, 0f)) + inverseSize = 1f; + else + inverseSize = 1f / inverseSize; + + for (int i = 0; i < count; i++) + { + toFill.PopulateUIVertex(ref m_WorkVert, i); + float time = m_Direction == GradientDirection.Horizontal + ? (m_WorkVert.position.x - bounds.xMin) * inverseSize + : (m_WorkVert.position.y - bounds.yMin) * inverseSize; + m_WorkVert.color = EvaluateCachedGradient(Mathf.Clamp01(time)); + toFill.SetUIVertex(m_WorkVert, i); + } + } + + private Rect GetGradientBounds(VertexHelper toFill, int count) + { + toFill.PopulateUIVertex(ref m_WorkVert, 0); + float minX = m_WorkVert.position.x; + float maxX = minX; + float minY = m_WorkVert.position.y; + float maxY = minY; + + for (int i = 1; i < count; i++) + { + toFill.PopulateUIVertex(ref m_WorkVert, i); + Vector3 position = m_WorkVert.position; + if (position.x < minX) minX = position.x; + if (position.x > maxX) maxX = position.x; + if (position.y < minY) minY = position.y; + if (position.y > maxY) maxY = position.y; + } + + return Rect.MinMaxRect(minX, minY, maxX, maxY); + } + + private Color32 EvaluateCachedGradient(float time) + { + Color result = EvaluateColor(time); + result.a = EvaluateAlpha(time); + return result; + } + + private Color EvaluateColor(float time) + { + GradientColorKey[] keys = m_CachedColorKeys; + if (keys == null || keys.Length == 0) + return color; + + GradientColorKey first = keys[0]; + if (time <= first.time) + return first.color; + + for (int i = 1; i < keys.Length; i++) + { + GradientColorKey next = keys[i]; + if (time <= next.time) + { +#if UNITY_2022_2_OR_NEWER + if (m_GradientColor.mode == GradientMode.Blend || m_GradientColor.mode == GradientMode.PerceptualBlend) +#else + if (m_GradientColor.mode == GradientMode.Blend) +#endif + { + GradientColorKey previous = keys[i - 1]; + float span = next.time - previous.time; + if (Mathf.Approximately(span, 0f)) + return previous.color; + + return Color.Lerp(previous.color, next.color, (time - previous.time) / span); + } + + return next.color; + } + } + + return keys[keys.Length - 1].color; + } + + private float EvaluateAlpha(float time) + { + GradientAlphaKey[] keys = m_CachedAlphaKeys; + if (keys == null || keys.Length == 0) + return color.a; + + GradientAlphaKey first = keys[0]; + if (time <= first.time) + return first.alpha; + + for (int i = 1; i < keys.Length; i++) + { + GradientAlphaKey next = keys[i]; + if (time <= next.time) + { +#if UNITY_2022_2_OR_NEWER + if (m_GradientColor.mode == GradientMode.Blend || m_GradientColor.mode == GradientMode.PerceptualBlend) +#else + if (m_GradientColor.mode == GradientMode.Blend) +#endif + { + GradientAlphaKey previous = keys[i - 1]; + float span = next.time - previous.time; + if (Mathf.Approximately(span, 0f)) + return previous.alpha; + + return Mathf.Lerp(previous.alpha, next.alpha, (time - previous.time) / span); + } + + return next.alpha; + } + } + + return keys[keys.Length - 1].alpha; + } private void GenerateSprite(VertexHelper vh, bool lPreserveAspect) { @@ -1200,131 +956,8 @@ namespace UnityEngine.UI vh.AddTriangle(triangles[i + 0], triangles[i + 1], triangles[i + 2]); } } - private void GenerateSprite(VertexHelper vh, List vertlist, bool lPreserveAspect) - { - var spriteSize = new Vector2(overrideSprite.rect.width, overrideSprite.rect.height); - // Covert sprite pivot into normalized space. - var spritePivot = overrideSprite.pivot / spriteSize; - var rectPivot = rectTransform.pivot; - Rect r = GetPixelAdjustedRect(); - - if (lPreserveAspect & spriteSize.sqrMagnitude > 0.0f) - { - PreserveSpriteAspectRatio(ref r, spriteSize); - } - - var drawingSize = new Vector2(r.width, r.height); - var spriteBoundSize = overrideSprite.bounds.size; - - // Calculate the drawing offset based on the difference between the two pivots. - var drawOffset = (rectPivot - spritePivot) * drawingSize; - - var color32 = color; - vh.Clear(); - - Vector2[] vertices = overrideSprite.vertices; - Vector2[] uvs = overrideSprite.uv; - List colors = new List(); - for (int i = 0; i < uvs.Length; ++i) - { - //Debug.Log(uvs[i]); - if (m_Direction == GradientDirection.Vertical) - { - float tolerp = uvs[i].y; - //基于uv y值做插值,通过二分查找插值位置 - int le = 0; - int ri = vertlist.Count; - int mid; - while (le < ri - 1) - { - mid = (le + ri) / 2; - float midY = vertlist[mid].uv0.y; - if (midY == tolerp) - { - break; - } - else if (midY < tolerp) - { - le = mid; - } - else - { - ri = mid; - } - } - if ((vertlist[(le + ri) / 2 + 1].uv0.y - vertlist[(le + ri) / 2].uv0.y) != 0 && (le + ri) / 2 != vertlist.Count) - { - Color lerpColor = Color.Lerp(vertlist[(le + ri) / 2].color, vertlist[(le + ri) / 2 + 1].color, - (tolerp - vertlist[(le + ri) / 2].uv0.y) / (vertlist[(le + ri) / 2 + 1].uv0.y - vertlist[(le + ri) / 2].uv0.y)); - colors.Add(lerpColor); - } - else - { - Color lerpColor = vertlist[(le + ri) / 2].color; - colors.Add(lerpColor); - } - } - else - { - //基于x做插值 - float tolerp = uvs[i].x; - int le = 0; - int ri = vertlist.Count; - int mid; - while (le < ri - 1) - { - mid = (le + ri) / 2; - float midY = vertlist[mid].uv0.x; - if (midY == tolerp) - { - break; - } - else if (midY < tolerp) - { - le = mid; - } - else - { - ri = mid; - } - } - if ((vertlist[(le + ri) / 2 + 1].uv0.x - vertlist[(le + ri) / 2].uv0.x) != 0 && (le + ri) / 2 != vertlist.Count) - { - Color lerpColor = Color.Lerp(vertlist[(le + ri) / 2].color, vertlist[(le + ri) / 2 + 1].color, - (tolerp - vertlist[(le + ri) / 2].uv0.x) / (vertlist[(le + ri) / 2 + 1].uv0.x - vertlist[(le + ri) / 2].uv0.x)); - colors.Add(lerpColor); - } - else - { - Color lerpColor = vertlist[(le + ri) / 2].color; - colors.Add(lerpColor); - } - } - } - // - //Debug.Log(vertices.Length); - for (int i = 0; i < vertices.Length; ++i) - { - //Debug.Log(vertices[i]); - // - vh.AddVert(new Vector3((vertices[i].x / spriteBoundSize.x) * drawingSize.x - drawOffset.x, - (vertices[i].y / spriteBoundSize.y) * drawingSize.y - drawOffset.y), colors[i], new Vector2(uvs[i].x, uvs[i].y)); - } - - UInt16[] triangles = overrideSprite.triangles; - //Debug.Log(triangles.Length); - for (int i = 0; i < triangles.Length; i += 3) - { - //Debug.Log(triangles[i] + " " + triangles[i + 1] + " " + triangles[i + 2]); - vh.AddTriangle(triangles[i + 0], triangles[i + 1], triangles[i + 2]); - } - - } - - static readonly Vector2[] s_VertScratch = new Vector2[4]; - static readonly Vector2[] s_UVScratch = new Vector2[4]; - private void GenerateSlicedSprite(VertexHelper toFill/*, List vlist*/) + private void GenerateSlicedSprite(VertexHelper toFill) { if (!hasBorder) { @@ -1355,29 +988,29 @@ namespace UnityEngine.UI border = GetAdjustedBorders(border / pixelsPerUnit, rect); padding = padding / pixelsPerUnit; - s_VertScratch[0] = new Vector2(padding.x, padding.y); - s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w); + m_VertScratch[0] = new Vector2(padding.x, padding.y); + m_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w); - s_VertScratch[1].x = border.x; - s_VertScratch[1].y = border.y; - s_VertScratch[2].x = rect.width - border.z; - s_VertScratch[2].y = rect.height - border.w; + m_VertScratch[1].x = border.x; + m_VertScratch[1].y = border.y; + m_VertScratch[2].x = rect.width - border.z; + m_VertScratch[2].y = rect.height - border.w; - float vertWidth = s_VertScratch[3].x - s_VertScratch[0].x; - float vertHeight = s_VertScratch[3].y - s_VertScratch[0].y; + float vertWidth = m_VertScratch[3].x - m_VertScratch[0].x; + float vertHeight = m_VertScratch[3].y - m_VertScratch[0].y; for (int i = 0; i < 4; ++i) { - s_VertScratch[i].x += rect.x; - s_VertScratch[i].y += rect.y; + m_VertScratch[i].x += rect.x; + m_VertScratch[i].y += rect.y; } - s_UVScratch[0] = new Vector2(outer.x, outer.y); - s_UVScratch[1] = new Vector2(inner.x, inner.y); - s_UVScratch[2] = new Vector2(inner.z, inner.w); - s_UVScratch[3] = new Vector2(outer.z, outer.w); + m_UvScratch[0] = new Vector2(outer.x, outer.y); + m_UvScratch[1] = new Vector2(inner.x, inner.y); + m_UvScratch[2] = new Vector2(inner.z, inner.w); + m_UvScratch[3] = new Vector2(outer.z, outer.w); toFill.Clear(); @@ -1392,112 +1025,19 @@ namespace UnityEngine.UI int y2 = y + 1; - Vector2 uv1Min = new Vector2((s_VertScratch[x].x - rect.x) / vertWidth, (s_VertScratch[y].y - rect.y) / vertHeight); - Vector2 uv1Max = new Vector2((s_VertScratch[x2].x - rect.x) / vertWidth, (s_VertScratch[y2].y - rect.y) / vertHeight); + Vector2 uv1Min = new Vector2((m_VertScratch[x].x - rect.x) / vertWidth, (m_VertScratch[y].y - rect.y) / vertHeight); + Vector2 uv1Max = new Vector2((m_VertScratch[x2].x - rect.x) / vertWidth, (m_VertScratch[y2].y - rect.y) / vertHeight); AddQuad(toFill, - new Vector2(s_VertScratch[x].x, s_VertScratch[y].y), - new Vector2(s_VertScratch[x2].x, s_VertScratch[y2].y), - color, - new Vector2(s_UVScratch[x].x, s_UVScratch[y].y), - new Vector2(s_UVScratch[x2].x, s_UVScratch[y2].y), - uv1Min, uv1Max); + new Vector2(m_VertScratch[x].x, m_VertScratch[y].y), + new Vector2(m_VertScratch[x2].x, m_VertScratch[y2].y), + color, + new Vector2(m_UvScratch[x].x, m_UvScratch[y].y), + new Vector2(m_UvScratch[x2].x, m_UvScratch[y2].y), + uv1Min, uv1Max); } } } - private void GenerateSlicedSprite(VertexHelper toFill, List vlist) - { - if (!hasBorder) - { - GenerateSimpleSprite(toFill, vlist, false); - //GenerateSimpleSprite(toFill, false); - return; - } - Vector4 outer, inner, padding, border; - - if (overrideSprite != null) - { - outer = Sprites.DataUtility.GetOuterUV(overrideSprite); - inner = Sprites.DataUtility.GetInnerUV(overrideSprite); - padding = Sprites.DataUtility.GetPadding(overrideSprite); - border = overrideSprite.border; - } - else - { - outer = Vector4.zero; - inner = Vector4.zero; - padding = Vector4.zero; - border = Vector4.zero; - } - - Rect rect = GetPixelRectByFlipDirection(flipMode, flipWithCopy, flipEdge, flipFillCenter); - - border = GetAdjustedBorders(border / pixelsPerUnit, rect); - padding = padding / pixelsPerUnit; - //Debug.Log(padding); - //Debug.Log(border); - //Debug.Log(inner); - //Debug.Log(outer); - //Debug.Log(rect.width); - - s_VertScratch[0] = new Vector2(padding.x, padding.y); - s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w); - - - s_VertScratch[1].x = border.x; - s_VertScratch[1].y = border.y; - s_VertScratch[2].x = rect.width - border.z; - s_VertScratch[2].y = rect.height - border.w; - - float vertWidth = s_VertScratch[3].x - s_VertScratch[0].x; - float vertHeight = s_VertScratch[3].y - s_VertScratch[0].y; - - // for (int i = 0; i < 4; i++) - // { - // Debug.Log(s_VertScratch[i]); - // } - - for (int i = 0; i < 4; ++i) - { - s_VertScratch[i].x += rect.x; - s_VertScratch[i].y += rect.y; - } - - s_UVScratch[0] = new Vector2(outer.x, outer.y); - s_UVScratch[1] = new Vector2(inner.x, inner.y); - s_UVScratch[2] = new Vector2(inner.z, inner.w); - s_UVScratch[3] = new Vector2(outer.z, outer.w); - - // for (int i = 0; i < 4; ++i) - // { - // Debug.Log(s_UVScratch[i]); - // } - - toFill.Clear(); - - for (int x = 0; x < 3; ++x) - { - int x2 = x + 1; - - for (int y = 0; y < 3; ++y) - { - if (!fillCenter && x == 1 && y == 1) - continue; - - int y2 = y + 1; - - Vector2 uv1Min = new Vector2((s_VertScratch[x].x - rect.x) / vertWidth, (s_VertScratch[y].y - rect.y) / vertHeight); - Vector2 uv1Max = new Vector2((s_VertScratch[x2].x - rect.x) / vertWidth, (s_VertScratch[y2].y - rect.y) / vertHeight); - AddQuad(toFill, - new Vector2(s_VertScratch[x].x, s_VertScratch[y].y), - new Vector2(s_VertScratch[x2].x, s_VertScratch[y2].y), - color, - new Vector2(s_UVScratch[x].x, s_UVScratch[y].y), - new Vector2(s_UVScratch[x2].x, s_UVScratch[y2].y), - uv1Min, uv1Max); - } - } - } private void GenerateTiledSprite(VertexHelper toFill) { Vector4 outer, inner, border; @@ -1638,6 +1178,7 @@ namespace UnityEngine.UI clipped.y = uvMin.y + (uvMax.y - uvMin.y) * (yMax - y1) / (y2 - y1); y2 = yMax; } + clipped.x = uvMax.x; for (int i = 0; i < nTilesW; i++) { @@ -1648,6 +1189,7 @@ namespace UnityEngine.UI clipped.x = uvMin.x + (uvMax.x - uvMin.x) * (xMax - x1) / (x2 - x1); x2 = xMax; } + Vector2 posMin = new Vector2(x1, y1) + rect.position; Vector2 posMax = new Vector2(x2, y2) + rect.position; Vector2 localPosMin = new Vector2(x1, y1); @@ -1656,6 +1198,7 @@ namespace UnityEngine.UI } } } + if (hasBorder) { clipped = uvMax; @@ -1670,6 +1213,7 @@ namespace UnityEngine.UI clipped.y = uvMin.y + (uvMax.y - uvMin.y) * (yMax - y1) / (y2 - y1); y2 = yMax; } + AddQuad(toFill, new Vector2(0, y1) + rect.position, new Vector2(xMin, y2) + rect.position, @@ -1686,7 +1230,6 @@ namespace UnityEngine.UI new Vector2(outer.z, clipped.y), new Vector2(xMax / width, y1 / height), new Vector2(rect.width / width, y2 / height)); - } // Bottom and top tiled border @@ -1700,272 +1243,7 @@ namespace UnityEngine.UI clipped.x = uvMin.x + (uvMax.x - uvMin.x) * (xMax - x1) / (x2 - x1); x2 = xMax; } - AddQuad(toFill, - new Vector2(x1, 0) + rect.position, - new Vector2(x2, yMin) + rect.position, - color, - new Vector2(uvMin.x, outer.y), - new Vector2(clipped.x, uvMin.y), - new Vector2(x1 / width, 0), - new Vector2(x2 / width, yMin / height)); - AddQuad(toFill, - new Vector2(x1, yMax) + rect.position, - new Vector2(x2, rect.height) + rect.position, - color, - new Vector2(uvMin.x, uvMax.y), - new Vector2(clipped.x, outer.w), - new Vector2(x1 / width, yMax / height), - new Vector2(x2 / width, rect.height / height)); - } - // Corners - AddQuad(toFill, - new Vector2(0, 0) + rect.position, - new Vector2(xMin, yMin) + rect.position, - color, - new Vector2(outer.x, outer.y), - new Vector2(uvMin.x, uvMin.y), - new Vector2(0, 0), - new Vector2(xMin / width, yMin / height)); - AddQuad(toFill, - new Vector2(xMax, 0) + rect.position, - new Vector2(rect.width, yMin) + rect.position, - color, - new Vector2(uvMax.x, outer.y), - new Vector2(outer.z, uvMin.y), - new Vector2(xMax / width, 0), - new Vector2(rect.width / width, yMin / height)); - AddQuad(toFill, - new Vector2(0, yMax) + rect.position, - new Vector2(xMin, rect.height) + rect.position, - color, - new Vector2(outer.x, uvMax.y), - new Vector2(uvMin.x, outer.w), - new Vector2(0, yMax / height), - new Vector2(xMin / width, rect.height / height)); - AddQuad(toFill, - new Vector2(xMax, yMax) + rect.position, - new Vector2(rect.width, rect.height) + rect.position, - color, - new Vector2(uvMax.x, uvMax.y), - new Vector2(outer.z, outer.w), - new Vector2(xMax / width, yMax / height), - new Vector2(rect.width / width, rect.height / height)); - } - } - else - { - // Texture has no border, is in repeat mode and not packed. Use texture tiling. - Vector2 uvScale = new Vector2((xMax - xMin) / tileWidth, (yMax - yMin) / tileHeight); - - if (fillCenter) - { - AddQuad(toFill, new Vector2(xMin, yMin) + rect.position, new Vector2(xMax, yMax) + rect.position, color, Vector2.Scale(uvMin, uvScale), Vector2.Scale(uvMax, uvScale), Vector2.zero, Vector2.one); - } - } - } - private void GenerateTiledSprite(VertexHelper toFill, List vertlist) - { - Vector4 outer, inner, border; - Vector2 spriteSize; - - if (overrideSprite != null) - { - outer = Sprites.DataUtility.GetOuterUV(overrideSprite); - inner = Sprites.DataUtility.GetInnerUV(overrideSprite); - border = overrideSprite.border; - spriteSize = overrideSprite.rect.size; - } - else - { - outer = Vector4.zero; - inner = Vector4.zero; - border = Vector4.zero; - spriteSize = Vector2.one * 100; - } - - Rect rect = GetPixelRectByFlipDirection(flipMode, flipWithCopy, flipEdge, flipFillCenter); - - float tileWidth = (spriteSize.x - border.x - border.z) / pixelsPerUnit; - float tileHeight = (spriteSize.y - border.y - border.w) / pixelsPerUnit; - border = GetAdjustedBorders(border / pixelsPerUnit, rect); - - - var uvMin = new Vector2(inner.x, inner.y); - var uvMax = new Vector2(inner.z, inner.w); - - // Min to max max range for tiled region in coordinates relative to lower left corner. - float xMin = border.x; - float xMax = rect.width - border.z; - float yMin = border.y; - float yMax = rect.height - border.w; - - toFill.Clear(); - var clipped = uvMax; - - // if either width is zero we cant tile so just assume it was the full width. - if (tileWidth <= 0) - tileWidth = xMax - xMin; - - if (tileHeight <= 0) - tileHeight = yMax - yMin; - - if (overrideSprite != null && (hasBorder || overrideSprite.packed || overrideSprite.texture.wrapMode != TextureWrapMode.Repeat)) - { - // Sprite has border, or is not in repeat mode, or cannot be repeated because of packing. - // We cannot use texture tiling so we will generate a mesh of quads to tile the texture. - - // Evaluate how many vertices we will generate. Limit this number to something sane, - // especially since meshes can not have more than 65000 vertices. - - int nTilesW = 0; - int nTilesH = 0; - if (fillCenter) - { - nTilesW = (int)Math.Ceiling((xMax - xMin) / tileWidth); - nTilesH = (int)Math.Ceiling((yMax - yMin) / tileHeight); - - int nVertices = 0; - if (hasBorder) - { - nVertices = (nTilesW + 2) * (nTilesH + 2) * 4; // 4 vertices per tile - } - else - { - nVertices = nTilesW * nTilesH * 4; // 4 vertices per tile - } - - if (nVertices > 65000) - { - double maxTiles = 65000.0 / 4.0; // Max number of vertices is 65000; 4 vertices per tile. - double imageRatio; - if (hasBorder) - { - imageRatio = (nTilesW + 2.0) / (nTilesH + 2.0); - } - else - { - imageRatio = (double)nTilesW / nTilesH; - } - - double targetTilesW = Math.Sqrt(maxTiles / imageRatio); - double targetTilesH = targetTilesW * imageRatio; - if (hasBorder) - { - targetTilesW -= 2; - targetTilesH -= 2; - } - - nTilesW = (int)Math.Floor(targetTilesW); - nTilesH = (int)Math.Floor(targetTilesH); - - tileWidth = (xMax - xMin) / nTilesW; - tileHeight = (yMax - yMin) / nTilesH; - } - } - else - { - if (hasBorder) - { - // Texture on the border is repeated only in one direction. - nTilesW = (int)Math.Ceiling((xMax - xMin) / tileWidth); - nTilesH = (int)Math.Ceiling((yMax - yMin) / tileHeight); - int nVertices = (nTilesH + nTilesW + 2 /*corners*/) * 2 /*sides*/ * 4 /*vertices per tile*/; - if (nVertices > 65000) - { - double maxTiles = 65000.0 / 4.0; // Max number of vertices is 65000; 4 vertices per tile. - double imageRatio = (double)nTilesW / nTilesH; - double targetTilesW = (maxTiles - 4 /*corners*/) / (2 * (1.0 + imageRatio)); - double targetTilesH = targetTilesW * imageRatio; - - nTilesW = (int)Math.Floor(targetTilesW); - nTilesH = (int)Math.Floor(targetTilesH); - tileWidth = (xMax - xMin) / nTilesW; - tileHeight = (yMax - yMin) / nTilesH; - } - } - else - { - nTilesH = nTilesW = 0; - } - } - - if (fillCenter) - { - // TODO: we could share vertices between quads. If vertex sharing is implemented. update the computation for the number of vertices accordingly. - float width = nTilesW * tileWidth; - float height = nTilesH * tileHeight; - for (int j = 0; j < nTilesH; j++) - { - float y1 = yMin + j * tileHeight; - float y2 = yMin + (j + 1) * tileHeight; - if (y2 > yMax) - { - clipped.y = uvMin.y + (uvMax.y - uvMin.y) * (yMax - y1) / (y2 - y1); - y2 = yMax; - } - clipped.x = uvMax.x; - for (int i = 0; i < nTilesW; i++) - { - float x1 = xMin + i * tileWidth; - float x2 = xMin + (i + 1) * tileWidth; - if (x2 > xMax) - { - clipped.x = uvMin.x + (uvMax.x - uvMin.x) * (xMax - x1) / (x2 - x1); - x2 = xMax; - } - Vector2 posMin = new Vector2(x1, y1) + rect.position; - Vector2 posMax = new Vector2(x2, y2) + rect.position; - Vector2 localPosMin = new Vector2(x1, y1); - Vector2 localPosMax = new Vector2(x2, y2); - AddQuad(toFill, posMin, posMax, color, uvMin, clipped, new Vector2(localPosMin.x / width, localPosMin.y / height), new Vector2(localPosMax.x / width, localPosMax.y / height)); - } - } - } - if (hasBorder) - { - clipped = uvMax; - float width = nTilesW * tileWidth; - float height = nTilesH * tileHeight; - for (int j = 0; j < nTilesH; j++) - { - float y1 = yMin + j * tileHeight; - float y2 = yMin + (j + 1) * tileHeight; - if (y2 > yMax) - { - clipped.y = uvMin.y + (uvMax.y - uvMin.y) * (yMax - y1) / (y2 - y1); - y2 = yMax; - } - AddQuad(toFill, - new Vector2(0, y1) + rect.position, - new Vector2(xMin, y2) + rect.position, - color, - new Vector2(outer.x, uvMin.y), - new Vector2(uvMin.x, clipped.y), - new Vector2(0, y1 / height), - new Vector2(xMin / width, y2 / height)); - AddQuad(toFill, - new Vector2(xMax, y1) + rect.position, - new Vector2(rect.width, y2) + rect.position, - color, - new Vector2(uvMax.x, uvMin.y), - new Vector2(outer.z, clipped.y), - new Vector2(xMax / width, y1 / height), - new Vector2(rect.width / width, y2 / height)); - - } - - // Bottom and top tiled border - clipped = uvMax; - for (int i = 0; i < nTilesW; i++) - { - float x1 = xMin + i * tileWidth; - float x2 = xMin + (i + 1) * tileWidth; - if (x2 > xMax) - { - clipped.x = uvMin.x + (uvMax.x - uvMin.x) * (xMax - x1) / (x2 - x1); - x2 = xMax; - } AddQuad(toFill, new Vector2(x1, 0) + rect.position, new Vector2(x2, yMin) + rect.position, @@ -2084,13 +1362,11 @@ namespace UnityEngine.UI border[axis + 2] *= borderScaleRatio; } } + return border; } - static readonly Vector3[] s_Xy = new Vector3[4]; - static readonly Vector3[] s_Uv = new Vector3[4]; - static readonly Vector3[] s_Uv1 = new Vector3[4]; void GenerateFilledSprite(VertexHelper toFill, bool preserveAspect) { toFill.Clear(); @@ -2143,28 +1419,28 @@ namespace UnityEngine.UI } } - s_Xy[0] = new Vector2(v.x, v.y); - s_Xy[1] = new Vector2(v.x, v.w); - s_Xy[2] = new Vector2(v.z, v.w); - s_Xy[3] = new Vector2(v.z, v.y); + m_Xy[0] = new Vector2(v.x, v.y); + m_Xy[1] = new Vector2(v.x, v.w); + m_Xy[2] = new Vector2(v.z, v.w); + m_Xy[3] = new Vector2(v.z, v.y); - s_Uv[0] = new Vector2(tx0, ty0); - s_Uv[1] = new Vector2(tx0, ty1); - s_Uv[2] = new Vector2(tx1, ty1); - s_Uv[3] = new Vector2(tx1, ty0); + m_Uv[0] = new Vector2(tx0, ty0); + m_Uv[1] = new Vector2(tx0, ty1); + m_Uv[2] = new Vector2(tx1, ty1); + m_Uv[3] = new Vector2(tx1, ty0); - s_Uv1[0] = new Vector2(0, 0); - s_Uv1[1] = new Vector2(0, 1); - s_Uv1[2] = new Vector2(1, 1); - s_Uv1[3] = new Vector2(1, 0); + m_Uv1[0] = new Vector2(0, 0); + m_Uv1[1] = new Vector2(0, 1); + m_Uv1[2] = new Vector2(1, 1); + m_Uv1[3] = new Vector2(1, 0); { if (fillAmount < 1f && fillMethod != FillMethod.Horizontal && fillMethod != FillMethod.Vertical) { if (fillMethod == FillMethod.Radial90) { - if (RadialCut(s_Xy, s_Uv, fillAmount, fillClockwise, fillOrigin)) - AddQuad(toFill, s_Xy, color, s_Uv, s_Uv1); + if (RadialCut(m_Xy, m_Uv, fillAmount, fillClockwise, fillOrigin)) + AddQuad(toFill, m_Xy, color, m_Uv, m_Uv1); } else if (fillMethod == FillMethod.Radial180) { @@ -2204,31 +1480,31 @@ namespace UnityEngine.UI } } - s_Xy[0].x = Mathf.Lerp(v.x, v.z, fx0); - s_Xy[1].x = s_Xy[0].x; - s_Xy[2].x = Mathf.Lerp(v.x, v.z, fx1); - s_Xy[3].x = s_Xy[2].x; + m_Xy[0].x = Mathf.Lerp(v.x, v.z, fx0); + m_Xy[1].x = m_Xy[0].x; + m_Xy[2].x = Mathf.Lerp(v.x, v.z, fx1); + m_Xy[3].x = m_Xy[2].x; - s_Xy[0].y = Mathf.Lerp(v.y, v.w, fy0); - s_Xy[1].y = Mathf.Lerp(v.y, v.w, fy1); - s_Xy[2].y = s_Xy[1].y; - s_Xy[3].y = s_Xy[0].y; + m_Xy[0].y = Mathf.Lerp(v.y, v.w, fy0); + m_Xy[1].y = Mathf.Lerp(v.y, v.w, fy1); + m_Xy[2].y = m_Xy[1].y; + m_Xy[3].y = m_Xy[0].y; - s_Uv[0].x = Mathf.Lerp(tx0, tx1, fx0); - s_Uv[1].x = s_Uv[0].x; - s_Uv[2].x = Mathf.Lerp(tx0, tx1, fx1); - s_Uv[3].x = s_Uv[2].x; + m_Uv[0].x = Mathf.Lerp(tx0, tx1, fx0); + m_Uv[1].x = m_Uv[0].x; + m_Uv[2].x = Mathf.Lerp(tx0, tx1, fx1); + m_Uv[3].x = m_Uv[2].x; - s_Uv[0].y = Mathf.Lerp(ty0, ty1, fy0); - s_Uv[1].y = Mathf.Lerp(ty0, ty1, fy1); - s_Uv[2].y = s_Uv[1].y; - s_Uv[3].y = s_Uv[0].y; + m_Uv[0].y = Mathf.Lerp(ty0, ty1, fy0); + m_Uv[1].y = Mathf.Lerp(ty0, ty1, fy1); + m_Uv[2].y = m_Uv[1].y; + m_Uv[3].y = m_Uv[0].y; float val = fillClockwise ? fillAmount * 2f - side : fillAmount * 2f - (1 - side); - if (RadialCut(s_Xy, s_Uv, Mathf.Clamp01(val), fillClockwise, ((side + fillOrigin + 3) % 4))) + if (RadialCut(m_Xy, m_Uv, Mathf.Clamp01(val), fillClockwise, ((side + fillOrigin + 3) % 4))) { - AddQuad(toFill, s_Xy, color, s_Uv, s_Uv1); + AddQuad(toFill, m_Xy, color, m_Uv, m_Uv1); } } } @@ -2260,38 +1536,36 @@ namespace UnityEngine.UI fy1 = 1f; } - s_Xy[0].x = Mathf.Lerp(v.x, v.z, fx0); - s_Xy[1].x = s_Xy[0].x; - s_Xy[2].x = Mathf.Lerp(v.x, v.z, fx1); - s_Xy[3].x = s_Xy[2].x; + m_Xy[0].x = Mathf.Lerp(v.x, v.z, fx0); + m_Xy[1].x = m_Xy[0].x; + m_Xy[2].x = Mathf.Lerp(v.x, v.z, fx1); + m_Xy[3].x = m_Xy[2].x; - s_Xy[0].y = Mathf.Lerp(v.y, v.w, fy0); - s_Xy[1].y = Mathf.Lerp(v.y, v.w, fy1); - s_Xy[2].y = s_Xy[1].y; - s_Xy[3].y = s_Xy[0].y; + m_Xy[0].y = Mathf.Lerp(v.y, v.w, fy0); + m_Xy[1].y = Mathf.Lerp(v.y, v.w, fy1); + m_Xy[2].y = m_Xy[1].y; + m_Xy[3].y = m_Xy[0].y; - s_Uv[0].x = Mathf.Lerp(tx0, tx1, fx0); - s_Uv[1].x = s_Uv[0].x; - s_Uv[2].x = Mathf.Lerp(tx0, tx1, fx1); - s_Uv[3].x = s_Uv[2].x; + m_Uv[0].x = Mathf.Lerp(tx0, tx1, fx0); + m_Uv[1].x = m_Uv[0].x; + m_Uv[2].x = Mathf.Lerp(tx0, tx1, fx1); + m_Uv[3].x = m_Uv[2].x; - s_Uv[0].y = Mathf.Lerp(ty0, ty1, fy0); - s_Uv[1].y = Mathf.Lerp(ty0, ty1, fy1); - s_Uv[2].y = s_Uv[1].y; - s_Uv[3].y = s_Uv[0].y; + m_Uv[0].y = Mathf.Lerp(ty0, ty1, fy0); + m_Uv[1].y = Mathf.Lerp(ty0, ty1, fy1); + m_Uv[2].y = m_Uv[1].y; + m_Uv[3].y = m_Uv[0].y; - float val = fillClockwise ? - fillAmount * 4f - ((corner + fillOrigin) % 4) : - fillAmount * 4f - (3 - ((corner + fillOrigin) % 4)); + float val = fillClockwise ? fillAmount * 4f - ((corner + fillOrigin) % 4) : fillAmount * 4f - (3 - ((corner + fillOrigin) % 4)); - if (RadialCut(s_Xy, s_Uv, Mathf.Clamp01(val), fillClockwise, ((corner + 2) % 4))) - AddQuad(toFill, s_Xy, color, s_Uv, s_Uv1); + if (RadialCut(m_Xy, m_Uv, Mathf.Clamp01(val), fillClockwise, ((corner + 2) % 4))) + AddQuad(toFill, m_Xy, color, m_Uv, m_Uv1); } } } else { - AddQuad(toFill, s_Xy, color, s_Uv, s_Uv1); + AddQuad(toFill, m_Xy, color, m_Uv, m_Uv1); } } } @@ -2299,7 +1573,6 @@ namespace UnityEngine.UI /// /// Adjust the specified quad, making it be radially filled instead. /// - static bool RadialCut(Vector3[] xy, Vector3[] uv, float fill, bool invert, int corner) { // Nothing to fill @@ -2328,7 +1601,6 @@ namespace UnityEngine.UI /// /// Adjust the specified quad, making it be radially filled instead. /// - static void RadialCut(Vector3[] xy, float cos, float sin, bool invert, int corner) { int i0 = corner; @@ -2405,9 +1677,6 @@ namespace UnityEngine.UI } - - - static void AddQuad(VertexHelper vertexHelper, Vector3[] quadPositions, Color32 color, Vector3[] quadUVs) { int startIndex = vertexHelper.currentVertCount; @@ -2418,6 +1687,7 @@ namespace UnityEngine.UI vertexHelper.AddTriangle(startIndex, startIndex + 1, startIndex + 2); vertexHelper.AddTriangle(startIndex + 2, startIndex + 3, startIndex); } + static void AddQuad(VertexHelper vertexHelper, Vector2 posMin, Vector2 posMax, Color32 color, Vector2 uvMin, Vector2 uvMax) { int startIndex = vertexHelper.currentVertCount; @@ -2430,118 +1700,5 @@ namespace UnityEngine.UI vertexHelper.AddTriangle(startIndex, startIndex + 1, startIndex + 2); vertexHelper.AddTriangle(startIndex + 2, startIndex + 3, startIndex); } - void AddQuads(VertexHelper vertexHelper, List vlist) - { - #if UNITY_2022_2_OR_NEWER - if (m_GradientColor.mode == GradientMode.Blend || m_GradientColor.mode == GradientMode.PerceptualBlend) - #else - if (m_GradientColor.mode == GradientMode.Blend) - #endif - { - int startIndex = vertexHelper.currentVertCount; - - foreach (Vert2D v in vlist) - { - vertexHelper.AddVert(v.position, v.color, v.uv0); - } - for (; startIndex < vertexHelper.currentVertCount - 2; startIndex++) - { - vertexHelper.AddTriangle(startIndex, startIndex + 1, startIndex + 2); - } - } - else - { - List newvlist = new List(); - if (m_Direction == GradientDirection.Horizontal) - { - for (int i = 0; i < vlist.Count; i++) - { - if (i == 0 || i == 1) - { - Vert2D newvert = new Vert2D(vlist[i].type); - newvert.SetAlpha(vlist[i + 2].color.a); - newvert.SetColor(vlist[i + 2].color); - newvert.SetUV(vlist[i].uv0.x, vlist[i].uv0.y); - newvert.SetPosition(vlist[i].position); - newvlist.Add(newvert); - } - else if (i == vlist.Count - 2 || i == vlist.Count - 1) - { - newvlist.Add(vlist[i]); - - } - else - { - newvlist.Add(vlist[i]); - Vert2D newvert = new Vert2D(vlist[i].type); - newvert.SetAlpha(vlist[i + 2].color.a); - newvert.SetColor(vlist[i + 2].color); - newvert.SetUV(vlist[i].uv0.x, vlist[i].uv0.y); - newvert.SetPosition(vlist[i].position); - newvlist.Add(newvert); - } - } - } - else - { - for (int i = 0; i < vlist.Count; i++) - { - if (i == 0 || i == 1) - { - newvlist.Add(vlist[i]); - } - else if (i == vlist.Count - 2 || i == vlist.Count - 1) - { - Vert2D newvert = new Vert2D(vlist[i].type); - newvert.SetAlpha(vlist[i - 2].color.a); - newvert.SetColor(vlist[i - 2].color); - newvert.SetUV(vlist[i].uv0.x, vlist[i].uv0.y); - newvert.SetPosition(vlist[i].position); - newvlist.Add(newvert); - - } - else - { - - Vert2D newvert = new Vert2D(vlist[i].type); - newvert.SetAlpha(vlist[i - 2].color.a); - newvert.SetColor(vlist[i - 2].color); - newvert.SetUV(vlist[i].uv0.x, vlist[i].uv0.y); - newvert.SetPosition(vlist[i].position); - newvlist.Add(newvert); - newvlist.Add(vlist[i]); - } - } - } - foreach (Vert2D v in newvlist) - { - vertexHelper.AddVert(v.position, v.color, v.uv0); - } - int s = -1; - for (; s < newvlist.Count - 1; s += 4) - { - if (s == -1) - { - vertexHelper.AddTriangle(s + 1, s + 2, s + 3); - } - else - { - vertexHelper.AddTriangle(s, s + 2, s + 3); - } - if (s == newvlist.Count - 5) - { - vertexHelper.AddTriangle(s + 2, s + 3, s + 4); - } - else - { - vertexHelper.AddTriangle(s + 2, s + 3, s + 5); - } - } - } - } - - #endregion } - } - diff --git a/Runtime/UXComponent/Navigation/UXInputMode.cs b/Runtime/UXComponent/Navigation/UXInputMode.cs index 3007441..b161de5 100644 --- a/Runtime/UXComponent/Navigation/UXInputMode.cs +++ b/Runtime/UXComponent/Navigation/UXInputMode.cs @@ -4,7 +4,9 @@ namespace UnityEngine.UI public enum UXInputMode : byte { Pointer = 0, - Gamepad = 1 + Keyboard = 1, + Gamepad = 2, + Touch = 3 } } #endif diff --git a/Runtime/UXComponent/Navigation/UXInputModeService.cs b/Runtime/UXComponent/Navigation/UXInputModeService.cs index f75bb5e..9f07b11 100644 --- a/Runtime/UXComponent/Navigation/UXInputModeService.cs +++ b/Runtime/UXComponent/Navigation/UXInputModeService.cs @@ -7,10 +7,15 @@ namespace UnityEngine.UI { internal sealed class UXInputModeService : MonoBehaviour { + private const float StickThresholdSqr = 0.04f; + private const float AxisThreshold = 0.2f; + private static UXInputModeService _instance; private InputAction _pointerAction; + private InputAction _keyboardAction; private InputAction _gamepadAction; + private InputAction _touchAction; public static UXInputMode CurrentMode { get; private set; } = UXInputMode.Pointer; @@ -29,7 +34,7 @@ namespace UnityEngine.UI return _instance; } - var go = new GameObject("[UXInputModeService]"); + GameObject go = new GameObject("[UXInputModeService]"); go.hideFlags = HideFlags.HideAndDontSave; DontDestroyOnLoad(go); _instance = go.AddComponent(); @@ -47,16 +52,17 @@ namespace UnityEngine.UI _instance = this; DontDestroyOnLoad(gameObject); hideFlags = HideFlags.HideAndDontSave; + CreateActions(); } private void OnEnable() { - CreateActions(); + SetActionsEnabled(true); } private void OnDisable() { - DisposeActions(); + SetActionsEnabled(false); } private void OnDestroy() @@ -70,26 +76,63 @@ namespace UnityEngine.UI private void CreateActions() { - if (_pointerAction != null || _gamepadAction != null) + if (_pointerAction != null) { return; } _pointerAction = new InputAction("UXPointerInput", InputActionType.PassThrough); - _pointerAction.AddBinding("/anyKey"); _pointerAction.AddBinding("/delta"); _pointerAction.AddBinding("/scroll"); _pointerAction.AddBinding("/leftButton"); _pointerAction.AddBinding("/rightButton"); _pointerAction.AddBinding("/middleButton"); _pointerAction.performed += OnPointerInput; - _pointerAction.Enable(); + + _keyboardAction = new InputAction("UXKeyboardInput", InputActionType.PassThrough); + _keyboardAction.AddBinding("/anyKey"); + _keyboardAction.performed += OnKeyboardInput; _gamepadAction = new InputAction("UXGamepadInput", InputActionType.PassThrough); - _gamepadAction.AddBinding("/*"); - _gamepadAction.AddBinding("/*"); + _gamepadAction.AddBinding("/buttonSouth"); + _gamepadAction.AddBinding("/buttonNorth"); + _gamepadAction.AddBinding("/buttonEast"); + _gamepadAction.AddBinding("/buttonWest"); + _gamepadAction.AddBinding("/startButton"); + _gamepadAction.AddBinding("/selectButton"); + _gamepadAction.AddBinding("/leftShoulder"); + _gamepadAction.AddBinding("/rightShoulder"); + _gamepadAction.AddBinding("/dpad"); + _gamepadAction.AddBinding("/leftStick"); + _gamepadAction.AddBinding("/rightStick"); _gamepadAction.performed += OnGamepadInput; - _gamepadAction.Enable(); + + _touchAction = new InputAction("UXTouchInput", InputActionType.PassThrough); + _touchAction.AddBinding("/primaryTouch/press"); + _touchAction.AddBinding("/primaryTouch/delta"); + _touchAction.performed += OnTouchInput; + } + + private void SetActionsEnabled(bool enabled) + { + if (_pointerAction == null) + { + return; + } + + if (enabled) + { + _pointerAction.Enable(); + _keyboardAction.Enable(); + _gamepadAction.Enable(); + _touchAction.Enable(); + return; + } + + _pointerAction.Disable(); + _keyboardAction.Disable(); + _gamepadAction.Disable(); + _touchAction.Disable(); } private void DisposeActions() @@ -97,38 +140,62 @@ namespace UnityEngine.UI if (_pointerAction != null) { _pointerAction.performed -= OnPointerInput; - _pointerAction.Disable(); _pointerAction.Dispose(); _pointerAction = null; } + if (_keyboardAction != null) + { + _keyboardAction.performed -= OnKeyboardInput; + _keyboardAction.Dispose(); + _keyboardAction = null; + } + if (_gamepadAction != null) { _gamepadAction.performed -= OnGamepadInput; - _gamepadAction.Disable(); _gamepadAction.Dispose(); _gamepadAction = null; } + + if (_touchAction != null) + { + _touchAction.performed -= OnTouchInput; + _touchAction.Dispose(); + _touchAction = null; + } } private static void OnPointerInput(InputAction.CallbackContext context) { - if (!IsInputMeaningful(context.control)) + if (IsInputMeaningful(context.control)) { - return; + SetMode(UXInputMode.Pointer); } + } - SetMode(UXInputMode.Pointer); + private static void OnKeyboardInput(InputAction.CallbackContext context) + { + if (IsInputMeaningful(context.control)) + { + SetMode(UXInputMode.Keyboard); + } } private static void OnGamepadInput(InputAction.CallbackContext context) { - if (!IsInputMeaningful(context.control)) + if (IsInputMeaningful(context.control)) { - return; + SetMode(UXInputMode.Gamepad); } + } - SetMode(UXInputMode.Gamepad); + private static void OnTouchInput(InputAction.CallbackContext context) + { + if (IsInputMeaningful(context.control)) + { + SetMode(UXInputMode.Touch); + } } private static bool IsInputMeaningful(InputControl control) @@ -143,11 +210,11 @@ namespace UnityEngine.UI case ButtonControl button: return button.IsPressed(); case StickControl stick: - return stick.ReadValue().sqrMagnitude >= 0.04f; + return stick.ReadValue().sqrMagnitude >= StickThresholdSqr; case Vector2Control vector2: - return vector2.ReadValue().sqrMagnitude >= 0.04f; + return vector2.ReadValue().sqrMagnitude >= StickThresholdSqr; case AxisControl axis: - return Mathf.Abs(axis.ReadValue()) >= 0.2f; + return Mathf.Abs(axis.ReadValue()) >= AxisThreshold; default: return !control.noisy; } diff --git a/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs b/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs deleted file mode 100644 index c86c411..0000000 --- a/Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs +++ /dev/null @@ -1,20 +0,0 @@ -#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION -namespace UnityEngine.UI -{ - [DisallowMultipleComponent] - internal sealed class UXNavigationLayerWatcher : MonoBehaviour - { - private UXNavigationRuntime _runtime; - - internal void Initialize(UXNavigationRuntime runtime) - { - _runtime = runtime; - } - - private void OnTransformChildrenChanged() - { - _runtime?.MarkDiscoveryDirty(); - } - } -} -#endif diff --git a/Runtime/UXComponent/Navigation/UXNavigationRuntime.cs b/Runtime/UXComponent/Navigation/UXNavigationRuntime.cs index b7f7094..e81cee6 100644 --- a/Runtime/UXComponent/Navigation/UXNavigationRuntime.cs +++ b/Runtime/UXComponent/Navigation/UXNavigationRuntime.cs @@ -1,35 +1,42 @@ #if INPUTSYSTEM_SUPPORT && UX_NAVIGATION -using System.Collections.Generic; using AlicizaX; using AlicizaX.UI.Runtime; using UnityEngine.EventSystems; namespace UnityEngine.UI { - internal sealed class UXNavigationRuntime : MonoBehaviour + public interface IUXNavigationCursorPolicy { + void OnInputModeChanged(UXInputMode mode, UXNavigationScope topScope); + } + + public sealed class UXNavigationRuntime : MonoBehaviour + { + private const int InitialScopeCapacity = 64; + private const int InvalidIndex = -1; + private static UXNavigationRuntime _instance; - private static readonly string CacheLayerName = $"Layer{(int)UILayer.All}-{UILayer.All}"; + private static IUXNavigationCursorPolicy _cursorPolicy; - private readonly HashSet _scopeSet = new(); - private readonly List _scopes = new(32); - private readonly HashSet _interactiveLayerRoots = new(); - private readonly List _holderBuffer = new(32); + private UXNavigationScope[] _scopes = new UXNavigationScope[InitialScopeCapacity]; + private int[] _freeIndices = new int[InitialScopeCapacity]; + private int _freeCount; + private int _scopeCount; + private int _scopeCapacityHighWater; - private Transform _uiCanvasRoot; private UXNavigationScope _topScope; - private GameObject _lastObservedSelection; - private bool _discoveryDirty = true; private ulong _activationSerial; - private bool _missingEventSystemLogged; - - private bool _topScopeDirty = true; + private bool _stateDirty = true; private bool _suppressionDirty = true; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void Bootstrap() { - if (!AppServices.TryGetApp(out var uiService) || uiService == null) return; + if (!AppServices.TryGetApp(out var uiService) || uiService == null) + { + return; + } + EnsureInstance(); UXInputModeService.EnsureInstance(); } @@ -41,7 +48,7 @@ namespace UnityEngine.UI return _instance; } - var go = new GameObject("[UXNavigationRuntime]"); + GameObject go = new GameObject("[UXNavigationRuntime]"); go.hideFlags = HideFlags.HideAndDontSave; DontDestroyOnLoad(go); _instance = go.AddComponent(); @@ -54,14 +61,33 @@ namespace UnityEngine.UI return runtime != null; } - internal static bool IsHolderWithinTopScope(UIHolderObjectBase holder) + public static void SetCursorPolicy(IUXNavigationCursorPolicy cursorPolicy) + { + _cursorPolicy = cursorPolicy; + } + + public static void NotifySelection(GameObject selectedObject) + { + if (_instance != null) + { + _instance.RecordSelection(selectedObject); + } + } + + public static bool IsHolderWithinTopScope(UIHolderObjectBase holder) { if (_instance == null || _instance._topScope == null || holder == null) { return true; } - return _instance.IsHolderOwnedByScope(holder, _instance._topScope); + UXNavigationScope scope = holder.GetComponent(); + if (scope == null) + { + scope = holder.GetComponentInParent(true); + } + + return scope == _instance._topScope; } private void Awake() @@ -98,19 +124,42 @@ namespace UnityEngine.UI internal void RegisterScope(UXNavigationScope scope) { - if (scope == null || !_scopeSet.Add(scope)) + if (scope == null || scope.RuntimeIndex != InvalidIndex) { return; } - _scopes.Add(scope); - _topScopeDirty = true; - _suppressionDirty = true; + int index; + if (_freeCount > 0) + { + index = _freeIndices[--_freeCount]; + } + else + { + if (_scopeCapacityHighWater >= _scopes.Length) + { + ReportCapacityExceeded("UXNavigationRuntime scope capacity exceeded."); + return; + } + + index = _scopeCapacityHighWater++; + } + + _scopes[index] = scope; + scope.RuntimeIndex = index; + _scopeCount++; + MarkStateDirty(); } internal void UnregisterScope(UXNavigationScope scope) { - if (scope == null || !_scopeSet.Remove(scope)) + if (scope == null) + { + return; + } + + int index = scope.RuntimeIndex; + if (index < 0 || index >= _scopes.Length || _scopes[index] != scope) { return; } @@ -121,42 +170,46 @@ namespace UnityEngine.UI } scope.IsAvailable = false; + scope.WasAvailable = false; scope.SetNavigationSuppressed(false); + scope.RuntimeIndex = InvalidIndex; + _scopes[index] = null; + _freeIndices[_freeCount++] = index; + _scopeCount--; + MarkStateDirty(); + } - int idx = _scopes.IndexOf(scope); - if (idx >= 0) - { - int last = _scopes.Count - 1; - _scopes[idx] = _scopes[last]; - _scopes.RemoveAt(last); - } - - _topScopeDirty = true; + internal void MarkStateDirty() + { + _stateDirty = true; _suppressionDirty = true; } - internal void MarkDiscoveryDirty() + internal void MarkSuppressionDirty() { - _discoveryDirty = true; + _suppressionDirty = true; } - private void Update() + internal void InvalidateSkipCaches() { - TryBindUIRoot(); - if (_uiCanvasRoot == null) + for (int i = 0; i < _scopeCapacityHighWater; i++) { - return; + UXNavigationScope scope = _scopes[i]; + if (scope != null) + { + scope.InvalidateSkipCacheOnly(); + } } - if (_discoveryDirty) - { - DiscoverScopes(); - } + MarkStateDirty(); + } - if (_topScopeDirty) + private void FlushStateIfDirty() + { + if (_stateDirty) { UXNavigationScope newTopScope = FindTopScope(); - _topScopeDirty = false; + _stateDirty = false; if (!ReferenceEquals(_topScope, newTopScope)) { _topScope = newTopScope; @@ -170,122 +223,16 @@ namespace UnityEngine.UI _suppressionDirty = false; } - if (UXInputModeService.CurrentMode == UXInputMode.Gamepad) + if (UXInputModeService.CurrentMode == UXInputMode.Gamepad || UXInputModeService.CurrentMode == UXInputMode.Keyboard) { - EnsureGamepadSelection(); - if (_topScope != null) - { - Cursor.visible = false; - } + EnsureNavigationSelection(); } - - TrackSelection(); - } - - private void TryBindUIRoot() - { - if (_uiCanvasRoot != null) - { - return; - } - - IUIService uiModule = AppServices.RequireApp(); - if (uiModule?.UICanvasRoot == null) - { - return; - } - - _uiCanvasRoot = uiModule.UICanvasRoot; - EnsureWatcher(_uiCanvasRoot); - CacheInteractiveLayers(); - DiscoverScopes(); - } - - private void CacheInteractiveLayers() - { - _interactiveLayerRoots.Clear(); - if (_uiCanvasRoot == null) - { - return; - } - - EnsureWatcher(_uiCanvasRoot); - for (int i = 0; i < _uiCanvasRoot.childCount; i++) - { - Transform child = _uiCanvasRoot.GetChild(i); - if (child == null || child.name == CacheLayerName) - { - continue; - } - - _interactiveLayerRoots.Add(child); - EnsureWatcher(child); - } - } - - private void DiscoverScopes() - { - _discoveryDirty = false; - CacheInteractiveLayers(); - bool addedScope = false; - - foreach (Transform layerRoot in _interactiveLayerRoots) - { - if (layerRoot == null || IsNavigationSkipped(layerRoot)) - { - continue; - } - - _holderBuffer.Clear(); - layerRoot.GetComponentsInChildren(true, _holderBuffer); - for (int i = 0; i < _holderBuffer.Count; i++) - { - UIHolderObjectBase holder = _holderBuffer[i]; - if (holder == null - || holder.GetComponent() != null - || IsNavigationSkipped(holder.transform)) - { - continue; - } - - holder.gameObject.AddComponent(); - addedScope = true; - } - } - - _holderBuffer.Clear(); - - if (addedScope) - { - for (int i = 0; i < _scopes.Count; i++) - { - _scopes[i]?.InvalidateSelectableCache(); - } - - _topScopeDirty = true; - _suppressionDirty = true; - } - } - - private void EnsureWatcher(Transform target) - { - if (target == null) - { - return; - } - - if (!target.TryGetComponent(out UXNavigationLayerWatcher watcher)) - { - watcher = target.gameObject.AddComponent(); - } - - watcher.Initialize(this); } private UXNavigationScope FindTopScope() { UXNavigationScope bestScope = null; - for (int i = 0; i < _scopes.Count; i++) + for (int i = 0; i < _scopeCapacityHighWater; i++) { UXNavigationScope scope = _scopes[i]; if (scope == null) @@ -306,12 +253,7 @@ namespace UnityEngine.UI _suppressionDirty = true; } - if (!available) - { - continue; - } - - if (bestScope == null || CompareScopePriority(scope, bestScope) < 0) + if (available && (bestScope == null || IsHigherPriority(scope, bestScope))) { bestScope = scope; } @@ -320,7 +262,7 @@ namespace UnityEngine.UI return bestScope; } - private bool IsScopeAvailable(UXNavigationScope scope) + private static bool IsScopeAvailable(UXNavigationScope scope) { if (scope == null || !scope.isActiveAndEnabled || !scope.gameObject.activeInHierarchy) { @@ -328,64 +270,15 @@ namespace UnityEngine.UI } Canvas canvas = scope.Canvas; - if (canvas == null || canvas.gameObject.layer != UIComponent.UIShowLayer) - { - return false; - } - - return !scope.IsNavigationSkipped - && scope.HasAvailableSelectable() - && TryGetInteractiveLayerRoot(scope.transform, out _); - } - - // 保留静态方法用于 DiscoverScopes 中对 LayerRoot / Holder 节点的检测 - // 这些节点无 UXNavigationScope,调用频次低(仅在 dirty 时),无需缓存 - private static bool IsNavigationSkipped(Transform current) - { - return current != null && current.GetComponentInParent(true) != null; - } - - private bool TryGetInteractiveLayerRoot(Transform current, out Transform layerRoot) - { - layerRoot = null; - if (current == null || _uiCanvasRoot == null) - { - return false; - } - - while (current != null) - { - if (current.parent == _uiCanvasRoot) - { - layerRoot = current; - return _interactiveLayerRoots.Contains(current); - } - - current = current.parent; - } - - return false; - } - - private bool IsHolderOwnedByScope(UIHolderObjectBase holder, UXNavigationScope scope) - { - if (holder == null || scope == null) - { - return false; - } - - UXNavigationScope nearestScope = holder.GetComponent(); - if (nearestScope == null) - { - nearestScope = holder.GetComponentInParent(true); - } - - return nearestScope == scope; + return canvas != null + && canvas.gameObject.layer == UIComponent.UIShowLayer + && !scope.IsNavigationSkipped + && scope.HasAvailableSelectable(); } private void ApplyScopeSuppression() { - for (int i = 0; i < _scopes.Count; i++) + for (int i = 0; i < _scopeCapacityHighWater; i++) { UXNavigationScope scope = _scopes[i]; if (scope == null) @@ -395,30 +288,17 @@ namespace UnityEngine.UI bool suppress = scope.IsAvailable && _topScope != null - && !ReferenceEquals(scope, _topScope) + && scope != _topScope && _topScope.BlockLowerScopes - && CompareScopePriority(_topScope, scope) < 0; + && IsHigherPriority(_topScope, scope); scope.SetNavigationSuppressed(suppress); } } - private void EnsureGamepadSelection() + private void EnsureNavigationSelection() { EventSystem eventSystem = EventSystem.current; - if (eventSystem == null) - { - if (!_missingEventSystemLogged) - { - Debug.LogWarning("UXNavigationRuntime requires an active EventSystem for gamepad navigation."); - _missingEventSystemLogged = true; - } - - return; - } - - _missingEventSystemLogged = false; - - if (_topScope == null || !_topScope.RequireSelectionWhenGamepad) + if (eventSystem == null || _topScope == null || !_topScope.RequireSelectionWhenGamepad) { return; } @@ -432,78 +312,60 @@ namespace UnityEngine.UI Selectable preferred = _topScope.GetPreferredSelectable(); eventSystem.SetSelectedGameObject(preferred != null ? preferred.gameObject : null); - _lastObservedSelection = eventSystem.currentSelectedGameObject; - if (_lastObservedSelection != null) + GameObject selectedObject = eventSystem.currentSelectedGameObject; + if (selectedObject != null) { - _topScope.RecordSelection(_lastObservedSelection); + _topScope.RecordSelection(selectedObject); } } - private void TrackSelection() + private void RecordSelection(GameObject selectedObject) { - EventSystem eventSystem = EventSystem.current; - if (eventSystem == null) + if (_stateDirty || _suppressionDirty) { - _lastObservedSelection = null; - return; + FlushStateIfDirty(); } - GameObject currentSelected = eventSystem.currentSelectedGameObject; - if (ReferenceEquals(_lastObservedSelection, currentSelected)) + if (_topScope != null && _topScope.IsSelectableOwnedAndValid(selectedObject)) { - return; - } - - _lastObservedSelection = currentSelected; - if (_topScope != null && _topScope.IsSelectableOwnedAndValid(currentSelected)) - { - _topScope.RecordSelection(currentSelected); + _topScope.RecordSelection(selectedObject); } } private void OnInputModeChanged(UXInputMode mode) { - EventSystem eventSystem = EventSystem.current; - if (mode == UXInputMode.Pointer) + _cursorPolicy?.OnInputModeChanged(mode, _topScope); + if (mode == UXInputMode.Gamepad || mode == UXInputMode.Keyboard) { - if (_topScope != null) - { - Cursor.visible = true; - if (eventSystem != null) - { - eventSystem.SetSelectedGameObject(null); - } - } - - _lastObservedSelection = null; - return; + FlushStateIfDirty(); + EnsureNavigationSelection(); } - - if (_topScope != null) - { - Cursor.visible = false; - } - - EnsureGamepadSelection(); } - private static int CompareScopePriority(UXNavigationScope left, UXNavigationScope right) + private static bool IsHigherPriority(UXNavigationScope left, UXNavigationScope right) { int leftOrder = left.Canvas != null ? left.Canvas.sortingOrder : int.MinValue; int rightOrder = right.Canvas != null ? right.Canvas.sortingOrder : int.MinValue; - int orderCompare = rightOrder.CompareTo(leftOrder); - if (orderCompare != 0) + if (leftOrder != rightOrder) { - return orderCompare; + return leftOrder > rightOrder; } - int hierarchyCompare = right.GetHierarchyDepth().CompareTo(left.GetHierarchyDepth()); - if (hierarchyCompare != 0) + int leftDepth = left.GetHierarchyDepth(); + int rightDepth = right.GetHierarchyDepth(); + if (leftDepth != rightDepth) { - return hierarchyCompare; + return leftDepth > rightDepth; } - return right.ActivationSerial.CompareTo(left.ActivationSerial); + return left.ActivationSerial > right.ActivationSerial; + } + + private static void ReportCapacityExceeded(string message) + { +#if UNITY_EDITOR || DEVELOPMENT_BUILD + Debug.LogError(message); +#endif } } } diff --git a/Runtime/UXComponent/Navigation/UXNavigationScope.cs b/Runtime/UXComponent/Navigation/UXNavigationScope.cs index 6d578cd..da540d4 100644 --- a/Runtime/UXComponent/Navigation/UXNavigationScope.cs +++ b/Runtime/UXComponent/Navigation/UXNavigationScope.cs @@ -1,49 +1,52 @@ #if INPUTSYSTEM_SUPPORT && UX_NAVIGATION -using System.Collections.Generic; using AlicizaX.UI.Runtime; +using UnityEngine.EventSystems; namespace UnityEngine.UI { [DisallowMultipleComponent] [AddComponentMenu("UI/UX Navigation Scope")] - public sealed class UXNavigationScope : MonoBehaviour + public sealed class UXNavigationScope : MonoBehaviour, ISelectHandler { + private const int InvalidIndex = -1; + private const int DefaultRuntimeSelectableCapacity = 16; + [SerializeField, Header("默认选中控件")] private Selectable _defaultSelectable; - - [SerializeField, Header("显式导航控件列表")] private List _explicitSelectables = new(); - + [SerializeField, Header("编辑器烘焙导航控件")] private Selectable[] _bakedSelectables = System.Array.Empty(); + [SerializeField, Header("动态控件容量")] private int _runtimeSelectableCapacity = DefaultRuntimeSelectableCapacity; [SerializeField, Header("记住上次选中")] private bool _rememberLastSelection = true; - - [SerializeField, Header("手柄模式必须有选中")] private bool _requireSelectionWhenGamepad = true; - + [SerializeField, Header("手柄/键盘模式必须有选中")] private bool _requireSelectionWhenGamepad = true; [SerializeField, Header("阻断下层导航域")] private bool _blockLowerScopes = true; - [SerializeField, Header("自动选中首个可用控件")] private bool _autoSelectFirstAvailable = true; - private readonly List _cachedSelectables = new(16); - private readonly HashSet _cachedSelectableSet = new(); - private readonly Dictionary _baselineNavigation = new(); - private readonly List _removeBuffer = new(8); - private readonly List _selectableScanBuffer = new(16); - + private Selectable[] _runtimeSelectables; + private Navigation[] _bakedBaselineNavigation; + private Navigation[] _runtimeBaselineNavigation; private Canvas _cachedCanvas; private UIHolderObjectBase _cachedHolder; private Selectable _lastSelected; - private bool _cacheDirty = true; private bool _navigationSuppressed; private int _cachedHierarchyDepth = -1; - private bool _cachedIsSkipped; private bool _isSkippedCacheValid; + private int _runtimeSelectableCount; + internal int RuntimeIndex { get; set; } = InvalidIndex; internal ulong ActivationSerial { get; set; } internal bool IsAvailable { get; set; } internal bool WasAvailable { get; set; } + public bool NavigationSuppressed => _navigationSuppressed; + internal int BakedSelectableCount => _bakedSelectables != null ? _bakedSelectables.Length : 0; + public int RuntimeSelectableCount => _runtimeSelectableCount; public Selectable DefaultSelectable { get => _defaultSelectable; - set => _defaultSelectable = value; + set + { + _defaultSelectable = value; + MarkRuntimeStateDirty(); + } } public bool RememberLastSelection => _rememberLastSelection; @@ -59,6 +62,7 @@ namespace UnityEngine.UI _cachedIsSkipped = GetComponentInParent(true) != null; _isSkippedCacheValid = true; } + return _cachedIsSkipped; } } @@ -93,9 +97,16 @@ namespace UnityEngine.UI } } + private void Awake() + { + EnsureRuntimeBuffers(); + CaptureAllBaselines(); + } + private void OnEnable() { - _cacheDirty = true; + EnsureRuntimeBuffers(); + CaptureAllBaselines(); UXNavigationRuntime.EnsureInstance().RegisterScope(this); } @@ -115,25 +126,108 @@ namespace UnityEngine.UI private void OnTransformChildrenChanged() { - _cacheDirty = true; + MarkRuntimeStateDirty(); } private void OnTransformParentChanged() { - _cacheDirty = true; _cachedCanvas = null; _cachedHolder = null; _cachedHierarchyDepth = -1; _isSkippedCacheValid = false; + MarkRuntimeStateDirty(); } #if UNITY_EDITOR private void OnValidate() { - _cacheDirty = true; + if (_runtimeSelectableCapacity < 0) + { + _runtimeSelectableCapacity = 0; + } } #endif + public bool RegisterSelectable(Selectable selectable) + { + if (selectable == null || !Owns(selectable.gameObject) || ContainsSelectable(selectable)) + { + return false; + } + + EnsureRuntimeBuffers(); + if (_runtimeSelectableCount >= _runtimeSelectables.Length) + { + ReportCapacityExceeded(); + return false; + } + + _runtimeSelectables[_runtimeSelectableCount] = selectable; + _runtimeBaselineNavigation[_runtimeSelectableCount] = selectable.navigation; + _runtimeSelectableCount++; + if (_navigationSuppressed) + { + SetSelectableSuppressed(selectable, true); + } + + MarkRuntimeStateDirty(); + return true; + } + + public bool UnregisterSelectable(Selectable selectable) + { + if (selectable == null || _runtimeSelectables == null) + { + return false; + } + + for (int i = 0; i < _runtimeSelectableCount; i++) + { + if (_runtimeSelectables[i] != selectable) + { + continue; + } + + if (_navigationSuppressed) + { + selectable.navigation = _runtimeBaselineNavigation[i]; + } + + int last = _runtimeSelectableCount - 1; + _runtimeSelectables[i] = _runtimeSelectables[last]; + _runtimeBaselineNavigation[i] = _runtimeBaselineNavigation[last]; + _runtimeSelectables[last] = null; + _runtimeBaselineNavigation[last] = default(Navigation); + _runtimeSelectableCount--; + if (_lastSelected == selectable) + { + _lastSelected = null; + } + + MarkRuntimeStateDirty(); + return true; + } + + return false; + } + + public void InvalidateSelectableCache() + { + CaptureAllBaselines(); + MarkRuntimeStateDirty(); + } + + public void OnSelect(BaseEventData eventData) + { + GameObject selectedObject = eventData != null ? eventData.selectedObject : null; + UXNavigationRuntime.NotifySelection(selectedObject); + } + + internal void InvalidateSkipCacheOnly() + { + _isSkippedCacheValid = false; + } + internal int GetHierarchyDepth() { if (_cachedHierarchyDepth >= 0) @@ -160,14 +254,12 @@ namespace UnityEngine.UI return false; } - var nearestScope = target.GetComponentInParent(true); + UXNavigationScope nearestScope = target.GetComponentInParent(true); return nearestScope == this; } internal Selectable GetPreferredSelectable() { - RefreshSelectableCache(); - if (_rememberLastSelection && IsSelectableValid(_lastSelected)) { return _lastSelected; @@ -183,36 +275,20 @@ namespace UnityEngine.UI return null; } - for (int i = 0; i < _cachedSelectables.Count; i++) + Selectable selectable = FirstUsable(_bakedSelectables, BakedSelectableCount); + if (selectable != null) { - Selectable selectable = _cachedSelectables[i]; - if (IsSelectableUsable(selectable)) - { - return selectable; - } + return selectable; } - return null; + return FirstUsable(_runtimeSelectables, _runtimeSelectableCount); } internal bool HasAvailableSelectable() { - RefreshSelectableCache(); - - if (IsSelectableValid(_defaultSelectable)) - { - return true; - } - - for (int i = 0; i < _cachedSelectables.Count; i++) - { - if (IsSelectableUsable(_cachedSelectables[i])) - { - return true; - } - } - - return false; + return IsSelectableValid(_defaultSelectable) + || FirstUsable(_bakedSelectables, BakedSelectableCount) != null + || FirstUsable(_runtimeSelectables, _runtimeSelectableCount) != null; } internal void RecordSelection(GameObject selectedObject) @@ -241,109 +317,142 @@ namespace UnityEngine.UI return; } - RefreshSelectableCache(); + CaptureAllBaselines(); _navigationSuppressed = suppressed; - for (int i = 0; i < _cachedSelectables.Count; i++) + ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, suppressed); + ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, suppressed); + } + + private void EnsureRuntimeBuffers() + { + int capacity = _runtimeSelectableCapacity > 0 ? _runtimeSelectableCapacity : 0; + if (_runtimeSelectables == null || _runtimeSelectables.Length != capacity) { - Selectable selectable = _cachedSelectables[i]; + _runtimeSelectables = capacity > 0 ? new Selectable[capacity] : System.Array.Empty(); + _runtimeBaselineNavigation = capacity > 0 ? new Navigation[capacity] : System.Array.Empty(); + _runtimeSelectableCount = 0; + } + + int bakedCount = BakedSelectableCount; + if (_bakedBaselineNavigation == null || _bakedBaselineNavigation.Length != bakedCount) + { + _bakedBaselineNavigation = bakedCount > 0 ? new Navigation[bakedCount] : System.Array.Empty(); + } + } + + private void CaptureAllBaselines() + { + EnsureRuntimeBuffers(); + CaptureBaseline(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount); + CaptureBaseline(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount); + } + + private static void CaptureBaseline(Selectable[] selectables, Navigation[] baseline, int count) + { + if (selectables == null || baseline == null) + { + return; + } + + for (int i = 0; i < count; i++) + { + Selectable selectable = selectables[i]; + if (selectable != null) + { + baseline[i] = selectable.navigation; + } + } + } + + private static void ApplySuppression(Selectable[] selectables, Navigation[] baseline, int count, bool suppressed) + { + if (selectables == null || baseline == null) + { + return; + } + + for (int i = 0; i < count; i++) + { + Selectable selectable = selectables[i]; if (selectable == null) { continue; } - if (!_baselineNavigation.ContainsKey(selectable)) - { - _baselineNavigation[selectable] = selectable.navigation; - } - if (suppressed) { - Navigation navigation = selectable.navigation; - navigation.mode = Navigation.Mode.None; - selectable.navigation = navigation; + SetSelectableSuppressed(selectable, true); } - else if (_baselineNavigation.TryGetValue(selectable, out Navigation navigation)) + else { - selectable.navigation = navigation; + selectable.navigation = baseline[i]; } } } - internal void RefreshSelectableCache() + private static void SetSelectableSuppressed(Selectable selectable, bool suppressed) { - if (!_cacheDirty) + if (selectable == null || !suppressed) { return; } - _cacheDirty = false; - _cachedSelectables.Clear(); - _cachedSelectableSet.Clear(); - - if (_explicitSelectables != null && _explicitSelectables.Count > 0) - { - for (int i = 0; i < _explicitSelectables.Count; i++) - { - TryAddSelectable(_explicitSelectables[i]); - } - } - else - { - _selectableScanBuffer.Clear(); - GetComponentsInChildren(true, _selectableScanBuffer); - for (int i = 0; i < _selectableScanBuffer.Count; i++) - { - TryAddSelectable(_selectableScanBuffer[i]); - } - } - - _removeBuffer.Clear(); - foreach (Selectable key in _baselineNavigation.Keys) - { - if (!_cachedSelectableSet.Contains(key)) - { - _removeBuffer.Add(key); - } - } - - for (int i = 0; i < _removeBuffer.Count; i++) - { - _baselineNavigation.Remove(_removeBuffer[i]); - } - - _selectableScanBuffer.Clear(); + Navigation navigation = selectable.navigation; + navigation.mode = Navigation.Mode.None; + selectable.navigation = navigation; } - public void InvalidateSelectableCache() + private bool ContainsSelectable(Selectable selectable) { - _cacheDirty = true; - } - - private void TryAddSelectable(Selectable selectable) - { - if (selectable == null || !Owns(selectable.gameObject) || !_cachedSelectableSet.Add(selectable)) - { - return; - } - - _cachedSelectables.Add(selectable); - if (!_baselineNavigation.ContainsKey(selectable) || !_navigationSuppressed) - { - _baselineNavigation[selectable] = selectable.navigation; - } + return IndexOf(_bakedSelectables, BakedSelectableCount, selectable) >= 0 + || IndexOf(_runtimeSelectables, _runtimeSelectableCount, selectable) >= 0; } private bool IsSelectableValid(Selectable selectable) { - return IsSelectableUsable(selectable) - && (_cachedSelectableSet.Contains(selectable) || Owns(selectable.gameObject)); + return IsSelectableUsable(selectable) && ContainsSelectable(selectable); + } + + private static Selectable FirstUsable(Selectable[] selectables, int count) + { + if (selectables == null) + { + return null; + } + + for (int i = 0; i < count; i++) + { + Selectable selectable = selectables[i]; + if (IsSelectableUsable(selectable)) + { + return selectable; + } + } + + return null; + } + + private static int IndexOf(Selectable[] selectables, int count, Selectable selectable) + { + if (selectables == null || selectable == null) + { + return InvalidIndex; + } + + for (int i = 0; i < count; i++) + { + if (selectables[i] == selectable) + { + return i; + } + } + + return InvalidIndex; } private static bool IsSelectableUsable(Selectable selectable) { - return selectable != null - && selectable.IsActive() - && selectable.IsInteractable(); + return selectable != null && selectable.IsActive() && selectable.IsInteractable(); } private static Selectable GetSelectableFromObject(GameObject selectedObject) @@ -357,6 +466,21 @@ namespace UnityEngine.UI ? selectable : selectedObject.GetComponentInParent(); } + + private void MarkRuntimeStateDirty() + { + if (UXNavigationRuntime.TryGetInstance(out var runtime)) + { + runtime.MarkStateDirty(); + } + } + + private static void ReportCapacityExceeded() + { +#if UNITY_EDITOR || DEVELOPMENT_BUILD + Debug.LogError("UXNavigationScope runtime selectable capacity exceeded."); +#endif + } } } #endif diff --git a/Runtime/UXComponent/Navigation/UXNavigationSkip.cs b/Runtime/UXComponent/Navigation/UXNavigationSkip.cs index f8d043f..27e1e64 100644 --- a/Runtime/UXComponent/Navigation/UXNavigationSkip.cs +++ b/Runtime/UXComponent/Navigation/UXNavigationSkip.cs @@ -5,6 +5,28 @@ namespace UnityEngine.UI [AddComponentMenu("UI/UX Navigation Skip")] public sealed class UXNavigationSkip : MonoBehaviour { + private void OnEnable() + { + InvalidateNavigation(); + } + + private void OnDisable() + { + InvalidateNavigation(); + } + + private void OnTransformParentChanged() + { + InvalidateNavigation(); + } + + private static void InvalidateNavigation() + { + if (UXNavigationRuntime.TryGetInstance(out var runtime)) + { + runtime.InvalidateSkipCaches(); + } + } } } #endif