UIToolkit 기본 UI 개발 중

This commit is contained in:
logonkhi
2026-01-08 20:15:57 +09:00
parent ef4e86820c
commit 71831dd4c3
319 changed files with 28283 additions and 761 deletions

View File

@@ -0,0 +1,332 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit.Modal;
namespace UVC.UIToolkit
{
/// <summary>
/// Alert 팝업 컴포넌트.
/// 사용자에게 중요한 정보를 알립니다.
/// </summary>
[UxmlElement]
public partial class UTKAlert : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Modal/UTKAlert";
#endregion
#region Fields
private bool _disposed;
private UTKModalBlocker? _blocker;
private VisualElement? _iconContainer;
private Label? _iconLabel;
private Label? _titleLabel;
private Label? _messageLabel;
private VisualElement? _buttonContainer;
private string _title = "";
private string _message = "";
private AlertType _alertType = AlertType.Info;
#endregion
#region Events
/// <summary>닫힘 이벤트</summary>
public event Action? OnClosed;
/// <summary>확인 이벤트</summary>
public event Action? OnConfirm;
/// <summary>취소 이벤트</summary>
public event Action? OnCancel;
#endregion
#region Properties
/// <summary>제목</summary>
[UxmlAttribute]
public string Title
{
get => _title;
set
{
_title = value;
if (_titleLabel != null)
{
_titleLabel.text = value;
_titleLabel.style.display = string.IsNullOrEmpty(value) ? DisplayStyle.None : DisplayStyle.Flex;
}
}
}
/// <summary>메시지</summary>
[UxmlAttribute]
public string Message
{
get => _message;
set
{
_message = value;
if (_messageLabel != null) _messageLabel.text = value;
}
}
/// <summary>알림 유형</summary>
[UxmlAttribute]
public AlertType Type
{
get => _alertType;
set
{
_alertType = value;
UpdateType();
}
}
#endregion
#region Enums
public enum AlertType
{
Info,
Success,
Warning,
Error,
Confirm
}
#endregion
#region Constructor
public UTKAlert()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
CreateUI();
SubscribeToThemeChanges();
}
public UTKAlert(string title, string message, AlertType type = AlertType.Info) : this()
{
Title = title;
Message = message;
Type = type;
}
#endregion
#region Static Factory
/// <summary>
/// Info 알림 표시
/// </summary>
public static UTKAlert ShowInfo(VisualElement parent, string title, string message, Action? onClose = null)
{
return Show(parent, title, message, AlertType.Info, onClose);
}
/// <summary>
/// Success 알림 표시
/// </summary>
public static UTKAlert ShowSuccess(VisualElement parent, string title, string message, Action? onClose = null)
{
return Show(parent, title, message, AlertType.Success, onClose);
}
/// <summary>
/// Warning 알림 표시
/// </summary>
public static UTKAlert ShowWarning(VisualElement parent, string title, string message, Action? onClose = null)
{
return Show(parent, title, message, AlertType.Warning, onClose);
}
/// <summary>
/// Error 알림 표시
/// </summary>
public static UTKAlert ShowError(VisualElement parent, string title, string message, Action? onClose = null)
{
return Show(parent, title, message, AlertType.Error, onClose);
}
/// <summary>
/// Confirm 대화상자 표시
/// </summary>
public static UTKAlert ShowConfirm(VisualElement parent, string title, string message, Action? onConfirm, Action? onCancel = null)
{
var alert = Show(parent, title, message, AlertType.Confirm, null);
alert.OnConfirm = onConfirm;
alert.OnCancel = onCancel;
return alert;
}
/// <summary>
/// 알림 표시
/// </summary>
public static UTKAlert Show(VisualElement parent, string title, string message, AlertType type, Action? onClose)
{
var alert = new UTKAlert(title, message, type);
alert.OnClosed = onClose;
alert._blocker = UTKModalBlocker.Show(parent, 0.5f, true);
alert._blocker.OnBlockerClicked += alert.Close;
alert._blocker.Add(alert);
// 중앙 정렬
alert.style.position = Position.Absolute;
alert.style.left = Length.Percent(50);
alert.style.top = Length.Percent(50);
alert.style.translate = new Translate(Length.Percent(-50), Length.Percent(-50));
return alert;
}
#endregion
#region UI Creation
private void CreateUI()
{
AddToClassList("utk-alert");
_iconContainer = new VisualElement { name = "icon-container" };
_iconContainer.AddToClassList("utk-alert__icon-container");
Add(_iconContainer);
_iconLabel = new Label { name = "icon" };
_iconLabel.AddToClassList("utk-alert__icon");
_iconContainer.Add(_iconLabel);
var contentContainer = new VisualElement { name = "content" };
contentContainer.AddToClassList("utk-alert__content");
Add(contentContainer);
_titleLabel = new Label { name = "title" };
_titleLabel.AddToClassList("utk-alert__title");
contentContainer.Add(_titleLabel);
_messageLabel = new Label { name = "message" };
_messageLabel.AddToClassList("utk-alert__message");
contentContainer.Add(_messageLabel);
_buttonContainer = new VisualElement { name = "buttons" };
_buttonContainer.AddToClassList("utk-alert__buttons");
Add(_buttonContainer);
UpdateType();
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region Methods
private void UpdateType()
{
RemoveFromClassList("utk-alert--info");
RemoveFromClassList("utk-alert--success");
RemoveFromClassList("utk-alert--warning");
RemoveFromClassList("utk-alert--error");
RemoveFromClassList("utk-alert--confirm");
var typeClass = _alertType switch
{
AlertType.Success => "utk-alert--success",
AlertType.Warning => "utk-alert--warning",
AlertType.Error => "utk-alert--error",
AlertType.Confirm => "utk-alert--confirm",
_ => "utk-alert--info"
};
AddToClassList(typeClass);
// 아이콘 업데이트
if (_iconLabel != null)
{
_iconLabel.text = _alertType switch
{
AlertType.Success => "✓",
AlertType.Warning => "⚠",
AlertType.Error => "✕",
AlertType.Confirm => "?",
_ => ""
};
}
// 버튼 업데이트
UpdateButtons();
}
private void UpdateButtons()
{
if (_buttonContainer == null) return;
_buttonContainer.Clear();
if (_alertType == AlertType.Confirm)
{
var cancelBtn = new UTKButton("Cancel", "", UTKButton.ButtonVariant.Normal);
cancelBtn.AddToClassList("utk-alert__btn");
cancelBtn.OnClicked += () =>
{
OnCancel?.Invoke();
Close();
};
_buttonContainer.Add(cancelBtn);
var confirmBtn = new UTKButton("OK", "", UTKButton.ButtonVariant.Primary);
confirmBtn.AddToClassList("utk-alert__btn");
confirmBtn.OnClicked += () =>
{
OnConfirm?.Invoke();
Close();
};
_buttonContainer.Add(confirmBtn);
}
else
{
var okBtn = new UTKButton("OK", "", UTKButton.ButtonVariant.Primary);
okBtn.AddToClassList("utk-alert__btn");
okBtn.OnClicked += Close;
_buttonContainer.Add(okBtn);
}
}
/// <summary>
/// 알림 닫기
/// </summary>
public void Close()
{
OnClosed?.Invoke();
if (_blocker != null)
{
_blocker.OnBlockerClicked -= Close;
_blocker.Hide();
}
_blocker = null;
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
OnClosed = null;
OnConfirm = null;
OnCancel = null;
_blocker = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 969edca0103007147b72d7f0020eea98

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cb6c13fd5d2027542a2c6de9c1c33344

View File

@@ -0,0 +1,572 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Locale;
using UVC.UIToolkit.Input;
namespace UVC.UIToolkit.Modal
{
/// <summary>
/// UIToolkit 기반 날짜/시간 피커 모달
/// 캘린더 그리드 + 시간 선택 지원
/// </summary>
[UxmlElement]
public partial class UTKDatePicker : VisualElement, IDisposable
{
#region Enums
public enum PickerMode
{
DateOnly,
DateAndTime
}
#endregion
#region Constants
private const string UXML_PATH = "UIToolkit/Modal/UTKDatePicker";
private const string USS_PATH = "UIToolkit/Modal/UTKDatePicker";
private const int DAYS_IN_GRID = 42; // 6 rows x 7 columns
private static readonly string[] DAY_NAME_KEYS = { "day_sun", "day_mon", "day_tue", "day_wed", "day_thu", "day_fri", "day_sat" };
#endregion
#region Fields
private bool _disposed;
private PickerMode _mode;
private DateTime _selectedDate = DateTime.Today;
private DateTime _displayMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
private UTKModalBlocker? _blocker;
private readonly List<Button> _dayButtons = new();
// UI 요소
private Label? _titleLabel;
private Button? _closeButton;
private Label? _yearMonthLabel;
private Button? _prevYearButton;
private Button? _prevMonthButton;
private Button? _nextMonthButton;
private Button? _nextYearButton;
private VisualElement? _dayNamesRow;
private VisualElement? _timeRow;
private UTKNumberStepper? _hourStepper;
private UTKNumberStepper? _minuteStepper;
private Button? _cancelButton;
private Button? _confirmButton;
#endregion
#region Events
/// <summary>날짜가 선택되었을 때 발생</summary>
public event Action<DateTime>? OnDateSelected;
/// <summary>피커가 닫힐 때 발생</summary>
public event Action? OnClosed;
#endregion
#region Constructor
public UTKDatePicker()
{
// 테마 스타일시트 적용
UTKThemeManager.Instance.ApplyThemeToElement(this);
// 컴포넌트 스타일시트 로드
var styleSheet = Resources.Load<StyleSheet>(USS_PATH);
if (styleSheet != null)
{
styleSheets.Add(styleSheet);
}
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
if (visualTree != null)
{
visualTree.CloneTree(this);
}
else
{
CreateUI();
}
QueryElements();
SetupEvents();
SubscribeToThemeChanges();
// 캘린더 초기화
UpdateCalendar();
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
// 패널에서 분리될 때 이벤트 구독 해제
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region Static Factory
/// <summary>
/// 날짜 피커를 표시합니다.
/// </summary>
public static UTKDatePicker Show(
VisualElement parent,
DateTime initialDate,
PickerMode mode = PickerMode.DateOnly,
string title = "Select Date")
{
var picker = new UTKDatePicker();
picker._mode = mode;
picker._selectedDate = initialDate;
picker._displayMonth = new DateTime(initialDate.Year, initialDate.Month, 1);
// 블로커 추가
picker._blocker = UTKModalBlocker.Show(parent, 0.5f, false);
picker._blocker.OnBlockerClicked += picker.Close;
// 피커 추가
parent.Add(picker);
// UI 초기화
picker.SetTitle(title);
picker.SetTimeVisible(mode == PickerMode.DateAndTime);
picker.UpdateTimeFields();
picker.UpdateCalendar();
picker.CenterOnScreen();
return picker;
}
#endregion
#region Public Methods
public void SetDate(DateTime date)
{
_selectedDate = date;
_displayMonth = new DateTime(date.Year, date.Month, 1);
UpdateTimeFields();
UpdateCalendar();
}
public DateTime GetDate()
{
if (_mode == PickerMode.DateAndTime)
{
int hour = _hourStepper?.Value ?? 0;
int minute = _minuteStepper?.Value ?? 0;
return new DateTime(
_selectedDate.Year,
_selectedDate.Month,
_selectedDate.Day,
hour,
minute,
0
);
}
return _selectedDate.Date;
}
public void PreviousYear() => NavigateMonth(-12);
public void NextYear() => NavigateMonth(12);
public void PreviousMonth() => NavigateMonth(-1);
public void NextMonth() => NavigateMonth(1);
public void Close()
{
OnClosed?.Invoke();
_blocker?.Hide();
RemoveFromHierarchy();
Dispose();
}
#endregion
#region Private Methods - UI Creation
private void CreateUI()
{
AddToClassList("utk-date-picker");
// Header
var header = new VisualElement { name = "header" };
header.AddToClassList("utk-date-picker__header");
_titleLabel = new Label("Select Date") { name = "title" };
_titleLabel.AddToClassList("utk-date-picker__title");
_closeButton = new Button { name = "close-btn", text = "\u2715" }; // X
_closeButton.AddToClassList("utk-date-picker__close-btn");
header.Add(_titleLabel);
header.Add(_closeButton);
Add(header);
// Navigation
var navRow = new VisualElement { name = "nav-row" };
navRow.AddToClassList("utk-date-picker__nav-row");
var leftNav = new VisualElement();
leftNav.AddToClassList("utk-date-picker__nav-group");
_prevYearButton = CreateNavButton("prev-year", "\u00AB"); // <<
_prevYearButton.AddToClassList("utk-date-picker__nav-btn--prev-year");
_prevMonthButton = CreateNavButton("prev-month", "\u2039"); // <
leftNav.Add(_prevYearButton);
leftNav.Add(_prevMonthButton);
_yearMonthLabel = new Label { name = "year-month" };
_yearMonthLabel.AddToClassList("utk-date-picker__year-month-label");
var rightNav = new VisualElement();
rightNav.AddToClassList("utk-date-picker__nav-group");
_nextMonthButton = CreateNavButton("next-month", "\u203A"); // >
_nextYearButton = CreateNavButton("next-year", "\u00BB"); // >>
_nextYearButton.AddToClassList("utk-date-picker__nav-btn--next-year");
rightNav.Add(_nextMonthButton);
rightNav.Add(_nextYearButton);
navRow.Add(leftNav);
navRow.Add(_yearMonthLabel);
navRow.Add(rightNav);
Add(navRow);
// Day names row
_dayNamesRow = new VisualElement { name = "day-names" };
_dayNamesRow.AddToClassList("utk-date-picker__day-names");
for (int i = 0; i < DAY_NAME_KEYS.Length; i++)
{
var dayText = LocalizationManager.Instance.GetString(DAY_NAME_KEYS[i]);
var label = new Label(dayText) { name = $"day-name-{i}" };
label.AddToClassList("utk-date-picker__day-name");
// 요일별 색상 클래스 (0=일요일, 6=토요일)
if (i == 0) label.AddToClassList("utk-date-picker__day-name--sunday");
else if (i == 6) label.AddToClassList("utk-date-picker__day-name--saturday");
_dayNamesRow.Add(label);
}
Add(_dayNamesRow);
// Calendar grid - 6 rows
for (int row = 0; row < 6; row++)
{
var rowContainer = new VisualElement { name = $"calendar-row-{row}" };
rowContainer.AddToClassList("utk-date-picker__calendar-row");
for (int col = 0; col < 7; col++)
{
var dayButton = new Button { name = $"day-{row * 7 + col}" };
dayButton.AddToClassList("utk-date-picker__day-btn");
int index = row * 7 + col;
dayButton.clicked += () => OnDayClicked(index);
_dayButtons.Add(dayButton);
rowContainer.Add(dayButton);
}
Add(rowContainer);
}
// Time row (optional)
_timeRow = new VisualElement { name = "time-row" };
_timeRow.AddToClassList("utk-date-picker__time-row");
var timeLabel = new Label("시간:");
timeLabel.AddToClassList("utk-date-picker__time-label");
_hourStepper = new UTKNumberStepper(0, 23, 0, 1) { name = "hour-stepper" };
_hourStepper.AddToClassList("utk-date-picker__time-stepper");
_hourStepper.WrapAround = true;
var colonLabel = new Label(":");
colonLabel.AddToClassList("utk-date-picker__time-separator");
_minuteStepper = new UTKNumberStepper(0, 59, 0, 1) { name = "minute-stepper" };
_minuteStepper.AddToClassList("utk-date-picker__time-stepper");
_minuteStepper.WrapAround = true;
// Tab 키로 시간 <-> 분 이동
_hourStepper.OnTabPressed += () => _minuteStepper?.Focus();
_minuteStepper.OnShiftTabPressed += () => _hourStepper?.Focus();
_timeRow.Add(timeLabel);
_timeRow.Add(_hourStepper);
_timeRow.Add(colonLabel);
_timeRow.Add(_minuteStepper);
Add(_timeRow);
// Buttons
var buttonRow = new VisualElement { name = "button-row" };
buttonRow.AddToClassList("utk-date-picker__button-row");
_cancelButton = new Button { name = "cancel-btn", text = "Cancel" };
_cancelButton.AddToClassList("utk-date-picker__cancel-btn");
_confirmButton = new Button { name = "confirm-btn", text = "OK" };
_confirmButton.AddToClassList("utk-date-picker__confirm-btn");
buttonRow.Add(_cancelButton);
buttonRow.Add(_confirmButton);
Add(buttonRow);
}
private Button CreateNavButton(string name, string text)
{
var btn = new Button { name = name, text = text };
btn.AddToClassList("utk-date-picker__nav-btn");
return btn;
}
private void QueryElements()
{
_titleLabel ??= this.Q<Label>("title");
_closeButton ??= this.Q<Button>("close-btn");
_yearMonthLabel ??= this.Q<Label>("year-month");
_prevYearButton ??= this.Q<Button>("prev-year");
_prevMonthButton ??= this.Q<Button>("prev-month");
_nextMonthButton ??= this.Q<Button>("next-month");
_nextYearButton ??= this.Q<Button>("next-year");
_dayNamesRow ??= this.Q<VisualElement>("day-names");
_timeRow ??= this.Q<VisualElement>("time-row");
_hourStepper ??= this.Q<UTKNumberStepper>("hour-stepper");
_minuteStepper ??= this.Q<UTKNumberStepper>("minute-stepper");
_cancelButton ??= this.Q<Button>("cancel-btn");
_confirmButton ??= this.Q<Button>("confirm-btn");
// Day buttons 쿼리
if (_dayButtons.Count == 0)
{
for (int i = 0; i < DAYS_IN_GRID; i++)
{
var btn = this.Q<Button>($"day-{i}");
if (btn != null)
{
_dayButtons.Add(btn);
int index = i;
btn.clicked += () => OnDayClicked(index);
}
}
}
}
private void SetupEvents()
{
_closeButton?.RegisterCallback<ClickEvent>(_ => Close());
_cancelButton?.RegisterCallback<ClickEvent>(_ => Close());
_confirmButton?.RegisterCallback<ClickEvent>(_ => Confirm());
_prevYearButton?.RegisterCallback<ClickEvent>(_ => PreviousYear());
_prevMonthButton?.RegisterCallback<ClickEvent>(_ => PreviousMonth());
_nextMonthButton?.RegisterCallback<ClickEvent>(_ => NextMonth());
_nextYearButton?.RegisterCallback<ClickEvent>(_ => NextYear());
// 언어 변경 이벤트 구독
LocalizationManager.Instance.OnLanguageChanged += OnLanguageChanged;
}
private void OnLanguageChanged(string newLanguage)
{
UpdateDayNameLabels();
}
private void UpdateDayNameLabels()
{
if (_dayNamesRow == null) return;
for (int i = 0; i < DAY_NAME_KEYS.Length; i++)
{
var label = _dayNamesRow.Q<Label>($"day-name-{i}");
if (label != null)
{
label.text = LocalizationManager.Instance.GetString(DAY_NAME_KEYS[i]);
}
}
}
#endregion
#region Private Methods - Logic
private void NavigateMonth(int months)
{
_displayMonth = _displayMonth.AddMonths(months);
UpdateCalendar();
}
private void UpdateCalendar()
{
if (_yearMonthLabel != null)
{
_yearMonthLabel.text = $"{_displayMonth.Year}년 {_displayMonth.Month:D2}월";
}
// 첫째 날의 요일
DateTime firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1);
int startDayOfWeek = (int)firstDay.DayOfWeek; // 0=Sunday
// 해당 월의 일 수
int daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month);
DateTime today = DateTime.Today;
for (int i = 0; i < DAYS_IN_GRID; i++)
{
var btn = _dayButtons[i];
// 동적 클래스 초기화
btn.RemoveFromClassList("utk-date-picker__day-btn--sunday");
btn.RemoveFromClassList("utk-date-picker__day-btn--saturday");
btn.RemoveFromClassList("utk-date-picker__day-btn--today");
btn.RemoveFromClassList("utk-date-picker__day-btn--selected");
btn.RemoveFromClassList("utk-date-picker__day-btn--other-month");
int dayNumber = i - startDayOfWeek + 1;
if (dayNumber < 1 || dayNumber > daysInMonth)
{
// 이번 달이 아닌 날
btn.text = "";
btn.SetEnabled(false);
btn.AddToClassList("utk-date-picker__day-btn--other-month");
}
else
{
btn.text = dayNumber.ToString();
btn.SetEnabled(true);
DateTime currentDate = new DateTime(_displayMonth.Year, _displayMonth.Month, dayNumber);
DayOfWeek dow = currentDate.DayOfWeek;
// 선택된 날짜인지 확인
bool isSelected = currentDate.Date == _selectedDate.Date;
// 오늘인지 확인
bool isToday = currentDate.Date == today;
// 선택 상태 클래스
if (isSelected)
{
btn.AddToClassList("utk-date-picker__day-btn--selected");
}
// 오늘 클래스 (선택되지 않은 경우에만)
if (isToday && !isSelected)
{
btn.AddToClassList("utk-date-picker__day-btn--today");
}
// 요일별 클래스 (선택되지 않은 경우에만)
if (!isSelected)
{
if (dow == DayOfWeek.Sunday)
{
btn.AddToClassList("utk-date-picker__day-btn--sunday");
}
else if (dow == DayOfWeek.Saturday)
{
btn.AddToClassList("utk-date-picker__day-btn--saturday");
}
}
}
}
}
private void OnDayClicked(int index)
{
// 첫째 날의 요일
DateTime firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1);
int startDayOfWeek = (int)firstDay.DayOfWeek;
int dayNumber = index - startDayOfWeek + 1;
int daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month);
if (dayNumber >= 1 && dayNumber <= daysInMonth)
{
// 시간 유지
int hour = _hourStepper?.Value ?? _selectedDate.Hour;
int minute = _minuteStepper?.Value ?? _selectedDate.Minute;
_selectedDate = new DateTime(
_displayMonth.Year,
_displayMonth.Month,
dayNumber,
hour,
minute,
0
);
UpdateCalendar();
}
}
private void Confirm()
{
OnDateSelected?.Invoke(GetDate());
Close();
}
private void SetTitle(string title)
{
if (_titleLabel != null)
_titleLabel.text = title;
}
private void SetTimeVisible(bool visible)
{
if (_timeRow != null)
_timeRow.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
}
private void UpdateTimeFields()
{
if (_hourStepper != null)
_hourStepper.SetValue(_selectedDate.Hour, notify: false);
if (_minuteStepper != null)
_minuteStepper.SetValue(_selectedDate.Minute, notify: false);
}
private void CenterOnScreen()
{
schedule.Execute(() =>
{
var parent = this.parent;
if (parent == null) return;
float parentWidth = parent.resolvedStyle.width;
float parentHeight = parent.resolvedStyle.height;
float selfWidth = resolvedStyle.width;
float selfHeight = resolvedStyle.height;
style.left = (parentWidth - selfWidth) / 2;
style.top = (parentHeight - selfHeight) / 2;
});
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 언어 변경 이벤트 구독 해제
LocalizationManager.Instance.OnLanguageChanged -= OnLanguageChanged;
_dayButtons.Clear();
OnDateSelected = null;
OnClosed = null;
_blocker = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d01e3cea7bbfd8442a4576a7059802a2

View File

@@ -0,0 +1,285 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit.Modal;
namespace UVC.UIToolkit
{
/// <summary>
/// 모달 창 컴포넌트.
/// 사용자 정의 콘텐츠를 포함할 수 있는 모달 대화상자입니다.
/// </summary>
[UxmlElement]
public partial class UTKModal : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Modal/UTKModal";
#endregion
#region Fields
private bool _disposed;
private UTKModalBlocker? _blocker;
private VisualElement? _header;
private Label? _titleLabel;
private Button? _closeButton;
private VisualElement? _content;
private VisualElement? _footer;
private string _title = "Modal";
private bool _showCloseButton = true;
private bool _closeOnBackdropClick = true;
private ModalSize _size = ModalSize.Medium;
#endregion
#region Events
/// <summary>닫힘 이벤트</summary>
public event Action? OnClosed;
#endregion
#region Properties
/// <summary>모달 제목</summary>
[UxmlAttribute]
public string Title
{
get => _title;
set
{
_title = value;
if (_titleLabel != null) _titleLabel.text = value;
}
}
/// <summary>닫기 버튼 표시 여부</summary>
[UxmlAttribute]
public bool ShowCloseButton
{
get => _showCloseButton;
set
{
_showCloseButton = value;
if (_closeButton != null)
{
_closeButton.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
/// <summary>배경 클릭 시 닫기 여부</summary>
[UxmlAttribute]
public bool CloseOnBackdropClick
{
get => _closeOnBackdropClick;
set => _closeOnBackdropClick = value;
}
/// <summary>모달 크기</summary>
[UxmlAttribute]
public ModalSize Size
{
get => _size;
set
{
_size = value;
UpdateSize();
}
}
/// <summary>콘텐츠 컨테이너</summary>
public VisualElement? ContentContainer => _content;
/// <summary>푸터 컨테이너</summary>
public VisualElement? FooterContainer => _footer;
#endregion
#region Enums
public enum ModalSize
{
Small,
Medium,
Large,
FullScreen
}
#endregion
#region Constructor
public UTKModal()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
CreateUI();
SetupEvents();
SubscribeToThemeChanges();
}
public UTKModal(string title, ModalSize size = ModalSize.Medium) : this()
{
Title = title;
Size = size;
}
#endregion
#region Static Factory
/// <summary>
/// 모달 표시
/// </summary>
public static UTKModal Show(VisualElement parent, string title, ModalSize size = ModalSize.Medium)
{
var modal = new UTKModal(title, size);
modal._blocker = UTKModalBlocker.Show(parent, 0.5f, false);
if (modal._closeOnBackdropClick)
{
modal._blocker.OnBlockerClicked += modal.Close;
}
modal._blocker.Add(modal);
// 중앙 정렬
modal.style.position = Position.Absolute;
modal.style.left = Length.Percent(50);
modal.style.top = Length.Percent(50);
modal.style.translate = new Translate(Length.Percent(-50), Length.Percent(-50));
return modal;
}
#endregion
#region UI Creation
private void CreateUI()
{
AddToClassList("utk-modal");
// Header
_header = new VisualElement { name = "header" };
_header.AddToClassList("utk-modal__header");
hierarchy.Add(_header);
_titleLabel = new Label { name = "title", text = _title };
_titleLabel.AddToClassList("utk-modal__title");
_header.Add(_titleLabel);
_closeButton = new Button { name = "close-btn", text = "✕" };
_closeButton.AddToClassList("utk-modal__close-btn");
_header.Add(_closeButton);
// Content
_content = new VisualElement { name = "content" };
_content.AddToClassList("utk-modal__content");
hierarchy.Add(_content);
// Footer
_footer = new VisualElement { name = "footer" };
_footer.AddToClassList("utk-modal__footer");
hierarchy.Add(_footer);
UpdateSize();
}
private void SetupEvents()
{
_closeButton?.RegisterCallback<ClickEvent>(_ => Close());
RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode == KeyCode.Escape)
{
Close();
evt.StopPropagation();
}
});
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region Methods
private void UpdateSize()
{
RemoveFromClassList("utk-modal--small");
RemoveFromClassList("utk-modal--medium");
RemoveFromClassList("utk-modal--large");
RemoveFromClassList("utk-modal--fullscreen");
var sizeClass = _size switch
{
ModalSize.Small => "utk-modal--small",
ModalSize.Large => "utk-modal--large",
ModalSize.FullScreen => "utk-modal--fullscreen",
_ => "utk-modal--medium"
};
AddToClassList(sizeClass);
}
/// <summary>
/// 콘텐츠 추가
/// </summary>
public new void Add(VisualElement element)
{
_content?.Add(element);
}
/// <summary>
/// 푸터에 요소 추가
/// </summary>
public void AddToFooter(VisualElement element)
{
_footer?.Add(element);
}
/// <summary>
/// 푸터 표시/숨김
/// </summary>
public void SetFooterVisible(bool visible)
{
if (_footer != null)
{
_footer.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
}
}
/// <summary>
/// 모달 닫기
/// </summary>
public void Close()
{
OnClosed?.Invoke();
if (_blocker != null)
{
_blocker.OnBlockerClicked -= Close;
_blocker.Hide();
}
_blocker = null;
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
OnClosed = null;
_blocker = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b126ba0e77214b54796e203a14746d45

View File

@@ -0,0 +1,173 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit.Modal
{
/// <summary>
/// 모달 뒤의 배경을 차단하고 클릭 시 닫힘 처리를 담당하는 컴포넌트
/// </summary>
[UxmlElement]
public partial class UTKModalBlocker : VisualElement, IDisposable
{
#region Constants
private const string USS_CLASS = "utk-modal-blocker";
private const float DEFAULT_OPACITY = 0.7f;
private const int FADE_DURATION_MS = 200;
#endregion
#region Fields
private bool _disposed;
private bool _closeOnClick;
private float _targetOpacity;
#endregion
#region Events
/// <summary>
/// 블로커가 클릭되었을 때 발생
/// </summary>
public event Action? OnBlockerClicked;
#endregion
#region Constructor
/// <summary>
/// UTKModalBlocker 생성자
/// </summary>
public UTKModalBlocker()
{
AddToClassList(USS_CLASS);
// 기본 스타일 설정
style.position = Position.Absolute;
style.left = 0;
style.top = 0;
style.right = 0;
style.bottom = 0;
style.backgroundColor = new Color(0, 0, 0, 0);
// 클릭 이벤트 등록
RegisterCallback<ClickEvent>(OnClick);
}
#endregion
#region Static Factory
/// <summary>
/// 모달 블로커를 생성하고 표시합니다.
/// </summary>
/// <param name="root">블로커를 추가할 루트 요소</param>
/// <param name="opacity">배경 투명도 (0~1)</param>
/// <param name="closeOnClick">클릭 시 자동 닫힘 여부</param>
/// <returns>생성된 UTKModalBlocker 인스턴스</returns>
public static UTKModalBlocker Show(VisualElement root, float opacity = DEFAULT_OPACITY, bool closeOnClick = false)
{
var blocker = new UTKModalBlocker
{
_closeOnClick = closeOnClick,
_targetOpacity = Mathf.Clamp01(opacity)
};
root.Add(blocker);
blocker.FadeIn();
return blocker;
}
#endregion
#region Public Methods
/// <summary>
/// 블로커를 숨기고 제거합니다.
/// </summary>
public void Hide()
{
FadeOut(() =>
{
RemoveFromHierarchy();
Dispose();
});
}
/// <summary>
/// 모달 콘텐츠를 블로커 앞에 추가합니다.
/// </summary>
/// <param name="modalContent">모달 콘텐츠 요소</param>
public void ShowModal(VisualElement modalContent)
{
if (modalContent == null)
return;
// 모달 콘텐츠를 블로커 다음에 추가
var parent = this.parent;
if (parent != null)
{
int blockerIndex = parent.IndexOf(this);
parent.Insert(blockerIndex + 1, modalContent);
}
}
/// <summary>
/// 블로커를 맨 위로 가져옵니다.
/// </summary>
public void BringToFront()
{
this.BringToFront();
}
#endregion
#region Private Methods
private void OnClick(ClickEvent evt)
{
// 블로커 자체가 클릭된 경우에만 처리 (버블링된 이벤트 무시)
if (evt.target != this)
return;
OnBlockerClicked?.Invoke();
if (_closeOnClick)
{
Hide();
}
evt.StopPropagation();
}
private void FadeIn()
{
// 시작 투명도
style.backgroundColor = new Color(0, 0, 0, 0);
// 애니메이션
schedule.Execute(() =>
{
style.transitionDuration = new StyleList<TimeValue>(new List<TimeValue> { new TimeValue(FADE_DURATION_MS, TimeUnit.Millisecond) });
style.transitionProperty = new StyleList<StylePropertyName>(new List<StylePropertyName> { new StylePropertyName("background-color") });
style.backgroundColor = new Color(0, 0, 0, _targetOpacity);
});
}
private void FadeOut(Action? onComplete = null)
{
style.transitionDuration = new StyleList<TimeValue>(new List<TimeValue> { new TimeValue(FADE_DURATION_MS, TimeUnit.Millisecond) });
style.transitionProperty = new StyleList<StylePropertyName>(new List<StylePropertyName> { new StylePropertyName("background-color") });
style.backgroundColor = new Color(0, 0, 0, 0);
// 페이드 아웃 완료 후 콜백
schedule.Execute(() => onComplete?.Invoke()).ExecuteLater(FADE_DURATION_MS);
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
UnregisterCallback<ClickEvent>(OnClick);
OnBlockerClicked = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 21cd16195edffc849a96bb6a83934c4c

View File

@@ -0,0 +1,350 @@
#nullable enable
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 알림 창 컴포넌트.
/// 화면 모서리에 표시되는 알림 메시지입니다.
/// </summary>
[UxmlElement]
public partial class UTKNotification : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Modal/UTKNotification";
private const int DEFAULT_DURATION_MS = 5000;
#endregion
#region Fields
private bool _disposed;
private VisualElement? _header;
private Label? _iconLabel;
private Label? _titleLabel;
private Button? _closeButton;
private Label? _messageLabel;
private VisualElement? _actions;
private string _title = "";
private string _message = "";
private NotificationType _type = NotificationType.Info;
private int _duration = DEFAULT_DURATION_MS;
private NotificationPosition _position = NotificationPosition.TopRight;
#endregion
#region Events
/// <summary>닫힘 이벤트</summary>
public event Action? OnClosed;
/// <summary>액션 클릭 이벤트</summary>
public event Action<string>? OnActionClicked;
#endregion
#region Properties
/// <summary>제목</summary>
[UxmlAttribute]
public string Title
{
get => _title;
set
{
_title = value;
if (_titleLabel != null) _titleLabel.text = value;
}
}
/// <summary>메시지</summary>
[UxmlAttribute]
public string Message
{
get => _message;
set
{
_message = value;
if (_messageLabel != null) _messageLabel.text = value;
}
}
/// <summary>알림 유형</summary>
[UxmlAttribute]
public NotificationType Type
{
get => _type;
set
{
_type = value;
UpdateType();
}
}
/// <summary>표시 시간 (밀리초, 0이면 수동 닫기)</summary>
[UxmlAttribute]
public int Duration
{
get => _duration;
set => _duration = value;
}
/// <summary>표시 위치</summary>
[UxmlAttribute]
public NotificationPosition Position
{
get => _position;
set => _position = value;
}
/// <summary>액션 컨테이너</summary>
public VisualElement? ActionsContainer => _actions;
#endregion
#region Enums
public enum NotificationType
{
Info,
Success,
Warning,
Error
}
public enum NotificationPosition
{
TopLeft,
TopRight,
BottomLeft,
BottomRight
}
#endregion
#region Constructor
public UTKNotification()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
CreateUI();
SubscribeToThemeChanges();
}
public UTKNotification(string title, string message, NotificationType type = NotificationType.Info) : this()
{
Title = title;
Message = message;
Type = type;
}
#endregion
#region Static Factory
/// <summary>
/// Info 알림 표시
/// </summary>
public static UTKNotification ShowInfo(VisualElement parent, string title, string message, int duration = DEFAULT_DURATION_MS)
{
return Show(parent, title, message, NotificationType.Info, NotificationPosition.TopRight, duration);
}
/// <summary>
/// Success 알림 표시
/// </summary>
public static UTKNotification ShowSuccess(VisualElement parent, string title, string message, int duration = DEFAULT_DURATION_MS)
{
return Show(parent, title, message, NotificationType.Success, NotificationPosition.TopRight, duration);
}
/// <summary>
/// Warning 알림 표시
/// </summary>
public static UTKNotification ShowWarning(VisualElement parent, string title, string message, int duration = DEFAULT_DURATION_MS)
{
return Show(parent, title, message, NotificationType.Warning, NotificationPosition.TopRight, duration);
}
/// <summary>
/// Error 알림 표시
/// </summary>
public static UTKNotification ShowError(VisualElement parent, string title, string message, int duration = DEFAULT_DURATION_MS)
{
return Show(parent, title, message, NotificationType.Error, NotificationPosition.TopRight, duration);
}
/// <summary>
/// 알림 표시
/// </summary>
public static UTKNotification Show(VisualElement parent, string title, string message,
NotificationType type, NotificationPosition position, int duration = DEFAULT_DURATION_MS)
{
var notification = new UTKNotification(title, message, type);
notification._duration = duration;
notification._position = position;
parent.Add(notification);
// 위치 설정
notification.style.position = UnityEngine.UIElements.Position.Absolute;
notification.ApplyPosition(position);
// 자동 닫기
if (duration > 0)
{
notification.AutoCloseAsync(duration).Forget();
}
return notification;
}
#endregion
#region UI Creation
private void CreateUI()
{
AddToClassList("utk-notification");
// Header
_header = new VisualElement { name = "header" };
_header.AddToClassList("utk-notification__header");
Add(_header);
_iconLabel = new Label { name = "icon" };
_iconLabel.AddToClassList("utk-notification__icon");
_header.Add(_iconLabel);
_titleLabel = new Label { name = "title" };
_titleLabel.AddToClassList("utk-notification__title");
_header.Add(_titleLabel);
_closeButton = new Button { name = "close-btn", text = "✕" };
_closeButton.AddToClassList("utk-notification__close-btn");
_closeButton.RegisterCallback<ClickEvent>(_ => Close());
_header.Add(_closeButton);
// Message
_messageLabel = new Label { name = "message" };
_messageLabel.AddToClassList("utk-notification__message");
Add(_messageLabel);
// Actions
_actions = new VisualElement { name = "actions" };
_actions.AddToClassList("utk-notification__actions");
Add(_actions);
UpdateType();
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region Methods
private void UpdateType()
{
RemoveFromClassList("utk-notification--info");
RemoveFromClassList("utk-notification--success");
RemoveFromClassList("utk-notification--warning");
RemoveFromClassList("utk-notification--error");
var typeClass = _type switch
{
NotificationType.Success => "utk-notification--success",
NotificationType.Warning => "utk-notification--warning",
NotificationType.Error => "utk-notification--error",
_ => "utk-notification--info"
};
AddToClassList(typeClass);
if (_iconLabel != null)
{
_iconLabel.text = _type switch
{
NotificationType.Success => "✓",
NotificationType.Warning => "⚠",
NotificationType.Error => "✕",
_ => ""
};
}
}
private void ApplyPosition(NotificationPosition position)
{
const int offset = 16;
switch (position)
{
case NotificationPosition.TopLeft:
style.top = offset;
style.left = offset;
break;
case NotificationPosition.TopRight:
style.top = offset;
style.right = offset;
break;
case NotificationPosition.BottomLeft:
style.bottom = offset;
style.left = offset;
break;
case NotificationPosition.BottomRight:
style.bottom = offset;
style.right = offset;
break;
}
}
private async UniTaskVoid AutoCloseAsync(int delayMs)
{
await UniTask.Delay(delayMs);
if (!_disposed)
{
Close();
}
}
/// <summary>
/// 액션 버튼 추가
/// </summary>
public void AddAction(string label, string actionId)
{
if (_actions == null) return;
var btn = new UTKButton(label, "", UTKButton.ButtonVariant.Ghost);
btn.Size = UTKButton.ButtonSize.Small;
btn.OnClicked += () => OnActionClicked?.Invoke(actionId);
_actions.Add(btn);
}
/// <summary>
/// 알림 닫기
/// </summary>
public void Close()
{
OnClosed?.Invoke();
RemoveFromHierarchy();
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
OnClosed = null;
OnActionClicked = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b5b1d71d12b91f24585078c70006df42

View File

@@ -0,0 +1,297 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 패널 컨테이너 컴포넌트.
/// 콘텐츠를 그룹화하고 시각적으로 구분합니다.
/// </summary>
[UxmlElement]
public partial class UTKPanel : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Modal/UTKPanel";
#endregion
#region Fields
private bool _disposed;
private VisualElement? _header;
private Label? _titleLabel;
private VisualElement? _headerActions;
private VisualElement? _content;
private VisualElement? _footer;
private string _title = "";
private bool _showHeader = true;
private bool _showFooter;
private PanelVariant _variant = PanelVariant.Default;
private bool _isCollapsible;
private bool _isCollapsed;
#endregion
#region Events
/// <summary>접힘 상태 변경 이벤트</summary>
public event Action<bool>? OnCollapsedChanged;
#endregion
#region Properties
/// <summary>패널 제목</summary>
[UxmlAttribute]
public string Title
{
get => _title;
set
{
_title = value;
if (_titleLabel != null) _titleLabel.text = value;
}
}
/// <summary>헤더 표시 여부</summary>
[UxmlAttribute]
public bool ShowHeader
{
get => _showHeader;
set
{
_showHeader = value;
if (_header != null)
{
_header.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
/// <summary>푸터 표시 여부</summary>
[UxmlAttribute]
public bool ShowFooter
{
get => _showFooter;
set
{
_showFooter = value;
if (_footer != null)
{
_footer.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
/// <summary>패널 스타일</summary>
[UxmlAttribute]
public PanelVariant Variant
{
get => _variant;
set
{
_variant = value;
UpdateVariant();
}
}
/// <summary>접기 가능 여부</summary>
[UxmlAttribute]
public bool IsCollapsible
{
get => _isCollapsible;
set
{
_isCollapsible = value;
EnableInClassList("utk-panel--collapsible", value);
}
}
/// <summary>접힘 상태</summary>
[UxmlAttribute]
public bool IsCollapsed
{
get => _isCollapsed;
set => SetCollapsed(value, true);
}
/// <summary>콘텐츠 영역</summary>
public VisualElement? ContentContainer => _content;
/// <summary>푸터 영역</summary>
public VisualElement? FooterContainer => _footer;
/// <summary>헤더 액션 영역</summary>
public VisualElement? HeaderActionsContainer => _headerActions;
#endregion
#region Enums
public enum PanelVariant
{
Default,
Elevated,
Outlined,
Flat
}
#endregion
#region Constructor
public UTKPanel()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
CreateUI();
SetupEvents();
SubscribeToThemeChanges();
}
public UTKPanel(string title) : this()
{
Title = title;
}
#endregion
#region UI Creation
private void CreateUI()
{
AddToClassList("utk-panel");
// Header
_header = new VisualElement { name = "header" };
_header.AddToClassList("utk-panel__header");
hierarchy.Add(_header);
_titleLabel = new Label { name = "title" };
_titleLabel.AddToClassList("utk-panel__title");
_header.Add(_titleLabel);
_headerActions = new VisualElement { name = "header-actions" };
_headerActions.AddToClassList("utk-panel__header-actions");
_header.Add(_headerActions);
// Content
_content = new VisualElement { name = "content" };
_content.AddToClassList("utk-panel__content");
hierarchy.Add(_content);
// Footer
_footer = new VisualElement { name = "footer" };
_footer.AddToClassList("utk-panel__footer");
_footer.style.display = DisplayStyle.None;
hierarchy.Add(_footer);
UpdateVariant();
}
private void SetupEvents()
{
_header?.RegisterCallback<ClickEvent>(OnHeaderClick);
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region Event Handlers
private void OnHeaderClick(ClickEvent evt)
{
if (!_isCollapsible) return;
SetCollapsed(!_isCollapsed, true);
evt.StopPropagation();
}
#endregion
#region Methods
/// <summary>
/// 접힘 상태 설정
/// </summary>
public void SetCollapsed(bool collapsed, bool notify)
{
if (_isCollapsed == collapsed) return;
_isCollapsed = collapsed;
EnableInClassList("utk-panel--collapsed", collapsed);
if (_content != null)
{
_content.style.display = collapsed ? DisplayStyle.None : DisplayStyle.Flex;
}
if (_footer != null && _showFooter)
{
_footer.style.display = collapsed ? DisplayStyle.None : DisplayStyle.Flex;
}
if (notify)
{
OnCollapsedChanged?.Invoke(collapsed);
}
}
private void UpdateVariant()
{
RemoveFromClassList("utk-panel--default");
RemoveFromClassList("utk-panel--elevated");
RemoveFromClassList("utk-panel--outlined");
RemoveFromClassList("utk-panel--flat");
var variantClass = _variant switch
{
PanelVariant.Elevated => "utk-panel--elevated",
PanelVariant.Outlined => "utk-panel--outlined",
PanelVariant.Flat => "utk-panel--flat",
_ => "utk-panel--default"
};
AddToClassList(variantClass);
}
/// <summary>
/// 콘텐츠 추가
/// </summary>
public new void Add(VisualElement element)
{
_content?.Add(element);
}
/// <summary>
/// 헤더 액션 추가
/// </summary>
public void AddHeaderAction(VisualElement element)
{
_headerActions?.Add(element);
}
/// <summary>
/// 푸터에 요소 추가
/// </summary>
public void AddToFooter(VisualElement element)
{
_footer?.Add(element);
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
OnCollapsedChanged = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e437e0457f225ff4db4735261757835b

View File

@@ -0,0 +1,274 @@
#nullable enable
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 토스트 알림 컴포넌트.
/// 화면 하단에 일시적으로 표시되는 알림입니다.
/// </summary>
[UxmlElement]
public partial class UTKToast : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Modal/UTKToast";
private const int DEFAULT_DURATION_MS = 3000;
#endregion
#region Fields
private bool _disposed;
private Label? _iconLabel;
private Label? _messageLabel;
private Button? _closeButton;
private string _message = "";
private ToastType _type = ToastType.Info;
private int _duration = DEFAULT_DURATION_MS;
private bool _showCloseButton;
#endregion
#region Events
/// <summary>닫힘 이벤트</summary>
public event Action? OnClosed;
#endregion
#region Properties
/// <summary>메시지</summary>
[UxmlAttribute]
public string Message
{
get => _message;
set
{
_message = value;
if (_messageLabel != null) _messageLabel.text = value;
}
}
/// <summary>토스트 유형</summary>
[UxmlAttribute]
public ToastType Type
{
get => _type;
set
{
_type = value;
UpdateType();
}
}
/// <summary>표시 시간 (밀리초)</summary>
[UxmlAttribute]
public int Duration
{
get => _duration;
set => _duration = value;
}
/// <summary>닫기 버튼 표시 여부</summary>
[UxmlAttribute]
public bool ShowCloseButton
{
get => _showCloseButton;
set
{
_showCloseButton = value;
if (_closeButton != null)
{
_closeButton.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
#endregion
#region Enums
public enum ToastType
{
Info,
Success,
Warning,
Error
}
#endregion
#region Constructor
public UTKToast()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
CreateUI();
SubscribeToThemeChanges();
}
public UTKToast(string message, ToastType type = ToastType.Info) : this()
{
Message = message;
Type = type;
}
#endregion
#region Static Factory
/// <summary>
/// Info 토스트 표시
/// </summary>
public static UTKToast ShowInfo(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS)
{
return Show(parent, message, ToastType.Info, duration);
}
/// <summary>
/// Success 토스트 표시
/// </summary>
public static UTKToast ShowSuccess(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS)
{
return Show(parent, message, ToastType.Success, duration);
}
/// <summary>
/// Warning 토스트 표시
/// </summary>
public static UTKToast ShowWarning(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS)
{
return Show(parent, message, ToastType.Warning, duration);
}
/// <summary>
/// Error 토스트 표시
/// </summary>
public static UTKToast ShowError(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS)
{
return Show(parent, message, ToastType.Error, duration);
}
/// <summary>
/// 토스트 표시
/// </summary>
public static UTKToast Show(VisualElement parent, string message, ToastType type, int duration = DEFAULT_DURATION_MS)
{
var toast = new UTKToast(message, type);
toast._duration = duration;
parent.Add(toast);
// 하단 중앙 정렬
toast.style.position = Position.Absolute;
toast.style.left = Length.Percent(50);
toast.style.bottom = 20;
toast.style.translate = new Translate(Length.Percent(-50), 0);
// 자동 닫기
if (duration > 0)
{
toast.AutoCloseAsync(duration).Forget();
}
return toast;
}
#endregion
#region UI Creation
private void CreateUI()
{
AddToClassList("utk-toast");
_iconLabel = new Label { name = "icon" };
_iconLabel.AddToClassList("utk-toast__icon");
Add(_iconLabel);
_messageLabel = new Label { name = "message" };
_messageLabel.AddToClassList("utk-toast__message");
Add(_messageLabel);
_closeButton = new Button { name = "close-btn", text = "✕" };
_closeButton.AddToClassList("utk-toast__close-btn");
_closeButton.style.display = DisplayStyle.None;
_closeButton.RegisterCallback<ClickEvent>(_ => Close());
Add(_closeButton);
UpdateType();
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
#endregion
#region Methods
private void UpdateType()
{
RemoveFromClassList("utk-toast--info");
RemoveFromClassList("utk-toast--success");
RemoveFromClassList("utk-toast--warning");
RemoveFromClassList("utk-toast--error");
var typeClass = _type switch
{
ToastType.Success => "utk-toast--success",
ToastType.Warning => "utk-toast--warning",
ToastType.Error => "utk-toast--error",
_ => "utk-toast--info"
};
AddToClassList(typeClass);
if (_iconLabel != null)
{
_iconLabel.text = _type switch
{
ToastType.Success => "✓",
ToastType.Warning => "⚠",
ToastType.Error => "✕",
_ => ""
};
}
}
private async UniTaskVoid AutoCloseAsync(int delayMs)
{
await UniTask.Delay(delayMs);
if (!_disposed)
{
Close();
}
}
/// <summary>
/// 토스트 닫기
/// </summary>
public void Close()
{
OnClosed?.Invoke();
RemoveFromHierarchy();
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
OnClosed = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f578f7e9af679ed42922a5edf6d35618