340 lines
11 KiB
C#
340 lines
11 KiB
C#
|
|
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<TextMeshProUGUI>(true);
|
||
|
|
if (tmp != null)
|
||
|
|
{
|
||
|
|
bubbleBackground = tmp.GetComponentInParent<Image>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (bubbleBackground != null && textComponent == null)
|
||
|
|
{
|
||
|
|
textComponent = bubbleBackground.GetComponentInChildren<TextMeshProUGUI>(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (rowLayout == null && bubbleBackground != null && bubbleBackground.transform.parent != null)
|
||
|
|
{
|
||
|
|
rowLayout = bubbleBackground.transform.parent.GetComponent<HorizontalLayoutGroup>();
|
||
|
|
}
|
||
|
|
|
||
|
|
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<Image>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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<VerticalLayoutGroup>();
|
||
|
|
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<LayoutElement>();
|
||
|
|
if (rowElement == null)
|
||
|
|
{
|
||
|
|
rowElement = rowLayout.gameObject.AddComponent<LayoutElement>();
|
||
|
|
}
|
||
|
|
|
||
|
|
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<LayoutElement>();
|
||
|
|
if (textLayout == null)
|
||
|
|
{
|
||
|
|
textLayout = textComponent.gameObject.AddComponent<LayoutElement>();
|
||
|
|
}
|
||
|
|
|
||
|
|
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<RectTransform>());
|
||
|
|
}
|
||
|
|
|
||
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(GetComponent<RectTransform>());
|
||
|
|
|
||
|
|
RectTransform parentRt = transform.parent as RectTransform;
|
||
|
|
if (parentRt != null)
|
||
|
|
{
|
||
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(parentRt);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private float GetViewportWidth()
|
||
|
|
{
|
||
|
|
ScrollRect scrollRect = GetComponentInParent<ScrollRect>();
|
||
|
|
if (scrollRect != null)
|
||
|
|
{
|
||
|
|
RectTransform viewport = scrollRect.viewport != null
|
||
|
|
? scrollRect.viewport
|
||
|
|
: scrollRect.GetComponent<RectTransform>();
|
||
|
|
if (viewport != null) return viewport.rect.width;
|
||
|
|
}
|
||
|
|
|
||
|
|
RectTransform parentRt = transform.parent as RectTransform;
|
||
|
|
if (parentRt != null) return parentRt.rect.width;
|
||
|
|
|
||
|
|
return 0f;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|