fix(Performance): 对齐 semantic 分支控件 API 并修复 headless 关停丢任务

This commit is contained in:
chinboy
2026-09-14 14:33:00 +08:00
parent 454cc0d002
commit 628ca582d4
24 changed files with 503 additions and 528 deletions
@@ -10,7 +10,7 @@ public sealed class PerfApplication : Application
{
this.UseAtomUI(builder =>
{
builder.WithDefaultTheme(IThemeManager.DEFAULT_THEME_ID);
builder.Theme.WithInitialTheme(IThemeManager.DEFAULT_THEME_ID);
builder.UseDesktopControls();
builder.UseDesktopColorPicker();
builder.UseDesktopDataGrid();
@@ -446,9 +446,15 @@ internal static partial class Program
private static void SetupAvalonia()
{
// OnLastWindowClose (the default) lets the first verification-window dispose begin
// application shutdown; from Avalonia 12.1.2 the dispatcher then silently drops all
// later posts, which kills multi-window verifications like the DataGrid range suite.
AppBuilder.Configure<PerfApplication>()
.UseHeadless(new AvaloniaHeadlessPlatformOptions())
.SetupWithLifetime(new ClassicDesktopStyleApplicationLifetime());
.SetupWithLifetime(new ClassicDesktopStyleApplicationLifetime
{
ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown
});
}
private static IReadOnlyList<PerfScenario> CreateScenarios(string suite)
@@ -87,7 +87,11 @@ internal static partial class Program
LeftAddOn = leftAddOn,
RightAddOn = rightAddOn
};
lineEdit.FormFeedback = formFeedback;
if (formFeedback is not null)
{
// FormFeedback 变为私有 setter,通过 IFormItemFeedbackAware 设置(与测试用法一致)
((IFormItemFeedbackAware)lineEdit).SetFeedbackControl(formFeedback);
}
if (maxLength > 0)
{
@@ -10,6 +10,7 @@ using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.VisualTree;
using System.Collections;
using System.Reflection;
using AtomTextBox = AtomUI.Desktop.Controls.TextBox;
@@ -177,7 +178,9 @@ internal static partial class Program
};
using var realized = RealizeControl(box);
var contentFrame = FindVisualByName<Border>(box, AddOnDecoratedBoxThemeConstants.ContentFramePart);
// ContentFrame 基类从 Border 改为 PixelAlignedBorder(DashedBorder/Decorator 系),
// 按 Control 查找再断言具体类型;Padding 断言保留,StyleKey 检查随 Border 基类移除而删除。
var contentFrame = FindVisualByName<Decorator>(box, AddOnDecoratedBoxThemeConstants.ContentFramePart);
Expect(contentFrame is AddOnDecoratedBoxContentFrame,
$"Content frame should use {nameof(AddOnDecoratedBoxContentFrame)} to avoid per-instance pointer handlers.",
failures);
@@ -185,9 +188,6 @@ internal static partial class Program
{
return;
}
Expect(contentFrame.StyleKey == typeof(Border),
$"Content frame should keep Border StyleKey so Border#PART_ContentFrame padding styles apply, actual {contentFrame.StyleKey}.",
failures);
Expect(contentFrame.Padding.Left > 0 && contentFrame.Padding.Top > 0,
$"Content frame should keep token padding after replacing the template Border, actual {contentFrame.Padding}.",
failures);
@@ -255,6 +255,85 @@ internal static partial class Program
KeyModifiers.None));
}
// 以下三个 helper 原先定义在 DataGrid 套件中,DataGrid 套件移除后由本文件承接,
// 供 AddOnDecoratedBox / PopupConfirm / DatePicker 套件共享(partial class Program)。
private static HashSet<string> GetLocalRoutedHandlerNames(Control control)
{
var field = typeof(Interactive).GetField("_eventHandlers", BindingFlags.Instance | BindingFlags.NonPublic);
if (field?.GetValue(control) is not IDictionary eventHandlers)
{
return [];
}
var names = new HashSet<string>();
foreach (DictionaryEntry entry in eventHandlers)
{
if (entry.Key != null)
{
names.Add(entry.Key.ToString() ?? string.Empty);
}
}
return names;
}
private static void RaiseControlPrimaryPointerPressed(Control target, Visual root)
{
var localPoint = new Point(
Math.Max(1, target.Bounds.Width / 2),
Math.Max(1, target.Bounds.Height / 2));
var rootPoint = target.TranslatePoint(localPoint, root) ?? localPoint;
target.RaiseEvent(CreatePrimaryPointerPressedEventArgs(target, root, rootPoint));
}
private static void RaiseControlPrimaryPointerReleased(Control target, Visual root)
{
var pointer = new Avalonia.Input.Pointer(
Avalonia.Input.Pointer.GetNextFreeId(),
PointerType.Mouse,
true);
var properties = new PointerPointProperties(
RawInputModifiers.None,
PointerUpdateKind.LeftButtonReleased);
var localPoint = new Point(
Math.Max(1, target.Bounds.Width / 2),
Math.Max(1, target.Bounds.Height / 2));
var rootPoint = target.TranslatePoint(localPoint, root) ?? localPoint;
target.RaiseEvent(new PointerReleasedEventArgs(
target,
pointer,
root,
rootPoint,
1,
properties,
KeyModifiers.None,
MouseButton.Left));
}
private static PointerPressedEventArgs CreatePrimaryPointerPressedEventArgs(
Control source,
Visual root,
Point rootPoint)
{
var pointer = new Avalonia.Input.Pointer(
Avalonia.Input.Pointer.GetNextFreeId(),
PointerType.Mouse,
true);
var properties = new PointerPointProperties(
RawInputModifiers.LeftMouseButton,
PointerUpdateKind.LeftButtonPressed);
return new PointerPressedEventArgs(
source,
pointer,
root,
rootPoint,
1,
properties,
KeyModifiers.None);
}
private static void VerifyCompactSpaceGeometry(ICollection<string> failures)
{
VerifyCompactSpaceGeometry(Orientation.Horizontal, failures);
@@ -31,7 +31,7 @@ internal static partial class Program
new PerfScenario("AutoComplete.SearchEdit.Closed", _ => new AutoCompleteSearchEdit
{
Width = 300,
SearchButtonStyle = SearchEditButtonStyle.Primary,
SearchButtonStyle = SearchButtonType.Primary,
OptionsSource = CreateAutoCompleteOptions()
}),
new PerfScenario("AutoComplete.TextArea.Closed", _ => new AutoCompleteTextArea
@@ -3,13 +3,12 @@ using AtomUI.Desktop.Controls;
using AtomUI.Icons.AntDesign;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
namespace AtomUI.Performance;
internal static partial class Program
{
private static readonly Lazy<Bitmap> AvatarBitmap = new(CreateAvatarBitmap);
private static readonly Lazy<BytesImageSource> AvatarBitmapSource = new(CreateAvatarBytesSource);
private static IReadOnlyList<PerfScenario> CreateAvatarScenarios()
{
@@ -19,8 +18,8 @@ internal static partial class Program
new PerfScenario("Avatar.Icon.Custom64", _ => CreateIconAvatar(64)),
new PerfScenario("Avatar.Text.Single", _ => new Avatar { Text = "U" }),
new PerfScenario("Avatar.Text.Long.Custom40", _ => new Avatar { Size = 40, Text = "USER" }),
new PerfScenario("Avatar.Svg", _ => new Avatar { Src = GetAvatarSvgPath() }),
new PerfScenario("Avatar.Bitmap", _ => new Avatar { BitmapSrc = AvatarBitmap.Value }),
new PerfScenario("Avatar.Svg", _ => new Avatar { Source = CreateAvatarFileSource() }),
new PerfScenario("Avatar.Bitmap", _ => new Avatar { Source = AvatarBitmapSource.Value }),
new PerfScenario("Avatar.Group.NoFold", _ => CreateAvatarGroup(maxDisplayCount: null)),
new PerfScenario("Avatar.Group.Fold2", _ => CreateAvatarGroup(maxDisplayCount: 2)),
new PerfScenario("Avatar.GalleryShape", _ => CreateAvatarGalleryShape())
@@ -45,7 +44,7 @@ internal static partial class Program
FoldInfoAvatarBackground = AvatarBrush("#fde3cf")
};
group.Children.Add(new Avatar { Src = GetAvatarSvgPath() });
group.Children.Add(new Avatar { Source = CreateAvatarFileSource() });
group.Children.Add(new Avatar { Background = AvatarBrush("#f56a00"), Text = "K" });
group.Children.Add(new Avatar { Background = AvatarBrush("#87d068"), Icon = new UserOutlined() });
group.Children.Add(new Avatar { Background = AvatarBrush("#1677ff"), Icon = new AntDesignOutlined() });
@@ -81,7 +80,7 @@ internal static partial class Program
new Avatar { Icon = new UserOutlined() },
new Avatar { Text = "U" },
new Avatar { Size = 40, Text = "USER" },
new Avatar { Src = GetAvatarSvgPath() },
new Avatar { Source = CreateAvatarFileSource() },
new Avatar { Background = AvatarBrush("#fde3cf"), Foreground = AvatarBrush("#f56a00"), Text = "U" },
new Avatar { Background = AvatarBrush("#87d068"), Icon = new UserOutlined() }));
@@ -118,7 +117,7 @@ internal static partial class Program
FoldInfoAvatarBackground = AvatarBrush("#fde3cf"),
Children =
{
new Avatar { Src = GetAvatarSvgPath() },
new Avatar { Source = CreateAvatarFileSource() },
new Avatar { Background = AvatarBrush("#f56a00"), Text = "K" },
new Avatar { Background = AvatarBrush("#87d068"), Icon = new UserOutlined() },
new Avatar { Background = AvatarBrush("#1677ff"), Icon = new AntDesignOutlined() }
@@ -134,7 +133,7 @@ internal static partial class Program
FoldInfoAvatarBackground = AvatarBrush("#fde3cf"),
Children =
{
new Avatar { BitmapSrc = AvatarBitmap.Value },
new Avatar { Source = AvatarBitmapSource.Value },
new Avatar { Background = AvatarBrush("#f56a00"), Text = "K" },
new Avatar { Background = AvatarBrush("#87d068"), Icon = new UserOutlined() },
new Avatar { Background = AvatarBrush("#1677ff"), Icon = new AntDesignOutlined() }
@@ -176,16 +175,20 @@ internal static partial class Program
return Path.GetFullPath("resources/images/readme/AntDesign.svg");
}
private static FileImageSource CreateAvatarFileSource()
{
return new FileImageSource(GetAvatarSvgPath());
}
private static IBrush AvatarBrush(string color)
{
return Avalonia.Media.Brush.Parse(color);
}
private static Bitmap CreateAvatarBitmap()
private static BytesImageSource CreateAvatarBytesSource()
{
var bytes = Convert.FromBase64String(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/lw7v9wAAAABJRU5ErkJggg==");
using var stream = new MemoryStream(bytes);
return new Bitmap(stream);
return new BytesImageSource(bytes);
}
}
@@ -42,12 +42,12 @@ internal static partial class Program
"PART_TextPresenter",
failures);
VerifyAvatarPresenterVisibility(
new Avatar { Src = GetAvatarSvgPath() },
new Avatar { Source = CreateAvatarFileSource() },
"Svg Avatar",
"SvgPresenter",
"ImagePresenter",
failures);
VerifyAvatarPresenterVisibility(
new Avatar { BitmapSrc = AvatarBitmap.Value },
new Avatar { Source = AvatarBitmapSource.Value },
"Bitmap Avatar",
"ImagePresenter",
failures);
@@ -59,9 +59,20 @@ internal static partial class Program
ICollection<string> failures)
{
using var realized = RealizeControl(avatar);
WaitUntilAvatarImageTerminal(avatar, realized.Window);
ExpectAvatarPresenterVisibility(avatar, label, visiblePresenterName, failures);
}
private static void WaitUntilAvatarImageTerminal(Avatar avatar, Avalonia.Controls.Window window)
{
if (avatar.Source is null)
{
return;
}
WaitUntil(() => avatar.LoadState is ImageLoadState.Loaded or ImageLoadState.Failed, "avatar image terminal state");
RefreshLayout(window);
}
private static void VerifyAvatarContentTypeSwitching(ICollection<string> failures)
{
var avatar = new Avatar
@@ -84,21 +95,20 @@ internal static partial class Program
ExpectAvatarPresenterVisibility(avatar, "Avatar after switching to Text", "PART_TextPresenter", failures);
var textPresenter = FindVisualByName<Avalonia.Controls.TextBlock>(avatar, "PART_TextPresenter");
avatar.SetCurrentValue(Avatar.SrcProperty, GetAvatarSvgPath());
RefreshLayout(realized.Window);
avatar.SetCurrentValue(AbstractAvatar.SourceProperty, CreateAvatarFileSource());
WaitUntilAvatarImageTerminal(avatar, realized.Window);
Expect(textPresenter?.GetVisualParent() != null && !textPresenter.IsVisible,
"Avatar should keep static TextBlock hidden when switching to Svg.",
"Avatar should keep static TextBlock hidden when switching to image content.",
failures);
ExpectAvatarPresenterVisibility(avatar, "Avatar after switching to Svg", "SvgPresenter", failures);
ExpectAvatarPresenterVisibility(avatar, "Avatar after switching to image source", "ImagePresenter", failures);
var svgPresenter = FindVisualByTypeName(avatar, "Svg", "SvgPresenter");
avatar.SetCurrentValue(Avatar.SrcProperty, null);
avatar.SetCurrentValue(Avatar.BitmapSrcProperty, AvatarBitmap.Value);
RefreshLayout(realized.Window);
Expect(svgPresenter?.GetVisualParent() != null && !svgPresenter.IsVisible,
"Avatar should keep static Svg hidden when switching to BitmapSrc.",
var imagePresenter = FindVisualByName<Avalonia.Controls.Image>(avatar, "ImagePresenter");
avatar.SetCurrentValue(AbstractAvatar.SourceProperty, AvatarBitmapSource.Value);
WaitUntilAvatarImageTerminal(avatar, realized.Window);
Expect(imagePresenter != null && imagePresenter.IsVisible,
"Avatar should keep ImagePresenter visible when switching between image sources.",
failures);
ExpectAvatarPresenterVisibility(avatar, "Avatar after switching to BitmapSrc", "ImagePresenter", failures);
ExpectAvatarPresenterVisibility(avatar, "Avatar after switching to bytes source", "ImagePresenter", failures);
}
private static void VerifyAvatarGroupFoldLifecycle(ICollection<string> failures)
@@ -163,7 +173,6 @@ internal static partial class Program
{
FindVisualByName<Control>(avatar, "IconPresenter"),
FindVisualByName<Control>(avatar, "ImagePresenter"),
FindVisualByName<Control>(avatar, "SvgPresenter"),
FindVisualByName<Control>(avatar, "PART_TextPresenter")
};
@@ -2,74 +2,70 @@ using Avalonia.Controls;
using AtomCalendar = AtomUI.Desktop.Controls.Calendar;
using AtomCalendarDateRange = AtomUI.Desktop.Controls.CalendarDateRange;
using AtomCalendarMode = AtomUI.Desktop.Controls.CalendarMode;
using AtomCalendarSelectionMode = AtomUI.Desktop.Controls.CalendarSelectionMode;
namespace AtomUI.Performance;
internal static partial class Program
{
// Calendar API 演进:DisplayDate/DisplayMode/SelectionMode/SelectedDates/BlackoutDates 已移除,
// 由 Value/Mode/ValidRange/DisabledDate/RangeBars 取代;CalendarMode 仅剩 Month/Year。
private static IReadOnlyList<PerfScenario> CreateCalendarScenarios()
{
return
[
new PerfScenario("Calendar.Default", _ => CreateCalendar()),
new PerfScenario("Calendar.SingleDate.Selected", _ => CreateCalendar(
selectionMode: AtomCalendarSelectionMode.SingleDate,
selectedDate: new DateTime(2024, 1, 20))),
new PerfScenario("Calendar.SingleRange.Selected", _ => CreateCalendar(
selectionMode: AtomCalendarSelectionMode.SingleRange,
rangeStart: new DateTime(2024, 1, 12),
rangeEnd: new DateTime(2024, 1, 20))),
new PerfScenario("Calendar.MultipleRange.Blackout", _ => CreateCalendarWithBlackout()),
new PerfScenario("Calendar.YearMode", _ => CreateCalendar(displayMode: AtomCalendarMode.Year)),
new PerfScenario("Calendar.DecadeMode", _ => CreateCalendar(displayMode: AtomCalendarMode.Decade)),
new PerfScenario("Calendar.ShowWeek", _ => CreateCalendar(showWeek: true)),
new PerfScenario("Calendar.YearMode", _ => CreateCalendar(mode: AtomCalendarMode.Year)),
new PerfScenario("Calendar.DisabledDate", _ => CreateCalendarWithDisabledDates()),
new PerfScenario("Calendar.RangeRestricted", _ => CreateCalendar(
displayDateStart: new DateTime(2024, 1, 10),
displayDateEnd: new DateTime(2024, 3, 20))),
new PerfScenario("Calendar.MotionDisabled", _ => CreateCalendar(isMotionEnabled: false)),
validRange: new AtomCalendarDateRange(new DateTime(2024, 1, 10), new DateTime(2024, 3, 20)))),
new PerfScenario("Calendar.RangeBars", _ => CreateCalendarWithRangeBars()),
new PerfScenario("Calendar.Mini", _ => CreateCalendar(fullscreen: false)),
new PerfScenario("Calendar.Batch4", _ => CreateCalendarBatch())
];
}
private static AtomCalendar CreateCalendar(
AtomCalendarMode displayMode = AtomCalendarMode.Month,
AtomCalendarSelectionMode selectionMode = AtomCalendarSelectionMode.SingleRange,
AtomCalendarMode mode = AtomCalendarMode.Month,
DateTime? selectedDate = null,
DateTime? rangeStart = null,
DateTime? rangeEnd = null,
DateTime? displayDateStart = null,
DateTime? displayDateEnd = null,
bool isMotionEnabled = true)
bool showWeek = false,
bool fullscreen = true,
AtomCalendarDateRange? validRange = null)
{
var calendar = new AtomCalendar
{
DisplayDate = new DateTime(2024, 1, 1),
DisplayDateStart = displayDateStart,
DisplayDateEnd = displayDateEnd,
DisplayMode = displayMode,
SelectionMode = selectionMode,
IsMotionEnabled = isMotionEnabled
Mode = mode,
ShowWeek = showWeek,
Fullscreen = fullscreen,
ValidRange = validRange
};
if (selectedDate.HasValue)
{
calendar.SelectedDate = selectedDate.Value;
}
if (rangeStart.HasValue && rangeEnd.HasValue)
{
calendar.SelectedDates.AddRange(rangeStart.Value, rangeEnd.Value);
calendar.Value = selectedDate.Value;
}
return calendar;
}
private static AtomCalendar CreateCalendarWithBlackout()
private static AtomCalendar CreateCalendarWithDisabledDates()
{
var calendar = CreateCalendar(selectionMode: AtomCalendarSelectionMode.MultipleRange);
calendar.BlackoutDates.Add(new AtomCalendarDateRange(new DateTime(2024, 1, 5), new DateTime(2024, 1, 7)));
calendar.BlackoutDates.Add(new AtomCalendarDateRange(new DateTime(2024, 1, 21), new DateTime(2024, 1, 25)));
calendar.SelectedDates.AddRange(new DateTime(2024, 1, 12), new DateTime(2024, 1, 16));
var calendar = CreateCalendar();
calendar.DisabledDate = date => date.Day is 5 or 6 or 7;
return calendar;
}
private static AtomCalendar CreateCalendarWithRangeBars()
{
var calendar = CreateCalendar();
calendar.RangeBars.Add(new AtomUI.Desktop.Controls.CalendarRangeBar
{
StartDate = new DateTime(2024, 1, 12),
EndDate = new DateTime(2024, 1, 20),
Label = "Range"
});
return calendar;
}
@@ -81,11 +77,9 @@ internal static partial class Program
Children =
{
CreateCalendar(),
CreateCalendar(
selectionMode: AtomCalendarSelectionMode.SingleDate,
selectedDate: new DateTime(2024, 1, 20)),
CreateCalendar(displayMode: AtomCalendarMode.Year),
CreateCalendar(displayMode: AtomCalendarMode.Decade, isMotionEnabled: false)
CreateCalendar(selectedDate: new DateTime(2024, 1, 20)),
CreateCalendar(mode: AtomCalendarMode.Year),
CreateCalendar(showWeek: true)
}
};
}
@@ -1,10 +1,8 @@
using System.Reflection;
using Avalonia.Controls;
using Avalonia.VisualTree;
using AtomCalendar = AtomUI.Desktop.Controls.Calendar;
using AtomCalendarButton = AtomUI.Desktop.Controls.CalendarButton;
using AtomCalendarDayButton = AtomUI.Desktop.Controls.CalendarDayButton;
using AtomCalendarMode = AtomUI.Desktop.Controls.CalendarMode;
using AtomCalendarSelectionMode = AtomUI.Desktop.Controls.CalendarSelectionMode;
namespace AtomUI.Performance;
@@ -13,9 +11,10 @@ internal static partial class Program
private static bool RunCalendarStateVerification()
{
var failures = new List<string>();
VerifyMonthModeLazyYearView(failures);
VerifyInitialYearModeLazyMonthView(failures);
VerifyInitialDecadeModeLazyMonthView(failures);
VerifyCalendarMonthGridShape(failures);
VerifyCalendarModeSwitching(failures);
VerifyCalendarValueSelection(failures);
VerifyCalendarDisabledDates(failures);
if (failures.Count == 0)
{
@@ -31,140 +30,128 @@ internal static partial class Program
return false;
}
private static void VerifyMonthModeLazyYearView(ICollection<string> failures)
private static void VerifyCalendarMonthGridShape(ICollection<string> failures)
{
var selectedDate = new DateTime(2024, 1, 20);
var calendar = CreateVerificationCalendar(selectedDate: selectedDate);
var calendar = CreateCalendar(selectedDate: new DateTime(2024, 1, 20));
using var realized = RealizeControl(calendar);
ExpectCalendarShape(calendar, 42, 0, "Default Month Calendar", failures);
ExpectSelectedDate(calendar, selectedDate, failures);
ExpectCalendarShape(calendar, dateCells: 42, monthCells: 0, weekCells: 0, "Month Calendar", failures);
calendar.DisplayMode = AtomCalendarMode.Year;
RefreshLayout(realized.Window);
ExpectCalendarShape(calendar, 42, 12, "Month -> Year Calendar", failures);
calendar.DisplayMode = AtomCalendarMode.Decade;
RefreshLayout(realized.Window);
ExpectCalendarShape(calendar, 42, 12, "Year -> Decade Calendar", failures);
calendar.DisplayMode = AtomCalendarMode.Year;
RefreshLayout(realized.Window);
ExpectCalendarShape(calendar, 42, 12, "Decade -> Year Calendar", failures);
calendar.DisplayMode = AtomCalendarMode.Month;
RefreshLayout(realized.Window);
ExpectCalendarShape(calendar, 42, 12, "Year -> Month Calendar", failures);
ExpectSelectedDate(calendar, selectedDate, failures);
var showWeekCalendar = CreateCalendar(selectedDate: new DateTime(2024, 1, 20), showWeek: true);
using var weekRealized = RealizeControl(showWeekCalendar);
ExpectCalendarShape(showWeekCalendar, dateCells: 42, monthCells: 0, weekCells: 6, "ShowWeek Calendar", failures);
}
private static void VerifyInitialYearModeLazyMonthView(ICollection<string> failures)
private static void VerifyCalendarModeSwitching(ICollection<string> failures)
{
var selectedDate = new DateTime(2024, 1, 20);
var calendar = CreateVerificationCalendar(AtomCalendarMode.Year, selectedDate);
var calendar = CreateCalendar(selectedDate: selectedDate);
using var realized = RealizeControl(calendar);
ExpectCalendarShape(calendar, 0, 12, "Initial Year Calendar", failures);
ExpectCalendarShape(calendar, dateCells: 42, monthCells: 0, weekCells: 0, "Default Month Calendar", failures);
calendar.DisplayMode = AtomCalendarMode.Month;
calendar.Mode = AtomCalendarMode.Year;
RefreshLayout(realized.Window);
ExpectCalendarShape(calendar, 42, 12, "Initial Year -> Month Calendar", failures);
ExpectSelectedDate(calendar, selectedDate, failures);
ExpectCalendarShape(calendar, dateCells: 0, monthCells: 12, weekCells: 0, "Month -> Year Calendar", failures);
calendar.DisplayMode = AtomCalendarMode.Year;
calendar.Mode = AtomCalendarMode.Month;
RefreshLayout(realized.Window);
calendar.DisplayMode = AtomCalendarMode.Month;
RefreshLayout(realized.Window);
ExpectCalendarShape(calendar, 42, 12, "Repeated Year/Month Calendar", failures);
ExpectSelectedDate(calendar, selectedDate, failures);
ExpectCalendarShape(calendar, dateCells: 42, monthCells: 0, weekCells: 0, "Year -> Month Calendar", failures);
Expect(calendar.Value == selectedDate,
$"Calendar Value should survive mode switches, actual {calendar.Value:yyyy-MM-dd}.",
failures);
}
private static void VerifyInitialDecadeModeLazyMonthView(ICollection<string> failures)
private static void VerifyCalendarValueSelection(ICollection<string> failures)
{
var selectedDate = new DateTime(2024, 1, 20);
var calendar = CreateVerificationCalendar(AtomCalendarMode.Decade, selectedDate);
var calendar = CreateCalendar(selectedDate: selectedDate);
using var realized = RealizeControl(calendar);
ExpectCalendarShape(calendar, 0, 12, "Initial Decade Calendar", failures);
var selectedCells = FindCalendarCells(calendar, ":selected");
Expect(selectedCells.Count == 1,
$"Calendar should mark exactly one selected cell, actual {selectedCells.Count}.",
failures);
var selectedCellDate = GetCalendarCellValue(selectedCells.FirstOrDefault());
Expect(selectedCellDate == selectedDate,
$"Selected cell should carry {selectedDate:yyyy-MM-dd}, actual {selectedCellDate:yyyy-MM-dd}.",
failures);
calendar.DisplayMode = AtomCalendarMode.Year;
calendar.Value = new DateTime(2024, 1, 25);
RefreshLayout(realized.Window);
ExpectCalendarShape(calendar, 0, 12, "Initial Decade -> Year Calendar", failures);
calendar.DisplayMode = AtomCalendarMode.Month;
RefreshLayout(realized.Window);
ExpectCalendarShape(calendar, 42, 12, "Initial Decade -> Month Calendar", failures);
ExpectSelectedDate(calendar, selectedDate, failures);
selectedCells = FindCalendarCells(calendar, ":selected");
Expect(selectedCells.Count == 1 &&
GetCalendarCellValue(selectedCells.FirstOrDefault()) == new DateTime(2024, 1, 25),
"Calendar Value change should move the selected cell.",
failures);
}
private static AtomCalendar CreateVerificationCalendar(
AtomCalendarMode displayMode = AtomCalendarMode.Month,
DateTime? selectedDate = null)
private static void VerifyCalendarDisabledDates(ICollection<string> failures)
{
var calendar = new AtomCalendar
{
DisplayDate = new DateTime(2024, 1, 1),
DisplayMode = displayMode,
SelectionMode = AtomCalendarSelectionMode.SingleDate,
IsMotionEnabled = false
};
var calendar = CreateCalendarWithDisabledDates();
if (selectedDate.HasValue)
{
calendar.SelectedDate = selectedDate.Value;
}
return calendar;
using var realized = RealizeControl(calendar);
var disabledCells = FindCalendarCells(calendar, ":disabled");
Expect(disabledCells.Count == 3,
$"Calendar should mark the three DisabledDate days as disabled, actual {disabledCells.Count}.",
failures);
Expect(disabledCells.All(cell => GetCalendarCellValue(cell).Day is 5 or 6 or 7),
"Disabled cells should carry the DisabledDate days.",
failures);
}
private static void ExpectCalendarShape(
AtomCalendar calendar,
int expectedDayButtons,
int expectedCalendarButtons,
int dateCells,
int monthCells,
int weekCells,
string label,
ICollection<string> failures)
{
var dayButtonCount = CountVisuals<AtomCalendarDayButton>(calendar);
var calendarButtonCount = CountVisuals<AtomCalendarButton>(calendar);
var actualDateCells = CountCalendarCells(calendar, ":date");
var actualMonthCells = CountCalendarCells(calendar, ":month");
var actualWeekCells = CountCalendarCells(calendar, ":week");
Expect(dayButtonCount == expectedDayButtons,
$"{label} should have {expectedDayButtons} CalendarDayButton visuals, actual {dayButtonCount}.",
Expect(actualDateCells == dateCells,
$"{label} should have {dateCells} date cells, actual {actualDateCells}.",
failures);
Expect(calendarButtonCount == expectedCalendarButtons,
$"{label} should have {expectedCalendarButtons} CalendarButton visuals, actual {calendarButtonCount}.",
Expect(actualMonthCells == monthCells,
$"{label} should have {monthCells} month cells, actual {actualMonthCells}.",
failures);
Expect(actualWeekCells == weekCells,
$"{label} should have {weekCells} week cells, actual {actualWeekCells}.",
failures);
}
private static void ExpectSelectedDate(
AtomCalendar calendar,
DateTime selectedDate,
ICollection<string> failures)
private static int CountCalendarCells(Control root, string pseudoClass)
{
Expect(calendar.SelectedDate == selectedDate,
$"Calendar SelectedDate should remain {selectedDate:yyyy-MM-dd}, actual {calendar.SelectedDate:yyyy-MM-dd}.",
failures);
Expect(calendar.SelectedDates.Count == 1 && calendar.SelectedDates[0] == selectedDate,
$"Calendar SelectedDates should keep only {selectedDate:yyyy-MM-dd}, actual count {calendar.SelectedDates.Count}.",
failures);
var selectedButton = FindDayButton(calendar, selectedDate);
Expect(selectedButton?.IsSelected == true,
$"Calendar day button for {selectedDate:yyyy-MM-dd} should be selected after mode changes.",
failures);
return root.GetSelfAndVisualDescendants()
.OfType<Control>()
.Count(control => control.GetType().Name == "CalendarViewCell" &&
control.Classes.Contains(pseudoClass));
}
private static AtomCalendarDayButton? FindDayButton(AtomCalendar calendar, DateTime date)
private static List<Control> FindCalendarCells(Control root, string pseudoClass)
{
return calendar.GetSelfAndVisualDescendants()
.OfType<AtomCalendarDayButton>()
.FirstOrDefault(button =>
button.DataContext is DateTime day &&
day.Date == date.Date);
return root.GetSelfAndVisualDescendants()
.OfType<Control>()
.Where(control => control.GetType().Name == "CalendarViewCell" &&
control.Classes.Contains(pseudoClass))
.ToList();
}
private static int CountVisuals<T>(Control control)
where T : Control
private static DateTime GetCalendarCellValue(Control? cell)
{
return control.GetSelfAndVisualDescendants().OfType<T>().Count();
if (cell is null)
{
return default;
}
var model = cell.GetType()
.GetProperty("Model", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
?.GetValue(cell);
var value = model?.GetType()
.GetProperty("Value", BindingFlags.Instance | BindingFlags.Public)
?.GetValue(model);
return value is DateTime dateTime ? dateTime : default;
}
}
@@ -1,3 +1,4 @@
using AtomUI.Controls;
using AtomUI.Desktop.Controls;
using AtomUI.Icons.AntDesign;
using Avalonia.Controls;
@@ -272,7 +273,7 @@ internal static partial class Program
{
return new Avatar
{
BitmapSrc = CardCoverBitmap.Value
Icon = new UserOutlined()
};
}
@@ -235,20 +235,23 @@ internal static partial class Program
Expect(explicitItem.EffectiveContentPadding == new Thickness(4),
"Collapse should not override an item-level ContentPadding local value.",
failures);
Expect(normalItem.HeaderBorderThickness == new Thickness(0, 0, 0, 1),
$"Collapsed non-last CollapseItem should keep a header bottom separator, actual {DescribeThickness(normalItem.HeaderBorderThickness)}.",
// CollapseItem 边框 API 演进:旧 HeaderBorderThickness 由 ItemBorderThickness 取代
// (分隔线移到包裹整个 item 的 shell 边框,不再区分展开/收起状态);
// ContentBorderThickness 恒为顶部单边分隔线 (0, line, 0, 0)。
Expect(normalItem.ItemBorderThickness == new Thickness(0, 0, 0, 1),
$"Collapsed non-last CollapseItem should keep an item shell bottom separator, actual {DescribeThickness(normalItem.ItemBorderThickness)}.",
failures);
Expect(normalItem.ContentBorderThickness == new Thickness(0),
$"Collapsed CollapseItem should not keep a content border, actual {DescribeThickness(normalItem.ContentBorderThickness)}.",
Expect(normalItem.ContentBorderThickness == new Thickness(0, 1, 0, 0),
$"Collapsed CollapseItem should keep the constant content top separator (content frame is hidden while collapsed), actual {DescribeThickness(normalItem.ContentBorderThickness)}.",
failures);
Expect(explicitItem.HeaderBorderThickness == new Thickness(0),
$"Expanded non-last CollapseItem should not draw the header/content separator on the header, actual {DescribeThickness(explicitItem.HeaderBorderThickness)}.",
Expect(explicitItem.ItemBorderThickness == new Thickness(0, 0, 0, 1),
$"Expanded non-last CollapseItem should keep the item shell bottom separator, actual {DescribeThickness(explicitItem.ItemBorderThickness)}.",
failures);
Expect(explicitItem.ContentBorderThickness == new Thickness(0, 1, 0, 1),
$"Expanded non-last CollapseItem should use content top and bottom borders, actual {DescribeThickness(explicitItem.ContentBorderThickness)}.",
Expect(explicitItem.ContentBorderThickness == new Thickness(0, 1, 0, 0),
$"Expanded non-last CollapseItem should use the content top separator, actual {DescribeThickness(explicitItem.ContentBorderThickness)}.",
failures);
Expect(lastItem.HeaderBorderThickness == new Thickness(0),
$"Expanded last CollapseItem should not keep a header bottom border, actual {DescribeThickness(lastItem.HeaderBorderThickness)}.",
Expect(lastItem.ItemBorderThickness == new Thickness(0),
$"Expanded last CollapseItem should not keep an item shell border, actual {DescribeThickness(lastItem.ItemBorderThickness)}.",
failures);
Expect(lastItem.ContentBorderThickness == new Thickness(0, 1, 0, 0),
$"Expanded last CollapseItem should keep only the content top separator, actual {DescribeThickness(lastItem.ContentBorderThickness)}.",
@@ -283,12 +283,9 @@ internal static partial class Program
{
Header = header,
FieldId = fieldId,
Binding = new Binding(bindingPath)
Binding = new Binding(bindingPath),
Filters = filters
};
foreach (var filter in filters)
{
column.Filters.Add(filter);
}
return column;
}
@@ -2,28 +2,29 @@ using Avalonia.Controls;
using Avalonia.Layout;
using AtomImagePreviewer = AtomUI.Desktop.Controls.ImagePreviewer;
using AtomImageGroupPreviewer = AtomUI.Desktop.Controls.ImageGroupPreviewer;
using ImagePreviewItem = AtomUI.Desktop.Controls.ImagePreviewItem;
using ImageSource = AtomUI.Controls.ImageSource;
namespace AtomUI.Performance;
internal static partial class Program
{
private static readonly string[] ImagePreviewerDefaultImages =
[
GetImagePreviewerAssetUri("1.png")
];
// ImagePreviewer API 演进:ItemsSource 从 string[] 变为 IEnumerable<ImagePreviewItem>,
// 图片地址需通过 ImageSource.Parse 包装;FallbackImageSrc / CoverImageSrc 已移除,
// 分别由 ImagePreviewItem.FallbackSource / ThumbnailSource 取代。
private static readonly ImagePreviewItem[] ImagePreviewerDefaultImages =
CreateImagePreviewerItems(GetImagePreviewerAssetUri("1.png"));
private static readonly string[] ImagePreviewerThreeImages =
[
GetImagePreviewerAssetUri("4.webp"),
GetImagePreviewerAssetUri("5.webp"),
GetImagePreviewerAssetUri("6.webp")
];
private static readonly ImagePreviewItem[] ImagePreviewerThreeImages =
CreateImagePreviewerItems(
GetImagePreviewerAssetUri("4.webp"),
GetImagePreviewerAssetUri("5.webp"),
GetImagePreviewerAssetUri("6.webp"));
private static readonly string[] ImagePreviewerTwoImages =
[
GetImagePreviewerAssetUri("2.svg"),
GetImagePreviewerAssetUri("3.svg")
];
private static readonly ImagePreviewItem[] ImagePreviewerTwoImages =
CreateImagePreviewerItems(
GetImagePreviewerAssetUri("2.svg"),
GetImagePreviewerAssetUri("3.svg"));
private static readonly string ImagePreviewerFallbackImage = GetImagePreviewerAssetUri("Fallback.png");
private static readonly string ImagePreviewerBlurImage = GetImagePreviewerAssetUri("Blur.png");
@@ -41,6 +42,13 @@ internal static partial class Program
];
}
private static ImagePreviewItem[] CreateImagePreviewerItems(params string[] sources)
{
return sources
.Select(source => new ImagePreviewItem(ImageSource.Parse(source)))
.ToArray();
}
private static AtomImagePreviewer CreateBasicImagePreviewer()
{
return new AtomImagePreviewer
@@ -52,10 +60,17 @@ internal static partial class Program
private static AtomImagePreviewer CreateFallbackImagePreviewer()
{
// FallbackImageSrc 已移除:回退图改为 ImagePreviewItem.FallbackSource
return new AtomImagePreviewer
{
Width = 200,
FallbackImageSrc = ImagePreviewerFallbackImage
Width = 200,
ItemsSource = new[]
{
new ImagePreviewItem(ImageSource.Parse(GetImagePreviewerAssetUri("1.png")))
{
FallbackSource = ImageSource.Parse(ImagePreviewerFallbackImage)
}
}
};
}
@@ -70,11 +85,17 @@ internal static partial class Program
private static AtomImagePreviewer CreateCustomCoverImagePreviewer()
{
// CoverImageSrc 已移除:自定义封面改为 ImagePreviewItem.ThumbnailSource
return new AtomImagePreviewer
{
Width = 200,
ItemsSource = ImagePreviewerDefaultImages,
CoverImageSrc = ImagePreviewerBlurImage
Width = 200,
ItemsSource = new[]
{
new ImagePreviewItem(ImageSource.Parse(GetImagePreviewerAssetUri("1.png")))
{
ThumbnailSource = ImageSource.Parse(ImagePreviewerBlurImage)
}
}
};
}
@@ -1,6 +1,9 @@
using System.Collections;
using System.Reflection;
using AtomUI.Controls;
using Avalonia.Threading;
using ImagePreviewItem = AtomUI.Desktop.Controls.ImagePreviewItem;
using ImageSource = AtomUI.Controls.ImageSource;
using AtomImagePreviewer = AtomUI.Desktop.Controls.ImagePreviewer;
using AtomImageGroupPreviewer = AtomUI.Desktop.Controls.ImageGroupPreviewer;
@@ -36,11 +39,13 @@ internal static partial class Program
var previewer = CreateMultiSourceImagePreviewer();
using var realized = RealizeControl(previewer);
Expect(GetImagePreviewerEffectiveSourceCount(previewer) == 0,
"Single ImagePreviewer closed state should not materialize dialog source list.",
// 新管线中 EffectiveItems 在 attach 时即建立(条目为轻量包装),
// 关闭状态的裁剪点改为:仅封面项触发缩略图加载请求。
Expect(GetImagePreviewerEffectiveSourceCount(previewer) == ImagePreviewerThreeImages.Length,
"Single ImagePreviewer should keep lightweight entries for all items.",
failures);
Expect(GetImagePreviewerEffectiveCoverImage(previewer) != null,
"Single ImagePreviewer closed state should materialize only the visible cover image.",
Expect(previewer.CoverLoadState != AtomUI.Controls.ImageLoadState.Idle,
"Single ImagePreviewer closed state should request the visible cover thumbnail load.",
failures);
Expect(CountVisualByTypeName(previewer, "ImagePreviewerCover") == 1,
"Single ImagePreviewer should keep one cover visual.",
@@ -49,10 +54,10 @@ internal static partial class Program
previewer.ItemsSource = null;
RefreshLayout(realized.Window);
Expect(GetImagePreviewerEffectiveSourceCount(previewer) == 0,
"Clearing ItemsSource should clear effective dialog sources.",
"Clearing ItemsSource should clear effective entries.",
failures);
Expect(GetImagePreviewerEffectiveCoverImage(previewer) == null,
"Clearing ItemsSource should release the effective cover image when no custom cover or fallback exists.",
"Clearing ItemsSource should release the effective cover image when no fallback exists.",
failures);
}
@@ -68,7 +73,11 @@ internal static partial class Program
"OpenDialog should set IsOpen=true.",
failures);
Expect(GetImagePreviewerEffectiveSourceCount(previewer) == ImagePreviewerThreeImages.Length,
"Opening ImagePreviewer should materialize all preview dialog sources.",
"Opening ImagePreviewer should keep dialog entries materialized.",
failures);
Expect(previewer.CurrentLoadState == AtomUI.Controls.ImageLoadState.Loading ||
previewer.IsCurrentLoading,
"Opening ImagePreviewer should request the current full image load.",
failures);
previewer.IsOpen = false;
@@ -113,35 +122,65 @@ internal static partial class Program
{
AtomImagePreviewer previewer = CreateCustomCoverImagePreviewer();
using var realized = RealizeControl(previewer);
var diagLoader = Avalonia.Application.Current?.GetImageLoader();
var diagResult = diagLoader?.LoadAsync(new AtomUI.Controls.ImageLoadRequest(
ImageSource.Parse(GetImagePreviewerAssetUri("1.png")))).AsTask().GetAwaiter().GetResult();
Console.Error.WriteLine($"[diag] direct load success={diagResult?.IsSuccess} err={diagResult?.Error} snapshot={diagLoader?.Snapshot}");
// 新管线封面走异步缩略图加载提交,等待加载完成后再断言(与测试项目 WaitUntil 用法一致)
WaitForImagePreviewer(() => previewer.CoverLoadState == AtomUI.Controls.ImageLoadState.Loaded);
var firstCover = GetImagePreviewerEffectiveCoverImage(previewer);
Expect(firstCover != null,
"Custom cover ImagePreviewer should materialize its custom cover image.",
failures);
Expect(GetImagePreviewerEffectiveSourceCount(previewer) == 0,
"Custom cover ImagePreviewer should not materialize dialog sources while closed.",
Expect(previewer.CurrentLoadState == AtomUI.Controls.ImageLoadState.Idle,
"Custom cover ImagePreviewer should not request full dialog image loads while closed.",
failures);
previewer.CoverImageSrc = ImagePreviewerFallbackImage;
// CoverImageSrc 已移除:自定义封面由 ImagePreviewItem.ThumbnailSource 提供,
// 替换自定义封面等价于替换带不同 ThumbnailSource 的 ItemsSource。
previewer.ItemsSource = new[]
{
new ImagePreviewItem(ImageSource.Parse(GetImagePreviewerAssetUri("1.png")))
{
ThumbnailSource = ImageSource.Parse(ImagePreviewerFallbackImage)
}
};
RefreshLayout(realized.Window);
WaitForImagePreviewer(() => previewer.CoverLoadState == AtomUI.Controls.ImageLoadState.Loaded);
var secondCover = GetImagePreviewerEffectiveCoverImage(previewer);
Expect(secondCover != null && !ReferenceEquals(firstCover, secondCover),
"Replacing CoverImageSrc should replace the effective cover image.",
"Replacing the custom cover thumbnail source should replace the effective cover image.",
failures);
previewer.CoverImageSrc = null;
previewer.ItemsSource = ImagePreviewerDefaultImages;
RefreshLayout(realized.Window);
WaitForImagePreviewer(() => previewer.CoverLoadState == AtomUI.Controls.ImageLoadState.Loaded);
Expect(GetImagePreviewerEffectiveCoverImage(previewer) != null,
"Clearing CoverImageSrc should fall back to the first ItemsSource image as the cover.",
"Clearing the custom cover thumbnail should fall back to the item main source as the cover.",
failures);
Expect(GetImagePreviewerEffectiveSourceCount(previewer) == 0,
"Clearing CoverImageSrc should not materialize dialog sources while closed.",
Expect(previewer.CurrentLoadState == AtomUI.Controls.ImageLoadState.Idle,
"Replacing the custom cover thumbnail should not request full dialog image loads while closed.",
failures);
}
private static void WaitForImagePreviewer(Func<bool> condition)
{
var deadline = Environment.TickCount64 + 5000;
while (true)
{
Dispatcher.UIThread.RunJobs();
if (condition() || Environment.TickCount64 >= deadline)
{
return;
}
Thread.Sleep(10);
}
}
private static int GetImagePreviewerEffectiveSourceCount(object previewer)
{
return GetNonPublicProperty(previewer, "AtomUI.Desktop.Controls.AbstractImagePreviewer", "EffectiveSources") is ICollection sources
return GetNonPublicProperty(previewer, "AtomUI.Desktop.Controls.AbstractImagePreviewer", "EffectiveItems") is ICollection sources
? sources.Count
: 0;
}
@@ -30,8 +30,8 @@ internal static partial class Program
rightAddOn: new Avalonia.Controls.TextBlock { Text = ".com" })),
new PerfScenario("SearchEdit.Default", _ => CreateSearchEdit()),
new PerfScenario("SearchEdit.TextButton", _ => CreateSearchEdit(searchButtonText: "Search")),
new PerfScenario("SearchEdit.Primary", _ => CreateSearchEdit(searchButtonText: "Search", searchButtonStyle: SearchEditButtonStyle.Primary)),
new PerfScenario("SearchEdit.Loading", _ => CreateSearchEdit(searchButtonStyle: SearchEditButtonStyle.Primary, isOperating: true)),
new PerfScenario("SearchEdit.Primary", _ => CreateSearchEdit(searchButtonText: "Search", searchButtonStyle: SearchButtonType.Primary)),
new PerfScenario("SearchEdit.Loading", _ => CreateSearchEdit(searchButtonStyle: SearchButtonType.Primary, isOperating: true)),
new PerfScenario("SearchEdit.AllowClear", _ => CreateSearchEdit(text: "search", isAllowClear: true)),
new PerfScenario("SearchEdit.InnerRight", _ => CreateSearchEdit(innerRightContent: new AudioOutlined { Width = 16, Height = 16 })),
new PerfScenario("SearchEdit.LeftAddOn", _ => CreateSearchEdit(leftAddOn: new Avalonia.Controls.TextBlock { Text = "https://" })),
@@ -111,7 +111,7 @@ internal static partial class Program
private static SearchEdit CreateSearchEdit(
string? text = null,
bool isAllowClear = false,
SearchEditButtonStyle searchButtonStyle = SearchEditButtonStyle.Default,
SearchButtonType searchButtonStyle = SearchButtonType.Default,
string? searchButtonText = null,
bool isOperating = false,
object? innerRightContent = null,
@@ -192,7 +192,7 @@ internal static partial class Program
panel.Children.Add(CreateSearchEdit(
text: i % 5 == 0 ? "search" : null,
isAllowClear: i % 9 == 0,
searchButtonStyle: i % 3 == 0 ? SearchEditButtonStyle.Primary : SearchEditButtonStyle.Default,
searchButtonStyle: i % 3 == 0 ? SearchButtonType.Primary : SearchButtonType.Default,
searchButtonText: i % 2 == 0 ? "Search" : null,
isOperating: i % 11 == 0,
innerRightContent: i % 10 == 0 ? new AudioOutlined { Width = 16, Height = 16 } : null,
@@ -1,5 +1,7 @@
using AtomUI.Desktop.Controls;
using AtomUI.Icons.AntDesign;
using AtomUI.Theme.Algorithms;
using AtomUI.Theme.DesignTokens;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
@@ -36,8 +38,11 @@ internal static partial class Program
private static void VerifyInputSelectionTokenMapping(ICollection<string> failures)
{
// 主题算法 API 演进:Calculate(token) → Evaluate(effectiveSeed, previousMap, nextMap)。
// 与 ThemeCompiler 一致,seed 与 map 使用独立实例(Dark 算法在 previousMap 为 null 时
// 会先执行 Default 算法并写回 nextMap,若 seed 与 map 同实例会污染 Dark 的基色读取)。
var defaultToken = new DesignToken();
new DefaultThemeVariantCalculator().Calculate(defaultToken);
new DefaultThemeVariantCalculator().Evaluate(new DesignToken(), null, defaultToken);
Expect(defaultToken.SelectionBackground == defaultToken.ColorPrimary,
$"Default SelectionBackground should use ColorPrimary ({defaultToken.ColorPrimary}), actual {defaultToken.SelectionBackground}.",
@@ -47,7 +52,7 @@ internal static partial class Program
failures);
var darkToken = new DesignToken();
new DarkThemeVariantCalculator(new DefaultThemeVariantCalculator()).Calculate(darkToken);
new DarkThemeVariantCalculator().Evaluate(new DesignToken(), null, darkToken);
Expect(darkToken.SelectionBackground == darkToken.ColorPrimary,
$"Dark SelectionBackground should use ColorPrimary ({darkToken.ColorPrimary}), actual {darkToken.SelectionBackground}.",
@@ -319,7 +324,8 @@ internal static partial class Program
};
using var realized = RealizeControl(searchEdit);
Expect(FindVisualByName<SearchButton>(searchEdit, "PART_RightAddOn") != null,
// SearchButton 控件类型已移除:搜索按钮现在是 SearchEditDecoratedBox 模板中的 atom:Button#PART_RightAddOn
Expect(FindVisualByName<Button>(searchEdit, "PART_RightAddOn") != null,
"SearchEdit should keep its search button.", failures);
Expect(FindVisualByName<InputClearIconButton>(searchEdit, "PART_ClearButton") == null,
"SearchEdit default should not create a clear button.", failures);
@@ -1,9 +1,7 @@
using System.Collections;
using System.ComponentModel;
using AtomUI.Controls.Data;
using AtomUI.Controls.Utils;
using AtomUI.Desktop.Controls;
using Avalonia.Controls.Selection;
namespace AtomUI.Performance;
@@ -17,7 +15,8 @@ internal static partial class Program
VerifyListViewCollectionFilterLifecycle(failures);
VerifyListViewEmptyIndicatorLazyMaterialization(failures);
VerifyListViewPaginationLifecycle(failures);
VerifyListViewSelectionModelLifecycle(failures);
// VerifyListViewSelectionModelLifecycle 已删除:ListView.Selection 变为只读的内部固定
// ListViewSelectionModel(不再支持外部替换选择模型),替换模型的订阅释放验证已无等价 API。
if (failures.Count == 0)
{
@@ -184,159 +183,4 @@ internal static partial class Program
"New bottom pagination should receive the current ListView page size.",
failures);
}
private static void VerifyListViewSelectionModelLifecycle(ICollection<string> failures)
{
var firstSelection = new CountingSelectionModel();
var secondSelection = new CountingSelectionModel();
var listView = CreateListView(CreateListViewItems(5));
listView.Selection = firstSelection;
using var realized = RealizeControl(listView);
listView.Selection = secondSelection;
RefreshLayout(realized.Window);
Expect(firstSelection.PropertyChangedSubscriberCount == 0,
"Replaced ListView selection model should release PropertyChanged subscription.",
failures);
Expect(firstSelection.SelectionChangedSubscriberCount == 0,
"Replaced ListView selection model should release SelectionChanged subscription.",
failures);
Expect(firstSelection.LostSelectionSubscriberCount == 0,
"Replaced ListView selection model should release LostSelection subscription.",
failures);
}
private sealed class CountingSelectionModel : ISelectionModel
{
private readonly List<int> _selectedIndexes = [];
private readonly List<object?> _selectedItems = [];
private PropertyChangedEventHandler? _propertyChanged;
private EventHandler<SelectionModelIndexesChangedEventArgs>? _indexesChanged;
private EventHandler<SelectionModelSelectionChangedEventArgs>? _selectionChanged;
private EventHandler? _lostSelection;
private EventHandler? _sourceReset;
public int PropertyChangedSubscriberCount { get; private set; }
public int SelectionChangedSubscriberCount { get; private set; }
public int LostSelectionSubscriberCount { get; private set; }
public IEnumerable? Source { get; set; }
public bool SingleSelect { get; set; }
public int SelectedIndex { get; set; } = -1;
public IReadOnlyList<int> SelectedIndexes => _selectedIndexes;
public object? SelectedItem { get; set; }
public IReadOnlyList<object?> SelectedItems => _selectedItems;
public int AnchorIndex { get; set; } = -1;
public int Count => Source is ICollection collection ? collection.Count : 0;
public event PropertyChangedEventHandler? PropertyChanged
{
add
{
_propertyChanged += value;
PropertyChangedSubscriberCount++;
}
remove
{
_propertyChanged -= value;
PropertyChangedSubscriberCount--;
}
}
public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged
{
add => _indexesChanged += value;
remove => _indexesChanged -= value;
}
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged
{
add
{
_selectionChanged += value;
SelectionChangedSubscriberCount++;
}
remove
{
_selectionChanged -= value;
SelectionChangedSubscriberCount--;
}
}
public event EventHandler? LostSelection
{
add
{
_lostSelection += value;
LostSelectionSubscriberCount++;
}
remove
{
_lostSelection -= value;
LostSelectionSubscriberCount--;
}
}
public event EventHandler? SourceReset
{
add => _sourceReset += value;
remove => _sourceReset -= value;
}
public void BeginBatchUpdate()
{
}
public void EndBatchUpdate()
{
}
public bool IsSelected(int index) => _selectedIndexes.Contains(index);
public void Select(int index)
{
_selectedIndexes.Clear();
_selectedItems.Clear();
_selectedIndexes.Add(index);
SelectedIndex = index;
AnchorIndex = index;
}
public void Deselect(int index)
{
_selectedIndexes.Remove(index);
if (SelectedIndex == index)
{
SelectedIndex = -1;
}
}
public void SelectRange(int start, int end)
{
for (var i = start; i <= end; i++)
{
Select(i);
}
}
public void DeselectRange(int start, int end)
{
for (var i = start; i <= end; i++)
{
Deselect(i);
}
}
public void SelectAll()
{
}
public void Clear()
{
_selectedIndexes.Clear();
_selectedItems.Clear();
SelectedIndex = -1;
SelectedItem = null;
}
}
}
@@ -491,13 +491,14 @@ internal static partial class Program
using var realized = RealizeControl(candidateList);
RefreshLayout(realized.Window);
candidateList.SelectedItems = new List<object?>(options);
// SelectedItems 变为只读,等价操作通过 Selection 模型完成
candidateList.Selection.SelectAll();
RefreshLayout(realized.Window);
Expect(candidateList.IsEffectiveEmptyVisible,
"SelectCandidateList should show empty indicator when all options are selected and hidden.",
failures);
candidateList.SelectedItems = options.Take(options.Count - 1).Cast<object?>().ToList();
candidateList.Selection.Deselect(options.Count - 1);
RefreshLayout(realized.Window);
var removedContainer = candidateList.ContainerFromItem(options[^1]) as SelectCandidateListItem;
Expect(removedContainer is { IsVisible: true },
@@ -1,5 +1,8 @@
using System.Reflection;
using AtomUI.Desktop.Controls;
using AtomUI.Theme;
using AtomUI.Theme.Algorithms;
using AtomUI.Theme.Configuration;
using Avalonia;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
@@ -174,15 +177,18 @@ internal static partial class Program
private static void VerifySplitButtonSecondaryButtonRearrangesAfterCompactThemeToggle(ICollection<string> failures)
{
var application = Application.Current;
Expect(application is not null,
"SplitButton compact theme layout verification requires an Avalonia application.",
var themeManager = application?.GetThemeManager();
Expect(themeManager is not null,
"SplitButton compact theme layout verification requires the AtomUI theme manager.",
failures);
if (application is null)
if (themeManager is null)
{
return;
}
application.SetCompactThemeMode(false);
// SetCompactThemeMode 扩展已随旧 ThemeManager 移除:
// 紧凑模式现在通过 ThemeAlgorithm.Compact 算法以 ApplyThemeAsync 应用。
ApplyCompactThemeMode(themeManager, isCompact: false);
var splitButton = new SplitButton
{
Content = "Split",
@@ -199,24 +205,39 @@ internal static partial class Program
failures);
if (secondaryButton is null)
{
ApplyCompactThemeMode(themeManager, isCompact: false);
return;
}
application.SetCompactThemeMode(true);
ApplyCompactThemeMode(themeManager, isCompact: true);
RefreshLayout(realized.Window);
Expect(Math.Abs(secondaryButton.Bounds.Right - splitButton.Bounds.Width) <= 0.001,
$"Secondary button should be rearranged after compact theme is enabled. SplitButton width: {splitButton.Bounds.Width:0.###}, secondary right: {secondaryButton.Bounds.Right:0.###}.",
failures);
application.SetCompactThemeMode(false);
ApplyCompactThemeMode(themeManager, isCompact: false);
RefreshLayout(realized.Window);
Expect(Math.Abs(secondaryButton.Bounds.Right - splitButton.Bounds.Width) <= 0.001,
$"Secondary button should be rearranged after compact theme is disabled. SplitButton width: {splitButton.Bounds.Width:0.###}, secondary right: {secondaryButton.Bounds.Right:0.###}.",
failures);
application.SetCompactThemeMode(false);
ApplyCompactThemeMode(themeManager, isCompact: false);
}
private static void ApplyCompactThemeMode(IThemeManager themeManager, bool isCompact)
{
var algorithms = isCompact
? new[] { ThemeAlgorithm.Default, ThemeAlgorithm.Compact }
: new[] { ThemeAlgorithm.Default };
var config = new ThemeConfigBuilder()
.WithAlgorithms(algorithms)
.Build();
themeManager.ApplyThemeAsync(new ThemeRequest(
themeManager.CurrentTheme?.ThemeId ?? IThemeManager.DEFAULT_THEME_ID,
config,
ThemeTransitionReason.UserRequest)).GetAwaiter().GetResult();
}
private static int GetFlyoutOpenedHandlerCount(FlyoutBase flyout)
@@ -13,11 +13,11 @@ internal static partial class Program
return
[
new PerfScenario("Steps.Basic.Items3", _ => CreateSteps()),
new PerfScenario("Steps.Vertical.Items3", _ => CreateSteps(orientation: Orientation.Vertical, currentStep: 1)),
new PerfScenario("Steps.Dot.Items4", _ => CreateSteps(count: 4, indicatorType: StepsItemIndicatorType.Dot, currentStep: 1)),
new PerfScenario("Steps.Navigation.Items4", _ => CreateSteps(count: 4, style: StepsStyle.Navigation, isClickable: true)),
new PerfScenario("Steps.Inline.Items3", _ => CreateSteps(style: StepsStyle.Inline, currentStep: 1)),
new PerfScenario("Steps.Progress.Items3", _ => CreateSteps(currentStep: 1, progressValue: 60, isShowProgress: true)),
new PerfScenario("Steps.Vertical.Items3", _ => CreateSteps(orientation: Orientation.Vertical, current: 1)),
new PerfScenario("Steps.Dot.Items4", _ => CreateSteps(count: 4, type: StepsType.Dot, current: 1)),
new PerfScenario("Steps.Navigation.Items4", _ => CreateSteps(count: 4, type: StepsType.Navigation, isClickable: true)),
new PerfScenario("Steps.Inline.Items3", _ => CreateSteps(type: StepsType.Inline, current: 1)),
new PerfScenario("Steps.Progress.Items3", _ => CreateSteps(current: 1, percent: 60)),
new PerfScenario("Steps.Icon.Items4", _ => CreateIconSteps()),
new PerfScenario("Steps.GalleryShape", _ => CreateStepsGalleryShape())
];
@@ -25,32 +25,28 @@ internal static partial class Program
private static Steps CreateSteps(int count = 3,
Orientation orientation = Orientation.Horizontal,
StepsStyle style = StepsStyle.Default,
StepsItemIndicatorType indicatorType = StepsItemIndicatorType.Default,
int currentStep = 0,
StepsType type = StepsType.Default,
int current = 0,
bool isClickable = false,
bool isShowProgress = false,
double progressValue = 0)
double? percent = null)
{
var steps = new Steps
{
Width = orientation == Orientation.Horizontal ? 760 : 360,
CurrentStep = currentStep,
Orientation = orientation,
Style = style,
ItemIndicatorType = indicatorType,
IsItemClickable = isClickable,
IsShowItemProgress = isShowProgress,
ProgressValue = progressValue
Width = orientation == Orientation.Horizontal ? 760 : 360,
Current = current,
Orientation = orientation,
Type = type,
IsItemClickable = isClickable,
Percent = percent
};
for (var i = 0; i < count; i++)
{
steps.Items.Add(new StepsItem
{
Header = $"Step {i + 1}",
SubHeader = i == 1 ? "Left 00:00:08" : null,
Description = "This is a description."
Header = $"Step {i + 1}",
SubHeader = i == 1 ? "Left 00:00:08" : null,
Content = "This is a description."
});
}
@@ -60,10 +56,10 @@ internal static partial class Program
private static Steps CreateIconSteps()
{
var steps = CreateSteps(count: 0);
steps.Items.Add(new StepsItem { Header = "Login", Status = StepsItemStatus.Finish, Icon = new UserOutlined() });
steps.Items.Add(new StepsItem { Header = "Verification", Status = StepsItemStatus.Finish, Icon = new SolutionOutlined() });
steps.Items.Add(new StepsItem { Header = "Pay", Status = StepsItemStatus.Process, Icon = new LoadingOutlined() });
steps.Items.Add(new StepsItem { Header = "Done", Status = StepsItemStatus.Wait, Icon = new SmileOutlined() });
steps.Items.Add(new StepsItem { Header = "Login", Status = StepsStatus.Finish, Icon = new UserOutlined() });
steps.Items.Add(new StepsItem { Header = "Verification", Status = StepsStatus.Finish, Icon = new SolutionOutlined() });
steps.Items.Add(new StepsItem { Header = "Pay", Status = StepsStatus.Process, Icon = new LoadingOutlined() });
steps.Items.Add(new StepsItem { Header = "Done", Status = StepsStatus.Wait, Icon = new SmileOutlined() });
return steps;
}
@@ -75,17 +71,17 @@ internal static partial class Program
};
root.Children.Add(CreateSteps());
root.Children.Add(CreateSteps(currentStep: 1, orientation: Orientation.Vertical));
root.Children.Add(CreateSteps(current: 1, orientation: Orientation.Vertical));
root.Children.Add(CreateIconSteps());
root.Children.Add(CreateSteps(count: 4, indicatorType: StepsItemIndicatorType.Dot, currentStep: 1));
root.Children.Add(CreateSteps(count: 4, indicatorType: StepsItemIndicatorType.Dot, orientation: Orientation.Vertical, currentStep: 1));
root.Children.Add(CreateSteps(count: 4, style: StepsStyle.Navigation, isClickable: true));
root.Children.Add(CreateSteps(count: 4, style: StepsStyle.Navigation, orientation: Orientation.Vertical, isClickable: true));
root.Children.Add(CreateSteps(currentStep: 1, progressValue: 60, isShowProgress: true));
var verticalLabelSteps = CreateSteps(currentStep: 1, progressValue: 45, isShowProgress: true);
verticalLabelSteps.LabelPlacement = Orientation.Vertical;
root.Children.Add(verticalLabelSteps);
root.Children.Add(CreateSteps(style: StepsStyle.Inline, currentStep: 1));
root.Children.Add(CreateSteps(count: 4, type: StepsType.Dot, current: 1));
root.Children.Add(CreateSteps(count: 4, type: StepsType.Dot, orientation: Orientation.Vertical, current: 1));
root.Children.Add(CreateSteps(count: 4, type: StepsType.Navigation, isClickable: true));
root.Children.Add(CreateSteps(count: 4, type: StepsType.Navigation, orientation: Orientation.Vertical, isClickable: true));
root.Children.Add(CreateSteps(current: 1, percent: 60));
var verticalTitleSteps = CreateSteps(current: 1, percent: 45);
verticalTitleSteps.TitlePlacement = Orientation.Vertical;
root.Children.Add(verticalTitleSteps);
root.Children.Add(CreateSteps(type: StepsType.Inline, current: 1));
root.Children.Add(new AtomTextBlock { Text = "Steps gallery shape sentinel" });
return root;
@@ -12,10 +12,9 @@ internal static partial class Program
{
var failures = new List<string>();
VerifyStepsPanelDefinitions(failures);
VerifyStepsIconProgressState(failures);
VerifyStepsProgressState(failures);
VerifyStepsStatusMatrix(failures);
VerifyStepsIndicatorTemplateShape(failures);
VerifyStepsCurrentContentLifecycle(failures);
if (failures.Count == 0)
{
@@ -54,9 +53,9 @@ internal static partial class Program
failures);
}
private static void VerifyStepsIconProgressState(ICollection<string> failures)
private static void VerifyStepsProgressState(ICollection<string> failures)
{
var steps = CreateSteps(currentStep: 1, isShowProgress: true, progressValue: 50);
var steps = CreateSteps(current: 1, percent: 50);
using var realized = RealizeControl(steps);
var items = GetRealizedStepsItems(steps);
@@ -69,26 +68,29 @@ internal static partial class Program
}
var first = items[0];
Expect(GetNonPublicProperty<bool>(first, "IsEffectiveShowProgress"),
"StepsItem should enable effective progress when progress is shown and no custom icon is set.",
Expect(GetNonPublicProperty<bool>(first, "IsProgressVisible"),
"StepsItem should show progress on the current item when Percent is set and no custom icon is used.",
failures);
Expect(!GetNonPublicProperty<bool>(items[1], "IsProgressVisible") || items[1].Status == StepsStatus.Process,
"StepsItem should not show progress on non-current items when Percent is set.",
failures);
first.Icon = new UserOutlined();
RefreshLayout(realized.Window);
Expect(!GetNonPublicProperty<bool>(first, "IsEffectiveShowProgress"),
"StepsItem should disable effective progress when a custom icon is added.",
Expect(!GetNonPublicProperty<bool>(first, "IsProgressVisible"),
"StepsItem should hide progress when a custom icon is added.",
failures);
first.Icon = null;
RefreshLayout(realized.Window);
Expect(GetNonPublicProperty<bool>(first, "IsEffectiveShowProgress"),
"StepsItem should re-enable effective progress when the custom icon is cleared.",
Expect(GetNonPublicProperty<bool>(first, "IsProgressVisible"),
"StepsItem should re-enable progress when the custom icon is cleared.",
failures);
}
private static void VerifyStepsStatusMatrix(ICollection<string> failures)
{
var steps = CreateSteps(currentStep: 1);
var steps = CreateSteps(current: 1);
using var realized = RealizeControl(steps);
var items = GetRealizedStepsItems(steps);
@@ -100,62 +102,35 @@ internal static partial class Program
return;
}
Expect(items[0].Status == StepsItemStatus.Finish && GetNonPublicProperty<bool>(items[0], "IsFinished"),
$"Step before current should be Finish/finished, actual {items[0].Status}/{GetNonPublicProperty<bool>(items[0], "IsFinished")}.",
Expect(items[0].Status == StepsStatus.Finish,
$"Step before current should be Finish, actual {items[0].Status}.",
failures);
Expect(items[1].Status == StepsItemStatus.Process && !GetNonPublicProperty<bool>(items[1], "IsFinished"),
$"Current step should be Process/not finished, actual {items[1].Status}/{GetNonPublicProperty<bool>(items[1], "IsFinished")}.",
Expect(items[1].Status == StepsStatus.Process,
$"Current step should be Process, actual {items[1].Status}.",
failures);
Expect(items[2].Status == StepsItemStatus.Wait && !GetNonPublicProperty<bool>(items[2], "IsFinished"),
$"Step after current should be Wait/not finished, actual {items[2].Status}/{GetNonPublicProperty<bool>(items[2], "IsFinished")}.",
Expect(items[2].Status == StepsStatus.Wait,
$"Step after current should be Wait, actual {items[2].Status}.",
failures);
steps.CurrentStepStatus = StepsItemStatus.Error;
steps.Status = StepsStatus.Error;
RefreshLayout(realized.Window);
Expect(items[1].Status == StepsItemStatus.Error,
$"CurrentStepStatus change should update the selected item, actual {items[1].Status}.",
Expect(items[1].Status == StepsStatus.Error,
$"Steps.Status change should update the current item, actual {items[1].Status}.",
failures);
steps.CurrentStep = 2;
steps.Current = 2;
RefreshLayout(realized.Window);
Expect(items[1].Status == StepsItemStatus.Finish && GetNonPublicProperty<bool>(items[1], "IsFinished"),
$"Previously current step should become Finish/finished, actual {items[1].Status}/{GetNonPublicProperty<bool>(items[1], "IsFinished")}.",
Expect(items[1].Status == StepsStatus.Finish,
$"Previously current step should become Finish, actual {items[1].Status}.",
failures);
Expect(items[2].Status == StepsItemStatus.Error && !GetNonPublicProperty<bool>(items[2], "IsFinished"),
$"New current step should inherit CurrentStepStatus Error/not finished, actual {items[2].Status}/{GetNonPublicProperty<bool>(items[2], "IsFinished")}.",
failures);
}
private static void VerifyStepsCurrentContentLifecycle(ICollection<string> failures)
{
var steps = new Steps
{
CurrentStep = 0
};
steps.Items.Add(new StepsItem { Header = "First", Content = "First content" });
steps.Items.Add(new StepsItem { Header = "Second", Content = "Second content" });
using (var realized = RealizeControl(steps))
{
Expect(Equals(steps.CurrentContent, "First content"),
$"Steps.CurrentContent should track the selected item content, actual {steps.CurrentContent ?? "<null>"}.",
failures);
steps.CurrentStep = 1;
RefreshLayout(realized.Window);
Expect(Equals(steps.CurrentContent, "Second content"),
$"Steps.CurrentContent should update after CurrentStep changes, actual {steps.CurrentContent ?? "<null>"}.",
failures);
}
Expect(GetPrivateField(steps, "AtomUI.Desktop.Controls.Steps", "_currentItemSubscriptions") is null,
"Steps should dispose current-item content subscriptions when detached from the visual tree.",
Expect(items[2].Status == StepsStatus.Error,
$"New current step should inherit Steps.Status Error, actual {items[2].Status}.",
failures);
}
private static void VerifyStepsIndicatorTemplateShape(ICollection<string> failures)
{
var steps = CreateSteps(currentStep: 1);
var steps = CreateSteps(current: 1);
using var realized = RealizeControl(steps);
Expect(CountNamedVisuals(steps, "FinishedMark") == 1,
@@ -164,14 +139,14 @@ internal static partial class Program
Expect(CountNamedVisuals(steps, "ErrorMark") == 0,
$"Steps default indicator should not materialize ErrorMark when no item is in error, actual {CountNamedVisuals(steps, "ErrorMark")}.",
failures);
Expect(CountNamedVisuals(steps, "PositionText") == 2,
$"Steps default indicator should keep PositionText only for wait/process items, actual {CountNamedVisuals(steps, "PositionText")}.",
Expect(CountNamedVisuals(steps, "StepNumberText") >= 2,
$"Steps default indicator should keep StepNumberText for wait/process items, actual {CountNamedVisuals(steps, "StepNumberText")}.",
failures);
Expect(CountNamedVisuals(steps, "CustomIconPresenter") == 0,
$"Steps without custom icons should not materialize CustomIconPresenter, actual {CountNamedVisuals(steps, "CustomIconPresenter")}.",
failures);
steps.CurrentStepStatus = StepsItemStatus.Error;
steps.Status = StepsStatus.Error;
RefreshLayout(realized.Window);
Expect(CountNamedVisuals(steps, "ErrorMark") == 1,
$"Steps error status should materialize one ErrorMark, actual {CountNamedVisuals(steps, "ErrorMark")}.",
@@ -193,7 +168,7 @@ internal static partial class Program
{
return steps.GetSelfAndVisualDescendants()
.OfType<StepsItem>()
.OrderBy(item => GetNonPublicProperty<int>(item, "Position"))
.OrderBy(item => GetNonPublicProperty<int>(item, "StepNumber"))
.ToList();
}
@@ -83,8 +83,11 @@ internal static partial class Program
IsMaximizeCaptionButtonVisible = true,
IsMinimizeCaptionButtonVisible = true,
IsWindowActive = true,
IsWindowFullScreen = isFullScreen,
IsWindowMaximized = isMaximized
HostWindowState = isFullScreen
? WindowState.FullScreen
: isMaximized
? WindowState.Maximized
: WindowState.Normal
};
group.SetValue(CaptionButtonGroup.OsTypeProperty, osType);
return group;
@@ -1,8 +1,8 @@
using System.Collections;
using AtomUI.Controls;
using AtomUI.Desktop.Controls;
using Avalonia.Controls;
using Avalonia.VisualTree;
using AtomWindow = AtomUI.Desktop.Controls.Window;
namespace AtomUI.Performance;
@@ -13,7 +13,6 @@ internal static partial class Program
var failures = new List<string>();
VerifyCaptionButtonGroupVisibilityStates(failures);
VerifyCaptionButtonSingleIconPresenter(failures);
VerifyCaptionButtonGroupDetachClearsTemplateHandlers(failures);
if (failures.Count == 0)
{
@@ -36,12 +35,11 @@ internal static partial class Program
using var realized = RealizeControl(group);
ExpectCaptionButtonVisibility(group, true, true, true, true, true, "default Windows caption group", failures);
group.IsWindowMaximized = true;
group.HostWindowState = WindowState.Maximized;
RefreshLayout(realized.Window);
ExpectCaptionButtonVisibility(group, false, true, true, true, true, "maximized Windows caption group", failures);
group.IsWindowMaximized = false;
group.IsWindowFullScreen = true;
group.HostWindowState = WindowState.FullScreen;
RefreshLayout(realized.Window);
ExpectCaptionButtonVisibility(group, true, true, false, false, true, "fullscreen Windows caption group", failures);
@@ -49,35 +47,15 @@ internal static partial class Program
RefreshLayout(realized.Window);
ExpectCaptionButtonVisibility(group, false, true, false, false, true, "fullscreen button disabled", failures);
group.IsWindowFullScreen = false;
group.IsMaximizeCaptionButtonVisible = false;
group.IsMinimizeCaptionButtonVisible = false;
group.IsPinCaptionButtonVisible = false;
group.IsCloseCaptionButtonVisible = false;
group.HostWindowState = WindowState.Normal;
group.IsMaximizeCaptionButtonVisible = false;
group.IsMinimizeCaptionButtonVisible = false;
group.IsPinCaptionButtonVisible = false;
group.IsCloseCaptionButtonVisible = false;
RefreshLayout(realized.Window);
ExpectCaptionButtonVisibility(group, false, false, false, false, false, "all optional caption buttons disabled", failures);
}
private static void VerifyCaptionButtonGroupDetachClearsTemplateHandlers(ICollection<string> failures)
{
var host = new AtomWindow();
var group = CreateCaptionButtonGroup(OsType.Windows);
group.Attach(host);
using var _ = RealizeControl(group);
Expect(GetTemplateHandlerCount(group) > 0,
"Attached CaptionButtonGroup should register template button handlers after template apply.",
failures);
group.Detach();
Expect(GetTemplateHandlerCount(group) == 0,
"CaptionButtonGroup.Detach should clear template button handlers.",
failures);
Expect(GetPrivateField(group, "AtomUI.Desktop.Controls.CaptionButtonGroup", "_disposables") == null,
"CaptionButtonGroup.Detach should dispose host bindings.",
failures);
}
private static void VerifyCaptionButtonSingleIconPresenter(ICollection<string> failures)
{
VerifyCaptionButtonSingleIconPresenter(OsType.Windows, "Windows caption group", failures);
@@ -185,11 +163,4 @@ internal static partial class Program
failures);
}
}
private static int GetTemplateHandlerCount(CaptionButtonGroup group)
{
return GetPrivateField(group, "AtomUI.Desktop.Controls.CaptionButtonGroup", "_disposeActions") is ICollection actions
? actions.Count
: 0;
}
}
@@ -23,6 +23,21 @@ internal static partial class Program
.FirstOrDefault(control => control.Name == name);
}
private static bool WaitUntil(Func<bool> condition, string description, int timeoutMs = 5000)
{
var deadline = Environment.TickCount64 + timeoutMs;
while (Environment.TickCount64 < deadline)
{
Avalonia.Threading.Dispatcher.UIThread.RunJobs();
if (condition())
{
return true;
}
Thread.Sleep(10);
}
return condition();
}
private static bool IsZeroCornerRadius(CornerRadius cornerRadius)
{
return cornerRadius.TopLeft == 0 &&