173 lines
7.6 KiB
C#
173 lines
7.6 KiB
C#
#nullable enable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using DTNavigation.Model;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace DTNavigation.View
|
|
{
|
|
/// <summary>
|
|
/// 사이드바 UI DOM 빌더.
|
|
/// UXML 쿼리, 네비게이션 항목 생성, 상태 클래스 전환을 담당합니다.
|
|
/// </summary>
|
|
public sealed class SidebarView : IDisposable
|
|
{
|
|
// ── 상태별 CSS 클래스 ──────────────────────────────────────
|
|
private static readonly string[] StateClasses =
|
|
{ "sidebar--one-depth", "sidebar--two-depth", "sidebar--icon-only" };
|
|
|
|
// ── UXML 요소 참조 ─────────────────────────────────────────
|
|
private readonly VisualElement _sidebarRoot;
|
|
private readonly VisualElement _navList;
|
|
private readonly Button _toggleBtn;
|
|
|
|
// ── 토글 버튼 콜백 (RegisterCallback 대칭 해제용) ──────────
|
|
private EventCallback<ClickEvent>? _toggleBtnCallback;
|
|
|
|
// ── 외부 이벤트 ────────────────────────────────────────────
|
|
public event Action? OnToggle;
|
|
|
|
/// <summary>1뎁스 그룹 헤더 클릭 시 발행 (그룹 모델 전달)</summary>
|
|
public event Action<NavItemModel>? OnNavGroupClicked;
|
|
|
|
/// <summary>2뎁스 자식 항목 클릭 시 발행 (itemId 전달)</summary>
|
|
public event Action<string>? OnNavItemSelected;
|
|
|
|
// ──────────────────────────────────────────────────────────
|
|
public SidebarView(VisualElement documentRoot)
|
|
{
|
|
_sidebarRoot = documentRoot.Q<VisualElement>("sidebar-root")
|
|
?? throw new InvalidOperationException("'sidebar-root' 요소를 찾을 수 없습니다.");
|
|
_navList = documentRoot.Q<VisualElement>("nav-list")
|
|
?? throw new InvalidOperationException("'nav-list' 요소를 찾을 수 없습니다.");
|
|
_toggleBtn = documentRoot.Q<Button>("sidebar-toggle-btn")
|
|
?? throw new InvalidOperationException("'sidebar-toggle-btn' 요소를 찾을 수 없습니다.");
|
|
|
|
_toggleBtnCallback = _ => OnToggle?.Invoke();
|
|
_toggleBtn.RegisterCallback(_toggleBtnCallback);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────
|
|
/// <summary>상태, 데이터, 선택 항목을 받아 사이드바 전체를 다시 그립니다.</summary>
|
|
public void Render(SidebarState state, IReadOnlyList<NavItemModel> items, string? selectedItemId)
|
|
{
|
|
ApplyStateClass(state);
|
|
UpdateToggleButton(state);
|
|
BuildNavList(state, items, selectedItemId);
|
|
}
|
|
|
|
// ── 상태 클래스 전환 ───────────────────────────────────────
|
|
private void ApplyStateClass(SidebarState state)
|
|
{
|
|
foreach (var cls in StateClasses)
|
|
_sidebarRoot.RemoveFromClassList(cls);
|
|
|
|
_sidebarRoot.AddToClassList(state switch
|
|
{
|
|
SidebarState.OneDepth => "sidebar--one-depth",
|
|
SidebarState.TwoDepth => "sidebar--two-depth",
|
|
SidebarState.IconOnly => "sidebar--icon-only",
|
|
_ => "sidebar--one-depth",
|
|
});
|
|
}
|
|
|
|
// ── 토글 버튼 텍스트 ───────────────────────────────────────
|
|
private void UpdateToggleButton(SidebarState state)
|
|
=> _toggleBtn.text = state == SidebarState.IconOnly ? ">" : "<";
|
|
|
|
// ── 네비게이션 목록 빌드 ───────────────────────────────────
|
|
private void BuildNavList(SidebarState state, IReadOnlyList<NavItemModel> items, string? selectedItemId)
|
|
{
|
|
_navList.Clear();
|
|
for (var i = 0; i < items.Count; i++)
|
|
_navList.Add(CreateNavItem(state, items[i], selectedItemId));
|
|
}
|
|
|
|
private VisualElement CreateNavItem(SidebarState state, NavItemModel item, string? selectedItemId)
|
|
{
|
|
var container = new VisualElement();
|
|
container.AddToClassList("nav-item");
|
|
|
|
container.Add(CreateNavItemHeader(state, item, selectedItemId));
|
|
|
|
if (item.HasChildren && state == SidebarState.TwoDepth)
|
|
container.Add(CreateChildList(item, selectedItemId));
|
|
|
|
return container;
|
|
}
|
|
|
|
private VisualElement CreateNavItemHeader(SidebarState state, NavItemModel item, string? selectedItemId)
|
|
{
|
|
var header = new VisualElement();
|
|
header.AddToClassList("nav-item__header");
|
|
|
|
if (item.IsTopLevel)
|
|
{
|
|
header.AddToClassList("nav-item__header--top");
|
|
|
|
var label = new Label(item.Label);
|
|
label.AddToClassList("nav-item__label");
|
|
label.AddToClassList("nav-item__label--top");
|
|
header.Add(label);
|
|
|
|
// 최상위 항목은 선택 가능한 단독 항목으로 처리
|
|
if (item.Id == selectedItemId)
|
|
header.AddToClassList("nav-item__header--selected");
|
|
|
|
header.RegisterCallback<ClickEvent>(_ => OnNavItemSelected?.Invoke(item.Id));
|
|
}
|
|
else
|
|
{
|
|
var icon = new Label("i");
|
|
icon.AddToClassList("nav-item__icon");
|
|
header.Add(icon);
|
|
|
|
var labelText = state == SidebarState.OneDepth
|
|
? $"{item.Label} OVERVIEW"
|
|
: item.Label;
|
|
|
|
var label = new Label(labelText);
|
|
label.AddToClassList("nav-item__label");
|
|
header.Add(label);
|
|
|
|
// 1뎁스 그룹 헤더 클릭 → 자식 탭 추가 이벤트
|
|
header.RegisterCallback<ClickEvent>(_ => OnNavGroupClicked?.Invoke(item));
|
|
}
|
|
|
|
return header;
|
|
}
|
|
|
|
private VisualElement CreateChildList(NavItemModel item, string? selectedItemId)
|
|
{
|
|
var childList = new VisualElement();
|
|
childList.AddToClassList("nav-item__children");
|
|
|
|
for (var i = 0; i < item.Children.Count; i++)
|
|
{
|
|
var child = item.Children[i];
|
|
var childEl = new Label(child.Label);
|
|
childEl.AddToClassList("nav-child-item");
|
|
|
|
if (child.Id == selectedItemId)
|
|
childEl.AddToClassList("nav-child-item--selected");
|
|
|
|
childEl.RegisterCallback<ClickEvent>(_ => OnNavItemSelected?.Invoke(child.Id));
|
|
childList.Add(childEl);
|
|
}
|
|
|
|
return childList;
|
|
}
|
|
|
|
// ── IDisposable ────────────────────────────────────────────
|
|
public void Dispose()
|
|
{
|
|
if (_toggleBtnCallback != null)
|
|
{
|
|
_toggleBtn.UnregisterCallback(_toggleBtnCallback);
|
|
_toggleBtnCallback = null;
|
|
}
|
|
}
|
|
}
|
|
}
|