25 KiB
Modal 桌面版实现原理
本文档描述 Dialog 和 MessageBox 的当前内部实现、状态所有权、组合结构、资源边界和释放规则。公共契约见 Modal 桌面版架构设计,关闭动效编排见 Modal Dialog 关闭动效设计,宿主尺寸算法见 Modal 宿主尺寸与 Resize 设计,内容区弹层叠放见 Modal 内容弹层叠放设计,Token 语义见 Modal Token 设计。
1. 实现定位
实现由一个 Dialog 打开意图、一个当前 DialogSession、两种 IDialogPresenter 和一个共享 DialogSurface 组成。私有方法和具体 AXAML selector 仍以源码为准;本文只记录稳定职责和维护不变量。
2. 源码文件结构
| 路径 | 职责 |
|---|---|
Dialog.cs |
public 属性、事件、内容、按钮和内部协作入口。 |
Dialog.Lifecycle.cs |
IsOpen reconcile、OpenAsync、事件通知和 presenter 选择。 |
Dialog.StaticAPI.cs |
静态 modeless/modal 异步创建入口。 |
DialogSession.cs |
单次展示状态机、关闭仲裁、取消、结果、焦点和 teardown。 |
IDialogPresenter.cs |
Overlay/Window 共用的最小异步协议。 |
DialogSurface.cs |
标题、内容、Footer、按钮和 Overlay resize 的共享表面;负责 PART_SurfaceContentLayer 的 template part 生命周期。 |
ButtonBox/DialogButtonBox.cs |
标准按钮生成、唯一有效按钮序列和自定义集合同步。 |
OverlayHost/DialogOverlayLayer.cs |
解析 owning TopLevel 的 Avalonia OverlayLayer 或局部 scope fallback,并管理 owner scope 内的 presenter stack。 |
OverlayHost/OverlayDialogPresenter.cs |
同时拥有 mask、Surface、placement、drag/resize 和 Overlay close motion choreography。 |
WindowHost/WindowDialogPresenter.cs |
原生 Window 属性映射、modal owner、尺寸、位置和生命周期。 |
WindowHost/DialogWindow.cs |
原生 caption close 仲裁和显式尺寸应用。 |
MessageBox/MessageBox.cs |
Dialog 派生的消息语义、静态 API 和按钮配置。 |
MessageBox/MessageBoxContent.cs |
MessageBox 的图标与内容组合。 |
Dialog/Themes / MessageBox/Themes |
共享 Surface、Overlay presenter 和 MessageBox AXAML 结构。 |
src/AtomUI.Core/MotionScene/AbstractMotion.cs |
共享 Motion 的 transition completion boundary;等待全部 transition 或安全超时后才报告完成。 |
对应回归测试位于 tests/AtomUI.Desktop.Controls.Tests/Dialog 和 tests/AtomUI.Desktop.Controls.Tests/MessageBox。
3. 核心类职责
Dialog只拥有最新IsOpen意图和当前 Session 引用,不拥有 presenter 视觉状态。DialogSession独占一次展示的状态、取消源、普通关闭策略、结果、owner/target 订阅、前一焦点和完成任务。IDialogPresenter只暴露ShowAsync、CloseAsync、FocusScope、CloseRequested和IAsyncDisposable。DialogSurface是两种 presenter 的唯一内容/按钮视觉源。DialogButtonBox的_effectiveButtons是唯一视觉按钮序列。标准按钮来自私有元数据,自定义按钮仍由用户集合拥有。DialogOverlayLayer解析当前 owner 的视觉宿主,并管理同一 owner scope 内的 presenter 顺序、可用尺寸和栈顶键盘路由。OverlayDialogPresenter是 Dialog layer 的直接子节点;mask 与 Surface 不拆成独立 popup。WindowDialogPresenter拥有一个DialogWindow,并将同一个DialogSurface作为原生 Window 内容。MessageBox不拥有隐藏 Dialog;它覆盖 Surface 内容和按钮配置 hook。
4. 状态与数据流
IsOpen / OpenAsync
-> Dialog reconcile
-> create DialogSession + concrete presenter
-> presenter.ShowAsync
-> focus DialogSurface
-> Dialog.Opened
-> one close request
-> Closing / BeforeCloseAsync
-> commit result and outcome events
-> presenter.CloseAsync + DisposeAsync
-> restore focus
-> Closed and task completion
Session 状态为 Created -> Opening -> Open -> ClosePending -> Closing -> Closed。
- 普通关闭只在
Open接受,并在执行Closing/BeforeCloseAsync前进入ClosePending。 - veto 或关闭策略异常返回
Open;声明式IsOpen=false被 veto 时恢复为true。 - 强制关闭取消 pending policy 与 opening motion,从可关闭状态直接进入
Closing;它仍触发Closing,但忽略Cancel、跳过BeforeCloseAsync,并忽略IsConfirmLoading。 - 一旦结果提交,后续事件或 presenter 异常不允许把 Session 恢复为 Open。
- completion 只有在 presenter close/dispose、焦点恢复和 Session
Closed后才完成、取消或 fault。
Dialog 的 reconcile 负责区分实例 OpenAsync 与声明式重开:实例在活跃 Session 期间拒绝并发打开;声明式 IsOpen=true 可等待当前 Closing 完成后重新创建 Session。
OpenAsync、静态 Dialog/MessageBox 创建入口以及 Accept/Reject/Done 在读取视觉树或推进 Session 前统一切回 UI Dispatcher;泛型静态入口的 TView 也只在 UI Dispatcher 上实例化。DialogSession 不承担跨线程状态同步,调用方不需要通过空 Dispatcher 调度或延迟来避开 routed event 时序。
5. 组合结构模型
flowchart TD
D["Dialog / MessageBox"] --> S["DialogSession"]
S --> P{"IDialogPresenter"}
P --> OP["OverlayDialogPresenter"]
P --> WP["WindowDialogPresenter"]
OP --> DL["DialogOverlayLayer"]
OP --> MA["Mask MotionActor"]
OP --> SA["Surface MotionActor"]
WP --> W["DialogWindow"]
SA --> DS["DialogSurface"]
W --> DS
DS --> DSCL["PART_SurfaceContentLayer"]
DSCL --> H["PART_Header"]
DSCL --> C["Content / MessageBoxContent"]
DSCL --> B["PART_ButtonBox"]
B --> EB["Effective button sequence"]
| 节点 | 来源 | 稳定性 | Agent 使用边界 |
|---|---|---|---|
Dialog / MessageBox |
public control | public-stable | 用户可直接使用。 |
DialogSession |
runtime C# | internal-observable | 只用于理解状态与任务边界。 |
OverlayDialogPresenter / WindowDialogPresenter |
runtime C# | internal-observable | 只用于维护宿主一致性。 |
DialogSurface |
runtime + DialogSurfaceTheme.axaml |
internal-observable | 两种宿主必须共享,不建议用户直接创建。 |
PART_Header, PART_ButtonBox, PART_Resizer |
DialogSurfaceTheme.axaml |
template-stable | 变更需同步实现、主题、测试和文档。 |
PART_MaskMotionActor, PART_SurfaceMotionActor |
OverlayDialogPresenterTheme.axaml |
template-stable | 变更需保持 mask/Surface 同一 presenter。 |
PART_SurfaceContentLayer |
DialogSurfaceTheme.axaml |
internal template collaboration | 关闭时承载前景 opacity;重套模板时由 DialogSurface 释放旧引用。 |
DialogOverlayLayer |
runtime C# | internal-observable | 只管理 scope 内栈,不提供全局 service。 |
Avalonia OverlayLayer / PopupOverlayLayer |
runtime infrastructure | internal-observable | 分别承载 Dialog presentation 与内容 popup,保留中间 light-dismiss 层。 |
Window 和 Overlay 各自拥有一个 Surface 实例,不共享同一个视觉对象;“共享 Surface”指共享类型、主题与行为实现。
6. 生命周期与模板接入
| 获取 | Owner | 释放 |
|---|---|---|
presenter CloseRequested |
DialogSession |
BeginClosing |
| placement target detach / owner closed | DialogSession |
BeginClosing |
| pending close CancellationTokenSource | DialogSession |
veto、commit 或 forced close |
| presenter opening cancellation | concrete presenter | CloseAsync / DisposeAsync |
| Dialog property bindings | DialogSurface / presenter |
Dispose / DisposeAsync |
| custom button Click handlers | DialogButtonBox |
collection change、template release、Surface dispose |
| Overlay mask/header/resize handlers 与 pointer capture | OverlayDialogPresenter / OverlayDialogResizer |
release、capture lost、re-template 或 DisposeAsync |
| Window events and property bindings | WindowDialogPresenter |
DisposeAsync |
| Dialog-to-Window resource bridge | WindowDialogPresenter |
从 Window resources 移除并在 DisposeAsync 退订 |
| Surface composition child links | concrete presenter | Overlay 在退出 motion 后、移除 layer 前断开;Window 在原生关闭后于 dispose 中断开 |
| inheritance/resource parent | presenter | 从 layer/window 移除后清空 |
| MessageBox default button content cache | MessageBox 当前 Surface |
ReleaseSurfaceButtons |
DialogSurface.OnApplyTemplate 先释放旧 Header/ButtonBox/Resizer 订阅,再接入新 parts。DialogButtonBox 在 template 为空或重套用时立即清空旧 panel 和视觉父级。
Overlay presenter 在外层 Surface、内容层和 modal mask 的关闭任务全部完成后,先断开 DialogSurface 子树的 composition children,再释放 Surface 并移除 Overlay layer;空的 DialogOverlayLayer 随后从实际 host 删除并解绑 size 事件。Window presenter 先等待原生 Window 关闭,再在 dispose 中断开 composition children、释放 Surface、bindings、资源 bridge 和 Window.Content。两条路径都防止调用方保留 Content/CustomButton 等子控件时,其旧 CompositionVisual.Parent 链反向保留 Presenter 和 Surface;原生关闭或后续释放抛出时仍继续 teardown,最后传播首个异常。
7. 交互与事件处理
- DialogSurface 把标准/自定义按钮点击转成
DialogPresenterCloseRequestedEventArgs,Session 决定是否关闭。 - Enter/Escape 由栈顶 Overlay presenter 或当前 Window 转发给 Surface 的有效按钮序列。
- modal mask 只在左键、真实 mask visual subtree、且 presenter 为栈顶时请求关闭;
IsMaskClosable=false时该输入被吞掉且不发起任何关闭请求。该门控只存在于 Overlay presenter 的 mask 输入路径,不复制到 mask 控件或 Session veto 层。 - modeless presenter 在 Surface 外不阻断 pointer hit-test;点击 Surface 会把整个 presenter 激活到栈顶。
- Overlay 标题栏拖动通过复用的 render-only translation 更新 Surface,并同步 Dialog offset 作为持久化状态;resize 修改 offset 或 Surface 正文尺寸。resize handle 按下后捕获 pointer,即使指针离开细小 handle,release/capture lost 仍会清理 origin 与 dragging state。Surface 正文受 Window visible frame 与有效 drawn frame thickness 共同约束,BoxShadow 不参与定位。
- Window caption close 先请求 Session 关闭;Session commit 后
DialogWindowCloseState.Closing放行一次真实 native close。OwnerWindowClosing始终放行,随后由 owner closed 强制 teardown。 - Window presenter 禁用 Surface 内隐藏 header 的
TitleIcon,由原生 Window 标题栏唯一承载该图标,避免两个 presenter 争用同一个PathIconvisual parent。 - Session 在 Show 前记录当前焦点,Show 后聚焦可聚焦的 DialogSurface,Close 后优先恢复原焦点,其次恢复 placement target。
8. 内部算法与关键流程
8.1 有效按钮序列
DialogButtonBox 使用一张私有标准按钮定义表创建标准按钮,再与 CustomButtons 组合成 _effectiveButtons。每次 Add/Remove/Replace/Move/Reset/Clear 都重建视觉分组并对称同步自定义按钮 Click 订阅。Surface 对有效序列取快照,用于键盘查找、confirm loading binding 和 MessageBox 配置。
标准按钮默认文案使用 Template priority binding;MessageBox 的显式 OK/Cancel 文案使用 local value 覆盖,清空后自动恢复最新语言资源。MessageBox 的样式、默认按钮和启动位置也只写入 Template priority,调用方 local 配置始终优先。MessageBox 语义配置运行时变化后重新执行现有 ButtonsConfigure,维持调用方配置最后生效的顺序。
8.2 自然尺寸与启动定位
- Overlay 将
HostWidth/Height=NaN保留为 auto,先应用 min/max,再测量 Surface 的DesiredSize并计算 placement。 - Window 在 native
Show()前应用 managed Window/Surface styling 和 template,并测量 Window tree,使ContentPresenter附加DialogSurface。Presenter 将自然测量、结构性约束、chrome、owner screen working area 与 render scaling 一次性解析为最终 ClientSize 和初始 placement;首个原生可见帧直接使用这份几何快照,不使用 opacity staging、Dispatcher 延迟或 post-show reposition。打开后只跟踪实际 native resize/state 与相关 capacity/chrome 变化;运行时 finite 值通过DialogWindow.ApplyRequestedSize更新 ClientSize。 - 不存在固定
520x240fallback 或额外像素补偿。
Dialog 的水平和垂直 startup anchor 注册默认值均为 Center,与 DialogOptions 和静态 API 保持一致。显式选择 Custom 时,对应轴由 HorizontalOffset / VerticalOffset 解析;未提供 offset 时该轴从 owner bounds 的起点开始。显式 Left / Right / Top / Bottom 的 anchor 计算保持独立。
自然尺寸只提供初始 preferred size,不参与结构性 minimum。用户交互 resize 形成的 actual size 由 presenter 持有,不反向写入 HostWidth/Height。
8.3 结构性最小尺寸与 Resize
DialogSurface 根据当前 Header、Footer、有效按钮和 DialogToken.MinWidth/MinHeight 正文 viewport 基线形成 structural minimum。presenter 将 structural minimum、requested HostMin/Max 与 host capacity 交给共享纯值解析职责,得到 normal 状态的 effective constraints。
Overlay 直接把 effective constraints 应用于 Surface,并在 resize handler 中只读取缓存约束;handle 捕获 pointer,release 与 capture lost 复用同一个幂等结束路径。自然轴被新约束 clamp 后会提交为 presenter-owned actual geometry,后续放宽约束不回落。Window presenter 按当前 Template 模式把 Surface constraints 加回 chrome:managed/drawn title bar 使用 padding、frame shadow 与有效标题栏高度,CSD 使用 padding 与 WindowDecorationMargin;CSD、visible frame、screen、DPI 与相关 frame metrics 变化都会重新解析。Window 的 CanMaximize 由 IsMaximizable 与两个 HostMax* 组合得出,任一 finite maximum 都禁用 native maximize。Surface 的 measure invalidation 会重新比较 structural minimum,只有数值改变才通知 presenter。完整公式、maximize/restore 和 capacity 退化规则见 Modal 宿主尺寸与 Resize 设计。
Overlay 首次 placement 完成前收到 StructuralMinimumChanged 时,只重新解析 constraints 和 requested size,不 clamp 尚未解析的 Surface position,也不向 OffsetX / OffsetY 写回位置差值。首次 placement 完成后,同一路径才允许 clamp actual geometry 并同步 offsets;这样 template 初始布局不会把临时 (0, 0) 坐标固化为相对居中位置的负偏移。
8.4 Motion
Overlay 的 mask 和 Surface motion 并行等待;关闭时 OverlayDialogPresenter 还为 PART_SurfaceContentLayer 创建同 duration 的线性 opacity animation,并使用 Task.WhenAll 聚合 Surface、内容层和 mask 的任务。全部任务完成后才断开 composition children、Dispose Surface 和移除 presenter。AbstractMotion 对一个 Motion 内部的多个 transition 使用 Task.WhenAll 作为正常完成条件,并保留最长 duration 加安全余量的 timeout;不能因首个 transition 完成就提前报告 Motion 完成。Window host 不创建 Surface MotionActor:它在 native Show() 前完成初始尺寸和 placement,首个可见帧直接使用已解析的几何。DialogWindow.Opened 和 DialogWindow.Closed 是 Window presenter 的原生生命周期边界;关闭流程等待原生 Window 关闭和资源清理,不依赖 Surface close motion。Overlay 的 opening/closing motion 仍由其 presenter 等待,duration 来自 Dialog scope 的 MotionDurationMid。详细 choreography 见 Modal Dialog 关闭动效设计。
8.5 Overlay 宿主与窗口几何
DialogOverlayLayer 优先解析 placement target 所属 TopLevel 的 Avalonia OverlayLayer;该层高于普通 Window content,低于 LightDismissOverlayLayer 与 PopupOverlayLayer。无可用 TopLevel overlay 时,使用 placement target 所在的 ScopeAwareOverlayLayer fallback。TopLevel host 的 AvailableSize 以 TopLevel.ClientSize 为真源;局部 scope 使用 scope layer 的可用尺寸。最后一个 presenter 移除后,Dialog layer 从实际 host 删除,并释放 host size 与 TopLevel size 订阅。
WindowDrawnDecorations overlay 是 TopLevelHost 中与 Window 同级的 chrome 视觉,不参与 Dialog 宿主解析。modal presenter 活跃时获取 Window chrome suppression lease;多个 Dialog 或 Drawer 通过引用计数共享可见性 owner,最后一个 lease 释放后恢复 drawn chrome。
OverlayDialogPresenter 分离三套几何:
| 几何 | 真源 | 用途 |
|---|---|---|
| mask bounds | 完整 DialogOverlayLayer.AvailableSize |
mask、mask motion 和 modal pointer 阻断。 |
| Window visible frame | 完整 layer 按 FrameShadowThickness 内缩 |
Window 统一外轮廓和透明 shadow buffer。 |
| Dialog body owner bounds | visible frame 按当前有效 drawn FrameThickness 内缩 |
Surface 正文测量、placement、drag、resize、maximize 和 restore。 |
| Dialog BoxShadow extents | DialogSurface 主题绘制 |
仅绘制,不参与正文尺寸和位置约束。 |
AtomUI Window 的 visible frame 统一复用 WindowVisualLayerClip 计算,完整 layer bounds 先按 FrameShadowThickness 内缩。Dialog 正文 owner bounds 再读取当前 drawn decorations 已按 render scaling 取整的有效 FrameThickness;该值通过现有集中反射兼容边界按能力获取,并由 WindowDecorationMargin/frame shadow 几何变化驱动缓存刷新,不能在拖动热路径反射,也不能按 OS 硬编码。
HostWidth / HostHeight、HostMin* 和 HostMax* 均描述 Surface 正文。普通 TopLevel 和局部 scope 没有 drawn decorations frame 契约,frame thickness 为零。Dialog maximize 继续使用同一正文 owner-bounds 算法;owner Window 自身进入 maximized/fullscreen 后,Avalonia 会把其有效 drawn frame thickness 发布为零。
macOS 原生 caption chrome 位于 Avalonia 客户端 visual tree 外,Overlay 无法对该系统区域绘制 mask;popup overlay 仍覆盖完整 Avalonia client layer,Surface 使用同一 visible-frame 规则。客户端之外的原生 chrome 边界不通过额外原生窗口或第二套 mask 模拟。
mask 始终使用完整 layer bounds,不复用 owner bounds。Window theme 中的 WindowVisualLayerClip 是窗口 frame shadow 和 CornerRadius 的统一外轮廓裁剪者;Presenter 不为 mask 复制 margin、圆角或第二套 clip。Window resize、ClientSize、frame shadow、drawn frame thickness 和 Window state 变化后,layer 与 presenter 重新解析上述几何。
Dialog 内容区内的 popup 沿 placement target 解析同一 Window TopLevel,再由 Avalonia 选择该 manager 的 PopupOverlayLayer 或原生 Popup host。Dialog 所在 OverlayLayer、LightDismissOverlayLayer 与 PopupOverlayLayer 使用 Avalonia 固定层序,因此内容弹层位于 presenter 之上且外点关闭有效;AtomUI 不创建嵌套 layer manager,也不接管 Popup host 生命周期。完整契约见 Modal 内容弹层叠放设计。
9. 资源、性能与 AOT 边界
- Overlay presenter 在 Dialog 已附加时以 Dialog 为 inheritance parent,否则以 placement target 为 parent。
- Window 保留 DialogSurface 到 Window
ContentPresenter的正常 styling parent 链,避免在未附加树中提前实例化的嵌套控件失去 ControlTheme。Dialog/owner 资源由 presenter-ownedDialogResourceBridge转发到 Window resources;bridge 对称转发ResourcesChanged,并在DisposeAsync中移除和退订。 - runtime binding 只用于动态 presenter/Surface/按钮关系,并由 owning presenter、Surface 或 ButtonBox 对称释放。
- Presenter 为 Surface 复用单一
MatrixTransform作为位置 owner。拖动PointerMoved只更新 Matrix translation 并同步不触发布局的Dialog.OffsetX/Y;位置先按 DPI 取整,再二次 clamp 到 body owner bounds,避免取整重新越界。 - drawn decorations 反射兼容边界只读取 frame/titlebar 几何;Modal 不反射发现业务 host,也不新增 trimming root。实现不使用反射修改 TemplatedParent,不扫描程序集发现 Dialog API,不使用同步 DispatcherFrame。
- Session、Presenter、Surface 和 Content 的关闭回收由 Overlay/Window WeakReference 测试覆盖。
- 状态机、按钮表和 presenter 选择都是静态类型路径,保持 NativeAOT 友好。
10. 维护不变量
Dialog打开意图与一个当前 Session 是唯一生命周期 owner。- 所有关闭来源最终执行同一个
CompleteCloseAsyncteardown。 - 普通 veto 发生在结果提交前;结果提交后只允许完成 teardown 和传播异常。
- Overlay 与 Window 的
ShowAsync/CloseAsync都等待真实 presentation 边界。 - mask 与 Surface 必须保留在同一个 Overlay presenter 中。
- Overlay 关闭时 Surface 外层、
PART_SurfaceContentLayer和 modal mask 的任务必须由同一个 presenter 聚合;所有任务完成前不得断开 composition children、Dispose Surface 或移除 presenter。 AbstractMotion只能在全部 transition 完成或安全 timeout 后报告 Motion 完成;不能按首个 transition 的完成通知 teardown。PART_SurfaceContentLayer是可选内部协作节点;缺失时仅退化为外层 motion,不能阻断基本关闭流程。动画期间不得改变 Surface Bounds、布局或 visual parent。- 所有平台的 Overlay presenter 必须保留在 owning
TopLevel的OverlayLayer;drawn decorations overlay 只绘制 chrome,不能承载业务 presentation。 - Dialog 内容、Popup placement target 与 owning Window 必须解析到同一
TopLevel;Popup 使用更高的 Avalonia popup layer,并保留中间 light-dismiss 层。 - mask bounds、Window visible frame、Dialog body owner bounds 和 Dialog BoxShadow extents 必须保持独立。mask 覆盖完整 layer;所有平台的 Surface 正文都可进入 managed/drawn 标题栏但不能覆盖有效 frame;BoxShadow 允许由 Window visual-layer clip 在外轮廓处裁剪。
- Dialog 与 Drawer 共享 Window chrome suppression 的引用计数 owner,但不共享 layer、容器或 presentation 生命周期状态。
- Surface structural minimum、requested Host constraints 和 host capacity 必须由同一纯值规则解析;Overlay 与 Window 不能分别定义默认最小尺寸语义。
- Window presenter 只能在 Surface constraints 解析完成后加回 Window chrome;live resize 热路径不能重新测量结构区域。
- MessageBox 继续作为 Dialog 派生类,不增加平行 host/session/button cache 生命周期。
- mask 外点关闭入口只由
IsMaskClosable在 Overlay presenter 的 mask 输入路径统一门控;不引入第二条 mask 关闭路径,也不在 Session veto 层复制该判断。 - 新增 binding、事件、资源 parent、motion source 或内容引用时,必须在同一个 owner 中增加释放点和回归测试。
11. 测试与验证
重点测试:
DialogSessionTests: 状态转换、veto、forced close、异常和 presenter failure。DialogLifecycleTests: 实例/声明式打开、取消、detach、重开、嵌套焦点和 WeakReference。OverlayDialogPresenterTests/DialogContentPopupLayeringTests: mask ownership、modal/modeless 输入、栈顶路由、IsMaskClosable门控、TopLevel ownership、popup/light-dismiss、chrome suppression 引用计数、完整 mask bounds、平台 body bounds、结构性最小尺寸、拖动 resize、capacity 退化、maximize/restore 和 motion。WindowDialogPresenterTests: Opened/Closed 原生生命周期、首帧几何、原生关闭、owner close、自然尺寸、Surface/chrome constraints 换算、native resize、placement 和资源 parent。DialogButtonBoxTests/DialogSurfaceTests: 有效按钮集合、template 生命周期、内容和配置。- MessageBox tests: 派生结构、语义样式、motion anchor、重入和按钮引用释放。
DialogPopupPrimitiveLayeringTests、DialogPopupControlFamilyTests、DataGridFilterDialogPopupTests与PopupEntryInventoryTests分别覆盖原语、控件家族、DataGrid 和入口库存契约;真实窗口证据按平台独立记录。
最终 owning TopLevel/Overlay/Popup 分层已在 Windows CSD、macOS 原生 chrome,以及 Ubuntu GNOME Wayland 环境实机测试; Wayland 证据仅覆盖 Dialog 内容 Popup 真实窗口人工回归。Linux X11 尚未测试。Headless 测试只能证明 共享 managed 不变量,不能替代缺失平台的实机证据。
迭代先运行 Dialog/MessageBox filter,再运行完整 AtomUI.Desktop.Controls.Tests、Gallery tests/build、NativeAOT publish 和 git diff --check。