跳转到内容

7. OverlayHost 与传送门(portals)

OverlayHost 是一个用于 tooltip、popup、modal、下拉菜单、toast 以及开发者控制台的传送门容器。 它渲染在路由屏幕之上(在 screen-space DOM 中是最后一个 sibling——UI Toolkit 没有 z-index)。

World-space UI(血条、姓名牌)——始终在屏幕之下,绝不放进这个 host。 World 标记属于 3D 对象,所以菜单或 HUD 必须能够遮住它们——它们绝不能存在于 OverlayHost (最顶层)中。有两种模式,都在屏幕之下:

  • 方案 A(默认——SusApp / SusBootstrap.EnsureWorldSpacePanel):一个独立面板 (SusWorldSpacePanelPanelSettings.renderMode = WorldSpace),由相机绘制在所有屏幕 UI 之下。
  • 方案 B(回退方案,没有 world-space 面板):WorldSpaceService 把扁平的 screen-space 标记渲染进一个专用的 WorldMarkerLayer——屏幕根的第一个 child(z 值最低, 低于屏幕以及这个 OverlayHost)——每帧通过 WorldToScreenPoint 重新定位。

参见 API 参考中的 WorldSpaceService 以及 11-api-reference

SusApp 总是在 UIDocument 根上构建一个固定的三层脚手架——world 标记留在屏幕之下, overlay 留在最上层,而所有应用内容都挂载到中间的 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

Tooltip 和下拉菜单被特意放在模态框之上:一个 modal 可能包含 Select、菜单或带有 hover tooltip 的控件——这些 popup 不能被绘制在对话框下面。

创建

AddToOverlay / RemoveFromOverlayOverlayHost 上的方法(不在 SusComponent 上)。 通过 SusBootstrap 获取 host:

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 顺序

数值越大 = 越晚绘制 = 越靠上(没有 USS z-index)。数值来自 sus-core/Runtime/OverlayCategory.cs

分类数值作用
World0遗留 / 内部使用。 World 标记不应该放进这个 host(否则会盖在屏幕上面)。它们渲染在屏幕之下——方案 A 在独立的 SusWorldSpacePanel 中,方案 B 在专用的 WorldMarkerLayer 中(根的第一个 child)。这个值只是为了保留给已废弃的 WorldSpaceService.OverlayHost 回退路径。
Transition10路由过渡幕布
Modal20对话框、抽屉
Tooltip30悬停提示(在 modal 之上)
Dropdown40菜单、自动补全(在 modal 之上)
Toast45Snackbar
Console50开发者控制台 / 错误浮层(最顶层)

在同一个分类内,最后添加的条目在最上面(modal 栈、toast 栈等)。

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);

主题(主题能到达 overlay 吗?)

能。 OverlayHost 是屏幕根的最后一个 child,在 bootstrap 阶段(SusApp / SusBootstrap.GetOrCreateOverlay)会接收到完整的 design-token 级联,此外还有:

  • SusApp.UseCustomStyles(...)(品牌化)和 UseFonts(...) 会像应用到屏幕根一样应用到 OverlayHost,因此 popup 会匹配你的品牌颜色和字体。
  • SusThemeService.Instance.SetTheme(root, theme) 也会在 OverlayHost 以及每一个当前打开的 子 overlay 上切换 .theme-* class,因此被重新挂载(reparented)的 popup 会以当前主题 重新绘制。

通常你什么都不需要做——通过 SusApp 挂载,overlay 会自动继承 token + 主题 + 品牌化。 手动搭建 bootstrap 时,必须在构建 overlay 之后调用 SetTheme,它才能应用非默认主题。

相关内容(Core)

部分状态备注
开发者控制台SusConsoleService已发布OverlayCategory.Console
SusToast / toast 组件计划中等你实现它时使用 OverlayCategory.Toast / SusToastBase
World-space 血条 / 姓名牌下游 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;
}