├── .gitignore ├── LICENSE ├── LICENSE-MIT ├── LICENSE-MIT.meta ├── LICENSE.meta ├── TabSystem.meta ├── TabSystem ├── Editor.meta ├── Editor │ ├── TabSystemEditor.cs │ └── TabSystemEditor.cs.meta ├── TabButtonC.cs ├── TabButtonC.cs.meta ├── TabSystem.cs └── TabSystem.cs.meta ├── package.json ├── package.json.meta ├── readme.md └── readme.md.meta /.gitignore: -------------------------------------------------------------------------------- 1 | *.temp 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2024 Barbaros Bayat [b3x2088@gmail.com]. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE-MIT.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6075a3e077b1a5149acba728e4b90e3f 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bdec3ba45610fbd43a8a0f3b0e8ead15 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /TabSystem.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: af61514afb013f842909269457c41df4 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /TabSystem/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5c2a496e786df294fa6392ba0fab63ca 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /TabSystem/Editor/TabSystemEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using UnityEngine.UI; 7 | using Object = UnityEngine.Object; 8 | 9 | /// 10 | /// An editor for the . 11 | ///
Allows for dynamic modification & generation on inspector.
12 | ///
13 | [CustomEditor(typeof(TabSystem)), CanEditMultipleObjects] 14 | public class TabSystemEditor : Editor { 15 | //////////// Object Creation 16 | [MenuItem("GameObject/UI/Tab System")] 17 | public static void CreateTabSystem(MenuCommand command) { 18 | Undo.IncrementCurrentGroup(); 19 | Undo.SetCurrentGroupName("create 'TabSystem'"); 20 | int undoGroupIndex = Undo.GetCurrentGroup(); 21 | 22 | // Create primary gameobject. 23 | GameObject tabSystem = new GameObject("Tab System"); 24 | 25 | // Align stuff 26 | GameObject parentObject = (GameObject)command.context; 27 | if (parentObject == null || parentObject.GetComponentInParent() == null) { 28 | // There probably exists a shorthand editor utility for doing this, please let me know if it actually does. 29 | // but otherwise this does mostly the same thing as creating a button menu item, requiring an actual canvas 30 | Canvas firstCanvas = FindFirstObjectByType(); 31 | if (firstCanvas != null) { 32 | parentObject = firstCanvas.gameObject; 33 | } 34 | // No canvas exists, create a blank canvas on the scene root. 35 | else { 36 | parentObject = new GameObject("Canvas"); 37 | firstCanvas = parentObject.AddComponent(); 38 | // Required by the tabsystem 39 | parentObject.AddComponent(); 40 | parentObject.AddComponent(); 41 | 42 | // Setup 'firstCanvas' 43 | firstCanvas.renderMode = RenderMode.ScreenSpaceOverlay; 44 | 45 | Undo.RegisterCompleteObjectUndo(parentObject, string.Empty); 46 | } 47 | } 48 | 49 | GameObjectUtility.SetParentAndAlign(tabSystem, parentObject); 50 | 51 | // TabSystem on empty object. 52 | TabSystem tabSystemScript = tabSystem.AddComponent(); 53 | // Layout group 54 | HorizontalLayoutGroup tabSystemLayoutGroup = tabSystem.AddComponent(); 55 | tabSystemLayoutGroup.childControlHeight = true; 56 | tabSystemLayoutGroup.childControlWidth = true; 57 | tabSystemLayoutGroup.spacing = 10f; 58 | // Tab Button 59 | tabSystemScript.CreateTab(); 60 | 61 | // Resize stuff accordingly. 62 | // Width -- Height 63 | RectTransform tabSystemTransform = tabSystem.GetComponent(); 64 | tabSystemTransform.sizeDelta = new Vector2(200, 100); 65 | 66 | // Set Unity Stuff 67 | Undo.RegisterCreatedObjectUndo(tabSystem, string.Empty); 68 | Undo.CollapseUndoOperations(undoGroupIndex); 69 | Selection.activeObject = tabSystem; 70 | } 71 | 72 | /// 73 | /// Record of undo objects to save. 74 | ///
Used for filtering and .
75 | ///
76 | private readonly List undoRecord = new List(); 77 | 78 | /// 79 | /// Records a generative event for a tab system targets array. 80 | /// 81 | public void UndoRecordGenerativeEvent(Action generativeEvent, string undoMsg) { 82 | TabSystem[] targets = base.targets.Cast().ToArray(); 83 | 84 | if (undoRecord.Count > 0) { 85 | undoRecord.Clear(); 86 | } 87 | 88 | Undo.IncrementCurrentGroup(); 89 | Undo.SetCurrentGroupName(undoMsg); 90 | int undoID = Undo.GetCurrentGroup(); 91 | 92 | // to be destroyed / created TabSystem gameobjects 93 | // Since we are iterating an array of arrays, we don't use the utility method (as this is called one time) 94 | // Register all buttons into the undo record. 95 | foreach (TabSystem system in targets) { 96 | foreach (TabButtonC btn in system.TabButtons) { 97 | if (btn == null) { 98 | continue; 99 | } 100 | 101 | undoRecord.Add(btn.gameObject); 102 | } 103 | } 104 | 105 | foreach (TabSystem target in targets) { 106 | // Undo.RecordObject does not work 107 | // Because unity. 108 | // Undo.RecordObject(target, string.Empty); 109 | Undo.RegisterCompleteObjectUndo(target, string.Empty); 110 | 111 | if (!PrefabUtility.IsPartOfAnyPrefab(target)) { 112 | EditorUtility.SetDirty(target); 113 | } 114 | 115 | generativeEvent(target); 116 | 117 | if (PrefabUtility.IsPartOfAnyPrefab(target)) { 118 | // RegisterCompleteObjectUndo does not immediately add the object into the Undo list 119 | // So do this to avoid bugs, as this needs to be done after the undo list was updated. 120 | 121 | EditorApplication.delayCall += () => { 122 | PrefabUtility.RecordPrefabInstancePropertyModifications(target); 123 | }; 124 | } 125 | } 126 | 127 | // Apply serializedObject 128 | foreach (TabSystem target in targets) { 129 | foreach (TabButtonC createdUndoRegister in target.TabButtons.Where(tb => !undoRecord.Contains(tb.gameObject))) { 130 | if (createdUndoRegister == null) { 131 | continue; 132 | } 133 | 134 | Undo.RegisterCreatedObjectUndo(createdUndoRegister.gameObject, string.Empty); 135 | } 136 | } 137 | 138 | Undo.CollapseUndoOperations(undoID); 139 | } 140 | 141 | public override void OnInspectorGUI() { 142 | // TODO : This is an eyesore, yes. Everything here needs refactor but i am lazy and it works. 143 | // ---- 144 | 145 | // Support multiple object editing 146 | TabSystem[] targets = base.targets.Cast().ToArray(); 147 | undoRecord.Clear(); 148 | 149 | // PropertyField's (using SerializedObject) are already handled by CanEditMultipleObjects attribute 150 | // For manual GUI, we need to compensate. 151 | SerializedObject tabSO = serializedObject; 152 | bool gEnabled = GUI.enabled; 153 | bool showMixed = EditorGUI.showMixedValue; 154 | tabSO.Update(); 155 | 156 | // Draw the 'm_Script' field that unity makes (with disabled gui) 157 | GUI.enabled = false; 158 | EditorGUILayout.PropertyField(tabSO.FindProperty("m_Script")); 159 | GUI.enabled = gEnabled; 160 | 161 | // Setup variables 162 | EditorGUILayout.LabelField("Standard Settings", EditorStyles.boldLabel); 163 | 164 | GUILayout.BeginHorizontal(); // TabButtonAmount 165 | int tBtnAmountTest = targets[0].TabButtonAmount; // Get a test variable, for showing mixed view. 166 | EditorGUI.showMixedValue = targets.Any(ts => ts.TabButtonAmount != tBtnAmountTest); 167 | 168 | EditorGUI.BeginChangeCheck(); 169 | bool hasChangedTabButtonAmount = false; 170 | int TBtnAmount = EditorGUILayout.IntField(ObjectNames.NicifyVariableName(nameof(TabSystem.TabButtonAmount)), tBtnAmountTest); 171 | if (GUILayout.Button("+", GUILayout.Width(20f))) { TBtnAmount++; } 172 | if (GUILayout.Button("-", GUILayout.Width(20f))) { TBtnAmount--; } 173 | EditorGUI.showMixedValue = showMixed; 174 | GUILayout.EndHorizontal(); 175 | hasChangedTabButtonAmount = EditorGUI.EndChangeCheck(); 176 | 177 | // Show warning if TabButtonAmount is 0 or lower. 178 | if (targets.Any(tb => tb.TabButtonAmount <= 0)) { 179 | string disabledMsg = targets.Length > 1 ? "(Some) TabSystem(s) are disabled. " : "TabSystem is disabled."; 180 | disabledMsg += "To enable it again set TabButtonAmount to 1 or more."; 181 | 182 | EditorGUILayout.HelpBox(disabledMsg, MessageType.Warning); 183 | } 184 | 185 | EditorGUI.BeginChangeCheck(); 186 | bool hasChangedCurrentReference = false; 187 | int tReferenceBtnTest = targets[0].ReferenceTabButtonIndex; 188 | EditorGUI.showMixedValue = targets.Any(ts => ts.TabButtonAmount != tBtnAmountTest); 189 | int cRefTB = EditorGUILayout.IntField(ObjectNames.NicifyVariableName(nameof(TabSystem.ReferenceTabButtonIndex)), tReferenceBtnTest); 190 | EditorGUI.showMixedValue = showMixed; 191 | hasChangedCurrentReference = EditorGUI.EndChangeCheck(); 192 | 193 | EditorGUI.BeginChangeCheck(); 194 | bool hasChangedSelectedBtnIndex = false; 195 | int selectedBtnIndexTest = targets[0].SelectedTabIndex; 196 | EditorGUI.showMixedValue = targets.Any(ts => ts.SelectedTabIndex != selectedBtnIndexTest); 197 | int tBtnSelected = EditorGUILayout.IntField(ObjectNames.NicifyVariableName(nameof(TabSystem.SelectedTabIndex)), selectedBtnIndexTest); 198 | EditorGUI.showMixedValue = showMixed; 199 | hasChangedSelectedBtnIndex = EditorGUI.EndChangeCheck(); 200 | 201 | EditorGUI.BeginChangeCheck(); 202 | bool hasChangedInteractable = false; 203 | bool tInteractableTest = targets[0].Interactable; 204 | EditorGUI.showMixedValue = targets.Any(ts => ts.Interactable != tInteractableTest); 205 | bool tInteractable = EditorGUILayout.Toggle(nameof(TabSystem.Interactable), tInteractableTest); 206 | EditorGUI.showMixedValue = showMixed; 207 | hasChangedInteractable = EditorGUI.EndChangeCheck(); 208 | 209 | EditorGUILayout.Space(); 210 | EditorGUILayout.LabelField("Fade Settings", EditorStyles.boldLabel); 211 | 212 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.ButtonFadeType))); 213 | FadeType tFadeTypeTest = targets[0].ButtonFadeType; 214 | bool isFadeTypeMixedValue = targets.Any(ts => ts.ButtonFadeType != tFadeTypeTest); 215 | 216 | // Button fade 217 | // Hide any button fade options if the types are different, otherwise show. 218 | if (!isFadeTypeMixedValue) { 219 | switch (tFadeTypeTest) { 220 | case FadeType.ColorFade: 221 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.FadeColorDuration))); 222 | // Set the default color dynamically on the editor 223 | EditorGUI.BeginChangeCheck(); 224 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.FadeColorTargetOnDefault))); 225 | if (EditorGUI.EndChangeCheck()) { 226 | // Lazy way of intercepting undo, as property fields don't really like other undos registered after itself 227 | foreach (TabSystem target in targets) { 228 | Undo.undoRedoPerformed += () => { 229 | target.UpdateButtonAppearances(); 230 | SceneView.RepaintAll(); 231 | }; 232 | 233 | target.UpdateButtonAppearances(); 234 | SceneView.RepaintAll(); 235 | } 236 | } 237 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.FadeColorTargetOnHover))); 238 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.FadeColorTargetOnClick))); 239 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.FadeColorTargetOnDisabled))); 240 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.FadeSubtractsFromCurrentColor))); 241 | break; 242 | case FadeType.SpriteSwap: 243 | EditorGUI.BeginChangeCheck(); 244 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.SpriteTargetOnDefault))); 245 | if (EditorGUI.EndChangeCheck()) { 246 | foreach (TabSystem target in targets) { 247 | Undo.undoRedoPerformed += () => { 248 | target.UpdateButtonAppearances(); 249 | SceneView.RepaintAll(); 250 | }; 251 | 252 | target.UpdateButtonAppearances(); 253 | SceneView.RepaintAll(); 254 | } 255 | } 256 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.SpriteTargetOnHover))); 257 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.SpriteTargetOnClick))); 258 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.SpriteTargetOnDisabled))); 259 | break; 260 | case FadeType.CustomUnityEvent: 261 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.ButtonCustomEventOnReset))); 262 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.ButtonCustomEventOnHover))); 263 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.ButtonCustomEventOnClick))); 264 | break; 265 | 266 | default: 267 | case FadeType.None: 268 | break; 269 | } 270 | } 271 | 272 | EditorGUILayout.Space(); 273 | EditorGUILayout.LabelField("Tab Event", EditorStyles.boldLabel); 274 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.OnTabButtonClicked))); 275 | EditorGUILayout.PropertyField(tabSO.FindProperty(nameof(TabSystem.OnTabButtonCreated))); 276 | 277 | if (hasChangedInteractable || hasChangedTabButtonAmount || hasChangedCurrentReference || hasChangedSelectedBtnIndex) { 278 | // As an optimization, refrain from executing end change check with arrays as much as possible 279 | // This will only be possible if we check all tab button amount and interactibility states ofc, which is inconvenient. 280 | UndoRecordGenerativeEvent((TabSystem target) => { 281 | if (hasChangedTabButtonAmount && target.TabButtonAmount != TBtnAmount) { 282 | target.TabButtonAmount = TBtnAmount; 283 | } 284 | if (hasChangedInteractable && target.Interactable != tInteractable) { 285 | target.Interactable = tInteractable; 286 | SceneView.RepaintAll(); // Update views instantly 287 | } 288 | 289 | if (hasChangedCurrentReference) { 290 | target.ReferenceTabButtonIndex = cRefTB; 291 | } 292 | 293 | if (hasChangedSelectedBtnIndex) { 294 | target.SelectedTabIndex = tBtnSelected; 295 | } 296 | }, "change variable on TabSystem"); 297 | } 298 | 299 | tabSO.ApplyModifiedProperties(); 300 | 301 | // -- Tab List Actions 302 | EditorGUILayout.Space(); 303 | EditorGUILayout.LabelField("Tab List", EditorStyles.boldLabel); 304 | 305 | EditorGUI.indentLevel++; // indentLevel = normal + 1 306 | bool prevGUIEnabled = GUI.enabled; 307 | GUI.enabled = false; 308 | // Apparently CanEditMultipleObjects ReorderableList has issues while drawing properties (keeps spamming you should stop calling next) 309 | // Most likely it uses serializedProperty.arraySize instead of iterating properly so we have to ditch the view if there's more than 2 views 310 | if (targets.Length <= 1) { 311 | EditorGUILayout.PropertyField(tabSO.FindProperty("tabButtons")); 312 | } 313 | 314 | GUI.enabled = prevGUIEnabled; 315 | 316 | GUILayout.BeginHorizontal(); 317 | if (GUILayout.Button("Clear Tabs")) { 318 | UndoRecordGenerativeEvent((TabSystem target) => { 319 | target.ClearTabs(); 320 | }, "clear tabs on TabSystem"); 321 | } 322 | if (GUILayout.Button("Generate Tabs")) { 323 | UndoRecordGenerativeEvent((TabSystem target) => { 324 | target.GenerateTabs(); 325 | }, "generate tabs on TabSystem"); 326 | } 327 | if (GUILayout.Button("Reset Tabs")) { 328 | UndoRecordGenerativeEvent((TabSystem target) => { 329 | target.ResetTabs(); 330 | }, "reset tabs on TabSystem"); 331 | } 332 | GUILayout.EndHorizontal(); 333 | 334 | EditorGUI.indentLevel--; // indentLevel = normal 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /TabSystem/Editor/TabSystemEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 42de207f759d83346a1b914b0a2e077e -------------------------------------------------------------------------------- /TabSystem/TabButtonC.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using TMPro; 4 | using UnityEngine; 5 | using UnityEngine.Events; 6 | using UnityEngine.EventSystems; 7 | using UnityEngine.Serialization; 8 | using UnityEngine.UI; 9 | 10 | /// 11 | /// A tab button component, handles the pointer events and the color/graphic transitions. 12 | /// The parent manages it's settings. 13 | /// 14 | [RequireComponent(typeof(RectTransform))] 15 | public class TabButtonC : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerEnterHandler, IPointerExitHandler { 16 | /// 17 | /// Used with a tab button related transitioning event. 18 | ///
The parameter is for the background image of the button and the parameter is for the button itself.
19 | ///
20 | [Serializable] 21 | public class ButtonTransitionEvent : UnityEvent { } 22 | 23 | // primary type 24 | /// 25 | /// Primary fading type used that is from the parent tab system. 26 | /// 27 | public FadeType FadeType { get { return parentTabSystem.ButtonFadeType; } } 28 | // images 29 | private Image buttonBackgroundImage; 30 | /// 31 | /// Background image assigned to this tab button. 32 | ///
This is the image used for the fading ().
33 | ///
34 | public Image ButtonBackgroundImage { 35 | get { 36 | if (buttonBackgroundImage == null) { 37 | buttonBackgroundImage = GetComponent(); 38 | } 39 | 40 | return buttonBackgroundImage; 41 | } 42 | } 43 | 44 | // color fade 45 | public Color PrevColor { get; private set; } 46 | public Color DisableColor { get { return parentTabSystem.FadeColorTargetOnDisabled; } } 47 | public Color HoverColor { get { return parentTabSystem.FadeColorTargetOnHover; } } 48 | // sprite swap 49 | private Sprite PrevSprite; 50 | 51 | [Header(":: Tab Button Content")] 52 | [Tooltip("Text Content of this button.\nSet this to update the text.")] 53 | [SerializeField, TextArea] private string buttonText = "Tab Button"; 54 | /// 55 | /// Text contained inside this tab button. 56 | ///
Setting this will change the 's text property value.
57 | ///
58 | public string ButtonText { 59 | get { return buttonText; } 60 | set { 61 | buttonText = value; 62 | GenerateButtonContent(); 63 | } 64 | } 65 | [Tooltip("Text Content of this button.\nSet this to update the icon.")] 66 | [SerializeField] private Sprite buttonSprite; 67 | /// 68 | /// Text contained inside this tab button. 69 | ///
Setting this will change the 's sprite property value.
70 | ///
Note : This does not contain the background sprite. Instead this is an accompanying value.
71 | ///
72 | public Sprite ButtonSprite { 73 | get { return buttonSprite; } 74 | set { 75 | buttonSprite = value; 76 | GenerateButtonContent(); 77 | } 78 | } 79 | /// 80 | /// Mostly editor only, receive content ( and ) from the added button components. 81 | /// 82 | [SerializeField] private bool receiveContentFromComponents; 83 | 84 | [SerializeField, FormerlySerializedAs("mInteractable")] private bool m_Interactable = true; 85 | /// 86 | /// Whether if this button is interactable. 87 | ///
Note : The parent tab system's interactability overrides this buttons.
88 | ///
89 | public bool Interactable { 90 | get { return parentTabSystem.Interactable && m_Interactable; } 91 | set { m_Interactable = value; } 92 | } 93 | 94 | [Header(":: Tab Button Reference")] 95 | [SerializeField] private TMP_Text buttonTMPText; 96 | [SerializeField] private Image buttonImage; 97 | /// 98 | /// Text attached to this button. 99 | /// 100 | public TMP_Text ButtonTMPText { get { return buttonTMPText; } internal set { buttonTMPText = value; } } 101 | /// 102 | /// Image (complimentary one, for the background use ) attached to this button. 103 | ///
104 | ///
This image is on the left side of the button (by default), but it can be at the "anywhere else" side of the button.
105 | ///
106 | public Image ButtonImage { get { return buttonImage; } internal set { buttonImage = value; } } 107 | 108 | // Internal Reference (don't touch this unless necessary) 109 | [SerializeField, HideInInspector] private TabSystem parentTabSystem; 110 | /// yes, this is indeed janky. 111 | /// 112 | /// Returns the index of this button. 113 | /// 114 | public int ButtonIndex { get { return parentTabSystem == null ? transform.GetSiblingIndex() : parentTabSystem.GetButtonIndex(this); } } 115 | /// 116 | /// The parent tab system that this button was initialized with. 117 | /// 118 | public TabSystem ParentTabSystem { get { return parentTabSystem; } } 119 | 120 | // -- Initilaze 121 | /// 122 | /// Whether if this button is initialized. 123 | /// 124 | public bool IsInit => parentTabSystem != null; 125 | /// 126 | /// Initializes and sets up the button. 127 | /// 128 | public void Initilaze(TabSystem parent) { 129 | if (IsInit) { 130 | return; 131 | } 132 | 133 | parentTabSystem = parent; 134 | } 135 | private void Start() { 136 | if (parentTabSystem == null) { 137 | Debug.LogWarning("[TabButton::Start] The parent tab system is null. Getting the parent component.", this); 138 | TabSystem parentTab = GetComponentInParent(); 139 | 140 | if (parentTab == null) { 141 | Debug.LogWarning("[TabButton::Start] The parent tab system is null. Failed to get component.", this); 142 | return; 143 | } 144 | 145 | parentTabSystem = parentTab; 146 | } 147 | 148 | // Set Colors + Images 149 | PrevColor = ButtonBackgroundImage.color; 150 | PrevSprite = ButtonBackgroundImage.sprite; 151 | 152 | // If current button is the selected object. 153 | if (ButtonIndex == parentTabSystem.SelectedTabIndex) { 154 | parentTabSystem.SelectedTab = this; 155 | 156 | // Set visuals. 157 | SetButtonAppearance(ButtonState.Click); 158 | } 159 | } 160 | 161 | /// 162 | ///
Generates content from .
163 | ///
164 | /// 165 | /// This parameter specifies whether if this method was called from an 'OnValidate' method. 166 | ///
Do not touch this unless you are calling this from 'OnValidate' (Changes behaviour)
167 | /// 168 | internal void GenerateButtonContent(bool onValidateCall = false) { 169 | if (ButtonTMPText != null) { 170 | // Apply content if we have content 171 | if (!string.IsNullOrWhiteSpace(ButtonText)) { 172 | ButtonTMPText.SetText(ButtonText); 173 | ButtonTMPText.gameObject.SetActive(true); 174 | } 175 | // Receive content if the 'image or sprite' does exist (& our content is null) 176 | else if (!string.IsNullOrWhiteSpace(ButtonTMPText.text) && receiveContentFromComponents) { 177 | ButtonText = ButtonTMPText.text; 178 | ButtonTMPText.gameObject.SetActive(true); 179 | } 180 | // No content, bail out 181 | else { 182 | ButtonTMPText.gameObject.SetActive(false); 183 | } 184 | } else if (Application.isPlaying && !onValidateCall && !string.IsNullOrWhiteSpace(ButtonText)) { 185 | // Print only if tried to set content 186 | Debug.LogWarning(string.Format("[TabButton::GenerateButtonContent] ButtonTMPText field in button \"{0}\" is null.", name)); 187 | } 188 | 189 | if (ButtonImage != null) { 190 | if (ButtonSprite != null) { 191 | ButtonImage.sprite = ButtonSprite; 192 | ButtonImage.gameObject.SetActive(true); 193 | } else if (ButtonImage.sprite != null && receiveContentFromComponents) { 194 | ButtonSprite = ButtonImage.sprite; 195 | ButtonImage.gameObject.SetActive(true); 196 | } else { 197 | ButtonImage.gameObject.SetActive(false); 198 | } 199 | } else if (Application.isPlaying && !onValidateCall && ButtonSprite != null) { 200 | Debug.LogWarning(string.Format("[TabButton::GenerateButtonContent] ButtonImage field in button \"{0}\" is null.", name)); 201 | } 202 | } 203 | private void OnValidate() { 204 | GenerateButtonContent(true); 205 | } 206 | 207 | #region PointerClick Events 208 | // -- Invoke the actual click here. 209 | public void OnPointerClick(PointerEventData eventData) { 210 | if (!Interactable) { 211 | return; 212 | } 213 | 214 | parentTabSystem.OnTabButtonClicked?.Invoke(transform.GetSiblingIndex()); 215 | 216 | parentTabSystem.SelectedTab = this; 217 | parentTabSystem.UpdateButtonAppearances(); 218 | } 219 | 220 | // -- Visual Updates 221 | public void OnPointerDown(PointerEventData eventData) { 222 | if (!Interactable) { 223 | return; 224 | } 225 | 226 | if (parentTabSystem.SelectedTab != this) { 227 | SetButtonAppearance(ButtonState.Click); 228 | } 229 | } 230 | public void OnPointerEnter(PointerEventData eventData) { 231 | if (!Interactable) { 232 | return; 233 | } 234 | 235 | if (parentTabSystem.SelectedTab != this) { 236 | SetButtonAppearance(ButtonState.Hover); 237 | } 238 | } 239 | public void OnPointerExit(PointerEventData eventData) { 240 | if (!Interactable) { 241 | return; 242 | } 243 | 244 | if (parentTabSystem.SelectedTab != this) { 245 | SetButtonAppearance(ButtonState.Reset); 246 | } else // ParentTabSystem.CurrentSelectedTab == this 247 | { 248 | SetButtonAppearance(ButtonState.Click); 249 | } 250 | } 251 | 252 | /// 253 | /// State of the button to set the appearence into. 254 | ///
You can get the states of the button using events.
255 | ///
256 | internal enum ButtonState { Reset, Hover, Click, Disable } 257 | /// 258 | /// Sets the button appearence. 259 | ///
Do not call this method.
260 | ///
261 | internal void SetButtonAppearance(ButtonState state) { 262 | switch (state) { 263 | case ButtonState.Reset: 264 | switch (FadeType) { 265 | case FadeType.ColorFade: 266 | TweenColorFade(parentTabSystem.FadeColorTargetOnDefault, parentTabSystem.FadeColorDuration); 267 | break; 268 | case FadeType.SpriteSwap: 269 | if (PrevSprite != null) { ButtonBackgroundImage.sprite = parentTabSystem.SpriteTargetOnDefault; } else { ButtonBackgroundImage.sprite = null; } 270 | break; 271 | case FadeType.CustomUnityEvent: 272 | parentTabSystem.ButtonCustomEventOnReset?.Invoke(ButtonBackgroundImage, this); 273 | break; 274 | } 275 | break; 276 | case ButtonState.Hover: 277 | switch (FadeType) { 278 | case FadeType.ColorFade: 279 | TweenColorFade(parentTabSystem.FadeColorTargetOnHover, parentTabSystem.FadeColorDuration); 280 | break; 281 | case FadeType.SpriteSwap: 282 | ButtonBackgroundImage.sprite = parentTabSystem.SpriteTargetOnHover; 283 | break; 284 | case FadeType.CustomUnityEvent: 285 | parentTabSystem.ButtonCustomEventOnHover?.Invoke(ButtonBackgroundImage, this); 286 | break; 287 | } 288 | break; 289 | case ButtonState.Click: 290 | switch (FadeType) { 291 | case FadeType.ColorFade: 292 | TweenColorFade(parentTabSystem.FadeColorTargetOnClick, parentTabSystem.FadeColorDuration); 293 | break; 294 | case FadeType.SpriteSwap: 295 | ButtonBackgroundImage.sprite = parentTabSystem.SpriteTargetOnClick; 296 | break; 297 | case FadeType.CustomUnityEvent: 298 | parentTabSystem.ButtonCustomEventOnClick?.Invoke(ButtonBackgroundImage, this); 299 | break; 300 | } 301 | break; 302 | case ButtonState.Disable: 303 | switch (FadeType) { 304 | case FadeType.ColorFade: 305 | TweenColorFade(DisableColor, parentTabSystem.FadeColorDuration); 306 | break; 307 | case FadeType.SpriteSwap: 308 | if (PrevSprite != null) { ButtonBackgroundImage.sprite = parentTabSystem.SpriteTargetOnDisabled; } else { ButtonBackgroundImage.sprite = null; } 309 | break; 310 | case FadeType.CustomUnityEvent: 311 | parentTabSystem.ButtonCustomEventOnDisable?.Invoke(ButtonBackgroundImage, this); 312 | break; 313 | } 314 | break; 315 | 316 | default: 317 | // Reset if no state was assigned. 318 | Debug.LogWarning($"[TabButton::SetButtonAppearance] No behaviour defined for state : \"{state}\". Reseting instead."); 319 | goto case ButtonState.Reset; 320 | } 321 | } 322 | #endregion 323 | 324 | #region Color Fading 325 | /// 326 | /// Tweens the color of to with the duration of given . 327 | /// 328 | /// Target color to end the tween on. 329 | /// Duration of the tween. 330 | private void TweenColorFade(Color target, float duration) { 331 | if (!gameObject.activeInHierarchy) { 332 | return; // Do not start coroutines if the object isn't active. 333 | } 334 | 335 | StartCoroutine(CoroutineTweenColorFade(target, duration)); 336 | } 337 | /// 338 | /// 339 | ///
This is the coroutine, call the to directly dispatch the coroutine.
340 | ///
341 | /// 342 | private IEnumerator CoroutineTweenColorFade(Color target, float duration) { 343 | // Color manipulation 344 | Color currentPrevColor = ButtonBackgroundImage.color; 345 | bool targetIsPrevColor = target == PrevColor; 346 | 347 | if (parentTabSystem.FadeSubtractsFromCurrentColor && !targetIsPrevColor) { 348 | target = currentPrevColor - target; 349 | } 350 | 351 | #if UNITY_EDITOR 352 | if (!Application.isPlaying) { 353 | // Set the color instantly as the 'UnityEditor' doesn't support tween. 354 | ButtonBackgroundImage.color = target; 355 | 356 | yield break; 357 | } 358 | #endif 359 | if (duration <= 0f) { 360 | ButtonBackgroundImage.color = target; 361 | 362 | yield break; 363 | } 364 | 365 | // Fade 366 | float t = 0f; 367 | 368 | while (t <= 1.0f) { 369 | t += Time.deltaTime / duration; 370 | ButtonBackgroundImage.color = Color.Lerp(currentPrevColor, target, Mathf.SmoothStep(0, 1, t)); 371 | yield return null; 372 | } 373 | 374 | // Set end value. 375 | ButtonBackgroundImage.color = target; 376 | } 377 | #endregion 378 | } 379 | -------------------------------------------------------------------------------- /TabSystem/TabButtonC.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8d487354177197e408473e9ee4e71d49 -------------------------------------------------------------------------------- /TabSystem/TabSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using TMPro; 4 | using UnityEngine; 5 | using UnityEngine.Events; 6 | using UnityEngine.EventSystems; 7 | using UnityEngine.Serialization; 8 | using UnityEngine.UI; 9 | 10 | /// 11 | /// The fading type of TabButton. 12 | /// 13 | ///
Instead of using an , this was the solution I resorted to like 1(?) year ago.
14 | /// However, newer solutions that i will do will break compat. 15 | public enum FadeType { 16 | None, 17 | ColorFade, 18 | SpriteSwap, 19 | CustomUnityEvent 20 | } 21 | 22 | /// 23 | /// The tab system itself. 24 | ///
Allows to control the buttons and events, this is the main component that manages the of the s.
25 | ///
26 | [ExecuteAlways, DisallowMultipleComponent] 27 | public class TabSystem : UIBehaviour { 28 | /// 29 | /// An unity event that takes an int parameter. 30 | /// 31 | [Serializable] 32 | public class IntUnityEvent : UnityEvent { } 33 | /// 34 | /// An unity event that takes a tab button parameter (with an index). 35 | /// 36 | [Serializable] 37 | public class TabButtonUnityEvent : UnityEvent { } 38 | 39 | ///////////// Public 40 | /// 41 | /// The amount of the tab buttons. 0 means disabled. 42 | /// 43 | public int TabButtonAmount { 44 | get { 45 | return _TabButtonAmount; 46 | } 47 | set { 48 | int prevValue = _TabButtonAmount; 49 | // The weird value is because that the 'TabButtonAmount' will kill your pc if not clampped. 50 | _TabButtonAmount = Mathf.Clamp(value, 0, ushort.MaxValue); 51 | if (prevValue != value) // Generate if value is changed 52 | { 53 | GenerateTabs(prevValue); 54 | } 55 | } 56 | } 57 | [SerializeField] private int _TabButtonAmount = 1; 58 | 59 | /// 60 | /// The index of the currently referenced tab button. 61 | /// 62 | public int ReferenceTabButtonIndex { 63 | get { 64 | // Also clamp the return as that's necessary to protect sanity 65 | // (Note : clamp with TabButtons.Count as that's the actual button amount). 66 | return Mathf.Clamp(_ReferenceTabButtonIndex, 0, tabButtons.Count - 1); 67 | } 68 | set { 69 | if (_ReferenceTabButtonIndex == value) { 70 | return; 71 | } 72 | 73 | _ReferenceTabButtonIndex = Mathf.Clamp(value, 0, tabButtons.Count - 1); 74 | } 75 | } 76 | [SerializeField, FormerlySerializedAs("_CurrentReferenceTabButton")] private int _ReferenceTabButtonIndex = 0; 77 | 78 | /// 79 | /// The index of the currently selected tab button. 80 | /// 81 | public int SelectedTabIndex { 82 | get { 83 | return GetSelectedButtonIndex(); 84 | } 85 | set { 86 | SetSelectedButtonIndex(value); 87 | } 88 | } 89 | [SerializeField] private int _SelectedTabIndex = 0; 90 | 91 | // -- Fade Styles 92 | /// 93 | /// Manages the button fading type. 94 | ///
95 | ///
: None of the '//'s are used.
96 | ///
: Fades the using the variables prefixed with 'FadeColor'.
97 | ///
: Changes the of the tab buttons using the variables prefixed with 'SpriteTarget'.
98 | ///
: Calls the on each interacted button using the variables prefixed with 'ButtonCustomEvent'.
99 | ///
100 | public FadeType ButtonFadeType = FadeType.ColorFade; 101 | // ButtonFadeType = ColorFade 102 | /// 103 | /// Time (in seconds) used to fade transition a button to another color. 104 | /// 105 | [FormerlySerializedAs("FadeSpeed"), Range(0f, 4f)] public float FadeColorDuration = 0.15f; 106 | [FormerlySerializedAs("FadeColorTargetDefault")] public Color FadeColorTargetOnDefault = new Color(1f, 1f, 1f); 107 | [FormerlySerializedAs("FadeColorTargetHover")] public Color FadeColorTargetOnHover = new Color(.95f, .95f, .95f); 108 | [FormerlySerializedAs("FadeColorTargetClick")] public Color FadeColorTargetOnClick = new Color(.9f, .9f, .9f); 109 | [FormerlySerializedAs("FadeColorTargetDisabled")] public Color FadeColorTargetOnDisabled = new Color(.5f, .5f, .5f, .5f); 110 | /// 111 | /// Whether if the target colors subtract from the previous color of the button. 112 | ///
Basically a transition with this being is done 113 | /// by directly making the background's color to the target state's .
114 | ///
If this value is , the transition color is calculated as : previousButtonColor - targetStateColor
115 | ///
116 | public bool FadeSubtractsFromCurrentColor = false; 117 | // ButtonFadeType = SpriteSwap 118 | [FormerlySerializedAs("DefaultSpriteToSwap")] public Sprite SpriteTargetOnDefault; 119 | [FormerlySerializedAs("HoverSpriteToSwap")] public Sprite SpriteTargetOnHover; 120 | [FormerlySerializedAs("TargetSpriteToSwap")] public Sprite SpriteTargetOnClick; 121 | [FormerlySerializedAs("DisabledSpriteToSwap")] public Sprite SpriteTargetOnDisabled; 122 | // ButtonFadeType = CustomUnityEvent 123 | public TabButtonC.ButtonTransitionEvent ButtonCustomEventOnReset; 124 | public TabButtonC.ButtonTransitionEvent ButtonCustomEventOnHover; 125 | public TabButtonC.ButtonTransitionEvent ButtonCustomEventOnClick; 126 | public TabButtonC.ButtonTransitionEvent ButtonCustomEventOnDisable; 127 | 128 | // -- Standard event 129 | // This variable is added to take more control of the generation of the buttons. 130 | /// 131 | /// Called when a tab button is created. 132 | ///
parameter : Returns the index.
133 | ///
parameter : Returns the created button.
134 | ///
135 | [FormerlySerializedAs("OnTabButtonsClicked")] public TabButtonUnityEvent OnTabButtonCreated; 136 | /// 137 | /// Called when a tab button is clicked on the tab system. 138 | /// 139 | public IntUnityEvent OnTabButtonClicked; 140 | 141 | /// 142 | private TabButtonC _SelectedTab; 143 | /// 144 | /// Returns the current selected tab. 145 | ///
This value could be null, but it can't be set to null.
146 | ///
147 | public TabButtonC SelectedTab { 148 | get { 149 | return _SelectedTab; 150 | } 151 | internal set { 152 | _SelectedTabIndex = GetButtonIndex(value); 153 | _SelectedTab = value; 154 | } 155 | } 156 | /// 157 | /// Get a tab button by directly indexing a tab system. 158 | /// 159 | /// 160 | public TabButtonC this[int index] { 161 | get { 162 | return TabButtons[index]; 163 | } 164 | } 165 | 166 | [SerializeField] private List tabButtons = new List(); 167 | /// 168 | /// List of the currently registered tab buttons. 169 | /// 170 | public IReadOnlyList TabButtons { 171 | get { 172 | return tabButtons; 173 | } 174 | } 175 | 176 | // UIBehaviour 177 | #region Interaction Status 178 | [Tooltip("Can the TabButton be interacted with?")] 179 | [SerializeField] private bool interactable = true; 180 | /// 181 | /// Whether if this element is interactable with. 182 | /// 183 | public bool Interactable { 184 | get { return IsInteractable(); } 185 | set { 186 | interactable = value; 187 | 188 | UpdateButtonAppearances(); 189 | } 190 | } 191 | /// 192 | /// Runtime variable for whether if the object is allowed to be interacted with. 193 | /// 194 | private bool groupsAllowInteraction = true; 195 | /// 196 | /// Whether if the UI element is allowed to be interactable. 197 | /// 198 | internal virtual bool IsInteractable() { 199 | if (groupsAllowInteraction) { 200 | return interactable; 201 | } 202 | 203 | return false; 204 | } 205 | private readonly List canvasGroupCache = new List(); 206 | protected override void OnCanvasGroupChanged() { 207 | // This event is part of Selectable (but i adapted it to this script). 208 | // Search for 'CanvasGroup' behaviours & apply preferences to this object. 209 | // 1: Search for transforms that contain 'CanvasGroup' 210 | // 2: Keep them in cache 211 | // 3: Update the interaction state accordingly 212 | bool groupAllowInteraction = true; 213 | Transform t = transform; 214 | 215 | while (t != null) { 216 | t.GetComponents(canvasGroupCache); 217 | bool shouldBreak = false; 218 | 219 | for (int i = 0; i < canvasGroupCache.Count; i++) { 220 | if (!canvasGroupCache[i].interactable) { 221 | groupAllowInteraction = false; 222 | shouldBreak = true; 223 | } 224 | if (canvasGroupCache[i].ignoreParentGroups) { 225 | shouldBreak = true; 226 | } 227 | } 228 | if (shouldBreak) { 229 | break; 230 | } 231 | 232 | t = t.parent; 233 | } 234 | if (groupAllowInteraction != groupsAllowInteraction) { 235 | groupsAllowInteraction = groupAllowInteraction; 236 | UpdateButtonAppearances(); 237 | } 238 | } 239 | #endregion 240 | 241 | /// 242 | /// Internal call of 243 | ///
Required to check 0 / 1 tabs disable-enable state.
244 | ///
245 | /// Previous index passed by the 's setter. 246 | protected void GenerateTabs(int prevIndex) { 247 | if (tabButtons.Count <= 0) { 248 | // Generate tabs from scratch if there is none. 249 | GenerateTabs(); 250 | return; 251 | } 252 | 253 | // Ignore if count is 0 or less 254 | // While this isn't a suitable place for tab management, i wanted to add an '0' state to it. 255 | TabButtonC firstTBtn = tabButtons[0]; 256 | 257 | if (TabButtonAmount <= 0) { 258 | // Make sure the first tab button exists as we need to call 'GenerateTabs' for first spawn. 259 | if (firstTBtn != null) { 260 | firstTBtn.gameObject.SetActive(false); 261 | 262 | // Clean the buttons as that's necessary. (otherwise there's stray buttons) 263 | for (int i = 1; i < tabButtons.Count; i++) { 264 | if (Application.isPlaying) { 265 | if (tabButtons[i] != null) { 266 | DestroyImmediate(tabButtons[i].gameObject); // Have to use DestroyImmediate here as well, otherwise unity gets stuck. 267 | } else { 268 | // Tab button is null, call CleanTabButtonsList 269 | CleanTabButtonsList(); 270 | continue; 271 | } 272 | } 273 | #if UNITY_EDITOR 274 | else { 275 | if (tabButtons[i] != null) { 276 | UnityEditor.Undo.DestroyObjectImmediate(tabButtons[i].gameObject); 277 | } else { 278 | // Tab button is null, call CleanTabButtonsList 279 | CleanTabButtonsList(); 280 | continue; 281 | } 282 | } 283 | #endif 284 | } 285 | 286 | CleanTabButtonsList(); 287 | return; 288 | } 289 | // In this case of this if statement, it's not necessary as the button amount is already 0. 290 | } else if (TabButtonAmount == 1 && prevIndex <= 0) { 291 | // Make sure the first tab button exists as we need to call 'GenerateTabs' for first spawn. 292 | if (firstTBtn != null) { 293 | // This is bad, calling fake event here. 294 | // But the thing is : 0 tab button amount mean disabled 295 | firstTBtn.gameObject.SetActive(true); 296 | // Do status update - management 297 | // This should have been done all in 'CreateTab' method but yeah 298 | // firstTBtn.parentTabSystem = this; 299 | if (!firstTBtn.IsInit) { 300 | firstTBtn.Initilaze(this); 301 | } 302 | 303 | OnTabButtonCreated?.Invoke(0, firstTBtn); 304 | } else { 305 | // List needs to be cleaned (has null member that we can't access, will throw exceptions) 306 | CleanTabButtonsList(); 307 | } 308 | } 309 | 310 | // Generate tabs normally after dealing with the '0' stuff. 311 | GenerateTabs(); 312 | } 313 | /// 314 | /// Generates tabs. 315 | /// 316 | public void GenerateTabs() { 317 | // Normal creation 318 | while (tabButtons.Count > TabButtonAmount) { 319 | if (Application.isPlaying) { 320 | if (tabButtons[tabButtons.Count - 1] != null) { 321 | // We need to use DestroyImmediate here as there's no need for the reference 322 | // Otherwise the script gets stuck at an infinite loop and dies. 323 | // (this is because the while loop is on the main thread, but the 'Destroy' stuff is also done on the main thread after this method is done, 324 | // basically not destroying the object, 'while' loop is just constantly calling 'Destroy' to the same object) 325 | // (this is kinda similar to the godot's 'queue_free' and 'free' distinction, now comparing 'tabButtons[tabButtons.Count - 1]' to null will be always true) 326 | DestroyImmediate(tabButtons[tabButtons.Count - 1].gameObject); 327 | } else { 328 | // Tab button is null, call CleanTabButtonsList 329 | CleanTabButtonsList(); 330 | continue; 331 | } 332 | } 333 | #if UNITY_EDITOR 334 | else { 335 | if (tabButtons[tabButtons.Count - 1] != null) { 336 | UnityEditor.Undo.DestroyObjectImmediate(tabButtons[tabButtons.Count - 1].gameObject); 337 | } else { 338 | // Tab button is null, call CleanTabButtonsList 339 | CleanTabButtonsList(); 340 | continue; 341 | } 342 | } 343 | #endif 344 | CleanTabButtonsList(); 345 | } 346 | while (tabButtons.Count < TabButtonAmount) { 347 | CreateTab(); 348 | } 349 | } 350 | /// 351 | /// Reset tabs. 352 | ///
Call this method if you have an issue with your tabs.
353 | ///
354 | public void ResetTabs() { 355 | ClearTabs(true, true); 356 | 357 | // Destroy all childs 358 | if (tabButtons.Count <= 1 && transform.childCount > 1) { 359 | int tChild = transform.childCount; 360 | for (int i = 0; i < tChild; i++) { 361 | if (Application.isPlaying) { 362 | Destroy(transform.GetChild(0).gameObject); 363 | } 364 | #if UNITY_EDITOR 365 | else { 366 | UnityEditor.Undo.DestroyObjectImmediate(transform.GetChild(0).gameObject); 367 | } 368 | #endif 369 | } 370 | } 371 | 372 | // Create new tab and refresh 373 | TabButtonC tab = CreateTab(false); 374 | tab.Initilaze(this); // this may be redundant 375 | tabButtons.Clear(); 376 | tabButtons.Add(tab); 377 | } 378 | /// 379 | /// Clears tabs. 380 | /// 381 | /// Sets internal variable of TabButtonAmount to be 1. 382 | /// Clears all of the buttons (hard reset parameter). 383 | public void ClearTabs(bool resetTabBtnAmount = true, bool clearAll = false) { 384 | CleanTabButtonsList(); 385 | 386 | // Destroy array. 387 | foreach (TabButtonC button in tabButtons) { 388 | if (button.ButtonIndex == 0 && !clearAll) { 389 | continue; 390 | } 391 | 392 | if (Application.isPlaying) { 393 | Destroy(button.gameObject); 394 | } 395 | #if UNITY_EDITOR 396 | else { 397 | UnityEditor.Undo.DestroyObjectImmediate(button.gameObject); 398 | } 399 | #endif 400 | } 401 | 402 | if (tabButtons.Count > 1) { 403 | tabButtons.RemoveRange(1, Mathf.Max(1, tabButtons.Count - 1)); 404 | } 405 | 406 | if (!clearAll) { 407 | TabButtonC tempTabBtn = tabButtons[0]; 408 | tabButtons.Clear(); 409 | tabButtons.Add(tempTabBtn); 410 | tempTabBtn.Initilaze(this); 411 | } 412 | 413 | if (resetTabBtnAmount) { 414 | _TabButtonAmount = 1; 415 | } 416 | } 417 | 418 | /// 419 | /// Creates Button for TabSystem. 420 | /// Info : This command already adds to the list . 421 | /// 422 | /// Whether to use the referenced tab from index . 423 | /// Creation button result. 424 | public TabButtonC CreateTab(bool useReferenceTab = true) { 425 | TabButtonC tabButtonScript; 426 | 427 | if (tabButtons.Count <= 0 || !useReferenceTab) { 428 | GameObject tabButton = new GameObject("Tab"); 429 | tabButton.transform.SetParent(transform); 430 | tabButton.transform.localScale = Vector3.one; 431 | 432 | tabButtonScript = tabButton.AddComponent(); 433 | tabButton.AddComponent(); 434 | 435 | // -- Text 436 | GameObject tabText = new GameObject("Tab Text"); 437 | tabText.transform.SetParent(tabButton.transform); 438 | TextMeshProUGUI ButtonTMPText = tabText.AddComponent(); 439 | tabButtonScript.ButtonTMPText = ButtonTMPText; 440 | // Set Text Options. 441 | ButtonTMPText.SetText("Tab Button"); 442 | ButtonTMPText.color = Color.black; 443 | ButtonTMPText.alignment = TextAlignmentOptions.Center; 444 | tabText.transform.localScale = Vector3.one; 445 | // Set Text Anchor. (Stretch all) 446 | ButtonTMPText.rectTransform.anchorMin = new Vector2(.33f, 0f); 447 | ButtonTMPText.rectTransform.anchorMax = new Vector2(1f, 1f); 448 | ButtonTMPText.rectTransform.offsetMin = Vector2.zero; 449 | ButtonTMPText.rectTransform.offsetMax = Vector2.zero; 450 | 451 | // -- Image 452 | GameObject tabImage = new GameObject("Tab Image"); 453 | tabImage.transform.SetParent(tabButton.transform); 454 | Image ButtonImage = tabImage.AddComponent(); 455 | tabButtonScript.ButtonImage = ButtonImage; 456 | // Image Options 457 | tabImage.transform.localScale = Vector3.one; 458 | ButtonImage.preserveAspect = true; 459 | // Set anchor to left & stretch along the anchor. 460 | ButtonImage.rectTransform.anchorMin = new Vector2(0f, 0f); 461 | ButtonImage.rectTransform.anchorMax = new Vector2(.33f, 1f); 462 | ButtonImage.rectTransform.offsetMin = Vector2.zero; 463 | ButtonImage.rectTransform.offsetMax = Vector2.zero; 464 | 465 | tabButtonScript.GenerateButtonContent(); 466 | } else { 467 | TabButtonC tabButtonInstantiationTarget = tabButtons[ReferenceTabButtonIndex]; 468 | if (tabButtonInstantiationTarget == null) { 469 | // No reference tab to create from, don't use a reference. 470 | return CreateTab(false); 471 | } 472 | 473 | tabButtonScript = Instantiate(tabButtonInstantiationTarget); 474 | 475 | tabButtonScript.transform.SetParent(tabButtonInstantiationTarget.transform.parent); 476 | tabButtonScript.transform.localScale = tabButtonInstantiationTarget.transform.localScale; 477 | } 478 | 479 | // Init button 480 | tabButtonScript.Initilaze(this); 481 | tabButtonScript.gameObject.name = tabButtonScript.gameObject.name.Replace("(Clone)", string.Empty); 482 | int objectNameIndexSplit = tabButtonScript.gameObject.name.LastIndexOf('_'); 483 | if (objectNameIndexSplit != -1) { 484 | // If the previous name was prefixed with an underscore, remove the underscore 485 | tabButtonScript.gameObject.name = tabButtonScript.gameObject.name.Substring(0, objectNameIndexSplit); 486 | } 487 | // Prefix the name with underscore 488 | tabButtonScript.gameObject.name = string.Format("{0}_{1}", tabButtonScript.gameObject.name, tabButtons.Count); 489 | 490 | tabButtons.Add(tabButtonScript); 491 | OnTabButtonCreated?.Invoke(tabButtons.Count - 1, tabButtonScript); 492 | 493 | return tabButtonScript; 494 | } 495 | 496 | // Tab Cleanup 497 | /// 498 | /// Updates the appearances of the buttons. 499 | ///
Call this when you need to visually update the button.
500 | ///
501 | public void UpdateButtonAppearances() { 502 | foreach (TabButtonC button in tabButtons) { 503 | if (button == null) { 504 | continue; 505 | } 506 | 507 | if (!Interactable) { 508 | button.SetButtonAppearance(TabButtonC.ButtonState.Disable); 509 | continue; 510 | } 511 | 512 | button.SetButtonAppearance(SelectedTab == button ? TabButtonC.ButtonState.Click : TabButtonC.ButtonState.Reset); 513 | } 514 | } 515 | /// 516 | /// Cleans the list in case of null and other stuff. 517 | /// 518 | public void CleanTabButtonsList() { 519 | tabButtons.RemoveAll((x) => x == null); 520 | } 521 | 522 | /// 523 | /// Gather the index for the . 524 | /// 525 | /// The button parameter. This can't be null. 526 | /// The index of in this tabsystem. If this tabsystem does not have the given this returns -1. 527 | /// 528 | public int GetButtonIndex(TabButtonC button) { 529 | if (button == null) { 530 | throw new ArgumentNullException(nameof(button), "[TabSystem::GetButtonIndex] Given argument was null."); 531 | } 532 | 533 | return tabButtons.IndexOf(button); 534 | } 535 | 536 | /// 537 | /// Returns the currently selected buttons index. 538 | /// 539 | public int GetSelectedButtonIndex() { 540 | return Mathf.Clamp(_SelectedTabIndex, 0, tabButtons.Count - 1); 541 | } 542 | /// 543 | /// Selects a button if it's selectable. 544 | /// 545 | /// Index to select. Clamped value. 546 | /// 547 | /// Whether if the event should invoke. 548 | /// This is set to by default. 549 | /// 550 | public void SetSelectedButtonIndex(int btnSelect, bool silentSelect = false) { 551 | _SelectedTabIndex = Mathf.Clamp(btnSelect, 0, tabButtons.Count - 1); 552 | #if UNITY_EDITOR 553 | if (!Application.isPlaying) { 554 | return; 555 | } 556 | #endif 557 | TabButtonC ButtonToSelScript = tabButtons[_SelectedTabIndex]; 558 | 559 | if (ButtonToSelScript != null) { 560 | SelectedTab = ButtonToSelScript; 561 | ButtonToSelScript.SetButtonAppearance(TabButtonC.ButtonState.Click); 562 | 563 | if (!silentSelect) { 564 | OnTabButtonClicked?.Invoke(_SelectedTabIndex); 565 | } 566 | 567 | UpdateButtonAppearances(); 568 | } else { 569 | Debug.LogError($"[TabSystem] The tab button to select is null. The index was {_SelectedTabIndex}.", this); 570 | } 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /TabSystem/TabSystem.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4e092977da9c90f46b1d15b5b9e90c09 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.b3x.tabsystem", 3 | "version": "1.1.0", 4 | "displayName": "UGUI TabSystem", 5 | "description": "This addition can be used to easily create tabs (like browser tabs, etc.) on the unity's UGUI system.", 6 | "dependencies": { 7 | "com.unity.textmeshpro": "3.0.6" 8 | }, 9 | "author": { 10 | "name": "Barbaros Bayat", 11 | "email": "b3x2088@gmail.com", 12 | "url": "https://github.com/b3x206/" 13 | } 14 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 32bf043a0aa65aa4081f828365ce9843 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Unity Tab System 2 | [![CodeFactor](https://www.codefactor.io/repository/github/b3x206/unity-tabsystem/badge)](https://www.codefactor.io/repository/github/b3x206/unity-tabsystem) 3 | 4 | Use this tab system to easily create tabs in your UI. (like the browser tabs or menu tabs) 5 | 6 | ## Notes : 7 | Before usage, make sure to import TextMeshPro text. 8 | 9 | 10 | ![How To Import](https://raw.githubusercontent.com/b3x206/unity-tabsystem/resources/img/import-tmp.jpg "Import TMP") 11 | 12 | ## Preview : 13 | ![Scene](https://raw.githubusercontent.com/b3x206/unity-tabsystem/resources/img/preview.png "How it look (with customize)") 14 | ![Scene](https://raw.githubusercontent.com/b3x206/unity-tabsystem/resources/img/previewAnim.gif "Toggling menus") 15 | 16 | [Font Used In The Previews](https://fonts.google.com/specimen/Comfortaa) 17 | 18 | ## Code : 19 | ```C# 20 | // Reference to the tab system. 21 | public TabSystem tabSystem; 22 | 23 | void Start() 24 | { 25 | // Register event 26 | tabSystem.OnTabButtonsClicked.AddListener(EventExample); 27 | // Sets current selected tab to index (if the index exists) 28 | tabSystem.SetSelectedButtonIndex(2); 29 | // Sets tab button amount (this generates tabs if the count isn't already 4) 30 | tabSystem.TabButtonAmount = 4; 31 | } 32 | 33 | // ... 34 | // Register this event from inspector if you want, using the unity event. 35 | public void EventExample(int SelectedTabIndex) 36 | { 37 | // Information 38 | Debug.Log(string.Format("Selected tab was : {0}", SelectedTabIndex)); 39 | // Toggle menus from here. 40 | } 41 | ``` 42 | 43 | ## Documentation : 44 | * Documentation will be added as a wiki page, but this is the quick start thing. 45 | * Create tab using GameObject>UI>TabSystem 46 | * Just play with the 'TabAmount' and use the 'CurrentReferenceTabIndex' variable for which tab to instantiate from. 47 | * Use the inspector (like you would do on a GameObject>UI>Button) to attach events and change stuff to your liking. 48 | 49 | ## Licensing : 50 | This repository is dual licensed to be both [unlicense/public domain](https://unlicense.org) and/or [MIT license](https://opensource.org/license/mit/). Use the one that is suitable for you and your organization. 51 | -------------------------------------------------------------------------------- /readme.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ef6bbb57dc1ed14478f558a4cd7796a1 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------