채팅 추가
This commit is contained in:
529
Assets/02_Scripts/Chat/ChatSettingsUI.cs
Normal file
529
Assets/02_Scripts/Chat/ChatSettingsUI.cs
Normal file
@@ -0,0 +1,529 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// 설정 화면. 채팅 패널 위를 덮는 겹판으로 만든다.
|
||||
///
|
||||
/// 별도 창이 아니라 겹판인 이유:
|
||||
/// - 클릭 통과 히트테스트에 등록된 사각형이 채팅 패널 하나뿐이라, 같은 사각형
|
||||
/// 안에 있으면 히트테스트를 손댈 필요가 없다.
|
||||
/// - 캐릭터를 따라다니는 배치 계산도 그대로 재사용된다.
|
||||
///
|
||||
/// MonoBehaviour 가 아닌 이유: 씬 수명주기가 필요 없고, 채팅창이 자기 자식으로
|
||||
/// 만들어 들고 있으면 충분하다. 네트워크 요청이 필요한 "연결 테스트"는 직접 하지 않고
|
||||
/// TestRequested 이벤트로 넘긴다 — UI 는 백엔드를 몰라야 한다.
|
||||
/// </summary>
|
||||
public class ChatSettingsUI
|
||||
{
|
||||
const float HeaderHeight = 34f;
|
||||
const float FooterHeight = 48f;
|
||||
const float RowHeight = 34f;
|
||||
const float SegmentHeight = 30f;
|
||||
|
||||
static readonly (string Label, string Id)[] Providers =
|
||||
{
|
||||
("Claude", ChatConfig.ProviderAnthropic),
|
||||
("Gemini", ChatConfig.ProviderGemini),
|
||||
};
|
||||
|
||||
readonly RectTransform root;
|
||||
readonly Segmented providerSegments;
|
||||
readonly InputField keyField;
|
||||
readonly Text keyPlaceholder;
|
||||
readonly Text keyStatus;
|
||||
readonly Text testResult;
|
||||
readonly Button testButton;
|
||||
readonly Button revealButton;
|
||||
readonly GameObject anthropicModelRow;
|
||||
readonly GameObject geminiModelRow;
|
||||
readonly Segmented anthropicModelSegments;
|
||||
readonly Segmented geminiModelSegments;
|
||||
readonly GameObject effortGroup;
|
||||
readonly Segmented effortSegments;
|
||||
readonly InputField personaField;
|
||||
|
||||
ChatConfig working;
|
||||
|
||||
/// <summary>지금 입력칸이 어느 제공자의 키를 들고 있는지. 전환할 때 되돌려 넣어야 한다.</summary>
|
||||
string editingProvider = ChatConfig.ProviderAnthropic;
|
||||
|
||||
/// <summary>저장 버튼. 인자는 UI 값이 반영된 설정 객체.</summary>
|
||||
public event Action<ChatConfig> SaveRequested;
|
||||
|
||||
/// <summary>설정을 닫고 대화로 돌아간다.</summary>
|
||||
public event Action CloseRequested;
|
||||
|
||||
/// <summary>연결 테스트. 두 번째 인자로 결과를 돌려받는다 (성공 여부, 사람이 읽을 문장).</summary>
|
||||
public event Action<ChatConfig, Action<bool, string>> TestRequested;
|
||||
|
||||
public bool IsOpen => root != null && root.gameObject.activeSelf;
|
||||
|
||||
public ChatSettingsUI(RectTransform parent)
|
||||
{
|
||||
root = ChatUiBuilder.NewRect("Settings", parent);
|
||||
ChatUiBuilder.Stretch(root);
|
||||
|
||||
// 아래 대화 내용이 비쳐 보이면 읽기 어렵다. 패널 배경을 불투명하게 덮는다.
|
||||
var background = root.gameObject.AddComponent<Image>();
|
||||
background.sprite = ChatUiTheme.RoundedPanel;
|
||||
background.type = Image.Type.Sliced;
|
||||
background.color = new Color(
|
||||
ChatUiTheme.PanelBackground.r, ChatUiTheme.PanelBackground.g,
|
||||
ChatUiTheme.PanelBackground.b, 1f);
|
||||
|
||||
BuildHeader();
|
||||
RectTransform content = BuildScrollArea();
|
||||
|
||||
// --- 제공자 ---
|
||||
AddSection(content, "AI 제공자");
|
||||
var providerRow = ChatUiBuilder.NewRect("ProviderRow", content);
|
||||
ChatUiBuilder.SetHeight(providerRow, SegmentHeight);
|
||||
providerSegments = new Segmented(providerRow, Providers, OnProviderChanged);
|
||||
|
||||
// --- API 키 ---
|
||||
AddSection(content, "API 키");
|
||||
|
||||
var keyRow = ChatUiBuilder.NewRect("KeyRow", content);
|
||||
ChatUiBuilder.SetHeight(keyRow, RowHeight);
|
||||
|
||||
keyField = ChatUiBuilder.NewInputField("Key", keyRow, "sk-ant-...", 13);
|
||||
keyField.contentType = InputField.ContentType.Password;
|
||||
keyPlaceholder = keyField.placeholder as Text;
|
||||
var keyRect = keyField.GetComponent<RectTransform>();
|
||||
keyRect.anchorMin = Vector2.zero;
|
||||
keyRect.anchorMax = Vector2.one;
|
||||
keyRect.offsetMin = Vector2.zero;
|
||||
keyRect.offsetMax = new Vector2(-52f, 0f);
|
||||
keyField.onValueChanged.AddListener(_ => RefreshKeyStatus());
|
||||
|
||||
revealButton = ChatUiBuilder.NewButton("Reveal", keyRow, "보기", 12,
|
||||
ChatUiTheme.InputBackground, ChatUiTheme.DimText);
|
||||
var revealRect = revealButton.GetComponent<RectTransform>();
|
||||
revealRect.anchorMin = new Vector2(1f, 0f);
|
||||
revealRect.anchorMax = Vector2.one;
|
||||
revealRect.pivot = new Vector2(1f, 0.5f);
|
||||
revealRect.sizeDelta = new Vector2(46f, 0f);
|
||||
revealRect.anchoredPosition = Vector2.zero;
|
||||
revealButton.onClick.AddListener(ToggleReveal);
|
||||
|
||||
keyStatus = ChatUiBuilder.NewText("KeyStatus", content, string.Empty, 11, ChatUiTheme.DimText);
|
||||
keyStatus.alignment = TextAnchor.UpperLeft;
|
||||
keyStatus.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
keyStatus.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
ChatUiBuilder.SetHeight(keyStatus.transform, 48f);
|
||||
|
||||
var testRow = ChatUiBuilder.NewRect("TestRow", content);
|
||||
ChatUiBuilder.SetHeight(testRow, SegmentHeight);
|
||||
|
||||
testButton = ChatUiBuilder.NewButton("Test", testRow, "연결 테스트", 12,
|
||||
ChatUiTheme.InputBackground, ChatUiTheme.PrimaryText);
|
||||
var testRect = testButton.GetComponent<RectTransform>();
|
||||
testRect.anchorMin = Vector2.zero;
|
||||
testRect.anchorMax = new Vector2(0f, 1f);
|
||||
testRect.pivot = new Vector2(0f, 0.5f);
|
||||
testRect.sizeDelta = new Vector2(88f, 0f);
|
||||
testRect.anchoredPosition = Vector2.zero;
|
||||
testButton.onClick.AddListener(RunTest);
|
||||
|
||||
testResult = ChatUiBuilder.NewText("TestResult", testRow, string.Empty, 11, ChatUiTheme.DimText);
|
||||
testResult.alignment = TextAnchor.MiddleLeft;
|
||||
testResult.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
var resultRect = testResult.rectTransform;
|
||||
resultRect.anchorMin = Vector2.zero;
|
||||
resultRect.anchorMax = Vector2.one;
|
||||
resultRect.offsetMin = new Vector2(96f, 0f);
|
||||
resultRect.offsetMax = Vector2.zero;
|
||||
|
||||
// --- 모델 ---
|
||||
// 제공자마다 목록이 다르므로 두 벌을 만들어 두고 보이는 쪽만 바꾼다.
|
||||
// 하나를 만들어 놓고 항목을 갈아끼우면 버튼을 부수고 다시 짓게 된다.
|
||||
AddSection(content, "모델");
|
||||
|
||||
var anthropicRow = ChatUiBuilder.NewRect("AnthropicModels", content);
|
||||
ChatUiBuilder.SetHeight(anthropicRow, SegmentHeight);
|
||||
anthropicModelRow = anthropicRow.gameObject;
|
||||
anthropicModelSegments = new Segmented(anthropicRow, AnthropicChatBackend.SelectableModels);
|
||||
|
||||
var geminiRow = ChatUiBuilder.NewRect("GeminiModels", content);
|
||||
ChatUiBuilder.SetHeight(geminiRow, SegmentHeight);
|
||||
geminiModelRow = geminiRow.gameObject;
|
||||
geminiModelSegments = new Segmented(geminiRow, GeminiChatBackend.SelectableModels);
|
||||
|
||||
// --- 응답 깊이 (Claude 전용) ---
|
||||
// Gemini 에는 대응하는 설정을 넣지 않았다. 모델 세대마다 thinkingConfig 모양이
|
||||
// 달라서 잘못 보내면 400 이 난다. 그래서 항목 자체를 숨긴다.
|
||||
var effortContainer = ChatUiBuilder.NewRect("EffortGroup", content);
|
||||
ChatUiBuilder.MakeVerticalList(effortContainer, new RectOffset(0, 0, 0, 0), 5f);
|
||||
effortGroup = effortContainer.gameObject;
|
||||
|
||||
AddSection(effortContainer, "응답 깊이");
|
||||
var effortRow = ChatUiBuilder.NewRect("EffortRow", effortContainer);
|
||||
ChatUiBuilder.SetHeight(effortRow, SegmentHeight);
|
||||
effortSegments = new Segmented(effortRow, AnthropicChatBackend.SelectableEfforts);
|
||||
|
||||
// --- 성격 ---
|
||||
AddSection(content, "성격 (캐릭터가 어떻게 말할지)");
|
||||
personaField = ChatUiBuilder.NewInputField("Persona", content, "예: 친근한 반말로 짧게 대답해줘", 12, true);
|
||||
personaField.characterLimit = 4000;
|
||||
ChatUiBuilder.SetHeight(personaField.transform, 130f);
|
||||
|
||||
BuildFooter();
|
||||
|
||||
root.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 열고 닫기
|
||||
|
||||
/// <summary>현재 설정을 화면에 채우고 연다. 취소하면 이 객체는 건드려지지 않는다.</summary>
|
||||
public void Open(ChatConfig config)
|
||||
{
|
||||
working = config ?? ChatConfig.Load();
|
||||
|
||||
// 순서가 중요하다. Select 는 값이 바뀌면 OnProviderChanged 를 부르고, 그 안의
|
||||
// StashKey 가 "입력칸에 지금 들어있는 글자"를 editingProvider 자리에 써넣는다.
|
||||
// 입력칸을 먼저 채워두지 않으면 지난번에 열었을 때의 글자가 엉뚱한 제공자의
|
||||
// 키를 덮어쓴다. 먼저 채워두면 콜백이 돌아도 같은 값을 같은 자리에 다시 넣을 뿐이다.
|
||||
editingProvider = working.provider;
|
||||
keyField.text = KeyFor(editingProvider);
|
||||
SetRevealed(false);
|
||||
|
||||
providerSegments.Select(working.provider);
|
||||
|
||||
anthropicModelSegments.Select(working.model);
|
||||
geminiModelSegments.Select(working.geminiModel);
|
||||
effortSegments.Select(working.effort);
|
||||
personaField.text = working.persona ?? string.Empty;
|
||||
|
||||
testResult.text = string.Empty;
|
||||
ApplyProviderToScreen(editingProvider);
|
||||
|
||||
root.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (root != null) root.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void Apply()
|
||||
{
|
||||
if (working == null) return;
|
||||
|
||||
StashKey();
|
||||
|
||||
working.provider = providerSegments.Value;
|
||||
working.model = anthropicModelSegments.Value;
|
||||
working.geminiModel = geminiModelSegments.Value;
|
||||
working.effort = effortSegments.Value;
|
||||
working.persona = personaField.text;
|
||||
|
||||
SaveRequested?.Invoke(working);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 제공자 전환
|
||||
|
||||
void OnProviderChanged(string providerId)
|
||||
{
|
||||
// 입력칸에 들고 있던 키를 원래 제공자 자리에 되돌려 놓고 새 제공자 것을 꺼낸다.
|
||||
// 이걸 안 하면 제공자를 왔다갔다 할 때 방금 붙여넣은 키가 사라진다.
|
||||
StashKey();
|
||||
editingProvider = providerId;
|
||||
keyField.text = KeyFor(providerId);
|
||||
SetRevealed(false);
|
||||
|
||||
testResult.text = string.Empty;
|
||||
ApplyProviderToScreen(providerId);
|
||||
}
|
||||
|
||||
/// <summary>지금 입력칸의 값을 editingProvider 자리에 저장한다.</summary>
|
||||
void StashKey()
|
||||
{
|
||||
if (working == null) return;
|
||||
|
||||
string typed = keyField.text.Trim();
|
||||
if (editingProvider == ChatConfig.ProviderGemini) working.geminiApiKey = typed;
|
||||
else working.apiKey = typed;
|
||||
}
|
||||
|
||||
string KeyFor(string providerId)
|
||||
{
|
||||
if (working == null) return string.Empty;
|
||||
return (providerId == ChatConfig.ProviderGemini ? working.geminiApiKey : working.apiKey)
|
||||
?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>제공자에 맞게 보이는 항목과 안내 문구를 바꾼다.</summary>
|
||||
void ApplyProviderToScreen(string providerId)
|
||||
{
|
||||
bool gemini = providerId == ChatConfig.ProviderGemini;
|
||||
|
||||
anthropicModelRow.SetActive(!gemini);
|
||||
geminiModelRow.SetActive(gemini);
|
||||
effortGroup.SetActive(!gemini);
|
||||
|
||||
if (keyPlaceholder != null) keyPlaceholder.text = gemini ? "AIza..." : "sk-ant-...";
|
||||
|
||||
RefreshKeyStatus();
|
||||
}
|
||||
|
||||
void RefreshKeyStatus()
|
||||
{
|
||||
bool gemini = editingProvider == ChatConfig.ProviderGemini;
|
||||
bool hasTyped = !string.IsNullOrWhiteSpace(keyField.text);
|
||||
bool hasEnv = ChatConfig.EnvironmentKeyFor(editingProvider) != null;
|
||||
|
||||
string where = gemini ? "aistudio.google.com" : "console.anthropic.com";
|
||||
string envName = gemini ? "GEMINI_API_KEY" : "ANTHROPIC_API_KEY";
|
||||
|
||||
if (hasTyped)
|
||||
{
|
||||
keyStatus.text = "저장하면 이 키를 씁니다.";
|
||||
keyStatus.color = ChatUiTheme.DimText;
|
||||
}
|
||||
else if (hasEnv)
|
||||
{
|
||||
keyStatus.text = $"지금은 환경 변수 {envName} 를 쓰고 있어요.\n여기에 입력하면 그쪽이 우선합니다.";
|
||||
keyStatus.color = ChatUiTheme.DimText;
|
||||
}
|
||||
else
|
||||
{
|
||||
keyStatus.text = $"키가 없어요. {where} 에서 발급받아 붙여넣어 주세요.";
|
||||
keyStatus.color = new Color(0.93f, 0.72f, 0.42f, 1f);
|
||||
}
|
||||
|
||||
// 무료 티어의 대가는 결정하는 자리에서 알려야 의미가 있다.
|
||||
if (gemini)
|
||||
{
|
||||
keyStatus.text += "\n※ 무료 등급은 주고받은 대화가 Google 제품 개선에 쓰입니다.";
|
||||
}
|
||||
}
|
||||
|
||||
void ToggleReveal()
|
||||
{
|
||||
SetRevealed(keyField.contentType == InputField.ContentType.Password);
|
||||
}
|
||||
|
||||
void SetRevealed(bool revealed)
|
||||
{
|
||||
keyField.contentType = revealed
|
||||
? InputField.ContentType.Standard
|
||||
: InputField.ContentType.Password;
|
||||
|
||||
// contentType 을 바꾸면 표시 문자열을 다시 그려야 별표/원문이 실제로 바뀐다.
|
||||
keyField.ForceLabelUpdate();
|
||||
|
||||
var label = revealButton.GetComponentInChildren<Text>();
|
||||
if (label != null) label.text = revealed ? "숨김" : "보기";
|
||||
}
|
||||
|
||||
void RunTest()
|
||||
{
|
||||
if (TestRequested == null || working == null) return;
|
||||
|
||||
// 아직 저장하지 않은 화면의 값으로 시험해야 사용자가 기대한 결과가 나온다.
|
||||
string typed = keyField.text.Trim();
|
||||
bool gemini = providerSegments.Value == ChatConfig.ProviderGemini;
|
||||
|
||||
var candidate = new ChatConfig { provider = providerSegments.Value };
|
||||
if (gemini)
|
||||
{
|
||||
candidate.geminiApiKey = typed;
|
||||
candidate.geminiModel = geminiModelSegments.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
candidate.apiKey = typed;
|
||||
candidate.model = anthropicModelSegments.Value;
|
||||
}
|
||||
|
||||
testButton.interactable = false;
|
||||
testResult.text = "확인 중…";
|
||||
testResult.color = ChatUiTheme.DimText;
|
||||
|
||||
TestRequested.Invoke(candidate, (ok, message) =>
|
||||
{
|
||||
testButton.interactable = true;
|
||||
testResult.text = message;
|
||||
testResult.color = ok
|
||||
? new Color(0.55f, 0.85f, 0.6f, 1f)
|
||||
: new Color(0.95f, 0.6f, 0.6f, 1f);
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 계층 만들기
|
||||
|
||||
void BuildHeader()
|
||||
{
|
||||
var header = ChatUiBuilder.NewRect("Header", root);
|
||||
header.anchorMin = new Vector2(0f, 1f);
|
||||
header.anchorMax = Vector2.one;
|
||||
header.pivot = new Vector2(0.5f, 1f);
|
||||
header.sizeDelta = new Vector2(0f, HeaderHeight);
|
||||
header.anchoredPosition = Vector2.zero;
|
||||
|
||||
// 기호 대신 한글을 쓴다. OS 한글 글꼴에 없는 기호는 빈칸으로 나온다.
|
||||
var back = ChatUiBuilder.NewButton("Back", header, "뒤로", 12, Color.clear, ChatUiTheme.DimText);
|
||||
var backRect = back.GetComponent<RectTransform>();
|
||||
backRect.anchorMin = new Vector2(0f, 0.5f);
|
||||
backRect.anchorMax = new Vector2(0f, 0.5f);
|
||||
backRect.pivot = new Vector2(0f, 0.5f);
|
||||
backRect.sizeDelta = new Vector2(40f, HeaderHeight);
|
||||
backRect.anchoredPosition = new Vector2(6f, 0f);
|
||||
back.onClick.AddListener(() => CloseRequested?.Invoke());
|
||||
|
||||
var title = ChatUiBuilder.NewText("Title", header, "설정", 13, ChatUiTheme.DimText);
|
||||
title.alignment = TextAnchor.MiddleCenter;
|
||||
ChatUiBuilder.Stretch(title.rectTransform, 52f, 0f);
|
||||
|
||||
var divider = ChatUiBuilder.NewImage("Divider", header, null, new Color(1f, 1f, 1f, 0.08f));
|
||||
var dividerRect = divider.rectTransform;
|
||||
dividerRect.anchorMin = Vector2.zero;
|
||||
dividerRect.anchorMax = new Vector2(1f, 0f);
|
||||
dividerRect.pivot = new Vector2(0.5f, 0f);
|
||||
dividerRect.sizeDelta = new Vector2(-16f, 1f);
|
||||
dividerRect.anchoredPosition = Vector2.zero;
|
||||
}
|
||||
|
||||
RectTransform BuildScrollArea()
|
||||
{
|
||||
var scrollRect = ChatUiBuilder.NewRect("Scroll", root);
|
||||
scrollRect.anchorMin = Vector2.zero;
|
||||
scrollRect.anchorMax = Vector2.one;
|
||||
scrollRect.offsetMin = new Vector2(10f, FooterHeight);
|
||||
scrollRect.offsetMax = new Vector2(-10f, -HeaderHeight);
|
||||
|
||||
var scroll = scrollRect.gameObject.AddComponent<ScrollRect>();
|
||||
scroll.horizontal = false;
|
||||
scroll.vertical = true;
|
||||
scroll.movementType = ScrollRect.MovementType.Clamped;
|
||||
scroll.scrollSensitivity = 26f;
|
||||
|
||||
var viewport = ChatUiBuilder.NewRect("Viewport", scrollRect);
|
||||
ChatUiBuilder.Stretch(viewport);
|
||||
viewport.gameObject.AddComponent<RectMask2D>();
|
||||
|
||||
var content = ChatUiBuilder.NewRect("Content", viewport);
|
||||
content.anchorMin = new Vector2(0f, 1f);
|
||||
content.anchorMax = Vector2.one;
|
||||
content.pivot = new Vector2(0.5f, 1f);
|
||||
content.sizeDelta = Vector2.zero;
|
||||
|
||||
ChatUiBuilder.MakeVerticalList(content, new RectOffset(0, 6, 6, 12), 5f);
|
||||
var fitter = content.gameObject.AddComponent<ContentSizeFitter>();
|
||||
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
scroll.viewport = viewport;
|
||||
scroll.content = content;
|
||||
return content;
|
||||
}
|
||||
|
||||
void BuildFooter()
|
||||
{
|
||||
var footer = ChatUiBuilder.NewRect("Footer", root);
|
||||
footer.anchorMin = Vector2.zero;
|
||||
footer.anchorMax = new Vector2(1f, 0f);
|
||||
footer.pivot = new Vector2(0.5f, 0f);
|
||||
footer.sizeDelta = new Vector2(0f, FooterHeight);
|
||||
footer.anchoredPosition = Vector2.zero;
|
||||
|
||||
var cancel = ChatUiBuilder.NewButton("Cancel", footer, "취소", 13,
|
||||
ChatUiTheme.InputBackground, ChatUiTheme.DimText);
|
||||
var cancelRect = cancel.GetComponent<RectTransform>();
|
||||
cancelRect.anchorMin = new Vector2(0f, 0.5f);
|
||||
cancelRect.anchorMax = new Vector2(0f, 0.5f);
|
||||
cancelRect.pivot = new Vector2(0f, 0.5f);
|
||||
cancelRect.sizeDelta = new Vector2(70f, 30f);
|
||||
cancelRect.anchoredPosition = new Vector2(10f, 0f);
|
||||
cancel.onClick.AddListener(() => CloseRequested?.Invoke());
|
||||
|
||||
var save = ChatUiBuilder.NewButton("Save", footer, "저장", 13,
|
||||
ChatUiTheme.AccentButton, ChatUiTheme.PrimaryText);
|
||||
var saveRect = save.GetComponent<RectTransform>();
|
||||
saveRect.anchorMin = new Vector2(1f, 0.5f);
|
||||
saveRect.anchorMax = new Vector2(1f, 0.5f);
|
||||
saveRect.pivot = new Vector2(1f, 0.5f);
|
||||
saveRect.sizeDelta = new Vector2(80f, 30f);
|
||||
saveRect.anchoredPosition = new Vector2(-10f, 0f);
|
||||
save.onClick.AddListener(Apply);
|
||||
}
|
||||
|
||||
static void AddSection(RectTransform content, string label)
|
||||
{
|
||||
var text = ChatUiBuilder.NewSectionLabel(label, content, label);
|
||||
ChatUiBuilder.SetHeight(text.transform, 20f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 몇 개 안 되는 선택지를 가로로 나눠 붙인 버튼 묶음.
|
||||
///
|
||||
/// uGUI Dropdown 을 코드로 만들려면 템플릿 계층(뷰포트/스크롤/아이템/체크마크)을
|
||||
/// 통째로 손으로 세워야 한다. 선택지가 서너 개뿐이라 이쪽이 훨씬 간단하고,
|
||||
/// 목록을 펼치지 않아도 현재 값이 보인다.
|
||||
/// </summary>
|
||||
class Segmented
|
||||
{
|
||||
readonly (string Label, string Id)[] options;
|
||||
readonly Image[] backgrounds;
|
||||
readonly Action<string> onChanged;
|
||||
|
||||
public string Value { get; private set; }
|
||||
|
||||
public Segmented(RectTransform row, (string Label, string Id)[] options,
|
||||
Action<string> onChanged = null)
|
||||
{
|
||||
this.options = options;
|
||||
this.onChanged = onChanged;
|
||||
backgrounds = new Image[options.Length];
|
||||
|
||||
for (int i = 0; i < options.Length; i++)
|
||||
{
|
||||
int index = i;
|
||||
var button = ChatUiBuilder.NewButton(options[i].Id, row, options[i].Label, 12,
|
||||
ChatUiTheme.InputBackground, ChatUiTheme.PrimaryText);
|
||||
|
||||
var rect = button.GetComponent<RectTransform>();
|
||||
rect.anchorMin = new Vector2(i / (float)options.Length, 0f);
|
||||
rect.anchorMax = new Vector2((i + 1) / (float)options.Length, 1f);
|
||||
rect.offsetMin = new Vector2(i == 0 ? 0f : 2f, 0f);
|
||||
rect.offsetMax = new Vector2(i == options.Length - 1 ? 0f : -2f, 0f);
|
||||
|
||||
backgrounds[i] = button.targetGraphic as Image;
|
||||
button.onClick.AddListener(() => Select(this.options[index].Id));
|
||||
}
|
||||
|
||||
// 생성 시점에는 알릴 상대가 아직 없으므로 콜백 없이 첫 항목만 칠한다.
|
||||
Paint(0);
|
||||
Value = options[0].Id;
|
||||
}
|
||||
|
||||
public void Select(string id)
|
||||
{
|
||||
int selected = -1;
|
||||
for (int i = 0; i < options.Length; i++)
|
||||
{
|
||||
if (options[i].Id == id) { selected = i; break; }
|
||||
}
|
||||
|
||||
// 설정 파일에 목록에 없는 값이 적혀 있을 수 있다. 그때는 첫 항목으로 되돌린다.
|
||||
if (selected < 0) selected = 0;
|
||||
|
||||
bool changed = Value != options[selected].Id;
|
||||
Value = options[selected].Id;
|
||||
Paint(selected);
|
||||
|
||||
if (changed) onChanged?.Invoke(Value);
|
||||
}
|
||||
|
||||
void Paint(int selected)
|
||||
{
|
||||
for (int i = 0; i < backgrounds.Length; i++)
|
||||
{
|
||||
if (backgrounds[i] == null) continue;
|
||||
backgrounds[i].color = i == selected
|
||||
? ChatUiTheme.AccentButton
|
||||
: ChatUiTheme.InputBackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user