diff --git a/Editor/EventExtensions.cs b/Editor/EventExtensions.cs deleted file mode 100644 index b492efb..0000000 --- a/Editor/EventExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -using UnityEngine; - -/// -/// Utility extensions for semantic Event queries. //TODO: Go through this -/// -public static class EventExtensions { - /// - /// Check if an Event is a context click. - /// - /// Event to query. - /// Context button (default is RMB). - /// True, if the Event was a context click. - public static bool IsContextClick(this Event current, int contextButton = 1) { - - return current.type == EventType.MouseDown && current.button == contextButton; - - } - - /// - /// Check if an Event is a left click. - /// - /// Event to query. - /// True, if the Event was a left click. - public static bool IsLeftClick(this Event current) { - - return current.type == EventType.MouseDown && current.button == 0; - - } - - /// - /// Check if an Event is a drag. - /// - /// Event to query. - /// Drag button (default is LMB). - /// True, if the Event was a drag. - public static bool IsDrag(this Event current, int dragButton = 0) { - - return current.type == EventType.MouseDrag && current.button == dragButton; - } - - /// - /// Check if an Event is arrow key. - /// - /// true, if arrow key was ised, false otherwise. - /// Current. - public static bool IsArrowKey(this Event current) { - - return current.keyCode == KeyCode.LeftArrow || current.keyCode == KeyCode.RightArrow || current.keyCode == KeyCode.UpArrow || current.keyCode == KeyCode.DownArrow; - - } - -} - diff --git a/Editor/RectExtensions.cs b/Editor/RectExtensions.cs deleted file mode 100644 index 230dc94..0000000 --- a/Editor/RectExtensions.cs +++ /dev/null @@ -1,315 +0,0 @@ -using UnityEngine; - -/// -/// Utility extensions for manipulating Rect values. -/// -public static class RectExtMethods { - public static Rect VioletSetPosition(this Rect rect, Vector2 position) { - return rect.VioletSetPosition(position.x, position.y); - } - - public static Rect VioletSetPosition(this Rect rect, float x, float y) { - return new Rect(x, y, rect.width, rect.height); - } - - public static Rect VioletDisplaceX(this Rect rect, float x) { - return new Rect(rect.x + x, rect.y, rect.width, rect.height); - } - - public static Rect VioletDisplaceY(this Rect rect, float y) { - return new Rect(rect.x, rect.y + y, rect.width, rect.height); - } - - public static Rect VioletDisplace(this Rect rect, float x, float y) { - return new Rect(rect.x + x, rect.y + y, rect.width, rect.height); - } - - public static Rect VioletDisplace(this Rect rect, Vector2 displacement) { - return new Rect(rect.position + displacement, rect.size); - } - - public static Rect VioletSetWidth(this Rect rect, float width) { - return new Rect(rect.x, rect.y, width, rect.height); - } - - public static Rect VioletSetHeight(this Rect rect, float height) { - return new Rect(rect.x, rect.y, rect.width, height); - } - - public static Rect VioletTransform(this Rect rect, Matrix4x4 matrix) { - return new Rect(matrix.MultiplyPoint3x4(rect.position), matrix.MultiplyVector(rect.size)); - } - - public static float ShortestSide(this Rect rect) { - return Mathf.Min(rect.width, rect.height); - } - - public static float longestSide(this Rect rect) { - return Mathf.Max(rect.width, rect.height); - } - - public static Rect VioletSetSize(this Rect rect, float size) { - return rect.VioletSetSize(Vector2.one * size); - } - - public static Rect VioletSetSize(this Rect rect, float width, float height) { - return rect.VioletSetSize(new Vector2(width, height)); - } - - public static Rect VioletSetSize(this Rect rect, Vector2 size) { - return new Rect(rect.position, size); - } - - public static Rect VioletSetSizeCentered(this Rect rect, float size) { - return rect.VioletSetSizeCentered(Vector2.one * size); - } - - public static Rect VioletSetSizeCentered(this Rect rect, float width, float height) { - return rect.VioletSetSizeCentered(new Vector2(width, height)); - } - - public static Rect VioletSetSizeCentered(this Rect rect, Vector2 size) { - return new Rect(rect.center - size * 0.5f, size); - } - - #region Transalation - public static Rect VioletTranslate(this Rect rect, float x, float y) { - return VioletTranslate(rect, new Vector2(x, y)); - } - - public static Rect VioletTranslate(this Rect rect, Vector2 translation) { - return new Rect(rect.position + translation, rect.size); - } - #endregion - - #region Insetting - /// - /// Split a Rect by insetting from the right edge. - /// - /// Rect to split. - /// Inset width. - /// Inset Rect. - /// - public static Rect VioletSplitRight(this Rect rect, float inset, out Rect result) { - result = new Rect(rect.x + rect.width - inset, rect.y, inset, rect.height); - - return rect.VioletPadRight(inset); - } - - /// - /// Split a Rect by insetting from the left edge. - /// - /// Rect to split. - /// Inset width. - /// Inset Rect. - /// - public static Rect VioletSplitLeft(this Rect rect, float inset, out Rect result) { - result = new Rect(rect.x, rect.y, inset, rect.height); - - return rect.VioletPadLeft(inset); - } - - /// - /// Split a Rect by insetting from the bottom edge. - /// - /// Rect to split. - /// Inset height. - /// Inset Rect. - /// - public static Rect VioletSplitBottom(this Rect rect, float inset, out Rect result) { - result = new Rect(rect.x, rect.y + rect.height - inset, rect.width, inset); - - return rect.VioletPadBottom(inset); - } - - /// - /// Split a Rect by insetting from the top edge. - /// - /// Rect to split. - /// Inset height. - /// Inset Rect. - /// - public static Rect VioletSplitTop(this Rect rect, float inset, out Rect result) { - result = new Rect(rect.x, rect.y, rect.width, inset); - - return rect.VioletPadTop(inset); - } - #endregion - - #region Padding - /// - /// Pad a Rect from all sides. - /// - /// Rect to pad. - /// Amount to pad. - /// Padded Rect. - public static Rect VioletPad(this Rect rect, float padding) { - return VioletPad(rect, padding, padding, padding, padding); - } - - /// - /// Pad a Rect from its left edge. - /// - /// Rect to pad. - /// Amount to pad. - /// Padded Rect. - public static Rect VioletPadLeft(this Rect rect, float padding) { - return VioletPad(rect, padding, 0.0f, 0.0f, 0.0f); - } - - /// - /// Pad a Rect from its right edge. - /// - /// Rect to pad. - /// Amount to pad. - /// Padded Rect. - public static Rect VioletPadRight(this Rect rect, float padding) { - return VioletPad(rect, 0.0f, padding, 0.0f, 0.0f); - } - - /// - /// Pad a Rect from its top edge. - /// - /// Rect to pad. - /// Amount to pad. - /// Padded Rect. - public static Rect VioletPadTop(this Rect rect, float padding) { - return VioletPad(rect, 0.0f, 0.0f, padding, 0.0f); - } - - /// - /// Pad a Rect from its bottom edge. - /// - /// Rect to pad. - /// Amount to pad. - /// Padded Rect. - public static Rect VioletPadBottom(this Rect rect, float padding) { - return VioletPad(rect, 0.0f, 0.0f, 0.0f, padding); - } - - /// - /// Pad a Rect from its left and right edges. - /// - /// Rect to pad. - /// Amount to pad. - /// Padded Rect. - public static Rect VioletPadHorizontal(this Rect rect, float padding) { - return VioletPad(rect, padding, padding, 0.0f, 0.0f); - } - - /// - /// Pad a Rect from its top and bottom edges. - /// - /// Rect to pad. - /// Amount to pad. - /// Padded Rect. - public static Rect VioletPadVertical(this Rect rect, float padding) { - return VioletPad(rect, 0.0f, 0.0f, padding, padding); - } - - /// - /// Pad a Rect from its left, right, top, and bottom edges. - /// - /// Rect to pad. - /// Amount to pad left edge. - /// Amount to pad right edge. - /// Amount to pad top edge. - /// Amount to pad left edge. - /// Padded Rect. - public static Rect VioletPad(this Rect rect, float left, float right, float top, float bottom) { - return new Rect(rect.x + left, rect.y + top, rect.width - left - right, rect.height - top - bottom); - } - #endregion - - #region Sequencing - public static Rect VioletStepDown(this Rect rect, float padding = 0.0f) { - return new Rect(rect.x, rect.y + rect.height + padding, rect.width, rect.height); - } - - public static Rect VioletStepUp(this Rect rect, float padding = 0.0f) { - return new Rect(rect.x, rect.y - rect.height - padding, rect.width, rect.height); - } - - public static Rect VioletStepLeft(this Rect rect, float padding = 0.0f) { - return new Rect(rect.x - rect.width - padding, rect.y, rect.width, rect.height); - } - - public static Rect VioletStepRight(this Rect rect, float padding = 0.0f) { - return new Rect(rect.x + rect.width + padding, rect.y, rect.width, rect.height); - } - #endregion - - #region Extrusions - public static Rect VioletExtrudeLeft(this Rect rect, float width) { - return new Rect(rect.x - width, rect.y, width, rect.height); - } - - public static Rect VioletExtrudeRight(this Rect rect, float width) { - return new Rect(rect.x + rect.width, rect.y, width, rect.height); - } - - public static Rect VioletExtrudeTop(this Rect rect, float height) { - return new Rect(rect.x, rect.y - height, rect.width, height); - } - - public static Rect VioletExtrudeBottom(this Rect rect, float height) { - return new Rect(rect.x, rect.y + rect.height, rect.width, height); - } - #endregion - - - #region Backfills - public static Rect VioletBackfillLeft(this Rect rect, float width) { - return rect.VioletPadRight(rect.width - width); - } - - public static Rect VioletBackfillRight(this Rect rect, float width) { - return rect.VioletPadLeft(rect.width - width); - } - - public static Rect VioletBackfillTop(this Rect rect, float height) { - return rect.VioletPadBottom(rect.height - height); - } - - public static Rect VioletBackfillBottom(this Rect rect, float height) { - return rect.VioletPadTop(rect.height - height); - } - #endregion - - #region Events - public static bool WasContextClicked(this Rect rect, int contextButton = 1, bool useEvent = true) { - if (Event.current.IsContextClick(contextButton) && rect.Contains(Event.current.mousePosition)) { - if (useEvent) { - Event.current.Use(); - } - - return true; - } - - return false; - } - - public static bool WasLeftClicked(this Rect rect, bool useEvent = true) { - if (Event.current.IsLeftClick() && rect.Contains(Event.current.mousePosition)) { - if (useEvent) { - Event.current.Use(); - } - - return true; - } - - return false; - } - public static bool WasDragged(this Rect rect, int dragButton = 0, bool useEvent = true) { - if (Event.current.IsDrag(dragButton) && rect.Contains(Event.current.mousePosition)) { - if (useEvent) { - Event.current.Use(); - } - - return true; - } - - return false; - } - #endregion -} \ No newline at end of file diff --git a/Editor/ScreenEditor.cs b/Editor/ScreenEditor.cs index d0ad2bb..f173432 100644 --- a/Editor/ScreenEditor.cs +++ b/Editor/ScreenEditor.cs @@ -9,159 +9,88 @@ namespace VioletUI { //run this class when unity opens [InitializeOnLoad] - public class ScreenEditor { - - //width of inspector buttons - private const float BUTTON_WIDTH = 60f; + public static class ScreenEditor { //where to show the buttons private static GUIStyle singleStyle, leftStyle, rightStyle; - //hash set of all menus in scene - private static HashSet menus = new HashSet(); - - //menu screens to remove - private static List garbage = new List(); + static Navigator navigator; + static Color violet = new Color(0.898f, 0.745f, 0.935f); + static Color saturatedViolet; - //constructor static ScreenEditor() { - - //only show while !playing - if (!EditorApplication.isPlaying) { - - //actual editor event functions - EditorApplication.hierarchyWindowItemOnGUI += DrawHierarchyItem; - EditorApplication.update += Update; - - } - + EditorApplication.hierarchyWindowItemOnGUI -= DrawHierarchyItem; + EditorApplication.hierarchyWindowItemOnGUI += DrawHierarchyItem; + float h,s,v; + Color.RGBToHSV(violet, out h, out s, out v); + saturatedViolet = Color.HSVToRGB(h, 1f, 1f); } - //remove all menu screens that are no longer used to avoid memory leak - private static void Clean(HashSet menus) { - - //clear the unused menus - garbage.Clear(); - - - //add all menus that no longer exist to the garbage list - foreach (var menu in menus) { - if (menu == null) { - garbage.Add(menu); - } - } + static void DrawHierarchyItem(int instanceID, Rect rect) { + var gameObject = EditorUtility.InstanceIDToObject(instanceID) as GameObject; + if (gameObject == null) { return; } - //remove the garbage from the menus list - foreach (var menu in garbage) { - menus.Remove(menu); + navigator = gameObject.GetComponent(); + if (navigator != null) { + DrawNavigator(navigator, rect); + return; } - } - - //update visuals - private static void Update() { - //remove the garbage from the menu list - Clean(menus); - - foreach (var menu in Object.FindObjectsOfType()) { - menus.Add(menu); + var screen = gameObject.GetComponent(); + if (screen != null) { + DrawScreen(screen, rect); + return; } } - private static GUIStyle InitStyle(ref GUIStyle style, GUIStyle original, TextAnchor alignment) { - if (style == null) { - style = new GUIStyle(original); - style.alignment = alignment; - style.richText = true; + static void DrawNavigator(Navigator navigator, Rect rect) { + if (navigator.EditingScreen == null) { + if (Button(rect, "Add")) { + navigator.AddScreen(); + } + } else if (navigator.transform.childCount > 1) { + if (Button(rect, "Save", true)) { + navigator.FinishEditing(); + } } - - return style; } - private static bool Button(Rect rect, string label, string tooltip, Color color, FontStyle fontStyle = FontStyle.Normal, int dot = 0, int i = 0, int n = 1) { - rect = rect.VioletBackfillRight(BUTTON_WIDTH); - GUIStyle style = null; + static void DrawScreen(Screen screen, Rect rect) { + var navigator = screen.transform.parent.GetComponent(); + if (navigator.EditingScreen != null && screen != navigator.EditingScreen) { return; } - if (n == 1) { - style = InitStyle(ref singleStyle, EditorStyles.miniButton, TextAnchor.MiddleLeft); - } else if (n == 2) { - switch (i) { - case 0: - style = InitStyle(ref leftStyle, EditorStyles.miniButtonLeft, TextAnchor.MiddleLeft); - break; - case 1: - style = InitStyle(ref rightStyle, EditorStyles.miniButtonRight, TextAnchor.MiddleRight); - break; + if (Button(rect, screen.isActiveAndEnabled ? "Save" : "Edit", screen.isActiveAndEnabled)) { + if (navigator == null) { + throw new VioletException($"Tried to edit {screen.name} without a Navigator. Try adding a Navigator component to {screen.transform.parent.name}"); + } + if (screen.isActiveAndEnabled) { + navigator.FinishEditing(screen); + } else { + navigator.Edit(screen); } } - - var content = new GUIContent(" " + label, null, tooltip); - - style.fontStyle = fontStyle; - - var response = GUI.Button(rect, content, singleStyle); - - style.fontStyle = FontStyle.Normal; - - var shadowColor = Color.Lerp(color, Color.black, 0.5f); - shadowColor.a = 0.8f; - - if (dot > 0) { - var dotRect = rect.VioletBackfillRight(rect.height).VioletDisplaceX(1f); - var dotColor = dot == 2 ? color : Color.grey; - - dotColor.a = 0.5f; - - GUI.Box(dotRect, GUIContent.none); - EditorGUI.DrawRect(dotRect.VioletPad(3f).VioletDisplaceX(-1f), dotColor); - - } - - rect = rect.VioletSetWidth(3f).VioletDisplaceX(4f).VioletPadVertical(1f); - - EditorGUI.DrawRect(rect.VioletExtrudeLeft(1f), shadowColor); - EditorGUI.DrawRect(rect.VioletExtrudeRight(1f), shadowColor); - - color.a = 0.5f; - - EditorGUI.DrawRect(rect, color); - - return response; } - private static bool TryGetManagerColor(int instanceID, out Color color) { - color = new Color(0.898f, 0.745f, 0.935f); - return true; - } + static bool Button(Rect rect, string label, bool isActive = false) { + // by default the button is 100% width + // we move the left edge to make button 60 pixels wide, right aligned + var buttonWidth = 60; + rect.xMin = rect.xMax - buttonWidth; + + // extend unity mini button style with small tweaks + var style = new GUIStyle(EditorStyles.miniButton); + style.padding = new RectOffset(3, 0, 1, 1); + style.fixedHeight -= 2; + style.fontStyle = isActive ? FontStyle.Bold : FontStyle.Normal; + style.alignment = TextAnchor.MiddleLeft; + + // set color to violet if active + var originalColor = GUI.color; + GUI.color = isActive ? violet : originalColor; + var response = GUI.Button(rect, new GUIContent(label), style); + GUI.color = originalColor; - private static void DrawHierarchyItem(int instanceID, Rect rect) { - var gameObject = EditorUtility.InstanceIDToObject(instanceID) as GameObject; - - Color violet = new Color(0.898f, 0.745f, 0.935f); - float h = 0f, s = 0f, v = 0f; - Color.RGBToHSV(violet, out h, out s, out v); - Color saturatedViolet = Color.HSVToRGB(h, 1f, 1f); - if (gameObject == null) { return; } - - // TODO: just find the parent of any number of Navigation Screens - if (gameObject.GetComponent() != null) { - if (Button(rect, "Save", "", saturatedViolet, FontStyle.Normal)) { - foreach (var menu in menus) { - menu.gameObject.GetComponent()?.StopEditing(); - } - } - return; - } - - if (gameObject.GetComponent() != null) { - if (Button(rect, "Edit", "", saturatedViolet, FontStyle.Normal, gameObject.activeSelf ? 2 : 1)) { - foreach (var menu in menus) { - menu.gameObject.GetComponent()?.StopEditing(); - } - gameObject.GetComponent().StartEditing(); - } - return; - } + return response; } } } \ No newline at end of file diff --git a/Runtime/Exceptions.cs b/Runtime/Exceptions.cs index cc0d7a2..8d112c3 100644 --- a/Runtime/Exceptions.cs +++ b/Runtime/Exceptions.cs @@ -9,4 +9,8 @@ public Bail(string message) : base(message) { } public class VioletException : Exception { public VioletException(string message) : base(message) { } } + + public class VioletEnumException : VioletException { + public VioletEnumException(string message) : base(message) { } + } } \ No newline at end of file diff --git a/Runtime/Navigation/NavigationController.cs b/Runtime/Navigation/NavigationController.cs deleted file mode 100644 index 928bf77..0000000 --- a/Runtime/Navigation/NavigationController.cs +++ /dev/null @@ -1,6 +0,0 @@ -using UnityEngine; - -namespace VioletUI { - public class NavigationController : TidyBehaviour { - } -} diff --git a/Runtime/Navigation/NavigationScreen.cs b/Runtime/Navigation/NavigationScreen.cs deleted file mode 100644 index 6c0c658..0000000 --- a/Runtime/Navigation/NavigationScreen.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using UnityEngine; -#if UNITY_EDITOR -using UnityEditor; -#endif - -namespace VioletUI { - - public class NavigationScreen : TidyBehaviour { - #if UNITY_EDITOR - public static bool IsSavingPrefab; - - public void StartEditing() { - IsSavingPrefab = false; - try { - PrefabUtility.UnpackPrefabInstance(gameObject, PrefabUnpackMode.OutermostRoot, InteractionMode.AutomatedAction); - } catch (ArgumentException e) { - UnityEngine.Debug.LogWarning($"VioletUI: Couldn't unpack prefab for {name}. Make sure the NavigationScreen is a prefab. {e}"); - } - gameObject.SetActive(true); - } - - public void StopEditing() { - IsSavingPrefab = true; - PrefabUtility.SaveAsPrefabAssetAndConnect(gameObject, $"Assets/Menus/{name}.prefab", InteractionMode.AutomatedAction); - gameObject.SetActive(false); - } - #endif - } -} \ No newline at end of file diff --git a/Runtime/Navigation/Navigator.cs b/Runtime/Navigation/Navigator.cs new file mode 100644 index 0000000..1787896 --- /dev/null +++ b/Runtime/Navigation/Navigator.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using Sirenix.OdinInspector; +using System.Threading; +using UniRx.Async; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace VioletUI { + /// + /// Screens maintains a map of enum to menu screens. + /// + /// It is primarily used to navigate between screens, and also exposes the and lifecycle events. + /// + [ExecuteAlways] + public class Navigator : TidyBehaviour { + public ScreenId homeScreen = ScreenId.None; + public bool hasCamera; + [ShowIf("hasCamera")] + public Camera worldCamera; + + public Action OnReady; + + public Action OnWillVisit; + public Action OnWillLeave; + public Action OnDidVisit; + public Action OnDidLeave; + + public Action OnModalShow; + public Action OnModalHide; + + Dictionary screens = new Dictionary(); + ScreenId lastScreen = ScreenId.None; + ScreenId currentScreen = ScreenId.None; + ScreenId currentModal = ScreenId.None; + + [NonSerialized] + CancellationTokenSource canceler = null; + + void Awake() { + if (!Application.isPlaying) { return; } + LoadScreens(); + VisitFirstScreen(); + } + void LoadScreens() { + if (transform.childCount == 0) { + throw new Exception($"Tried to create a NavigationController with no children - try adding some NavigationScreens to {gameObject.name}"); + } + + ScreenId screenId = ScreenId.None; + foreach (Screen screen in GetComponentsInChildren(true)) { + var isValid = Enum.TryParse(screen.name, out screenId); + if (!isValid) { + throw new VioletException($"{screen.name} does not have a valid ScreenId. Make sure this screen is added to MenuBuilder."); + } + + if (hasCamera) { + if (worldCamera == null) { + throw new VioletException($"{name} does not have a camera attached. Either set hasCamera to false or attach a camera."); + } + var canvas = screen.gameObject.GetComponent(); + if (canvas == null) { + throw new VioletException($"{screen.name} does not have a Canvas component to attach {worldCamera.name} to. Either make {screen.name} a canvas or remove the camera from {name}"); + } + canvas.renderMode = RenderMode.ScreenSpaceCamera; + canvas.worldCamera = worldCamera; + } + + screens[screenId] = screen; + screen.gameObject.SetActive(false); + } + + OnReady?.Invoke(); + } + + /// + /// Visit takes a and transitions the menu to that scene. + /// + /// + public async void Visit(ScreenId screenId) { + if (!screens.ContainsKey(screenId)) { + throw new Exception($"Tried to visit {screenId} but it doesn't exist in the current scene. You'll want to add the {screenId} prefab to this scene or to the MenuBuilder prefab. Or change the Home Screen to the screen you want."); + } + + // if we're currently in a transition, cancel the transition and run OnHide/OnShow immediately + if (canceler != null) { + canceler.Cancel(); + canceler.Dispose(); + } + canceler = new CancellationTokenSource(); + + // current screen exits + bool ok = true; + var hidePromise = UniTask.CompletedTask; + if (currentScreen != ScreenId.None) { + lastScreen = currentScreen; + OnWillLeave?.Invoke(lastScreen); + hidePromise = screens[lastScreen].Hide(canceler.Token).ContinueWith((x) => { + ok = x; + OnDidLeave?.Invoke(lastScreen); + }); + } + await hidePromise; + + // new screen enters + OnWillVisit?.Invoke(screenId); + ok &= await screens[screenId].Show(canceler.Token); + OnDidVisit?.Invoke(screenId); + + currentScreen = screenId; + if (ok) { + canceler.Dispose(); + canceler = null; + } + } + + public void ShowModal(ScreenId screenId) { + // we have to call this before setting things to active because + // it causes all input listeners to unsubscribe + OnModalShow?.Invoke(screenId); + screens[screenId].gameObject.SetActive(true); + currentModal = screenId; + } + + public void CloseModal() { + if (currentModal == ScreenId.None) { return; } + screens[currentModal].gameObject.SetActive(false); + OnModalHide?.Invoke(currentModal); + currentModal = ScreenId.None; + } + + // Sigh. + // https://forum.unity.com/threads/ability-to-add-enum-argument-to-button-functions.270817/ + /// + /// Visit by string is for use in UnityEvents only. In code, please use Visit by enum + /// + /// + public void Visit(string screenIdString) { + ScreenId screenId; + var isValid = Enum.TryParse(screenIdString.Replace(" ", ""), out screenId); + if (!isValid) { + throw new Exception($"Couldn't find a screen with the id {screenIdString.Replace(" ", "")}. Please check the spelling."); + } + Visit(screenId); + } + + public void ShowModal (string screenIdString) { + ScreenId screenId; + var isValid = Enum.TryParse(screenIdString.Replace(" ", ""), out screenId); + if (!isValid) { + throw new Exception($"Couldn't find a screen with the id {screenIdString.Replace(" ", "")}. Please check the spelling."); + } + + ShowModal(screenId); + } + + protected virtual void VisitFirstScreen() { + Visit(homeScreen); + } + + ScreenId ScreenToScreenId(Screen screen) { + ScreenId ret; + var slug = screen.name.Replace(" ", ""); + var ok = Enum.TryParse(slug, out ret); + + if (!ok) { + throw new VioletEnumException($"{slug} does not exist in ScreenId. This should be set up automatically by navigator but you can add manually as a workaround."); + } + return ret; + } + +#if UNITY_EDITOR + [HideInInspector] public Screen EditingScreen; + [SerializeField, HideInInspector] ScreenId originalHomeScreen; + + public void Edit(Screen screen) { + try { + homeScreen = ScreenToScreenId(screen); + } catch (VioletEnumException) { + ScreenIdGenerator.Generate(screen); + Debug.LogWarning($"VioletUI - Couldn't find {screen.name} in the ScreenId enum. This should be fixed if you try your action again. If not, please report a bug."); + return; + } + + try { + PrefabUtility.UnpackPrefabInstance(screen.gameObject, PrefabUnpackMode.OutermostRoot, InteractionMode.AutomatedAction); + } catch (ArgumentException) {} + + screen.gameObject.SetActive(true); + EditingScreen = screen; + originalHomeScreen = homeScreen; + + } + + public void FinishEditing(Screen screen = null) { + if (EditingScreen == null) { EditingScreen = gameObject.GetComponentInChildren(); } + if (screen == null) { screen = EditingScreen; } + PrefabUtility.SaveAsPrefabAssetAndConnect(screen.gameObject, $"Assets/Menus/{screen.name}.prefab", InteractionMode.AutomatedAction); + screen.gameObject.SetActive(false); + if (screen == EditingScreen) { EditingScreen = null; } ; + homeScreen = originalHomeScreen; + } + + public void AddScreen() { + var gameObject = new GameObject("Rename Me"); + var screen = gameObject.AddComponent(); + var canvas = gameObject.AddComponent(); + + gameObject.transform.SetParent(transform, false); + gameObject.transform.position = new Vector3(0,0,0); + canvas.renderMode = worldCamera == null ? RenderMode.ScreenSpaceOverlay : RenderMode.ScreenSpaceCamera; + EditingScreen = screen; + } + + float lastUpdate; + int lastCount; + void Update() { + if (Application.isPlaying) { return; } + if (transform.childCount == 0) { return; } + if (transform.childCount == lastCount) { return; } + if (Time.time - lastUpdate <= .5f) { return; } + lastCount = transform.childCount; + lastUpdate = Time.time; + RegenerateEnums(); + } + + [Button] + void RegenerateEnums() { + var screens = GetComponentsInChildren(true); + ScreenIdGenerator.Generate(screens); + } +#endif + } +} diff --git a/Runtime/Navigation/NavigationController.cs.meta b/Runtime/Navigation/Navigator.cs.meta similarity index 83% rename from Runtime/Navigation/NavigationController.cs.meta rename to Runtime/Navigation/Navigator.cs.meta index a14f4e2..278f0a1 100644 --- a/Runtime/Navigation/NavigationController.cs.meta +++ b/Runtime/Navigation/Navigator.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 139d31900cb6042c79cf5e3cd42871fe +guid: 5f588ebd1ec10485cbfd7f576fa49a3b MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Navigation/Screen.cs b/Runtime/Navigation/Screen.cs new file mode 100644 index 0000000..ffdf30e --- /dev/null +++ b/Runtime/Navigation/Screen.cs @@ -0,0 +1,23 @@ +using System; +using UnityEngine; +using System.Threading.Tasks; +using System.Threading; +using UniRx.Async; + +namespace VioletUI { + public class Screen : TidyBehaviour { + + internal async UniTask Show(CancellationToken token) { + gameObject.SetActive(true); + await UniTask.DelayFrame(1); + return true; + } + + internal async UniTask Hide(CancellationToken token) { + gameObject.SetActive(false); + await UniTask.DelayFrame(1); + return true; + } + + } +} \ No newline at end of file diff --git a/Editor/EventExtensions.cs.meta b/Runtime/Navigation/Screen.cs.meta similarity index 83% rename from Editor/EventExtensions.cs.meta rename to Runtime/Navigation/Screen.cs.meta index b69ae68..d196580 100644 --- a/Editor/EventExtensions.cs.meta +++ b/Runtime/Navigation/Screen.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 56a32b8aa1bdf4af39eeb066256f8fe4 +guid: 3b4b68ba246ae487eb6c4045d9182b91 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Navigation/ScreenId.cs b/Runtime/Navigation/ScreenId.cs new file mode 100644 index 0000000..eed3b1c --- /dev/null +++ b/Runtime/Navigation/ScreenId.cs @@ -0,0 +1,6 @@ +//Names are automatically added through ScreenIdGenerator.cs, deletions are done manually :) +namespace VioletUI { + public enum ScreenId { + None = 0 + } +} diff --git a/Runtime/Navigation/NavigationScreen.cs.meta b/Runtime/Navigation/ScreenId.cs.meta similarity index 83% rename from Runtime/Navigation/NavigationScreen.cs.meta rename to Runtime/Navigation/ScreenId.cs.meta index 9bd2748..cafdc79 100644 --- a/Runtime/Navigation/NavigationScreen.cs.meta +++ b/Runtime/Navigation/ScreenId.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: fbd2e3005c2c74f999485b6d69bc544d +guid: 378935ae0584e4bea961ab67d341a3ed MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Navigation/ScreenIdGenerator.cs b/Runtime/Navigation/ScreenIdGenerator.cs new file mode 100644 index 0000000..4fb550e --- /dev/null +++ b/Runtime/Navigation/ScreenIdGenerator.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEngine; + +namespace VioletUI { + public class ScreenIdGenerator { + static StringBuilder sb = new StringBuilder(); + + public static void Generate(Screen screen) { + Generate(new Screen[] {screen}); + } + + public static void Generate(Screen[] screens) { + var screenStrings = new List(screens.Length); + foreach (var screen in screens) { + screenStrings.Add(screen.name); + } + Generate(screenStrings); + } + + public static void Generate(List screens) { + var newScreens = Filter(screens); + if (newScreens.Count == 0) { return; } + AddScreens(newScreens); + } + + static List Filter(List screens) { + var ret = new List(); + foreach (var screen in screens) { + if (screen == null) { continue; } + var name = sanitize(screen); + if (Enum.TryParse(screen, out _)) { continue; } + ret.Add(name); + } + return ret; + } + + static void AddScreens(List screens) { + sb.Clear(); + sb.AppendLine("//Names are automatically added through ScreenIdGenerator.cs, deletions are done manually :)"); + sb.AppendLine("namespace VioletUI {"); + sb.AppendLine("\tpublic enum ScreenId {"); + sb.AppendLine("\t\tNone = 0,"); + + // write all existing Enum values - note that unused screens + // will have to be deleted manually. this is to avoid losing references. + foreach (ScreenId screenId in Enum.GetValues(typeof(ScreenId))) { + if (screenId == ScreenId.None) { continue; } + sb.AppendLine($"\t\t{Enum.GetName(typeof(ScreenId), screenId)} = {(int)screenId},"); + } + + // write any new screen names with incrementing ids + var nextId = Enum.GetValues(typeof(ScreenId)).Cast().Max() + 1; + foreach (var screen in screens) { + sb.AppendLine($"\t\t{screen} = {nextId++},"); + } + sb.AppendLine("\t}"); + sb.AppendLine("}"); + string path = Path.GetFullPath("Packages/violetui/Runtime/Navigation/ScreenId.cs"); + File.WriteAllText(path, sb.ToString()); + AssetDatabase.Refresh(); + } + + static string sanitize(string s) { + return s.Replace(" ", ""); + } + } +} diff --git a/Editor/RectExtensions.cs.meta b/Runtime/Navigation/ScreenIdGenerator.cs.meta similarity index 83% rename from Editor/RectExtensions.cs.meta rename to Runtime/Navigation/ScreenIdGenerator.cs.meta index 2f203e4..acc0284 100644 --- a/Editor/RectExtensions.cs.meta +++ b/Runtime/Navigation/ScreenIdGenerator.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 21a9203cf16324e1199c1bffab99b1c1 +guid: 3cb3551e7a6b249a39cbd045f31ce5b6 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/NeilSarkar.VioletUI.Runtime.asmdef b/Runtime/NeilSarkar.VioletUI.Runtime.asmdef index e3b7f87..4fc7e53 100644 --- a/Runtime/NeilSarkar.VioletUI.Runtime.asmdef +++ b/Runtime/NeilSarkar.VioletUI.Runtime.asmdef @@ -1,7 +1,8 @@ { "name": "NeilSarkar.VioletUI.Runtime", "references": [ - "GUID:8e3dba6773ea84ec89c8bb46293c6471" + "GUID:8e3dba6773ea84ec89c8bb46293c6471", + "GUID:f51ebe6a0ceec4240a699833d6309b23" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/StateMonoBehaviour.cs b/Runtime/StateMonoBehaviour.cs index 7d72ab3..2bc8962 100644 --- a/Runtime/StateMonoBehaviour.cs +++ b/Runtime/StateMonoBehaviour.cs @@ -4,6 +4,9 @@ using UnityEngine; using Dispatch; using Sirenix.OdinInspector; +#if UNITY_EDITOR +using UnityEditor.ShortcutManagement; +#endif namespace VioletUI { [ExecuteAlways] @@ -50,14 +53,15 @@ void OnValidate() { } [Button, GUIColor(0.898f, 0.745f, 0.935f)] + [Shortcut("VioletRender", KeyCode.Period, ShortcutModifiers.Action)] void Render() { + print($"Rendering!"); State.TriggerChange(); CopyState(); } void Update() { if (Application.isPlaying) { return; } - Singleton = this; } #endif