/* * Copyright 2022 Peter Han * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using PeterHan.PLib.Core; using PeterHan.PLib.Detours; using System; using UnityEngine; using UnityEngine.UI; using ContentFitMode = UnityEngine.UI.ContentSizeFitter.FitMode; namespace PeterHan.PLib.UI { /// /// Used for creating and managing UI elements. /// public sealed class PUIElements { /// /// Safely adds a LocText to a game object without throwing an NRE on construction. /// /// The game object to add the LocText. /// The text style. /// The added LocText object. internal static LocText AddLocText(GameObject parent, TextStyleSetting setting = null) { if (parent == null) throw new ArgumentNullException(nameof(parent)); bool active = parent.activeSelf; parent.SetActive(false); var text = parent.AddComponent(); // This is enough to let it activate UIDetours.LOCTEXT_KEY.Set(text, string.Empty); UIDetours.LOCTEXT_STYLE.Set(text, setting ?? PUITuning.Fonts.UIDarkStyle); parent.SetActive(active); return text; } /// /// Adds an auto-fit resizer to a UI element. /// /// UI elements should be active before any layouts are added, especially if they are /// to be frozen. /// /// The element to resize. /// true to use the Unity content size fitter which adjusts to /// content changes, or false to set the size only once. /// The sizing mode to use in the horizontal direction. /// The sizing mode to use in the vertical direction. /// The UI element, for call chaining. public static GameObject AddSizeFitter(GameObject uiElement, bool dynamic = false, ContentFitMode modeHoriz = ContentFitMode.PreferredSize, ContentFitMode modeVert = ContentFitMode.PreferredSize) { if (uiElement == null) throw new ArgumentNullException(nameof(uiElement)); if (dynamic) { var fitter = uiElement.AddOrGet(); fitter.horizontalFit = modeHoriz; fitter.verticalFit = modeVert; fitter.enabled = true; } else FitSizeNow(uiElement, modeHoriz, modeVert); return uiElement; } /// /// Creates a UI game object. /// /// The parent of the UI object. If not set now, or added/changed /// later, the anchors must be redefined. /// The object name. /// true to add a canvas renderer, or false otherwise. /// How to anchor the object horizontally. /// How to anchor the object vertically. /// The UI object with transform and canvas initialized. public static GameObject CreateUI(GameObject parent, string name, bool canvas = true, PUIAnchoring horizAnchor = PUIAnchoring.Stretch, PUIAnchoring vertAnchor = PUIAnchoring.Stretch) { var element = new GameObject(name); if (parent != null) element.SetParent(parent); // Size and position var transform = element.AddOrGet(); transform.localScale = Vector3.one; SetAnchors(element, horizAnchor, vertAnchor); // Almost all UI components need a canvas renderer for some reason if (canvas) element.AddComponent(); element.layer = LayerMask.NameToLayer("UI"); return element; } /// /// Does nothing, to make the buttons appear. /// private static void DoNothing() { } /// /// Fits the UI element's size immediately, as if ContentSizeFitter was created on it, /// but does not create a component and only affects the size once. /// /// The element to resize. /// The sizing mode to use in the horizontal direction. /// The sizing mode to use in the vertical direction. /// The UI element, for call chaining. private static void FitSizeNow(GameObject uiElement, ContentFitMode modeHoriz, ContentFitMode modeVert) { float width = 0.0f, height = 0.0f; if (uiElement == null) throw new ArgumentNullException(nameof(uiElement)); // Follow order in https://docs.unity3d.com/Manual/UIAutoLayout.html var elements = uiElement.GetComponents(); var constraints = uiElement.AddOrGet(); var rt = uiElement.AddOrGet(); // Calculate horizontal foreach (var layoutElement in elements) layoutElement.CalculateLayoutInputHorizontal(); if (modeHoriz != ContentFitMode.Unconstrained) { // Layout horizontal foreach (var layoutElement in elements) switch (modeHoriz) { case ContentFitMode.MinSize: width = Math.Max(width, layoutElement.minWidth); break; case ContentFitMode.PreferredSize: width = Math.Max(width, layoutElement.preferredWidth); break; default: break; } width = Math.Max(width, constraints.minWidth); rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width); constraints.minWidth = width; constraints.flexibleWidth = 0.0f; } // Calculate vertical foreach (var layoutElement in elements) layoutElement.CalculateLayoutInputVertical(); if (modeVert != ContentFitMode.Unconstrained) { // Layout vertical foreach (var layoutElement in elements) switch (modeVert) { case ContentFitMode.MinSize: height = Math.Max(height, layoutElement.minHeight); break; case ContentFitMode.PreferredSize: height = Math.Max(height, layoutElement.preferredHeight); break; default: break; } height = Math.Max(height, constraints.minHeight); rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height); constraints.minHeight = height; constraints.flexibleHeight = 0.0f; } } /// /// Sets the anchor location of a UI element. The offsets will be reset, use /// SetAnchorOffsets to adjust the offset from the new anchor locations. /// /// The UI element to modify. /// The horizontal anchor mode. /// The vertical anchor mode. /// The UI element, for call chaining. public static GameObject SetAnchors(GameObject uiElement, PUIAnchoring horizAnchor, PUIAnchoring vertAnchor) { Vector2 aMax = new Vector2(), aMin = new Vector2(), pivot = new Vector2(); if (uiElement == null) throw new ArgumentNullException(nameof(uiElement)); var transform = uiElement.rectTransform(); // Anchor: horizontal switch (horizAnchor) { case PUIAnchoring.Center: aMin.x = 0.5f; aMax.x = 0.5f; pivot.x = 0.5f; break; case PUIAnchoring.End: aMin.x = 1.0f; aMax.x = 1.0f; pivot.x = 1.0f; break; case PUIAnchoring.Stretch: aMin.x = 0.0f; aMax.x = 1.0f; pivot.x = 0.5f; break; default: aMin.x = 0.0f; aMax.x = 0.0f; pivot.x = 0.0f; break; } // Anchor: vertical switch (vertAnchor) { case PUIAnchoring.Center: aMin.y = 0.5f; aMax.y = 0.5f; pivot.y = 0.5f; break; case PUIAnchoring.End: aMin.y = 1.0f; aMax.y = 1.0f; pivot.y = 1.0f; break; case PUIAnchoring.Stretch: aMin.y = 0.0f; aMax.y = 1.0f; pivot.y = 0.5f; break; default: aMin.y = 0.0f; aMax.y = 0.0f; pivot.y = 0.0f; break; } transform.anchorMax = aMax; transform.anchorMin = aMin; transform.pivot = pivot; transform.anchoredPosition = Vector2.zero; transform.offsetMax = Vector2.zero; transform.offsetMin = Vector2.zero; return uiElement; } /// /// Sets the offsets of the UI component from its anchors. Positive for each value /// denotes towards the component center, and negative away from the component center. /// /// The UI element to modify. /// The offset of each corner from the anchors. /// The UI element, for call chaining. public static GameObject SetAnchorOffsets(GameObject uiElement, RectOffset border) { return SetAnchorOffsets(uiElement, border.left, border.right, border.top, border. bottom); } /// /// Sets the offsets of the UI component from its anchors. Positive for each value /// denotes towards the component center, and negative away from the component center. /// /// The UI element to modify. /// The left border in pixels. /// The right border in pixels. /// The top border in pixels. /// The bottom border in pixels. /// The UI element, for call chaining. public static GameObject SetAnchorOffsets(GameObject uiElement, float left, float right, float top, float bottom) { if (uiElement == null) throw new ArgumentNullException(nameof(uiElement)); var transform = uiElement.rectTransform(); transform.offsetMin = new Vector2(left, bottom); transform.offsetMax = new Vector2(-right, -top); return uiElement; } /// /// Sets a UI element's text. /// /// The UI element to modify. /// The text to display on the element. /// The UI element, for call chaining. public static GameObject SetText(GameObject uiElement, string text) { if (uiElement == null) throw new ArgumentNullException(nameof(uiElement)); var lt = uiElement.GetComponentInChildren(); if (lt != null) lt.SetText(text ?? string.Empty); return uiElement; } /// /// Sets a UI element's tool tip. /// /// The UI element to modify. /// The tool tip text to display when hovered. /// The UI element, for call chaining. public static GameObject SetToolTip(GameObject uiElement, string tooltip) { if (uiElement == null) throw new ArgumentNullException(nameof(uiElement)); if (!string.IsNullOrEmpty(tooltip)) uiElement.AddOrGet().toolTip = tooltip; return uiElement; } /// /// Shows a confirmation dialog. /// /// The dialog's parent. /// The message to display. /// The action to invoke if Yes or OK is selected. /// The action to invoke if No or Cancel is selected. /// The text for the OK/Yes button. /// The text for the Cancel/No button. /// The dialog created. public static ConfirmDialogScreen ShowConfirmDialog(GameObject parent, string message, System.Action onConfirm, System.Action onCancel = null, string confirmText = null, string cancelText = null) { if (parent == null) parent = PDialog.GetParentObject(); var obj = Util.KInstantiateUI(ScreenPrefabs.Instance.ConfirmDialogScreen. gameObject, parent, false); if (obj.TryGetComponent(out ConfirmDialogScreen confirmDialog)) { UIDetours.POPUP_CONFIRM.Invoke(confirmDialog, message, onConfirm, onCancel ?? DoNothing, null, null, null, confirmText, cancelText); obj.SetActive(true); } else confirmDialog = null; return confirmDialog; } /// /// Shows a message dialog. /// /// The dialog's parent. /// The message to display. /// The dialog created. public static ConfirmDialogScreen ShowMessageDialog(GameObject parent, string message) { return ShowConfirmDialog(parent, message, DoNothing); } // This class should probably be static, but that might be binary compatibility // breaking private PUIElements() { } } /// /// The anchor mode to set a UI component. /// public enum PUIAnchoring { // Stretch to all Stretch, // Relative to beginning Beginning, // Relative to center Center, // Relative to end End } }