using TMPro; using UnityEngine; using UnityEngine.UI; namespace UI { [RequireComponent(typeof(VerticalLayoutGroup))] public class MessageAligner : MonoBehaviour { [Header("Bubble")] public Image bubbleBackground; public Sprite playerBubbleSprite; public Sprite aiBubbleSprite; public Color playerColor = new Color(0.2f, 0.6f, 1f); public Color aiColor = new Color(0.2f, 0.2f, 0.2f); public Color playerTextColor = Color.white; public Color aiTextColor = Color.white; [Header("Avatar Row")] public HorizontalLayoutGroup rowLayout; public GameObject avatarObject; public Image avatarImage; public Sprite playerAvatarSprite; public Sprite aiAvatarSprite; public Color playerAvatarColor = Color.white; public Color aiAvatarColor = Color.white; public bool showPlayerAvatar = true; public bool showAiAvatar = true; public float rowHorizontalPadding = 20f; public float rowSpacingWhenAvatarVisible = 14f; public float rowSpacingWhenAvatarHidden = 0f; public float rowMinHeight = 80f; [Header("Layout")] [Tooltip("Hard cap for bubble width before wrapping kicks in.")] public float maxBubbleWidth = 1000f; public bool clampToViewportWidth = true; [Range(0.1f, 1f)] public float viewportWidthRatio = 0.7f; private TextMeshProUGUI textComponent; private Sprite fallbackBubbleSprite; private Sprite fallbackAvatarSprite; private bool lastIsPlayer; private void Awake() { ResolveReferences(); CacheFallbacks(); } private void OnValidate() { ResolveReferences(); CacheFallbacks(); } public void SetAlignment(bool isPlayer) { lastIsPlayer = isPlayer; ResolveReferences(); CacheFallbacks(); ApplyRootLayout(); ApplyAvatarStyle(isPlayer); ApplyRowLayout(isPlayer); ApplyBubbleStyle(isPlayer); ApplyTextStyle(isPlayer); ApplyWidthLimit(); RebuildLayout(); } public void Reapply() { SetAlignment(lastIsPlayer); } private void ResolveReferences() { if (bubbleBackground == null) { TextMeshProUGUI tmp = GetComponentInChildren(true); if (tmp != null) { bubbleBackground = tmp.GetComponentInParent(); } } if (bubbleBackground != null && textComponent == null) { textComponent = bubbleBackground.GetComponentInChildren(true); } if (rowLayout == null && bubbleBackground != null && bubbleBackground.transform.parent != null) { rowLayout = bubbleBackground.transform.parent.GetComponent(); } if (avatarObject == null && rowLayout != null) { foreach (Transform child in rowLayout.transform) { if (bubbleBackground == null || child.gameObject != bubbleBackground.gameObject) { avatarObject = child.gameObject; break; } } } if (avatarImage == null && avatarObject != null) { avatarImage = avatarObject.GetComponent(); } } private void CacheFallbacks() { if (bubbleBackground != null) { if (fallbackBubbleSprite == null) { fallbackBubbleSprite = bubbleBackground.sprite; } if (aiBubbleSprite == null) aiBubbleSprite = fallbackBubbleSprite; if (playerBubbleSprite == null) playerBubbleSprite = fallbackBubbleSprite; } if (avatarImage != null) { if (fallbackAvatarSprite == null) { fallbackAvatarSprite = avatarImage.sprite; } if (aiAvatarSprite == null) aiAvatarSprite = fallbackAvatarSprite; if (playerAvatarSprite == null) playerAvatarSprite = fallbackAvatarSprite; } } private void ApplyRootLayout() { VerticalLayoutGroup layoutGroup = GetComponent(); if (layoutGroup == null) { return; } int horizontalPadding = Mathf.RoundToInt(Mathf.Max(0f, rowHorizontalPadding)); layoutGroup.childControlWidth = true; layoutGroup.childForceExpandWidth = true; layoutGroup.childControlHeight = true; layoutGroup.childForceExpandHeight = false; layoutGroup.childAlignment = TextAnchor.UpperLeft; layoutGroup.padding.left = horizontalPadding; layoutGroup.padding.right = horizontalPadding; } private void ApplyRowLayout(bool isPlayer) { if (rowLayout == null) { return; } LayoutElement rowElement = rowLayout.GetComponent(); if (rowElement == null) { rowElement = rowLayout.gameObject.AddComponent(); } rowElement.flexibleWidth = 1f; if (rowMinHeight > 0f) { rowElement.minHeight = rowMinHeight; } bool avatarVisible = avatarObject != null && avatarObject.activeSelf; rowLayout.childAlignment = isPlayer ? TextAnchor.LowerRight : TextAnchor.LowerLeft; rowLayout.reverseArrangement = isPlayer && avatarVisible; rowLayout.spacing = avatarVisible ? rowSpacingWhenAvatarVisible : rowSpacingWhenAvatarHidden; rowLayout.childControlWidth = false; rowLayout.childControlHeight = false; rowLayout.childForceExpandWidth = false; rowLayout.childForceExpandHeight = false; } private void ApplyAvatarStyle(bool isPlayer) { bool shouldShowAvatar = isPlayer ? showPlayerAvatar : showAiAvatar; if (avatarObject != null) { avatarObject.SetActive(shouldShowAvatar); } if (!shouldShowAvatar || avatarImage == null) { return; } avatarImage.sprite = ResolveAvatarSprite(isPlayer); avatarImage.color = isPlayer ? playerAvatarColor : aiAvatarColor; avatarImage.preserveAspect = true; } private void ApplyBubbleStyle(bool isPlayer) { if (bubbleBackground == null) { return; } bubbleBackground.sprite = ResolveBubbleSprite(isPlayer); bubbleBackground.color = isPlayer ? playerColor : aiColor; } private void ApplyTextStyle(bool isPlayer) { if (textComponent == null) { return; } textComponent.color = isPlayer ? playerTextColor : aiTextColor; } private void ApplyWidthLimit() { if (textComponent == null) { return; } float effectiveMaxWidth = maxBubbleWidth; if (clampToViewportWidth) { float viewportWidth = GetViewportWidth(); if (viewportWidth > 0f) { float viewportMaxWidth = viewportWidth * viewportWidthRatio; if (viewportMaxWidth > 0f) { effectiveMaxWidth = effectiveMaxWidth <= 0f ? viewportMaxWidth : Mathf.Min(effectiveMaxWidth, viewportMaxWidth); } } } LayoutElement textLayout = textComponent.GetComponent(); if (textLayout == null) { textLayout = textComponent.gameObject.AddComponent(); } Vector2 preferredSize = textComponent.GetPreferredValues(textComponent.text, float.PositiveInfinity, float.PositiveInfinity); if (effectiveMaxWidth > 0f && preferredSize.x > effectiveMaxWidth) { textLayout.enabled = true; textLayout.preferredWidth = effectiveMaxWidth; } else { textLayout.enabled = false; } } private Sprite ResolveBubbleSprite(bool isPlayer) { if (isPlayer) { if (playerBubbleSprite != null) return playerBubbleSprite; if (aiBubbleSprite != null) return aiBubbleSprite; } else { if (aiBubbleSprite != null) return aiBubbleSprite; if (playerBubbleSprite != null) return playerBubbleSprite; } return fallbackBubbleSprite; } private Sprite ResolveAvatarSprite(bool isPlayer) { if (isPlayer) { if (playerAvatarSprite != null) return playerAvatarSprite; if (aiAvatarSprite != null) return aiAvatarSprite; } else { if (aiAvatarSprite != null) return aiAvatarSprite; if (playerAvatarSprite != null) return playerAvatarSprite; } return fallbackAvatarSprite; } private void RebuildLayout() { if (rowLayout != null) { LayoutRebuilder.ForceRebuildLayoutImmediate(rowLayout.GetComponent()); } LayoutRebuilder.ForceRebuildLayoutImmediate(GetComponent()); RectTransform parentRt = transform.parent as RectTransform; if (parentRt != null) { LayoutRebuilder.ForceRebuildLayoutImmediate(parentRt); } } private float GetViewportWidth() { ScrollRect scrollRect = GetComponentInParent(); if (scrollRect != null) { RectTransform viewport = scrollRect.viewport != null ? scrollRect.viewport : scrollRect.GetComponent(); if (viewport != null) return viewport.rect.width; } RectTransform parentRt = transform.parent as RectTransform; if (parentRt != null) return parentRt.rect.width; return 0f; } } }