#nullable enable using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// 코드 샘플을 표시하는 컴포넌트. /// 텍스트 선택 및 복사가 가능하며, 복사 버튼을 제공합니다. /// /// /// CodeBlock(코드 블록)이란? /// /// CodeBlock은 코드 샘플을 읽기 쉽게 표시하는 컴포넌트입니다. /// 고정폭 폰트, 배경색, 복사 버튼 등으로 코드를 강조하여 표시합니다. /// 문서, 튜토리얼, 샘플 앱 등에서 코드 예제를 보여줄 때 사용합니다. /// /// /// 주요 기능: /// /// 고정폭 폰트로 코드 가독성 향상 /// 텍스트 선택 및 복사 가능 /// 복사 버튼으로 원클릭 복사 /// 제목 표시 (예: "C#", "UXML", "JSON") /// /// /// UXML에서 코드 설정 방법: /// /// code 속성 사용: code="var x = 1;\nvar y = 2;" (줄바꿈은 \n) /// 텍스트 콘텐츠 사용: 태그 사이에 직접 코드 작성 (줄바꿈 유지) /// /// /// 실제 활용 예시: /// /// API 문서 - 함수 사용법 예제 /// 튜토리얼 - 단계별 코드 설명 /// StyleGuide 샘플 - C#/UXML 코드 예시 /// 오류 로그 - 스택 트레이스 표시 /// /// /// /// UXML에서 사용 (code 속성): /// /// /// /// UXML에서 사용 (텍스트 콘텐츠): /// /// /// var x = 1; /// var y = 2; /// /// /// C# 코드에서 사용: /// /// var codeBlock = new UTKCodeBlock /// { /// Title = "C#", /// Code = "var player = new Player();\nplayer.Move(10, 0);" /// }; /// /// // 여러 코드 블록 컨테이너 생성 /// var container = UTKCodeBlock.CreateCodeSampleContainer( /// csharpCode: "var btn = new UTKButton();", /// uxmlCode: "" /// ); /// /// [UxmlElement] public partial class UTKCodeBlock : VisualElement { private static class UssClasses { public const string Root = "utk-code-block"; public const string Header = "utk-code-block__header"; public const string Title = "utk-code-block__title"; public const string CopyButton = "utk-code-block__copy-btn"; public const string Content = "utk-code-block__content"; public const string Code = "utk-code-block__code"; } private Label? _titleLabel; private UTKButton? _copyButton; private TextField? _codeField; private string _title = ""; private string _code = ""; private bool _codeSetFromAttribute; /// /// 코드 블록 제목 (예: "C# Code", "UXML") /// [UxmlAttribute("title")] public string Title { get => _title; set { _title = value; if (_titleLabel != null) { _titleLabel.text = value; _titleLabel.style.display = string.IsNullOrEmpty(value) ? DisplayStyle.None : DisplayStyle.Flex; } } } /// /// 표시할 코드 /// UXML에서 줄바꿈은 \n 문자열로 표현하거나, 텍스트 콘텐츠로 직접 입력할 수 있습니다. /// [UxmlAttribute("code")] public string Code { get => _code; set { // UXML에서 "\n" 문자열 리터럴이 들어올 수 있으므로 실제 줄바꿈으로 변환 _code = (value ?? "").Replace("\\n", "\n"); _codeSetFromAttribute = !string.IsNullOrEmpty(_code); if (_codeField != null) { _codeField.value = _code; UpdateCodeFieldHeight(); } } } /// /// 텍스트 콘텐츠로 코드를 설정합니다. /// code 속성이 설정되지 않은 경우에만 적용됩니다. /// public void SetCodeFromContent(string content) { if (_codeSetFromAttribute) return; // 앞뒤 빈 줄 제거하고 공통 들여쓰기 제거 _code = TrimAndNormalizeIndent(content); if (_codeField != null) { _codeField.value = _code; UpdateCodeFieldHeight(); } } /// /// 텍스트의 앞뒤 빈 줄을 제거하고 공통 들여쓰기를 정규화합니다. /// private static string TrimAndNormalizeIndent(string text) { if (string.IsNullOrEmpty(text)) return ""; var lines = text.Split('\n'); // 앞뒤 빈 줄 제거 int startIndex = 0; int endIndex = lines.Length - 1; while (startIndex <= endIndex && string.IsNullOrWhiteSpace(lines[startIndex])) startIndex++; while (endIndex >= startIndex && string.IsNullOrWhiteSpace(lines[endIndex])) endIndex--; if (startIndex > endIndex) return ""; // 공통 들여쓰기 찾기 int minIndent = int.MaxValue; for (int i = startIndex; i <= endIndex; i++) { var line = lines[i]; if (string.IsNullOrWhiteSpace(line)) continue; int indent = 0; foreach (char c in line) { if (c == ' ') indent++; else if (c == '\t') indent += 4; else break; } minIndent = Mathf.Min(minIndent, indent); } if (minIndent == int.MaxValue) minIndent = 0; // 공통 들여쓰기 제거하고 결과 생성 var result = new System.Text.StringBuilder(); for (int i = startIndex; i <= endIndex; i++) { var line = lines[i]; if (string.IsNullOrWhiteSpace(line)) { result.AppendLine(); } else { int removed = 0; int charIndex = 0; while (removed < minIndent && charIndex < line.Length) { if (line[charIndex] == ' ') { removed++; charIndex++; } else if (line[charIndex] == '\t') { removed += 4; charIndex++; } else break; } result.AppendLine(line[charIndex..]); } } return result.ToString().TrimEnd('\r', '\n'); } private void UpdateCodeFieldHeight() { if (_codeField == null) return; // 줄 수에 따라 높이 조정 var lineCount = string.IsNullOrEmpty(_code) ? 1 : _code.Split('\n').Length; var height = Mathf.Max(40, lineCount * 18 + 24); _codeField.style.height = height; } public UTKCodeBlock() { AddToClassList(UssClasses.Root); // Header var header = new VisualElement(); header.AddToClassList(UssClasses.Header); Add(header); _titleLabel = new Label(); _titleLabel.AddToClassList(UssClasses.Title); header.Add(_titleLabel); _copyButton = new UTKButton("Copy", UTKMaterialIcons.ContentCopy, UTKButton.ButtonVariant.Text, 12); _copyButton.AddToClassList(UssClasses.CopyButton); _copyButton.OnClicked += OnCopyClicked; header.Add(_copyButton); // Content var content = new VisualElement(); content.AddToClassList(UssClasses.Content); Add(content); // TextField를 사용하여 텍스트 선택 가능하게 _codeField = new TextField(); _codeField.AddToClassList(UssClasses.Code); _codeField.multiline = true; _codeField.isReadOnly = true; content.Add(_codeField); // 초기값 적용 (UXML에서 속성이 먼저 설정된 경우) if (!string.IsNullOrEmpty(_title)) { _titleLabel.text = _title; } if (!string.IsNullOrEmpty(_code)) { _codeField.value = _code; UpdateCodeFieldHeight(); } // UXML 텍스트 콘텐츠 처리를 위한 콜백 RegisterCallback(OnAttachToPanel); } private void OnAttachToPanel(AttachToPanelEvent evt) { // UXML에서 텍스트 콘텐츠가 있으면 처리 // Unity는 텍스트 콘텐츠를 Label 자식으로 추가하므로 확인 schedule.Execute(() => { if (_codeSetFromAttribute) return; // 자식 Label에서 텍스트 추출 시도 var textLabel = this.Q