mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-17 05:31:01 +08:00
fix(Performance): 对齐 semantic 分支控件 API 并修复 headless 关停丢任务
This commit is contained in:
@@ -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)
|
||||
|
||||
+5
-1
@@ -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)
|
||||
{
|
||||
|
||||
+83
-4
@@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+83
-96
@@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+13
-10
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+41
-20
@@ -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)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+55
-16
@@ -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);
|
||||
|
||||
+2
-158
@@ -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 },
|
||||
|
||||
+28
-7
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -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;
|
||||
|
||||
+8
-37
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user