// Filename: EC_UIUtility.cs // Creator: Extracted from EC_InventoryUI.cs // Date: 2024 // Purpose: Reusable UI helper functions for panel positioning, text handling, and layout management using UnityEngine; using UnityEngine.UI; namespace BrewMonster.Scripts.UI { /// /// General-purpose UI utility class providing reusable functions for: /// - Smart panel positioning near buttons with edge detection /// - Dual text component support (Legacy Text + TextMeshPro) /// - Layout refresh helpers /// public static class EC_UIUtility { /// /// Position a panel near a button with smart edge detection to keep panel on screen. /// Automatically chooses left or right placement based on available space. /// /// The panel RectTransform to position /// The button RectTransform to position near /// Offset from button (x: horizontal spacing, y: vertical offset) public static void PositionPanelNearButton(RectTransform panelRect, RectTransform buttonRect, Vector2 offset) { if (panelRect == null || buttonRect == null) { Debug.LogWarning("[EC_UIUtility] PositionPanelNearButton: panelRect or buttonRect is null"); return; } var canvas = panelRect.GetComponentInParent(); if (canvas == null) { Debug.LogWarning("[EC_UIUtility] PositionPanelNearButton: Cannot find parent Canvas"); return; } var parentRect = panelRect.parent as RectTransform; if (parentRect == null) { Debug.LogWarning("[EC_UIUtility] PositionPanelNearButton: Panel parent is not a RectTransform"); return; } Camera eventCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera; Vector3 worldCenter = buttonRect.TransformPoint(buttonRect.rect.center); Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(eventCamera, worldCenter); Vector2 localPoint; if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRect, screenPoint, eventCamera, out localPoint)) { Debug.LogWarning("[EC_UIUtility] PositionPanelNearButton: Failed to convert screen point to local point"); return; } float btnHalfW = buttonRect.rect.width * 0.5f; float panelW = panelRect.rect.width; float panelH = panelRect.rect.height; float pivotX = panelRect.pivot.x; float pivotY = panelRect.pivot.y; // Compute right-placement candidate (panel's left edge at button's right edge + offset) float leftEdgeRightPlacement = localPoint.x + btnHalfW + offset.x; float candidateXRight = leftEdgeRightPlacement + pivotX * panelW; // Compute left-placement candidate (panel's right edge at button's left edge - offset) float rightEdgeLeftPlacement = localPoint.x - btnHalfW - offset.x; float candidateXLeft = rightEdgeLeftPlacement - (1f - pivotX) * panelW; // Vertical clamping honoring pivot float minY = parentRect.rect.yMin + pivotY * panelH; float maxY = parentRect.rect.yMax - (1f - pivotY) * panelH; float candidateY = Mathf.Clamp(localPoint.y + offset.y, minY, maxY); // Choose side based on available space float rightEdgeOfRight = candidateXRight + (1f - pivotX) * panelW; float canvasRight = parentRect.rect.xMax; float canvasLeft = parentRect.rect.xMin; float leftEdgeOfLeft = candidateXLeft - pivotX * panelW; Vector2 finalPos; if (rightEdgeOfRight <= canvasRight) { // Right placement fits finalPos = new Vector2(candidateXRight, candidateY); } else if (leftEdgeOfLeft >= canvasLeft) { // Left placement fits finalPos = new Vector2(candidateXLeft, candidateY); } else { // Fallback: clamp within canvas horizontally float minX = canvasLeft + pivotX * panelW; float maxX = canvasRight - (1f - pivotX) * panelW; finalPos = new Vector2(Mathf.Clamp(candidateXRight, minX, maxX), candidateY); } panelRect.anchoredPosition = finalPos; } /// /// Force Unity to rebuild layout immediately for a RectTransform. /// Useful after dynamically changing content that affects layout. /// /// The RectTransform to refresh public static void RefreshLayout(RectTransform rectTransform) { if (rectTransform == null) { Debug.LogWarning("[EC_UIUtility] RefreshLayout: rectTransform is null"); return; } // Force Unity to rebuild layout immediately rectTransform.ForceUpdateRectTransforms(); LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform); } /// /// Show or hide a panel with layout refresh. /// /// The GameObject panel to show/hide /// True to show, false to hide public const int OverlayPanelSortOrder = 2000; public static void ShowPanel(GameObject panel, bool show) { if (panel == null) { Debug.LogWarning("[EC_UIUtility] ShowPanel: panel is null"); return; } panel.SetActive(show); if (show) { var rectTransform = panel.GetComponent(); if (rectTransform != null) { RefreshLayout(rectTransform); } } } /// Draw panel above other UI dialogs (sibling order + canvas sort). public static void BringPanelToFront(GameObject panel) { if (panel == null) return; panel.transform.SetAsLastSibling(); var canvas = panel.GetComponent(); if (canvas == null) canvas = panel.AddComponent(); canvas.overrideSorting = true; canvas.sortingOrder = OverlayPanelSortOrder; if (panel.GetComponent() == null) panel.AddComponent(); } /// /// Helper class for managing dual text component support (Legacy Text + TextMeshPro). /// Allows UI components to work with both text systems simultaneously. /// [System.Serializable] public class TextOutlet { [SerializeField] public Text legacy; [SerializeField] public TMPro.TextMeshProUGUI tmp; /// /// Set text on both legacy and TMP components with automatic formatting. /// /// The text to display public void Set(string value) { if (legacy != null) { legacy.text = EC_Utility.FormatForLegacyText(value ?? string.Empty); } if (tmp != null) { tmp.text = EC_Utility.FormatForTextMeshPro(value ?? string.Empty); } } /// /// Set text with explicit formatting preference. /// /// Raw text with formatting codes /// Whether to prefer TextMeshPro formatting public void SetFormatted(string value, bool preferTextMeshPro = true) { string formattedText = preferTextMeshPro ? EC_Utility.FormatForTextMeshPro(value ?? string.Empty) : EC_Utility.FormatForLegacyText(value ?? string.Empty); if (legacy != null) { legacy.text = formattedText; } if (tmp != null) { tmp.text = formattedText; } } } } }