using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; using UnityGameFramework.Runtime; namespace GeometryTD.UI { public class ItemDescForm : UGuiForm { [SerializeField] private RectTransform _content; [SerializeField] private TMP_Text _itemTitle; [SerializeField] private TMP_Text _itemTypeText; [SerializeField] private RectTransform _descScrollView; [SerializeField] private TMP_Text _itemDescription; [SerializeField] private TMP_Text _itemPrice; [SerializeField] private Transform _tagAreaParent; [SerializeField] private GameObject _tagItemPrefab; [SerializeField] private bool _autoResizeHeight = true; [SerializeField] private float _fixedTopHeight = 150f; [SerializeField] private float _fixedBottomHeight = 150f; [SerializeField] private float _fixedWidth = 300f; [SerializeField] private float _maxDescriptionViewportHeight = 0f; [SerializeField] private float _screenEdgePadding = 0f; [SerializeField] private float _sideGap = 16f; [SerializeField] private float _anchorItemWidth = 0f; private readonly List _runtimeTagItems = new List(); private ItemDescFormContext _context; private Vector2 _targetPos; private bool _isOpened; public void RefreshUI(ItemDescFormContext context) { if (context == null) { Log.Warning("ItemDescForm context is invalid."); return; } _context = context; _targetPos = ConvertScreenToAnchored(_context.ScreenPosition); if (_content != null) { _content.anchoredPosition3D = _targetPos; } if (_itemTitle != null) _itemTitle.text = _context.Title ?? string.Empty; if (_itemTypeText != null) _itemTypeText.text = _context.TypeText ?? string.Empty; if (_itemDescription != null) _itemDescription.text = _context.Description ?? string.Empty; if (_itemPrice != null) _itemPrice.text = $"Price: {_context.Price} Gold"; RefreshTags(_context.Tags); ResizeToFitContent(); ApplySideAnchoredTargetPos(); } protected override void OnOpen(object userData) { base.OnOpen(userData); _isOpened = true; if (!(userData is ItemDescFormContext context)) { Log.Error("ItemDescFormContext is invalid."); return; } RefreshUI(context); } protected override void OnClose(bool isShutdown, object userData) { _isOpened = false; ClearTags(); _context = null; base.OnClose(isShutdown, userData); } public void OnBlankAreaClick() { CloseSelf(); } public void OnBackgroundClick() { CloseSelf(); } private void Update() { if (!_isOpened || _content == null) { return; } if (!TryGetPointerDownScreenPosition(out Vector2 screenPoint)) { return; } if (IsPointerInContent(screenPoint)) { return; } CloseSelf(); } private void ResizeToFitContent() { if (!_autoResizeHeight || _content == null) { return; } if (_itemDescription == null) { return; } _itemDescription.ForceMeshUpdate(); float descriptionHeight = Mathf.Max(0f, _itemDescription.preferredHeight); SetRectHeight(_itemDescription.rectTransform, descriptionHeight); RectTransform scrollContent = _itemDescription.rectTransform.parent as RectTransform; if (scrollContent != null) { SetRectHeight(scrollContent, descriptionHeight); } float viewportHeight = descriptionHeight; if (_descScrollView != null) { viewportHeight = Mathf.Min(descriptionHeight, ResolveMaxDescriptionViewportHeight()); SetRectHeight(_descScrollView, viewportHeight); if (_descScrollView.TryGetComponent(out ScrollRect scrollRect)) { scrollRect.verticalNormalizedPosition = 1f; } } float targetHeight = _fixedTopHeight + viewportHeight + _fixedBottomHeight; Vector2 size = new Vector2(_fixedWidth, targetHeight); _content.sizeDelta = size; } private float ResolveMaxDescriptionViewportHeight() { float maxViewportHeight = _maxDescriptionViewportHeight; RectTransform parent = _content.parent as RectTransform; if (parent != null) { float availableHeight = parent.rect.height - _fixedTopHeight - _fixedBottomHeight - _screenEdgePadding * 2f; availableHeight = Mathf.Max(0f, availableHeight); maxViewportHeight = maxViewportHeight > 0f ? Mathf.Min(maxViewportHeight, availableHeight) : availableHeight; } return maxViewportHeight > 0f ? maxViewportHeight : float.MaxValue; } private static void SetRectHeight(RectTransform rectTransform, float height) { if (rectTransform == null) { return; } Vector2 size = rectTransform.sizeDelta; size.y = height; rectTransform.sizeDelta = size; } private void ApplySideAnchoredTargetPos() { if (_content == null) { return; } RectTransform parent = _content.parent as RectTransform; if (parent == null) { return; } Vector2 size = _content.sizeDelta; if (size.x <= 0f || size.y <= 0f) { size = _content.rect.size; } Vector2 pivot = _content.pivot; float halfWidth = parent.rect.width * 0.5f; float halfHeight = parent.rect.height * 0.5f; float minX = -halfWidth + size.x * pivot.x + _screenEdgePadding; float maxX = halfWidth - size.x * (1f - pivot.x) - _screenEdgePadding; float minY = -halfHeight + size.y * pivot.y + _screenEdgePadding; float maxY = halfHeight - size.y * (1f - pivot.y) - _screenEdgePadding; float clampedY = minY <= maxY ? Mathf.Clamp(_targetPos.y, minY, maxY) : 0f; float anchorHalfWidth = Mathf.Max(0f, _anchorItemWidth * 0.5f); float sideGap = Mathf.Max(0f, _sideGap); float horizontalOffset = anchorHalfWidth + size.x * 0.5f + sideGap; float leftCandidateX = _targetPos.x - horizontalOffset; float rightCandidateX = _targetPos.x + horizontalOffset; float finalX; if (IsInsideBounds(leftCandidateX, minX, maxX)) { finalX = leftCandidateX; } else if (IsInsideBounds(rightCandidateX, minX, maxX)) { finalX = rightCandidateX; } else { finalX = minX <= maxX ? Mathf.Clamp(leftCandidateX, minX, maxX) : 0f; } _content.anchoredPosition = new Vector2(finalX, clampedY); } private Vector2 ConvertScreenToAnchored(Vector2 screenPosition) { if (_content == null) { return screenPosition; } RectTransform parent = _content.parent as RectTransform; if (parent == null) { return screenPosition; } Canvas canvas = _content.GetComponentInParent(); Canvas rootCanvas = canvas != null ? canvas.rootCanvas : null; Camera uiCamera = null; if (rootCanvas != null && rootCanvas.renderMode != RenderMode.ScreenSpaceOverlay) { uiCamera = rootCanvas.worldCamera != null ? rootCanvas.worldCamera : Camera.main; } if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(parent, screenPosition, uiCamera, out Vector2 localPoint)) { return screenPosition; } return localPoint; } private static bool IsInsideBounds(float value, float min, float max) { return value >= min && value <= max; } private void RefreshTags(TagItemContext[] tags) { ClearTags(); if (_tagAreaParent == null || _tagItemPrefab == null || tags == null || tags.Length <= 0) { return; } if (_tagItemPrefab.scene.IsValid() && _tagItemPrefab.transform.IsChildOf(_tagAreaParent)) { _tagItemPrefab.SetActive(false); } for (int i = 0; i < tags.Length; i++) { TagItemContext tagContext = tags[i]; if (tagContext == null || string.IsNullOrWhiteSpace(tagContext.TagName)) { continue; } GameObject tagGo = Instantiate(_tagItemPrefab, _tagAreaParent); tagGo.SetActive(true); if (tagGo.TryGetComponent(out TagItem tagItem)) { tagItem.OnInit(tagContext); } else { TMP_Text tagText = tagGo.GetComponentInChildren(true); if (tagText != null) { tagText.text = tagContext.TagName; } } _runtimeTagItems.Add(tagGo); } } private void ClearTags() { for (int i = _runtimeTagItems.Count - 1; i >= 0; i--) { GameObject tagGo = _runtimeTagItems[i]; if (tagGo != null) { Destroy(tagGo); } } _runtimeTagItems.Clear(); } private void CloseSelf() { GameEntry.UI.CloseUIForm(this); } private bool IsPointerInContent(Vector2 screenPoint) { Canvas canvas = _content.GetComponentInParent(); Canvas rootCanvas = canvas != null ? canvas.rootCanvas : null; Camera uiCamera = null; if (rootCanvas != null && rootCanvas.renderMode != RenderMode.ScreenSpaceOverlay) { uiCamera = rootCanvas.worldCamera != null ? rootCanvas.worldCamera : Camera.main; } return RectTransformUtility.RectangleContainsScreenPoint(_content, screenPoint, uiCamera); } private static bool TryGetPointerDownScreenPosition(out Vector2 screenPoint) { if (Input.touchCount > 0) { for (int i = 0; i < Input.touchCount; i++) { Touch touch = Input.GetTouch(i); if (touch.phase != TouchPhase.Began) { continue; } screenPoint = touch.position; return true; } } if (Input.GetMouseButtonDown(0)) { screenPoint = Input.mousePosition; return true; } screenPoint = default; return false; } } }