mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-16 21:21:03 +08:00
perf(Core): 以 BootstrapRegistration 注解优化两阶段注册并完善失败清理
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<CompilerVisibleProperty Include="AtomUIRegistrationPackageId" />
|
||||
<CompilerVisibleProperty Include="AtomUICriticalControls" />
|
||||
<CompilerVisibleProperty Include="AtomUIThemeAssetProjectDirectory" />
|
||||
<CompilerVisibleProperty Include="AtomUIThemeControlCatalog" />
|
||||
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="Link" />
|
||||
|
||||
@@ -28,8 +28,7 @@ public partial class GalleryApplication : BaseGalleryApplication
|
||||
[LanguageTags.EnUS, LanguageTags.ZhCN, LanguageTags.ZhTW, LanguageTags.PtBR]);
|
||||
builder.WithInitialTheme(IThemeManager.DEFAULT_THEME_ID);
|
||||
builder.UseAlibabaSansFont();
|
||||
// Stage 1:Desktop 包按 AtomUICriticalControls 关键集分区注册(chrome 闭包先行,
|
||||
// 余量 fragments 由 Stage 2 注册窗口收集——生成器分区发射,见 Desktop.Controls.csproj)。
|
||||
// Stage 1:注册 Desktop 的 BootstrapRegistration 片段;其余在 Stage 2 补齐。
|
||||
builder.UseDesktopControls();
|
||||
// Stage 1:GalleryBase 含 GalleryWindowTitleBar(窗口 chrome),必须先行。
|
||||
builder.UseGalleryBase(AtomUIGalleryModule.Configure);
|
||||
|
||||
@@ -218,6 +218,7 @@ full/trimmed 等价性以相同宿主输入契约得到满足为前提;未使
|
||||
|
||||
隐藏资源记录包含 AssetId、Phase、Order、factory。各包只暂存记录;全部包收集后统一校验,再创建和挂载。
|
||||
跨包按真实 `PackageCommitOrdinal`,包内按 `(Phase, Order, AssetId)` 固定排序。
|
||||
Bootstrap 补齐替换原包快照并保留 provider/ordinal;partial 包无完整 SchemaFingerprint。详见[注册分期](lazy-control-registration.md)。
|
||||
|
||||
Order 来自正常包资源契约和显式包含次序,不能来自 TypeMap 查询次序、candidate key 字母序、程序集扫描次序或所选控件数量。
|
||||
嵌套 MergedDictionaries 的声明次序保存在 factory 内。默认主题 export key 冲突必须有明确覆盖契约,否则构建诊断。
|
||||
@@ -240,18 +241,18 @@ Order 来自正常包资源契约和显式包含次序,不能来自 TypeMap
|
||||
[惰性物化与资源查询](../systems/theming/lazy-materialization.md)拥有;统一提交由
|
||||
[分级启动演化](staged-startup-evolution.md)拥有。
|
||||
|
||||
- 每代 registry 的 Revision 证据必须覆盖该代实际贡献集合。空包集合、全指纹非分区包、分区包以及混合
|
||||
- 每代 registry 的 Revision 证据必须覆盖该代实际贡献集合。空包集合、全指纹包以及混合
|
||||
手写包均适用;清除 request 去重键不能代替更新 schema Revision 和快照缓存身份。
|
||||
- 当证据无法描述新增贡献时,先回退实际 schema 的完整计算;不得继续使用 Stage 1 的包列表生成 Stage 2 Revision。
|
||||
- 分区指纹方案需分别表达关键集和最终集合,并保留裁剪/平台选中集合的正确性。完整收集后的非 staged 路径
|
||||
不应因为包支持分区就永久失去安全的组合快路径。
|
||||
- 包完整收集后提供完整指纹;真实裁剪路径根据实际 schema 计算,保留裁剪/平台选中集合的正确性。
|
||||
- 数字 slot 只在单代 registry 内稳定,跨代共享必须校验 exact identity、CLR type、schema 与计算输入。
|
||||
- 同一资产跨片段/跨阶段只挂一次;新段按完整排序键合并,且静态层相对根 Token provider 的优先级保持不变。
|
||||
- 分区 ABI 使用独立方法名以保留既有位置 lambda 的源码重载解析;旧 generated package 的支持边界必须包含
|
||||
- 同一资产跨片段/跨阶段只挂一次;新增资源按完整排序键合并,且静态层相对根 Token provider 的优先级保持不变。
|
||||
- 旧分区 ABI 保留签名,但两段在同次注册中完整收集,失败不发布半包;新生成代码使用 RegisterPackage,阶段选择在片段 Add 前执行。
|
||||
旧 generated package 的支持边界必须包含
|
||||
实际 descriptor/fingerprint 协议版本,不能仅根据方法仍存在就宣称所有旧二进制免重编。
|
||||
|
||||
构建期紧凑完整 schema/稳定身份表是后续候选,不是当前契约的既成实现;采用前必须证明没有把全部运行时
|
||||
descriptor 构造重新搬回首帧,也没有破坏官方 TypeMap/条件后端的精细选择。
|
||||
注册优化优先复用现有 descriptor 和不可变集合。最终保留集合与 registry slot 由运行期实际组合确定;
|
||||
不得用包级全控件 Type/factory 表破坏官方 TypeMap/条件后端的精细选择。
|
||||
|
||||
兼容边界(2026-10-12 验证):本次资源键工厂是增量 ABI;同 schema 协议的消费端没有该工厂时,生成器回退
|
||||
旧四参构造函数。它不扩展为任意历史 Generator/Core 混用承诺。以当前生成器直接引用已发布 Core 6.2.4
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# 控件注册分期与按需创建
|
||||
|
||||
状态:BootstrapRegistration 已实现;测量、测试及发布验证范围见[实施记录](../../superpowers/progress/2026-10-14-bootstrap-registration.md)。
|
||||
|
||||
## 1. 目标与决策
|
||||
|
||||
普通 Rider / dotnet run 与发布应用都需要缩短首帧前的注册工作。完整注册基线把普通 JIT 首 Render 从
|
||||
849.92 ms 增至 986.47 ms(同一临时骨架程序、中位数);Ready 基本相当。两项 cache getter 实验均未消除这项成本。
|
||||
采用源码 `[BootstrapRegistration]` 声明首阶段需要的少量控件/资源,让源生成器输出阶段事实。
|
||||
应用继续使用 `UseDesktopControls()` 和既有 `Deferred`,无需枚举或手工控件名称配置。
|
||||
|
||||
主题字典与控件快照仍按实际使用物化;Token 派生算法仍在运行时执行。本模块只调节注册片段的执行阶段。
|
||||
|
||||
## 2. 分期契约
|
||||
|
||||
- `AtomUI.Registration.BootstrapRegistrationAttribute` 标注本包控件或导出主题的资源类,非继承、无参数。没有标记的包维持完整注册;有标记的包仅把对应片段作为 bootstrap。无贡献的标记是构建错误,避免静默无效。
|
||||
- 复用现有资产关系补齐同包 Token owner、语义 owner 及不可分割字典的导出目标。C# 创建的 chrome helper 必须就地标记;不分析应用调用图。跨包 owner 仍由实际 registry 验证。
|
||||
- 隐藏生成 ABI `ControlRegistrationFragmentAttribute.IsBootstrap` 为 virtual bool,默认 true,保持既有片段行为;支持分期的生成片段覆写为编译期常量。net10 非裁剪路径统一经过 `AddFragment`,net8 的 `CollectFull` 精确 IL 模板和 TypeMap 条件不变。标记不增加裁剪根。
|
||||
- 初始 collection 在片段 Add 前保留非 bootstrap 片段;不会触碰其 descriptor 或资源工厂。完成初始 collection 时发布不可变包快照,pending 只属于该包。
|
||||
- 通过 `Build(staged: false)` 同步补齐;staged 则在 Stage 2 成功封存前补齐。补齐复制已选元数据并执行一次 pending 片段,替换原包槽位,不追加第二份包或 segment 注册表。package/provider/commit ordinal 不变,服务与 complete callback 不重跑。
|
||||
- partial 包的 SchemaFingerprint 为 null;仅完整非裁剪包使用原生成指纹,裁剪路径继续按实际贡献计算。
|
||||
- 同一 merge 路径按旧/新不可变包快照的差集注入新增 descriptor;资源按现有完整排序键归并,并沿用原子 theme/language evolution。补齐失败使 builder 永久失败;取消及所有终态清空 pending,旧可见主题保持有效。
|
||||
- 只允许在初始配置保留 pending;第二阶段新注册的包完整收集。晚期写入依然拒绝。
|
||||
|
||||
## 3. 维护与复杂度
|
||||
|
||||
标记写在声明旁,使用真实 Symbol 身份,支持 public/internal/nested 类。纯 Avalonia 主题目标通过本地资源类声明,
|
||||
不修改第三方类型。修改 chrome 的 C# 组合关系时检查相应内部控件;已有资产 owner/export 关系在生成期闭合,
|
||||
以队列与已访问集合保证每个片段/资产最多处理一次。此闭合只选择阶段,不新增片段或 linker 保留边。
|
||||
|
||||
无独立主题、Token 或 Semantic 内容的帮助类型不需要标记;错误标记由 ATOMUIREG010 阻止。
|
||||
默认普通包完整注册;支持分期的包必须提供完整可用的 bootstrap 表面,不能在 Stage 1 使用未收集控件。
|
||||
`Deferred` 是应用承诺延后业务内容的边界;初始化成功、失败、取消沿用 IAtomUIStartup。
|
||||
|
||||
## 4. 资源与生命周期
|
||||
|
||||
partial 和 full 共用既有不可变 descriptor,provider 与 package ordinal 稳定;两阶段按同一
|
||||
(Phase, Order, AssetId) 顺序挂载,已物化资源不重新构造。跨包重复、owner 身份与 schema 验证保持原强度。
|
||||
未知资源键保持有序回退,不能因静态索引缺失而被排除。
|
||||
|
||||
pending collection 只被配置 builder 持有,成功补齐前取走 continuation;失败禁止再次使用 builder,
|
||||
关闭注册窗口清空所有 continuation。ApplicationScope 的各终态清除 hooks/actions,不建立新的静态缓存或 owner 订阅。
|
||||
准备阶段不发布有效资源,仍由 StageTwoEvolutionPlan 原子交换主题与语言;关闭/取消后禁止提交。
|
||||
|
||||
## 5. 验证要求
|
||||
|
||||
- 临时测试证明非 bootstrap 工厂未执行,以及同步/分期终态等价、完整指纹恢复、重复片段去重、服务仅一次。
|
||||
- 覆盖资源类、第三方 StyledElement、多目标资产、Token owner、无效标记和非继承语义。
|
||||
- net8 固定 collector 模板、net10 TypeMap、NativeAOT 与 Browser 选中片段不增加裁剪根。
|
||||
- 同程序普通 JIT 对比同步段、首 Render、Ready;渲染回调不是物理显示器 presentation 证据。
|
||||
- macOS 红绿灯、应用名、主菜单与主题切换需真实窗口验证。仅构建或测试通过不能替代 UX 证据。
|
||||
@@ -56,7 +56,7 @@ flowchart LR
|
||||
- Builder 按操作能力校验阶段:初始主题、FollowSystem 模板、应用标识、字体、owned services、initializer 和
|
||||
Deferred 声明仅在初始配置窗口可用;包、Resolver、用户目录和本地化声明可在各自注册窗口收集。
|
||||
不支持的晚期变更在入口拒绝,不允许接受后静默失效。
|
||||
- 成功路径收集余量后通过同一 `Freeze` 检查失败或未完成包;失败回调内捕获异常不能解除失败标记。
|
||||
- 成功路径完成后置包注册及 bootstrap 包补齐后通过同一 `Freeze` 检查失败或未完成包;失败回调内捕获异常不能解除失败标记。
|
||||
`CloseStageTwoWindow` 仅负责异常/取消路径的幂等关闭,不替代成功校验。
|
||||
|
||||
### Prepare
|
||||
@@ -131,12 +131,12 @@ single-view 等待已声明 MainView 挂载到 TopLevel。显式 OnExplicitShutd
|
||||
优先复用已经验证的不变部分:全局 schema、全局 Token 计算结果、未变化 Catalog 的语言行、已创建资产与独立控件槽。
|
||||
完整 registry 改变不能简单迫使全局计算重跑,也不能因为“全局值应不变”而错误复用跨代控件 slot。
|
||||
|
||||
分区未完成和真实裁剪路径按实际 schema 计算;非裁剪完整 Collect 提供包完整指纹,余量全部消费后才允许组合。
|
||||
真实裁剪路径按实际 schema 计算;非裁剪完整 Collect 提供包完整指纹。包在注册入口返回前已经完整,不存在余量未消费状态。
|
||||
Stage 2 刷新所有实际包证据,不能继续使用 Stage 1 的包清单。
|
||||
|
||||
## 6. 生命周期与复杂度预算
|
||||
|
||||
- Stage 2 结束后清除 `_deferredRegistrations`、协调 hooks、builder 和段收集输入。应用服务只保留最终运行时状态。
|
||||
- Stage 2 结束后清除 `_deferredRegistrations`、协调 hooks、builder 和后置注册输入。应用服务只保留最终运行时状态。
|
||||
- LocalizationHost 不额外长期保留已被替代的 Stage 1 registry/snapshot 集;活动读取捕获的旧代引用自然存活到读取结束。
|
||||
- 已发布快照的可达性只能由活动 context、调用方合法读取视图、有界 cache 和进行中的提交解释,不能由历史父链保活。
|
||||
- 有序资源旧集与增量优先使用线性 merge;合并已排序包时保留完整排序键与冲突校验,不能把有序分段直接拼接。
|
||||
|
||||
@@ -130,7 +130,7 @@ Popup/Flyout 通过逻辑树自然继承,独立 Window/Dialog/Notification Top
|
||||
|
||||
`UseAtomUI()` 默认把全部注册与初始化在窗口出现前同步完成。对于注册面较大的应用(如 Gallery 的全部
|
||||
可选包),可以通过**分级启动**把"完整 AtomUI chrome 首帧"与"框架完备初始化"拆成两个阶段:窗口出现
|
||||
只等待 chrome 闭包(分区关键集 + 公共语言 + 关键字体)。指定宿主的可见根完成 compositor rendered batch,
|
||||
等待已启用包的 bootstrap 注册、初始主题、公共语言与关键字体;主题字典和控件快照按实际使用创建。指定宿主的可见根完成 compositor rendered batch,
|
||||
且普通前台窗口收到请求的激活事件后,调度剩余注册。
|
||||
这不证明显示器实际呈现。机制是**纯 opt-in**——不调用 `Deferred` 的应用走同步构建路径。
|
||||
|
||||
@@ -139,7 +139,7 @@ this.UseAtomUI(builder =>
|
||||
{
|
||||
builder.UseLanguages(LanguageTags.EnUS, [LanguageTags.EnUS, LanguageTags.ZhCN]);
|
||||
builder.WithInitialTheme(IThemeManager.DEFAULT_THEME_ID);
|
||||
builder.UseDesktopControls(); // Stage 1:分区包按关键集注册(见下节)
|
||||
builder.UseDesktopControls(); // Stage 1:注册 Desktop 的 bootstrap 片段,资源与快照按需创建
|
||||
|
||||
builder.Deferred(() => // Stage 2 注册块(按声明序执行)
|
||||
{
|
||||
@@ -187,32 +187,20 @@ Gallery 构造加载态后等待 `Completion`,成功后将 Shell 构建排入
|
||||
其成本单独测量。框架 Ready 不代表 Shell 已绘制,宿主就绪证据见
|
||||
[就绪、取消与重入](staged-startup-evolution.md#4-就绪取消与重入)。
|
||||
|
||||
### 控件粒度分区
|
||||
### Bootstrap 注册与按需创建
|
||||
|
||||
`builder.UseDesktopControls()` 的包可以由生成器按**关键集**分区:csproj 声明
|
||||
`AtomUICriticalControls`(逗号分隔的控件简单名,如 `Window,WindowTitleBar,Menu,Icon,Popup`;该值通过
|
||||
`.editorconfig` 暴露给生成器,不能使用会被解释为注释起始符的分号),
|
||||
生成器发射 `CollectCritical/CollectDeferred` 两段收集,并调用独立 ABI 入口
|
||||
`RegisterPartitionedPackage` / `EnsurePartitionedPackage`。Stage 1 只执行关键集收集
|
||||
(chrome 闭包先行),余量在 Stage 2 注册窗口由 `RunDeferredCollects` 收集为与包同 id/provider 的
|
||||
「余量段」,经 `MergeStageTwoInto` 并入后重建 registry(v2)并按当前请求重应用主题(控件快照零
|
||||
物化——首查编译,见 runtime.md §7/§8)。
|
||||
控件包通过声明旁的 `[BootstrapRegistration]` 指定首阶段所需的片段。生成器复用既有资源关系补齐本包
|
||||
owner/export;不维护 `.csproj` 字符串名单,也不要求应用传控件枚举。详见[注册分期与按需创建](lazy-control-registration.md)。
|
||||
|
||||
- `AtomUICriticalControls` 已退役,`ATOMUIREG009` 不再产生;net8 与 net10 通过同一片段执行边界分期。
|
||||
- 未标记包保持完整注册;无 `Deferred` 的应用在 Build 内补齐全部片段。
|
||||
- Stage 1 的部分包保持同一 provider 与提交序号;Stage 2 补齐为完整快照并走现有 theme/language 原子演化。
|
||||
- 标记只影响实际选中片段的执行阶段,不改变 AOT 保留关系;无效标记报告 `ATOMUIREG010`。
|
||||
|
||||
- 未声明 `AtomUICriticalControls` 的包不分区,发射保持 6 参调用原样;使用匹配 schema 协议的老生成包在分期应用
|
||||
中回退为整包 `Deferred` 粒度(保留旧入口 ABI,历史指纹协议边界见[注册契约](control-registration-contracts.md#71-分级演化的身份和资源顺序))。**方向是单向的**:分区包(新生成器产出,独立分区入口调用)
|
||||
必须配新 AtomUI.Core(含 `RegisterPartitionedPackage` / `EnsurePartitionedPackage`)——旧 Core 上加载会以 MissingMethodException 失败,同 F2
|
||||
陈旧包先例方向;条件(net8)后端不支持分区,声明了 `AtomUICriticalControls` 的 net8 兼容包会被
|
||||
生成器显式降级为不分区并给出 `ATOMUIREG009` warning;该回退保持完整注册和可构建性。
|
||||
仓库 Desktop 项目仅对兼容 net10.0 的目标声明关键集;net8 构建不传入该配置,直接生成完整单阶段入口,
|
||||
正常多目标构建不产生回退警告。该条件按目标框架兼容性判断,保留现代平台后缀目标的分区配置。
|
||||
- 关键集必须同时覆盖 descriptor、窗口装饰资产、标题栏、菜单和真实平台依赖。仅统计 headless 查询过的
|
||||
`{Window, WindowTitleBar}` token 槽位不能证明完整 chrome 闭包;应联合资产断言和原生首帧视觉/交互证据。
|
||||
- 裁剪路径的等价性由生成器保证:`CandidateKeysCritical ∪ CandidateKeysDeferred` 与未分区的全量
|
||||
`CandidateKeys` 逐项一致。
|
||||
|
||||
### Stage 2 演化与失败语义
|
||||
|
||||
- **主题**:注册窗口(受控重开/再冻结)内完成 Deferred 块 → 冻结本地化输入 → 余量段收集与并入 →
|
||||
- **主题**:注册窗口(受控重开/再冻结)内完成 Deferred 块和原包余量 → 冻结本地化输入 → 完整包贡献并入 →
|
||||
准备 registry、目录、根/局部快照、Semantic registry 和静态资源表。已有 provider/owner 保持稳定。
|
||||
- **本地化**:增量准备 Catalog 行、全部支持语言 Snapshot 和定义,以匹配的 registry/revision 读取视图交换
|
||||
(manager/provider/localizer 与订阅者零身份变化);未被覆盖的既有键值不变,新键即时可查,Stage 2 后切换
|
||||
@@ -225,8 +213,8 @@ Gallery 构造加载态后等待 `Completion`,成功后将 Shell 构建排入
|
||||
### 已知约束
|
||||
|
||||
- 同步模式共用启动结果对象,`UseAtomUI()` 返回时任务已经完成,不引入后台初始化或调度。
|
||||
- 分区包的余量资源按 provider 以 `(Phase, Order, AssetId)` 全局归并落地——合并字典查找自尾向头
|
||||
扫描,资源序不得按段拼接(否则既有资源首查被迫构造余量段全部字典,非分期路径也会变慢)。
|
||||
- 包资源按 `(Phase, Order, AssetId)` 规范顺序挂载。跨包遵循提交顺序,同一资产去重;合并字典查找自尾向头,
|
||||
已知键候选与开放资源回退必须遵循同一优先级。
|
||||
- AOT:分期不引入反射/表达式树;`Deferred` 只存 `Action` 委托。发布门(Gallery staged +
|
||||
StartupPerformance 非 staged 双 AOT 冒烟)为常规验收项。
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ slot 仅在单个 registry revision 内稳定。增加一个按身份排序靠
|
||||
ThemeManager 只挂载一个稳定的 `StaticThemeResourceProvider`。它持有不可变的有序资源表,在演化时交换表引用:
|
||||
|
||||
- 静态层相对 Token provider 的优先级始终不变;重建静态集合不能使普通 Token 查询先遍历整个静态链。
|
||||
- 已存在真实字典不因 Stage 2 再次 detach/attach;包主体和余量段只贡献资源事实,不重复注册宿主。
|
||||
- 已存在真实字典不因 Stage 2 再次 detach/attach;后置包只贡献新的资源事实,不重复挂载已存在的宿主。
|
||||
- 同 AssetId/同 metadata 的资源只出现一次,冲突在 Prepare 失败;已有与增量有序序列线性归并。
|
||||
- 交换准备好的查询表不调用工厂、用户代码或 owner 回调。统一 Publish 阶段只发出一次该资源根的变更通知。
|
||||
- owner 关系由代理统一管理,加载前后语义一致;RemoveOwner 必须清理代理与内层关系。
|
||||
|
||||
@@ -1190,7 +1190,7 @@ Token,也不由某个控件实例创建私有子作用域。当前 `ThemeConte
|
||||
|
||||
不得先挂载空资源层再异步补主题,也不得通过 ThemeLoaded 事件修补 DefaultFont、断点或其他 Token。
|
||||
|
||||
**分级启动(staged)状态**:保持 opt-in。Stage 1 注册关键集与先行包;Stage 2 等待指定宿主的 rendered batch
|
||||
**分级启动(staged)状态**:保持 opt-in。Stage 1 注册先行包的 bootstrap 片段,未声明分期的包完整注册;资源字典与控件快照按需创建;Stage 2 等待指定宿主的 rendered batch
|
||||
和其请求的激活,再捕获/冻结输入、后台构建纯数据。自定义回调保留 UI 线程,静态 provider 不重挂载。
|
||||
主题与本地化先统一 CommitCore,再 Publish;Shell 由应用单独调度。批次完成不等于显示器实际呈现。
|
||||
跨系统职责、失败语义和生命周期统一由
|
||||
|
||||
@@ -90,7 +90,8 @@ dotnet_diagnostic.ATOMUIAOT001.severity = error
|
||||
| `ATOMUIREG006` | Registration | Error | marker ABI/Group 无效、目标框架不受支持或与官方 TypeMap API/实际工具版本不匹配、平台字符串/资源域不兼容,或链接后端不受支持 | 重新构建匹配 ABI 的包/后端并修正平台或链接配置 | Registration generator / TypeMap linker |
|
||||
| `ATOMUIREG007` | Registration | Error | 可达 TypeMap accessor 未正确转换,或 Sweep/输出产物与转换结果不一致 | 启用匹配后端,重新链接并验证生成 accessor/helper | TypeMap linker |
|
||||
| `ATOMUIREG008` | Registration | Error | Native8 发布的隔离输出、SDK/runtime、trim 或 whole-program 输入不满足已验收边界 | 使用受支持的 Native8 发布参数并保持 registration scope 输出隔离 | Native8 build backend |
|
||||
| `ATOMUIREG009` | Registration | Warning | 控件包声明了分级关键集,但当前条件注册后端不支持分区,生成器将该包降级为单阶段完整注册 | 将 `AtomUICriticalControls` 限定到支持官方 TypeMap 的目标框架;net8 不声明关键集,正常执行完整单阶段注册 | Registration generator |
|
||||
| `ATOMUIREG009` | Registration | 已退役 | 历史启动分区后端降级诊断,当前各后端统一片段分期,不再产生 | 删除旧 `AtomUICriticalControls` 配置;编号保留,不复用 | Registration generator |
|
||||
| `ATOMUIREG010` | Registration | Error | BootstrapRegistration 标记未关联实际片段或导出主题资源 | 把标记放在首阶段所需的实际控件或资源类上;无独立贡献的帮助类型不标记 | Registration generator |
|
||||
| `ATOMUIAOT001` | AOT | Warning | 已知 data member path 的 model type,但找不到 generated accessor | 给类型、接口或基类添加 `[GenerateDataMemberAccessors]`,或显式传入 `IDataMemberAccessorDescriptor` | DataMemberAccessors |
|
||||
| `ATOMUIAOT002` | AOT | Warning | model type 有 generated accessor,但目标 path 不会被生成 | 改成可访问实例属性、修正 path,或显式传入 descriptor | DataMemberAccessors |
|
||||
| `ATOMUIAOT003` | AOT | Warning | 使用字符串常量 path,编译期无法确认 model type | 使用 `nameof(Type.Property)`,或显式传入 descriptor | DataMemberAccessors |
|
||||
@@ -140,7 +141,7 @@ dotnet_diagnostic.ATOMUIAOT001.severity = error
|
||||
|
||||
## TypeMap 注册诊断
|
||||
|
||||
`REG` 是当前控件注册领域的诊断前缀。以下语义依次对应已实现的 `ATOMUIREG001`–`ATOMUIREG009`,
|
||||
`REG` 是当前控件注册领域的诊断前缀。以下语义依次对应注册诊断编号(`ATOMUIREG009` 已退役),
|
||||
精确编号见上表;Core 启动时才可判断的冲突使用运行时异常。旧 `LINK` ID 不复用。
|
||||
类型与资源行为以 [控件注册契约](../../architecture/foundations/control-registration-contracts.md)为准,
|
||||
发布 ABI 以 [TypeMap 契约](../../reference/aot/typemap-contract.md)为准。
|
||||
|
||||
@@ -62,6 +62,14 @@ delegate;Group 和共享生成类不能通过静态初始化保留全部控件
|
||||
|
||||
条件 TypeMap 声明预期产生的裁剪诊断只在对应生成代码位置局部处理并说明理由;不得全局关闭 IL2026/IL3050。
|
||||
|
||||
注册 helper 对各后端统一发射单个 `Collect`,不读取首帧关键集配置、不按简单控件名分区。
|
||||
`[BootstrapRegistration]` 读取真实控件或导出资源类 Symbol;仅在存在标记的包内生成逐片段 `IsBootstrap` 常量。
|
||||
既有 asset export、Token owner 与语义 owner 关系在生成期闭合。无标记包使用旧默认完整执行;无贡献标记报告
|
||||
`ATOMUIREG010`。普通和裁剪路径均先经 `AddFragment` 再访问 descriptor 工厂;net8 `CollectFull` 精确模板不变。
|
||||
阶段选择不增加 TypeMap/ConditionalRecord 条件,不把未保留片段重新引入。
|
||||
生成 descriptor 缓存与资源/快照惰性创建保留;Core 在同步 Build 或 Stage 2 补齐同一包快照,详见
|
||||
[注册分期](../../architecture/foundations/lazy-control-registration.md)。
|
||||
|
||||
## 4. 主题导出与资源
|
||||
|
||||
Control 到资产的关联来自顶层导出的 ControlTheme key/TargetType。嵌套模板 TargetType、局部定制主题或 standalone 非默认
|
||||
|
||||
@@ -62,11 +62,16 @@ Core 的隐藏生成 ABI 采用以下形态:
|
||||
```csharp
|
||||
public abstract class ControlRegistrationFragmentAttribute : Attribute
|
||||
{
|
||||
public virtual bool IsBootstrap => true;
|
||||
public abstract string FragmentId { get; }
|
||||
public abstract void Add(ControlPackageRegistrationBuilder builder);
|
||||
}
|
||||
```
|
||||
|
||||
`IsBootstrap` 是兼容增量:旧片段默认在初始配置执行;含 `[BootstrapRegistration]` 的包由生成器覆写常量。
|
||||
`AddFragment` 在 Add/descriptor factory 前选择阶段;标记不增加 TypeMap key、条件或链接根。net8 固定 collector
|
||||
模板保持不变。仅新生成包依赖新 Core 属性,不能把它用于未提供该成员的旧运行时。
|
||||
|
||||
代理是带自身 Attribute 的 sealed 派生类型。查询到目标 Type 后,注册代码读取已知基类的 Attribute;
|
||||
不对未知类型进行构造函数搜索或运行时程序集发现。
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# 启动注册方案全局复核(2026-10-13)
|
||||
|
||||
## 范围与结论
|
||||
|
||||
源码基线:`ea77bcb72`。本次只分析、测量并调整设计文档,没有修改产品实现。
|
||||
检查范围包括 Generator 片段与 descriptor 输出、包收集/冻结/合并、Core registry、Semantic、
|
||||
生成主题绑定、资源查询、快照、两阶段事务以及 AOT 后端边界。
|
||||
|
||||
推荐继续采用完整包注册与既有按需资源/快照,但应将方案具体化为:
|
||||
**同一份 descriptor,明确的构建边界,有限的生成代码优化。**
|
||||
现阶段没有证据支持另建惰性描述符系统、应用依赖扫描器或注册索引解释器。
|
||||
也没有证据证明直接移除名单可以保持当前首帧耗时。
|
||||
|
||||
## 本次测量
|
||||
|
||||
重新构建 Desktop Release net10.0,0 warnings / 0 errors。
|
||||
使用系统临时目录中的独立 .NET 10 控制台探针,通过反射调用当前产物的内部生成收集方法与 registry 构造。
|
||||
反射只用于取证,不是建议的产品实现;方法解析和 builder 构造位于收集计时外。
|
||||
测量线程的分配计数不代表进程 RSS 或全部 JIT/native 分配。
|
||||
|
||||
收集探针:6 个独立进程,交替执行 Critical→Deferred 与 Deferred→Critical,每进程再执行一遍作为
|
||||
已缓存路径。以下为两段时间之和的中位数,不能把各自中位数相加替代:
|
||||
|
||||
| 当前 Desktop 收集路径 | 时间 | 当前线程分配 |
|
||||
| --- | ---: | ---: |
|
||||
| 独立进程首次执行,两段合计 | 206.916 ms | 1,761,160 B |
|
||||
| 同进程再次执行,两段合计 | 0.506 ms | 69,840 B |
|
||||
|
||||
当前两段合计包含 183 个 Control descriptor、99 个 Semantic descriptor、315 个资产和315 个资源注册。
|
||||
首次与再次执行的差异混合了 JIT、类型/代码加载、不可变缓存填充及相关首次构造;没有使用采样分析器,
|
||||
不能把全部差值归因于 JIT。此处“首次”不意味着操作系统磁盘冷缓存。
|
||||
|
||||
另用独立进程做分项探针,各3次,结果为中位数:
|
||||
|
||||
| 独立探针 | 时间 | 说明 |
|
||||
| --- | ---: | --- |
|
||||
| Token factory | 69.363 ms | 直接调用183个工厂 |
|
||||
| Semantic factory | 20.428 ms | 同一探针接着调用99个工厂 |
|
||||
| Asset factory | 38.552 ms | 同一探针接着调用315个工厂;不构造实际主题字典 |
|
||||
| 完整 registry 首次构造 | 17.142 ms | Common+Desktop 已收集,global schema已创建,使用包指纹组合入口 |
|
||||
| 完整 registry 再次构造 | 1.304 ms | 同一不可变描述输入 |
|
||||
|
||||
分项探针受执行顺序、公共代码预热与反射调用数量影响。不是无侵入的耗时归因,不能相加为
|
||||
完整注册成本,也不能与206.916 ms作差来声称某个具体函数占比。
|
||||
没有测量当前方案的 Gallery 首帧、Ready、发布AOT、Browser或新方案端到端性能。
|
||||
|
||||
原始数据与被测DLL SHA-256保存在 `.superpowers/sdd/2026-10-13-registration-design-audit/`。
|
||||
临时工程、源码和可执行产物已在分析结束时删除。
|
||||
|
||||
## 代码层面的具体发现
|
||||
|
||||
### 1. 已存在的优化不能再次计算收益
|
||||
|
||||
- `TypeMapRegistrationWriter.WriteCachedDescriptor` 已按类型/资产缓存不可变 descriptor。
|
||||
- `ControlThemeAssetDescriptor.CreateGeneratedPreSorted` 已有生成数据预排序路径。
|
||||
- `ThemeSchemaRegistry.ToOrderedArray` 已对有序输入使用线性检查。
|
||||
- `TokenDescriptor` 已共享 codec/accessor,避免每Token一套委托。
|
||||
- `StaticThemeResourceProvider.ResourceTable` 已有资源键索引,并保留开放资源的有序回退。
|
||||
- `ThemeCompiler.CompileControls` 已创建惰性cell,重用父cell;无OwnToken且无配置时使用空快照。
|
||||
|
||||
因此新方案的主要维护收益来自删除手工分区及其状态,而不是“再实现一次按需加载”。
|
||||
|
||||
### 2. 重复工作可从现有边界内消除
|
||||
|
||||
`ControlPackageRegistration` 对资源、控件、资产、Semantic分别复制/排序/校验;
|
||||
`ThemeManagerBuilder.MergeContributionsInto` 又对资源排序;registry再复制/检查/绑定slot。
|
||||
|
||||
其中部分属于合法的不同边界:公开可变输入的防御性复制、跨包冲突、最终slot分配不能省略。
|
||||
应先明确已冻结数组的所有权和规范顺序,在内部消费路径复用;不能以“生成数据可信”为由删除所有验证。
|
||||
|
||||
资产跨片段引用当前会通过 `HasSameMetadata` 重复比较。生成工厂已复用同一实例,
|
||||
对象同一性的快速返回可能避免无意义的序列遍历,属于小范围候选优化;不同实例仍需结构比较。
|
||||
|
||||
### 3. 冷启动与热注册不是同一个问题
|
||||
|
||||
六进程数据说明,热态收集循环不是当前首要问题。生成片段、缓存getter、Token/Semantic/资产工厂的
|
||||
首次执行与构造应优先剖析。可以评估减少重复生成方法体、共享普通辅助实现、缩减临时对象;
|
||||
不要默认引入二进制schema解释器、通用VM或并行初始化调度器。
|
||||
|
||||
### 4. 编译期排序有明确边界
|
||||
|
||||
包内稳定顺序和包内schema可在生成期确定;最终应用的保留集合、跨包冲突和registry dense slot
|
||||
仍取决于实际注册包与裁剪结果。不能生成全局固定slot,也不能让全包Type/工厂静态表成为链接根。
|
||||
生成契约指纹使用metadata身份,而部分运行期顺序使用真实类型身份,不能直接假定两种顺序一致。
|
||||
|
||||
### 5. 分区是可删除的复杂度,事务不全是
|
||||
|
||||
关键集/余量collector、余量segment、消费标记、未完整包指纹分支可以随迁移退场。
|
||||
显式Deferred可选包仍会改变registry,主题/语言原子提交、取消和稳定owner保护仍需要保留。
|
||||
不能把删除包内分区描述成“一次性删除所有Stage 2演化代码”。
|
||||
|
||||
## 备选方案比较
|
||||
|
||||
| 方案 | 首次执行成本 | 维护性和语义 | 结论 |
|
||||
| --- | --- | --- | --- |
|
||||
| 手写关键集或改成特性 | 可减少首帧前构造 | 依赖闭包继续人工维护 | 排除 |
|
||||
| 自动推导应用首帧闭包 | 可能减少构造,但未证明完整 | 动态创建/跨包复杂,重复链接器职责 | 排除 |
|
||||
| 所有描述符按需创建 | 能推迟更多首次构造 | 增加查询状态、校验时机和错误处理复杂度 | 不作为基础架构;仅在证据充分时局部考虑 |
|
||||
| 完整注册现有descriptor,复用既有惰性资源和快照 | 首帧承担完整元数据构造,必须测量和优化 | 边界最清楚、无人工依赖表、改动可控 | 当前推荐 |
|
||||
| 独立原生加载窗,框架之后一次初始化 | 有利于快速显示加载界面,但本次未测 | 改变首屏产品形态与窗口交接,不能等价于完整AtomUI主窗首帧 | 只有产品明确接受时才考虑 |
|
||||
|
||||
若完整注册的冷态成本优化后仍不满足首帧预算,需要在预编译部署、首屏交互范围或局部惰性描述之间
|
||||
做有数字支撑的取舍。不能同时承诺任意完整AtomUI首屏、几乎零注册耗时、以及没有任何延迟初始化复杂度。
|
||||
|
||||
## 建议执行顺序
|
||||
|
||||
1. 固定当前端到端基线,明确首帧/Ready/首次页面/内存预算。
|
||||
2. 优化现有生成代码的首次执行与冻结数据的内部消费,逐项A/B,保持公共入口防御性校验。
|
||||
3. 移除包内分区,验证同包任意控件首阶段可用,进行完整端到端比较。
|
||||
4. 只有证据显示必要时才讨论局部描述延迟,不默认扩大架构。
|
||||
|
||||
此次是方案分析,不是重构验收;没有运行全量回归,也没有创建提交。
|
||||
|
||||
|
||||
## 实施后补充验收
|
||||
|
||||
本次改动已删除名单、双collector与Core余量段,保留旧ABI签名作为同次顺序收集适配。
|
||||
复用原有descriptor缓存、资源与快照惰性路径;包内有序数组复制一次,同实例metadata快速去重。
|
||||
尝试用BCL LazyInitializer替换生成缓存发布代码,8组交错进程采样从216.734ms/1,756,496B变为
|
||||
236.598ms/1,822,360B;该试验已撤回,没有将新增委托开销引入最终实现。
|
||||
|
||||
清理后的focused测试:Core79、Generator118、Desktop17、Gallery26,共240项通过、0跳过。
|
||||
net8与net10 Release构建通过。Release Gallery由用户确认红绿灯、顶部应用名、窗内菜单、五款主题色及
|
||||
未用控件页面均正常。NativeAOT osx-arm64产物校验与5秒启动检查通过;Browser Release裁剪版在IAB实际
|
||||
渲染Gallery首页,控制台无错误。未运行全量回归;Browser AOT与net8发布本轮未执行。
|
||||
|
||||
NativeAOT首轮遭遇SDK csc进程139退出;限制DOTNET_PROCESSOR_COUNT=2后同源发布通过。
|
||||
现有ReactiveUI trim/AOT、Homebrew dylib最低系统版本警告仍存在。本次没有通过修改产品源码或屏蔽诊断解决编译器崩溃。
|
||||
|
||||
### 空窗口端到端对照
|
||||
|
||||
基线是隔离worktree中的ea77bcb72,当前是本次工作树。独立临时程序引用两侧真实项目,采用相同的
|
||||
800×500 AtomUI Window、Render哨兵、默认主题、en-US、AlibabaSans和Desktop包;两侧均设置空Deferred块,
|
||||
等待IAtomUIStartup.Completion后在ApplicationIdle退出。它测量框架注册与窗口路径,不包含Gallery shell。
|
||||
每侧先预热一个独立进程,再做6组交错独立进程采样;未设置DOTNET_PROCESSOR_COUNT运行探针。
|
||||
下表为自Main中的Stopwatch启动计时;不含托管Main之前的进程/运行时启动时间,也不代表显示器presentation。
|
||||
|
||||
| 指标(中位数) | 基线 | 当前 | 差值 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| UseAtomUI同步段 | 247.88 ms | 370.72 ms | +122.84 ms |
|
||||
| Window.Opened | 804.96 ms | 938.86 ms | +133.90 ms |
|
||||
| 首次Render | 849.92 ms | 986.47 ms | +136.55 ms |
|
||||
| Framework Ready | 1169.70 ms | 1161.03 ms | −8.67 ms |
|
||||
|
||||
**结论:这次替换有明确的JIT首帧成本,完整就绪基本相当。** 维护性改善来自移除人工依赖表和内部段状态;
|
||||
不能把本次重构称为首帧提速。NativeAOT端到端性能没有新旧对照,不能将JIT差值外推到AOT。
|
||||
若产品要求恢复旧版JIT首帧预算,应重新评估局部描述延迟或部署方式,不能静默恢复名单。
|
||||
|
||||
原始样本和DLL哈希在`.superpowers/sdd/2026-10-13-complete-registration/`。
|
||||
临时测试、A/B探针、端到端程序、发布目录、浏览器页与服务器均清理;基线worktree归档。
|
||||
|
||||
最终自审另外修正旧双collector入口的失败短路:第一段吞掉collection异常时也不得执行第二段。临时用例RED→GREEN,清理后再次运行Core定向验证。Native/Browser发布验收先于此项旧入口异常路径修正,普通生成注册与裁剪映射未改变。
|
||||
@@ -0,0 +1,67 @@
|
||||
# 完整注册的137ms首帧成本恢复调查(2026-10-13)
|
||||
|
||||
## 范围
|
||||
|
||||
保持已批准的完整注册方案,不恢复关键集,不增加平行描述符模型、预热调度器或运行期缓存。
|
||||
本轮未保留新的产品源码改动;发布策略待用户明确开发/发布优先目标。
|
||||
进入本轮前已有的Gallery TemporaryStartupTimeline.cs属于外部工作,不覆盖或删除。
|
||||
|
||||
## JIT取证
|
||||
|
||||
使用当前Release产物,对Desktop完整Collect捕获EventPipe JIT/Loader事件及线程栈采样。
|
||||
工厂事件的OptimizationTier是QuickJitted。跟踪会放大耗时,以下为定位证据,不是无跟踪性能基线:
|
||||
|
||||
| 生成方法 | 方法数 | 配对JIT事件时长合计 |
|
||||
| --- | ---: | ---: |
|
||||
| Control descriptor工厂 | 183 | 82.197ms |
|
||||
| Asset descriptor工厂 | 315 | 82.061ms |
|
||||
| Semantic descriptor工厂 | 99 | 32.281ms |
|
||||
| 缓存getter | 912 | 52.188ms |
|
||||
|
||||
实测负担集中在首次编译元数据构造代码,不能将其解释为主题派生算法计算慢。
|
||||
|
||||
## 实验一:缓存发布并入Add
|
||||
|
||||
只改生成器输出,把Volatile.Read/CompareExchange放入原来的片段/资产Add方法,保留原缓存字段、
|
||||
异常和重试语义,消除独立getter。临时测试先失败,再实际编译生成输出,验证两次收集共享元数据而
|
||||
不共享builder;Generator Registration及临时测试120项通过。
|
||||
|
||||
8组独立进程交错采样:
|
||||
|
||||
| 无跟踪完整Collect | 原生成形态 | 合并getter |
|
||||
| --- | ---: | ---: |
|
||||
| 首次收集中位数 | 276.650ms | 277.054ms |
|
||||
| 当前线程分配 | 1,756,600B | 1,756,624B |
|
||||
|
||||
**无收益,已撤回。** 删除方法包装层没有消除构造体本身的JIT成本,不把代码变形当作优化。
|
||||
|
||||
## 实验二:标准ReadyToRun发布
|
||||
|
||||
独立临时程序引用当前Core/Desktop/字体项目,用.NET SDK标准PublishReadyToRun=true,
|
||||
Release、osx-arm64、framework-dependent发布。它不改变框架源码或运行期初始化协议。
|
||||
对比普通构建IL输出与同一程序的ReadyToRun输出;未禁用整个CLR的ReadyToRun,以免把BCL变化混入对比。
|
||||
|
||||
相同800×500 AtomUI Window、默认主题、en-US、AlibabaSans、Desktop包、空Deferred块和Render哨兵。
|
||||
两侧各预热一个进程,再作6组交错独立进程采样;从Main中Stopwatch开始计时,不含Main之前的运行时启动,
|
||||
不是显示器presentation,也不是完整Gallery shell测量。
|
||||
|
||||
| 指标(中位数) | 普通IL构建 | ReadyToRun发布 | 缩短 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| UseAtomUI同步段 | 366.49ms | 142.36ms | 224.13ms |
|
||||
| Window.Opened | 924.83ms | 515.12ms | 409.71ms |
|
||||
| 首次Render | 971.57ms | 547.51ms | 424.06ms |
|
||||
| Framework Ready | 1150.12ms | 679.70ms | 470.42ms |
|
||||
|
||||
**对于发布产物,已实测覆盖此前约137ms的回退。** 效果包含依赖库与窗口路径的预编译收益,
|
||||
不是声称只优化了注册的137ms。普通Rider/dotnet run构建不会自动获得这份发布结果。
|
||||
|
||||
代价:Core DLL从904,704B增至2,414,080B;Desktop DLL从8,422,400B增至23,901,696B。
|
||||
另有发布预编译耗时和RID相关产物;本轮只验证osx-arm64,不能外推其他平台。
|
||||
NativeAOT本来就是预编译发布路径,本轮没有改变或重新测量其性能。
|
||||
|
||||
## 处置
|
||||
|
||||
- 失败的代码优化已撤回,临时测试删除;最终Generator Registration重新验证。
|
||||
- 运行期架构与现有未提交重构保持原样,没有自动打开发布开关。
|
||||
- 同轮原始样本、JIT汇总和DLL哈希位于`.superpowers/sdd/2026-10-13-startup-recovery/`。
|
||||
- 本轮创建的临时工程、跟踪文件和发布产物在调查结束时清理。
|
||||
@@ -0,0 +1,64 @@
|
||||
# BootstrapRegistration 实施与验证记录(2026-10-14)
|
||||
|
||||
设计:[控件注册分期与按需创建](../../architecture/foundations/lazy-control-registration.md)。
|
||||
|
||||
## 实施
|
||||
|
||||
- 新增 `[BootstrapRegistration]`,在 25 个 Desktop 控件与 5 个本地资源类上声明首阶段职责。生成结果选中 30/319 个片段;其余在 Stage 2 收集。没有声明的包保持完整注册。
|
||||
- 源生成器按现有资产 export、Token owner、语义 owner 关系补齐本包阶段,不建立应用调用图,不改变裁剪根。无贡献标记报告 ATOMUIREG010。
|
||||
- Core 在片段 Add 前分流。包补齐替换原快照,沿用 provider、commit ordinal 与统一资源排序;partial 指纹为空,完整包才恢复完整指纹。同步启动在返回前补齐。
|
||||
- 收集/补齐失败均封存 builder,清空已发布和未发布 pending;初始化配置失败、取消和 Dispose 同样释放 continuation。首次资源工厂仍按查询执行。
|
||||
- 既有 csproj AtomUICriticalControls 移除保持;旧 partitioned ABI 维持顺序完整收集适配。应用入口不变。
|
||||
|
||||
## 普通 JIT 对照
|
||||
|
||||
同一本机 macOS arm64、Release net10 Gallery,baseline 为本任务开始时完整注册工作树的实编产物;bootstrap 为本任务新产物。两份产物包含相同既有 Gallery timeline;没有启用 ReadyToRun。每版预热一次,之后 6 组交错进程;采样期间无本任务构建并发。单位 ms,表中为各组中位数。
|
||||
|
||||
| 指标 | 完整注册 | Bootstrap | 差值 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Desktop 注册 | 186.80 | 55.95 | -130.85 |
|
||||
| UseAtomUI 同步段 | 438.75 | 284.90 | -153.85 |
|
||||
| 窗口首次 Render | 1372.95 | 1140.45 | -232.50 |
|
||||
| 框架 Ready | 1807.60 | 1729.65 | -77.95 |
|
||||
| Shell Ready | 2050.75 | 1971.75 | -79.00 |
|
||||
|
||||
首次 Render 从 Main 内探针起算,代表托管渲染回调,不是进程创建或显示器 presentation。此处使用实际 Gallery,不能把其绝对值与前一轮 800×500 骨架窗相混。记录的是单机样本,不能承诺所有机器固定减少同样的毫秒数。最后的未发布失败清理修复只调整异常路径,不改变采样成功路径的片段集合与执行顺序。
|
||||
|
||||
### 首 Render / Ready 原始样本
|
||||
|
||||
| 组 | baseline Render | bootstrap Render | baseline Ready | bootstrap Ready |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 1 | 1408.8 | 1173.5 | 1819.8 | 1745.4 |
|
||||
| 2 | 1309.0 | 1333.7 | 1705.4 | 1933.5 |
|
||||
| 3 | 1294.6 | 1138.5 | 1800.2 | 1713.9 |
|
||||
| 4 | 1473.9 | 1142.4 | 1931.4 | 1661.3 |
|
||||
| 5 | 1404.8 | 1130.2 | 1815.0 | 1649.9 |
|
||||
| 6 | 1341.1 | 1107.4 | 1708.7 | 1768.3 |
|
||||
|
||||
## 测试生命周期与审查
|
||||
|
||||
- 临时 RED→GREEN:首次分流、缺失 foreign theme、封存后修改指纹、补齐失败清理、配置失败清理、收集未发布时 payload 保留。
|
||||
- 临时覆盖:两阶段终态/同步路径、资源排序与 provider 稳定、去重、服务一次、partial 指纹、无效/非继承标记、多目标资产、同包及跨包 owner、应用真实取消并保留 builder。
|
||||
- 独立审查 P2:收集器未发布即失败时仍持有 pending。修复后直接抛错、捕获 mutation 异常和缺资源工厂三种场景的 WeakReference 均可回收,后续 mutation 被拒绝。
|
||||
- 修复后 Core 专项 82/82;生成器 123/123;Desktop 15/15。这里包含 16+5+1 个临时验证案例。临时源码、helper 和目录已全部删除;最终常驻测试净新增为 0。保留既有测试对新内部契约和两种后端生成形态的调整。
|
||||
|
||||
## 验证状态
|
||||
|
||||
- net10 Release Gallery、net8 Release Desktop:构建成功,均 0 warnings / 0 errors。
|
||||
- 当前工作区 Release Gallery PID 56299:用户确认启动红绿灯、macOS 顶部应用名、窗内主菜单、五款主题、暗色/紧凑和新页面全部正常。精确路径 CUA 不能绑定,未用同名应用代替。进程已按 launcher 身份停止。
|
||||
- 清理后聚焦验证:Core 66/66、Generator 118/118、Desktop 14/14;全部通过,无跳过。采用精确 filter 与已构建的当前 Debug 依赖,未运行全量回归。
|
||||
- macOS arm64 NativeAOT:PublishToLocal.ps1 成功,产物验证通过;精确原生二进制(Mach-O arm64)以 timeline 启动,记录到 StartupReady/ShellReady 后 exit 0。此轮是正确性 smoke,不是发布包性能 A/B。
|
||||
- 发布保留既有 ReactiveUI / ReactiveUI.Avalonia 的 IL2104、IL3053 和本机 Homebrew 动态库部署版本警告;未发现新增注册诊断。
|
||||
- Browser Release trimmed interpreter:发布成功,TypeMap link inputs 已重新生成;本机临时 HTTP 服务加载真实产物,IAB 中 Overview 完整渲染,控制台 error 列表为空。测试标签页已关闭。
|
||||
|
||||
## 决策及范围
|
||||
|
||||
- 沿用用户正在审阅的工作区,保留已有改动;实现与验证完成后按用户明确授权提交。
|
||||
- IsBootstrap 是 source marker 必需的隐藏生成 ABI 增量,旧片段默认 true;没有新增应用配置参数。
|
||||
- 跨包 owner 保持真实 registry 校验:首阶段引用的 foreign owner 也必须已注册,否则明确失败;不跨程序集推测自动闭合。
|
||||
- 测量时使用工作区已有的 Gallery timeline;提交前其调用已由其他工作清理,遗留的未跟踪探针文件不纳入本次提交。本次自建临时测试与测量产物已清理。
|
||||
- Native/Browser、性能、真实 UI 三类证据分别验证;静态审查不替代实际发布或用户界面验收。
|
||||
|
||||
## 收尾
|
||||
|
||||
本次临时测试及专用目录已删除;测量/发布副本与临时服务已清理。保留上述原始样本和验证结论。`git diff --check` 通过。
|
||||
@@ -23,13 +23,12 @@ public static class ApplicationExtensions
|
||||
{
|
||||
bootstrap.RegisterApplicationLanguages(builder.Localization);
|
||||
}
|
||||
configure?.Invoke(builder);
|
||||
|
||||
LocalizationHost? localizationHost = null;
|
||||
ApplicationScope? applicationScope = null;
|
||||
List<IAtomUIOwnedService>? ownedServices = null;
|
||||
try
|
||||
{
|
||||
configure?.Invoke(builder);
|
||||
// staged 判定须先于本地化构建:Stage 1 是中间态,应用级语言包可能先于其目标包的
|
||||
// catalog 到达(catalog 随包注册被后置),此时允许未解析引用,由 Stage 2 演化闭合。
|
||||
var staged = builder.DeferredRegistrations.Count > 0;
|
||||
@@ -64,7 +63,7 @@ public static class ApplicationExtensions
|
||||
applicationScope.StageTwoRegistrationCompleted = async cancellation =>
|
||||
{
|
||||
// 成功封存与 finally 关闭分开:被捕获的包注册异常仍会在 Freeze 拒绝。
|
||||
themeBuilder.RunDeferredCollects();
|
||||
themeBuilder.CompletePendingRegistrations();
|
||||
themeBuilder.ControlRegistrationState.Freeze();
|
||||
localizationBuilder.CloseStageTwoWindow();
|
||||
// 先准备本地化终态;任何 catalog/bundle/语言配置错误都必须在主题注册并入和
|
||||
@@ -109,6 +108,7 @@ public static class ApplicationExtensions
|
||||
}
|
||||
catch (Exception initializationException)
|
||||
{
|
||||
builder.ThemeManagerBuilder.DiscardPendingRegistrations();
|
||||
try
|
||||
{
|
||||
if (applicationScope is not null)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace AtomUI.Registration;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a control or theme resource class whose registration is required before the startup surface is shown.
|
||||
/// Packages without this marker register all their fragments during initial configuration.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class BootstrapRegistrationAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@@ -13,16 +13,30 @@ public sealed class ControlPackageRegistrationBuilder
|
||||
private readonly Dictionary<string, ControlThemeAssetDescriptor> _assets = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, ControlThemeResourceRegistration> _resources = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, Type> _fragments = new(StringComparer.Ordinal);
|
||||
private readonly bool _deferFragments;
|
||||
private List<ControlRegistrationFragmentAttribute>? _pendingFragments;
|
||||
private Exception? _failure;
|
||||
private bool _sealed;
|
||||
private ulong? _packageSchemaFingerprint;
|
||||
|
||||
internal ControlPackageRegistrationBuilder() { }
|
||||
internal ControlPackageRegistrationBuilder() : this(false)
|
||||
{
|
||||
}
|
||||
|
||||
internal ControlPackageRegistrationBuilder(bool deferFragments)
|
||||
{
|
||||
_deferFragments = deferFragments;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成注册 Collect 写入的包级 schema 指纹常量(编译期预计算)。手工收集留空,
|
||||
/// 启动 registry 的 Revision 随之回退到全量遍历算法。
|
||||
/// </summary>
|
||||
public ulong? PackageSchemaFingerprint { get; set; }
|
||||
public ulong? PackageSchemaFingerprint
|
||||
{
|
||||
get => _packageSchemaFingerprint;
|
||||
set => Mutate(value, static (builder, fingerprint) => builder._packageSchemaFingerprint = fingerprint);
|
||||
}
|
||||
|
||||
public void AddControl(ControlTokenDescriptor descriptor) => Mutate(descriptor, static (builder, descriptor) =>
|
||||
{
|
||||
@@ -95,21 +109,15 @@ public sealed class ControlPackageRegistrationBuilder
|
||||
return;
|
||||
}
|
||||
builder._fragments.Add(fragment.FragmentId, type);
|
||||
if (builder._deferFragments && !fragment.IsBootstrap)
|
||||
{
|
||||
(builder._pendingFragments ??= []).Add(fragment);
|
||||
return;
|
||||
}
|
||||
fragment.Add(builder);
|
||||
});
|
||||
|
||||
internal ControlPackageRegistration Build(string id, IControlThemesProvider provider)
|
||||
{
|
||||
return Build(id, provider, deferredCollect: null);
|
||||
}
|
||||
|
||||
// 分区收集(分级启动):关键集已收集进本 builder,余量收集以委托暂存在包注册上——
|
||||
// 非 staged 应用在 ThemeManagerBuilder.Build 前内联执行(终态与全量收集等价);
|
||||
// staged 应用由 Stage 2 注册窗口执行(registry v2 增补)。
|
||||
internal ControlPackageRegistration Build(
|
||||
string id,
|
||||
IControlThemesProvider provider,
|
||||
Action<ControlPackageRegistrationBuilder>? deferredCollect)
|
||||
{
|
||||
ThrowIfUnavailable();
|
||||
_sealed = true;
|
||||
@@ -120,14 +128,50 @@ public sealed class ControlPackageRegistrationBuilder
|
||||
throw new InvalidOperationException($"Theme asset '{asset.AssetId}' has no resource factory.");
|
||||
}
|
||||
}
|
||||
var pending = _pendingFragments;
|
||||
_pendingFragments = null;
|
||||
return new(id, _controls, _assets.Values, _semantics, provider, _resources.Values)
|
||||
{
|
||||
SchemaFingerprint = PackageSchemaFingerprint,
|
||||
DeferredCollect = deferredCollect
|
||||
SchemaFingerprint = pending is null ? PackageSchemaFingerprint : null,
|
||||
CompleteRegistration = pending is null ? null : () => Complete(id, provider, pending)
|
||||
};
|
||||
}
|
||||
|
||||
internal void Fail(Exception exception) => _failure ??= exception;
|
||||
private ControlPackageRegistration Complete(string id, IControlThemesProvider provider,
|
||||
IReadOnlyList<ControlRegistrationFragmentAttribute> pending)
|
||||
{
|
||||
// The published snapshot and original collection stay sealed. Complete in a fresh collection;
|
||||
// shared assets are validated against the original contributions before a replacement is published.
|
||||
var completion = new ControlPackageRegistrationBuilder
|
||||
{
|
||||
PackageSchemaFingerprint = PackageSchemaFingerprint
|
||||
};
|
||||
completion._controls.AddRange(_controls);
|
||||
completion._semantics.AddRange(_semantics);
|
||||
foreach (var (key, value) in _assets)
|
||||
{
|
||||
completion._assets.Add(key, value);
|
||||
}
|
||||
foreach (var (key, value) in _resources)
|
||||
{
|
||||
completion._resources.Add(key, value);
|
||||
}
|
||||
foreach (var (key, value) in _fragments)
|
||||
{
|
||||
completion._fragments.Add(key, value);
|
||||
}
|
||||
foreach (var fragment in pending)
|
||||
{
|
||||
completion.Mutate(fragment, static (builder, value) => value.Add(builder));
|
||||
}
|
||||
return completion.Build(id, provider);
|
||||
}
|
||||
|
||||
internal void Fail(Exception exception)
|
||||
{
|
||||
_failure ??= exception;
|
||||
_pendingFragments = null;
|
||||
}
|
||||
|
||||
private void Mutate<TState>(TState state, Action<ControlPackageRegistrationBuilder, TState> action)
|
||||
{
|
||||
@@ -138,12 +182,12 @@ public sealed class ControlPackageRegistrationBuilder
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_failure ??= exception;
|
||||
Fail(exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfUnavailable()
|
||||
internal void ThrowIfUnavailable()
|
||||
{
|
||||
if (_failure is not null || _sealed)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace AtomUI.Registration;
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public abstract class ControlRegistrationFragmentAttribute : Attribute
|
||||
{
|
||||
public virtual bool IsBootstrap => true;
|
||||
public abstract string FragmentId { get; }
|
||||
public abstract void Add(ControlPackageRegistrationBuilder builder);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ public static class ControlRegistrationRuntime
|
||||
Action<IAtomUIBuilder>? complete = null) =>
|
||||
Register(builder, packageId, createProvider, collect, prepare, complete, ensure: false);
|
||||
|
||||
// 分区收集(分级启动):关键集先行,余量以委托暂存在包注册上(见 ControlPackageRegistration
|
||||
// .DeferredCollect 注释)。使用独立方法名,保持既有 RegisterPackage 的源码重载解析与二进制兼容。
|
||||
// Legacy generated ABI: both collectors now contribute to one complete package before publication.
|
||||
public static IAtomUIBuilder RegisterPartitionedPackage(
|
||||
IAtomUIBuilder builder,
|
||||
string packageId,
|
||||
@@ -36,7 +35,7 @@ public static class ControlRegistrationRuntime
|
||||
Action<IAtomUIBuilder>? prepare = null,
|
||||
Action<IAtomUIBuilder>? complete = null) =>
|
||||
Register(builder, packageId, createProvider, collectCritical, prepare, complete, ensure: false,
|
||||
collectDeferred: collectDeferred);
|
||||
collectRemaining: collectDeferred);
|
||||
|
||||
public static IAtomUIBuilder EnsurePackage(
|
||||
IAtomUIBuilder builder,
|
||||
@@ -47,7 +46,7 @@ public static class ControlRegistrationRuntime
|
||||
Action<IAtomUIBuilder>? complete = null) =>
|
||||
Register(builder, packageId, createProvider, collect, prepare, complete, ensure: true);
|
||||
|
||||
// 分区收集(分级启动)Ensure 变体,独立方法名避免改变既有 EnsurePackage 的重载解析。
|
||||
// Legacy Ensure ABI uses the same complete collection and idempotency checks.
|
||||
public static IAtomUIBuilder EnsurePartitionedPackage(
|
||||
IAtomUIBuilder builder,
|
||||
string packageId,
|
||||
@@ -57,7 +56,7 @@ public static class ControlRegistrationRuntime
|
||||
Action<IAtomUIBuilder>? prepare = null,
|
||||
Action<IAtomUIBuilder>? complete = null) =>
|
||||
Register(builder, packageId, createProvider, collectCritical, prepare, complete, ensure: true,
|
||||
collectDeferred: collectDeferred);
|
||||
collectRemaining: collectDeferred);
|
||||
|
||||
public static void CollectFragments(
|
||||
ControlPackageRegistrationBuilder builder,
|
||||
@@ -103,7 +102,7 @@ public static class ControlRegistrationRuntime
|
||||
Action<IAtomUIBuilder>? prepare,
|
||||
Action<IAtomUIBuilder>? complete,
|
||||
bool ensure,
|
||||
Action<ControlPackageRegistrationBuilder>? collectDeferred = null)
|
||||
Action<ControlPackageRegistrationBuilder>? collectRemaining = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
if (builder is not AtomUIBuilder root)
|
||||
@@ -111,6 +110,7 @@ public static class ControlRegistrationRuntime
|
||||
throw new ArgumentException("Generated control packages require the application AtomUI builder.", nameof(builder));
|
||||
}
|
||||
var state = root.ControlRegistrationState;
|
||||
ControlPackageRegistrationBuilder? collection = null;
|
||||
try
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(packageId);
|
||||
@@ -125,10 +125,17 @@ public static class ControlRegistrationRuntime
|
||||
state.ThrowIfUnavailable();
|
||||
var provider = createProvider();
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
var collection = new ControlPackageRegistrationBuilder();
|
||||
collection = new ControlPackageRegistrationBuilder(state.CanDeferFragments);
|
||||
collect(collection);
|
||||
state.ThrowIfUnavailable();
|
||||
var package = collection.Build(packageId, provider, collectDeferred);
|
||||
if (collectRemaining is not null)
|
||||
{
|
||||
// A collector can catch a mutation error; never run another callback on a failed collection.
|
||||
collection.ThrowIfUnavailable();
|
||||
collectRemaining(collection);
|
||||
state.ThrowIfUnavailable();
|
||||
}
|
||||
var package = collection.Build(packageId, provider);
|
||||
builder.Theme.AddControlPackage(package);
|
||||
complete?.Invoke(builder);
|
||||
state.Stage(packageId);
|
||||
@@ -136,7 +143,9 @@ public static class ControlRegistrationRuntime
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
collection?.Fail(exception);
|
||||
state.Fail(exception);
|
||||
root.ThemeManagerBuilder.DiscardPendingRegistrations();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ internal sealed class ControlRegistrationState
|
||||
private Exception? _failure;
|
||||
private bool _frozen;
|
||||
|
||||
internal bool CanDeferFragments => !_stageTwoWindowUsed;
|
||||
|
||||
internal void ThrowIfInitialConfigurationUnavailable()
|
||||
{
|
||||
ThrowIfUnavailable();
|
||||
|
||||
@@ -56,7 +56,17 @@ public sealed class ControlThemeResourceRegistration
|
||||
};
|
||||
}
|
||||
|
||||
internal static int CompareOrder(ControlThemeResourceRegistration left, ControlThemeResourceRegistration right)
|
||||
{
|
||||
var result = left.Phase.CompareTo(right.Phase);
|
||||
if (result == 0)
|
||||
{
|
||||
result = left.Order.CompareTo(right.Order);
|
||||
}
|
||||
return result == 0 ? StringComparer.Ordinal.Compare(left.AssetId, right.AssetId) : result;
|
||||
}
|
||||
|
||||
internal bool HasSameMetadata(ControlThemeResourceRegistration other) =>
|
||||
AssetId == other.AssetId && Phase == other.Phase && Order == other.Order && Factory.Equals(other.Factory) &&
|
||||
(ResourceKeys is null ? other.ResourceKeys is null : other.ResourceKeys is not null && ResourceKeys.SetEquals(other.ResourceKeys));
|
||||
ReferenceEquals(this, other) || (AssetId == other.AssetId && Phase == other.Phase && Order == other.Order && Factory.Equals(other.Factory) &&
|
||||
(ResourceKeys is null ? other.ResourceKeys is null : other.ResourceKeys is not null && ResourceKeys.SetEquals(other.ResourceKeys)));
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ public sealed class ControlPackageRegistration
|
||||
throw new ArgumentException("Resource registrations cannot contain null.", nameof(resources));
|
||||
}
|
||||
EnsureUnique(resourceArray, static resource => resource.AssetId, StringComparer.Ordinal, "resource AssetId");
|
||||
Resources = Array.AsReadOnly(resourceArray.OrderBy(static resource => resource.Phase)
|
||||
.ThenBy(static resource => resource.Order).ThenBy(static resource => resource.AssetId, StringComparer.Ordinal).ToArray());
|
||||
SortIfNeeded(resourceArray, ControlThemeResourceRegistration.CompareOrder);
|
||||
Resources = Array.AsReadOnly(resourceArray);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
ArgumentNullException.ThrowIfNull(controls);
|
||||
ArgumentNullException.ThrowIfNull(themeAssets);
|
||||
@@ -35,35 +35,29 @@ public sealed class ControlPackageRegistration
|
||||
nameof(controlThemesProvider));
|
||||
}
|
||||
|
||||
var controlArray = controls.OrderBy(static control => control.Identity.Catalog, StringComparer.Ordinal)
|
||||
.ThenBy(static control => control.Identity.Id, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var controlArray = controls.ToArray();
|
||||
SortIfNeeded(controlArray, static (left, right) => CompareIdentity(left.Identity, right.Identity));
|
||||
EnsureUnique(
|
||||
controlArray,
|
||||
static control => control.Identity,
|
||||
EqualityComparer<ControlTokenIdentity>.Default,
|
||||
"Control identity");
|
||||
var assetArray = themeAssets.OrderBy(
|
||||
static asset => asset.AssetUri.ToString(),
|
||||
StringComparer.Ordinal).ToArray();
|
||||
var assetArray = themeAssets.ToArray();
|
||||
SortIfNeeded(assetArray, static (left, right) => StringComparer.Ordinal.Compare(left.AssetUri.ToString(), right.AssetUri.ToString()));
|
||||
EnsureUnique(assetArray, static asset => asset.AssetId, StringComparer.Ordinal, "theme AssetId");
|
||||
EnsureUnique(
|
||||
assetArray,
|
||||
static asset => asset.AssetUri.ToString(),
|
||||
StringComparer.Ordinal,
|
||||
"theme asset URI");
|
||||
ControlSemanticDescriptor?[] nullableSemanticControls = semanticControls.ToArray();
|
||||
if (nullableSemanticControls.Any(static descriptor => descriptor is null))
|
||||
var semanticArray = semanticControls.ToArray();
|
||||
if (semanticArray.Any(static descriptor => descriptor is null))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Semantic Control descriptors cannot contain null.",
|
||||
nameof(semanticControls));
|
||||
}
|
||||
var semanticArray = nullableSemanticControls
|
||||
.Select(static descriptor => descriptor!)
|
||||
.OrderBy(static descriptor => descriptor.Identity.Catalog, StringComparer.Ordinal)
|
||||
.ThenBy(static descriptor => descriptor.Identity.Id, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
SortIfNeeded(semanticArray, static (left, right) => CompareIdentity(left.Identity, right.Identity));
|
||||
EnsureUnique(
|
||||
semanticArray,
|
||||
static descriptor => descriptor.Identity,
|
||||
@@ -87,22 +81,34 @@ public sealed class ControlPackageRegistration
|
||||
/// <summary>生成器 emit 的包级 schema 指纹常量;手工注册为空,启动 Revision 回退全量计算。</summary>
|
||||
public ulong? SchemaFingerprint { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分区收集(分级启动)的余量收集委托:关键集已随包主体注册;本委托由
|
||||
/// ThemeManagerBuilder 在非 staged 应用 Build 前内联执行,或由 Stage 2 注册窗口执行。
|
||||
/// </summary>
|
||||
internal Action<AtomUI.Registration.ControlPackageRegistrationBuilder>? DeferredCollect { get; init; }
|
||||
// Only the configuration builder owns this continuation. It is cleared on completion or cancellation.
|
||||
internal Func<ControlPackageRegistration>? CompleteRegistration { get; set; }
|
||||
|
||||
// 余量收集是否已执行(非 staged 内联执行或 Stage 2 窗口执行后置真)。
|
||||
// 未消费时包内容不完整,Revision 不允许使用全量 schema 指纹(v1/v2 注册表内容不同
|
||||
// 必须得到不同 Revision,否则快照缓存键碰撞)。
|
||||
internal bool DeferredCollectConsumed { get; set; }
|
||||
public string Id { get; }
|
||||
public IReadOnlyList<ControlTokenDescriptor> Controls { get; }
|
||||
public IReadOnlyList<ControlThemeAssetDescriptor> ThemeAssets { get; }
|
||||
public IReadOnlyList<ControlSemanticDescriptor> SemanticControls { get; }
|
||||
public IControlThemesProvider ControlThemesProvider { get; }
|
||||
|
||||
private static int CompareIdentity(ControlTokenIdentity left, ControlTokenIdentity right)
|
||||
{
|
||||
var result = StringComparer.Ordinal.Compare(left.Catalog, right.Catalog);
|
||||
return result != 0 ? result : StringComparer.Ordinal.Compare(left.Id, right.Id);
|
||||
}
|
||||
|
||||
// Inputs are copied before sorting. Reuse that owned array, including for arbitrary public inputs.
|
||||
private static void SortIfNeeded<T>(T[] values, Comparison<T> comparison)
|
||||
{
|
||||
for (var index = 1; index < values.Length; index++)
|
||||
{
|
||||
if (comparison(values[index - 1], values[index]) > 0)
|
||||
{
|
||||
Array.Sort(values, comparison);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureUnique<TItem, TKey>(
|
||||
IEnumerable<TItem> items,
|
||||
Func<TItem, TKey> keySelector,
|
||||
|
||||
@@ -334,14 +334,14 @@ public sealed class ControlThemeAssetDescriptor
|
||||
}
|
||||
|
||||
internal bool HasSameMetadata(ControlThemeAssetDescriptor other) =>
|
||||
AssetId == other.AssetId && AssetUri == other.AssetUri &&
|
||||
ReferenceEquals(this, other) || (AssetId == other.AssetId && AssetUri == other.AssetUri &&
|
||||
ResourceKeySchemaFingerprint == other.ResourceKeySchemaFingerprint &&
|
||||
ExportedThemes.SequenceEqual(other.ExportedThemes) &&
|
||||
RequiredTokenOwners.Select(static identity => (identity, identity.OwnerType))
|
||||
.SequenceEqual(other.RequiredTokenOwners.Select(static identity => (identity, identity.OwnerType))) &&
|
||||
SemanticThemeBindings.SequenceEqual(other.SemanticThemeBindings) &&
|
||||
SemanticThemeBindings.Select(static binding => binding.OwnerIdentity.OwnerType)
|
||||
.SequenceEqual(other.SemanticThemeBindings.Select(static binding => binding.OwnerIdentity.OwnerType));
|
||||
.SequenceEqual(other.SemanticThemeBindings.Select(static binding => binding.OwnerIdentity.OwnerType)));
|
||||
|
||||
public string AssetId { get; }
|
||||
public IReadOnlyList<ControlThemeExportDescriptor> ExportedThemes { get; }
|
||||
|
||||
@@ -66,7 +66,7 @@ internal class ThemeManager : Styles, IThemeManager, IDisposable
|
||||
private readonly Dictionary<IControlThemesProvider, IReadOnlyList<IResourceProvider>>
|
||||
_controlThemeProviderBaseResources = new(ReferenceEqualityComparer.Instance);
|
||||
private StaticThemeResourceProvider? _staticResourceProvider;
|
||||
// 语义部件描述符登记表(Stage 1 全量 + Stage 2 增量并入):SemanticParts 由其按需重建。
|
||||
// 语义部件描述符登记表(初始包 + Stage 2 后置包):SemanticParts 由其按需重建。
|
||||
private readonly List<ControlSemanticDescriptor> _stagedSemanticControls = [];
|
||||
private ThemeSchemaRegistry? _startupRegistry;
|
||||
private IReadOnlyList<PackageSchemaFingerprint>? _packageSchemaFingerprints;
|
||||
@@ -1285,7 +1285,7 @@ internal class ThemeManager : Styles, IThemeManager, IDisposable
|
||||
|
||||
/// <summary>
|
||||
/// 语义部件描述符登记:Stage 1 由 BuildCore 一次登记全量;分级启动的 Stage 2 并入新包与
|
||||
/// 分区包余量段时增量追加,SemanticParts 随之重建(含全部已登记项)。
|
||||
/// 后置包注册时增量追加,SemanticParts 随之重建(含全部已登记项)。
|
||||
/// </summary>
|
||||
internal void StageSemanticControlDescriptors(IReadOnlyList<ControlSemanticDescriptor> descriptors)
|
||||
{
|
||||
@@ -1927,19 +1927,21 @@ internal class ThemeManager : Styles, IThemeManager, IDisposable
|
||||
|
||||
internal void StageControlThemeResources(IControlThemesProvider provider, IReadOnlyList<ControlThemeResourceRegistration> resources)
|
||||
{
|
||||
// Existing providers also need a new static-resource table when a package is completed.
|
||||
RegisterControlThemesProvider(provider);
|
||||
if (!_stagedThemeResources.TryGetValue(provider, out var staged))
|
||||
{
|
||||
_stagedThemeResources.Add(provider, resources.ToList());
|
||||
return;
|
||||
}
|
||||
// 两个输入均由 Builder 按完整排序键准备。线性归并保留分区间的相对优先级,
|
||||
// 两个输入均由 Builder 按完整排序键准备。线性归并保留新旧资源间的相对优先级,
|
||||
// 避免每次演化重新排序所有既有资产。
|
||||
var merged = new List<ControlThemeResourceRegistration>(staged.Count + resources.Count);
|
||||
var oldIndex = 0;
|
||||
var newIndex = 0;
|
||||
while (oldIndex < staged.Count && newIndex < resources.Count)
|
||||
{
|
||||
if (CompareThemeResources(staged[oldIndex], resources[newIndex]) <= 0)
|
||||
if (ControlThemeResourceRegistration.CompareOrder(staged[oldIndex], resources[newIndex]) <= 0)
|
||||
{
|
||||
merged.Add(staged[oldIndex++]);
|
||||
}
|
||||
@@ -1959,16 +1961,6 @@ internal class ThemeManager : Styles, IThemeManager, IDisposable
|
||||
_stagedThemeResources[provider] = merged;
|
||||
}
|
||||
|
||||
private static int CompareThemeResources(ControlThemeResourceRegistration left, ControlThemeResourceRegistration right)
|
||||
{
|
||||
var result = left.Phase.CompareTo(right.Phase);
|
||||
if (result == 0)
|
||||
{
|
||||
result = left.Order.CompareTo(right.Order);
|
||||
}
|
||||
return result == 0 ? StringComparer.Ordinal.Compare(left.AssetId, right.AssetId) : result;
|
||||
}
|
||||
|
||||
private void MountStaticResources()
|
||||
{
|
||||
// 即使 Stage 1 没有包也先固定静态层,后续演化不改变它相对 Token provider 的优先级。
|
||||
|
||||
@@ -83,27 +83,7 @@ internal sealed class ThemeManagerBuilder : IThemeManagerBuilder
|
||||
throw new ThemeResourceRegisterException(
|
||||
$"Control package '{package.Id}' is already registered.");
|
||||
}
|
||||
foreach (var descriptor in package.Controls)
|
||||
{
|
||||
if (_registeredControlTokenIdentities.Contains(descriptor.Identity))
|
||||
{
|
||||
throw new ThemeResourceRegisterException(
|
||||
$"Control Token descriptor '{descriptor.Identity}' is already registered.");
|
||||
}
|
||||
}
|
||||
foreach (var descriptor in package.SemanticControls)
|
||||
{
|
||||
if (_registeredSemanticControlIdentities.Contains(descriptor.Identity))
|
||||
{
|
||||
throw new ThemeResourceRegisterException(
|
||||
$"Semantic Control descriptor '{descriptor.Identity}' is already registered.");
|
||||
}
|
||||
if (_registeredSemanticControlTypes.Contains(descriptor.ControlType))
|
||||
{
|
||||
throw new ThemeResourceRegisterException(
|
||||
$"Semantic Control type '{descriptor.ControlType.FullName}' is already registered.");
|
||||
}
|
||||
}
|
||||
ValidateNewDescriptors(package, null);
|
||||
if (_registeredControlThemeProviders.Contains(package.ControlThemesProvider.Id))
|
||||
{
|
||||
throw new ThemeResourceRegisterException(
|
||||
@@ -112,20 +92,74 @@ internal sealed class ThemeManagerBuilder : IThemeManagerBuilder
|
||||
_registeredControlPackageIds.Add(package.Id);
|
||||
package.PackageCommitOrdinal = _controlPackages.Count;
|
||||
_controlPackages.Add(package);
|
||||
foreach (var descriptor in package.Controls)
|
||||
{
|
||||
_registeredControlTokenIdentities.Add(descriptor.Identity);
|
||||
}
|
||||
foreach (var descriptor in package.SemanticControls)
|
||||
{
|
||||
_registeredSemanticControlIdentities.Add(descriptor.Identity);
|
||||
_registeredSemanticControlTypes.Add(descriptor.ControlType);
|
||||
}
|
||||
TrackDescriptors(package);
|
||||
_registeredControlThemeProviders.Add(package.ControlThemesProvider.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ControlRegistrationState.Fail(exception);
|
||||
DiscardPendingRegistrations();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateNewDescriptors(ControlPackageRegistration package, ControlPackageRegistration? previous)
|
||||
{
|
||||
foreach (var descriptor in package.Controls.Except(previous?.Controls ?? []))
|
||||
{
|
||||
if (_registeredControlTokenIdentities.Contains(descriptor.Identity))
|
||||
{
|
||||
throw new ThemeResourceRegisterException($"Control Token descriptor '{descriptor.Identity}' is already registered.");
|
||||
}
|
||||
}
|
||||
foreach (var descriptor in package.SemanticControls.Except(previous?.SemanticControls ?? []))
|
||||
{
|
||||
if (_registeredSemanticControlIdentities.Contains(descriptor.Identity) ||
|
||||
_registeredSemanticControlTypes.Contains(descriptor.ControlType))
|
||||
{
|
||||
throw new ThemeResourceRegisterException($"Semantic Control descriptor '{descriptor.Identity}' is already registered.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TrackDescriptors(ControlPackageRegistration package)
|
||||
{
|
||||
foreach (var descriptor in package.Controls)
|
||||
{
|
||||
_registeredControlTokenIdentities.Add(descriptor.Identity);
|
||||
}
|
||||
foreach (var descriptor in package.SemanticControls)
|
||||
{
|
||||
_registeredSemanticControlIdentities.Add(descriptor.Identity);
|
||||
_registeredSemanticControlTypes.Add(descriptor.ControlType);
|
||||
}
|
||||
}
|
||||
|
||||
internal void CompletePendingRegistrations()
|
||||
{
|
||||
try
|
||||
{
|
||||
ControlRegistrationState.ThrowIfUnavailable();
|
||||
for (var index = 0; index < _controlPackages.Count; index++)
|
||||
{
|
||||
var previous = _controlPackages[index];
|
||||
if (previous.CompleteRegistration is not { } complete)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
previous.CompleteRegistration = null;
|
||||
var package = complete();
|
||||
ControlRegistrationState.ThrowIfUnavailable();
|
||||
ValidateNewDescriptors(package, previous);
|
||||
TrackDescriptors(package);
|
||||
package.PackageCommitOrdinal = previous.PackageCommitOrdinal;
|
||||
_controlPackages[index] = package;
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ControlRegistrationState.Fail(exception);
|
||||
DiscardPendingRegistrations();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -190,21 +224,15 @@ internal sealed class ThemeManagerBuilder : IThemeManagerBuilder
|
||||
FontFamily = FontFamily.Parse(fontFamily);
|
||||
}
|
||||
|
||||
internal ThemeManager Build()
|
||||
{
|
||||
return Build(deferPackageCollects: false);
|
||||
}
|
||||
internal ThemeManager Build() => Build(staged: false);
|
||||
|
||||
internal ThemeManager Build(bool deferPackageCollects)
|
||||
internal ThemeManager Build(bool staged)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 非 staged 应用:分区包的余量收集在此内联执行,终态与旧全量 Collect 等价
|
||||
//(关键集先行 + 余量追补,registry 构建按 identity 排序,顺序无关)。
|
||||
// staged 应用(deferPackageCollects)由 Stage 2 注册窗口执行(分级启动)。
|
||||
if (!deferPackageCollects)
|
||||
if (!staged)
|
||||
{
|
||||
RunDeferredCollects();
|
||||
CompletePendingRegistrations();
|
||||
}
|
||||
ControlRegistrationState.Freeze();
|
||||
return BuildCore();
|
||||
@@ -212,12 +240,12 @@ internal sealed class ThemeManagerBuilder : IThemeManagerBuilder
|
||||
catch (Exception exception)
|
||||
{
|
||||
ControlRegistrationState.Fail(exception);
|
||||
DiscardPendingRegistrations();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private int _mergedPackageCount;
|
||||
private int _mergedSegmentCount;
|
||||
private readonly List<ControlPackageRegistration> _mergedPackages = [];
|
||||
private readonly Dictionary<string, ControlThemeAssetDescriptor> _mergedThemeAssets =
|
||||
new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, ControlThemeResourceRegistration> _mergedThemeResources =
|
||||
@@ -235,10 +263,19 @@ internal sealed class ThemeManagerBuilder : IThemeManagerBuilder
|
||||
internal void CloseStageTwoWindow()
|
||||
{
|
||||
ControlRegistrationState.CloseStageTwoWindow();
|
||||
DiscardPendingRegistrations();
|
||||
}
|
||||
|
||||
internal void DiscardPendingRegistrations()
|
||||
{
|
||||
foreach (var package in _controlPackages)
|
||||
{
|
||||
package.CompleteRegistration = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stage 2 注册块的包与分区包余量段并入已构建的 ThemeManager:逐项走与 BuildCore 相同的
|
||||
/// Stage 2 注册块的完整包并入已构建的 ThemeManager:逐项走与 BuildCore 相同的
|
||||
/// 贡献合并路径(provider/描述符/语义/资产/资源),随后由
|
||||
/// ThemeManager.ExecuteRegistrationEvolutionAsync 重建 registry 并重应用主题。
|
||||
/// </summary>
|
||||
@@ -248,31 +285,23 @@ internal sealed class ThemeManagerBuilder : IThemeManagerBuilder
|
||||
private void MergeContributionsInto(ThemeManager themeManager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(themeManager);
|
||||
var contributions = new List<(ControlPackageRegistration Registration, bool IsSegment)>();
|
||||
for (var index = _mergedPackageCount; index < _controlPackages.Count; index++)
|
||||
{
|
||||
contributions.Add((_controlPackages[index], false));
|
||||
}
|
||||
for (var index = _mergedSegmentCount; index < _deferredPackageSegments.Count; index++)
|
||||
{
|
||||
contributions.Add((_deferredPackageSegments[index], true));
|
||||
}
|
||||
|
||||
var resourcesByProvider = new Dictionary<
|
||||
IControlThemesProvider,
|
||||
List<ControlThemeResourceRegistration>>(ReferenceEqualityComparer.Instance);
|
||||
var registeredProviders = new HashSet<IControlThemesProvider>(ReferenceEqualityComparer.Instance);
|
||||
foreach (var (contribution, _) in contributions
|
||||
.OrderBy(static item => item.Registration.PackageCommitOrdinal)
|
||||
.ThenBy(static item => item.IsSegment))
|
||||
// Packages are appended only after nested preparation commits, so list order is commit order.
|
||||
for (var index = 0; index < _controlPackages.Count; index++)
|
||||
{
|
||||
if (registeredProviders.Add(contribution.ControlThemesProvider))
|
||||
var contribution = _controlPackages[index];
|
||||
var previous = index < _mergedPackages.Count ? _mergedPackages[index] : null;
|
||||
if (ReferenceEquals(previous, contribution))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (previous is null)
|
||||
{
|
||||
// Stage 1 provider 需要重新进入挂载队列以追加余量资源;Stage 2 分区包的包主体与
|
||||
// 余量段共享 provider,只登记一次,避免同一批 staged resources 被重复挂载。
|
||||
themeManager.RegisterControlThemesProvider(contribution.ControlThemesProvider);
|
||||
}
|
||||
MergeContributionMetadata(themeManager, contribution);
|
||||
MergeContributionMetadata(themeManager, contribution, previous);
|
||||
foreach (var resource in contribution.Resources)
|
||||
{
|
||||
if (_mergedThemeResources.TryGetValue(resource.AssetId, out var existing))
|
||||
@@ -295,28 +324,27 @@ internal sealed class ThemeManagerBuilder : IThemeManagerBuilder
|
||||
}
|
||||
foreach (var (provider, resources) in resourcesByProvider)
|
||||
{
|
||||
resources.Sort(ControlThemeResourceRegistration.CompareOrder);
|
||||
themeManager.StageControlThemeResources(
|
||||
provider,
|
||||
resources.OrderBy(static resource => resource.Phase)
|
||||
.ThenBy(static resource => resource.Order)
|
||||
.ThenBy(static resource => resource.AssetId, StringComparer.Ordinal)
|
||||
.ToArray());
|
||||
resources.ToArray());
|
||||
}
|
||||
|
||||
_mergedPackageCount = _controlPackages.Count;
|
||||
_mergedSegmentCount = _deferredPackageSegments.Count;
|
||||
_mergedPackages.Clear();
|
||||
_mergedPackages.AddRange(_controlPackages);
|
||||
UpdatePackageSchemaFingerprints(themeManager);
|
||||
}
|
||||
|
||||
private void MergeContributionMetadata(
|
||||
ThemeManager themeManager,
|
||||
ControlPackageRegistration contribution)
|
||||
ControlPackageRegistration contribution,
|
||||
ControlPackageRegistration? previous)
|
||||
{
|
||||
foreach (var descriptor in contribution.Controls)
|
||||
foreach (var descriptor in contribution.Controls.Except(previous?.Controls ?? []))
|
||||
{
|
||||
themeManager.RegisterControlTokenDescriptor(descriptor);
|
||||
}
|
||||
themeManager.StageSemanticControlDescriptors(contribution.SemanticControls);
|
||||
themeManager.StageSemanticControlDescriptors(contribution.SemanticControls.Except(previous?.SemanticControls ?? []).ToArray());
|
||||
foreach (var asset in contribution.ThemeAssets)
|
||||
{
|
||||
if (_mergedThemeAssets.TryGetValue(asset.AssetId, out var existing))
|
||||
@@ -333,77 +361,6 @@ internal sealed class ThemeManagerBuilder : IThemeManagerBuilder
|
||||
}
|
||||
}
|
||||
|
||||
// 分区收集的余量执行:fresh builder 收集增量,产出一个与包同 id、同 provider 的「余量段」。
|
||||
// 段与包主体共同构成该包的完整贡献,BuildCore(Stage 1)与 MergeStageTwoInto(Stage 2)消费
|
||||
// 同一贡献集合——余量段的描述符/语义/资产/资源对两条路径同等可见。
|
||||
internal void RunDeferredCollects()
|
||||
{
|
||||
foreach (var package in _controlPackages)
|
||||
{
|
||||
if (package.DeferredCollect is not { } deferredCollect || package.DeferredCollectConsumed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var delta = new ControlPackageRegistrationBuilder();
|
||||
deferredCollect(delta);
|
||||
var segment = delta.Build(package.Id, package.ControlThemesProvider);
|
||||
segment.PackageCommitOrdinal = package.PackageCommitOrdinal;
|
||||
// 增量复用 AddControlPackage 的校验语义,跳过包 id / provider 重复检查(同一包的补充段)。
|
||||
foreach (var descriptor in segment.Controls)
|
||||
{
|
||||
if (_registeredControlTokenIdentities.Contains(descriptor.Identity))
|
||||
{
|
||||
throw new ThemeResourceRegisterException(
|
||||
$"Control Token descriptor '{descriptor.Identity}' is already registered.");
|
||||
}
|
||||
}
|
||||
foreach (var descriptor in segment.SemanticControls)
|
||||
{
|
||||
if (_registeredSemanticControlIdentities.Contains(descriptor.Identity))
|
||||
{
|
||||
throw new ThemeResourceRegisterException(
|
||||
$"Semantic Control descriptor '{descriptor.Identity}' is already registered.");
|
||||
}
|
||||
if (_registeredSemanticControlTypes.Contains(descriptor.ControlType))
|
||||
{
|
||||
throw new ThemeResourceRegisterException(
|
||||
$"Semantic Control type '{descriptor.ControlType.FullName}' is already registered.");
|
||||
}
|
||||
}
|
||||
foreach (var descriptor in segment.Controls)
|
||||
{
|
||||
_registeredControlTokenIdentities.Add(descriptor.Identity);
|
||||
}
|
||||
foreach (var descriptor in segment.SemanticControls)
|
||||
{
|
||||
_registeredSemanticControlIdentities.Add(descriptor.Identity);
|
||||
_registeredSemanticControlTypes.Add(descriptor.ControlType);
|
||||
}
|
||||
_deferredPackageSegments.Add(segment);
|
||||
package.DeferredCollectConsumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 分区包的余量段:与包同 id、同 provider 的补充贡献(RunDeferredCollects 产出)。
|
||||
private readonly List<ControlPackageRegistration> _deferredPackageSegments = new();
|
||||
|
||||
/// <summary>
|
||||
/// 包的完整贡献集合:包主体(Stage 1 关键集)+ 分区包余量段(<see cref="RunDeferredCollects"/> 产出)。
|
||||
/// BuildCore(Stage 1 构建)与 MergeStageTwoInto(Stage 2 并入)都按本集合逐项消费——审查与
|
||||
/// 契约测试须用本视图,直接用 ControlPackages 只能看到包主体的关键集部分。
|
||||
/// </summary>
|
||||
internal IEnumerable<ControlPackageRegistration> EnumerateContributions()
|
||||
{
|
||||
foreach (var package in _controlPackages)
|
||||
{
|
||||
yield return package;
|
||||
}
|
||||
foreach (var segment in _deferredPackageSegments)
|
||||
{
|
||||
yield return segment;
|
||||
}
|
||||
}
|
||||
|
||||
// 两阶段必须使用同一套有效来源,包括 UseUserThemeDirectory 配置生成的 resolver。
|
||||
internal IReadOnlyList<ThemeDefinitionRegistration> CreateThemeDefinitionRegistrations()
|
||||
{
|
||||
@@ -432,7 +389,7 @@ internal sealed class ThemeManagerBuilder : IThemeManagerBuilder
|
||||
themeManager.ConfigureThemeDefinitions(themeDefinitionResolvers, ApplicationId!,
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
|
||||
themeManager.EnsureRegistrationCapacity(_registeredControlTokenIdentities.Count,
|
||||
EnumerateContributions().Sum(static contribution => contribution.ThemeAssets.Count),
|
||||
_controlPackages.Sum(static contribution => contribution.ThemeAssets.Count),
|
||||
_registeredControlThemeProviders.Count);
|
||||
MergeContributionsInto(themeManager);
|
||||
themeManager.PrepareRegistrationSchema();
|
||||
@@ -448,10 +405,7 @@ internal sealed class ThemeManagerBuilder : IThemeManagerBuilder
|
||||
var allFingerprinted = true;
|
||||
foreach (var package in _controlPackages)
|
||||
{
|
||||
// 余量未消费 = 包内容不完整,全量指纹不可用(见 ControlPackageRegistration 注释);
|
||||
// 仅约束分区包,普通包(DeferredCollect == null)不受影响。
|
||||
if (package.SchemaFingerprint is not { } schemaFingerprint ||
|
||||
(package.DeferredCollect is not null && !package.DeferredCollectConsumed))
|
||||
if (package.SchemaFingerprint is not { } schemaFingerprint)
|
||||
{
|
||||
allFingerprinted = false;
|
||||
break;
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' != '' and
|
||||
$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">
|
||||
<!-- 仅官方 TypeMap 目标启用控件分区;net8 条件后端按完整单阶段注册配置构建。 -->
|
||||
<!-- 分级启动关键集(chrome 闭包,实证法维护):Stage 1 仅注册这些控件 + 其主题资产,
|
||||
余量由 Stage 2 注册窗口收集。改动此列表须以 headless 骨架窗重证闭包覆盖——
|
||||
骨架窗已查询的控件槽位集合必须 ⊆ 本关键集(方法见
|
||||
docs/superpowers/progress/2026-10-12-startup-optimization-rd-log.md §2.3)。 -->
|
||||
<AtomUICriticalControls>Window,WindowDrawnDecorations,WindowVisualLayerClip,WindowResizer,MacStandardWindowButtons,MediaBreakPointIndicator,FullscreenPopoverLayer,WindowTitleBar,WindowTitleBarHostContext,WindowTitleBarLayoutPanel,WindowTitleBarButton,WindowTitleBarToggleButton,CaptionButtonGroup,Menu,MenuItem,MenuItemGroup,MenuSeparator,MenuFlyoutPresenter,MenuPopupScrollHost,Icon,IconPresenter,IconButton,Popup,PopupRoot,PopupMotionActor,BaseMotionActor,OverlayPopupHost,ShadowsAwareContainer,ScrollViewer,ScrollBar,ScrollBarThumb,CheckBox,RadioButton,CheckBoxGroup,RadioButtonGroup,PixelAlignedBorder,TextBlock,SelectableTextBlock,HyperLinkTextBlock,ToggleIconButton</AtomUICriticalControls>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(AtomUITargetFrameworks)</TargetFrameworks>
|
||||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using AtomUI.Controls.Commons;
|
||||
using AtomUI.Registration;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class IconButton : AbstractIconButton
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using AtomUI.Controls.Commons;
|
||||
using AtomUI.Registration;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class ToggleIconButton : AbstractToggleIconButton
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using AtomUI.Controls.Commons;
|
||||
using AtomUI.Registration;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public partial class CheckBox : AbstractCheckBox
|
||||
{
|
||||
public CheckBox()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using AtomUI.Controls.Commons;
|
||||
using AtomUI.Registration;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class CheckBoxGroup : AbstractCheckBoxGroup
|
||||
{
|
||||
public CheckBoxGroup()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Generated.AtomUIDesktopControls;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Platform;
|
||||
@@ -11,6 +12,7 @@ using Avalonia.Media;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class MenuFlyoutPresenter : MenuBase,
|
||||
IArrowAwareShadowMaskInfoProvider,
|
||||
IMotionAwareControl,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
@@ -11,6 +12,7 @@ namespace AtomUI.Desktop.Controls;
|
||||
|
||||
using AvaloniaMenu = Avalonia.Controls.Menu;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public partial class Menu : AvaloniaMenu,
|
||||
ICustomizableSizeTypeAware,
|
||||
IMotionAwareControl,
|
||||
|
||||
@@ -2,6 +2,7 @@ using AtomUI.Animations;
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Generated.AtomUIDesktopControls;
|
||||
using AtomUI.Reflection;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
@@ -16,6 +17,7 @@ namespace AtomUI.Desktop.Controls;
|
||||
using AvaloniaMenuItem = Avalonia.Controls.MenuItem;
|
||||
|
||||
[PseudoClasses(MenuItemPseudoClass.TopLevel)]
|
||||
[BootstrapRegistration]
|
||||
public class MenuItem : AvaloniaMenuItem, IMenuItemData, IScrollAwareControl
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AtomUI.Generated.AtomUIDesktopControls;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Presenters;
|
||||
@@ -12,6 +13,7 @@ namespace AtomUI.Desktop.Controls;
|
||||
/// 下拉菜单里的「分组标题 + 选项列表」容器,对应 antd 的
|
||||
/// <c>ant-menu-item-group</c>:标题渲染为不可交互的标题行,子项为普通菜单项。
|
||||
/// </summary>
|
||||
[BootstrapRegistration]
|
||||
public class MenuItemGroup : ItemsControl
|
||||
{
|
||||
public static readonly StyledProperty<object?> HeaderProperty =
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
internal class MenuPopupScrollHost : ContentControl, IMotionAwareControl, IScrollAwareControl
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AtomUI.Media;
|
||||
using AtomUI.Registration;
|
||||
using AtomUI.Utils;
|
||||
using Avalonia;
|
||||
using Avalonia.Layout;
|
||||
@@ -8,6 +9,7 @@ namespace AtomUI.Desktop.Controls;
|
||||
|
||||
using AvaloniaSeparator = Avalonia.Controls.Separator;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class MenuSeparator : AvaloniaSeparator
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
@@ -3,6 +3,7 @@ using AtomUI.Controls;
|
||||
using AtomUI.Controls.Primitives;
|
||||
using AtomUI.Media;
|
||||
using AtomUI.MotionScene;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives.PopupPositioning;
|
||||
@@ -18,6 +19,7 @@ namespace AtomUI.Desktop.Controls;
|
||||
|
||||
using AvaloniaPopup = Avalonia.Controls.Primitives.Popup;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class Popup : AvaloniaPopup, IMotionAwareControl
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:Class="AtomUI.Desktop.Controls.Themes.OverlayPopupHostTheme"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type OverlayPopupHost}" TargetType="OverlayPopupHost">
|
||||
<Setter Property="Template">
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using AtomUI.Registration;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace AtomUI.Desktop.Controls.Themes;
|
||||
|
||||
// The bootstrap menu uses this Avalonia-owned target; registration belongs to the local resource.
|
||||
[BootstrapRegistration]
|
||||
internal partial class OverlayPopupHostTheme : ResourceDictionary
|
||||
{
|
||||
public OverlayPopupHostTheme() => AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:Class="AtomUI.Desktop.Controls.Themes.PopupRootTheme"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type PopupRoot}" TargetType="PopupRoot">
|
||||
<Setter Property="Template">
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using AtomUI.Registration;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace AtomUI.Desktop.Controls.Themes;
|
||||
|
||||
// The bootstrap menu uses this Avalonia-owned target; registration belongs to the local resource.
|
||||
[BootstrapRegistration]
|
||||
internal partial class PopupRootTheme : ResourceDictionary
|
||||
{
|
||||
public PopupRootTheme() => AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using AtomUI.Controls.Commons;
|
||||
using AtomUI.Registration;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public partial class RadioButton : AbstractRadioButton
|
||||
{
|
||||
public RadioButton()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using AtomUI.Controls.Commons;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class RadioButtonGroup : AbstractRadioButtonGroup
|
||||
{
|
||||
public RadioButtonGroup()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using AtomUI.Controls.Commons;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class ScrollBar : AbstractScrollBar
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using AtomUI.Controls.Commons;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class ScrollBarThumb : AbstractScrollBarThumb
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using AtomUI.Controls.Commons;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class ScrollViewer : AbstractScrollViewer
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
xmlns:atomc="https://atomui.net/common-controls"
|
||||
x:Class="AtomUI.Desktop.Controls.Themes.AvaScrollBarTheme"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type ScrollBar}" TargetType="ScrollBar">
|
||||
<Style Selector="^:vertical">
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using AtomUI.Registration;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace AtomUI.Desktop.Controls.Themes;
|
||||
|
||||
// The bootstrap menu uses this Avalonia-owned target; registration belongs to the local resource.
|
||||
[BootstrapRegistration]
|
||||
internal partial class AvaScrollBarTheme : ResourceDictionary
|
||||
{
|
||||
public AvaScrollBarTheme() => AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:Class="AtomUI.Desktop.Controls.Themes.AvaScrollViewerTheme"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type ScrollViewer}" TargetType="ScrollViewer">
|
||||
<Setter Property="Template">
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using AtomUI.Registration;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace AtomUI.Desktop.Controls.Themes;
|
||||
|
||||
// The bootstrap menu uses this Avalonia-owned target; registration belongs to the local resource.
|
||||
[BootstrapRegistration]
|
||||
internal partial class AvaScrollViewerTheme : ResourceDictionary
|
||||
{
|
||||
public AvaScrollViewerTheme() => AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Windows.Input;
|
||||
using AtomUI.Animations;
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
@@ -15,6 +16,7 @@ using Avalonia.VisualTree;
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[PseudoClasses(StdPseudoClass.Pressed)]
|
||||
[BootstrapRegistration]
|
||||
public class HyperLinkTextBlock : TemplatedControl, IMotionAwareControl
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AtomUI.Media.TextFormatting;
|
||||
using AtomUI.Registration;
|
||||
using AtomUI.Utils;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
@@ -12,6 +13,7 @@ using Avalonia.VisualTree;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[BootstrapRegistration]
|
||||
public class SelectableTextBlock : TextBlock
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.Runtime.Versioning;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace AtomUI.Desktop.Controls.Themes;
|
||||
|
||||
[UnsupportedOSPlatform("browser")]
|
||||
[BootstrapRegistration]
|
||||
internal partial class WindowDrawnDecorationsTheme : ResourceDictionary
|
||||
{
|
||||
public WindowDrawnDecorationsTheme() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Reactive.Disposables;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
@@ -7,6 +8,7 @@ using Avalonia.Input;
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[global::System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
|
||||
[BootstrapRegistration]
|
||||
internal class FullscreenPopoverLayer : TemplatedControl
|
||||
{
|
||||
private const double PopoverTriggerZoneHeight = 1;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
@@ -7,6 +8,7 @@ using Avalonia.Input;
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[global::System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
|
||||
[BootstrapRegistration]
|
||||
internal class WindowResizer : TemplatedControl
|
||||
{
|
||||
public static readonly StyledProperty<Thickness> GripThicknessProperty =
|
||||
|
||||
@@ -6,6 +6,7 @@ using AtomUI.Desktop.Controls.DesignTokens;
|
||||
using AtomUI.Media;
|
||||
using AtomUI.Native;
|
||||
using AtomUI.Native.MacOS;
|
||||
using AtomUI.Registration;
|
||||
using AtomUI.Theme;
|
||||
using AtomUI.Utils;
|
||||
using Avalonia;
|
||||
@@ -27,6 +28,7 @@ namespace AtomUI.Desktop.Controls;
|
||||
using AvaloniaWindow = Avalonia.Controls.Window;
|
||||
|
||||
[global::System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
|
||||
[BootstrapRegistration]
|
||||
public partial class Window : AvaloniaWindow,
|
||||
IOperationSystemAware,
|
||||
IMediaBreakAwareControl
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Windows.Input;
|
||||
using AtomUI.Animations;
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
@@ -10,6 +11,7 @@ namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[PseudoClasses(StdPseudoClass.Normal, StdPseudoClass.Minimized, StdPseudoClass.Maximized, StdPseudoClass.Fullscreen)]
|
||||
[global::System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
|
||||
[BootstrapRegistration]
|
||||
internal class CaptionButtonGroup : TemplatedControl, IOperationSystemAware
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Windows.Input;
|
||||
using AtomUI.Animations;
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
@@ -17,6 +18,7 @@ namespace AtomUI.Desktop.Controls;
|
||||
[PseudoClasses(StdPseudoClass.Active)]
|
||||
[PseudoClasses(StdPseudoClass.Normal, StdPseudoClass.Minimized, StdPseudoClass.Maximized, StdPseudoClass.Fullscreen)]
|
||||
[global::System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
|
||||
[BootstrapRegistration]
|
||||
public class WindowTitleBar : TemplatedControl,
|
||||
IMotionAwareControl,
|
||||
IOperationSystemAware
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
@@ -5,6 +6,7 @@ namespace AtomUI.Desktop.Controls;
|
||||
/// <summary>
|
||||
/// An icon button intended for application-owned actions in a window title bar add-on.
|
||||
/// </summary>
|
||||
[BootstrapRegistration]
|
||||
public class WindowTitleBarButton : IconButton
|
||||
{
|
||||
internal static readonly AttachedProperty<bool> IsWindowActiveProperty =
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using AtomUI.Registration;
|
||||
using Avalonia;
|
||||
using Avalonia.Input;
|
||||
|
||||
@@ -6,6 +7,7 @@ namespace AtomUI.Desktop.Controls;
|
||||
/// <summary>
|
||||
/// A checked/unchecked icon button intended for application-owned actions in a window title bar add-on.
|
||||
/// </summary>
|
||||
[BootstrapRegistration]
|
||||
public class WindowTitleBarToggleButton : ToggleIconButton
|
||||
{
|
||||
internal static readonly AttachedProperty<bool> IsWindowActiveProperty =
|
||||
|
||||
@@ -10,7 +10,9 @@ internal static class AtomUIDiagnosticIds
|
||||
public const string RegistrationIdentityConflict = "ATOMUIREG005";
|
||||
public const string RegistrationUnsupportedBackend = "ATOMUIREG006";
|
||||
public const string RegistrationUnloweredTypeMapAccessor = "ATOMUIREG007";
|
||||
// Retired diagnostic: package collection no longer has backend-specific startup partitions.
|
||||
public const string RegistrationStagingFallback = "ATOMUIREG009";
|
||||
public const string RegistrationInvalidBootstrap = "ATOMUIREG010";
|
||||
public const string AotMissingGeneratedAccessor = "ATOMUIAOT001";
|
||||
public const string AotMissingGeneratedPath = "ATOMUIAOT002";
|
||||
public const string AotUnverifiableDataMemberPath = "ATOMUIAOT003";
|
||||
|
||||
@@ -11,13 +11,6 @@ internal static partial class AtomUIDiagnosticDescriptors
|
||||
internal static readonly DiagnosticDescriptor RegistrationInvalidResourceDependency = CreateRegistrationDiagnostic(AtomUIDiagnosticIds.RegistrationInvalidResourceDependency, "Resource dependency is invalid", "Resource dependency is invalid: {0}");
|
||||
internal static readonly DiagnosticDescriptor RegistrationIdentityConflict = CreateRegistrationDiagnostic(AtomUIDiagnosticIds.RegistrationIdentityConflict, "Registration identity conflicts", "Registration identity is conflicting or invalid: {0}");
|
||||
internal static readonly DiagnosticDescriptor RegistrationUnsupportedBackend = CreateRegistrationDiagnostic(AtomUIDiagnosticIds.RegistrationUnsupportedBackend, "Registration ABI or platform is unsupported", "Registration ABI or platform is unsupported: {0}");
|
||||
internal static readonly DiagnosticDescriptor RegistrationStagingFallback = new(
|
||||
AtomUIDiagnosticIds.RegistrationStagingFallback,
|
||||
"Registration staging falls back to one stage",
|
||||
"Registration staging falls back to one stage: {0}",
|
||||
AtomUIDiagnosticCategories.Registration,
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
customTags: [WellKnownDiagnosticTags.Telemetry]);
|
||||
internal static readonly DiagnosticDescriptor RegistrationInvalidBootstrap = CreateRegistrationDiagnostic(AtomUIDiagnosticIds.RegistrationInvalidBootstrap, "Bootstrap registration is invalid", "Bootstrap registration must identify a control fragment or an exported theme resource class: {0}");
|
||||
private static DiagnosticDescriptor CreateRegistrationDiagnostic(string id, string title, string message) => new(id, title, message, AtomUIDiagnosticCategories.Registration, DiagnosticSeverity.Error, true, customTags: [WellKnownDiagnosticTags.Telemetry]);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,10 @@ internal sealed record RegistrationAsset(string Id, string Uri, string Wrapper,
|
||||
}
|
||||
internal sealed record RegistrationControl(RegistrationType Type, string? TokenFactory, string? SemanticFactory,
|
||||
ValueArray<string> Triggers, ValueArray<string> Guards, ValueArray<string> Assets,
|
||||
ValueArray<RegistrationTrigger> ConditionalTriggers = default);
|
||||
ValueArray<RegistrationTrigger> ConditionalTriggers = default)
|
||||
{
|
||||
internal bool? IsBootstrap { get; init; }
|
||||
}
|
||||
internal sealed record RegistrationTrigger(RegistrationType Type, string Kind);
|
||||
internal sealed record RegistrationPackage(string Id, string AssemblyIdentity, string Namespace,
|
||||
ValueArray<RegistrationControl> Controls, ValueArray<RegistrationAsset> Assets, ValueArray<string> CompiledGlobalTokenNames,
|
||||
|
||||
@@ -296,6 +296,8 @@ internal sealed class RegistrationModelBuilder
|
||||
semantic is null ? null : SemanticPartManifestWriter.GetFactoryName(semantic), new(triggers.Select(t => t.Type.Name).Distinct().OrderBy(t => t, StringComparer.Ordinal)),
|
||||
new(Availability(symbol).Guards()), new(refs), new(triggers.Distinct().OrderBy(t => t.Type.Name, StringComparer.Ordinal))));
|
||||
}
|
||||
ApplyBootstrapRegistration(controls, assets);
|
||||
|
||||
foreach (var conflict in tokens.Where(t => t.HasDescriptor).GroupBy(t => t.ControlName, StringComparer.Ordinal).Where(g => g.Select(t => t.ControlTypeName).Distinct().Count() > 1))
|
||||
{
|
||||
_report(Diagnostic.Create(AtomUIDiagnosticDescriptors.RegistrationIdentityConflict, Location.None, _catalog + ":" + conflict.Key));
|
||||
@@ -324,6 +326,84 @@ internal sealed class RegistrationModelBuilder
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyBootstrapRegistration(List<RegistrationControl> controls, IReadOnlyList<RegistrationAsset> assets)
|
||||
{
|
||||
var marked = _types.Where(type => type.GetAttributes().Any(attribute =>
|
||||
attribute.AttributeClass?.ToDisplayString() == "AtomUI.Registration.BootstrapRegistrationAttribute")).ToArray();
|
||||
if (marked.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var byType = controls.ToDictionary(control => control.Type.AssemblyQualifiedName, StringComparer.Ordinal);
|
||||
var byAsset = assets.ToDictionary(asset => asset.Id, StringComparer.Ordinal);
|
||||
var selected = new HashSet<string>(StringComparer.Ordinal);
|
||||
var pending = new Queue<RegistrationControl>();
|
||||
void Select(RegistrationType type)
|
||||
{
|
||||
if (byType.TryGetValue(type.AssemblyQualifiedName, out var control) && selected.Add(type.AssemblyQualifiedName))
|
||||
{
|
||||
pending.Enqueue(control);
|
||||
}
|
||||
}
|
||||
void SelectAsset(RegistrationAsset asset)
|
||||
{
|
||||
// Reuse registration ownership facts. This is not a template or application call graph.
|
||||
foreach (var export in asset.Exports)
|
||||
{
|
||||
Select(export.Target);
|
||||
}
|
||||
foreach (var owner in asset.Owners)
|
||||
{
|
||||
Select(owner.Type);
|
||||
}
|
||||
foreach (var binding in asset.Bindings)
|
||||
{
|
||||
Select(binding.Owner.Type);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var symbol in marked)
|
||||
{
|
||||
var type = RegistrationType.From(symbol);
|
||||
if (byType.ContainsKey(type.AssemblyQualifiedName))
|
||||
{
|
||||
Select(type);
|
||||
continue;
|
||||
}
|
||||
var resourceAssets = _parsed.Values.Where(document =>
|
||||
document.Input.ResourceClassName == symbol.ToDisplayString())
|
||||
.Select(document => _packageId + ":" + document.Input.AssetPath)
|
||||
.Where(byAsset.ContainsKey).Select(id => byAsset[id]).ToArray();
|
||||
if (resourceAssets.Length == 0)
|
||||
{
|
||||
_report(Diagnostic.Create(AtomUIDiagnosticDescriptors.RegistrationInvalidBootstrap,
|
||||
symbol.Locations.FirstOrDefault(), type.Name));
|
||||
continue;
|
||||
}
|
||||
foreach (var asset in resourceAssets)
|
||||
{
|
||||
SelectAsset(asset);
|
||||
}
|
||||
}
|
||||
|
||||
var visitedAssets = new HashSet<string>(StringComparer.Ordinal);
|
||||
while (pending.Count != 0)
|
||||
{
|
||||
foreach (var assetId in pending.Dequeue().Assets)
|
||||
{
|
||||
if (visitedAssets.Add(assetId))
|
||||
{
|
||||
SelectAsset(byAsset[assetId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var index = 0; index < controls.Count; index++)
|
||||
{
|
||||
controls[index] = controls[index] with { IsBootstrap = selected.Contains(controls[index].Type.AssemblyQualifiedName) };
|
||||
}
|
||||
}
|
||||
|
||||
private ValueArray<RegistrationResourceKey>? CollectCompleteResourceKeys(ThemeAssetInfo input, XElement root)
|
||||
{
|
||||
// 手写字典可能在构造/重写中增加任意键;导出主题表并不是完整资源键集合。
|
||||
|
||||
@@ -10,8 +10,7 @@ internal static class TypeMapRegistrationWriter
|
||||
GenerationOutput context,
|
||||
RegistrationPackage package,
|
||||
Func<string, string>? keyHash = null,
|
||||
bool supportsTypeMap = true,
|
||||
HashSet<string>? criticalControls = null)
|
||||
bool supportsTypeMap = true)
|
||||
{
|
||||
if (package.Controls.Count == 0 && package.Assets.Count == 0)
|
||||
{
|
||||
@@ -35,31 +34,14 @@ internal static class TypeMapRegistrationWriter
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!supportsTypeMap && criticalControls is { Count: > 0 })
|
||||
{
|
||||
// 条件(net8)后端只有单段 Collect 模板,没有分区孪生——分区发射会引用不存在的
|
||||
// CollectCritical/CollectDeferred 导致产物不可编译且错误不指向根因。显式降级为
|
||||
// 不分区并诊断告知(不静默)。
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
AtomUIDiagnosticDescriptors.RegistrationStagingFallback,
|
||||
Location.None,
|
||||
"AtomUICriticalControls requires the TypeMap registration backend; the conditional (net8) " +
|
||||
"backend ignores critical controls and registers the package in one stage."));
|
||||
criticalControls = null;
|
||||
}
|
||||
context.AddSource(
|
||||
"GeneratedTypeMapRegistration.g.cs",
|
||||
GeneratedSourceText.From(Build(package, groupName, mapEntries, supportsTypeMap, criticalControls)));
|
||||
GeneratedSourceText.From(Build(package, groupName, mapEntries, supportsTypeMap)));
|
||||
}
|
||||
|
||||
private static string Build(RegistrationPackage package, string groupName, IReadOnlyList<TypeMapEntry> mapEntries,
|
||||
bool supportsTypeMap, HashSet<string>? criticalControls)
|
||||
bool supportsTypeMap)
|
||||
{
|
||||
// 分区判定(分级启动):关键集属性非空且为真子集时才分区发射。
|
||||
var partitioned = criticalControls is { Count: > 0 } &&
|
||||
package.Controls.Any(c => !CriticalControlMatcher.IsCritical(c, criticalControls)) &&
|
||||
package.Controls.Any(c => CriticalControlMatcher.IsCritical(c, criticalControls));
|
||||
|
||||
var source = new StringBuilder("// <auto-generated />\n#nullable enable\n");
|
||||
var group = "global::" + package.Namespace + "." + groupName;
|
||||
source.Append("[assembly: global::AtomUI.Registration.ControlPackageMarker(").Append(L(package.Id)).Append(", typeof(").Append(group).AppendLine("), 1)]");
|
||||
@@ -90,100 +72,34 @@ internal static class TypeMapRegistrationWriter
|
||||
source.AppendLine("internal static partial class GeneratedControlPackageRegistration\n{");
|
||||
if (supportsTypeMap)
|
||||
{
|
||||
if (partitioned)
|
||||
source.AppendLine(" private static readonly string[] CandidateKeys = new string[]\n {");
|
||||
foreach (var entry in mapEntries)
|
||||
{
|
||||
// 分区发射(分级启动):关键集/余量两段取代全量清单(合集与全量逐项等价),
|
||||
// 不发射全量数组——分区路径不引用它,发射即死数据(AOT 静态体积)。
|
||||
source.AppendLine(" private static readonly string[] CandidateKeysCritical = new string[]\n {");
|
||||
foreach (var entry in mapEntries)
|
||||
{
|
||||
if (CriticalControlMatcher.IsCritical(entry.Control, criticalControls!))
|
||||
{
|
||||
source.Append(" ").Append(L(entry.Key)).AppendLine(",");
|
||||
}
|
||||
}
|
||||
source.AppendLine(" };");
|
||||
source.AppendLine(" private static readonly string[] CandidateKeysDeferred = new string[]\n {");
|
||||
foreach (var entry in mapEntries)
|
||||
{
|
||||
if (!CriticalControlMatcher.IsCritical(entry.Control, criticalControls!))
|
||||
{
|
||||
source.Append(" ").Append(L(entry.Key)).AppendLine(",");
|
||||
}
|
||||
}
|
||||
source.AppendLine(" };");
|
||||
}
|
||||
else
|
||||
{
|
||||
source.AppendLine(" private static readonly string[] CandidateKeys = new string[]\n {");
|
||||
foreach (var entry in mapEntries)
|
||||
{
|
||||
source.Append(" ").Append(L(entry.Key)).AppendLine(",");
|
||||
}
|
||||
source.AppendLine(" };");
|
||||
source.Append(" ").Append(L(entry.Key)).AppendLine(",");
|
||||
}
|
||||
source.AppendLine(" };");
|
||||
}
|
||||
if (partitioned)
|
||||
{
|
||||
// 分区发射(分级启动):关键集先行 + 余量委托。未分区包保持既有入口原样;
|
||||
// 分区包调用独立 ABI 方法,避免新增重载破坏既有 lambda 调用的源码解析。
|
||||
source.AppendLine(" internal static global::AtomUI.IAtomUIBuilder Register(global::AtomUI.IAtomUIBuilder builder,");
|
||||
source.AppendLine(" global::System.Func<global::AtomUI.Theme.Resources.IControlThemesProvider> createProvider,");
|
||||
source.AppendLine(" global::System.Action<global::AtomUI.IAtomUIBuilder>? prepare = null, global::System.Action<global::AtomUI.IAtomUIBuilder>? complete = null)");
|
||||
source.Append(" => global::AtomUI.Registration.ControlRegistrationRuntime.RegisterPartitionedPackage(builder, ").Append(L(package.Id))
|
||||
.AppendLine(", createProvider, CollectCritical, CollectDeferred, prepare, complete);");
|
||||
source.AppendLine(" internal static global::AtomUI.IAtomUIBuilder Ensure(global::AtomUI.IAtomUIBuilder builder,");
|
||||
source.AppendLine(" global::System.Func<global::AtomUI.Theme.Resources.IControlThemesProvider> createProvider,");
|
||||
source.AppendLine(" global::System.Action<global::AtomUI.IAtomUIBuilder>? prepare = null, global::System.Action<global::AtomUI.IAtomUIBuilder>? complete = null)");
|
||||
source.Append(" => global::AtomUI.Registration.ControlRegistrationRuntime.EnsurePartitionedPackage(builder, ").Append(L(package.Id))
|
||||
.AppendLine(", createProvider, CollectCritical, CollectDeferred, prepare, complete);");
|
||||
}
|
||||
else
|
||||
{
|
||||
source.AppendLine(" internal static global::AtomUI.IAtomUIBuilder Register(global::AtomUI.IAtomUIBuilder builder,");
|
||||
source.AppendLine(" global::System.Func<global::AtomUI.Theme.Resources.IControlThemesProvider> createProvider,");
|
||||
source.AppendLine(" global::System.Action<global::AtomUI.IAtomUIBuilder>? prepare = null, global::System.Action<global::AtomUI.IAtomUIBuilder>? complete = null)");
|
||||
source.Append(" => global::AtomUI.Registration.ControlRegistrationRuntime.RegisterPackage(builder, ").Append(L(package.Id)).AppendLine(", createProvider, Collect, prepare, complete);");
|
||||
source.AppendLine(" internal static global::AtomUI.IAtomUIBuilder Ensure(global::AtomUI.IAtomUIBuilder builder,");
|
||||
source.AppendLine(" global::System.Func<global::AtomUI.Theme.Resources.IControlThemesProvider> createProvider,");
|
||||
source.AppendLine(" global::System.Action<global::AtomUI.IAtomUIBuilder>? prepare = null, global::System.Action<global::AtomUI.IAtomUIBuilder>? complete = null)");
|
||||
source.Append(" => global::AtomUI.Registration.ControlRegistrationRuntime.EnsurePackage(builder, ").Append(L(package.Id)).AppendLine(", createProvider, Collect, prepare, complete);");
|
||||
}
|
||||
source.AppendLine(" internal static global::AtomUI.IAtomUIBuilder Register(global::AtomUI.IAtomUIBuilder builder,");
|
||||
source.AppendLine(" global::System.Func<global::AtomUI.Theme.Resources.IControlThemesProvider> createProvider,");
|
||||
source.AppendLine(" global::System.Action<global::AtomUI.IAtomUIBuilder>? prepare = null, global::System.Action<global::AtomUI.IAtomUIBuilder>? complete = null)");
|
||||
source.Append(" => global::AtomUI.Registration.ControlRegistrationRuntime.RegisterPackage(builder, ").Append(L(package.Id)).AppendLine(", createProvider, Collect, prepare, complete);");
|
||||
source.AppendLine(" internal static global::AtomUI.IAtomUIBuilder Ensure(global::AtomUI.IAtomUIBuilder builder,");
|
||||
source.AppendLine(" global::System.Func<global::AtomUI.Theme.Resources.IControlThemesProvider> createProvider,");
|
||||
source.AppendLine(" global::System.Action<global::AtomUI.IAtomUIBuilder>? prepare = null, global::System.Action<global::AtomUI.IAtomUIBuilder>? complete = null)");
|
||||
source.Append(" => global::AtomUI.Registration.ControlRegistrationRuntime.EnsurePackage(builder, ").Append(L(package.Id)).AppendLine(", createProvider, Collect, prepare, complete);");
|
||||
if (supportsTypeMap)
|
||||
{
|
||||
var collectName = partitioned ? "CollectCritical" : "Collect";
|
||||
source.Append(" private static void ").Append(collectName).AppendLine("(global::AtomUI.Registration.ControlPackageRegistrationBuilder builder)\n {");
|
||||
source.AppendLine(" private static void Collect(global::AtomUI.Registration.ControlPackageRegistrationBuilder builder)\n {");
|
||||
source.AppendLine(" if (global::AtomUI.Registration.ControlRegistrationRuntime.IsTrimmed)\n {");
|
||||
source.Append(" global::AtomUI.Registration.ControlRegistrationRuntime.CollectFragments(builder, GetTypeMap(), ").Append(partitioned ? "CandidateKeysCritical" : "CandidateKeys").AppendLine(");");
|
||||
source.AppendLine(" global::AtomUI.Registration.ControlRegistrationRuntime.CollectFragments(builder, GetTypeMap(), CandidateKeys);");
|
||||
source.AppendLine(" }\n else\n {");
|
||||
// 完整常量只描述非裁剪包;Core 在余量消费前拒绝组合,完整收集后恢复快路径。
|
||||
// 裁剪路径按实际选中 schema 计算,不能把未保留的 fragment 纳入 Revision 证据。
|
||||
// Only the untrimmed collector has evidence for the complete package fingerprint.
|
||||
source.AppendLine(" builder.PackageSchemaFingerprint = GeneratedRegistrationFactories.PackageSchemaFingerprint;");
|
||||
foreach (var control in package.Controls)
|
||||
{
|
||||
if (partitioned && !CriticalControlMatcher.IsCritical(control, criticalControls!))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
source.Append(" new ").Append(RegistrationNames.Proxy(control)).AppendLine("().Add(builder);");
|
||||
source.Append(" builder.AddFragment(new ").Append(RegistrationNames.Proxy(control)).AppendLine("());");
|
||||
}
|
||||
source.AppendLine(" }\n }");
|
||||
if (partitioned)
|
||||
{
|
||||
source.AppendLine(" private static void CollectDeferred(global::AtomUI.Registration.ControlPackageRegistrationBuilder builder)\n {");
|
||||
source.AppendLine(" if (global::AtomUI.Registration.ControlRegistrationRuntime.IsTrimmed)\n {");
|
||||
source.AppendLine(" global::AtomUI.Registration.ControlRegistrationRuntime.CollectFragments(builder, GetTypeMap(), CandidateKeysDeferred);");
|
||||
source.AppendLine(" }\n else\n {");
|
||||
foreach (var control in package.Controls)
|
||||
{
|
||||
if (CriticalControlMatcher.IsCritical(control, criticalControls!))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
source.Append(" new ").Append(RegistrationNames.Proxy(control)).AppendLine("().Add(builder);");
|
||||
}
|
||||
source.AppendLine(" }\n }");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -219,10 +135,14 @@ internal static class TypeMapRegistrationWriter
|
||||
source.AppendLine(" }");
|
||||
}
|
||||
source.Append(" public override string FragmentId => ").Append(L(package.Id + ":" + control.Type.AssemblyQualifiedName)).AppendLine(";");
|
||||
if (control.IsBootstrap is { } bootstrap)
|
||||
{
|
||||
source.Append(" public override bool IsBootstrap => ").Append(bootstrap ? "true" : "false").AppendLine(";");
|
||||
}
|
||||
source.AppendLine(" public override void Add(global::AtomUI.Registration.ControlPackageRegistrationBuilder builder)\n {");
|
||||
foreach (var guard in control.Guards)
|
||||
{
|
||||
source.Append(" if (").Append(guard).AppendLine(") return;");
|
||||
source.Append(" if (").Append(guard).AppendLine(") { return; }");
|
||||
}
|
||||
|
||||
if (control.TokenFactory is not null)
|
||||
@@ -263,7 +183,7 @@ internal static class TypeMapRegistrationWriter
|
||||
source.Append(" internal static void AddAsset_").Append(RegistrationNames.Hash(asset.Id)).AppendLine("(global::AtomUI.Registration.ControlPackageRegistrationBuilder builder)\n {");
|
||||
foreach (var guard in asset.Guards)
|
||||
{
|
||||
source.Append(" if (").Append(guard).AppendLine(") return;");
|
||||
source.Append(" if (").Append(guard).AppendLine(") { return; }");
|
||||
}
|
||||
|
||||
WriteAssetRegistration(source, asset, " ");
|
||||
@@ -341,7 +261,7 @@ internal static class TypeMapRegistrationWriter
|
||||
source.Append(" internal static ").Append(type).Append(' ').Append(name).AppendLine("\n {");
|
||||
source.AppendLine(" get\n {");
|
||||
source.Append(" var cached = global::System.Threading.Volatile.Read(ref ").Append(field).AppendLine(");");
|
||||
source.AppendLine(" if (cached is not null) return cached;");
|
||||
source.AppendLine(" if (cached is not null) { return cached; }");
|
||||
source.Append(" var created = ").Append(expression).AppendLine(";");
|
||||
source.Append(" return global::System.Threading.Interlocked.CompareExchange(ref ").Append(field)
|
||||
.AppendLine(", created, null) ?? created;");
|
||||
@@ -384,20 +304,3 @@ internal static class TypeMapRegistrationWriter
|
||||
private sealed record TypeMapEntry(RegistrationControl Control, string Trigger, string Key);
|
||||
private static string L(string value) => RegistrationNames.Literal(value);
|
||||
}
|
||||
|
||||
/// <summary>分区判定:按控件类型简单名匹配(AtomUICriticalControls 属性值,逗号分隔)。</summary>
|
||||
internal static class CriticalControlMatcher
|
||||
{
|
||||
internal static bool IsCritical(RegistrationControl? control, HashSet<string> criticalControls)
|
||||
{
|
||||
// RegistrationType.Name 是全限定显示名、MetadataName 是命名空间限定名("Ns.Button");
|
||||
// 关键集属性按控件简单名匹配(截取最后 '.'/'+' 段,嵌套类型取内层名)。
|
||||
if (control?.Type?.MetadataName is not { } qualified || qualified.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var separator = qualified.LastIndexOfAny(['.', '+']);
|
||||
var simpleName = separator < 0 ? qualified : qualified.Substring(separator + 1);
|
||||
return criticalControls.Contains(simpleName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,22 +121,8 @@ public class TokenResourceKeyGenerator : IIncrementalGenerator
|
||||
{
|
||||
if (RegistrationFrameworkResolver.TryResolve(compilation, options.GlobalOptions, out var framework, out var error))
|
||||
{
|
||||
// 分级启动分区:控件包项目以 AtomUICriticalControls(逗号分隔的控件类型名;
|
||||
// 同时兼容直接 analyzer options 中的分号,MSBuild CompilerVisibleProperty
|
||||
// 会经 .editorconfig 传值,分号在该格式中是注释起始符),
|
||||
// 需 CompilerVisibleProperty 暴露。生成器据此分区发射收集方法。
|
||||
HashSet<string>? criticalControls = null;
|
||||
if (options.GlobalOptions.TryGetValue("build_property.AtomUICriticalControls", out var criticalList) &&
|
||||
!string.IsNullOrWhiteSpace(criticalList))
|
||||
{
|
||||
criticalControls = new HashSet<string>(
|
||||
criticalList.Split([',', ';']).Select(static name => name.Trim())
|
||||
.Where(static name => name.Length > 0),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
TypeMapRegistrationWriter.Write(context, package,
|
||||
supportsTypeMap: framework == AtomUI.Build.Tasks.Registration.RegistrationFrameworkFamily.OfficialTypeMap,
|
||||
criticalControls: criticalControls);
|
||||
supportsTypeMap: framework == AtomUI.Build.Tasks.Registration.RegistrationFrameworkFamily.OfficialTypeMap);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -372,7 +372,7 @@ public class ControlRegistrationRuntimeTests
|
||||
deferred.AddThemeAsset(sharedAsset, sharedResource);
|
||||
deferred.AddResource(CreateOrderedResource("stage-one-deferred", 10, "low"));
|
||||
});
|
||||
using var manager = builder.ThemeManagerBuilder.Build(deferPackageCollects: true);
|
||||
using var manager = builder.ThemeManagerBuilder.Build();
|
||||
manager.InitializeApplication(application);
|
||||
var stageTwoProvider = new Provider("StageTwo.Package");
|
||||
var stageTwoSemanticIdentity = ControlTokenIdentity.ForControl(
|
||||
@@ -405,7 +405,6 @@ public class ControlRegistrationRuntimeTests
|
||||
},
|
||||
deferred => deferred.AddResource(CreateOrderedResource("deferred", 20, "high")));
|
||||
builder.ThemeManagerBuilder.CloseStageTwoWindow();
|
||||
builder.ThemeManagerBuilder.RunDeferredCollects();
|
||||
builder.ThemeManagerBuilder.MergeStageTwoInto(manager);
|
||||
await manager.ExecuteRegistrationEvolutionAsync();
|
||||
|
||||
|
||||
@@ -35,9 +35,8 @@ public class NumericUpDownRootSurfaceTests
|
||||
{
|
||||
var builder = new AtomUIBuilder(new Application());
|
||||
builder.UseDesktopControls();
|
||||
// 分区包:余量段须先收集,才能看到包的完整贡献(关键集 + 余量)。
|
||||
builder.ThemeManagerBuilder.RunDeferredCollects();
|
||||
var registration = builder.ThemeManagerBuilder.EnumerateContributions()
|
||||
builder.ThemeManagerBuilder.CompletePendingRegistrations();
|
||||
var registration = builder.ThemeManagerBuilder.ControlPackages
|
||||
.Where(p => p.Id == "AtomUI.Desktop.Controls")
|
||||
.SelectMany(p => p.Resources)
|
||||
.Single(r => r.AssetId.EndsWith("NumericUpDown/Themes/NumericUpDownSpinnerTheme.axaml", StringComparison.Ordinal));
|
||||
|
||||
@@ -70,16 +70,15 @@ public class TypedPackageEntryTests
|
||||
{
|
||||
stageOneAssets.ShouldContain(chromeAsset);
|
||||
}
|
||||
// 分区包:余量段须先收集,才能看到包的完整贡献(关键集 + 余量)。
|
||||
firstBuilder.ThemeManagerBuilder.RunDeferredCollects();
|
||||
secondBuilder.ThemeManagerBuilder.RunDeferredCollects();
|
||||
|
||||
firstBuilder.ThemeManagerBuilder.CompletePendingRegistrations();
|
||||
secondBuilder.ThemeManagerBuilder.CompletePendingRegistrations();
|
||||
var firstContributions = firstBuilder.ThemeManagerBuilder
|
||||
.EnumerateContributions()
|
||||
.ControlPackages
|
||||
.Where(package => package.Id == "AtomUI.Desktop.Controls")
|
||||
.ToArray();
|
||||
var secondContributions = secondBuilder.ThemeManagerBuilder
|
||||
.EnumerateContributions()
|
||||
.ControlPackages
|
||||
.Where(package => package.Id == "AtomUI.Desktop.Controls")
|
||||
.ToArray();
|
||||
var firstPackage = firstContributions[0];
|
||||
|
||||
@@ -593,9 +593,9 @@ public partial class TypeMapIntegrationTests
|
||||
internal class ChromeTheme : Avalonia.Controls.ResourceDictionary { }
|
||||
""", Asset("Chrome", "<ControlTheme x:Key=\"Chrome\" TargetType=\"local:Chrome\" />", "Demo.ChromeTheme"));
|
||||
var source = Source(result, "GeneratedTypeMapRegistration.g.cs");
|
||||
source.ShouldContain("if (global::System.OperatingSystem.IsBrowser()) return;");
|
||||
source.ShouldContain("if (global::System.OperatingSystem.IsBrowser()) { return; }");
|
||||
source.ShouldContain("internal static void AddAsset_");
|
||||
source.IndexOf("if (global::System.OperatingSystem.IsBrowser()) return;", StringComparison.Ordinal)
|
||||
source.IndexOf("if (global::System.OperatingSystem.IsBrowser()) { return; }", StringComparison.Ordinal)
|
||||
.ShouldBeLessThan(source.IndexOf("builder.AddThemeAsset(", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,9 @@ public class TypeMapRegistrationGeneratorTests
|
||||
source.ShouldContain("ControlPackageMarker(");
|
||||
source.ShouldContain("GeneratedTypeMapAccessor(typeof(");
|
||||
source.ShouldContain("GetOrCreateExternalTypeMapping<");
|
||||
source.ShouldContain("ControlRegistrationRuntime.CollectFragments(builder, GetTypeMap(), CandidateKeysCritical)");
|
||||
source.ShouldContain("ControlRegistrationRuntime.CollectFragments(builder, GetTypeMap(), CandidateKeysDeferred)");
|
||||
source.ShouldContain("ControlRegistrationRuntime.CollectFragments(builder, GetTypeMap(), CandidateKeys)");
|
||||
source.ShouldNotContain("CollectCritical");
|
||||
source.ShouldNotContain("CollectDeferred");
|
||||
source.ShouldNotContain("UnitId");
|
||||
source.ShouldNotContain("AotTrim");
|
||||
AssertCompiles(result);
|
||||
@@ -173,42 +174,32 @@ public class TypeMapRegistrationGeneratorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Critical_Registration_Falls_Back_To_One_Stage_On_The_Conditional_Backend()
|
||||
public void Both_Backends_Emit_A_Single_Collector_With_Bootstrap_Fragment_Metadata()
|
||||
{
|
||||
var package = CreateStagingPackage();
|
||||
var fallbackOutput = new GenerationOutput();
|
||||
|
||||
TypeMapRegistrationWriter.Write(
|
||||
fallbackOutput,
|
||||
package,
|
||||
supportsTypeMap: false,
|
||||
criticalControls: new HashSet<string>(["Button"], StringComparer.Ordinal));
|
||||
|
||||
var fallbackResult = fallbackOutput.Freeze();
|
||||
var diagnostic = fallbackResult.Diagnostics.ShouldHaveSingleItem();
|
||||
diagnostic.Descriptor.Id.ShouldBe("ATOMUIREG009");
|
||||
diagnostic.Descriptor.DefaultSeverity.ShouldBe(DiagnosticSeverity.Warning);
|
||||
fallbackResult.Files.Single(file => file.HintName == "GeneratedTypeMapRegistration.g.cs")
|
||||
.Text.ShouldContain("private static void Collect(");
|
||||
|
||||
var ordinaryConditionalOutput = new GenerationOutput();
|
||||
TypeMapRegistrationWriter.Write(
|
||||
ordinaryConditionalOutput,
|
||||
package,
|
||||
supportsTypeMap: false);
|
||||
ordinaryConditionalOutput.Freeze().Diagnostics.ShouldBeEmpty();
|
||||
|
||||
var typeMapOutput = new GenerationOutput();
|
||||
TypeMapRegistrationWriter.Write(
|
||||
typeMapOutput,
|
||||
package,
|
||||
supportsTypeMap: true,
|
||||
criticalControls: new HashSet<string>(["Button"], StringComparer.Ordinal));
|
||||
var typeMapResult = typeMapOutput.Freeze();
|
||||
typeMapResult.Diagnostics.ShouldBeEmpty();
|
||||
var typeMapSource = typeMapResult.Files.ShouldHaveSingleItem().Text;
|
||||
typeMapSource.ShouldContain("private static void CollectCritical");
|
||||
typeMapSource.ShouldContain("private static void CollectDeferred");
|
||||
foreach (var supportsTypeMap in new[] { false, true })
|
||||
{
|
||||
var output = new GenerationOutput();
|
||||
var package = CreateStagingPackage();
|
||||
package = package with { Controls = new(package.Controls.Select((control, index) => control with { IsBootstrap = index == 0 })) };
|
||||
TypeMapRegistrationWriter.Write(output, package, supportsTypeMap: supportsTypeMap);
|
||||
var result = output.Freeze();
|
||||
result.Diagnostics.ShouldBeEmpty();
|
||||
var source = result.Files.Single(file => file.HintName == "GeneratedTypeMapRegistration.g.cs").Text;
|
||||
source.ShouldContain("private static void Collect(");
|
||||
source.ShouldContain("ControlRegistrationRuntime.RegisterPackage(");
|
||||
source.ShouldNotContain("CollectCritical");
|
||||
source.ShouldNotContain("CollectDeferred");
|
||||
source.ShouldContain("public override bool IsBootstrap => true;");
|
||||
source.ShouldContain("public override bool IsBootstrap => false;");
|
||||
if (!supportsTypeMap)
|
||||
{
|
||||
var tree = CSharpSyntaxTree.ParseText(source, cancellationToken: TestContext.Current.CancellationToken);
|
||||
var full = tree.GetRoot(TestContext.Current.CancellationToken).DescendantNodes().OfType<Microsoft.CodeAnalysis.CSharp.Syntax.MethodDeclarationSyntax>()
|
||||
.Single(method => method.Identifier.ValueText == "CollectFull");
|
||||
full.Body!.Statements.ShouldAllBe(statement => statement is Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionStatementSyntax);
|
||||
full.Body.Statements.Count.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user