К содержимому

7. OverlayHost и порталы

OverlayHost — портал-контейнер для тултипов, попапов, модалок, дропдаунов, тостов и dev-консоли. Рендерится поверх экранов маршрутов (последний sibling в screen-space DOM — у UI Toolkit нет z-index).

World-space UI (health bar'ы, nameplate'ы) — всегда ПОД экранами, никогда в этом хосте. World-маркеры принадлежат 3D-объектам, поэтому меню или HUD должны иметь возможность их перекрывать — они никогда не должны жить в OverlayHost (самом верхнем слое). Два режима, оба ниже экранов:

  • Вариант A (по умолчанию — SusApp / SusBootstrap.EnsureWorldSpacePanel): отдельная панель (SusWorldSpacePanel, PanelSettings.renderMode = WorldSpace), которую камера рисует под всем screen UI.
  • Вариант B (fallback, без world-space панели): WorldSpaceService рендерит плоские screen-space маркеры в выделенный WorldMarkerLayerпервый child корня экрана (наименьший z, ниже экранов и этого OverlayHost) — репозиционируется каждый кадр через WorldToScreenPoint.

См. WorldSpaceService в справочнике API и 11-api-reference.

SusApp всегда строит фиксированный трёхслойный каркас на корне UIDocument — world-маркеры остаются под экранами, оверлеи остаются сверху, а всё содержимое приложения монтируется в средний ScreenHost:

World-space panel (separate UIDocument)          ← variant A, UNDER screens
└── healthbar / nameplate / floating damage

Screen-space UIDocument (root)
├── WorldMarkerLayer  ← first child = lowest (variant-B world markers)
├── ScreenHost        ← app content: Mount<T> component / router SusRouteView
└── OverlayHost       ← last child = always on top of screens
     ├── transition (Transition = 10)
     ├── modal      (Modal      = 20)
     ├── tooltip    (Tooltip    = 30)  ← above modals
     ├── dropdown   (Dropdown   = 40)  ← above modals
     ├── toast      (Toast      = 45)
     └── console    (Console    = 50)  ← topmost

Тултипы и дропдауны намеренно сидят выше модалок: модалка может содержать Select, меню или контрол с hover-тултипом — эти попапы не должны отрисовываться под диалогом.

Создание

AddToOverlay / RemoveFromOverlay — это методы на OverlayHost (не на SusComponent). Получите хост через SusBootstrap:

csharp
var overlay = SusBootstrap.GetOrCreateOverlay(root);

var popup = BuildMyPopup();
overlay.AddToOverlay(popup, OverlayCategory.Dropdown, dismissOnClickOutside: true);
// ...
overlay.RemoveFromOverlay(popup);

overlay.AddToOverlay(myTooltip, OverlayCategory.Tooltip);
overlay.AddToOverlay(myModal, OverlayCategory.Modal);

OverlayCategory — z-order

Больше значение = рисуется позже = сверху (нет USS z-index). Значения из sus-core/Runtime/OverlayCategory.cs:

КатегорияЗначениеРоль
World0Legacy / internal. World-маркерам не место в этом хосте (они бы рисовались поверх экранов). Они рендерятся под экранами — вариант A в отдельной SusWorldSpacePanel, вариант B в выделенном WorldMarkerLayer (первый child корня). Это значение сохраняется только для устаревшего fallback'а WorldSpaceService.OverlayHost.
Transition10Занавес перехода маршрута
Modal20Диалоги, drawer'ы
Tooltip30Hover-подсказки (выше модалок)
Dropdown40Меню, автокомплит (выше модалок)
Toast45Snackbar'ы
Console50Dev-консоль / оверлеи ошибок (самый верхний)

Внутри одной категории последняя добавленная запись оказывается сверху (стек модалок, стек тостов, …).

OverlayEntry

csharp
var entry = overlay.AddToOverlay(dialog, OverlayCategory.Modal);
// entry.Element — the dialog itself
// entry.Category — category
// entry.DismissOnClickOutside — false (can be set at creation)
// entry.OnDismiss — callback

Очистка

csharp
overlay.ClearCategory(OverlayCategory.Tooltip);  // remove all tooltips
overlay.ClearAll();                               // remove everything

Вспомогательные методы

Для распространённых паттернов не нужно позиционировать элементы вручную:

csharp
// Tooltip anchored to an element (position: "top" | "bottom" | "left" | "right").
// Added to the Tooltip category, dismissOnClickOutside: true.
overlay.ShowTooltip(anchor, tooltipContent, position: "top");

// Dropdown below an anchor; auto-flips above when there's no room. Dropdown category.
overlay.ShowDropdown(anchor, menuContent);

// One-time Escape handler: dismisses the topmost dismissable overlay. Idempotent.
overlay.InstallEscapeHandler();

// Trap Tab / Shift+Tab focus within a modal element.
overlay.InstallFocusTrap(myModalRoot);

Тема (доходит ли тема до оверлеев?)

Да. OverlayHost — последний child корня экрана и получает полный каскад design-токенов во время bootstrap (SusApp / SusBootstrap.GetOrCreateOverlay), плюс:

  • SusApp.UseCustomStyles(...) (брендинг) и UseFonts(...) применяются к OverlayHost так же, как и к корню экрана, поэтому попапы соответствуют цветам и шрифту вашего бренда.
  • SusThemeService.Instance.SetTheme(root, theme) также переключает класс .theme-* на OverlayHost и на каждом текущем открытом дочернем оверлее, поэтому репарентнутые попапы перерисовываются с активной темой.

Обычно вам не нужно ничего делать — монтируйте через SusApp, и оверлеи автоматически наследуют токены + тему + брендинг. Ручные bootstrap'ы должны вызывать SetTheme после построения оверлея, чтобы он подхватил нестандартную тему.

Связанное (Core)

ЧастьСтатусПримечания
Dev-консоль (SusConsoleService)ПоставляетсяOverlayCategory.Console
SusToast / toast-виджетЗапланированоИспользуйте OverlayCategory.Toast / SusToastBase, когда будете его строить
World-space healthbar / nameplateDownstream UI-пакетОтдельная world-space панель — под экранами, не в OverlayHost

API

csharp
public class OverlayHost : SusLayer   // SusLayer : VisualElement
{
    public OverlayEntry AddToOverlay(VisualElement element, OverlayCategory category,
        bool dismissOnClickOutside = false, Action onDismiss = null);
    public void RemoveFromOverlay(VisualElement element);
    public void RemoveFromOverlay(OverlayEntry entry);
    public void ClearCategory(OverlayCategory category);
    public void ClearAll();
    public int Count { get; }
    public IReadOnlyList<OverlayEntry> Stack { get; }
    public void DumpStack();   // #if UNITY_EDITOR || DEVELOPMENT_BUILD - diagnostics
}

public class OverlayEntry
{
    public VisualElement Element;
    public OverlayCategory Category;
    public bool DismissOnClickOutside;
    public Action OnDismiss;
}