mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-29 07:16:33 +08:00
481 lines
15 KiB
C#
481 lines
15 KiB
C#
using System.Reflection;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.ApplicationLifetimes;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Media;
|
|
using Avalonia.Media.Imaging;
|
|
using Avalonia.Styling;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.Window;
|
|
|
|
public class WindowTitleBarEffectiveLogoTests
|
|
{
|
|
static WindowTitleBarEffectiveLogoTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Effective_Logo_Falls_Back_To_Window_Icon_When_Logo_Not_Set()
|
|
{
|
|
var icon = CreateIcon();
|
|
var window = new AtomUI.Desktop.Controls.Window
|
|
{
|
|
Width = 200,
|
|
Height = 100,
|
|
Icon = icon
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
GetEffectiveLogo(window).ShouldBe(icon);
|
|
GetEffectiveLogoTemplate(window).ShouldNotBeNull();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Runtime_Logo_Control_Replaces_Fallback_Without_Template_Residue()
|
|
{
|
|
var icon = CreateIcon();
|
|
var window = new AtomUI.Desktop.Controls.Window
|
|
{
|
|
Width = 200,
|
|
Height = 100,
|
|
Icon = icon
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var runtimeLogo = new Border();
|
|
window.Logo = runtimeLogo;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
GetEffectiveLogo(window).ShouldBe(runtimeLogo);
|
|
GetEffectiveLogoTemplate(window).ShouldBeNull();
|
|
|
|
window.Logo = null;
|
|
Dispatcher.UIThread.RunJobs();
|
|
GetEffectiveLogo(window).ShouldBe(icon);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Explicit_LogoTemplate_Suppresses_Icon_Fallback()
|
|
{
|
|
var icon = CreateIcon();
|
|
var template = new FuncDataTemplate<object>((_, _) => new Border());
|
|
var window = new AtomUI.Desktop.Controls.Window
|
|
{
|
|
Width = 200,
|
|
Height = 100,
|
|
Icon = icon,
|
|
LogoTemplate = template
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
GetEffectiveLogo(window).ShouldBeNull();
|
|
GetEffectiveLogoTemplate(window).ShouldBe(template);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Never_Visibility_Hides_Icon_Fallback_Logo()
|
|
{
|
|
var window = new AtomUI.Desktop.Controls.Window
|
|
{
|
|
Width = 200,
|
|
Height = 100,
|
|
Icon = CreateIcon(),
|
|
LogoVisibility = WindowTitleBarLogoVisibility.Never
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
AvaloniaHeadlessPlatform.ForceRenderTimerTick(1);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var titleBar = GetTitleBar(window);
|
|
titleBar.ShouldNotBeNull();
|
|
titleBar.ApplyTemplate();
|
|
GetIsEffectiveLogoVisible(titleBar).ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(OsType.Linux)]
|
|
[InlineData(OsType.Windows)]
|
|
[InlineData(OsType.macOS)]
|
|
public void Standalone_Title_Bar_Presenter_Renders_Explicit_Logo(OsType osType)
|
|
{
|
|
const string logo = "standalone-logo";
|
|
var titleBar = new WindowTitleBar
|
|
{
|
|
Width = 320,
|
|
Height = 40,
|
|
Logo = logo,
|
|
LogoVisibility = WindowTitleBarLogoVisibility.Always,
|
|
Title = "Standalone title bar"
|
|
};
|
|
titleBar.SetValue(WindowTitleBar.OsTypeProperty, osType);
|
|
Application.Current!.TryFindResource(typeof(WindowTitleBar), out var resource).ShouldBeTrue();
|
|
titleBar.Theme = resource.ShouldBeAssignableTo<ControlTheme>();
|
|
|
|
var host = new AvaloniaWindow
|
|
{
|
|
Width = 320,
|
|
Height = 100,
|
|
Content = titleBar
|
|
};
|
|
|
|
try
|
|
{
|
|
host.Show();
|
|
titleBar.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
host.UpdateLayout();
|
|
|
|
var logoPresenter = titleBar.GetVisualDescendants()
|
|
.OfType<ContentPresenter>()
|
|
.Single(presenter => presenter.Name == "PART_Logo");
|
|
logoPresenter.Content.ShouldBe(logo);
|
|
}
|
|
finally
|
|
{
|
|
host.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(OsType.Linux)]
|
|
[InlineData(OsType.Windows)]
|
|
[InlineData(OsType.macOS)]
|
|
public void Custom_Title_Bar_Logo_Takes_Precedence_Over_Host_Window_Fallback(OsType osType)
|
|
{
|
|
const string hostLogo = "host-logo";
|
|
const string titleBarLogo = "title-bar-logo";
|
|
var titleBar = new WindowTitleBar
|
|
{
|
|
Width = 320,
|
|
Height = 40,
|
|
Logo = titleBarLogo,
|
|
LogoVisibility = WindowTitleBarLogoVisibility.Always,
|
|
Title = "Custom title bar"
|
|
};
|
|
titleBar.SetValue(WindowTitleBar.OsTypeProperty, osType);
|
|
Application.Current!.TryFindResource(typeof(WindowTitleBar), out var resource).ShouldBeTrue();
|
|
titleBar.Theme = resource.ShouldBeAssignableTo<ControlTheme>();
|
|
|
|
var window = new AtomUI.Desktop.Controls.Window
|
|
{
|
|
Width = 400,
|
|
Height = 160,
|
|
Logo = hostLogo,
|
|
Title = "Host window",
|
|
Content = titleBar
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
titleBar.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
|
|
var logoPresenter = titleBar.GetVisualDescendants()
|
|
.OfType<ContentPresenter>()
|
|
.Single(presenter => presenter.Name == "PART_Logo");
|
|
logoPresenter.Content.ShouldBe(titleBarLogo);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Hosted_Title_Bar_Without_Explicit_Logo_Tracks_Host_Effective_Logo()
|
|
{
|
|
var titleBar = new WindowTitleBar
|
|
{
|
|
Width = 320,
|
|
Height = 40,
|
|
LogoVisibility = WindowTitleBarLogoVisibility.Always,
|
|
Title = "Hosted title bar"
|
|
};
|
|
titleBar.SetValue(WindowTitleBar.OsTypeProperty, OsType.Windows);
|
|
Application.Current!.TryFindResource(typeof(WindowTitleBar), out var resource).ShouldBeTrue();
|
|
titleBar.Theme = resource.ShouldBeAssignableTo<ControlTheme>();
|
|
|
|
var window = new AtomUI.Desktop.Controls.Window
|
|
{
|
|
Width = 400,
|
|
Height = 160,
|
|
Logo = "host-logo-1",
|
|
Title = "Host window",
|
|
Content = titleBar
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
titleBar.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
|
|
var logoPresenter = titleBar.GetVisualDescendants()
|
|
.OfType<ContentPresenter>()
|
|
.Single(presenter => presenter.Name == "PART_Logo");
|
|
logoPresenter.Content.ShouldBe("host-logo-1");
|
|
|
|
window.Logo = "host-logo-2";
|
|
Dispatcher.UIThread.RunJobs();
|
|
logoPresenter.Content.ShouldBe("host-logo-2");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Runtime_Icon_Changes_Update_Fullscreen_Logo_Visibility()
|
|
{
|
|
var window = new AtomUI.Desktop.Controls.Window
|
|
{
|
|
Title = "AtomUI",
|
|
LogoVisibility = WindowTitleBarLogoVisibility.Always
|
|
};
|
|
|
|
GetIsEffectiveFullscreenLogoVisible(window).ShouldBeFalse();
|
|
|
|
window.Icon = CreateIcon();
|
|
GetIsEffectiveFullscreenLogoVisible(window).ShouldBeTrue();
|
|
|
|
window.Icon = null;
|
|
GetIsEffectiveFullscreenLogoVisible(window).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Main_Window_Runtime_Logo_Changes_Update_Child_Fallback_Until_Child_Closes()
|
|
{
|
|
var application = Application.Current!;
|
|
var lifetimeField = typeof(Application).GetField(
|
|
"_applicationLifetime",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
var previousLifetime = lifetimeField.GetValue(application);
|
|
var lifetime = new ClassicDesktopStyleApplicationLifetime();
|
|
var mainWindow = new AtomUI.Desktop.Controls.Window { Logo = "main-logo-1" };
|
|
var childWindow = new AtomUI.Desktop.Controls.Window
|
|
{
|
|
Width = 200,
|
|
Height = 100
|
|
};
|
|
|
|
try
|
|
{
|
|
lifetime.MainWindow = mainWindow;
|
|
lifetimeField.SetValue(application, lifetime);
|
|
|
|
childWindow.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
GetEffectiveLogo(childWindow).ShouldBe("main-logo-1");
|
|
|
|
mainWindow.Logo = "main-logo-2";
|
|
Dispatcher.UIThread.RunJobs();
|
|
GetEffectiveLogo(childWindow).ShouldBe("main-logo-2");
|
|
|
|
childWindow.Close();
|
|
mainWindow.Logo = "main-logo-after-close";
|
|
Dispatcher.UIThread.RunJobs();
|
|
GetEffectiveLogo(childWindow).ShouldBe("main-logo-2");
|
|
}
|
|
finally
|
|
{
|
|
childWindow.Close();
|
|
lifetime.MainWindow = null;
|
|
lifetimeField.SetValue(application, previousLifetime);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Main_Window_Fallback_Does_Not_Subscribe_Before_Child_Is_Shown()
|
|
{
|
|
var application = Application.Current!;
|
|
var lifetimeField = typeof(Application).GetField(
|
|
"_applicationLifetime",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
var previousLifetime = lifetimeField.GetValue(application);
|
|
var lifetime = new ClassicDesktopStyleApplicationLifetime();
|
|
var mainWindow = new AtomUI.Desktop.Controls.Window { Logo = "main-logo" };
|
|
var childWindow = new AtomUI.Desktop.Controls.Window();
|
|
|
|
try
|
|
{
|
|
lifetime.MainWindow = mainWindow;
|
|
lifetimeField.SetValue(application, lifetime);
|
|
|
|
childWindow.Icon = CreateIcon();
|
|
childWindow.Icon = null;
|
|
|
|
GetMainWindowLogoFallbackLease(childWindow).ShouldBeNull();
|
|
GetEffectiveLogo(childWindow).ShouldBe("main-logo");
|
|
}
|
|
finally
|
|
{
|
|
lifetime.MainWindow = null;
|
|
lifetimeField.SetValue(application, previousLifetime);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Logo_Fallback_Resolution_Matrix()
|
|
{
|
|
var resolver = GetFallbackResolver();
|
|
resolver.ShouldNotBeNull();
|
|
|
|
var hostIcon = CreateIcon();
|
|
var mainWindowIcon = CreateIcon();
|
|
var mainWindowLogo = new Border();
|
|
|
|
var host = new AtomUI.Desktop.Controls.Window { Icon = hostIcon };
|
|
var mainWindow = new AtomUI.Desktop.Controls.Window { Icon = mainWindowIcon, Logo = mainWindowLogo };
|
|
|
|
// 1. 显式 Logo 优先,模板跟随显式设置
|
|
var explicitTemplate = new FuncDataTemplate<object>((_, _) => new Border());
|
|
host.LogoTemplate = explicitTemplate;
|
|
host.Logo = new Border();
|
|
Resolve(resolver, host, mainWindow).logo.ShouldNotBeNull();
|
|
|
|
// 2. 显式 LogoTemplate 存在时不回退
|
|
host.Logo = null;
|
|
var (logo2, template2) = Resolve(resolver, host, mainWindow);
|
|
logo2.ShouldBeNull();
|
|
template2.ShouldBe(explicitTemplate);
|
|
|
|
// 3. 自身 Icon 优先于主窗口
|
|
host.LogoTemplate = null;
|
|
var (logo3, template3) = Resolve(resolver, host, mainWindow);
|
|
logo3.ShouldBe(hostIcon);
|
|
template3.ShouldNotBeNull();
|
|
|
|
// 4. 自身无 Icon → 继承主窗口显式 Logo
|
|
host.Icon = null;
|
|
var (logo4, _) = Resolve(resolver, host, mainWindow);
|
|
logo4.ShouldBe(mainWindowLogo);
|
|
|
|
// 5. 主窗口也只余 Icon → 继承主窗口 Icon
|
|
mainWindow.Logo = null;
|
|
var (logo5, template5) = Resolve(resolver, host, mainWindow);
|
|
logo5.ShouldBe(mainWindowIcon);
|
|
template5.ShouldNotBeNull();
|
|
|
|
// 6. 全空 → 无 logo
|
|
mainWindow.Icon = null;
|
|
var (logo6, template6) = Resolve(resolver, host, null);
|
|
logo6.ShouldBeNull();
|
|
template6.ShouldBeNull();
|
|
}
|
|
|
|
private static WindowIcon CreateIcon()
|
|
{
|
|
var bitmap = new RenderTargetBitmap(new PixelSize(4, 4), new Vector(96, 96));
|
|
return new WindowIcon(bitmap);
|
|
}
|
|
|
|
private static object? GetEffectiveLogo(AtomUI.Desktop.Controls.Window window)
|
|
{
|
|
return typeof(AtomUI.Desktop.Controls.Window)
|
|
.GetProperty("EffectiveLogo", BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.GetValue(window);
|
|
}
|
|
|
|
private static object? GetEffectiveLogoTemplate(AtomUI.Desktop.Controls.Window window)
|
|
{
|
|
return typeof(AtomUI.Desktop.Controls.Window)
|
|
.GetProperty("EffectiveLogoTemplate", BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.GetValue(window);
|
|
}
|
|
|
|
private static WindowTitleBar? GetTitleBar(AtomUI.Desktop.Controls.Window window)
|
|
{
|
|
return typeof(AtomUI.Desktop.Controls.Window)
|
|
.GetProperty("TitleBar", BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.GetValue(window) as WindowTitleBar;
|
|
}
|
|
|
|
private static bool GetIsEffectiveLogoVisible(WindowTitleBar titleBar)
|
|
{
|
|
return (bool)typeof(WindowTitleBar)
|
|
.GetProperty("IsEffectiveLogoVisible", BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.GetValue(titleBar)!;
|
|
}
|
|
|
|
private static bool GetIsEffectiveFullscreenLogoVisible(AtomUI.Desktop.Controls.Window window)
|
|
{
|
|
return (bool)typeof(AtomUI.Desktop.Controls.Window)
|
|
.GetProperty("IsEffectiveFullscreenLogoVisible", BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.GetValue(window)!;
|
|
}
|
|
|
|
private static object? GetMainWindowLogoFallbackLease(AtomUI.Desktop.Controls.Window window)
|
|
{
|
|
return typeof(AtomUI.Desktop.Controls.Window)
|
|
.GetField("_mainWindowLogoFallbackLease", BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.GetValue(window);
|
|
}
|
|
|
|
private static MethodInfo? GetFallbackResolver()
|
|
{
|
|
return typeof(AtomUI.Desktop.Controls.Window).GetMethod(
|
|
"ResolveEffectiveLogo",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
}
|
|
|
|
private static (object? logo, object? template) Resolve(
|
|
MethodInfo resolver,
|
|
AtomUI.Desktop.Controls.Window host,
|
|
AtomUI.Desktop.Controls.Window? mainWindow)
|
|
{
|
|
var result = resolver!.Invoke(null, [host, mainWindow]);
|
|
var tupleType = result!.GetType();
|
|
return (tupleType.GetField("Logo")?.GetValue(result) ?? tupleType.GetField("Item1")!.GetValue(result),
|
|
tupleType.GetField("Template")?.GetValue(result) ?? tupleType.GetField("Item2")!.GetValue(result));
|
|
}
|
|
}
|