mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-16 21:21:03 +08:00
fix(Startup): hand off Splash after main window first frame
This commit is contained in:
@@ -6,7 +6,8 @@ using AtomUI.Localization;
|
|||||||
using AtomUI.Theme;
|
using AtomUI.Theme;
|
||||||
using AtomUI.Toolkits.GalleryBase;
|
using AtomUI.Toolkits.GalleryBase;
|
||||||
using AtomUIGallery.Localization;
|
using AtomUIGallery.Localization;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using AtomUIGallery.Workspace.Views;
|
||||||
|
using Window = Avalonia.Controls.Window;
|
||||||
|
|
||||||
namespace AtomUIGallery.Desktop;
|
namespace AtomUIGallery.Desktop;
|
||||||
|
|
||||||
@@ -38,22 +39,6 @@ public partial class GalleryApplication : BaseGalleryApplication
|
|||||||
builder.UseSplash(SplashMode.Auto, splash => splash
|
builder.UseSplash(SplashMode.Auto, splash => splash
|
||||||
.WithLogo(new Uri("avares://AtomUIGallery.Desktop/Assets/Images/AppLogo.iconset/icon_128x128@2x.png"))
|
.WithLogo(new Uri("avares://AtomUIGallery.Desktop/Assets/Images/AppLogo.iconset/icon_128x128@2x.png"))
|
||||||
.WithTitle("AtomUI Gallery"));
|
.WithTitle("AtomUI Gallery"));
|
||||||
}
|
builder.UseMainWindow(static _ => Task.FromResult<Window>(new WorkspaceWindow()));
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
|
||||||
{
|
|
||||||
switch (ApplicationLifetime)
|
|
||||||
{
|
|
||||||
case IClassicDesktopStyleApplicationLifetime desktop:
|
|
||||||
desktop.MainWindow = CreateWorkspaceWindow();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,12 +102,13 @@ public static void AtomUIStartup.Attach(Application application);
|
|||||||
生命周期已启动)。不是经典桌面生命周期(headless、single-view)时不投递,直接进入 `Ready`。判定主窗体:
|
生命周期已启动)。不是经典桌面生命周期(headless、single-view)时不投递,直接进入 `Ready`。判定主窗体:
|
||||||
|
|
||||||
1. 配置了 `UseMainWindow`:在 UI 线程执行工厂(可 await,经 `AtomUIMainWindowContext.Progress` 汇报进度),把返回的
|
1. 配置了 `UseMainWindow`:在 UI 线程执行工厂(可 await,经 `AtomUIMainWindowContext.Progress` 汇报进度),把返回的
|
||||||
窗口赋给 `desktop.MainWindow` 并显示。应用同时自行给 `desktop.MainWindow` 赋值视为配置错误。
|
窗口赋给 `desktop.MainWindow` 并显示;主窗体首个合成批次完成后才隐藏并关闭 Splash。应用同时自行给
|
||||||
|
`desktop.MainWindow` 赋值视为配置错误。
|
||||||
2. 否则 `desktop.MainWindow` 已赋值(传统写法、Prism):就是它。
|
2. 否则 `desktop.MainWindow` 已赋值(传统写法、Prism):就是它。
|
||||||
3. 否则视为没有主窗体:立即关闭 Splash,状态 → `Ready`。
|
3. 否则视为没有主窗体:立即关闭 Splash,状态 → `Ready`。
|
||||||
|
|
||||||
主窗体确定后状态 → `Presenting`;等它 `Opened`(上限 3 s)后的首个渲染批次(上限 3 s),然后 Splash 淡出并关闭,
|
主窗体确定后状态 → `Presenting`;等它 `Opened`(上限 3 s)后的首个渲染批次(上限 3 s),然后立即隐藏并关闭 Splash,
|
||||||
状态 → `Ready`。两个超时只写 Warning 并直接交接,防止最小化或被遮挡的窗口让 Splash 永不关闭。没有 Splash 时同样按
|
状态 → `Ready`。传统宿主自行显示的主窗体在该首帧后才淡出 Splash。两个主窗体等待超时只写 Warning 并直接交接,防止最小化或被遮挡的窗口让 Splash 永不关闭。没有 Splash 时同样按
|
||||||
此规则推进状态。主窗体在等待期间关闭、或用户关闭 Splash 触发启动取消时,关闭/取消信号会立即中断
|
此规则推进状态。主窗体在等待期间关闭、或用户关闭 Splash 触发启动取消时,关闭/取消信号会立即中断
|
||||||
`Opened` 与合成批次等待,不继续消耗 3 秒超时,也不记作成功首帧或 Ready。
|
`Opened` 与合成批次等待,不继续消耗 3 秒超时,也不记作成功首帧或 Ready。
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# AtomUI 启动系统架构
|
# AtomUI 启动系统架构
|
||||||
|
|
||||||
启动系统负责从 `AppBuilder` 配置到"主窗体完整出现"之间的全部 AtomUI 编排:收集应用配置、在平台初始化期间于后台
|
启动系统负责从 `AppBuilder` 配置到"主窗体完整出现"之间的全部 AtomUI 编排:收集应用配置、在平台初始化期间于后台
|
||||||
准备全局 Token、可选地显示启动 Splash、一次性提交 AtomUI 内核(本地化、主题、自有服务),并在主窗体首个渲染批次
|
准备全局 Token、可选地显示启动 Splash、一次性提交 AtomUI 内核(本地化、主题、自有服务),并编排 Splash 与主窗体的交接。
|
||||||
完成后交接。它横跨 `AtomUI.Core`(生命周期与提交)、`AtomUI.Controls`(Splash 控件与呈现器)和
|
主窗体首帧后立即隐藏并关闭 Splash。它横跨 `AtomUI.Core`(生命周期与提交)、`AtomUI.Controls`(Splash 控件与呈现器)和
|
||||||
`AtomUI.Generator`(`RegisterServices` 挂接生成器)。
|
`AtomUI.Generator`(`RegisterServices` 挂接生成器)。
|
||||||
|
|
||||||
## 设计目标
|
## 设计目标
|
||||||
@@ -40,7 +40,7 @@ StartWithClassicDesktopLifetime → SetupUnsafe
|
|||||||
Initialize() App.axaml;Prism 在此创建并显示 Shell
|
Initialize() App.axaml;Prism 在此创建并显示 Shell
|
||||||
AfterApplicationSetup 守卫:本实例未执行 Attach 则抛出带修复说明的异常
|
AfterApplicationSetup 守卫:本实例未执行 Attach 则抛出带修复说明的异常
|
||||||
OnFrameworkInitializationCompleted 传统写法在此创建主窗体
|
OnFrameworkInitializationCompleted 传统写法在此创建主窗体
|
||||||
主循环:判定主窗体 → 等其首个渲染批次 → Splash 淡出关闭 → Ready
|
主循环:创建主窗体 → 显示并等待首个渲染批次 → 隐藏并关闭 Splash → Ready
|
||||||
```
|
```
|
||||||
|
|
||||||
## 模块职责
|
## 模块职责
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Splash
|
# Splash
|
||||||
|
|
||||||
启动 Splash 在 AtomUI 内核提交之前显示,覆盖提交与主窗体构建的耗时,并在主窗体首个渲染批次完成后淡出。生命周期中的
|
启动 Splash 在 AtomUI 内核提交之前显示,覆盖提交与主窗体构建的耗时。主窗体首个合成批次完成后,Splash 立即隐藏并随后关闭。生命周期中的
|
||||||
位置见[启动生命周期](lifecycle.md);接入示例见[启动指南](../../../guides/startup/overview.md#2-启用-splash)。
|
位置见[启动生命周期](lifecycle.md);接入示例见[启动指南](../../../guides/startup/overview.md#2-启用-splash)。
|
||||||
|
|
||||||
## 1. 类型
|
## 1. 类型
|
||||||
@@ -94,21 +94,22 @@ ControlTheme。它合并在默认主题之后,同键优先。两种写法:
|
|||||||
|
|
||||||
## 6. 交接、错误态与关闭
|
## 6. 交接、错误态与关闭
|
||||||
|
|
||||||
- **交接**:主窗体首个渲染批次完成后,对窗口根合成视觉做 150 ms 透明度淡出,然后取消置顶并关闭窗口。
|
- **交接**:主窗体首个合成批次完成后立即隐藏 Splash,取消命中与置顶;窗口关闭投递到下一轮 UI 调度,避免在渲染回调中
|
||||||
|
同步销毁合成目标触发 Avalonia 合成器重入。UseMainWindow 与传统宿主均遵循此顺序,避免 Splash 消失后主窗体尚未上屏。
|
||||||
- **进度**:框架只在状态切换时发布进度,Splash 默认显示不确定进度;应用经 `UseMainWindow` 的
|
- **进度**:框架只在状态切换时发布进度,Splash 默认显示不确定进度;应用经 `UseMainWindow` 的
|
||||||
`AtomUIMainWindowContext.Progress` 汇报时,更新消息并切换为确定进度。
|
`AtomUIMainWindowContext.Progress` 汇报时,更新消息并切换为确定进度。
|
||||||
- **错误态**:提交失败或主窗体工厂抛错时,`Status` 切换为 `Error`,消息显示异常的根消息;用户点击关闭部件或按 Esc 后
|
- **错误态**:提交失败或主窗体工厂抛错时,`Status` 切换为 `Error`,消息显示异常的根消息;用户点击关闭部件或按 Esc 后
|
||||||
关闭窗口,启动按[出错与取消](lifecycle.md#7-出错与取消)继续。错误态隐藏进度条时也停止其合成帧请求;
|
关闭窗口,启动按[出错与取消](lifecycle.md#7-出错与取消)继续。呈现失败时若 Splash 已透明,恢复其不透明度、置顶与命中。
|
||||||
|
错误态隐藏进度条时也停止其合成帧请求;
|
||||||
普通页面预览被自身或祖先隐藏、或从 VisualTree 脱离时同样停止,重新可见后按当前进度状态恢复。
|
普通页面预览被自身或祖先隐藏、或从 VisualTree 脱离时同样停止,重新可见后按当前进度状态恢复。
|
||||||
- **用户关闭**:交接前用户关闭 Splash 视为取消启动并调用 `Shutdown()`。
|
- **用户关闭**:交接前用户关闭 Splash 视为取消启动并调用 `Shutdown()`。
|
||||||
|
|
||||||
## 7. 已知问题
|
## 7. 已知问题
|
||||||
|
|
||||||
- **交接瞬间的短暂停顿**:所有窗口共用一个渲染线程。主窗体首帧的栅格化(冷字体与字形缓存)占用渲染线程时,Splash 的
|
- **交接瞬间的短暂停顿**:所有窗口共用一个渲染线程。Splash 保持可见直到主窗体首帧完成,随后立即隐藏,避免出现长时间空档。
|
||||||
渲染线程动画随之暂停;Splash 关闭安排在主窗体首帧之后,最多看到一次短暂停顿。
|
|
||||||
- **macOS 主窗体上屏前的渲染线程阻塞**:AtomUI 的 `WithAtomUIDefaultOptions()` 在 macOS 上 Metal 优先,Metal 在渲染线程
|
- **macOS 主窗体上屏前的渲染线程阻塞**:AtomUI 的 `WithAtomUIDefaultOptions()` 在 macOS 上 Metal 优先,Metal 在渲染线程
|
||||||
直接呈现,提交占用 UI 线程时 Splash 动画照常上屏(OpenGL 与 Software 经 IOSurface 呈现,需要主线程设置图层内容,提交
|
直接呈现,提交占用 UI 线程时 Splash 动画照常上屏(OpenGL 与 Software 经 IOSurface 呈现,需要主线程设置图层内容,提交
|
||||||
期间帧送不上屏)。但 Avalonia 的 `Window.Show` 先开始渲染、后让窗口上屏;UI 线程在主窗体上屏前仍忙时,主窗体的
|
期间帧送不上屏)。但 Avalonia 的 `Window.Show` 先开始渲染、后让窗口上屏;UI 线程在主窗体上屏前仍忙时,主窗体的
|
||||||
`-[CAMetalLayer nextDrawable]` 可阻塞渲染线程约 1 s(drawable 池耗尽后的系统超时),Splash 在此期间停顿。这是 Avalonia
|
`-[CAMetalLayer nextDrawable]` 可阻塞渲染线程约 1 s(drawable 池耗尽后的系统超时),Splash 在此期间停顿。这是 Avalonia
|
||||||
多窗口共用渲染线程的行为,尚未处理。
|
多窗口共用渲染线程的行为;Splash 会保持可见直到主窗体首帧完成,不会留下提前隐藏后的长时间空档。
|
||||||
- **Wayland**:不允许应用定位窗口,Splash 的位置由合成器决定。
|
- **Wayland**:不允许应用定位窗口,Splash 的位置由合成器决定。
|
||||||
|
|||||||
@@ -94,4 +94,5 @@ macOS、Windows、Linux X11 与 Wayland 各执行一次,检查 Gallery 与一
|
|||||||
|
|
||||||
## 5. 已知不一致
|
## 5. 已知不一致
|
||||||
|
|
||||||
- macOS 主窗体上屏前约 1 s 的渲染线程阻塞会让 Splash 停顿,见 [Splash 已知问题](splash.md#7-已知问题)。
|
- 启动交接须验证主窗体首个合成批次完成后 Splash 才隐藏;呈现失败需恢复可见错误态。
|
||||||
|
- macOS 主窗体首帧前的渲染线程阻塞可能让 Splash 动画停顿,但不得造成 Splash 提前消失后的长时间空档,见 [Splash 已知问题](splash.md#7-已知问题)。
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ Splash 在内核提交之前显示,自定义主题必须遵守:
|
|||||||
|
|
||||||
- 汇报的 `Fraction`(0–1)使 Splash 切换为确定进度;`Message` 为 null 时不改消息。
|
- 汇报的 `Fraction`(0–1)使 Splash 切换为确定进度;`Message` 为 null 时不改消息。
|
||||||
- 用户在交接前关闭 Splash 时 `context.CancellationToken` 取消,应用随即退出。
|
- 用户在交接前关闭 Splash 时 `context.CancellationToken` 取消,应用随即退出。
|
||||||
|
- 工厂返回后显示主窗体,等待其首个渲染批次完成,再立即隐藏并关闭 Splash,避免 Splash 消失后主窗体长时间未出现。
|
||||||
- 使用 `UseMainWindow` 时不要再自行给 `desktop.MainWindow` 赋值;它只适用于经典桌面生命周期。
|
- 使用 `UseMainWindow` 时不要再自行给 `desktop.MainWindow` 赋值;它只适用于经典桌面生命周期。
|
||||||
|
|
||||||
## 5. Prism
|
## 5. Prism
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ using Avalonia.Rendering.Composition;
|
|||||||
namespace AtomUI.Controls;
|
namespace AtomUI.Controls;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Splash 呈现器:创建并显示 SplashWindow,汇报启动进度,错误态等待用户确认,交接时淡出关闭。
|
/// Splash 呈现器:创建并显示 SplashWindow,汇报启动进度,错误态等待用户确认,交接时先隐藏、后关闭。
|
||||||
/// 全部成员在 UI 线程调用(由 Core 的启动编排保证)。
|
/// 全部成员在 UI 线程调用(由 Core 的启动编排保证)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class SplashPresenter : IAtomUISplashPresenter
|
internal sealed class SplashPresenter : IAtomUISplashPresenter
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan FadeDuration = TimeSpan.FromMilliseconds(150);
|
|
||||||
|
|
||||||
private readonly TaskCompletionSource _firstFrame = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
private readonly TaskCompletionSource _firstFrame = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
private readonly TaskCompletionSource _errorAcknowledged = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
private readonly TaskCompletionSource _errorAcknowledged = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
private SplashWindow? _window;
|
private SplashWindow? _window;
|
||||||
@@ -69,25 +67,39 @@ internal sealed class SplashPresenter : IAtomUISplashPresenter
|
|||||||
}
|
}
|
||||||
splash.Status = SplashStatus.Error;
|
splash.Status = SplashStatus.Error;
|
||||||
splash.Message = exception.GetBaseException().Message;
|
splash.Message = exception.GetBaseException().Message;
|
||||||
|
// 主窗体呈现失败可能发生在 Splash 已透明之后;错误态必须重新可见、可操作。
|
||||||
|
if (_window is { } window)
|
||||||
|
{
|
||||||
|
window.Opacity = 1;
|
||||||
|
window.IsHitTestVisible = true;
|
||||||
|
window.Topmost = true;
|
||||||
|
if (ElementComposition.GetElementVisual(window) is { } visual)
|
||||||
|
{
|
||||||
|
visual.StopAnimation("Opacity");
|
||||||
|
visual.Opacity = 1;
|
||||||
|
}
|
||||||
|
if (!window.IsVisible)
|
||||||
|
{
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task FadeOutAndCloseAsync()
|
public Task HideAsync()
|
||||||
{
|
{
|
||||||
if (_window is not { } window)
|
if (_window is not { } window)
|
||||||
{
|
{
|
||||||
return;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
if (ElementComposition.GetElementVisual(window) is { } visual)
|
if (!ReferenceEquals(_window, window))
|
||||||
{
|
{
|
||||||
var fade = visual.Compositor.CreateScalarKeyFrameAnimation();
|
return Task.CompletedTask;
|
||||||
fade.Target = "Opacity";
|
|
||||||
fade.InsertKeyFrame(0f, 1f);
|
|
||||||
fade.InsertKeyFrame(1f, 0f);
|
|
||||||
fade.Duration = FadeDuration;
|
|
||||||
visual.StartAnimation("Opacity", fade);
|
|
||||||
await Task.Delay(FadeDuration + TimeSpan.FromMilliseconds(10));
|
|
||||||
}
|
}
|
||||||
Close();
|
// 主窗体首帧已经完成,直接隐藏 Splash,避免透明度合成和主窗体上屏之间形成空档。
|
||||||
|
window.IsHitTestVisible = false;
|
||||||
|
window.Topmost = false;
|
||||||
|
window.Hide();
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Close()
|
public void Close()
|
||||||
|
|||||||
@@ -246,13 +246,12 @@ internal sealed class AtomUIStartupRun
|
|||||||
|
|
||||||
_process.MoveTo(AtomUIStartupState.Presenting);
|
_process.MoveTo(AtomUIStartupState.Presenting);
|
||||||
// 主窗体在首帧前被关闭视为用户取消:不再等待首帧,也不再走正常交接。
|
// 主窗体在首帧前被关闭视为用户取消:不再等待首帧,也不再走正常交接。
|
||||||
var cancelledDuringPresentation = false;
|
|
||||||
void HandleMainWindowClosed(object? sender, EventArgs args)
|
void HandleMainWindowClosed(object? sender, EventArgs args)
|
||||||
{
|
{
|
||||||
if (_process.State == AtomUIStartupState.Presenting)
|
if (_process.State == AtomUIStartupState.Presenting)
|
||||||
{
|
{
|
||||||
cancelledDuringPresentation = true;
|
|
||||||
_process.Cancel();
|
_process.Cancel();
|
||||||
|
_cancellation.Cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,23 +260,17 @@ internal sealed class AtomUIStartupRun
|
|||||||
{
|
{
|
||||||
if (builder.MainWindowFactory is not null)
|
if (builder.MainWindowFactory is not null)
|
||||||
{
|
{
|
||||||
|
_cancellation.Token.ThrowIfCancellationRequested();
|
||||||
mainWindow.Show();
|
mainWindow.Show();
|
||||||
}
|
}
|
||||||
if (!cancelledDuringPresentation)
|
await WaitForFirstFrameAsync(mainWindow, _cancellation.Token);
|
||||||
{
|
|
||||||
await WaitForFirstFrameAsync(mainWindow, _cancellation.Token);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
mainWindow.Closed -= HandleMainWindowClosed;
|
mainWindow.Closed -= HandleMainWindowClosed;
|
||||||
}
|
}
|
||||||
if (cancelledDuringPresentation)
|
|
||||||
{
|
|
||||||
await CloseSplashAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
AtomUIStartupEventSource.Log.MainWindowFirstFrame();
|
AtomUIStartupEventSource.Log.MainWindowFirstFrame();
|
||||||
|
// 主窗体已经完成首个合成批次后才隐藏 Splash,避免 Splash 提前消失后主窗体长时间尚未上屏。
|
||||||
await CloseSplashAsync();
|
await CloseSplashAsync();
|
||||||
_process.CompleteReady();
|
_process.CompleteReady();
|
||||||
}
|
}
|
||||||
@@ -337,8 +330,10 @@ internal sealed class AtomUIStartupRun
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await visual.Compositor.RequestCompositionBatchCommitAsync().Rendered
|
await AwaitCompositionAsync(
|
||||||
.WaitAsync(MainWindowFirstFrameTimeout, closedOrCanceled.Token);
|
visual.Compositor.RequestCompositionBatchCommitAsync().Rendered,
|
||||||
|
MainWindowFirstFrameTimeout,
|
||||||
|
closedOrCanceled.Token);
|
||||||
}
|
}
|
||||||
catch (TimeoutException)
|
catch (TimeoutException)
|
||||||
{
|
{
|
||||||
@@ -351,14 +346,29 @@ internal sealed class AtomUIStartupRun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task CloseSplashAsync()
|
private static Task AwaitCompositionAsync(Task composition, TimeSpan timeout, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// CompositionBatch completes its TCS with synchronous continuations. Keep startup
|
||||||
|
// continuations off the render thread; the caller resumes on the UI dispatcher.
|
||||||
|
return composition.WaitAsync(timeout, cancellationToken).ContinueWith(
|
||||||
|
static completed => completed.GetAwaiter().GetResult(),
|
||||||
|
CancellationToken.None,
|
||||||
|
TaskContinuationOptions.ExecuteSynchronously,
|
||||||
|
TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CloseSplashAsync()
|
||||||
{
|
{
|
||||||
if (_splash is not { } splash)
|
if (_splash is not { } splash)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
}
|
}
|
||||||
|
await splash.HideAsync();
|
||||||
splash.ClosedByUser -= HandleSplashClosedByUser;
|
splash.ClosedByUser -= HandleSplashClosedByUser;
|
||||||
_splash = null;
|
_splash = null;
|
||||||
return splash.FadeOutAndCloseAsync();
|
// Window.Close synchronously tears down its composition target. Defer it until
|
||||||
|
// the current render/native callback has returned; otherwise Avalonia can enter
|
||||||
|
// ServerCompositor.Render again from SyncDisposeCompositionTarget.
|
||||||
|
Dispatcher.UIThread.Post(splash.Close, DispatcherPriority.Background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ internal interface IAtomUISplashPresenter
|
|||||||
|
|
||||||
Task ErrorAcknowledged { get; }
|
Task ErrorAcknowledged { get; }
|
||||||
|
|
||||||
/// <summary>淡出并关闭(交接)。</summary>
|
/// <summary>主窗体首帧完成后隐藏 Splash;窗口随后由启动编排安全关闭。</summary>
|
||||||
Task FadeOutAndCloseAsync();
|
Task HideAsync();
|
||||||
|
|
||||||
/// <summary>不等待动画立即关闭;用于启动取消或呈现失败时释放窗口。</summary>
|
/// <summary>不等待动画立即关闭;用于启动取消或呈现失败时释放窗口。</summary>
|
||||||
void Close();
|
void Close();
|
||||||
|
|||||||
@@ -13,16 +13,23 @@ public class AtomUIStartupContractTests
|
|||||||
{
|
{
|
||||||
// headless 会话的 dispatch 完成回调内联执行:必须阻塞式 dispatch,否则会话线程会在 Dispose 中等待自身。
|
// headless 会话的 dispatch 完成回调内联执行:必须阻塞式 dispatch,否则会话线程会在 Dispose 中等待自身。
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Generated_hook_commits_before_Initialize_and_reaches_Ready()
|
public async Task Generated_hook_commits_before_Initialize_and_reaches_Ready()
|
||||||
{
|
{
|
||||||
using var session = HeadlessUnitTestSession.StartNew(typeof(HookedEntry), AvaloniaTestIsolationLevel.PerTest);
|
var session = HeadlessUnitTestSession.StartNew(typeof(HookedEntry), AvaloniaTestIsolationLevel.PerTest);
|
||||||
session.Dispatch(() =>
|
try
|
||||||
{
|
{
|
||||||
var application = (TestApplication)Application.Current!;
|
await session.Dispatch(() =>
|
||||||
application.ThemeManagerSeenInInitialize.ShouldBeTrue();
|
{
|
||||||
application.GetStartup().State.ShouldBe(AtomUIStartupState.Ready);
|
var application = (TestApplication)Application.Current!;
|
||||||
application.GetStartup().Completion.IsCompletedSuccessfully.ShouldBeTrue();
|
application.ThemeManagerSeenInInitialize.ShouldBeTrue();
|
||||||
}, CancellationToken.None).GetAwaiter().GetResult();
|
application.GetStartup().State.ShouldBe(AtomUIStartupState.Ready);
|
||||||
|
application.GetStartup().Completion.IsCompletedSuccessfully.ShouldBeTrue();
|
||||||
|
}, TestContext.Current.CancellationToken);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await Task.Run(session.Dispose, TestContext.Current.CancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class HookedEntry
|
public static class HookedEntry
|
||||||
|
|||||||
Reference in New Issue
Block a user