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:
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:
| Категория | Значение | Роль |
|---|---|---|
World | 0 | Legacy / internal. World-маркерам не место в этом хосте (они бы рисовались поверх экранов). Они рендерятся под экранами — вариант A в отдельной SusWorldSpacePanel, вариант B в выделенном WorldMarkerLayer (первый child корня). Это значение сохраняется только для устаревшего fallback'а WorldSpaceService.OverlayHost. |
Transition | 10 | Занавес перехода маршрута |
Modal | 20 | Диалоги, drawer'ы |
Tooltip | 30 | Hover-подсказки (выше модалок) |
Dropdown | 40 | Меню, автокомплит (выше модалок) |
Toast | 45 | Snackbar'ы |
Console | 50 | Dev-консоль / оверлеи ошибок (самый верхний) |
Внутри одной категории последняя добавленная запись оказывается сверху (стек модалок, стек тостов, …).
OverlayEntry
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Очистка
overlay.ClearCategory(OverlayCategory.Tooltip); // remove all tooltips
overlay.ClearAll(); // remove everythingВспомогательные методы
Для распространённых паттернов не нужно позиционировать элементы вручную:
// 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 / nameplate | Downstream UI-пакет | Отдельная world-space панель — под экранами, не в OverlayHost |
API
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;
}