feat: SongSelect UI polish — marquee title, button states, font
- MarqueeText: scrolling title for long song names (RectMask2D clipped) - SongDetailPanel: difficulty selected = green (img.color direct set) - SongSelectManager: ALL/OWNED tab active state via img.color - Card layout: DestroyImmediate + correct rebuild order to fix zero-size title bug - NanumGothic SDF fallback configured on LiberationSans for Korean support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,19 +14,25 @@ public class SongSelectManager : MonoBehaviour
|
||||
[SerializeField] private GameObject loadingOverlay;
|
||||
[SerializeField] private GameObject errorOverlay;
|
||||
[SerializeField] private TMP_Text errorText;
|
||||
[SerializeField] private TMP_FontAsset cardFont;
|
||||
|
||||
private static readonly Color TabActive = Color.white;
|
||||
private static readonly Color TabInactive = new Color(0.6f, 0.6f, 0.6f);
|
||||
|
||||
private static readonly Color TabActive = new Color(1f, 1f, 1f, 0.45f);
|
||||
private static readonly Color TabInactive = new Color(1f, 1f, 1f, 0.12f);
|
||||
|
||||
private static string CachePath =>
|
||||
Path.Combine(Application.persistentDataPath, "songs_cache.json");
|
||||
|
||||
private List<SongInfo> allSongs = new List<SongInfo>();
|
||||
private bool showingOwned = false;
|
||||
private List<SongInfo> allSongs = new List<SongInfo>();
|
||||
private bool showingOwned = false;
|
||||
private TMP_FontAsset _cardFont;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// NanumGothic SDF를 직접 로드 — Resources 경로에 있어야 함
|
||||
_cardFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/NanumGothic SDF");
|
||||
if (_cardFont == null)
|
||||
_cardFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
|
||||
|
||||
tabAllBtn .onClick.AddListener(() => SwitchTab(false));
|
||||
tabOwnedBtn.onClick.AddListener(() => SwitchTab(true));
|
||||
detailPanel.gameObject.SetActive(false);
|
||||
@@ -49,9 +55,11 @@ public class SongSelectManager : MonoBehaviour
|
||||
|
||||
private static void ApplyTabColor(Button btn, Color c)
|
||||
{
|
||||
var colors = btn.colors;
|
||||
colors.normalColor = c;
|
||||
btn.colors = colors;
|
||||
if (btn.targetGraphic is Image img)
|
||||
img.color = c;
|
||||
var colors = btn.colors;
|
||||
colors.normalColor = Color.white;
|
||||
btn.colors = colors;
|
||||
}
|
||||
|
||||
private void FetchSongs()
|
||||
@@ -87,8 +95,9 @@ public class SongSelectManager : MonoBehaviour
|
||||
|
||||
public void RefreshCards()
|
||||
{
|
||||
// DestroyImmediate to avoid deferred-destroy interfering with layout
|
||||
for (int i = cardContainer.childCount - 1; i >= 0; i--)
|
||||
Destroy(cardContainer.GetChild(i).gameObject);
|
||||
DestroyImmediate(cardContainer.GetChild(i).gameObject);
|
||||
|
||||
List<SongInfo> songs = showingOwned
|
||||
? allSongs.FindAll(s => SongLibrary.Instance.IsSongDownloaded(s.id))
|
||||
@@ -96,6 +105,10 @@ public class SongSelectManager : MonoBehaviour
|
||||
|
||||
foreach (SongInfo song in songs)
|
||||
SpawnCard(song);
|
||||
|
||||
// Order matters: layout first → card gets size → then canvas update → anchored children recalculate
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(cardContainer);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
}
|
||||
|
||||
private void SpawnCard(SongInfo song)
|
||||
@@ -121,21 +134,33 @@ public class SongSelectManager : MonoBehaviour
|
||||
bc.fadeDuration = 0.1f;
|
||||
btn.colors = bc;
|
||||
|
||||
// Title
|
||||
// Title — RectMask2D 컨테이너 안에서 마퀴 스크롤
|
||||
var titleMask = new GameObject("TitleMask");
|
||||
titleMask.transform.SetParent(card.transform, false);
|
||||
var tmr = titleMask.AddComponent<RectTransform>();
|
||||
tmr.anchorMin = new Vector2(0f, 0.5f);
|
||||
tmr.anchorMax = new Vector2(1f, 1f);
|
||||
tmr.offsetMin = new Vector2(5f, 0f);
|
||||
tmr.offsetMax = new Vector2(downloaded ? -20f : -3f, 0f);
|
||||
titleMask.AddComponent<RectMask2D>();
|
||||
|
||||
var titleGO = new GameObject("Title");
|
||||
titleGO.transform.SetParent(card.transform, false);
|
||||
titleGO.transform.SetParent(titleMask.transform, false);
|
||||
var tr = titleGO.AddComponent<RectTransform>();
|
||||
tr.anchorMin = new Vector2(0f, 0.5f);
|
||||
tr.anchorMax = new Vector2(1f, 1f);
|
||||
tr.offsetMin = new Vector2(5f, 0f);
|
||||
tr.offsetMax = new Vector2(downloaded ? -18f : -3f, -1f);
|
||||
tr.anchorMin = new Vector2(0f, 0f);
|
||||
tr.anchorMax = new Vector2(0f, 1f);
|
||||
tr.pivot = new Vector2(0f, 0.5f);
|
||||
tr.anchoredPosition = Vector2.zero;
|
||||
tr.sizeDelta = new Vector2(500f, 0f);
|
||||
var tTmp = titleGO.AddComponent<TextMeshProUGUI>();
|
||||
if (cardFont != null) tTmp.font = cardFont;
|
||||
tTmp.text = song.title;
|
||||
tTmp.fontSize = 5f;
|
||||
tTmp.color = Color.white;
|
||||
tTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
tTmp.overflowMode = TextOverflowModes.Ellipsis;
|
||||
if (_cardFont != null) tTmp.font = _cardFont;
|
||||
tTmp.text = song.title;
|
||||
tTmp.fontSize = 5f;
|
||||
tTmp.color = Color.white;
|
||||
tTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
tTmp.overflowMode = TextOverflowModes.Overflow;
|
||||
tTmp.enableWordWrapping = false;
|
||||
titleGO.AddComponent<MarqueeText>();
|
||||
|
||||
// Artist
|
||||
var artistGO = new GameObject("Artist");
|
||||
@@ -146,7 +171,7 @@ public class SongSelectManager : MonoBehaviour
|
||||
ar.offsetMin = new Vector2(5f, 1f);
|
||||
ar.offsetMax = new Vector2(-3f, 0f);
|
||||
var aTmp = artistGO.AddComponent<TextMeshProUGUI>();
|
||||
if (cardFont != null) aTmp.font = cardFont;
|
||||
if (_cardFont != null) aTmp.font = _cardFont;
|
||||
aTmp.text = song.artist;
|
||||
aTmp.fontSize = 4f;
|
||||
aTmp.color = new Color(1f, 1f, 1f, 0.6f);
|
||||
|
||||
Reference in New Issue
Block a user