From 915deff4eb8b809fba02eb9771ae49cfbf214fd7 Mon Sep 17 00:00:00 2001 From: chinboy Date: Fri, 29 May 2026 17:40:13 +0800 Subject: [PATCH] perf(controls): reduce allocation and state sync overhead --- docs/performances/Form/README.md | 17 + docs/performances/Primitives/README.md | 183 +++++++++++ docs/performances/README.md | 118 +++---- docs/performances/Tag/README.md | 16 +- docs/performances/TextBlock/README.md | 13 + docs/performances/TimePicker/README.md | 14 + docs/performances/Tour/README.md | 14 + docs/performances/Transfer/README.md | 9 +- docs/performances/TreeView/README.md | 41 +++ docs/performances/Upload/README.md | 15 +- .../desktop-controls-optimization-roadmap.md | 72 ++--- .../Utils/BorderRenderHelper.cs | 7 +- .../Utils/DateTimeUtils.cs | 14 +- .../WaveSpirit/AbstractWavePainter.cs | 6 +- .../WaveSpirit/CircleWavePainter.cs | 4 +- .../WaveSpirit/PillWavePainter.cs | 4 +- .../WaveSpirit/RoundRectWavePainter.cs | 4 +- .../Badge/AbstractRibbonBadge.cs | 6 +- src/AtomUI.Controls/Badge/BadgeColorUtils.cs | 6 +- .../FloatButton/FloatButtonSeparatorLayer.cs | 13 +- src/AtomUI.Controls/Grid/GridColSize.cs | 27 +- .../AbstractOptionButtonGroup.cs | 10 +- .../AbstractArrowDecoratedBox.cs | 39 ++- .../Primitives/Motions/MotionGhostControl.cs | 34 +- .../Primitives/TreeNodePath.cs | 54 +++- .../Primitives/WaveSpiritDecorator.cs | 7 +- .../ProgressBar/AbstractCircleProgress.cs | 4 +- .../AbstractGeneralCircleProgress.cs | 60 ++-- .../AbstractGeneralDashboardProgress.cs | 66 ++-- .../ProgressBar/AbstractGeneralProgressBar.cs | 45 ++- .../ProgressBar/AbstractLineProgress.cs | 4 +- src/AtomUI.Controls/Tag/AbstractTag.cs | 16 +- src/AtomUI.Core/Media/PenUtils.cs | 60 +++- .../AutoComplete/AbstractAutoComplete.cs | 4 +- .../ButtonSpinner/ButtonSpinner.cs | 19 +- .../ButtonSpinner/ButtonSpinnerHandle.cs | 14 +- .../Card/CardActionPanel.cs | 19 +- .../Carousel/Carousel.cs | 4 +- .../Cascader/Cascader.cs | 90 ++++-- .../Cascader/CascaderView.Check.cs | 28 +- .../Cascader/CascaderViewLevelList.cs | 2 +- .../Cascader/CascaderViewPanel.cs | 10 +- .../Descriptions/Descriptions.cs | 19 +- .../Dialog/ButtonBox/DialogButtonBox.cs | 60 ++-- .../FloatButton/FloatButtonGroup.cs | 10 +- .../FloatButton/FloatButtonItemsControl.cs | 48 ++- src/AtomUI.Desktop.Controls/Form/Form.cs | 8 +- src/AtomUI.Desktop.Controls/Form/FormItem.cs | 8 +- .../GroupBox/GroupBox.cs | 4 +- .../ImagePreviewer/AbstractImagePreviewer.cs | 6 +- src/AtomUI.Desktop.Controls/Input/TextArea.cs | 21 +- .../Input/TextAreaDecoratedBox.cs | 14 +- src/AtomUI.Desktop.Controls/Input/TextBox.cs | 22 +- .../ListBox/ListBox.cs | 4 +- .../ListView/ListView.Virtualizing.cs | 4 +- .../Mentions/Mentions.cs | 16 +- .../Menu/MenuSeparator.cs | 10 +- .../Message/WindowMessageManager.cs | 28 +- .../Header/HorizontalNavMenuItemHeader.cs | 18 +- .../NavMenu/NavMenu.cs | 4 +- ...NotificationProgressBarVisibleConverter.cs | 7 +- .../WindowNotificationManager.cs | 5 +- .../NumericUpDown/NumericUpDown.cs | 31 +- .../AddOnDecoratedBox/AddOnDecoratedBox.cs | 93 ++++-- .../Primitives/CandidateList/CandidateList.cs | 105 +++++-- .../InfoPickerInput/InfoPickerInput.cs | 90 ++++-- .../InfoPickerInput/RangeInfoPickerInput.cs | 186 ++++++----- .../Select/AbstractSelect.cs | 36 ++- src/AtomUI.Desktop.Controls/Select/Select.cs | 72 ++++- .../Slider/SliderThumb.cs | 13 +- .../Slider/SliderTrack.cs | 29 +- .../Space/CompactSpace.cs | 71 ++++- .../Space/CompactSpaceAddOn.cs | 19 +- .../Space/CompactSpaceItem.cs | 44 ++- .../Splitter/SplitterPanel.cs | 38 ++- .../TabControl/BaseTabScrollViewer.cs | 56 ++-- .../TextBlock/HighlightableTextBlock.cs | 24 +- .../TimeView/DateTimePickerPanel.cs | 18 +- src/AtomUI.Desktop.Controls/Tour/Tour.cs | 39 ++- .../Transfer/AbstractTransfer.cs | 79 ++++- .../Transfer/ListTransfer.cs | 6 +- .../Transfer/TransferListView.cs | 71 ++++- .../Transfer/TransferTreeView.cs | 63 +++- .../Transfer/TreeTransfer.cs | 12 +- .../TreeView/TreeView.Filter.cs | 13 +- .../TreeView/TreeView.cs | 10 +- .../TreeView/TreeViewItemHeader.cs | 36 ++- .../UploadPicturePreviewContent.cs | 6 +- .../UploadPictureShapePreviewContent.cs | 6 +- src/AtomUI.Desktop.Controls/Upload/Upload.cs | 5 +- .../Upload/UploadDefaultDropArea.cs | 4 +- .../AddOnStateVerification.cs | 292 ++++++++++++++++++ .../DatePicker/DatePickerStateVerification.cs | 162 ++++++++++ .../ListBox/ListBoxStateVerification.cs | 134 ++++++++ 94 files changed, 2614 insertions(+), 757 deletions(-) create mode 100644 docs/performances/Form/README.md diff --git a/docs/performances/Form/README.md b/docs/performances/Form/README.md new file mode 100644 index 000000000..5ff9c8368 --- /dev/null +++ b/docs/performances/Form/README.md @@ -0,0 +1,17 @@ +# Form 性能优化 + +> 状态:已完成低风险结构与生命周期优化;本轮追加验证路径 structural-only 容量收敛。 + +## 追加结构优化:验证集合容量 + +`Form.ValidateAsync()` 和 `FormItem.ValidateValueAsync()` 的任务、结果和消息集合按 `Items.Count` / `Validators.Count` 预分配。验证顺序、并发/串行策略、错误/警告消息内容均保持不变。 + +| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 | +| --- | ---: | ---: | --- | ---: | --- | +| Form validate tasks list / validate | dynamic growth | exact upper-bound capacity | structural | 分配更紧 | 按 `Items.Count` 预分配 | +| Form validate results list / validate | dynamic growth | exact upper-bound capacity | structural | 分配更紧 | 按 `Items.Count` 预分配 | +| Form validate messages list / validate | dynamic growth | exact upper-bound capacity | structural | 分配更紧 | 按 `Items.Count` 预分配 | +| FormItem validator task dictionary / parallel validate | dynamic growth | exact upper-bound capacity | structural | 分配更紧 | 按 `Validators.Count` 预分配 | +| FormItem warning/error message lists / validate | dynamic growth | exact upper-bound capacity | structural | 分配更紧 | 按 `Validators.Count` 预分配 | + +说明:这是交互验证路径的结构性收益;不声明页面加载 timing 提升。 diff --git a/docs/performances/Primitives/README.md b/docs/performances/Primitives/README.md index 3cbd749cd..28080dd21 100644 --- a/docs/performances/Primitives/README.md +++ b/docs/performances/Primitives/README.md @@ -7,6 +7,14 @@ - `InfoPickerInput` 体系里的 `PickerClearUpButton`。它被 `DatePicker`、`RangeDatePicker`、`TimePicker`、`RangeTimePicker` 等 picker 输入控件复用。 - `AddOnDecoratedBox` 的 `PART_ContentFrame` pointer 状态路径。它被 LineEdit / Select / TreeSelect / Cascader / DatePicker / TimePicker / ButtonSpinner 等输入类控件复用。 - `CandidateList` 的键盘候选高亮路径。它被 AutoComplete / Mentions 等 popup 候选列表复用。 +- `RangeInfoPickerInput` 的 range indicator 布局写入路径。它被 RangeDatePicker / RangeTimePicker 复用。 +- `InfoPickerInput` 的通用 open / close 写入路径。它被 DatePicker / TimePicker 及 range picker 共享。 +- `TreeNodePath` 的路径派生操作。它被 Cascader / NavMenu / TreeView 等树形路径控件复用。 +- `MotionGhostControl` 的 shadow mask 几何更新路径。它被带 motion ghost 的浮层 / motion 控件复用。 +- `AbstractArrowDecoratedBox` 的 arrow shell 布局状态路径。它被 Popup / Flyout / DatePicker / TimePicker 等带箭头浮层复用。 +- `WaveSpiritDecorator` 的 wave painter 配置路径。它被 Button / Switch / Radio / CheckBox 等点击波纹控件复用。 +- `CompactSpaceItem` / `CompactSpaceAddOn` 的 compact-space 状态通知路径,以及 `TextBox` / `TextArea` / `ButtonSpinner` / `NumericUpDown` / `AbstractSelect` 的 compact-space / form feedback / validation 状态同步路径。 +- `PenUtils` / `BorderRenderHelper` 的 render-loop pen 复用路径。它被 DashedBorder / OptionButton / TreeView / ButtonSpinner / RadioIndicator 等自绘边框控件复用。 ## InfoPickerInput Root Cause @@ -21,12 +29,53 @@ - `PART_ContentFrame` 是每个 AddOnDecoratedBox 实例都存在的静态模板节点;LineEdit / Select / TreeSelect / Cascader / DatePicker / TimePicker 这类页面会按实例数线性放大这 4 条 handler。 - Avalonia 12 里 `PointerEntered` / `PointerExited` 是 Direct routed event,`PointerPressed` / `PointerReleased` 是 Tunnel + Bubble routed event;事件 CLR add/remove 会落入本地 `_eventHandlers`。参考 `.referenceprojects/Avalonia/src/Avalonia.Base/Input/InputElement.cs:144-177`、`:355-392`。 - Select / TreeSelect / Cascader 的 `PART_ContentRightAddOnPresenter.ContentTemplate` 还错误绑定到了 `ContentLeftAddOnTemplate`,导致 right addon template 不生效。 +- `AddOnDecoratedBox` 的 addon status foreground / icon brush、inner corner radius、inner border thickness 和 content-frame pointer state 路径原先会在结果未变化时仍调用写入 / clear。 ## CandidateList Root Cause - `CandidateList.TrySetCandidateItemSelected(index)` 原先每次候选高亮移动都会从 `0..ItemCount-1` 扫描全部容器,并对每个已实现 `CandidateListItem` 重写 `IsCandidateSelected`。 - 键盘上下导航只需要清理旧候选项并设置新候选项;全量扫描在 AutoComplete / Mentions popup 打开后会按按键次数线性放大。 - `CandidateVirtualizingStackPanel.ScrollCandidateItemIntoView(index)` 已经能把目标项滚入视图,本轮只复用目标容器,不改变虚拟化、过滤、提交 / 取消语义。 +- `CandidateList` 的 max-count enable 状态、candidate selected 状态、empty indicator 和 commit/reset 路径原先会在结果未变化时重复写入。 +- 空 `CandidateList` 的上下键导航原先会尝试写入无效 candidate index;commit/cancel 后的 `ClearState()` 和虚拟化清理也可能重复写 null / -1 / ClearValue。 + +## TreeNodePath Root Cause + +- `TreeNodePath.Append(string)`、`Append(TreeNodePath)`、`GetParent()` 和 `WithSegment()` 原先先创建新 segments 数组,再调用公开构造函数二次校验并复制一份数组。 +- 这些派生操作的来源路径已经是已校验的 `TreeNodePath`,新增 segment 也在方法入口单独校验;因此可以使用私有 trusted 构造,保留公开构造函数的外部输入校验语义,同时避免二次数组分配。 +- 空路径 append 已有路径、单段路径取 parent、同值 `WithSegment()` 原先仍会创建新数组 / 新 wrapper;这些场景都可以复用已有不可变对象。 +- `TreeNodePath.StartsWith(path)` 对同一不可变实例原先仍按 segment 数量逐项比较。 + +## MotionGhostControl Root Cause + +- `MotionGhostControl.HandleMaskCornerRadiusOrSizeChanged()` 原先在 `MaskCornerRadius` 或 `MaskSize` 任一变化时,都会对每个 shadow renderer 重写 `CornerRadius`、`Width`、`Height` 三项。 +- content size callback 原先也会直接重写 mask center frame 的 `Width` / `Height`。 +- 本轮只跳过同值写入,不改变 motion ghost 的创建、shadow renderer 数量、bitmap capture 或动画时序。 + +## ArrowDecoratedBox / WaveSpirit Root Cause + +- `AbstractArrowDecoratedBox` 在 template apply、`ArrowPosition` 变化和重复 arrange 后,会重复写 `ArrowDirection`、`ArrowIndicatorBounds`、`ArrowIndicatorLayoutBounds`;外部 shadow mask provider 重复设置相同 arrow opacity 时也会重复写 `ArrowOpacity`。 +- `WaveSpiritDecorator.ConfigureWavePainter()` 原先每次配置 painter 都创建两个新的 `CubicEaseOut`,但实际曲线固定不变。 +- wave painter 的 size / opacity animation 兜底路径原先在没有配置 easing 时会 `new LinearEasing()`;可以复用静态默认实例。 +- `CompactSpaceItem.MeasureOverride()` 原先每次 measure 都会为同一 offset 重建 `TranslateTransform`;从 Middle / Last 回到单项位置时还可能残留旧 offset transform。 +- `CompactSpaceItem` / `CompactSpaceAddOn` / `TextBox` / `ButtonSpinner` / `NumericUpDown` 原先在 repeated compact-space position / orientation 通知下继续写 `IsUsedInCompactSpace`、`CompactSpaceItemPosition`、`CompactSpaceOrientation`。 +- `TextBox` / `TextArea` / `AbstractSelect` 原先在 repeated same form feedback control 通知下继续写 `FormFeedback`,可能触发 feedback subscription refresh。 +- `TextArea` / `NumericUpDown` / `AbstractSelect` 原先在 repeated validation status 通知下继续写 `Status`。 +- `PenUtils.TryModifyOrCreate()` 原先在 immutable brush 路径下,即使 brush / thickness / dash / cap / join / miter 全部未变化,也会重新创建 `ImmutablePen`;带 dash 时还会重新创建 `ImmutableDashStyle`。 +- `BorderRenderHelper` 原先的 geometry cache key 未记录 dashed enabled 状态;同一个 helper 从非 dashed 切到 dashed 或反向切换时,可能沿用旧的复杂 / 简单渲染模式。 + +## RangeInfoPickerInput Root Cause + +- `InfoPickerInput.OnPointerReleased()`、`ClosePickerFlyout()`、window deactivated 和 owned presenter cleanup 都可能重复写 `IsPickerOpen`,即使当前开关状态已经相同。 +- `RangeInfoPickerInput.MeasureOverride()` 每次 layout 都会写 `PickerIndicatorOffsetY`,即使 indicator Y offset 没变也会进入 Avalonia styled property 写入路径。 +- `RangeInfoPickerInput.ArrangeOverride()` 每次 layout 都会写 `Canvas.Left` / `Canvas.Top` attached properties,即使 range indicator 位置没变也会进入 attached property 写入路径。 +- `RangeInfoPickerInput.OnPointerReleased()` 在判断 start / end 输入框命中时最多会调用两次 `args.GetPosition(this)`;同一次 pointer release 的相对坐标可以取一次后复用。 +- 带 `ContentLeftAddOn` 的 range 输入点击路径中,start / end hit-test 原先会各自换算一次 left addon 坐标;同一次 pointer release 可共用同一个 left boundary。 +- 点击已经打开的同一 range 输入端时,旧路径仍会重复写 `RangeActivatedPart` 和 `IsPickerOpen=true`;关闭已关闭 popup 时也会重复写 `IsPickerOpen=false`。 +- `RangeActivatedPart` 切换到 Start 时,`PickerPlacement` 默认已经是 `BottomEdgeAlignedLeft`,旧路径仍会重复写入;重复 setup 同一个 popup target 也会重复写 `PlacementTarget`。 +- `InfoPickerInput.ConfigureIsClearButtonVisible()`、`ConfigureShowArrowEffective()`、`ConfigureArrowPosition()` 和 `NotifyValidateStatus()` 原先会在结果未变化时重复写 styled property。 +- `InfoPickerInput` 的 compact-space position / orientation 通知和 form feedback 控件同步原先会在同一值重复下发时继续写 styled property,并触发模板绑定 / feedback 订阅链路。 +- RangeDatePicker / RangeTimePicker 的 range indicator 是静态模板节点,本轮只跳过 unchanged 写入,不改变模板、动画、popup 或 active range 语义。 ## Changes @@ -38,11 +87,38 @@ - 移除 `AddOnDecoratedBox.OnApplyTemplate()` 对 `ContentFrame` 的 4 条本地 pointer handler 注册 / 退订。 - 修复 Select / TreeSelect / Cascader 的 right addon presenter template binding:`ContentLeftAddOnTemplate` -> `ContentRightAddOnTemplate`。 - `CandidateList.TrySetCandidateItemSelected(index)` 改为只清旧 index、设置新 index,不再每次移动扫描全部 `ItemCount`。 +- `CandidateList` 的 container `IsEnabled`、`IsCandidateSelected`、`CandidateSelectedItem`、`CandidateSelectedIndex`、`SelectedItem` 和 `IsEffectiveEmptyVisible` 写入增加 unchanged guard。 +- `CandidateList` 空列表上下键导航直接返回;`ClearState()`、detach cleanup 和 virtualized container clear 路径增加 unchanged guard。 +- `AddOnDecoratedBox` 的 addon foreground / icon brush、inner radius / thickness 和 content-frame hover / pressed 状态写入增加 unchanged guard。 +- `TreeNodePath` 新增私有 trusted 构造;`Append` / `GetParent` / `WithSegment` 改为只分配目标 segments 数组一次。 +- `TreeNodePath.Empty.Append(path)`、单段 `GetParent()` 和同值 `WithSegment()` 改为直接返回已有不可变对象。 +- `TreeNodePath.StartsWith()` 对同一实例和 empty prefix 增加 fast path。 +- `MotionGhostControl` 的 shadow renderer radius / width / height 和 mask center frame width / height 写入增加 unchanged guard。 +- `AbstractArrowDecoratedBox` 的 arrow direction、indicator bounds、indicator layout bounds 和 arrow opacity 写入增加 unchanged guard。 +- `WaveSpiritDecorator` 复用静态 `CubicEaseOut` 作为默认 wave painter easing,避免每次配置创建 2 个 easing 对象。 +- `AbstractWavePainter` 复用静态 `LinearEasing` 作为 animation fallback easing,避免兜底路径重复创建 easing 对象。 +- `CompactSpaceItem` / `CompactSpaceAddOn` / `TextBox` / `ButtonSpinner` / `NumericUpDown` 的 compact-space position / orientation 写入增加 unchanged guard。 +- `CompactSpaceItem` 的 offset transform 改为同 offset 复用,进入单项 / first 状态时清理旧 transform。 +- `TextBox` / `TextArea` / `AbstractSelect` 的 form feedback control 写入增加 unchanged guard。 +- `TextArea` / `NumericUpDown` / `AbstractSelect` 的 validation status 写入增加 unchanged guard。 +- `PenUtils.TryModifyOrCreate()` 在已有 `IPen` 与目标 brush / thickness / dash / cap / join / miter 完全一致时直接返回,不再创建新 pen / dash style。 +- `BorderRenderHelper` 将 dashed enabled 状态加入 cache key,dash 状态切换会刷新 geometry rendering mode。 - `--verify-datepicker-states` 改为验证静态 popup shell / accessory slot:模板 shell 保留,但 presenter/content 在关闭态为空;detach 后 presenter/content 释放。 - `--verify-datepicker-states` 新增 clear button lifecycle 覆盖:隐藏 direct click 惰性、本地 Click handler 为 0、clear mode 下仍能清空 `SelectedDateTime`。 - `--verify-addon-states` 新增 ContentFrame pointer lifecycle 覆盖:本地 pointer routed handler 为 0,直接事件仍正确更新 hover / pressed 状态;同时覆盖 Select / TreeSelect / Cascader right-addon template。 - `--verify-addon-states` 覆盖 `PART_ContentFrame` 的 `StyleKey == Border` 和 token padding 非零,避免 DatePicker / TimePicker / LineEdit 输入框内边距回归。 +- `--verify-addon-states` 新增 CompactSpaceItem / CompactSpaceAddOn / TextBox / TextArea / ButtonSpinner / NumericUpDown / AbstractSelect repeated state notification 覆盖,以及 CompactSpaceItem offset transform 复用和 stale transform 清理覆盖。 +- `--verify-addon-states` 新增 `PenUtils` repeated immutable / dashed pen 复用覆盖。 - `--verify-listbox-states` 新增 CandidateList 候选高亮移动覆盖:index 0 -> 1 -> -1 时只保留当前候选态;同时修正 ListBox 静态 slot 校验口径。 +- `RangeInfoPickerInput` 的 range indicator 写入增加 unchanged guard:`PickerIndicatorOffsetY`、`Canvas.Left`、`Canvas.Top`、`RangePickerIndicatorOpacity`、indicator `Width`、`PickerIndicatorOffsetX` 只在值变化时写入。 +- `RangeInfoPickerInput.OnPointerReleased()` 将 pointer position 改为单次读取后复用,start / end hit-test 语义不变。 +- `RangeInfoPickerInput` 的 start / end hit-test 合并为一个 helper,`ContentLeftAddOn` boundary 单次计算后复用。 +- `RangeInfoPickerInput` 的 `IsPickerOpen`、`RangeActivatedPart`、`PickerPlacement`、popup `PlacementTarget` 写入增加 unchanged guard。 +- `InfoPickerInput` 新增共享 `SetPickerOpenIfChanged()`,单输入 picker 和 range picker 的 open / close 写入都先判断值是否变化。 +- `InfoPickerInput` 的 clear button 可见性、arrow effective、arrow position 和 validation status 写入增加 unchanged guard。 +- `InfoPickerInput` 的 compact-space position、compact-space orientation 和 form feedback 控件写入增加 unchanged guard。 +- `InfoPickerInput.IsPointerInInfoInputBox()` 直接返回 bounds 命中结果,去掉多余分支。 +- `--verify-datepicker-states` 新增 RangeDatePicker indicator 重复 layout 稳定性覆盖,以及 DatePicker repeated compact-space / form-feedback 通知不重写状态覆盖。 ## Benefit @@ -54,9 +130,114 @@ | Hidden clear direct-click side effects / click | 1 clear request | 0 clear requests | `(1 - 0) / 1` | 100.00% | 正确性修复;非 clear mode 不再清值 | | Local pointer handlers / `AddOnDecoratedBox` content frame | 4 / instance | 0 / instance | `(4 - 0) / 4` | 100.00% | 有效;静态 template part 保留,pointer 状态改由 `Border` 子类 override | | Local pointer handlers / `CompactSpace.LineEdit` root | 12 / root | 0 / root | `(12 - 0) / 12` | 100.00% | 有效;3 个 AddOnDecoratedBox × 4 条 handler | +| AddOnDecoratedBox addon foreground writes / repeated same status brush | 4 writes | 0 writes | `(4 - 0) / 4` | 100.00% | 结构收益;4 个 addon presenter foreground 已是同一 brush 时不写 | +| AddOnDecoratedBox icon brush writes / repeated same status brush per icon | 3 writes | 0 writes | `(3 - 0) / 3` | 100.00% | 结构收益;`FillBrush` / `StrokeBrush` / `Foreground` 已是同一 brush 时不写 | +| AddOnDecoratedBox inner geometry writes / repeated layout update | 2 writes | 0 writes | `(2 - 0) / 2` | 100.00% | 结构收益;inner radius / thickness 计算结果未变时不写 | +| AddOnDecoratedBox pointer state writes / repeated content-frame event | 2 writes | 0 writes | `(2 - 0) / 2` | 100.00% | 结构收益;hover / pressed 状态未变时不写 | | Wrong right-addon template bindings / Select family | 3 controls | 0 controls | `(3 - 0) / 3` | 100.00% | 正确性修复;Select / TreeSelect / Cascader 均已验证 | | Lost input content padding after ContentFrame subclassing | 1 shared path | 0 shared paths | `(1 - 0) / 1` | 100.00% | 正确性修复;`PART_ContentFrame` 继续命中 `Border#PART_ContentFrame` padding selector | | CandidateList candidate container checks / keyboard move (`Items20`) | 20 checks | <= 2 checks | `(20 - 2) / 20` | 90.00% | 结构收益;AutoComplete / Mentions 候选 popup 键盘上下移动少扫容器,不声明页面加载 speedup | +| CandidateList max-count enable writes / repeated same enabled state (`Items20`) | 20 writes | 0 writes | `(20 - 0) / 20` | 100.00% | 结构收益;容器 enabled 状态未变时不写 | +| CandidateList candidate selected writes / repeated same candidate | 2 writes | 0 writes | `(2 - 0) / 2` | 100.00% | 结构收益;当前 item 和 index 未变时不写 | +| CandidateList empty indicator writes / repeated same filter-empty state | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;empty indicator 结果未变时不写 | +| CandidateList invalid candidate writes / Down key on empty list | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益 + 正确性收敛;空列表不再尝试选择 index 0 | +| CandidateList clear-state writes / already cleared commit-cancel cleanup | 3 writes | 0 writes | `(3 - 0) / 3` | 100.00% | 结构收益;`SelectedItems` / `SelectedItem` / `SelectedIndex` 未变时不写 | +| CandidateList virtualized selected clear / already default container | 1 clear | 0 clears | `(1 - 0) / 1` | 100.00% | 结构收益;未设置 candidate selected 时不 `ClearValue` | +| TreeNodePath allocations / `Append(string)` | 2 arrays | 1 array | `(2 - 1) / 2` | 50.00% | 结构收益;新增 segment 已单独校验,避免公开构造二次复制 | +| TreeNodePath allocations / `Append(TreeNodePath)` | 2 arrays | 1 array | `(2 - 1) / 2` | 50.00% | 结构收益;两个来源路径均已校验,避免二次复制 | +| TreeNodePath allocations / `GetParent()` non-empty path | 2 arrays | 1 array | `(2 - 1) / 2` | 50.00% | 结构收益;去掉 range slice + 构造函数复制的双数组 | +| TreeNodePath allocations / `WithSegment(index, value)` | 2 arrays | 1 array | `(2 - 1) / 2` | 50.00% | 结构收益;替换值入口校验后复用 trusted 构造 | +| TreeNodePath allocations / `TreeNodePath.Empty.Append(path)` | 1 array | 0 arrays | `(1 - 0) / 1` | 100.00% | 结构收益;空路径 append 直接复用已有不可变 path | +| TreeNodePath allocations / single-segment `GetParent()` | 1 array | 0 arrays | `(1 - 0) / 1` | 100.00% | 结构收益;直接复用 `TreeNodePath.Empty` | +| TreeNodePath allocations / same-value `WithSegment(index, value)` | 1 array | 0 arrays | `(1 - 0) / 1` | 100.00% | 结构收益;segment 未变时返回当前不可变 path | +| TreeNodePath segment comparisons / `StartsWith(sameInstance)` length N | N comparisons | 0 comparisons | `(N - 0) / N` | 100.00% | 结构收益;同一不可变 path 直接返回 true | +| MotionGhostControl shadow renderer writes / `MaskCornerRadius` only changes, per shadow | 3 writes | 1 write | `(3 - 1) / 3` | 66.67% | 结构收益;width / height 未变时不写 | +| MotionGhostControl shadow renderer writes / `MaskSize` only changes, per shadow | 3 writes | 2 writes | `(3 - 2) / 3` | 33.33% | 结构收益;corner radius 未变时不写 | +| MotionGhostControl mask center size writes / repeated same size callback | 2 writes | 0 writes | `(2 - 0) / 2` | 100.00% | 结构收益;center frame width / height 未变时不写 | +| ArrowDecoratedBox arrow direction writes / same placement direction | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;`ArrowPosition` 映射出的方向未变时不写 | +| ArrowDecoratedBox indicator bounds writes / repeated arrange | 2 writes | 0 writes | `(2 - 0) / 2` | 100.00% | 结构收益;indicator bounds / layout bounds 未变时不写 | +| ArrowDecoratedBox arrow opacity writes / repeated same opacity | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;shadow mask provider 重复设置同值 opacity 时不写 | +| WaveSpiritDecorator default easing allocations / painter configure | 2 objects | 0 objects | `(2 - 0) / 2` | 100.00% | 结构收益;复用静态 `CubicEaseOut`,曲线不变 | +| WavePainter fallback easing allocations / animation build without configured easing | 1 object | 0 objects | `(1 - 0) / 1` | 100.00% | 结构收益;复用静态 `LinearEasing`,兜底曲线不变 | +| CompactSpaceItem compact-space usage writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;wrapper compact 状态未变时不写 | +| CompactSpaceItem compact-space position writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;wrapper position 未变时不写 | +| CompactSpaceItem compact-space orientation writes / repeated same orientation notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;wrapper orientation 未变时不写 | +| CompactSpaceItem offset transform allocations / repeated same measure after first | 1 object | 0 objects | `(1 - 0) / 1` | 100.00% | 结构收益;同 offset 复用现有 `TranslateTransform` | +| CompactSpaceItem stale offset transforms / become single item after middle/last | 1 stale transform | 0 stale transforms | `(1 - 0) / 1` | 100.00% | 正确性修复;单项 / first 状态不保留旧位移 | +| CompactSpaceAddOn compact-space usage writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;addon compact 状态未变时不写 | +| CompactSpaceAddOn compact-space position writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;addon position 未变时不写 | +| CompactSpaceAddOn compact-space orientation writes / repeated same orientation notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;addon orientation 未变时不写 | +| TextBox compact-space usage writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;TextBox compact 状态未变时不写 | +| TextBox compact-space position writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;TextBox position 未变时不写 | +| TextBox compact-space orientation writes / repeated same orientation notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;TextBox orientation 未变时不写 | +| TextBox form feedback writes / repeated same feedback control | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;同一个 feedback control 不重复写 | +| TextArea validation status writes / repeated same form status | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;重复 Error / Warning / Default 不写 `Status` | +| TextArea form feedback writes / repeated same feedback control | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;同一个 feedback control 不重复写 | +| ButtonSpinner compact-space usage writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;ButtonSpinner compact 状态未变时不写 | +| ButtonSpinner compact-space position writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;ButtonSpinner position 未变时不写 | +| ButtonSpinner compact-space orientation writes / repeated same orientation notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;ButtonSpinner orientation 未变时不写 | +| NumericUpDown compact-space usage writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;NumericUpDown compact 状态未变时不写 | +| NumericUpDown compact-space position writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;NumericUpDown position 未变时不写 | +| NumericUpDown compact-space orientation writes / repeated same orientation notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;NumericUpDown orientation 未变时不写 | +| NumericUpDown validation status writes / repeated same form status | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;重复 Error / Warning / Default 不写 `Status` | +| PenUtils allocations / repeated immutable brush pen request | 1 object | 0 objects | `(1 - 0) / 1` | 100.00% | 结构收益;同参数重复 render 复用已有 `ImmutablePen` | +| PenUtils allocations / repeated immutable dashed pen request | 2 objects | 0 objects | `(2 - 0) / 2` | 100.00% | 结构收益;同参数重复 render 不再创建 `ImmutableDashStyle` + `ImmutablePen` | +| OptionButtonGroup separator render pen allocations / repeated frame after first | 1 object | 0 objects | `(1 - 0) / 1` | 100.00% | 结构收益;选项分隔线复用缓存 `IPen`,不声明页面加载 speedup | +| FloatButtonSeparatorLayer separator render pen allocations / repeated frame after first | 1 object | 0 objects | `(1 - 0) / 1` | 100.00% | 结构收益;悬浮按钮分隔线复用缓存 `IPen`,不声明页面加载 speedup | +| ButtonSpinnerHandle divider render pen allocations / repeated frame after first | 2 calls creating pens | 0 new objects | `(2 - 0) / 2` | 100.00% | 结构收益;竖线/横线共用同参数缓存 `IPen`,不声明页面加载 speedup | +| CardActionPanel action separator render pen allocations / 3 actions repeated frame after first | 3 objects | 0 objects | `(3 - 0) / 3` | 100.00% | 结构收益;顶部线和 action 分隔线复用缓存 `IPen`,不声明页面加载 speedup | +| TextAreaDecoratedBox resize indicator render pen allocations / repeated frame after first | 1 object | 0 objects | `(1 - 0) / 1` | 100.00% | 结构收益;resize 指示线复用缓存 `IPen`,不声明页面加载 speedup | +| CascaderViewFrame column separator render pen allocations / repeated frame after first | 1 object | 0 objects | `(1 - 0) / 1` | 100.00% | 结构收益;列分隔线复用缓存 `IPen`,不声明页面加载 speedup | +| SliderThumb circle/outline render pen allocations / repeated frame after first | 2 objects | 0 objects | `(2 - 0) / 2` | 100.00% | 结构收益;thumb 圆和 outline 复用缓存 `IPen`,不声明页面加载 speedup | +| SliderTrack mark border render pen allocations / active + inactive marks repeated frame after first | 2 objects | 0 objects | `(2 - 0) / 2` | 100.00% | 结构收益;active / inactive mark border 各复用一个缓存 `IPen`,不声明页面加载 speedup | +| MenuSeparator line render pen allocations / repeated frame after first | 1 object | 0 objects | `(1 - 0) / 1` | 100.00% | 结构收益;按 render scaling 复用缓存 `IPen`,不声明页面加载 speedup | +| CircleProgress render pen allocations / normal repeated frame after first | 3 objects | 0 objects | `(3 - 0) / 3` | 100.00% | 结构收益;groove / indicator / success 三类 pen 复用,不声明页面加载 speedup | +| CircleProgress render pen allocations / step repeated frame after first | 3 objects | 0 objects | `(3 - 0) / 3` | 100.00% | 结构收益;step groove / indicator / success 三类 pen 复用,不声明页面加载 speedup | +| DashboardProgress render pen allocations / normal repeated frame after first | 3 objects | 0 objects | `(3 - 0) / 3` | 100.00% | 结构收益;dashboard groove / indicator / success 三类 pen 复用,不声明页面加载 speedup | +| DashboardProgress render pen allocations / step repeated frame after first | 3 objects | 0 objects | `(3 - 0) / 3` | 100.00% | 结构收益;dashboard step groove / indicator / success 三类 pen 复用,不声明页面加载 speedup | +| BorderRenderHelper dashed mode cache correctness / dash enabled toggle | stale mode risk | refreshed mode | `1 -> 0 stale cache risk` | 100.00% | 正确性收敛;dash 状态切换纳入 cache key | +| AbstractSelect compact-space usage writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;Select compact 状态未变时不写 | +| AbstractSelect compact-space position writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;Select position 未变时不写 | +| AbstractSelect compact-space orientation writes / repeated same orientation notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;Select orientation 未变时不写 | +| AbstractSelect validation status writes / repeated same form status | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;重复 Error / Warning / Default 不写 `Status` | +| AbstractSelect form feedback writes / repeated same feedback control | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;同一个 feedback control 不重复写 | +| RangeInfoPickerInput unchanged Y offset styled-property writes / repeated measure | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;RangeDatePicker / RangeTimePicker 重复 layout 少进一次 styled property 写入路径 | +| RangeInfoPickerInput unchanged indicator attached-property writes / repeated arrange | 2 writes | 0 writes | `(2 - 0) / 2` | 100.00% | 结构收益;`Canvas.Left` / `Canvas.Top` 位置没变时不再重复写入 | +| RangeInfoPickerInput unchanged active indicator setup writes / repeated setup | 3 writes | 0 writes | `(3 - 0) / 3` | 100.00% | 结构收益;active range 端重复 setup 时不再写 opacity、width、X offset | +| RangeInfoPickerInput unchanged indicator writes / repeated layout + setup | 6 writes | 0 writes | `(6 - 0) / 6` | 100.00% | 结构收益;不声明页面加载 speedup | +| RangeInfoPickerInput pointer position reads / secondary-box click | 2 reads | 1 read | `(2 - 1) / 2` | 50.00% | 结构收益;同一次 pointer release 复用坐标 | +| RangeInfoPickerInput left-addon coordinate translations / secondary-box click with left addon | 2 translations | 1 translation | `(2 - 1) / 2` | 50.00% | 结构收益;start / end hit-test 共用 left boundary | +| RangeInfoPickerInput repeated same-open writes / click already-open same side | 2 writes | 0 writes | `(2 - 0) / 2` | 100.00% | 结构收益;`RangeActivatedPart` 和 `IsPickerOpen` 未变化时不写 | +| RangeInfoPickerInput repeated close writes / click close while already closed | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;`IsPickerOpen=false` 未变化时不写 | +| RangeInfoPickerInput default start placement writes / first start activation | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;默认 `BottomEdgeAlignedLeft` 不重复写 | +| RangeInfoPickerInput repeated popup target writes / same target setup | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;同一 `PlacementTarget` 不重复写 | +| InfoPickerInput repeated open writes / click already-open input | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;DatePicker / TimePicker `IsPickerOpen=true` 未变化时不写 | +| InfoPickerInput repeated close writes / close already-closed picker | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;public close、window deactivated、cleanup 复用同一 guard | +| InfoPickerInput clear-button visibility writes / repeated same hover-text state | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;hover / readonly / text 长度结果未变时不写 | +| InfoPickerInput clear-button visibility writes / clear when already hidden | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;清空后 already hidden 不重复写 | +| InfoPickerInput arrow effective writes / unchanged placement or arrow visibility | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;arrow effective 结果未变时不写 | +| InfoPickerInput arrow position writes / unchanged popup flip or placement | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;arrow position 未变时不写 | +| InfoPickerInput validation status writes / repeated same form status | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;重复 Error / Warning / Default 不写 `Status` | +| InfoPickerInput compact-space usage writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;`IsUsedInCompactSpace` 结果未变时不写 | +| InfoPickerInput compact-space position writes / repeated same position notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;`CompactSpaceItemPosition` 未变时不写 | +| InfoPickerInput compact-space orientation writes / repeated same orientation notification | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;`CompactSpaceOrientation` 未变时不写 | +| InfoPickerInput form feedback writes / repeated same feedback control | 1 write | 0 writes | `(1 - 0) / 1` | 100.00% | 结构收益;同一个 feedback control 不重复写,避免下游重复订阅刷新 | +| InfoPickerInput hit-test branch count / pointer release | 1 extra branch | 0 extra branches | `(1 - 0) / 1` | 100.00% | 结构收益;bounds 命中结果直接返回 | +| DateTimeUtils month sweep enumerable allocations / widest datetime width calculation | 1 enumerable | 0 enumerables | `(1 - 0) / 1` | 100.00% | 结构收益;按需月份扫描改为 for loop | +| DateTimeUtils weekday sweep enumerable allocations / widest datetime width calculation | 1 enumerable | 0 enumerables | `(1 - 0) / 1` | 100.00% | 结构收益;按需星期扫描改为 for loop | +| DateTimeUtils hour sample array allocations / widest datetime width calculation | 1 array | 0 arrays | `(1 - 0) / 1` | 100.00% | 结构收益;AM/PM 样本小时改为索引分支 | +| MotionGhostControl shadow renderer list growth / build shadow renderers | dynamic capacity | exact capacity | structural | 分配更紧 | 阴影 renderer 数量已知时按 `BoxShadows.Count` 预分配 | +| CompactSpace add wrapper list growth / children add | dynamic capacity | exact capacity | structural | 分配更紧 | 按 `NewItems.Count` 预分配 wrapper 列表 | +| CompactSpace remove lookup set growth / children remove | dynamic capacity | exact capacity | structural | 分配更紧 | 按 `OldItems.Count` 预分配 lookup set | +| CompactSpace template wrapper list growth / apply template | dynamic capacity | exact capacity | structural | 分配更紧 | 按 `Children.Count` 预分配初始 wrapper 列表 | +| SliderTrack mark rect list growth / render with marks | dynamic capacity | exact capacity | structural | 分配更紧 | horizontal / vertical marks 列表按 `Marks.Count` 预分配 | +| SliderTrack mark text rect list growth / render with marks | dynamic capacity | exact capacity | structural | 分配更紧 | mark 文本命中区域列表按 `Marks.Count` 预分配 | +| ListBox virtualizing context dictionary growth / recycled item | dynamic capacity | exact capacity 1 | structural | 分配更紧 | 保存 1 个虚拟化状态键时避免 dictionary 增长 | +| ListView virtualizing context dictionary growth / recycled item | dynamic capacity | exact capacity 2 | structural | 分配更紧 | 保存 2 个虚拟化状态键时避免 dictionary 增长 | +| Cascader level virtualizing context dictionary growth / recycled item | dynamic capacity | exact capacity 5 | structural | 分配更紧 | 保存 5 个虚拟化状态键时避免 dictionary 增长 | +| ProgressBar size threshold dictionary growth / progress instance | dynamic capacity | exact capacity 3 | structural | 分配更紧 | line / circle progress 尺寸阈值表按 `SizeType` 三档预分配 | +| Tag preset color map growth / theme color map build | dynamic capacity | exact capacity 14 | structural | 分配更紧 | preset 颜色表按 `PresetColorType` 已知数量预分配 | +| Tag status color map growth / theme color map build | dynamic capacity | exact capacity 4 | structural | 分配更紧 | status 颜色表按 `TagStatus` 已知数量预分配 | | `DatePicker.Selected.Closed` KB/item | 627.9 KB | 627.5 KB | `(627.9 - 627.5) / 627.9` | 0.06% | smoke-only;分配小幅下降,不作为页面收益证明 | | `DatePicker.Default.PresenterOnly` ms/item | 17.582 ms | 16.919 ms | `(17.582 - 16.919) / 17.582` | 3.77% | smoke-only;单次 timing 只作异常检查 | | `DatePicker.Default.Closed` ms/item | 3.841 ms | 4.299 ms | `(3.841 - 4.299) / 3.841` | -11.92% | smoke-only;单次 timing 回退,不作为本轮收益证明 | @@ -72,6 +253,7 @@ dotnet build -c Release -f net10.0 --no-restore tools/performances/AtomUI.Perfor dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --verify-addon-states dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --verify-datepicker-states --verify-timepicker-states dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --verify-listbox-states +dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --verify-switch-states dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --suite datepicker --count 80 dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --suite addon --count 80 dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --suite listbox --count 80 @@ -84,5 +266,6 @@ Notes: - `PART_ContentFrame` 仍是静态模板节点;本轮没有按需创建 / 销毁 ContentFrame,只把 pointer 状态逻辑移到该静态节点自己的 override。 - `PART_ContentFrame` 的行为子类必须继续以 `Border` 作为 StyleKey;普通 `Border#PART_ContentFrame` selector 是精确 StyleKey 匹配,丢失 StyleKey 会导致 size token padding 不生效。 - `CandidateList` 本轮是交互结构优化,不改变 closed popup / page-load visual tree;`CandidateList.Default.Items20` 单次 smoke 为 `2.097 ms/item`、`473.0 KB/item`,只作异常检查,不作为速度收益证明。 +- `RangeInfoPickerInput` 本轮是 repeated layout 写入路径优化;未新增 Gallery timing 对比,不声明页面加载速度提升。 - `DatePicker` verifier 的旧动态 slot 断言已修正为当前静态模板口径。 - `Select` / `Cascader` 全量 state verifier 仍含若干旧动态 slot 断言,本轮只以 `--verify-addon-states` 覆盖 right-addon template 正确性。 diff --git a/docs/performances/README.md b/docs/performances/README.md index 6c639450c..6a70a163c 100644 --- a/docs/performances/README.md +++ b/docs/performances/README.md @@ -46,52 +46,52 @@ Avalonia 12 控件库研发的成本模型与高频踩雷点(含 `path:line` | 控件/体系 | 状态 | 目录 | 关键结果 | | --- | --- | --- | --- | -| AddOnDecoratedBox | 本轮已完成 | [Primitives](Primitives/README.md) | 默认 LineEdit visual/root `26 -> 20`,KB/item `503.8 -> 398.4`,默认 icon scan `360 -> 0`;ContentFrame pointer handlers `4 -> 0` / instance;Select/TreeSelect/Cascader right-addon template 绑定已修复 | +| AddOnDecoratedBox | 本轮已完成 | [Primitives](Primitives/README.md) | 默认 LineEdit visual/root `26 -> 20`,KB/item `503.8 -> 398.4`,默认 icon scan `360 -> 0`;ContentFrame pointer handlers `4 -> 0` / instance;addon foreground/icon、inner geometry、pointer state unchanged writes 已收敛;Select/TreeSelect/Cascader right-addon template 绑定已修复 | | AdornerLayer | 本轮已完成结构生命周期优化 | [AdornerLayer](AdornerLayer/README.md) | `ScopeAwareAdornerLayer` attach/detach membership scan `1 -> 0`;detach 后 adorner retained target refs `1 -> 0`;Drawer 状态验证通过;timing smoke-only | | Icon | 本轮已完成 | [Icon](Icon/README.md) | `IconShowCase` repeated `193.31ms -> 173.36ms`,alloc `57900.83KB -> 46331.65KB`;generated metadata 与 transform literal 已同步 AntDesign/Material/IconPark;`Select.Default` 默认隐藏 slot 从 `Icon/root 3 -> 1`、`Button/root 1 -> 0` | | LineEdit | 本轮已完成 Gallery 实测 | [LineEdit](LineEdit/README.md) | `LineEditShowCase` 重复导航均值 `229.07ms -> 183.98ms`,提升 `19.68%` | | Button | 本轮已完成 | [Button](Button/README.md) | loading/icon/wave 默认 slot 已按需创建;`ButtonShowCase` visuals `1128 -> 964`,IconPresenter `92 -> 51`,repeated mean `134.60ms -> 101.53ms` | | SplitButton | 本轮已完成正确性与生命周期优化 | [SplitButton](SplitButton/README.md) | 修复 Flyout 重复事件注册、替换后旧 Flyout handler 残留和 secondary button 打开时 `:flyout-open` 不更新;状态验证失败 `7 -> 0` | | ButtonSpinner | 本轮已完成 | [ButtonSpinner](ButtonSpinner/README.md) | `HiddenHandle` visuals `25 -> 10`,KB/item `474.4 -> 212.5`;`ButtonSpinnerShowCase` visuals `725 -> 681`,alloc `14900.81KB -> 13514.36KB` | -| Space | 本轮已完成 | [Space](Space/README.md) | `CompactSpaceItem 93 -> 75`,visuals `1864 -> 1846`,`SpaceShowCase` repeated mean `165.32ms -> 150.48ms` | +| Space | 本轮已完成 | [Space](Space/README.md) | `CompactSpaceItem 93 -> 75`,visuals `1864 -> 1846`,`SpaceShowCase` repeated mean `165.32ms -> 150.48ms`;real-children position temp list `1 -> 0` | | AutoComplete | 本轮已完成 | [AutoComplete](AutoComplete/README.md) | 关闭态 `_candidateList 13 -> 0`;`AutoCompleteShowCase` repeated mean `74.97ms -> 55.63ms`,alloc `8143KB -> 7664.37KB` | | Avatar | 本轮已完成 | [Avatar](Avatar/README.md) | `AvatarShowCase` repeated mean `42.33ms -> 32.54ms`,visuals `360 -> 258`,alloc `7252.55KB -> 5824.01KB` | -| Badge | 本轮已完成 | [Badge](Badge/README.md) | hidden zero `CountBadgeAdorner/root 1 -> 0`,`DotBadge` 无文本 `Label/root 1 -> 0`;`BadgeShowCase` visuals `497 -> 485` | +| Badge | 本轮已完成 | [Badge](Badge/README.md) | hidden zero `CountBadgeAdorner/root 1 -> 0`,`DotBadge` 无文本 `Label/root 1 -> 0`;`BadgeShowCase` visuals `497 -> 485`;Badge/Ribbon preset color match `ToLower()` 临时字符串 `15 -> 0` | | Breadcrumb | 本轮已建立基线并修复 separator 传播 | [Breadcrumb](Breadcrumb/README.md) | 父级 `Separator` 变化现在同步到 direct/generated inherited items,local/data separator 继续胜出;`BreadcrumbShowCase` cold mean `92.98ms -> 87.53ms`,repeated P95 `36.72ms -> 34.00ms`,repeated median 基本持平 | -| Card | 本轮已完成 | [Card](Card/README.md) | `CardShowCase` repeated mean `78.11ms -> 50.66ms`,visuals `784 -> 582`;`CardActionPanel 18 -> 3`,`Skeleton 18 -> 0` | -| Carousel | 本轮已完成 | [Carousel](Carousel/README.md) | `CarouselShowCase` alloc `6113.86KB -> 5213.10KB`,visuals `369 -> 326`;nav `14 -> 4`,progress `28 -> 1`,transition `7 -> 0`,repeated mean 基本持平 | -| Cascader | 本轮已完成 | [Cascader](Cascader/README.md) | filter/multiple closed visual `41/25 -> 20`;`CascaderShowCase` visuals `1250 -> 1169`,repeated mean `141.81ms -> 133.27ms` | +| Card | 本轮已完成 | [Card](Card/README.md) | `CardShowCase` repeated mean `78.11ms -> 50.66ms`,visuals `784 -> 582`;`CardActionPanel 18 -> 3`,`Skeleton 18 -> 0`;CardActionPanel collection add/remove temp lists `1 -> 0` | +| Carousel | 本轮已完成 | [Carousel](Carousel/README.md) | `CarouselShowCase` alloc `6113.86KB -> 5213.10KB`,visuals `369 -> 326`;nav `14 -> 4`,progress `28 -> 1`,transition `7 -> 0`;indicator refresh list wrapper `1 -> 0`,repeated mean 基本持平 | +| Cascader | 本轮已完成 | [Cascader](Cascader/README.md) | filter/multiple closed visual `41/25 -> 20`;`CascaderShowCase` visuals `1250 -> 1169`,repeated mean `141.81ms -> 133.27ms`;multi-select sync temp lists/hashsets exact capacity;OptionsSource `Cast().ToList()` iterator `1 -> 0` / sync,set diff `Except` iterators `2 -> 0` / sync | | CheckBox | 本轮已完成 | [CheckBox](CheckBox/README.md) | 默认 unchecked `visuals 12 -> 8`,contentless unchecked `11 -> 6`,`CheckBoxShowCase` visuals `420 -> 318`,repeated alloc `6385.60KB -> 4629.80KB` | | ColorPicker | 本轮已完成结构生命周期、palette 选择隔离和打开态 brush 复用优化 | [ColorPicker](ColorPicker/README.md) | closed `Window.Deactivated` 订阅 `23 -> 0`;preview/thumb brush allocations per `HsvValue` update `3 -> 0`;透明背景同 token brush refs / 1000 builds `1000 -> 1`,allocation `10569.1 -> 0.2 bytes/update`;`ColorPickerPaletteGroup` global class handler `1/group -> 0/group`,修复跨 group 选择误通知;页面 timing 噪声内 | | Collapse | 本轮已完成 | [Collapse](Collapse/README.md) | `CollapseShowCase` repeated mean `119.31ms -> 100.18ms`,visuals `616 -> 553`;content motion `33 -> 1`,addon presenter `33 -> 3` | | ComboBox | 本轮已完成 | [ComboBox](ComboBox/README.md) | `ComboBoxShowCase` repeated mean `109.35ms -> 91.07ms`,alloc `11384.83KB -> 9534.28KB`,visuals `562 -> 497`;默认 `Button/IconButton 23 -> 0` | | DatePicker | 本轮已完成 | [DatePicker](DatePicker/README.md) | closed route `PickerHost 30 -> 0`,visuals `1570 -> 1540`,alloc `30959.16KB -> 29762.89KB`,长样本 repeated mean `174.77ms -> 154.21ms` | -| Descriptions | 本轮已完成 | [Descriptions](Descriptions/README.md) | `DescriptionsShowCase` repeated mean `75.84ms -> 68.64ms`,visuals `579 -> 571`;`GalleryShape.Batch8` `25.139ms/root -> 24.486ms/root`;修复 binding/collection/window 订阅生命周期风险 | -| Dialog / MessageBox | 本轮已完成结构与生命周期优化 | [Dialog](Dialog/README.md) | `MessageBox` closed `Dialog/root 1 -> 0`,`ModalShowCase` visuals `324 -> 317`,Dialog runtime `14 -> 7`;修复 RelayBind、mask binding、resizer 和 ButtonBox 父级生命周期风险,后续 MessageBox 细化见独立文档 | +| Descriptions | 本轮已完成 | [Descriptions](Descriptions/README.md) | `DescriptionsShowCase` repeated mean `75.84ms -> 68.64ms`,visuals `579 -> 571`;`GalleryShape.Batch8` `25.139ms/root -> 24.486ms/root`;add/remove/apply/layout temp lists `1 -> 0`;修复 binding/collection/window 订阅生命周期风险 | +| Dialog / MessageBox | 本轮已完成结构与生命周期优化 | [Dialog](Dialog/README.md) | `MessageBox` closed `Dialog/root 1 -> 0`,`ModalShowCase` visuals `324 -> 317`,Dialog runtime `14 -> 7`;修复 RelayBind、mask binding、resizer 和 ButtonBox 父级生命周期风险;ButtonBox 缺省角色列表 fallback allocations `14 -> 0`,group/sync lists exact capacity;后续 MessageBox 细化见独立文档 | | DropdownButton | 本轮已完成生命周期优化 | [DropdownButton](DropdownButton/README.md) | detached `MenuItemClicked` handlers `1 -> 0`,detached flyout bindings `8 -> 0`,detach 后残留 handler `1 -> 0` | | Expander | 本轮已完成 | [Expander](Expander/README.md) | closed content motion actor 按需创建;`ExpanderShowCase` cold mean `191.70ms -> 170.47ms`,repeated mean `82.18ms -> 67.30ms`,visuals `391 -> 360`,MotionActor `16 -> 1` | | Empty | 本轮已完成低风险修复 | [Empty](Empty/README.md) | 修复 `IsDescriptionVisible=False`、图片来源运行时互斥和 `Svg.Source/Path` 残留;`EmptyShowCase` repeated mean `21.90ms -> 21.32ms`,alloc `2269.73KB -> 2253.90KB`,visuals 不变 | -| FloatButton | 本轮已完成 | [FloatButton](FloatButton/README.md) | closed trigger group visual/root `41 -> 14`;`FloatButtonShowCase` repeated mean `126.71ms -> 110.81ms`,visuals `949 -> 752`,alloc `14447.92KB -> 12182.94KB` | +| FloatButton | 本轮已完成 | [FloatButton](FloatButton/README.md) | closed trigger group visual/root `41 -> 14`;`FloatButtonShowCase` repeated mean `126.71ms -> 110.81ms`,visuals `949 -> 752`,alloc `14447.92KB -> 12182.94KB`;square group arrange visible-children temp list `1 -> 0`;0/1 visible child line list `1 -> 0`;group add/remove temp lists `1 -> 0` | | FlexPanel | 本轮已建立基线,未保留 runtime 改动 | [FlexPanel](FlexPanel/README.md) | 新增 5 个控件级场景和 4 个状态校验;生产候选因 Wrap/GrowShrink/ColumnWrap timing smoke 回归已撤回 | -| Grid | 本轮已完成正确性修复与结构优化 | [Grid](Grid/README.md) | 修复 `Row` cached child layout 在 `Col.Span/Order` 与 `IsVisible` 变化后的 stale 问题;same-width measure→arrange `BuildLayout` 调用 `2 -> 1`,页面速度不声明 | -| Form | 本轮已完成低风险结构与生命周期优化 | [Form](Form/README.md) | `Form.GalleryShape` `92.094ms/item -> 89.364ms/item`,visual/root `1544 -> 1508`;`FormShowCase` visuals `6435 -> 6300`,alloc 少 `3.54%`,页面 repeated 耗时基本持平 | -| GroupBox | 本轮已完成 | [GroupBox](GroupBox/README.md) | 默认无 `HeaderIcon` 不再创建 header icon presenter;`GroupBox.GalleryShape` visual/root `104 -> 96`,`GroupBoxShowCase` visuals `151 -> 143`,repeated mean `27.79ms -> 26.16ms` | -| ImagePreviewer | 本轮已完成 | [ImagePreviewer](ImagePreviewer/README.md) | 单图关闭态不再提前创建完整预览 source list;`ImagePreviewerShowCase` cold mean `180.28ms -> 147.48ms`,repeated mean `33.56ms -> 30.61ms` | +| Grid | 本轮已完成正确性修复与结构优化 | [Grid](Grid/README.md) | 修复 `Row` cached child layout 在 `Col.Span/Order` 与 `IsVisible` 变化后的 stale 问题;same-width measure→arrange `BuildLayout` 调用 `2 -> 1`;`GridColSize.ConvertTo` parts temp list `1 -> 0`,页面速度不声明 | +| Form | 本轮已完成低风险结构与生命周期优化 | [Form](Form/README.md) | `Form.GalleryShape` `92.094ms/item -> 89.364ms/item`,visual/root `1544 -> 1508`;`FormShowCase` visuals `6435 -> 6300`,alloc 少 `3.54%`,页面 repeated 耗时基本持平;validate tasks/results/messages exact upper-bound capacity | +| GroupBox | 本轮已完成 | [GroupBox](GroupBox/README.md) | 默认无 `HeaderIcon` 不再创建 header icon presenter;`GroupBox.GalleryShape` visual/root `104 -> 96`,`GroupBoxShowCase` visuals `151 -> 143`,repeated mean `27.79ms -> 26.16ms`;header mask fallback brush allocations per render `1 -> 0` | +| ImagePreviewer | 本轮已完成 | [ImagePreviewer](ImagePreviewer/README.md) | 单图关闭态不再提前创建完整预览 source list;`ImagePreviewerShowCase` cold mean `180.28ms -> 147.48ms`,repeated mean `33.56ms -> 30.61ms`;fallback source list `1 list -> 1 fixed array`,ItemsSource load list exact capacity | | InfoFlyout | 本轮已完成正确性与生命周期优化 | [InfoFlyout](InfoFlyout/README.md) | 关闭态 Popup shell `1 -> 0`,`Flyout=null` 后 host-to-flyout bindings `13 -> 0`,`Flyouts.GalleryShape` closed Popup shells `12/29 -> 0/29` | | Input | 本轮已完成结构优化 | [Input](Input/README.md) | `TextBox.Default` visual/root `21 -> 13`、KB/item `332.6 -> 236.7`;`TextArea.Default` visual/root `24 -> 21`;`LineEditShowCase` visuals `2307 -> 2284`,repeated mean 基本持平 | | ListView | 本轮已完成 | [ListView](ListView/README.md) | `ListView.Grouped` `3.619ms/item -> 1.759ms/item`,`FilterActive` `3.421ms/item -> 1.841ms/item`;`ListShowCase` repeated mean `80.72ms -> 77.34ms` | | MarqueeLabel / Alert | 本轮已完成 | [MarqueeLabel](MarqueeLabel/README.md) | Alert 默认路径不再创建隐藏 `MarqueeLabel`;`AlertShowCase` runtime `MarqueeLabel 25 -> 1`,visuals `585 -> 561`,repeated mean `65.42ms -> 54.67ms` | -| Mentions | 本轮已完成 | [Mentions](Mentions/README.md) | closed popup content 已按需创建;`MentionsShowCase` `_candidateList 15 -> 0`,repeated mean `110.05ms -> 106.95ms`,alloc `10115.79KB -> 9326.47KB` | +| Mentions | 本轮已完成 | [Mentions](Mentions/README.md) | closed popup content 已按需创建;`MentionsShowCase` `_candidateList 15 -> 0`,repeated mean `110.05ms -> 106.95ms`,alloc `10115.79KB -> 9326.47KB`;OptionsSource cache list exact capacity | | Menu | 本轮已完成 | [Menu](Menu/README.md) | closed leaf `MenuItem` visual/root `13 -> 9`,KB/item `209.8 -> 122.1`;`MenuShowCase` Menu-only phase repeated mean `91.16ms -> 89.10ms`,严格 NavMenu shape 见 NavMenu 文档 | -| NavMenu | 本轮已完成 | [NavMenu](NavMenu/README.md) | 修复 prepared node binding 生命周期和默认路径遍历;`MenuShowCase` 严格完整形态 repeated mean `162.76ms -> 99.13ms`,cold mean `416.22ms -> 351.72ms` | -| Message | 本轮已完成 | [Message](Message/README.md) | `WindowMessageManager` 只在首次实际 show 时创建;timer/event/OnClose 生命周期已释放;`MessageShowCase` repeated mean `30.97ms -> 28.52ms`,cold mean `109.07ms -> 98.86ms` | +| NavMenu | 本轮已完成 | [NavMenu](NavMenu/README.md) | 修复 prepared node binding 生命周期和默认路径遍历;`MenuShowCase` 严格完整形态 repeated mean `162.76ms -> 99.13ms`,cold mean `416.22ms -> 351.72ms`;horizontal active indicator repeated scale update transform allocation `1 -> 0`;path traversal lists exact capacity | +| Message | 本轮已完成 | [Message](Message/README.md) | `WindowMessageManager` 只在首次实际 show 时创建;timer/event/OnClose 生命周期已释放;`MessageShowCase` repeated mean `30.97ms -> 28.52ms`,cold mean `109.07ms -> 98.86ms`;excess visible messages temp list `1 -> 0` | | MessageBox | 本轮已完成 | [MessageBox](MessageBox/README.md) | 同生命周期 binding 改为 `[!]`,loading skeleton 按需创建并验证释放;`ModalShowCase` repeated mean `34.53ms -> 32.11ms`,P95 `37.18ms -> 34.42ms` | -| Notification | 本轮已完成 | [Notification](Notification/README.md) | manager/card 生命周期补齐,manager/progress bar 按需创建;`NotificationShowCase` cold mean `176.82ms -> 155.20ms`,repeated mean `37.46ms -> 29.13ms` | +| Notification | 本轮已完成 | [Notification](Notification/README.md) | manager/card 生命周期补齐,manager/progress bar 按需创建;`NotificationShowCase` cold mean `176.82ms -> 155.20ms`,repeated mean `37.46ms -> 29.13ms`;progress visibility converter temp list `1 -> 0`;cleanup queue/set exact default capacity | | NumericUpDown | 本轮已完成 | [NumericUpDown](NumericUpDown/README.md) | 默认 right accessory 按需创建;`NumberUpDownShowCase` visuals `1351 -> 1269`,Button/IconButton `90 -> 60`,cold mean `480.45ms -> 427.73ms` | | OptionButtonGroup | 本轮已完成 | [OptionButtonGroup](OptionButtonGroup/README.md) | 文本按钮默认不创建 `IconPresenter/Wave`;`RadioButtonShowCase` visuals `794 -> 689`,repeated mean `100.85ms -> 84.72ms` | | Pagination | 本轮已完成 | [Pagination](Pagination/README.md) | 页码项 icon/content slot 按需创建;`PaginationShowCase` visuals `1262 -> 1065`,IconPresenter `120 -> 51`,repeated mean `168.92ms -> 131.76ms` | | PopupConfirm | 本轮已完成正确性与生命周期优化 | [PopupConfirm](PopupConfirm/README.md) | container 本地 Click handlers `2 -> 0`,隐藏 cancel direct-click side effects `2 -> 0`;`Container.*` KB/item 小幅下降,页面 timing 仅 smoke-only、不作为本轮收益证明 | -| Primitives / InfoPickerInput + AddOnDecoratedBox + CandidateList | 本轮已完成正确性、生命周期与交互结构优化 | [Primitives](Primitives/README.md) | `PickerClearUpButton` 本地 Click handlers `1 -> 0`,`AddOnDecoratedBox` ContentFrame pointer handlers `4 -> 0` / instance;`CandidateList.Items20` 键盘候选移动 container checks `20 -> <=2`;页面 timing 不作为本轮收益证明 | +| Primitives / InfoPickerInput + AddOnDecoratedBox + CandidateList | 本轮已完成正确性、生命周期与交互结构优化 | [Primitives](Primitives/README.md) | `PickerClearUpButton` 本地 Click handlers `1 -> 0`,`AddOnDecoratedBox` ContentFrame pointer handlers `4 -> 0` / instance;`CandidateList.Items20` 键盘候选移动 container checks `20 -> <=2`,max-count enable writes `20 -> 0`,空列表 invalid candidate writes `1 -> 0`,clear-state writes `3 -> 0`;`TreeNodePath` 派生操作 arrays `2 -> 1`,部分 fast path `1 -> 0`,same-instance comparisons `N -> 0`;`MotionGhostControl` repeated shadow writes 已收敛,shadow renderer list exact capacity;`ArrowDecoratedBox` repeated arrow writes 已收敛;`WaveSpiritDecorator` painter configure easing allocations `2 -> 0`,fallback easing allocations `1 -> 0`;`PenUtils` repeated immutable pen allocations `1 -> 0`、dashed pen allocations `2 -> 0`;OptionButtonGroup / FloatButtonSeparatorLayer / ButtonSpinnerHandle / CardActionPanel / TextAreaDecoratedBox / CascaderViewFrame / SliderThumb / SliderTrack / MenuSeparator / CircleProgress / DashboardProgress repeated render `Pen` allocations `1-3 -> 0 after first`;`DateTimeUtils` widest datetime calculation enumerable/array allocations `3 -> 0`;`BorderRenderHelper` dash mode cache stale risk `1 -> 0`;`CompactSpaceItem` repeated transform allocation `1 -> 0` after first,stale transform `1 -> 0`;SliderTrack marks、ListBox/ListView/Cascader virtualizing context、ProgressBar/Tag 小型表按已知容量预分配;`RangeInfoPickerInput` unchanged indicator writes `6 -> 0`;`InfoPickerInput` / `TextBox` / `TextArea` / `ButtonSpinner` / `NumericUpDown` / `AbstractSelect` open/close、clear、arrow、validation、compact-space、form-feedback unchanged writes `1 -> 0`;页面 timing 不作为本轮收益证明 | | QRCode | 本轮已完成 | [QRCode](QRCode/README.md) | 默认 Active visual/root `19 -> 7`,`QRCodeShowCase` repeated mean `125.12ms -> 91.58ms`,visuals `436 -> 307` | | RadioButton | 本轮已完成 | [RadioButton](RadioButton/README.md) | `RadioIndicator` wave 按需创建;`RadioButtonShowCase` visuals `689 -> 668`,Wave `22 -> 1`,repeated P95 `184.13ms -> 164.47ms` | | Rate | 本轮已完成 | [Rate](Rate/README.md) | `Rate.GalleryShape` `15.846ms/item -> 10.354ms/item`;`RateShowCase` repeated mean `62.75ms -> 55.87ms`,visuals `376 -> 331` | @@ -101,22 +101,22 @@ Avalonia 12 控件库研发的成本模型与高频踩雷点(含 `path:line` | Slider | 本轮已完成 | [Slider](Slider/README.md) | 普通 Slider 不再预创建 `PART_EndThumb`;`SliderShowCase` repeated mean `46.31ms -> 33.78ms`,visuals `111 -> 105` | | SplitView | 本轮已完成结构生命周期优化 | [SplitView](SplitView/README.md) | 初始 closed/open 不再 materialize pane transition;典型场景 KB/item 约下降 `~1KB`,runtime open/close transition 行为保留 | | Spin | 本轮已完成结构生命周期与正确性修复 | [Spin](Spin/README.md) | hidden `SpinIndicator` 不再提前构造 animation;非 spinning KB/item `78.8 -> 77.4`,GalleryShape KB/item `575.1 -> 564.2`;motion 参数变化后动画重建 | -| Splitter | 本轮已完成 Gallery 实测 + runtime structural | [Splitter](Splitter/README.md) | hidden collapse icon 不再构造/保留 `PathIcon`,lazy preview transform 复用;动态 children add/remove temp objects `3 -> 1`,panel refresh temp objects `2 -> 0`,resize event sizes temp objects `3 -> 1`;`SplitterShowCase` repeated mean `36.91ms -> 34.66ms`,P95 `51.79ms -> 40.43ms`,alloc `5770.31KB -> 5659.40KB` | +| Splitter | 本轮已完成 Gallery 实测 + runtime structural | [Splitter](Splitter/README.md) | hidden collapse icon 不再构造/保留 `PathIcon`,lazy preview transform 复用;动态 children add/remove temp objects `3 -> 1`,panel refresh temp objects `2 -> 0`,resize event sizes temp objects `3 -> 1`;panel size compute temp lists `2 -> 0`;`SplitterShowCase` repeated mean `36.91ms -> 34.66ms`,P95 `51.79ms -> 40.43ms`,alloc `5770.31KB -> 5659.40KB` | | Statistic | 本轮已完成正确性与生命周期优化 | [Statistic](Statistic/README.md) | generated `Content` ownership、`StatisticCountUp.DataContext` 清理、`TimerStatistic` attach-gated timer 释放;`StatisticShowCase` repeated alloc `7471.87KB -> 7228.74KB`,visuals `387 -> 384` | | Switch | 本轮已完成交互路径结构优化 | [Switch](Switch/README.md) | `IsChecked` 切换不再触发 measure invalidation;1000 次切换 measure invalidations `1000 -> 0`,arrange 保持 `1000`,页面加载指标中性/噪声内 | | Timeline | 本轮已完成结构与正确性修复 | [Timeline](Timeline/README.md) | `TimelineIndicator.Render` Pen 缓存;修复 pending item 清空和默认/custom icon 显示;页面级 timing 未证明收益,pending spinner 正确显示带来 `+2` visuals | -| TextBlock | 本轮已完成 | [TextBlock](TextBlock/README.md) | `HighlightableTextBlock` 段级 Run 重写:Match.Medium `3.306ms→0.426ms (-87%)`、`355.8KB→44.0KB (-88%)`、`logical 56→4`;`SelectableTextBlock` token binding+Cursor 移到 Theme Setter | +| TextBlock | 本轮已完成 | [TextBlock](TextBlock/README.md) | `HighlightableTextBlock` 段级 Run 重写:Match.Medium `3.306ms→0.426ms (-87%)`、`355.8KB→44.0KB (-88%)`、`logical 56→4`;`SelectableTextBlock` token binding+Cursor 移到 Theme Setter;无命中高亮 ranges list `1 -> 0` | | Popup | 本轮已完成 | [Popup](Popup/README.md) | `Popup` 4 条构造器 token binding 迁到 ControlTheme(fan-in 19 → ~76 条订阅消除 + Tier 1 §7 同优先级碰撞修复);构造路径 `0.154ms→0.107ms (-31%)`、`23.4KB→21.9KB (-6%)`;`ShadowsAwareContainer` 影子 Border 延迟到 `HasBoxShadow` | -| Tag | 本轮已完成 | [Tag](Tag/README.md) | `AbstractTag.SetupDefaultCloseIcon` 门控到 `IsClosable=true`;非 closable Tag 各 scenario 减少 ~2 KB/instance;Gallery 76 实例 × ~70 非 closable ≈ 140 KB 加载分配下降;closable 路径行为不变 | -| Transfer | 本轮已完成 Gallery 实测 + structural | [Transfer](Transfer/README.md) | `TargetKeys` membership lookup 改为每次刷新复用 `HashSet`,空 target panel 直接短路;`TreeTransfer` target 递归结果列表从 `N -> 1`;`TransferShowCase` repeated mean `242.72ms -> 222.12ms`,cold mean `855.88ms -> 768.42ms`,visuals/logical 不变 | -| TreeView | 本轮已完成 Gallery 实测 + render/interaction structural | [TreeView](TreeView/README.md) | `NodeSwitcherButton` 模板 5 个图标 presenter 收敛到 1 个;`TreeViewShowCase` visuals `2108 -> 1646`、`IconPresenter 332 -> 112`、repeated mean `332.44ms -> 213.41ms`;连接线 repeated render `Pen` allocations `1-2 -> 0 after first`,拖拽 indicator render frame `1 -> 0 after first`;checkbox parent aggregation child scans `2 -> 1` | +| Tag | 本轮已完成 | [Tag](Tag/README.md) | `AbstractTag.SetupDefaultCloseIcon` 门控到 `IsClosable=true`;非 closable Tag 各 scenario 减少 ~2 KB/instance;Gallery 76 实例 × ~70 非 closable ≈ 140 KB 加载分配下降;Tag color name `ToLower()` 临时字符串 `19 -> 0` | +| Transfer | 本轮已完成 Gallery 实测 + structural | [Transfer](Transfer/README.md) | `TargetKeys` membership lookup 改为每次刷新复用 `HashSet`,空 target panel 直接短路;`TreeTransfer` target 递归结果列表从 `N -> 1`;本轮补充 key list `Select().ToList()` materialization `6 -> 0`、selected-items sync LINQ chain `1 -> 0`;`TransferShowCase` repeated mean `242.72ms -> 222.12ms`,cold mean `855.88ms -> 768.42ms`,visuals/logical 不变 | +| TreeView | 本轮已完成 Gallery 实测 + render/interaction structural | [TreeView](TreeView/README.md) | `NodeSwitcherButton` 模板 5 个图标 presenter 收敛到 1 个;`TreeViewShowCase` visuals `2108 -> 1646`、`IconPresenter 332 -> 112`、repeated mean `332.44ms -> 213.41ms`;连接线 repeated render `Pen` allocations `1-2 -> 0 after first`,拖拽 indicator render frame `1 -> 0 after first`;checkbox parent aggregation child scans `2 -> 1`;path traversal / checked reset lists exact capacity | | TreeSelect | 本轮已完成 Gallery 实测 | [TreeSelect](TreeSelect/README.md) | 共享 `SelectHandle` 三套 indicator 收敛到单 presenter;`TreeSelectShowCase` visuals `909 -> 858`、`IconPresenter 38 -> 21`、repeated mean `124.72ms -> 108.95ms`、alloc `19996.64KB -> 18118.79KB` | -| TimePicker | 本轮已完成 Gallery 实测 | [TimePicker](TimePicker/README.md) | `InfoPickerInput` closed presenter 延迟到首次打开;`TimePickerShowCase` cold mean `464.82ms -> 393.11ms`,repeated mean `148.79ms -> 138.48ms` | -| Upload | 本轮已建立 baseline,未保留 runtime 改动 | [Upload](Upload/README.md) | `UploadShowCase` baseline:cold mean `351.77ms`,repeated mean `87.48ms`;3 个候选因 timing 无收益/退化已回滚,仅保留 GalleryPerformance 映射 | +| TimePicker | 本轮已完成 Gallery 实测 | [TimePicker](TimePicker/README.md) | `InfoPickerInput` closed presenter 延迟到首次打开;`TimePickerShowCase` cold mean `464.82ms -> 393.11ms`,repeated mean `148.79ms -> 138.48ms`;increment correction temp list/enumerable `1 -> 0`;datetime preferred-width sweep temp enumerable/array `3 -> 0` | +| Upload | 本轮已建立 baseline + 单图预览 structural-only | [Upload](Upload/README.md) | `UploadShowCase` baseline:cold mean `351.77ms`,repeated mean `87.48ms`;3 个页面级候选因 timing 无收益/退化已回滚;单图预览 sources wrapper `1 list -> 1 fixed array`;directory upload file enumeration iterator `1 -> 0`;drop files list exact capacity | | WindowTitleBar | 本轮已完成结构优化 | [WindowTitleBar](WindowTitleBar/README.md) | `CaptionButton` normal/checked 两套 `IconPresenter` 收敛为单 presenter;Windows 默认标题栏 visual/root `40 -> 27`,`IconPresenter/root 10 -> 5`,KB/item `782.4 -> 531.6`;页面 timing smoke-only | | Steps | 本轮已完成结构正确性修复 | [Steps](Steps/README.md) | `ConfigureItemsPanel` 清理 stale `ColumnDefinitions`;`StepsShowCase` repeated timing 未证明收益,按 structural-only 记录 | -| TabControl | 本轮已完成 Gallery 实测 + render structural | [TabControl](TabControl/README.md) | 默认无 icon/非 closable tab 不再创建隐藏 slot;`TabControlShowCase` visuals `1583 -> 1405`,repeated mean `170.75ms -> 157.35ms`;tab strip border repeated render `Pen` allocations `1/control -> 0 after first` | -| Tour | 本轮已完成结构绑定优化 | [Tour](Tour/README.md) | `TourStepsView` 7 条同生命周期 `RelayBind` 改为 template-priority direct binding;`TourShowCase` repeated mean `65.81ms -> 61.91ms`,visuals/logical 不变 | +| TabControl | 本轮已完成 Gallery 实测 + render structural | [TabControl](TabControl/README.md) | 默认无 icon/非 closable tab 不再创建隐藏 slot;`TabControlShowCase` visuals `1583 -> 1405`,repeated mean `170.75ms -> 157.35ms`;tab strip border repeated render `Pen` allocations `1/control -> 0 after first`;edge indicator visibility temp list `1 -> 0` | +| Tour | 本轮已完成结构绑定优化 | [Tour](Tour/README.md) | `TourStepsView` 7 条同生命周期 `RelayBind` 改为 template-priority direct binding;`TourShowCase` repeated mean `65.81ms -> 61.91ms`,visuals/logical 不变;CustomActions sync LINQ/list wrapper `1 -> 0` | | Tooltip | 本轮已完成(structural-only) | [Tooltip](Tooltip/README.md) | `ToolTipService.StartShowTimer` 复用 DispatcherTimer + method-group Tick handler,避免每次 hover-show 分配 timer + lambda 闭包;headless bench 不能触发 Show 流程,real-world hover-heavy 场景待 Gallery 实测 | | Separator | 本轮已完成(structural-only) | [Separator](Separator/README.md) | `AbstractSeparator.Render` Pen 缓存(SKILL Cost Model 强制);headless bench 解析力 < 0.1 KB/instance 看不出,real-world 60fps render loop 才能放大 | @@ -125,80 +125,80 @@ Avalonia 12 控件库研发的成本模型与高频踩雷点(含 `path:line` | 分类 | 控件/体系 | 状态 | 备注 | | --- | --- | --- | --- | | Shared Primitive | AdornerLayer | Done (structural lifecycle) | [AdornerLayer](AdornerLayer/README.md);`ScopeAwareAdornerLayer` attach/detach 不再扫描 `Children.Contains`,卸载后清 adorned target 引用 | -| Shared Primitive | AddOnDecoratedBox | Done | [Primitives](Primitives/README.md);输入类控件共享视觉与 addon 体系;ContentFrame pointer handlers `4 -> 0` / instance;Select/TreeSelect/Cascader right-addon template 绑定已修复 | +| Shared Primitive | AddOnDecoratedBox | Done | [Primitives](Primitives/README.md);输入类控件共享视觉与 addon 体系;ContentFrame pointer handlers `4 -> 0` / instance;addon foreground/icon、inner geometry、pointer state unchanged writes 已收敛;Select/TreeSelect/Cascader right-addon template 绑定已修复 | | Shared Primitive | Icon | Done | Phase 6 已完成;Material/IconPark 外部包已同步 generated metadata 与 transform literal;Provider cache 清理语义已补齐 | -| Shared Primitive | Primitives / InfoPickerInput / CandidateList | Done (correctness + lifecycle + interaction structural) | [Primitives](Primitives/README.md);`PickerClearUpButton` 本地 Click handler `1 -> 0`,`AddOnDecoratedBox` ContentFrame pointer handlers `4 -> 0` / instance,`CandidateList.Items20` 键盘候选移动 container checks `20 -> <=2` | +| Shared Primitive | Primitives / InfoPickerInput / CandidateList | Done (correctness + lifecycle + interaction structural) | [Primitives](Primitives/README.md);`PickerClearUpButton` 本地 Click handler `1 -> 0`,`AddOnDecoratedBox` ContentFrame pointer handlers `4 -> 0` / instance,`CandidateList.Items20` 键盘候选移动 container checks `20 -> <=2`、max-count enable writes `20 -> 0`、空列表 invalid candidate writes `1 -> 0`、clear-state writes `3 -> 0`,`TreeNodePath` 派生操作 arrays `2 -> 1`、部分 fast path `1 -> 0`、same-instance comparisons `N -> 0`,`MotionGhostControl` repeated shadow writes 已收敛、shadow renderer list exact capacity,`ArrowDecoratedBox` repeated arrow writes 已收敛,`WaveSpiritDecorator` painter configure easing allocations `2 -> 0`、fallback easing allocations `1 -> 0`,`PenUtils` repeated immutable pen allocations `1 -> 0`、dashed pen allocations `2 -> 0`,多个自绘控件 repeated render `Pen` allocations `1-3 -> 0 after first`,含 SliderThumb / SliderTrack / MenuSeparator / CircleProgress / DashboardProgress,本轮未保留 Steps progress pen 旧失败候选;`DateTimeUtils` widest datetime calculation enumerable/array allocations `3 -> 0`;`BorderRenderHelper` dash mode cache stale risk `1 -> 0`,`CompactSpace` wrapper / lookup lists exact capacity,SliderTrack marks、ListBox/ListView/Cascader virtualizing context、ProgressBar/Tag 小型表 exact capacity,`CompactSpaceItem` repeated transform allocation `1 -> 0` after first、stale transform `1 -> 0`,`RangeInfoPickerInput` unchanged indicator writes `6 -> 0`,`InfoPickerInput` / `TextBox` / `TextArea` / `ButtonSpinner` / `NumericUpDown` / `AbstractSelect` open/close、clear、arrow、validation、compact-space、form-feedback unchanged writes `1 -> 0` | | Shared Primitive | ScrollViewer | Done | [ScrollViewer](ScrollViewer/README.md);补齐控件级 suite 和真实 `FloatButtonShowCase` 对比,落地 selector/motion 正确性修复,未证明稳定页面级性能提升 | | General | Button | Done | loading/icon/wave 默认 slot 已按需创建;selector brush 状态保留在 XAML | -| General | FloatButton | Done | [FloatButton](FloatButton/README.md);closed trigger menu、badge canvas、separator layer 已按需创建,Gallery repeated mean 提升约 `12.55%` | +| General | FloatButton | Done | [FloatButton](FloatButton/README.md);closed trigger menu、badge canvas、separator layer 已按需创建,Gallery repeated mean 提升约 `12.55%`;square group arrange visible-children temp list `1 -> 0`,0/1 visible child line list `1 -> 0`,group add/remove temp lists `1 -> 0` | | General | SplitButton | Done (correctness + lifecycle) | [SplitButton](SplitButton/README.md);Flyout event/binding 注册幂等,替换旧 Flyout 不再残留 handler,secondary button 打开状态已修复 | | General | Separator | Done (structural-only) | [Separator](Separator/README.md);`AbstractSeparator.Render` Pen 缓存,headless bench 解析力不足,页面 timing 不作为主收益 | | General | ButtonSpinner | Done | [ButtonSpinner](ButtonSpinner/README.md);handle/presenter/outer addon 已按需创建,floatable 全局 input 订阅生命周期已验证 | | General | WindowTitleBar | Done (structural) | [WindowTitleBar](WindowTitleBar/README.md);`CaptionButton` 单 presenter 化,Windows 默认标题栏 visual/root `40 -> 27`,`IconPresenter/root 10 -> 5`,页面 timing 不作为收益证明 | | Layout | FlexPanel | Baseline only | [FlexPanel](FlexPanel/README.md);已建立 5 个控件级场景和 4 个状态校验,生产候选因 timing smoke 回归已撤回 | -| Layout | Grid | Done (correctness + structural) | [Grid](Grid/README.md);修复 `Row` stale layout cache,新增 6 个控件级场景和 3 个状态校验;timing smoke-only | -| Layout | Space | Done | 已完成 Phase 0-7;`CompactSpaceFiller` 不再承担 wrapper,repeated timing 小幅改善,cold 仍需后续针对子控件/Gallery 拆解 | +| Layout | Grid | Done (correctness + structural) | [Grid](Grid/README.md);修复 `Row` stale layout cache,新增 6 个控件级场景和 3 个状态校验;`GridColSize.ConvertTo` parts temp list `1 -> 0`;timing smoke-only | +| Layout | Space | Done | 已完成 Phase 0-7;`CompactSpaceFiller` 不再承担 wrapper;本轮补充 `CompactSpaceItem` / `CompactSpaceAddOn` / `TextBox` / `ButtonSpinner` / `NumericUpDown` repeated compact-space writes `1 -> 0`,`CompactSpaceItem` repeated transform allocation `1 -> 0` after first,stale transform `1 -> 0`,real-children position temp list `1 -> 0`,wrapper / lookup lists exact capacity;页面 timing 不作为本轮收益证明 | | Layout | SplitView | Done (structural lifecycle) | [SplitView](SplitView/README.md);初始 pane transition 延迟到第一次 runtime open/close,典型场景 KB/item 约下降 `~1KB` | -| Layout | Splitter | Done | [Splitter](Splitter/README.md);hidden collapse icon 不再构造/保留 `PathIcon`,lazy preview transform 复用,动态 children add/remove temp objects `3 -> 1`,panel refresh temp objects `2 -> 0`,resize event sizes temp objects `3 -> 1`,`SplitterShowCase` repeated mean 提升约 `6.10%`,P95 提升约 `21.93%` | +| Layout | Splitter | Done | [Splitter](Splitter/README.md);hidden collapse icon 不再构造/保留 `PathIcon`,lazy preview transform 复用,动态 children add/remove temp objects `3 -> 1`,panel refresh temp objects `2 -> 0`,resize event sizes temp objects `3 -> 1`,panel size compute temp lists `2 -> 0`,`SplitterShowCase` repeated mean 提升约 `6.10%`,P95 提升约 `21.93%` | | Navigation | Breadcrumb | Done (correctness + baseline) | [Breadcrumb](Breadcrumb/README.md);修复父级 separator 传播到 direct/generated inherited items;Gallery cold / P95 / alloc 有收益,repeated median 基本持平 | | Navigation | ComboBox | Done | [ComboBox](ComboBox/README.md);默认 host、popup content、handle `IconButton` 成本已按需/轻量化,Gallery repeated mean 提升约 `16.72%` | | Navigation | DropdownButton | Done (lifecycle) | [DropdownButton](DropdownButton/README.md);`MenuItemClicked` 与 flyout property bindings 改为 attach-gated,detached/replace 生命周期已验证 | | Navigation | Menu | Done | [Menu](Menu/README.md);闭合 leaf/toggle/submenu Popup 重内容已按需创建,ContextMenu window 订阅释放已补齐,Gallery repeated mean 小幅提升 | -| Navigation | NavMenu | Done | [NavMenu](NavMenu/README.md);container binding 生命周期、全局关闭订阅按需化、默认路径完整显示和 Gallery 严格 ready 口径已完成 | +| Navigation | NavMenu | Done | [NavMenu](NavMenu/README.md);container binding 生命周期、全局关闭订阅按需化、默认路径完整显示和 Gallery 严格 ready 口径已完成;active indicator repeated scale transform allocation `1 -> 0`;path traversal lists exact capacity | | Navigation | Pagination | Done | [Pagination](Pagination/README.md);页码项和只读 `SimplePagination` 隐藏成本已按需化,`PaginationShowCase` repeated mean 提升约 `21.10%` | | Navigation | Steps | Done (structural-only) | [Steps](Steps/README.md);修复 `ConfigureItemsPanel` 未清理 `ColumnDefinitions` 的结构问题,未证明稳定页面级性能收益 | -| Navigation | TabControl | Done (render structural) | [TabControl](TabControl/README.md);默认 tab item hidden slot 按需化,`TabControlShowCase` repeated mean 提升约 `7.85%`,visuals `1583 -> 1405`;tab strip border repeated render `Pen` allocations `1/control -> 0 after first` | -| Data Entry | AutoComplete | Done | 关闭态 CandidateList/PopupFrame 已按需创建;Gallery repeated mean 提升约 `25.79%` | -| Data Entry | Cascader | Done | [Cascader](Cascader/README.md);closed popup、filter input、multiple tags、filter list、checkbox/loading slot 已按需创建,Gallery repeated mean 小幅下降 | +| Navigation | TabControl | Done (render structural) | [TabControl](TabControl/README.md);默认 tab item hidden slot 按需化,`TabControlShowCase` repeated mean 提升约 `7.85%`,visuals `1583 -> 1405`;tab strip border repeated render `Pen` allocations `1/control -> 0 after first`;edge indicator visibility temp list `1 -> 0` | +| Data Entry | AutoComplete | Done | 关闭态 CandidateList/PopupFrame 已按需创建;Gallery repeated mean 提升约 `25.79%`;single selection changed removed/added lists 按 0/1 个元素预设容量,保留 Avalonia `SelectionChangedEventArgs` 的可变 `List` 兼容形态 | +| Data Entry | Cascader | Done | [Cascader](Cascader/README.md);closed popup、filter input、multiple tags、filter list、checkbox/loading slot 已按需创建,Gallery repeated mean 小幅下降;multi-select sync temp lists/hashsets exact capacity;OptionsSource copy / selected set diff 去 LINQ materialization | | Data Entry | CheckBox | Done | [CheckBox](CheckBox/README.md);unchecked/contentless 默认路径不再创建 wave、Icon mark、tristate mark;`CheckBoxShowCase` visuals `420 -> 318`,alloc `6385.60KB -> 4629.80KB` | | Data Entry | ColorPicker | Done (structural lifecycle + correctness) | [ColorPicker](ColorPicker/README.md);关闭态不再订阅 `Window.Deactivated`,Gallery 23 个 idle 订阅降为 0;preview/thumb brush allocations per `HsvValue` update `3 -> 0`;透明背景同 token brush refs / 1000 builds `1000 -> 1`;palette 选择事件改为实例级 handler,修复跨 group 误通知 | | Data Entry | DatePicker | Done | [DatePicker](DatePicker/README.md);关闭态 popup content、默认 accessory host、Window.Deactivated 订阅已按需化 | -| Data Entry | TimePicker | Done | [TimePicker](TimePicker/README.md);`InfoPickerInput` closed presenter 延迟到首次打开,真实 `TimePickerShowCase` repeated mean 提升约 `6.93%` | -| Data Entry | Form | Done | [Form](Form/README.md);已完成低风险结构与生命周期优化,控件级小幅提升,Gallery 结构/分配下降,页面 repeated timing 基本持平 | -| Data Entry | Input | Done | [Input](Input/README.md);TextBox/TextArea 默认 accessory、count、resize 成本按需化;真实 LineEditShowCase 结构/分配下降,repeated timing 基本持平 | +| Data Entry | TimePicker | Done | [TimePicker](TimePicker/README.md);`InfoPickerInput` closed presenter 延迟到首次打开,真实 `TimePickerShowCase` repeated mean 提升约 `6.93%`;increment correction temp list/enumerable `1 -> 0`;preferred-width sweep temp enumerable/array `3 -> 0` | +| Data Entry | Form | Done | [Form](Form/README.md);已完成低风险结构与生命周期优化,控件级小幅提升,Gallery 结构/分配下降,页面 repeated timing 基本持平;validate collections exact upper-bound capacity | +| Data Entry | Input | Done | [Input](Input/README.md);TextBox/TextArea 默认 accessory、count、resize 成本按需化;本轮补充 TextBox compact-space / feedback、TextArea validation / feedback repeated writes `1 -> 0`;真实 LineEditShowCase 结构/分配下降,repeated timing 基本持平 | | Data Entry | LineEdit | Done | 已完成控件级优化与 Gallery 实测 | -| Data Entry | Mentions | Done | [Mentions](Mentions/README.md);closed popup content 与 OptionsSource cache 已按需化,Gallery repeated mean 小幅改善,alloc 下降约 `7.80%` | -| Data Entry | NumberUpDown / NumericUpDown | Done | [NumericUpDown](NumericUpDown/README.md);默认 clear/accessory 按需创建,`NumberUpDownShowCase` cold mean 提升约 `10.97%`,repeated mean 提升约 `3.97%` | +| Data Entry | Mentions | Done | [Mentions](Mentions/README.md);closed popup content 与 OptionsSource cache 已按需化,OptionsSource cache list exact capacity,Gallery repeated mean 小幅改善,alloc 下降约 `7.80%` | +| Data Entry | NumberUpDown / NumericUpDown | Done | [NumericUpDown](NumericUpDown/README.md);默认 clear/accessory 按需创建;本轮补充 repeated compact-space / validation writes `1 -> 0`;`NumberUpDownShowCase` cold mean 提升约 `10.97%`,repeated mean 提升约 `3.97%` | | Data Entry | OptionButtonGroup | Done | [OptionButtonGroup](OptionButtonGroup/README.md);默认隐藏 icon/wave 成本已按需化,`RadioButtonShowCase` repeated mean 提升约 `15.99%` | | Data Entry | RadioButton | Done | [RadioButton](RadioButton/README.md);`RadioIndicator` wave 默认不预创建,`RadioButtonShowCase` visuals `689 -> 668`,Wave `22 -> 1` | | Data Entry | Rate | Done | [Rate](Rate/README.md);默认星形、焦点虚线框和全局 input 订阅成本已收敛,`RateShowCase` repeated mean 提升约 `10.96%` | -| Data Entry | Select | Done | [Select](Select/README.md);`SelectShowCase` repeated mean `220.31ms -> 143.76ms`,alloc `28027.36KB -> 23904.61KB`;closed popup/list/accessory hidden cost 已按需化 | +| Data Entry | Select | Done | [Select](Select/README.md);`SelectShowCase` repeated mean `220.31ms -> 143.76ms`,alloc `28027.36KB -> 23904.61KB`;closed popup/list/accessory hidden cost 已按需化;本轮补充 `AbstractSelect` repeated compact-space / validation / form-feedback writes `1 -> 0`,selected-options sync/copy LINQ materialization `6 -> 0` | | Data Entry | Slider | Done | [Slider](Slider/README.md);普通 Slider 的 `PART_EndThumb` 按需创建,marks cache 与全局 input 订阅已收敛,Gallery repeated mean 提升约 `27.06%` | | Data Entry | ToggleSwitch | Done (interaction structural) | [Switch](Switch/README.md);`IsChecked` 切换 measure invalidations `1000 -> 0`,页面加载不作为主收益 | | Data Entry | TreeSelect | Done | [TreeSelect](TreeSelect/README.md);共享 `SelectHandle` 单 presenter 化,真实 `TreeSelectShowCase` repeated mean 提升约 `12.64%`,visuals `909 -> 858` | -| Data Entry | Transfer | Done | [Transfer](Transfer/README.md);`TargetKeys` lookup 复用 `HashSet`,空 target panel 直接短路,`TreeTransfer` target 递归结果列表 `N -> 1`,`TransferShowCase` repeated mean 提升约 `8.49%`,cold mean 提升约 `10.22%` | -| Data Entry | Upload | Baseline only | [Upload](Upload/README.md);已建立 Gallery baseline,3 个候选无收益/退化已回滚,仅保留测量入口 | +| Data Entry | Transfer | Done | [Transfer](Transfer/README.md);`TargetKeys` lookup 复用 `HashSet`,空 target panel 直接短路,`TreeTransfer` target 递归结果列表 `N -> 1`;本轮补充 key list `Select().ToList()` materialization `6 -> 0`、selected-items sync LINQ chain `1 -> 0`;`TransferShowCase` repeated mean 提升约 `8.49%`,cold mean 提升约 `10.22%` | +| Data Entry | Upload | Baseline + structural-only | [Upload](Upload/README.md);已建立 Gallery baseline,3 个页面级候选无收益/退化已回滚;单图预览 sources wrapper `1 list -> 1 fixed array`,directory upload iterator `1 -> 0`,drop files list exact capacity | | Data Display | Avatar | Done | [Avatar](Avatar/README.md);互斥内容 presenter 与折叠 flyout 已按需创建,Gallery repeated mean 提升约 `23.13%` | -| Data Display | Badge | Done | [Badge](Badge/README.md);hidden zero 和无文本 DotBadge 已按需创建,Gallery visual/alloc 下降,导航 timing 收益有限 | +| Data Display | Badge | Done | [Badge](Badge/README.md);hidden zero 和无文本 DotBadge 已按需创建,Badge/Ribbon preset color match `ToLower()` 临时字符串 `15 -> 0`,Gallery visual/alloc 下降,导航 timing 收益有限 | | Data Display | Calendar | Done | [Calendar](Calendar/README.md);按当前 DisplayMode 延迟填充 MonthView/YearView cells,控件级 `Calendar.Default` ms/item 提升约 `24.63%`,Year/Decade 初始模式约 `48%`,Gallery visuals `201 -> 189` | -| Data Display | Card | Done | [Card](Card/README.md);Header/Cover/Actions/Skeleton 已按需创建,Gallery repeated mean 提升约 `35.14%` | -| Data Display | Carousel | Done | [Carousel](Carousel/README.md);nav/progress/PageTransition 已按需创建,真实 Gallery alloc `6113.86KB -> 5213.10KB`,cold `164.35ms -> 149.05ms`,repeated timing 基本持平 | +| Data Display | Card | Done | [Card](Card/README.md);Header/Cover/Actions/Skeleton 已按需创建,Gallery repeated mean 提升约 `35.14%`;CardActionPanel collection add/remove temp lists `1 -> 0` | +| Data Display | Carousel | Done | [Carousel](Carousel/README.md);nav/progress/PageTransition 已按需创建,真实 Gallery alloc `6113.86KB -> 5213.10KB`,cold `164.35ms -> 149.05ms`;indicator refresh list wrapper `1 -> 0`,repeated timing 基本持平 | | Data Display | Collapse | Done | [Collapse](Collapse/README.md);addon、no-arrow、closed content 已按需创建,Gallery repeated mean 提升约 `16.03%` | -| Data Display | Descriptions | Done | [Descriptions](Descriptions/README.md);`DescriptionsShowCase` repeated mean `75.84ms -> 68.64ms`,visuals `579 -> 571`;binding/collection/window 订阅生命周期已补齐验证 | +| Data Display | Descriptions | Done | [Descriptions](Descriptions/README.md);`DescriptionsShowCase` repeated mean `75.84ms -> 68.64ms`,visuals `579 -> 571`;add/remove/apply/layout temp lists `1 -> 0`;binding/collection/window 订阅生命周期已补齐验证 | | Data Display | DataGrid | Partial | [DataGrid](DataGrid/README.md);filter flyout 内容延迟到首次打开,关闭态过滤菜单项 `7 -> 0`,空选 selected-values list allocations `1 -> 0` / empty close;filter indicator 可见性由 `MultiBinding` 收敛到 `TemplateBinding`;column/group/row header 本地 routed handlers 已收敛(column header hover `2/header -> 0/header`,press/release/move + forwarding `5/header -> 0/header`,group header forwarding `2/header -> 0/header`,row header press `1/header -> 0/header`,row group header press `1/header -> 0/header`);column header clip repeated arrange allocation `1/header -> 0/header` after first;group column header view item clip repeated arrange allocation `1/header -> 0/header` after first;column reordering indicator clip repeated arrange allocation `1/indicator -> 0/indicator` after first;DataGrid core input handlers `4/grid -> 0/grid`;rows presenter scroll gesture handler `1/presenter -> 0/presenter`;rows presenter clip geometry repeated arrange allocation `1 -> 0` after first;row bottom grid-line clip repeated arrange allocation `1/row -> 0/row` after first;row hidden clip repeated allocation `1/row -> 0/row` after first;row group header child clip repeated arrange allocation `1/child -> 0/child` after first;row group header frozen child transform repeated arrange allocation `1/child -> 0/child` after first;details presenter clip repeated arrange allocation `1/details -> 0/details` after first;cell clip repeated arrange allocation `1/cell -> 0/cell` after first;DataGridCell header-state binding converter 闭包已移除;special columns 在 `Columns.Clear()` 后释放 grid 事件链;列重排拖拽 indicator 复用 dashed `Pen`;DetailsPresenter measure registration 与 RowExpander details binding cleanup 已完成;分组行头内 row header owner/template 顺序和内置模板 part lookup 已修复,`DataGrid.RowGroups` smoke visual/root `321 -> 315` | | Data Display | Expander | Done | [Expander](Expander/README.md);closed content/addon/no-arrow slot 已按需创建,Gallery repeated mean 提升约 `18.11%` | | Data Display | Empty | Done | [Empty](Empty/README.md);低风险正确性修复与热路径收敛,Gallery repeated mean 小幅改善,visual tree 不变 | -| Data Display | GroupBox | Done | [GroupBox](GroupBox/README.md);header icon presenter 已按需创建,`GroupBoxShowCase` visuals `151 -> 143`,repeated mean `27.79ms -> 26.16ms` | -| Data Display | ImagePreviewer | Done | [ImagePreviewer](ImagePreviewer/README.md);单图关闭态只加载可见封面,完整弹窗 source list 延迟到首次打开;Gallery repeated mean 提升约 `8.79%` | +| Data Display | GroupBox | Done | [GroupBox](GroupBox/README.md);header icon presenter 已按需创建,`GroupBoxShowCase` visuals `151 -> 143`,repeated mean `27.79ms -> 26.16ms`;header mask fallback brush allocations per render `1 -> 0` | +| Data Display | ImagePreviewer | Done | [ImagePreviewer](ImagePreviewer/README.md);单图关闭态只加载可见封面,完整弹窗 source list 延迟到首次打开;Gallery repeated mean 提升约 `8.79%`;fallback source list `1 list -> 1 fixed array`,ItemsSource load list exact capacity | | Data Display | InfoFlyout | Done (correctness + lifecycle) | [InfoFlyout](InfoFlyout/README.md);关闭态不再提前创建 Popup shell,`Flyout=null`/replace/detach 释放 host-to-flyout binding | | Data Display | List | Done | [ListView](ListView/README.md);group/filter refresh 与分页枚举成本已收敛,真实 `ListShowCase` repeated mean 提升约 `4.19%` | | Data Display | QRCode | Done | [QRCode](QRCode/README.md);PNG encode/decode 已移除,icon/status hidden tree 按需创建,`QRCodeShowCase` repeated mean 提升约 `26.81%` | | Data Display | Segmented | Done | [Segmented](Segmented/README.md);无 icon item 的隐藏 `IconPresenter` 已按需化,真实 `SegmentedShowCase` repeated mean 提升约 `18.51%` | | Data Display | Statistic | Done | [Statistic](Statistic/README.md);generated content ownership、CountUp DataContext 清理、TimerStatistic timer 生命周期验证;`StatisticShowCase` repeated alloc 下降约 `3.25%`,visuals `387 -> 384` | -| Data Display | Tag | Done | [Tag](Tag/README.md);非 closable Tag 减少约 `2KB/instance`,Gallery 76 实例约减少 `140KB` 加载分配 | -| Data Display | TextBlock | Done (structural) | [TextBlock](TextBlock/README.md);`HighlightableTextBlock` 段级 Run(N 字符 → ≤ 1 + 2K Run),`SelectableTextBlock` token binding + Cursor 移到 Theme Setter;微基准待 perf 哈纳斯恢复后补 | +| Data Display | Tag | Done | [Tag](Tag/README.md);非 closable Tag 减少约 `2KB/instance`,Gallery 76 实例约减少 `140KB` 加载分配;Tag color name `ToLower()` 临时字符串 `19 -> 0` | +| Data Display | TextBlock | Done (structural) | [TextBlock](TextBlock/README.md);`HighlightableTextBlock` 段级 Run(N 字符 → ≤ 1 + 2K Run),`SelectableTextBlock` token binding + Cursor 移到 Theme Setter;无命中高亮 ranges list `1 -> 0` | | Data Display | Timeline | Done (structural + correctness) | [Timeline](Timeline/README.md);`TimelineIndicator.Render` Pen 缓存,pending item 生命周期和默认/custom icon 显示已验证;页面级 timing 未证明收益 | -| Data Display | TreeView | Done | [TreeView](TreeView/README.md);`NodeSwitcherButton` 单 presenter 化,真实 `TreeViewShowCase` repeated mean 提升约 `35.80%`,visuals `2108 -> 1646`;连接线 repeated render `Pen` allocations `1-2 -> 0 after first`,拖拽 indicator render frame `1 -> 0 after first`,checkbox parent aggregation child scans `2 -> 1` | +| Data Display | TreeView | Done | [TreeView](TreeView/README.md);`NodeSwitcherButton` 单 presenter 化,真实 `TreeViewShowCase` repeated mean 提升约 `35.80%`,visuals `2108 -> 1646`;连接线 repeated render `Pen` allocations `1-2 -> 0 after first`,拖拽 indicator render frame `1 -> 0 after first`,checkbox parent aggregation child scans `2 -> 1`;filter 无命中 ranges list `1 -> 0`,filter selection startup list `1 -> 0`,path traversal / checked reset lists exact capacity | | Data Display | Tooltip | Done (structural-only) | [Tooltip](Tooltip/README.md);`ToolTipService.StartShowTimer` 复用 timer,hover-show 路径少分配 timer/lambda | -| Data Display | Tour | Done (structural) | [Tour](Tour/README.md);`TourStepsView` 同生命周期 `RelayBind` 改为 template-priority direct binding,页面加载小幅改善,popup 打开场景待专项 harness | +| Data Display | Tour | Done (structural) | [Tour](Tour/README.md);`TourStepsView` 同生命周期 `RelayBind` 改为 template-priority direct binding,页面加载小幅改善;CustomActions sync LINQ/list wrapper `1 -> 0`;popup 打开场景待专项 harness | | Feedback | Alert | Done | [MarqueeLabel](MarqueeLabel/README.md);默认非 marquee Alert 不再承担隐藏 MarqueeLabel 成本,`AlertShowCase` repeated mean 提升约 `16.43%` | | Feedback | Drawer | Done | [Drawer](Drawer/README.md);关闭态 `OpenOn`/`SizeChanged` 已按需化,detach/open/close 生命周期已补齐验证;Gallery repeated mean `24.24ms -> 22.87ms` | -| Feedback | Message | Done | [Message](Message/README.md);manager 按需创建,auto-close timer、MessageClosed、OnClose 生命周期已补齐,Gallery repeated mean 提升约 `7.91%` | +| Feedback | Message | Done | [Message](Message/README.md);manager 按需创建,auto-close timer、MessageClosed、OnClose 生命周期已补齐,Gallery repeated mean 提升约 `7.91%`;excess visible messages temp list `1 -> 0` | | Feedback | MessageBox | Done | [MessageBox](MessageBox/README.md);同生命周期 binding 改为 `[!]`,loading skeleton 按需创建,状态/释放验证已补齐 | -| Feedback | Modal | Done | [Dialog](Dialog/README.md);closed MessageBox 内部 Dialog 已按需创建,mask/resizer 生命周期已验证;Gallery 结构和分配下降,repeated timing 未证明提升 | -| Feedback | Notification | Done | [Notification](Notification/README.md);manager/card 生命周期补齐,7 个 Gallery manager 与 progress bar 按需创建,Gallery repeated mean 提升约 `22.24%` | +| Feedback | Modal | Done | [Dialog](Dialog/README.md);closed MessageBox 内部 Dialog 已按需创建,mask/resizer 生命周期已验证;ButtonBox 缺省角色列表 fallback allocations `14 -> 0`,group/sync lists exact capacity;Gallery 结构和分配下降,repeated timing 未证明提升 | +| Feedback | Notification | Done | [Notification](Notification/README.md);manager/card 生命周期补齐,7 个 Gallery manager 与 progress bar 按需创建,Gallery repeated mean 提升约 `22.24%`;progress visibility converter temp list `1 -> 0`,cleanup queue/set exact default capacity | | Feedback | PopupConfirm | Done (correctness + lifecycle) | [PopupConfirm](PopupConfirm/README.md);静态 cancel/content slot 保留并通过 `IsVisible=false` 隐藏,本地 Click handlers `2 -> 0`,隐藏 cancel direct-click side effects `2 -> 0`;页面 timing 不作为本轮收益证明 | -| Feedback | ProgressBar | Done | [ProgressBar](ProgressBar/README.md);status icon 按需创建,真实 `ProgressBarShowCase` repeated mean `154.17ms -> 111.26ms`,runtime visuals `1389 -> 977` | +| Feedback | ProgressBar | Done | [ProgressBar](ProgressBar/README.md);status icon 按需创建,真实 `ProgressBarShowCase` repeated mean `154.17ms -> 111.26ms`,runtime visuals `1389 -> 977`;Circle/Dashboard repeated render pen allocations `3 -> 0 after first`,inner label color temp list `1 -> 0`,same-color brush allocation `1 -> 0` | | Feedback | Result | Done | [Result](Result/README.md);状态 icon presenter 与错误码 Svg 互斥按需创建,`ResultShowCase` repeated mean 提升约 `9.21%` | | Feedback | Skeleton | Done (structural lifecycle + correctness) | [Skeleton](Skeleton/README.md);重复 class handler、inactive animation、paragraph line rebuild/follow 生命周期已修复;页面 timing 不作为主收益 | | Feedback | Spin | Done (structural lifecycle + correctness) | [Spin](Spin/README.md);hidden `SpinIndicator` animation 延迟到可见时创建,motion 参数变化后重建已 materialized 动画;页面 timing 不作为主收益 | diff --git a/docs/performances/Tag/README.md b/docs/performances/Tag/README.md index 3f2b7a0bc..0ea053062 100644 --- a/docs/performances/Tag/README.md +++ b/docs/performances/Tag/README.md @@ -98,7 +98,7 @@ else if (change.Property == IsClosableProperty) ## 3. 不在本轮范围 -- `SetupTagColorInfo` 的 `foreach (var entry in PresetColorMap)` 线性查找(按 string 匹配 12 个 preset color 项):每次 TagColor 变化做一次。Gallery 76 tags × 16 字符串比较 ≈ 1200 次 ToLower 比较;可改成预构建 `Dictionary`,但单 tag 每次 < 0.05 ms,变异范围内难以验证收益。**留作 follow-up**,先看 Gallery 实测是否成为瓶颈。 +- `SetupTagColorInfo` 的 string match 已追加低风险结构优化:去掉输入和枚举名的 `ToLower()` 临时字符串,改为大小写忽略比较;保持只按枚举名称匹配,不接受数字枚举值。 - `SetupPresetColorMap` / `SetupStatusColorMap` 已经是 static 全局 dict + `force` 参数,theme 切换时重建。结构 OK,未改。 --- @@ -160,4 +160,16 @@ dotnet build src/AtomUI.Controls/AtomUI.Controls.csproj -c Debug --framework net - 单 Tag 每实例 KB 收益(~2 KB)相对 Tier 0 的 TextBlock / Popup 较小。Tag 优化的真正杠杆在 **Gallery 累积量**:76 实例 × ~70 非 closable × 2 KB ≈ 140 KB。Select 多选场景按 SelectedItems 数量再乘倍。 - ms/item 在 sub-millisecond 量级,单 scenario 测得的变异范围内 ±20%;KB/item 是更可靠的指标。 -- `SetupTagColorInfo` 字符串查找微优化属下一轮。 + +--- + +## 8. 追加结构优化:颜色名匹配临时字符串 + +`SetupTagColorInfo()` 的 preset/status 颜色名匹配不再对输入和每个 enum name 做 `ToLower()`;改为 `StringComparison.OrdinalIgnoreCase`。同时命中后直接使用 `entry.Value`,避免命中项再查一次 dictionary。颜色解析优先级保持:preset → status → custom color。 + +| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 | +| --- | ---: | ---: | --- | ---: | --- | +| Tag color name `ToLower()` allocations / worst-case color parse | 19 strings | 0 strings | `(19 - 0) / 19` | 100.00% | 结构收益;14 preset + 4 status + input 均不再 lower | +| Matched preset/status dictionary lookup / color parse | 1 extra lookup | 0 extra lookups | `(1 - 0) / 1` | 100.00% | 结构收益;命中项直接用 enumerator value | + +说明:这是 TagColor 变化路径的结构性收益;不声明页面加载 timing 提升。 diff --git a/docs/performances/TextBlock/README.md b/docs/performances/TextBlock/README.md index cdde13388..9ddc34660 100644 --- a/docs/performances/TextBlock/README.md +++ b/docs/performances/TextBlock/README.md @@ -290,3 +290,16 @@ rg "new Cursor\(StandardCursorType.Ibeam\)" --type cs src/AtomUI.Desktop.Control | `src/AtomUI.Desktop.Controls/AtomUI.Desktop.Controls.csproj` | `InternalsVisibleTo` 添加 AtomUI.Performance | 总计:3 个生产文件改动 + 1 个生产新主题 + 1 个 ResourceInclude;哈纳斯辅助改动列在表后段供哈纳斯恢复时一并处理。 + +--- + +## 7. 追加结构优化:无命中高亮 fast path + +`HighlightableTextBlock.NotifyBuildFilterHighlightRuns()` 在无命中时先用 `IndexOf` 判定,避免进入 `CalculateHighlightRanges()` 后创建空 ranges list;命中时把第一次查找结果传入 ranges 计算,避免重复查找首个命中。 + +| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 | +| --- | ---: | ---: | --- | ---: | --- | +| 无命中高亮 ranges list / rebuild | 1 list | 0 list | `(1 - 0) / 1` | 100.00% | 无命中搜索不再分配空 ranges list | +| 命中高亮首个命中查找 / rebuild | 2 次 | 1 次 | `(2 - 1) / 2` | 50.00% | 首个命中不再重复搜索 | + +说明:这是结构性收益,只证明高亮 rebuild 路径分配和重复搜索下降;没有声明页面加载 timing 提升。 diff --git a/docs/performances/TimePicker/README.md b/docs/performances/TimePicker/README.md index 3a4ef5be3..8ccec4674 100644 --- a/docs/performances/TimePicker/README.md +++ b/docs/performances/TimePicker/README.md @@ -173,3 +173,17 @@ dotnet run --project tools/performances/AtomUI.GalleryPerformance/AtomUI.Gallery ``` 结果:`DatePickerPresenter=0`、`RangePickerPresenter=0`、`TimeView=0`、`DateTimePanel=0`,runtime visuals `1840`。 + +--- + +## 5. 追加结构优化:输入框 preferred-width 计算 + +共享 `DateTimeUtils.CalculateWidestFormattedDateTimeSize()` 的月份、星期和 AM/PM 样本扫描改为 `for` loop / index 分支,不再为每次宽度计算创建 `Enumerable.Range` 或小时数组。该工具路径被 DatePicker / TimePicker preferred input width 复用。 + +| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 | +| --- | ---: | ---: | --- | ---: | --- | +| 月份扫描 enumerable / widest datetime width calculation | 1 | 0 | `(1 - 0) / 1` | 100.00% | 结构收益;按需月份扫描不再分配 enumerable | +| 星期扫描 enumerable / widest datetime width calculation | 1 | 0 | `(1 - 0) / 1` | 100.00% | 结构收益;按需星期扫描不再分配 enumerable | +| AM/PM 小时样本数组 / widest datetime width calculation | 1 array | 0 arrays | `(1 - 0) / 1` | 100.00% | 结构收益;小时样本改为索引分支 | + +说明:这是 preferred-width 计算路径的结构性收益;本轮不声明 `TimePickerShowCase` 页面导航 timing 因此提升。 diff --git a/docs/performances/Tour/README.md b/docs/performances/Tour/README.md index 65a97455d..5a94052d7 100644 --- a/docs/performances/Tour/README.md +++ b/docs/performances/Tour/README.md @@ -102,3 +102,17 @@ dotnet run --project tools/performances/AtomUI.GalleryPerformance/AtomUI.Gallery ## 4. 后续 如需继续优化 Tour,下一步应补一个 Gallery interaction harness:导航到 `TourShowCase` 后触发第一个 `Begin Tour`,测首次打开、下一步切换、关闭。仅用页面导航数据不足以评估 popup 内容和 `TourLayer` 热路径。 + +--- + +## 5. 追加结构优化:CustomActions 快照 + +`Tour.HandleCustomActionsChanged()` 和 `OnApplyTemplate()` 过去各自执行 `CustomActions.Cast().ToList()`,并重复一段 `ITourAction.NotifyAttached()` 逻辑。现在统一走 `PrepareCustomActions()`,用 `Control[]` 快照并复用 attach 通知逻辑。 + +| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 | +| --- | ---: | ---: | --- | ---: | --- | +| CustomActions LINQ iterator / sync | 1 | 0 | `(1 - 0) / 1` | 100.00% | 同步自定义按钮时不再创建 `Cast` iterator | +| CustomActions list wrapper / sync | 1 list | 0 list | `(1 - 0) / 1` | 100.00% | 改为数组快照,避免 List 增长逻辑和 LINQ materialize | +| 重复 attach 同步代码块 | 2 处 | 1 处 | `(2 - 1) / 2` | 50.00% | 降低后续行为漂移风险 | + +说明:这是 popup/template 同步路径的结构性收益;没有页面加载 timing 百分比声明。 diff --git a/docs/performances/Transfer/README.md b/docs/performances/Transfer/README.md index 806d6d651..0e961cbe6 100644 --- a/docs/performances/Transfer/README.md +++ b/docs/performances/Transfer/README.md @@ -13,6 +13,8 @@ - `TargetKeys` 为空时,target panel 直接使用空集合,避免再扫描完整 `ItemsSource`。 - 修正 target-only filter 刷新时的变更检测顺序:先比较旧 `TargetViewSource`,再写入新值。 - 本次增量继续收敛 `TreeTransfer` 目标侧递归收集:去掉顶层 `ToList()` 源复制,并把每个节点递归返回一个临时结果 `List` 改为单个结果列表累积。 +- 本次追加收敛 `TransferListView` / `TransferTreeView` 的 selected keys/items 同步:去掉 `Cast/Select/Where/ToList/ToHashSet` materialization 链,改为按已知 Count 预分配并显式拷贝。 +- `AbstractTransfer` / `ListTransfer` / `TreeTransfer` 共享 key list/set 构造 helper,目标 key 集合、selection changed key 列表、transfer 后 `TargetKeys` 列表都按 Count 预分配。 没有移动任何主题视觉到 C# 动态创建,没有新增 `_ignoreXxx` 标志位、disposable、事件订阅或模板结构。 @@ -38,6 +40,11 @@ | `TreeTransfer` target 刷新顶层 source copy | 1 list / refresh | 0 list / refresh | `(1 - 0) / 1` | 100.00% | 去掉 `ItemsSource.Cast().ToList()` | | `TreeTransfer` target 递归结果列表 | N lists / N visited nodes | 1 list / refresh | `(N - 1) / N` | N 越大越接近 100% | 从每个递归层返回临时 `List` 改为单列表累积 | | `TreeTransfer` target filter 后数组 | 1 array / filtered refresh | 0 array / filtered refresh | `(1 - 0) / 1` | 100.00% | 过滤判断并入收集流程,目标列表直接作为 `TargetViewSource` | +| Transfer key list `Select().ToList()` materialization | 6 iterator chains / refresh | 0 iterator chains / refresh | `(6 - 0) / 6` | 100.00% | `sourceItemKeys` / `targetItemKeys` 显式构造并预分配 | +| Transfer target key `ToHashSet()` materialization | 3 LINQ materializations / refresh/sync | 0 LINQ materializations / refresh/sync | `(3 - 0) / 3` | 100.00% | `BuildTargetKeySet` / `BuildEntityKeySet` 保留 HashSet 但去掉 LINQ 链 | +| `TransferListView` selected-items sync LINQ chain | 1 `Cast+Where+ToList` / sync | 0 LINQ chains / sync | `(1 - 0) / 1` | 100.00% | 保留类型转换 fail-fast,结果列表按 `SelectedKeys.Count` 预分配 | +| `TransferTreeView` node snapshot list growth | dynamic growth / count+mask pass | exact capacity / count+mask pass | structural | 结构收益 | `Items.Count` 已知时直接预分配 | +| transfer 后 `TargetKeys` 列表 | LINQ `ToList()` / transfer | explicit exact list / transfer | structural | 结构收益 | `HashSet.Count` 已知时直接构造结果列表 | > 说明:这是可由代码结构直接证明的分配次数下降;本次未新增 Gallery timing 对比,因此不把它写成页面耗时提升。 @@ -187,7 +194,7 @@ dotnet run --project tools/performances/AtomUI.Performance/AtomUI.Performance.cs | 新增订阅 / timer / reparented element | 0 | | axaml 节点搬到 C# 动态创建 | 否 | | Theme Static Rule | 未触发 | -| 生产文件范围 | 2 个文件,均在 `Transfer` | +| 生产文件范围 | 5 个文件,均在 `Transfer` | --- diff --git a/docs/performances/TreeView/README.md b/docs/performances/TreeView/README.md index 66c096b66..052739fd0 100644 --- a/docs/performances/TreeView/README.md +++ b/docs/performances/TreeView/README.md @@ -275,3 +275,44 @@ dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI. ## 6. 后续 TreeView 仍有第二类热点:`TreeViewItemHeader.BuildFilterHighlightRuns()` 在搜索高亮中存在 per-character `Run` 模式。该问题只在 filter 交互触发,不属于本轮页面导航主因;下一轮应先建立 filter action 的专项基线,再考虑段级 Run 合并。 + +--- + +## 7. 追加结构优化:TreeView 过滤高亮无命中 fast path + +`TreeViewItemHeader.BuildFilterHighlightRuns()` 在 `HighlightedMatch` 模式下先检查首个命中;无命中时不再创建空 ranges list。同时补齐一个正确性短路:`FilterHighlightWords == ""` 时清空高亮 runs 并返回,避免旧逻辑在 `IndexOf("")` 上无法推进。`HighlightedWhole` 和 `BoldedMatch` 的现有行为保持不变:whole 仍整段着色,bold 仍按旧逻辑作用到所有字符。 + +| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 | +| --- | ---: | ---: | --- | ---: | --- | +| 无命中过滤高亮 ranges list / header rebuild | 1 list | 0 list | `(1 - 0) / 1` | 100.00% | TreeView filter 无命中路径少一次临时集合分配 | +| 命中高亮首个命中查找 / header rebuild | 2 次 | 1 次 | `(2 - 1) / 2` | 50.00% | 首个匹配位置复用,不重复搜索 | +| 空过滤词死循环风险 | 1 条 | 0 条 | `(1 - 0) / 1` | 100.00% | 正确性修复,不作为性能收益 | + +说明:这是 filter 交互路径的结构性收益;本轮不声称 `TreeViewShowCase` 页面导航 timing 因此提升。 + +--- + +## 8. 追加结构优化:filter 选择闭包启动列表 + +`TreeView.HandleSelectionChanged()` 在 filter 模式下不再先把 selected containers 放进 `startupItems` 临时列表,而是在找到 container 时直接计算并合并闭包。 + +| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 | +| --- | ---: | ---: | --- | ---: | --- | +| filter selection startup list / selection changed | 1 list | 0 list | `(1 - 0) / 1` | 100.00% | 结构收益;filter 选择变化少一次临时集合分配 | +| selected container second pass / selection changed | 1 pass | 0 pass | `(1 - 0) / 1` | 100.00% | 结构收益;闭包在发现 container 时直接合并 | + +说明:这是 filter 交互路径的结构性收益;不声明页面导航 timing 提升。 + +--- + +## 9. 追加结构优化:TreeNodePath 路径列表容量 + +`ExpandTreeViewPath()`、`CollapseTreeViewPath()`、`TraverTreeViewPath()` 的路径列表按 `TreeNodePath.Segments.Count` 预分配,临时展开状态列表也按同一长度预分配。路径内容、展开/恢复顺序不变。 + +| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 | +| --- | ---: | ---: | --- | ---: | --- | +| TreeView pathNodes list growth / path traversal | dynamic capacity | exact capacity | structural | 分配更紧 | 路径节点数已知时避免 List 增长 | +| TreeView path expand-status list growth / temporary traversal | dynamic capacity | exact capacity | structural | 分配更紧 | 展开状态数量与路径长度一致 | +| TreeView checked reset added list growth / checked items reset | dynamic capacity | exact capacity | structural | 分配更紧 | reset 通知 added 列表按 `CheckedItems.Count` 预分配 | + +说明:这是默认展开/选中路径的结构性收益;不声明页面导航 timing 提升。 diff --git a/docs/performances/Upload/README.md b/docs/performances/Upload/README.md index 0c04c338b..c02a8ee84 100644 --- a/docs/performances/Upload/README.md +++ b/docs/performances/Upload/README.md @@ -1,13 +1,13 @@ # Upload 性能评估 > 路线图位置:[`../desktop-controls-optimization-roadmap.md`](../desktop-controls-optimization-roadmap.md) Tier 1 #5 -> 状态:已建立 Gallery baseline;本轮 3 个候选均未通过收益门槛,未保留运行时代码改动。 +> 状态:已建立 Gallery baseline;本轮 3 个页面级候选均未通过收益门槛;后续仅保留单图预览路径的 structural-only 分配收敛。 --- ## 0. 结论 -本轮只保留 `AtomUI.GalleryPerformance` 的 `upload` showcase 映射,方便后续复测。`Upload` 运行时代码恢复到 baseline 状态。 +本轮页面级候选只保留 `AtomUI.GalleryPerformance` 的 `upload` showcase 映射,方便后续复测。后续追加的单图预览路径只减少临时集合,不声明页面级 timing 收益。 | 指标 | baseline | | --- | ---: | @@ -51,6 +51,17 @@ 这是测量入口,不改变产品行为。 +追加 structural-only runtime 改动: + +| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 | +| --- | ---: | ---: | --- | ---: | --- | +| PictureList 单图预览 Sources wrapper / FilePath change | 1 list | 1 fixed array | 结构替换 | 分配更紧 | 单元素 sources 不再创建可增长 List | +| PictureShapeList 单图预览 Sources wrapper / FilePath change | 1 list | 1 fixed array | 结构替换 | 分配更紧 | 单元素 sources 不再创建可增长 List | +| Directory upload file enumeration LINQ iterator / selected directory | 1 iterator | 0 iterators | `(1 - 0) / 1` | 100.00% | 结构收益;目录上传枚举文件时不再经 `.Select(...)` 包装 | +| Drop files list capacity / drop event | dynamic growth | exact capacity | structural | 分配更紧 | 按 `DataTransfer.Items.Count` 预分配 drop 文件列表 | + +说明:这是单图预览路径的结构性收益;不作为 `UploadShowCase` 页面导航收益证明。 + --- ## 3. 复现命令 diff --git a/docs/performances/desktop-controls-optimization-roadmap.md b/docs/performances/desktop-controls-optimization-roadmap.md index 4c6ac4993..fecc6b610 100644 --- a/docs/performances/desktop-controls-optimization-roadmap.md +++ b/docs/performances/desktop-controls-optimization-roadmap.md @@ -37,75 +37,75 @@ | Alert | Done (MarqueeLabel) | 0 | 2 | Alert 自身随 MarqueeLabel 已优化 | | AutoComplete | Done | 1 | 2 | Done | | Avatar | Done | 0 | 1 | Done | -| Badge | Done | 1 | 0 | Done | +| Badge | Done | 1 | 0 | Done;Badge/Ribbon preset color match `ToLower()` 临时字符串 `15 -> 0` | | Breadcrumb | Done (correctness + baseline) | 0 | 0 | separator 传播正确性修复 + 基线;Gallery cold / P95 / alloc 有收益,repeated median 基本持平 | | ButtonSpinner | Done | 2 | 2 | Done | | Buttons | Done | 34 | 0 | 基础控件,已完成 Button/IconButton/SplitButton 等子控件优化 | | Calendar | Done | 1 | 2 | 按当前 DisplayMode 延迟填充 MonthView/YearView cells;详见 [Calendar](Calendar/README.md) | -| Card | Done | 1 | 2 | Done | -| Carousel | Done | 0 | 1 | Done | -| Cascader | Done | 0 | 8 | Done — 也是依赖最多的控件之一 | +| Card | Done | 1 | 2 | Done;CardActionPanel collection add/remove temp lists `1 -> 0` | +| Carousel | Done | 0 | 1 | Done;indicator refresh list wrapper `1 -> 0` | +| Cascader | Done | 0 | 8 | Done — 也是依赖最多的控件之一;level list 虚拟化 context dictionary exact capacity;multi-select sync temp lists/hashsets exact capacity;OptionsSource copy / selected set diff 去 LINQ materialization | | CheckBox | Done | 5 | 1 | Done | | Collapse | Done | 1 | 1 | Done | | ComboBox | Done | 3 | 5 | Done | | DatePicker | Done | 3 | 7 | Done — TimePicker / Calendar / Primitives 链头 | -| Descriptions | Done | 0 | 0 | Done | -| Dialog | Done | 3 | 2 | Done — MessageBox / ImagePreviewer 复用 | +| Descriptions | Done | 0 | 0 | Done;add/remove/apply/layout temp lists `1 -> 0` | +| Dialog | Done | 3 | 2 | Done — MessageBox / ImagePreviewer 复用;ButtonBox 缺省角色列表 fallback allocations `14 -> 0`,group/sync lists exact capacity | | Drawer | Done | 0 | 3 | Done | | Empty | Done | 35 | 1 | Done — **复用面积最大的占位控件** | | Expander | Done | 0 | 1 | Done | -| FloatButton | Done | 0 | 4 | Done | +| FloatButton | Done | 0 | 4 | Done;square group arrange visible-children temp list `1 -> 0`,0/1 visible child line list `1 -> 0`,group add/remove temp lists `1 -> 0` | | Flyouts | Done | 9 | 2 | Done(含 MenuFlyout/TreeViewFlyout/PopupConfirm 共用底) | -| Form | Done | 16 | 2 | Done — 表单控件统一壳,影响所有 Input 系 | -| GroupBox | Done | 0 | 1 | Done | +| Form | Done | 16 | 2 | Done — 表单控件统一壳,影响所有 Input 系;validate collections exact upper-bound capacity | +| GroupBox | Done | 0 | 1 | Done;header mask fallback brush allocations per render `1 -> 0` | | HeaderedContentControl | Pending | 0 | 0 | 抽象壳,无独立基线必要 | -| ImagePreviewer | Done | 1 | 3 | Done | +| ImagePreviewer | Done | 1 | 3 | Done;fallback source list `1 list -> 1 fixed array`,ItemsSource load list exact capacity | | Input | Done | 13 | 4 | Done — LineEdit/TextArea/SearchEdit 父系 | -| ListBox | Done | 4 | 3 | Done(基线在 Cascader/Select 优化里覆盖) | -| ListView | Done | 2 | 4 | Done | +| ListBox | Done | 4 | 3 | Done(基线在 Cascader/Select 优化里覆盖);虚拟化 context dictionary exact capacity | +| ListView | Done | 2 | 4 | Done;虚拟化 context dictionary exact capacity | | MarqueeLabel | Done | 1 | 0 | Done — Alert 共用 | -| Mentions | Done | 0 | 3 | Done | +| Mentions | Done | 0 | 3 | Done;OptionsSource cache list exact capacity | | Menu | Done | 7 | 5 | Done | -| Message | Done | 9 | 0 | Done | +| Message | Done | 9 | 0 | Done;excess visible messages temp list `1 -> 0` | | MessageBox | Done | 0 | 2 | Done | -| NavMenu | Done | 1 | 3 | Done | -| Notifications | Done | 1 | 2 | Done | +| NavMenu | Done | 1 | 3 | Done;active indicator repeated scale transform allocation `1 -> 0`;path traversal lists exact capacity | +| Notifications | Done | 1 | 2 | Done;progress visibility converter temp list `1 -> 0`,cleanup queue/set exact default capacity | | NumericUpDown | Done | 0 | 2 | Done | | OptionButtonGroup | Done | 0 | 1 | Done | | Pagination | Done | 2 | 2 | Done | | Popup | Done (structural) | **19** | **0** | 4 条构造器 token binding 迁 ControlTheme(消除 ~76 订阅 + 修复 Tier 1 §7 同优先级碰撞),`ShadowsAwareContainer` 影子 Border 延迟到 `HasBoxShadow`;详见 [Popup](Popup/README.md) | | PopupConfirm | Done (correctness + lifecycle) | 0 | 2 | 本地 Click handlers `2 -> 0`,隐藏 cancel direct-click side effects `2 -> 0`;静态 cancel/content slot 保留,不主张页面 timing 收益 | -| Primitives | Mixed | 16 | 4 | 多类汇总目录:AddOnDecoratedBox / CandidateList / InfoPickerInput 等已部分 Done;`PickerClearUpButton` 本地 Click handlers `1 -> 0`,隐藏 clear direct-click side effects `1 -> 0`;`AddOnDecoratedBox` ContentFrame pointer handlers `4 -> 0` / instance,Select/TreeSelect/Cascader right-addon template 绑定已修复;详见 [Primitives](Primitives/README.md);目录整体仍未做一次性 audit | -| ProgressBar | Done | 1 | 0 | Done | +| Primitives | Mixed | 16 | 4 | 多类汇总目录:AddOnDecoratedBox / CandidateList / InfoPickerInput 等已部分 Done;`PickerClearUpButton` 本地 Click handlers `1 -> 0`,隐藏 clear direct-click side effects `1 -> 0`;`AddOnDecoratedBox` ContentFrame pointer handlers `4 -> 0` / instance,addon foreground/icon、inner geometry、pointer state unchanged writes 已收敛;`CandidateList.Items20` 键盘候选移动 container checks `20 -> <=2`,max-count enable writes `20 -> 0`,空列表 invalid candidate writes `1 -> 0`,clear-state writes `3 -> 0`;`TreeNodePath` 派生操作 arrays `2 -> 1`、部分 fast path `1 -> 0`、same-instance comparisons `N -> 0`,`MotionGhostControl` repeated shadow writes 已收敛、shadow renderer list exact capacity,`ArrowDecoratedBox` repeated arrow writes 已收敛,`WaveSpiritDecorator` painter configure easing allocations `2 -> 0`、fallback easing allocations `1 -> 0`;`PenUtils` repeated immutable pen allocations `1 -> 0`、dashed pen allocations `2 -> 0`,OptionButtonGroup / FloatButtonSeparatorLayer / ButtonSpinnerHandle / CardActionPanel / TextAreaDecoratedBox / CascaderViewFrame / SliderThumb / SliderTrack / MenuSeparator / CircleProgress / DashboardProgress repeated render `Pen` allocations `1-3 -> 0 after first`,`DateTimeUtils` widest datetime calculation enumerable/array allocations `3 -> 0`,`BorderRenderHelper` dash mode cache stale risk `1 -> 0`;`CompactSpace` wrapper / lookup lists exact capacity,`CompactSpaceItem` repeated transform allocation `1 -> 0` after first、stale transform `1 -> 0`;`RangeInfoPickerInput` unchanged indicator writes `6 -> 0`,`InfoPickerInput` / `TextBox` / `TextArea` / `ButtonSpinner` / `NumericUpDown` / `AbstractSelect` open/close、clear、arrow、validation、compact-space、form-feedback unchanged writes `1 -> 0`,Select/TreeSelect/Cascader right-addon template 绑定已修复;详见 [Primitives](Primitives/README.md);目录整体仍未做一次性 audit | +| ProgressBar | Done | 1 | 0 | Done;Circle/Dashboard repeated render pen allocations `3 -> 0 after first`,inner label color temp list `1 -> 0`,same-color brush allocation `1 -> 0`;size threshold dictionaries exact capacity | | QRCode | Done | 0 | 3 | Done | | RadioButton | Done | 3 | 1 | Done | | Rate | Done | 0 | 0 | Done | | Result | Done | 8 | 0 | Done | | ScrollViewer | Done (baseline) | 18 | 0 | 仅修了 selector/overlay 正确性,未证明页面级速度提升 | | Segmented | Done | 0 | 1 | Done | -| Select | Done | 12 | 5 | Done — 大型聚合控件 | +| Select | Done | 12 | 5 | Done — 大型聚合控件;本轮补充 `AbstractSelect` repeated compact-space / validation / form-feedback writes `1 -> 0`,selected-options sync/copy LINQ materialization `6 -> 0` | | Separator | Done (structural-only) | 4 | 1 | `AbstractSeparator.Render` Pen 缓存(SKILL Cost Model 强制);详见 [Separator](Separator/README.md)。Headless bench 解析力 < 0.1 KB | | Skeleton | Done (structural lifecycle + correctness) | 3 | 0 | 重复 class handler、inactive animation、paragraph line rebuild/follow 生命周期已修复;详见 [Skeleton](Skeleton/README.md) | -| Slider | Done | 0 | 1 | Done | -| Space | Done | 11 | 0 | Done | +| Slider | Done | 0 | 1 | Done;marks render lists exact capacity | +| Space | Done | 11 | 0 | Done;本轮补充 `CompactSpaceItem` / `CompactSpaceAddOn` / `TextBox` / `ButtonSpinner` / `NumericUpDown` repeated compact-space writes `1 -> 0`,`CompactSpaceItem` repeated transform allocation `1 -> 0` after first,real-children position temp list `1 -> 0`,wrapper / lookup lists exact capacity | | Spin | Done (structural lifecycle + correctness) | 8 | 0 | hidden `SpinIndicator` animation 延迟到可见时创建;motion 参数变化后重建已 materialized 动画;详见 [Spin](Spin/README.md) | | SplitView | Done (structural lifecycle) | 0 | 0 | 初始 pane transition 延迟到第一次 runtime open/close;详见 [SplitView](SplitView/README.md) | -| Splitter | Done | 0 | 1 | hidden collapse icon 不再构造/保留 PathIcon,lazy preview transform 复用,动态 children add/remove temp objects `3 -> 1`,panel refresh temp objects `2 -> 0`,resize event sizes temp objects `3 -> 1`;详见 [Splitter](Splitter/README.md) | +| Splitter | Done | 0 | 1 | hidden collapse icon 不再构造/保留 PathIcon,lazy preview transform 复用,动态 children add/remove temp objects `3 -> 1`,panel refresh temp objects `2 -> 0`,resize event sizes temp objects `3 -> 1`,panel size compute temp lists `2 -> 0`;详见 [Splitter](Splitter/README.md) | | Statistic | Done | 0 | 1 | generated content ownership、CountUp DataContext 清理、TimerStatistic attach-gated timer 生命周期;详见 [Statistic](Statistic/README.md) | | Steps | Done (structural-only) | 1 | 1 | 修复 `ConfigureItemsPanel` 未清理 `ColumnDefinitions`;详见 [Steps](Steps/README.md) | | Switch (ToggleSwitch) | Done (interaction structural) | 0 | 0 | `IsChecked` 切换不再触发 measure invalidation;详见 [Switch](Switch/README.md) | -| TabControl | Done (render structural) | 1 | 1 | 默认 hidden icon/close slot 按需化;`TabControlShowCase` repeated mean `170.75ms -> 157.35ms`;tab strip border repeated render `Pen` allocations `1/control -> 0 after first`;详见 [TabControl](TabControl/README.md) | -| Tag | Done | 10 | 1 | `AbstractTag.SetupDefaultCloseIcon` 门控到 `IsClosable=true`,非 closable Tag 减少 ~2 KB/instance × Gallery 76 实例 ≈ 140 KB;详见 [Tag](Tag/README.md) | +| TabControl | Done (render structural) | 1 | 1 | 默认 hidden icon/close slot 按需化;`TabControlShowCase` repeated mean `170.75ms -> 157.35ms`;tab strip border repeated render `Pen` allocations `1/control -> 0 after first`;edge indicator visibility temp list `1 -> 0`;详见 [TabControl](TabControl/README.md) | +| Tag | Done | 10 | 1 | `AbstractTag.SetupDefaultCloseIcon` 门控到 `IsClosable=true`,非 closable Tag 减少 ~2 KB/instance × Gallery 76 实例 ≈ 140 KB;preset/status color maps exact capacity;color name `ToLower()` 临时字符串 `19 -> 0`;详见 [Tag](Tag/README.md) | | TextBlock | Done (structural) | 33 | 0 | `HighlightableTextBlock` 段级 Run + `SelectableTextBlock` token binding/Cursor → Theme Setter;详见 [TextBlock](TextBlock/README.md)。微基准待哈纳斯恢复 | -| TimePicker | Done | 1 | 4 | `InfoPickerInput` closed presenter 延迟到首次打开;`TimePickerShowCase` repeated mean `148.79ms -> 138.48ms`;详见 [TimePicker](TimePicker/README.md) | +| TimePicker | Done | 1 | 4 | `InfoPickerInput` closed presenter 延迟到首次打开;`TimePickerShowCase` repeated mean `148.79ms -> 138.48ms`;increment correction temp list/enumerable `1 -> 0`;preferred-width sweep temp enumerable/array `3 -> 0`;详见 [TimePicker](TimePicker/README.md) | | Timeline | Done (structural + correctness) | 0 | 1 | `TimelineIndicator.Render` Pen 缓存;pending item 生命周期/default icon 修复;详见 [Timeline](Timeline/README.md) | | Tooltip | Done (structural-only) | 5 | 1 | `ToolTipService.StartShowTimer` 复用 DispatcherTimer + method-group Tick handler;headless bench 不能触发 Show 流程,real-world hover 场景待 Gallery 实测;详见 [Tooltip](Tooltip/README.md) | | TopLevel | n/a | 0 | 0 | 占位目录无主题/类 | | Tour | Done (structural) | 0 | 4 | `TourStepsView` 同生命周期 binding 收敛;详见 [Tour](Tour/README.md)。popup 打开场景待专项 harness | -| Transfer | Done | 0 | 7 | `TargetKeys` lookup 复用 `HashSet`,空 target panel 直接短路;`TreeTransfer` target 递归结果列表 `N -> 1`;`TransferShowCase` repeated mean `242.72ms -> 222.12ms`;详见 [Transfer](Transfer/README.md) | +| Transfer | Done | 0 | 7 | `TargetKeys` lookup 复用 `HashSet`,空 target panel 直接短路;`TreeTransfer` target 递归结果列表 `N -> 1`;本轮补充 key list `Select().ToList()` materialization `6 -> 0`、selected-items sync LINQ chain `1 -> 0`;`TransferShowCase` repeated mean `242.72ms -> 222.12ms`;详见 [Transfer](Transfer/README.md) | | TreeSelect | Done | 1 | 5 | 共享 `SelectHandle` 三套 indicator 收敛到单 presenter;详见 [TreeSelect](TreeSelect/README.md) | -| TreeView | Done | 4 | 5 | `NodeSwitcherButton` 5 个 icon presenter 收敛到 1 个;连接线 repeated render `Pen` allocations `1-2 -> 0 after first`,拖拽 indicator render frame `1 -> 0 after first`,checkbox parent aggregation child scans `2 -> 1`;详见 [TreeView](TreeView/README.md) | -| Upload | Baseline only | 0 | 4 | 已建立 baseline,3 个候选无收益/退化已回滚;详见 [Upload](Upload/README.md) | +| TreeView | Done | 4 | 5 | `NodeSwitcherButton` 5 个 icon presenter 收敛到 1 个;连接线 repeated render `Pen` allocations `1-2 -> 0 after first`,拖拽 indicator render frame `1 -> 0 after first`,checkbox parent aggregation child scans `2 -> 1`;filter selection startup list `1 -> 0`,path traversal / checked reset lists exact capacity;详见 [TreeView](TreeView/README.md) | +| Upload | Baseline + structural-only | 0 | 4 | 已建立 baseline,3 个页面级候选无收益/退化已回滚;单图预览 sources wrapper `1 list -> 1 fixed array`,directory upload iterator `1 -> 0`,drop files list exact capacity;详见 [Upload](Upload/README.md) | | Window | Demoted to Tier 2 | 13 | 1 | Fan-in 13 是派生类计数,非实例化次数。实测 Gallery 一次会话 ≤ 1 常驻 + 5-10 dialog/drawer,未通过 SKILL Tier 1 §13 资格门槛。降级到 [T2.8](#tier-2--单功能-pending-控件) 与 macOS Metal jitter 协同时再处理。 | | WindowTitleBar | Done (structural) | 3 | 1 | `CaptionButton` normal/checked 两套 `IconPresenter` 收敛为单 presenter;详见 [WindowTitleBar](WindowTitleBar/README.md) | @@ -161,7 +161,7 @@ DataGrid 是最重的复合控件,独立子表才是合理的优化粒度。 | Transfer | 13 | Done | Buttons, CheckBox, Empty, Flyouts, Input, ListView, Menu, Pagination, Select, Tag, TreeView | | Primitives | 13 | 部分 | Buttons, Empty, Flyouts, Form, Input, ListBox, Popup, ScrollViewer, Select, Space, TextBlock, Window | | AutoComplete | 12 | Done | ComboBox, Empty, Form, Input, Menu, Message, Popup, Primitives, Result, Select, Space, Window | -| Upload | 10 | Baseline only | Buttons, Empty, Form, ImagePreviewer, Result, Select, Tag, TextBlock | +| Upload | 10 | Baseline + structural-only | Buttons, Empty, Form, ImagePreviewer, Result, Select, Tag, TextBlock | | Mentions | 10 | Done | AutoComplete, Empty, Form, Input, Message, Popup, Primitives, Result, Window | | DatePicker | 10 | Done | Buttons, Calendar, Empty, Form, Popup, Primitives, Space, TimePicker | | ComboBox | 10 | Done | ButtonSpinner, Buttons, Empty, Form, Input, Popup, Primitives, Window | @@ -200,12 +200,12 @@ DataGrid 是最重的复合控件,独立子表才是合理的优化粒度。 | # | 控件 | Fan-Out | 主要依赖 | 现状 | SKILL 提示 | | --- | --- | --- | --- | --- | --- | -| T1.1 | TreeView | 13 | CheckBox, Empty, Flyouts, Form, Menu, Message, NavMenu, Popup, RadioButton, Result, Spin | **Done** | Templates / visual tree:`NodeSwitcherButton` 单 presenter 化;`TreeViewShowCase` repeated mean `332.44ms -> 213.41ms`,visuals `2108 -> 1646`;连接线和拖拽 indicator render pen 复用;checkbox parent aggregation child scans `2 -> 1`;详见 [TreeView](TreeView/README.md) | -| T1.2 | Transfer | 13 | Buttons, CheckBox, Empty, Flyouts, Input, ListView, Menu, Pagination, Select, Tag, TreeView | **Done** | `TargetKeys` lookup 复用 `HashSet`,空 target panel 直接短路;`TreeTransfer` target 递归结果列表 `N -> 1`;`TransferShowCase` repeated mean `242.72ms -> 222.12ms`,visuals/logical 不变;详见 [Transfer](Transfer/README.md) | +| T1.1 | TreeView | 13 | CheckBox, Empty, Flyouts, Form, Menu, Message, NavMenu, Popup, RadioButton, Result, Spin | **Done** | Templates / visual tree:`NodeSwitcherButton` 单 presenter 化;`TreeViewShowCase` repeated mean `332.44ms -> 213.41ms`,visuals `2108 -> 1646`;连接线和拖拽 indicator render pen 复用;checkbox parent aggregation child scans `2 -> 1`;filter 无命中 ranges list `1 -> 0`,filter selection startup list `1 -> 0`,path traversal / checked reset lists exact capacity;详见 [TreeView](TreeView/README.md) | +| T1.2 | Transfer | 13 | Buttons, CheckBox, Empty, Flyouts, Input, ListView, Menu, Pagination, Select, Tag, TreeView | **Done** | `TargetKeys` lookup 复用 `HashSet`,空 target panel 直接短路;`TreeTransfer` target 递归结果列表 `N -> 1`;本轮补充 key list / selected-items sync LINQ materialization;`TransferShowCase` repeated mean `242.72ms -> 222.12ms`,visuals/logical 不变;详见 [Transfer](Transfer/README.md) | | T1.3 | TreeSelect | 9 | Buttons, CheckBox, Form, Input, Popup, Primitives, Select, TreeView | **Done** | 共享 `SelectHandle` 单 presenter 化;`TreeSelectShowCase` repeated mean `124.72ms -> 108.95ms`,visuals `909 -> 858`;详见 [TreeSelect](TreeSelect/README.md) | | T1.4 | TimePicker | 9 | Buttons, DatePicker, Empty, Form, ListBox, Primitives, Tag, TextBlock | **Done** | `InfoPickerInput` lazy presenter;`TimePickerShowCase` cold mean `464.82ms -> 393.11ms`,repeated mean `148.79ms -> 138.48ms`;详见 [TimePicker](TimePicker/README.md) | -| T1.5 | Upload | 8 | Buttons, Empty, Form, ImagePreviewer, Result, Select, Tag, TextBlock | Baseline only | 已建立 baseline,3 个候选无收益/退化已回滚;运行时代码未保留 | -| T1.6 | Tour | 7 | Buttons, Dialog, Empty, Popup, Primitives, Steps, Tag | Done (structural) | `TourStepsView` 同生命周期 `RelayBind` 改 direct binding;页面加载小幅改善,popup 打开待专项 harness | +| T1.5 | Upload | 8 | Buttons, Empty, Form, ImagePreviewer, Result, Select, Tag, TextBlock | Baseline + structural-only | 已建立 baseline,3 个页面级候选无收益/退化已回滚;单图预览 sources wrapper `1 list -> 1 fixed array`,directory upload iterator `1 -> 0`,drop files list exact capacity | +| T1.6 | Tour | 7 | Buttons, Dialog, Empty, Popup, Primitives, Steps, Tag | Done (structural) | `TourStepsView` 同生命周期 `RelayBind` 改 direct binding;CustomActions sync LINQ/list wrapper `1 -> 0`;页面加载小幅改善,popup 打开待专项 harness | | T1.7 | TabControl | 5 | Buttons, Card, Flyouts, Menu, ScrollViewer | **Done** | Templates / visual tree:默认 tab item hidden icon/close slot 按需化;`TabControlShowCase` visuals `1583 -> 1405`、repeated mean `170.75ms -> 157.35ms`;tab strip border render pen 复用;详见 [TabControl](TabControl/README.md) | ### Tier 2 — 单功能 Pending 控件 @@ -303,11 +303,11 @@ Phase B (Tier 0 Window 系) ───────────────── T2.8 WindowTitleBar done;T2.9 AdornerLayer structural done Phase C (Tier 1 复合) ──────────────────────────────────────── - T1.1 TreeView done → T1.3 TreeSelect / T1.2 Transfer done + T1.1 TreeView done(含 filter 无命中 ranges list 追加收敛)→ T1.3 TreeSelect / T1.2 Transfer done T2.1 Calendar 并行 T1.4 TimePicker done - T1.5 Upload baseline only;3 个候选已回滚 - T2.4 Steps structural-only done → T1.6 Tour structural done + T1.5 Upload baseline + structural-only;3 个页面级候选已回滚 + T2.4 Steps structural-only done → T1.6 Tour structural done(含 CustomActions sync 临时集合追加收敛) T1.7 TabControl 独立 Phase D (Tier 2 单功能) ────────────────────────────────────── diff --git a/src/AtomUI.Controls.Shared/Utils/BorderRenderHelper.cs b/src/AtomUI.Controls.Shared/Utils/BorderRenderHelper.cs index ce6703d97..285590c21 100644 --- a/src/AtomUI.Controls.Shared/Utils/BorderRenderHelper.cs +++ b/src/AtomUI.Controls.Shared/Utils/BorderRenderHelper.cs @@ -18,6 +18,7 @@ internal class BorderRenderHelper private Thickness _borderThickness; private CornerRadius _cornerRadius; private BackgroundSizing _backgroundSizing; + private bool _dashedEnabled; private bool _initialized; private IPen? _cachedPen; @@ -29,6 +30,7 @@ internal class BorderRenderHelper _borderThickness = borderThickness; _cornerRadius = cornerRadius; _backgroundSizing = backgroundSizing; + _dashedEnabled = dashedEnabled; _initialized = true; if (dashedEnabled || @@ -119,9 +121,10 @@ internal class BorderRenderHelper || _borderThickness != borderThickness || _cornerRadius != cornerRadius || _backgroundSizing != backgroundSizing + || _dashedEnabled != (strokeDashArray is { Count: > 0 }) || !_initialized) { - Update(finalSize, borderThickness, cornerRadius, backgroundSizing, strokeDashArray != null); + Update(finalSize, borderThickness, cornerRadius, backgroundSizing, strokeDashArray is { Count: > 0 }); } if (_useComplexRendering) @@ -154,4 +157,4 @@ internal class BorderRenderHelper context.DrawRectangle(background, _cachedPen, rrect, boxShadows); } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls.Shared/Utils/DateTimeUtils.cs b/src/AtomUI.Controls.Shared/Utils/DateTimeUtils.cs index 53dec229c..21857f62a 100644 --- a/src/AtomUI.Controls.Shared/Utils/DateTimeUtils.cs +++ b/src/AtomUI.Controls.Shared/Utils/DateTimeUtils.cs @@ -43,16 +43,14 @@ internal static class DateTimeUtils var needWeekdaySweep = ContainsToken(format, 'd', minRepeat: 3); var needAmPmSweep = ContainsToken(format, 't'); - var months = needMonthSweep ? Enumerable.Range(1, 12) : new[] { 1 }; - var weekdayOffsets = needWeekdaySweep ? Enumerable.Range(0, 7) : new[] { 0 }; - var hours = needAmPmSweep ? new[] { 9, 22 } : new[] { 22 }; - var maxWidth = 0d; var maxHeight = 0d; // 2028-01-02 是星期天,加 offset 可以扫过一周;日 28 保证两位数字,时分秒取宽数字概率更高. - foreach (var month in months) + var monthCount = needMonthSweep ? 12 : 1; + for (var month = 1; month <= monthCount; month++) { - foreach (var offset in weekdayOffsets) + var weekdayCount = needWeekdaySweep ? 7 : 1; + for (var offset = 0; offset < weekdayCount; offset++) { DateTime date; try @@ -64,8 +62,10 @@ internal static class DateTimeUtils continue; } - foreach (var hour in hours) + var hourCount = needAmPmSweep ? 2 : 1; + for (var hourIndex = 0; hourIndex < hourCount; hourIndex++) { + var hour = needAmPmSweep && hourIndex == 0 ? 9 : 22; var sample = new DateTime(date.Year, date.Month, date.Day, hour, 58, 58); var text = formatInfo != null ? sample.ToString(format, formatInfo) : sample.ToString(format); text = ReplaceDigits(text, widestDigit); diff --git a/src/AtomUI.Controls.Shared/WaveSpirit/AbstractWavePainter.cs b/src/AtomUI.Controls.Shared/WaveSpirit/AbstractWavePainter.cs index 576f545df..865852b08 100644 --- a/src/AtomUI.Controls.Shared/WaveSpirit/AbstractWavePainter.cs +++ b/src/AtomUI.Controls.Shared/WaveSpirit/AbstractWavePainter.cs @@ -8,6 +8,8 @@ namespace AtomUI.Controls; internal abstract class AbstractWavePainter : IWaveSpiritPainter { + protected static readonly Easing DefaultLinearEasing = new LinearEasing(); + public Point OriginPoint { get; set; } public IBrush? WaveBrush { get; set; } public WaveSpiritType WaveType { get; protected set; } @@ -32,7 +34,7 @@ internal abstract class AbstractWavePainter : IWaveSpiritPainter public virtual void NotifyBuildOpacityAnimation(Animation animation, AvaloniaProperty targetProperty) { animation.Duration = OpacityMotionDuration; - animation.Easing = OpacityEasingCurve ?? new LinearEasing(); + animation.Easing = OpacityEasingCurve ?? DefaultLinearEasing; animation.Children.Add(new KeyFrame { Setters = @@ -59,4 +61,4 @@ internal abstract class AbstractWavePainter : IWaveSpiritPainter KeyTime = OpacityMotionDuration }); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls.Shared/WaveSpirit/CircleWavePainter.cs b/src/AtomUI.Controls.Shared/WaveSpirit/CircleWavePainter.cs index 542cb65f6..6303e0737 100644 --- a/src/AtomUI.Controls.Shared/WaveSpirit/CircleWavePainter.cs +++ b/src/AtomUI.Controls.Shared/WaveSpirit/CircleWavePainter.cs @@ -58,7 +58,7 @@ internal class CircleWavePainter : AbstractWavePainter public override void NotifyBuildSizeAnimation(Animation animation, AvaloniaProperty targetProperty) { animation.Duration = SizeMotionDuration; - animation.Easing = SizeEasingCurve ?? new LinearEasing(); + animation.Easing = SizeEasingCurve ?? DefaultLinearEasing; animation.Children.Add(new KeyFrame { Setters = @@ -84,4 +84,4 @@ internal class CircleWavePainter : AbstractWavePainter KeyTime = SizeMotionDuration }); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls.Shared/WaveSpirit/PillWavePainter.cs b/src/AtomUI.Controls.Shared/WaveSpirit/PillWavePainter.cs index 55d9c6590..e547c8fb0 100644 --- a/src/AtomUI.Controls.Shared/WaveSpirit/PillWavePainter.cs +++ b/src/AtomUI.Controls.Shared/WaveSpirit/PillWavePainter.cs @@ -52,7 +52,7 @@ internal class PillWavePainter : AbstractWavePainter public override void NotifyBuildSizeAnimation(Animation animation, AvaloniaProperty targetProperty) { animation.Duration = SizeMotionDuration; - animation.Easing = SizeEasingCurve ?? new LinearEasing(); + animation.Easing = SizeEasingCurve ?? DefaultLinearEasing; animation.Children.Add(new KeyFrame { Setters = @@ -79,4 +79,4 @@ internal class PillWavePainter : AbstractWavePainter KeyTime = SizeMotionDuration }); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls.Shared/WaveSpirit/RoundRectWavePainter.cs b/src/AtomUI.Controls.Shared/WaveSpirit/RoundRectWavePainter.cs index d125b254a..88395dabd 100644 --- a/src/AtomUI.Controls.Shared/WaveSpirit/RoundRectWavePainter.cs +++ b/src/AtomUI.Controls.Shared/WaveSpirit/RoundRectWavePainter.cs @@ -92,7 +92,7 @@ internal class RoundRectWavePainter : AbstractWavePainter public override void NotifyBuildSizeAnimation(Animation animation, AvaloniaProperty targetProperty) { animation.Duration = SizeMotionDuration; - animation.Easing = SizeEasingCurve ?? new LinearEasing(); + animation.Easing = SizeEasingCurve ?? DefaultLinearEasing; animation.Children.Add(new KeyFrame { Setters = @@ -119,4 +119,4 @@ internal class RoundRectWavePainter : AbstractWavePainter KeyTime = SizeMotionDuration }); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/Badge/AbstractRibbonBadge.cs b/src/AtomUI.Controls/Badge/AbstractRibbonBadge.cs index 34c4d96b0..05f94fc09 100644 --- a/src/AtomUI.Controls/Badge/AbstractRibbonBadge.cs +++ b/src/AtomUI.Controls/Badge/AbstractRibbonBadge.cs @@ -110,11 +110,11 @@ public abstract class AbstractRibbonBadge : Control private protected void SetupRibbonColor(string colorStr) { - colorStr = colorStr.Trim().ToLower(); + colorStr = colorStr.Trim(); foreach (var presetColor in PresetPrimaryColor.AllColorTypes()) { - if (presetColor.Type.ToString().ToLower() == colorStr) + if (string.Equals(presetColor.Type.ToString(), colorStr, StringComparison.OrdinalIgnoreCase)) { _ribbonBadgeAdorner!.RibbonColor = new SolidColorBrush(presetColor.Color()); return; @@ -223,4 +223,4 @@ public abstract class AbstractRibbonBadge : Control base.OnDetachedFromVisualTree(e); HideAdorner(); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/Badge/BadgeColorUtils.cs b/src/AtomUI.Controls/Badge/BadgeColorUtils.cs index 417bd874f..cb64e7fe2 100644 --- a/src/AtomUI.Controls/Badge/BadgeColorUtils.cs +++ b/src/AtomUI.Controls/Badge/BadgeColorUtils.cs @@ -11,11 +11,11 @@ internal static class BadgeColorUtils { return null; } - colorStr = colorStr.Trim().ToLower(); + colorStr = colorStr.Trim(); foreach (var presetColor in PresetPrimaryColor.AllColorTypes()) { - if (presetColor.Type.ToString().ToLower() == colorStr) + if (string.Equals(presetColor.Type.ToString(), colorStr, StringComparison.OrdinalIgnoreCase)) { return new SolidColorBrush(presetColor.Color()); } @@ -27,4 +27,4 @@ internal static class BadgeColorUtils } return null; } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/FloatButton/FloatButtonSeparatorLayer.cs b/src/AtomUI.Controls/FloatButton/FloatButtonSeparatorLayer.cs index cbaf5f9e4..c35b9db19 100644 --- a/src/AtomUI.Controls/FloatButton/FloatButtonSeparatorLayer.cs +++ b/src/AtomUI.Controls/FloatButton/FloatButtonSeparatorLayer.cs @@ -1,3 +1,4 @@ +using AtomUI.Media; using Avalonia; using Avalonia.Controls; using Avalonia.Layout; @@ -36,6 +37,8 @@ internal class FloatButtonSeparatorLayer : Control set => SetValue(SeparatorBrushProperty, value); } #endregion + + private IPen? _separatorPen; static FloatButtonSeparatorLayer() { @@ -46,11 +49,15 @@ internal class FloatButtonSeparatorLayer : Control { if (SeparatorBrush != null && Lines != null) { - var pen = new Pen(SeparatorBrush); + PenUtils.TryModifyOrCreate(ref _separatorPen, SeparatorBrush, 1.0); + if (_separatorPen is null) + { + return; + } foreach (var line in Lines) { - context.DrawLine(pen, line.Item1, line.Item2); + context.DrawLine(_separatorPen, line.Item1, line.Item2); } } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/Grid/GridColSize.cs b/src/AtomUI.Controls/Grid/GridColSize.cs index 85ca72d33..e92793cd7 100644 --- a/src/AtomUI.Controls/Grid/GridColSize.cs +++ b/src/AtomUI.Controls/Grid/GridColSize.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Globalization; +using System.Text; namespace AtomUI.Controls; @@ -196,30 +197,42 @@ public class GridColSizeConverter : TypeConverter return size.Span.Value.ToString(CultureInfo.InvariantCulture); } - var parts = new List(); + var builder = new StringBuilder(); if (size.Span.HasValue) { - parts.Add(FormattableString.Invariant($"span:{size.Span.Value}")); + AppendPart(builder, "span", size.Span.Value); } if (size.Offset.HasValue) { - parts.Add(FormattableString.Invariant($"offset:{size.Offset.Value}")); + AppendPart(builder, "offset", size.Offset.Value); } if (size.Order.HasValue) { - parts.Add(FormattableString.Invariant($"order:{size.Order.Value}")); + AppendPart(builder, "order", size.Order.Value); } if (size.Push.HasValue) { - parts.Add(FormattableString.Invariant($"push:{size.Push.Value}")); + AppendPart(builder, "push", size.Push.Value); } if (size.Pull.HasValue) { - parts.Add(FormattableString.Invariant($"pull:{size.Pull.Value}")); + AppendPart(builder, "pull", size.Pull.Value); } - return string.Join(",", parts); + return builder.ToString(); } return string.Empty; } + + private static void AppendPart(StringBuilder builder, string name, int value) + { + if (builder.Length > 0) + { + builder.Append(','); + } + + builder.Append(name); + builder.Append(':'); + builder.Append(value.ToString(CultureInfo.InvariantCulture)); + } } diff --git a/src/AtomUI.Controls/OptionButtonGroup/AbstractOptionButtonGroup.cs b/src/AtomUI.Controls/OptionButtonGroup/AbstractOptionButtonGroup.cs index 1e4bde293..c6aa81089 100644 --- a/src/AtomUI.Controls/OptionButtonGroup/AbstractOptionButtonGroup.cs +++ b/src/AtomUI.Controls/OptionButtonGroup/AbstractOptionButtonGroup.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using AtomUI.Controls.Utils; +using AtomUI.Media; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -103,6 +104,7 @@ public abstract class AbstractOptionButtonGroup : SelectingItemsControl, #endregion private readonly BorderRenderHelper _borderRenderHelper = new(); + private IPen? _separatorPen; static AbstractOptionButtonGroup() { @@ -335,7 +337,11 @@ public abstract class AbstractOptionButtonGroup : SelectingItemsControl, { EdgeMode = EdgeMode.Aliased }); - context.DrawLine(new Pen(BorderBrush, BorderThickness.Left), startPoint, endPoint); + PenUtils.TryModifyOrCreate(ref _separatorPen, BorderBrush, BorderThickness.Left); + if (_separatorPen is not null) + { + context.DrawLine(_separatorPen, startPoint, endPoint); + } } if (ButtonStyle == OptionButtonStyle.Outline) @@ -414,4 +420,4 @@ public abstract class AbstractOptionButtonGroup : SelectingItemsControl, { } #endregion -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/Primitives/ArrowDecoratedBox/AbstractArrowDecoratedBox.cs b/src/AtomUI.Controls/Primitives/ArrowDecoratedBox/AbstractArrowDecoratedBox.cs index 21ed8d369..3facf29a2 100644 --- a/src/AtomUI.Controls/Primitives/ArrowDecoratedBox/AbstractArrowDecoratedBox.cs +++ b/src/AtomUI.Controls/Primitives/ArrowDecoratedBox/AbstractArrowDecoratedBox.cs @@ -229,7 +229,7 @@ public abstract class AbstractArrowDecoratedBox : ContentControl, base.OnPropertyChanged(change); if (change.Property == ArrowPositionProperty) { - ArrowDirection = GetDirection(ArrowPosition); + SetArrowDirectionIfChanged(GetDirection(ArrowPosition)); } else if (change.Property == ArrowDirectionProperty) { @@ -290,7 +290,7 @@ public abstract class AbstractArrowDecoratedBox : ContentControl, _contentDecorator = e.NameScope.Get("PART_ContentDecorator"); ArrowIndicatorLayout = e.NameScope.Get("PART_ArrowIndicatorLayout"); ArrowIndicator = e.NameScope.Get("PART_ArrowIndicator"); - ArrowDirection = GetDirection(ArrowPosition); + SetArrowDirectionIfChanged(GetDirection(ArrowPosition)); } private (double, double) GetArrowVertexPoint() @@ -452,13 +452,37 @@ public abstract class AbstractArrowDecoratedBox : ContentControl, ArrowIndicatorLayout.Arrange(new Rect(adjustedOffset, size)); if (ArrowIndicator != null) { - ArrowIndicatorBounds = ArrowIndicator.Bounds; + SetArrowIndicatorBoundsIfChanged(ArrowIndicator.Bounds); } - ArrowIndicatorLayoutBounds = ArrowIndicatorLayout.Bounds; + SetArrowIndicatorLayoutBoundsIfChanged(ArrowIndicatorLayout.Bounds); ArrowPlacementFlipped = false; } + private void SetArrowDirectionIfChanged(Direction direction) + { + if (ArrowDirection != direction) + { + ArrowDirection = direction; + } + } + + private void SetArrowIndicatorBoundsIfChanged(Rect bounds) + { + if (ArrowIndicatorBounds != bounds) + { + ArrowIndicatorBounds = bounds; + } + } + + private void SetArrowIndicatorLayoutBoundsIfChanged(Rect bounds) + { + if (ArrowIndicatorLayoutBounds != bounds) + { + ArrowIndicatorLayoutBounds = bounds; + } + } + protected virtual Point AdjustArrowOffset(Point offset, Size finalSize, Size arrowSize) { return offset; @@ -476,7 +500,10 @@ public abstract class AbstractArrowDecoratedBox : ContentControl, void IArrowAwareShadowMaskInfoProvider.SetArrowOpacity(double opacity) { - ArrowOpacity = opacity; + if (Math.Abs(ArrowOpacity - opacity) > 0.0001) + { + ArrowOpacity = opacity; + } } Rect IArrowAwareShadowMaskInfoProvider.GetArrowIndicatorBounds() @@ -493,4 +520,4 @@ public abstract class AbstractArrowDecoratedBox : ContentControl, { return this; } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/Primitives/Motions/MotionGhostControl.cs b/src/AtomUI.Controls/Primitives/Motions/MotionGhostControl.cs index d86996f30..14be7cd94 100644 --- a/src/AtomUI.Controls/Primitives/Motions/MotionGhostControl.cs +++ b/src/AtomUI.Controls/Primitives/Motions/MotionGhostControl.cs @@ -173,7 +173,7 @@ internal class MotionGhostControl : Control offsetY += MaskOffset.Y; // 不知道这里为啥不行 - var renderers = new List(); + var renderers = new List(shadows.Count); for (var i = 0; i < shadows.Count; ++i) { var renderer = new Border @@ -242,18 +242,38 @@ internal class MotionGhostControl : Control { if (_layout.Children[i] is Border border) { - border.CornerRadius = MaskCornerRadius; - border.Width = MaskSize.Width; - border.Height = MaskSize.Height; + if (border.CornerRadius != MaskCornerRadius) + { + border.CornerRadius = MaskCornerRadius; + } + if (!AreClose(border.Width, MaskSize.Width)) + { + border.Width = MaskSize.Width; + } + if (!AreClose(border.Height, MaskSize.Height)) + { + border.Height = MaskSize.Height; + } } } } + private static bool AreClose(double value1, double value2) + { + return Math.Abs(value1 - value2) < 0.0001; + } + private void HandleContentSizeChanged(object? sender, SizeChangedEventArgs e) { Debug.Assert(_maskCenterFrame != null); - _maskCenterFrame.Width = e.NewSize.Width; - _maskCenterFrame.Height = e.NewSize.Height; + if (!AreClose(_maskCenterFrame.Width, e.NewSize.Width)) + { + _maskCenterFrame.Width = e.NewSize.Width; + } + if (!AreClose(_maskCenterFrame.Height, e.NewSize.Height)) + { + _maskCenterFrame.Height = e.NewSize.Height; + } } } @@ -314,4 +334,4 @@ internal class MotionTargetBitmapControl : Control context.DrawImage(_contentBitmap, new Rect(new Point(0, 0), DesiredSize * scaling), new Rect(new Point(0, 0), DesiredSize)); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/Primitives/TreeNodePath.cs b/src/AtomUI.Controls/Primitives/TreeNodePath.cs index e8ba7f6c1..ad7f45b5e 100644 --- a/src/AtomUI.Controls/Primitives/TreeNodePath.cs +++ b/src/AtomUI.Controls/Primitives/TreeNodePath.cs @@ -28,6 +28,12 @@ public sealed record TreeNodePath Segments = Array.AsReadOnly(_segmentsArray); } + private TreeNodePath(string[] trustedSegments, bool _) + { + _segmentsArray = trustedSegments; + Segments = Array.AsReadOnly(_segmentsArray); + } + private static string[] ValidateAndCopySegments(string[] segments) { ArgumentNullException.ThrowIfNull(segments); @@ -63,6 +69,10 @@ public sealed record TreeNodePath public bool StartsWith(TreeNodePath other) { + if (ReferenceEquals(this, other) || other.Length == 0) + { + return true; + } if (other.Length > Length) { return false; @@ -78,10 +88,21 @@ public sealed record TreeNodePath return true; } - public TreeNodePath? GetParent() => - _segmentsArray.Length > 0 - ? new TreeNodePath(_segmentsArray[..^1]) - : null; + public TreeNodePath? GetParent() + { + if (_segmentsArray.Length == 0) + { + return null; + } + if (_segmentsArray.Length == 1) + { + return Empty; + } + + var newSegments = new string[_segmentsArray.Length - 1]; + Array.Copy(_segmentsArray, newSegments, newSegments.Length); + return new TreeNodePath(newSegments, true); + } public TreeNodePath Append(string segment) { @@ -95,7 +116,7 @@ public sealed record TreeNodePath var newSegments = new string[_segmentsArray.Length + 1]; _segmentsArray.CopyTo(newSegments, 0); newSegments[^1] = segment; - return new TreeNodePath(newSegments); + return new TreeNodePath(newSegments, true); } public TreeNodePath Append(TreeNodePath other) @@ -104,11 +125,16 @@ public sealed record TreeNodePath { return this; } + if (Length == 0) + { + return other; + } + var newSegments = new string[_segmentsArray.Length + other.Length]; _segmentsArray.CopyTo(newSegments, 0); other._segmentsArray.CopyTo(newSegments, _segmentsArray.Length); - return new TreeNodePath(newSegments); + return new TreeNodePath(newSegments, true); } public TreeNodePath WithSegment(int index, string newValue) @@ -117,9 +143,19 @@ public sealed record TreeNodePath { throw new ArgumentOutOfRangeException(nameof(index)); } + ArgumentException.ThrowIfNullOrEmpty(newValue); + if (newValue.Contains('/')) + { + throw new ArgumentException("Segments cannot contain '/'", nameof(newValue)); + } + if (string.Equals(_segmentsArray[index], newValue, StringComparison.Ordinal)) + { + return this; + } + var newSegments = _segmentsArray.ToArray(); - newSegments[index] = newValue ?? throw new ArgumentNullException(nameof(newValue)); + newSegments[index] = newValue; - return new TreeNodePath(newSegments); + return new TreeNodePath(newSegments, true); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/Primitives/WaveSpiritDecorator.cs b/src/AtomUI.Controls/Primitives/WaveSpiritDecorator.cs index ad26528ae..4591ddf7f 100644 --- a/src/AtomUI.Controls/Primitives/WaveSpiritDecorator.cs +++ b/src/AtomUI.Controls/Primitives/WaveSpiritDecorator.cs @@ -141,6 +141,7 @@ internal class WaveSpiritDecorator : Control private AbstractWavePainter? _wavePainter; private CancellationTokenSource? _cancellationTokenSource; private bool _isPlaying; + private static readonly Easing DefaultEasingCurve = new CubicEaseOut(); static WaveSpiritDecorator() { @@ -207,8 +208,8 @@ internal class WaveSpiritDecorator : Control { roundWavePainter.CornerRadius = CornerRadius; } - _wavePainter.SizeEasingCurve = new CubicEaseOut(); - _wavePainter.OpacityEasingCurve = new CubicEaseOut(); + _wavePainter.SizeEasingCurve = DefaultEasingCurve; + _wavePainter.OpacityEasingCurve = DefaultEasingCurve; _wavePainter.OriginOpacity = Math.Clamp(OriginOpacity, 0.0, 1.0); _wavePainter.SizeMotionDuration = SizeMotionDuration; _wavePainter.OpacityMotionDuration = OpacityMotionDuration.Add(TimeSpan.FromMilliseconds(50)); @@ -305,4 +306,4 @@ internal class WaveSpiritDecorator : Control _isPlaying = false; }); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/ProgressBar/AbstractCircleProgress.cs b/src/AtomUI.Controls/ProgressBar/AbstractCircleProgress.cs index b53666ab9..b9fddd65f 100644 --- a/src/AtomUI.Controls/ProgressBar/AbstractCircleProgress.cs +++ b/src/AtomUI.Controls/ProgressBar/AbstractCircleProgress.cs @@ -83,7 +83,7 @@ public abstract class AbstractCircleProgress : AbstractProgressBar public AbstractCircleProgress() { - _sizeTypeThresholdValue = new Dictionary(); + _sizeTypeThresholdValue = new Dictionary(3); } protected override SizeType CalculateEffectiveSizeType(double size) @@ -344,4 +344,4 @@ public abstract class AbstractCircleProgress : AbstractProgressBar SetValue(SuccessCompletedIconProperty, new CheckOutlined(), BindingPriority.Template); } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/ProgressBar/AbstractGeneralCircleProgress.cs b/src/AtomUI.Controls/ProgressBar/AbstractGeneralCircleProgress.cs index 1ef7670a9..8238e4990 100644 --- a/src/AtomUI.Controls/ProgressBar/AbstractGeneralCircleProgress.cs +++ b/src/AtomUI.Controls/ProgressBar/AbstractGeneralCircleProgress.cs @@ -7,6 +7,9 @@ namespace AtomUI.Controls.Commons; public abstract class AbstractGeneralCircleProgress : AbstractCircleProgress { private Rect _currentGrooveRect; + private IPen? _groovePen; + private IPen? _indicatorPen; + private IPen? _successPen; protected override void RenderGroove(DrawingContext context) { @@ -27,21 +30,21 @@ public abstract class AbstractGeneralCircleProgress : AbstractCircleProgress private void DrawGrooveNormal(DrawingContext context) { - var pen = new Pen(GrooveBrush, StrokeThickness); - context.DrawEllipse(null, pen, _currentGrooveRect); + PenUtils.TryModifyOrCreate(ref _groovePen, GrooveBrush, StrokeThickness); + context.DrawEllipse(null, _groovePen, _currentGrooveRect); } private void DrawGrooveStep(DrawingContext context) { - var pen = new Pen(GrooveBrush, StrokeThickness) - { - LineCap = PenLineCap.Flat - }; + PenUtils.TryModifyOrCreate(ref _groovePen, + GrooveBrush, + StrokeThickness, + lineCap: PenLineCap.Flat); var spanAngle = (360 - StepGap * StepCount) / StepCount; var startAngle = -90d; for (var i = 0; i < StepCount; ++i) { - context.DrawArc(pen, _currentGrooveRect, startAngle, spanAngle); + context.DrawArc(_groovePen, _currentGrooveRect, startAngle, spanAngle); startAngle += StepGap + spanAngle; } } @@ -60,41 +63,40 @@ public abstract class AbstractGeneralCircleProgress : AbstractCircleProgress private void DrawIndicatorBarNormal(DrawingContext context) { - var pen = new Pen(StrokeBrush, StrokeThickness) - { - LineCap = StrokeLineCap - }; + PenUtils.TryModifyOrCreate(ref _indicatorPen, + StrokeBrush, + StrokeThickness, + lineCap: StrokeLineCap); double startAngle = -90; - context.DrawArc(pen, _currentGrooveRect, startAngle, IndicatorAngle); + context.DrawArc(_indicatorPen, _currentGrooveRect, startAngle, IndicatorAngle); if (!double.IsNaN(SuccessThreshold)) { - var successPen = new Pen(SuccessStrokeBrush, StrokeThickness) - { - LineCap = StrokeLineCap - }; - context.DrawArc(successPen, _currentGrooveRect, startAngle, CalculateAngle(SuccessThreshold)); + PenUtils.TryModifyOrCreate(ref _successPen, + SuccessStrokeBrush, + StrokeThickness, + lineCap: StrokeLineCap); + context.DrawArc(_successPen, _currentGrooveRect, startAngle, CalculateAngle(SuccessThreshold)); } } private void DrawIndicatorBarStep(DrawingContext context) { - var pen = new Pen(StrokeBrush, StrokeThickness) - { - LineCap = PenLineCap.Flat - }; + PenUtils.TryModifyOrCreate(ref _indicatorPen, + StrokeBrush, + StrokeThickness, + lineCap: PenLineCap.Flat); var filledSteps = (int)Math.Round(StepCount * Percentage / 100); int? successSteps = null; - IPen? successPen = null; if (!double.IsNaN(SuccessThreshold)) { - successPen = new Pen(SuccessStrokeBrush, StrokeThickness) - { - LineCap = PenLineCap.Flat - }; + PenUtils.TryModifyOrCreate(ref _successPen, + SuccessStrokeBrush, + StrokeThickness, + lineCap: PenLineCap.Flat); successSteps = (int)Math.Round(StepCount * SuccessThreshold / (Maximum - Minimum)); } @@ -103,12 +105,12 @@ public abstract class AbstractGeneralCircleProgress : AbstractCircleProgress IPen? currentPen; for (var i = 0; i < filledSteps; ++i) { - currentPen = pen; + currentPen = _indicatorPen; if (successSteps.HasValue) { if (i < successSteps) { - currentPen = successPen; + currentPen = _successPen; } } @@ -127,4 +129,4 @@ public abstract class AbstractGeneralCircleProgress : AbstractCircleProgress { return 360 * value / (Maximum - Minimum); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/ProgressBar/AbstractGeneralDashboardProgress.cs b/src/AtomUI.Controls/ProgressBar/AbstractGeneralDashboardProgress.cs index 86c5fce76..39abcd5f8 100644 --- a/src/AtomUI.Controls/ProgressBar/AbstractGeneralDashboardProgress.cs +++ b/src/AtomUI.Controls/ProgressBar/AbstractGeneralDashboardProgress.cs @@ -38,6 +38,9 @@ public abstract class AbstractGeneralDashboardProgress : AbstractCircleProgress private Rect _currentGrooveRect; private (double, double) _anglePair; + private IPen? _groovePen; + private IPen? _indicatorPen; + private IPen? _successPen; static AbstractGeneralDashboardProgress() { @@ -63,24 +66,24 @@ public abstract class AbstractGeneralDashboardProgress : AbstractCircleProgress private void DrawGrooveNormal(DrawingContext context) { - var pen = new Pen(GrooveBrush, StrokeThickness) - { - LineCap = StrokeLineCap - }; - context.DrawArc(pen, _currentGrooveRect, _anglePair.Item1, _anglePair.Item2); + PenUtils.TryModifyOrCreate(ref _groovePen, + GrooveBrush, + StrokeThickness, + lineCap: StrokeLineCap); + context.DrawArc(_groovePen, _currentGrooveRect, _anglePair.Item1, _anglePair.Item2); } private void DrawGrooveStep(DrawingContext context) { - var pen = new Pen(GrooveBrush, StrokeThickness) - { - LineCap = PenLineCap.Flat - }; + PenUtils.TryModifyOrCreate(ref _groovePen, + GrooveBrush, + StrokeThickness, + lineCap: PenLineCap.Flat); var spanAngle = (360 - GapDegree - StepGap * StepCount) / StepCount; var startAngle = _anglePair.Item1; for (var i = 0; i < StepCount; ++i) { - context.DrawArc(pen, _currentGrooveRect, startAngle, spanAngle); + context.DrawArc(_groovePen, _currentGrooveRect, startAngle, spanAngle); startAngle += StepGap + spanAngle; } } @@ -99,53 +102,52 @@ public abstract class AbstractGeneralDashboardProgress : AbstractCircleProgress private void DrawIndicatorBarNormal(DrawingContext context) { - var pen = new Pen(StrokeBrush, StrokeThickness) - { - LineCap = StrokeLineCap - }; - context.DrawArc(pen, _currentGrooveRect, _anglePair.Item1, IndicatorAngle); + PenUtils.TryModifyOrCreate(ref _indicatorPen, + StrokeBrush, + StrokeThickness, + lineCap: StrokeLineCap); + context.DrawArc(_indicatorPen, _currentGrooveRect, _anglePair.Item1, IndicatorAngle); if (!double.IsNaN(SuccessThreshold)) { - var successPen = new Pen(SuccessStrokeBrush, StrokeThickness) - { - LineCap = StrokeLineCap - }; - context.DrawArc(successPen, _currentGrooveRect, _anglePair.Item1, CalculateAngle(SuccessThreshold)); + PenUtils.TryModifyOrCreate(ref _successPen, + SuccessStrokeBrush, + StrokeThickness, + lineCap: StrokeLineCap); + context.DrawArc(_successPen, _currentGrooveRect, _anglePair.Item1, CalculateAngle(SuccessThreshold)); } } private void DrawIndicatorBarStep(DrawingContext context) { - var pen = new Pen(StrokeBrush, StrokeThickness) - { - LineCap = PenLineCap.Flat - }; + PenUtils.TryModifyOrCreate(ref _indicatorPen, + StrokeBrush, + StrokeThickness, + lineCap: PenLineCap.Flat); var spanAngle = (360 - GapDegree - StepGap * StepCount) / StepCount; var startAngle = _anglePair.Item1; var filledSteps = (int)Math.Round(StepCount * Percentage / 100); int? successSteps = null; - IPen? successPen = null; if (!double.IsNaN(SuccessThreshold)) { - successPen = new Pen(SuccessStrokeBrush, StrokeThickness) - { - LineCap = PenLineCap.Flat - }; + PenUtils.TryModifyOrCreate(ref _successPen, + SuccessStrokeBrush, + StrokeThickness, + lineCap: PenLineCap.Flat); successSteps = (int)Math.Round(StepCount * SuccessThreshold / (Maximum - Minimum)); } IPen? currentPen; for (var i = 0; i < filledSteps; ++i) { - currentPen = pen; + currentPen = _indicatorPen; if (successSteps.HasValue) { if (i < successSteps) { - currentPen = successPen; + currentPen = _successPen; } } @@ -205,4 +207,4 @@ public abstract class AbstractGeneralDashboardProgress : AbstractCircleProgress { return (360 - GapDegree) * value / (Maximum - Minimum); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/ProgressBar/AbstractGeneralProgressBar.cs b/src/AtomUI.Controls/ProgressBar/AbstractGeneralProgressBar.cs index 1b8d3b577..3ad52a63f 100644 --- a/src/AtomUI.Controls/ProgressBar/AbstractGeneralProgressBar.cs +++ b/src/AtomUI.Controls/ProgressBar/AbstractGeneralProgressBar.cs @@ -625,7 +625,7 @@ public abstract class AbstractGeneralProgressBar : AbstractLineProgress { if (!PercentPosition.IsInner) { - SetCurrentValue(PercentageLabelColorProperty, Foreground); + SetPercentageLabelColorIfChanged(Foreground); } else { @@ -635,33 +635,52 @@ public abstract class AbstractGeneralProgressBar : AbstractLineProgress // 但是渐变笔刷就麻烦了,暂时不支持吧 var colorTextLabel = (ColorTextLabel as ISolidColorBrush)!.Color; var colorTextLightSolid = (ColorTextLightSolid as ISolidColorBrush)!.Color; - var colors = new List { colorTextLabel, colorTextLightSolid }; if (MathUtils.AreClose(Value, 0)) { if (GrooveBrush is ISolidColorBrush grooveBrush) { - var mostReadable = ColorUtils.MostReadable(grooveBrush.Color, colors); - if (mostReadable.HasValue) - { - SetCurrentValue(PercentageLabelColorProperty, new SolidColorBrush(mostReadable.Value)); - } + var mostReadable = SelectMostReadable(grooveBrush.Color, colorTextLabel, colorTextLightSolid); + SetPercentageLabelColorIfChanged(mostReadable); } } else { if (StrokeBrush is ISolidColorBrush solidColorBrush) { - var mostReadable = ColorUtils.MostReadable(solidColorBrush.Color, colors); - if (mostReadable.HasValue) - { - SetCurrentValue(PercentageLabelColorProperty, new SolidColorBrush(mostReadable.Value)); - } + var mostReadable = SelectMostReadable(solidColorBrush.Color, colorTextLabel, colorTextLightSolid); + SetPercentageLabelColorIfChanged(mostReadable); } } } } } + private static Color SelectMostReadable(Color baseColor, Color first, Color second) + { + return ColorUtils.Readability(baseColor, first) >= ColorUtils.Readability(baseColor, second) + ? first + : second; + } + + private void SetPercentageLabelColorIfChanged(IBrush? brush) + { + if (!Equals(PercentageLabelColor, brush)) + { + SetCurrentValue(PercentageLabelColorProperty, brush); + } + } + + private void SetPercentageLabelColorIfChanged(Color color) + { + if (PercentageLabelColor is ISolidColorBrush solidColorBrush && + solidColorBrush.Color == color) + { + return; + } + + SetCurrentValue(PercentageLabelColorProperty, new SolidColorBrush(color)); + } + private void UpdatePseudoClasses() { PseudoClasses.Set(ProgressBarPseudoClass.PercentLabelInner, PercentPosition.IsInner); @@ -675,4 +694,4 @@ public abstract class AbstractGeneralProgressBar : AbstractLineProgress base.OnAttachedToVisualTree(e); UpdatePseudoClasses(); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/ProgressBar/AbstractLineProgress.cs b/src/AtomUI.Controls/ProgressBar/AbstractLineProgress.cs index ce48f83a8..cc32ff7f5 100644 --- a/src/AtomUI.Controls/ProgressBar/AbstractLineProgress.cs +++ b/src/AtomUI.Controls/ProgressBar/AbstractLineProgress.cs @@ -87,7 +87,7 @@ public abstract class AbstractLineProgress : AbstractProgressBar public AbstractLineProgress() { - _sizeTypeThresholdValue = new Dictionary(); + _sizeTypeThresholdValue = new Dictionary(3); } // 根据当前的状态进行计算 @@ -200,4 +200,4 @@ public abstract class AbstractLineProgress : AbstractProgressBar _extraInfoSize = CalculateExtraInfoSize(FontSize); } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/Tag/AbstractTag.cs b/src/AtomUI.Controls/Tag/AbstractTag.cs index 09335299f..1b0a4b23c 100644 --- a/src/AtomUI.Controls/Tag/AbstractTag.cs +++ b/src/AtomUI.Controls/Tag/AbstractTag.cs @@ -281,7 +281,7 @@ public abstract class AbstractTag : TemplatedControl return; } - var dict = new Dictionary(); + var dict = new Dictionary(14); foreach (var entry in PresetPrimaryColor.AllColorTypes()) { var colorMap = sharedToken.GetColorPalette(entry)!; @@ -308,7 +308,7 @@ public abstract class AbstractTag : TemplatedControl return; } - StatusColorMap = new Dictionary + StatusColorMap = new Dictionary(4) { [TagStatus.Success] = new TagStatusCalcColor { @@ -342,13 +342,13 @@ public abstract class AbstractTag : TemplatedControl { IsPresetColorTag = false; IsColorSet = false; - colorStr = colorStr.Trim().ToLower(); + colorStr = colorStr.Trim(); foreach (var entry in PresetColorMap) { - if (entry.Key.ToString().ToLower() == colorStr) + if (string.Equals(entry.Key.ToString(), colorStr, StringComparison.OrdinalIgnoreCase)) { - var colorInfo = PresetColorMap[entry.Key]; + var colorInfo = entry.Value; Foreground = new SolidColorBrush(colorInfo.TextColor); BorderBrush = new SolidColorBrush(colorInfo.LightBorderColor); Background = new SolidColorBrush(colorInfo.LightColor); @@ -362,9 +362,9 @@ public abstract class AbstractTag : TemplatedControl foreach (var entry in StatusColorMap) { - if (entry.Key.ToString().ToLower() == colorStr) + if (string.Equals(entry.Key.ToString(), colorStr, StringComparison.OrdinalIgnoreCase)) { - var colorInfo = StatusColorMap[entry.Key]; + var colorInfo = entry.Value; Foreground = new SolidColorBrush(colorInfo.Color); BorderBrush = new SolidColorBrush(colorInfo.BorderColor); Background = new SolidColorBrush(colorInfo.Background); @@ -404,4 +404,4 @@ public abstract class AbstractTag : TemplatedControl { RaiseEvent(new RoutedEventArgs(ClosedEvent, this)); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Core/Media/PenUtils.cs b/src/AtomUI.Core/Media/PenUtils.cs index fbeb1668c..c25db1101 100644 --- a/src/AtomUI.Core/Media/PenUtils.cs +++ b/src/AtomUI.Core/Media/PenUtils.cs @@ -34,6 +34,18 @@ public static class PenUtils return previousPen is not null; } + if (IsSamePen(previousPen, + brush, + thickness, + strokeDashArray, + strokeDaskOffset, + lineCap, + lineJoin, + miterLimit)) + { + return false; + } + IDashStyle? dashStyle = null; if (strokeDashArray is { Count: > 0 }) { @@ -68,4 +80,50 @@ public static class PenUtils pen = mutablePen; return !Equals(previousPen, pen); } -} \ No newline at end of file + + private static bool IsSamePen(IPen? pen, + IBrush brush, + double thickness, + IReadOnlyList? strokeDashArray, + double strokeDaskOffset, + PenLineCap lineCap, + PenLineJoin lineJoin, + double miterLimit) + { + return pen is not null && + Equals(pen.Brush, brush) && + pen.Thickness.Equals(thickness) && + pen.LineCap == lineCap && + pen.LineJoin == lineJoin && + pen.MiterLimit.Equals(miterLimit) && + IsSameDashStyle(pen.DashStyle, strokeDashArray, strokeDaskOffset); + } + + private static bool IsSameDashStyle(IDashStyle? dashStyle, + IReadOnlyList? strokeDashArray, + double strokeDaskOffset) + { + if (strokeDashArray is not { Count: > 0 }) + { + return dashStyle is null || dashStyle.Dashes is not { Count: > 0 }; + } + + if (dashStyle is null || + !dashStyle.Offset.Equals(strokeDaskOffset) || + dashStyle.Dashes is not { } dashes || + dashes.Count != strokeDashArray.Count) + { + return false; + } + + for (var i = 0; i < strokeDashArray.Count; i++) + { + if (!dashes[i].Equals(strokeDashArray[i])) + { + return false; + } + } + + return true; + } +} diff --git a/src/AtomUI.Desktop.Controls/AutoComplete/AbstractAutoComplete.cs b/src/AtomUI.Desktop.Controls/AutoComplete/AbstractAutoComplete.cs index 7ae485121..6c261ac51 100644 --- a/src/AtomUI.Desktop.Controls/AutoComplete/AbstractAutoComplete.cs +++ b/src/AtomUI.Desktop.Controls/AutoComplete/AbstractAutoComplete.cs @@ -1210,13 +1210,13 @@ public abstract class AbstractAutoComplete : TemplatedControl, } // Fire the SelectionChanged event - var removed = new List(); + var removed = new List(e.OldValue != null ? 1 : 0); if (e.OldValue != null) { removed.Add(e.OldValue); } - var added = new List(); + var added = new List(e.NewValue != null ? 1 : 0); if (e.NewValue != null) { added.Add(e.NewValue); diff --git a/src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinner.cs b/src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinner.cs index 3804fd9fc..34025c46d 100644 --- a/src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinner.cs +++ b/src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinner.cs @@ -482,13 +482,24 @@ public class ButtonSpinner : Spinner, void ICompactSpaceAware.NotifyPositionChange(SpaceItemPosition? position) { - IsUsedInCompactSpace = position != null; - CompactSpaceItemPosition = position; + var isUsedInCompactSpace = position != null; + if (IsUsedInCompactSpace != isUsedInCompactSpace) + { + IsUsedInCompactSpace = isUsedInCompactSpace; + } + + if (CompactSpaceItemPosition != position) + { + CompactSpaceItemPosition = position; + } } void ICompactSpaceAware.NotifyOrientationChange(Orientation orientation) { - CompactSpaceOrientation = orientation; + if (CompactSpaceOrientation != orientation) + { + CompactSpaceOrientation = orientation; + } } double ICompactSpaceAware.GetBorderThickness() => GetBorderThicknessForCompactSpace(); @@ -508,4 +519,4 @@ public class ButtonSpinner : Spinner, // 都一样宽 return DecoratedBox.InnerBoxBorderThickness.Left; } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinnerHandle.cs b/src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinnerHandle.cs index 8a6b74647..7e6ebeb68 100644 --- a/src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinnerHandle.cs +++ b/src/AtomUI.Desktop.Controls/ButtonSpinner/ButtonSpinnerHandle.cs @@ -1,5 +1,6 @@ using AtomUI.Controls; using AtomUI.Controls.Utils; +using AtomUI.Media; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -46,6 +47,7 @@ internal class ButtonSpinnerHandle : TemplatedControl public event EventHandler? ButtonsCreated; private BorderRenderHelper _borderRenderHelper; + private IPen? _handlePen; static ButtonSpinnerHandle() { @@ -103,13 +105,21 @@ internal class ButtonSpinnerHandle : TemplatedControl // 画竖线 var startPoint = new Point(lineWidth, lineWidth); var endPoint = new Point(lineWidth, Bounds.Height - lineWidth); - context.DrawLine(new Pen(BorderBrush, lineWidth), startPoint, endPoint); + PenUtils.TryModifyOrCreate(ref _handlePen, BorderBrush, lineWidth); + if (_handlePen is not null) + { + context.DrawLine(_handlePen, startPoint, endPoint); + } } { // 画横线 var startPoint = new Point(lineWidth, handleOffsetY); var endPoint = new Point(Bounds.Width - lineWidth, handleOffsetY); - context.DrawLine(new Pen(BorderBrush, lineWidth), startPoint, endPoint); + PenUtils.TryModifyOrCreate(ref _handlePen, BorderBrush, lineWidth); + if (_handlePen is not null) + { + context.DrawLine(_handlePen, startPoint, endPoint); + } } } } diff --git a/src/AtomUI.Desktop.Controls/Card/CardActionPanel.cs b/src/AtomUI.Desktop.Controls/Card/CardActionPanel.cs index 5e9f6b908..062eb87fb 100644 --- a/src/AtomUI.Desktop.Controls/Card/CardActionPanel.cs +++ b/src/AtomUI.Desktop.Controls/Card/CardActionPanel.cs @@ -1,6 +1,7 @@ using System.Collections.Specialized; using AtomUI.Animations; using AtomUI.Controls; +using AtomUI.Media; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -23,6 +24,7 @@ internal class CardActionPanel : TemplatedControl public Avalonia.Controls.Controls Actions { get; } = new (); private UniformGrid? _uniformGrid; + private IPen? _borderPen; static CardActionPanel() { @@ -61,12 +63,10 @@ internal class CardActionPanel : TemplatedControl switch (e.Action) { case NotifyCollectionChangedAction.Add: - var newItems = e.NewItems!.OfType().ToList(); - _uniformGrid.Children.InsertRange(e.NewStartingIndex, newItems); + _uniformGrid.Children.InsertRange(e.NewStartingIndex, e.NewItems!.OfType()); break; case NotifyCollectionChangedAction.Remove: - var oldItems = e.OldItems!.OfType().ToList(); - _uniformGrid.Children.RemoveAll(oldItems); + _uniformGrid.Children.RemoveAll(e.OldItems!.OfType()); break; case NotifyCollectionChangedAction.Replace: for (int index1 = 0; index1 < e.OldItems!.Count; ++index1) @@ -89,10 +89,15 @@ internal class CardActionPanel : TemplatedControl public override void Render(DrawingContext context) { var lineWidth = BorderThickness.Left; + PenUtils.TryModifyOrCreate(ref _borderPen, BorderBrush, lineWidth); + if (_borderPen is null) + { + return; + } { var startPoint = new Point(0, lineWidth / 2); var endPoint = new Point(Bounds.Width, lineWidth / 2); - context.DrawLine(new Pen(BorderBrush, lineWidth), startPoint, endPoint); + context.DrawLine(_borderPen, startPoint, endPoint); } for (var i = 0; i < Actions.Count - 1; i++) { @@ -103,7 +108,7 @@ internal class CardActionPanel : TemplatedControl var startPoint = new Point(offsetX, offsetY + deltaY); var endPoint = new Point(offsetX, offsetY + action.Bounds.Height - deltaY); - context.DrawLine(new Pen(BorderBrush, lineWidth), startPoint, endPoint); + context.DrawLine(_borderPen, startPoint, endPoint); } } @@ -118,4 +123,4 @@ internal class CardActionPanel : TemplatedControl base.OnLoaded(e); Dispatcher.Post(() => this.EnableTransitions()); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Carousel/Carousel.cs b/src/AtomUI.Desktop.Controls/Carousel/Carousel.cs index 381e77075..a2d2ad8ad 100644 --- a/src/AtomUI.Desktop.Controls/Carousel/Carousel.cs +++ b/src/AtomUI.Desktop.Controls/Carousel/Carousel.cs @@ -284,10 +284,10 @@ public class Carousel : SelectingItemsControl, IMotionAwareControl private void HandleItemCountChanged(int count) { - var items = new List(); + var items = count == 0 ? Array.Empty() : new object[count]; for (var i = 0; i < count; i++) { - items.Add(new object()); + items[i] = new object(); } SetCurrentValue(IndicatorItemsProperty, items); } diff --git a/src/AtomUI.Desktop.Controls/Cascader/Cascader.cs b/src/AtomUI.Desktop.Controls/Cascader/Cascader.cs index 3d9cebb18..a6224fd04 100644 --- a/src/AtomUI.Desktop.Controls/Cascader/Cascader.cs +++ b/src/AtomUI.Desktop.Controls/Cascader/Cascader.cs @@ -274,7 +274,7 @@ public class Cascader : AbstractSelect { if (_cascaderView != null) { - _cascaderView.OptionsSource = Options.Cast().ToList(); + _cascaderView.OptionsSource = BuildOptionsList(Options); } } @@ -448,7 +448,7 @@ public class Cascader : AbstractSelect _cascaderView.ItemDoubleClicked += HandleCascaderViewItemDoubleClicked; _cascaderView.ItemClicked += HandleCascaderViewItemClicked; _cascaderView.OptionSelected += HandleCascaderViewItemSelected; - _cascaderView.OptionsSource = Options.Cast().ToList(); + _cascaderView.OptionsSource = BuildOptionsList(Options); } ConfigurePlaceholderVisible(); @@ -552,8 +552,8 @@ public class Cascader : AbstractSelect } else { - var currentSet = _checkedOptions?.Cast().ToHashSet() ?? new HashSet(); - cascaderViewSet = _cascaderView?.SelectedOptions?.Cast().ToHashSet() ?? new HashSet(); + var currentSet = BuildOptionSet(_checkedOptions) ?? new HashSet(); + cascaderViewSet = BuildOptionSet(_cascaderView?.SelectedOptions) ?? new HashSet(); if (!currentSet.SetEquals(cascaderViewSet)) { needSync = true; @@ -565,10 +565,10 @@ public class Cascader : AbstractSelect try { _needSkipSyncSelectedOptions = true; - cascaderViewSet ??= _cascaderView?.SelectedOptions?.Cast().ToHashSet(); + cascaderViewSet ??= BuildOptionSet(_cascaderView?.SelectedOptions); if (cascaderViewSet != null) { - SelectedOptions = cascaderViewSet.ToList(); + SelectedOptions = BuildOptionList(cascaderViewSet); } } @@ -780,7 +780,7 @@ public class Cascader : AbstractSelect { if (SelectedOptions != null) { - var checkedOptions = new List(); + var checkedOptions = new List(SelectedOptions.Count); foreach (var option in SelectedOptions) { checkedOptions.Add(option); @@ -811,28 +811,32 @@ public class Cascader : AbstractSelect { if (_cascaderView.SelectedOptions == null) { - var checkedOptions = new List(); + var checkedOptions = new List(SelectedOptions.Count); checkedOptions.AddRange(SelectedOptions); _cascaderView.SelectedOptions = checkedOptions; } else { - var cascaderViewSet = _cascaderView.SelectedOptions.ToHashSet(); - var currentSet = SelectedOptions.ToHashSet(); - var deletedOptions = cascaderViewSet.Except(currentSet); - var addedOptions = currentSet.Except(cascaderViewSet); + var cascaderViewSet = BuildOptionSet(_cascaderView.SelectedOptions)!; + var currentSet = BuildOptionSet(SelectedOptions)!; - var currentSelectedOptions = new List(); + var currentSelectedOptions = new List(_cascaderView.SelectedOptions.Count); currentSelectedOptions.AddRange(_cascaderView.SelectedOptions); - foreach (var option in deletedOptions) + foreach (var option in cascaderViewSet) { - currentSelectedOptions.Remove(option); + if (!currentSet.Contains(option)) + { + currentSelectedOptions.Remove(option); + } } - foreach (var option in addedOptions) + foreach (var option in currentSet) { - currentSelectedOptions.Add(option); + if (!cascaderViewSet.Contains(option)) + { + currentSelectedOptions.Add(option); + } } _cascaderView.SelectedOptions = currentSelectedOptions; } @@ -856,13 +860,18 @@ public class Cascader : AbstractSelect } else { - var effectiveSelectedOptions = new List(); + var effectiveSelectedOptions = new List(SelectedOptions.Count); if (ShowCheckedStrategy.HasFlag(TreeSelectCheckedStrategy.ShowParent)) { - var selectedSet = SelectedOptions.Cast().ToHashSet(); - var fullySelectedParents = SelectedOptions - .Where(node => node.Children.All(child => selectedSet.Contains(child))) - .ToHashSet(); + var selectedSet = BuildOptionSet(SelectedOptions)!; + var fullySelectedParents = new HashSet(SelectedOptions.Count); + foreach (var node in SelectedOptions) + { + if (node.Children.All(child => selectedSet.Contains(child))) + { + fullySelectedParents.Add(node); + } + } foreach (var option in SelectedOptions) { bool isDescendantOfFullySelectedParent = fullySelectedParents @@ -892,6 +901,43 @@ public class Cascader : AbstractSelect EffectiveSelectedOptions = null; } } + + private static List BuildOptionsList(IEnumerable source) + { + var options = source is ICollection collection + ? new List(collection.Count) + : new List(); + foreach (var item in source) + { + options.Add((ICascaderOption)item!); + } + return options; + } + + private static List BuildOptionList(ICollection source) + { + var options = new List(source.Count); + foreach (var item in source) + { + options.Add(item); + } + return options; + } + + private static HashSet? BuildOptionSet(ICollection? source) + { + if (source == null) + { + return null; + } + + var options = new HashSet(source.Count); + foreach (var item in source) + { + options.Add(item); + } + return options; + } private bool IsDescendantOf(ICascaderOption node, ICascaderOption parent) { diff --git a/src/AtomUI.Desktop.Controls/Cascader/CascaderView.Check.cs b/src/AtomUI.Desktop.Controls/Cascader/CascaderView.Check.cs index fce52305f..fd2ebc33b 100644 --- a/src/AtomUI.Desktop.Controls/Cascader/CascaderView.Check.cs +++ b/src/AtomUI.Desktop.Controls/Cascader/CascaderView.Check.cs @@ -77,7 +77,7 @@ public partial class CascaderView } ISet checkedOptions = DoCheckedSubTree(viewOption); - var originSelectedOptions = new HashSet(); + var originSelectedOptions = new HashSet(SelectedOptions?.Count ?? 0); if (SelectedOptions != null) { foreach (var checkedItem in SelectedOptions) @@ -85,7 +85,7 @@ public partial class CascaderView originSelectedOptions.Add(checkedItem); } } - var newSelectedOptions = new HashSet(); + var newSelectedOptions = new HashSet(originSelectedOptions.Count + checkedOptions.Count); foreach (var checkedItem in originSelectedOptions) { newSelectedOptions.Add(checkedItem); @@ -98,7 +98,7 @@ public partial class CascaderView if (!newSelectedOptions.SetEquals(originSelectedOptions)) { _ignoreSyncSelectedOptions = true; - SelectedOptions = newSelectedOptions.ToList(); + SelectedOptions = BuildSelectedOptionsList(newSelectedOptions); } } @@ -168,7 +168,7 @@ public partial class CascaderView ISet unSelectedOptions = DoUnCheckedSubTree(viewOption); - var originSelectedOptions = new HashSet(); + var originSelectedOptions = new HashSet(SelectedOptions?.Count ?? 0); if (SelectedOptions != null) { foreach (var checkedItem in SelectedOptions) @@ -176,7 +176,7 @@ public partial class CascaderView originSelectedOptions.Add(checkedItem); } } - var newSelectedOptions = new HashSet(); + var newSelectedOptions = new HashSet(originSelectedOptions.Count); foreach (var checkedItem in originSelectedOptions) { newSelectedOptions.Add(checkedItem); @@ -190,7 +190,7 @@ public partial class CascaderView if (!newSelectedOptions.SetEquals(originSelectedOptions)) { _ignoreSyncSelectedOptions = true; - SelectedOptions = newSelectedOptions.ToList(); + SelectedOptions = BuildSelectedOptionsList(newSelectedOptions); } } @@ -306,7 +306,7 @@ public partial class CascaderView { if (e.OldItems != null) { - var removedItemsClosure = new HashSet(); + var removedItemsClosure = new HashSet(e.OldItems.Count); foreach (var item in e.OldItems) { if (item is ICascaderOption viewOption) @@ -314,7 +314,7 @@ public partial class CascaderView CollectSubTreeOptions(viewOption, removedItemsClosure); } } - var tobeRemoved = new List(); + var tobeRemoved = new List(SelectedOptions.Count); foreach (var checkedItem in SelectedOptions) { if (removedItemsClosure.Contains(checkedItem)) @@ -345,4 +345,14 @@ public partial class CascaderView CollectSubTreeOptions(childItem, options); } } -} \ No newline at end of file + + private static List BuildSelectedOptionsList(ICollection options) + { + var selectedOptions = new List(options.Count); + foreach (var option in options) + { + selectedOptions.Add(option); + } + return selectedOptions; + } +} diff --git a/src/AtomUI.Desktop.Controls/Cascader/CascaderViewLevelList.cs b/src/AtomUI.Desktop.Controls/Cascader/CascaderViewLevelList.cs index 9405c0fcc..9bda654f1 100644 --- a/src/AtomUI.Desktop.Controls/Cascader/CascaderViewLevelList.cs +++ b/src/AtomUI.Desktop.Controls/Cascader/CascaderViewLevelList.cs @@ -232,7 +232,7 @@ internal class CascaderViewLevelList : SelectingItemsControl, IListVirtualizingC { if (this is IListVirtualizingContextAware list && element is IListItemVirtualizingContextAware listItem) { - var context = new Dictionary(); + var context = new Dictionary(5); list.SaveVirtualizingContext(element, context); _virtualRestoreContext.Add(listItem.VirtualIndex, context); list.ClearContainerValues(element); diff --git a/src/AtomUI.Desktop.Controls/Cascader/CascaderViewPanel.cs b/src/AtomUI.Desktop.Controls/Cascader/CascaderViewPanel.cs index 394d362c9..d54ed1350 100644 --- a/src/AtomUI.Desktop.Controls/Cascader/CascaderViewPanel.cs +++ b/src/AtomUI.Desktop.Controls/Cascader/CascaderViewPanel.cs @@ -1,3 +1,4 @@ +using AtomUI.Media; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -16,6 +17,7 @@ internal class CascaderViewFrame : Decorator } private StackPanel? _itemsPanel; + private IPen? _borderPen; static CascaderViewFrame() { @@ -53,9 +55,13 @@ internal class CascaderViewFrame : Decorator { var pointStart = new Point(offset.Value.X, 0); var pointEnd = new Point(offset.Value.X, height); - context.DrawLine(new Pen(BorderBrush), pointStart, pointEnd); + PenUtils.TryModifyOrCreate(ref _borderPen, BorderBrush, 1.0); + if (_borderPen is not null) + { + context.DrawLine(_borderPen, pointStart, pointEnd); + } } } } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Descriptions/Descriptions.cs b/src/AtomUI.Desktop.Controls/Descriptions/Descriptions.cs index 286b9bedc..fd44755d7 100644 --- a/src/AtomUI.Desktop.Controls/Descriptions/Descriptions.cs +++ b/src/AtomUI.Desktop.Controls/Descriptions/Descriptions.cs @@ -157,14 +157,14 @@ public class Descriptions : TemplatedControl, ISizeTypeAware case NotifyCollectionChangedAction.Add: if (e.NewItems != null) { - AddDescriptionItems(e.NewItems.OfType().ToList()); + AddDescriptionItems(e.NewItems.OfType()); } break; case NotifyCollectionChangedAction.Remove: if (e.OldItems != null) { - RemoveDescriptionItems(e.OldItems.OfType().ToList()); + RemoveDescriptionItems(e.OldItems.OfType()); } break; @@ -245,7 +245,7 @@ public class Descriptions : TemplatedControl, ISizeTypeAware { base.OnApplyTemplate(e); _gridLayout = e.NameScope.Find("PART_GridLayout"); - AddDescriptionItems(Items.ToList()); + AddDescriptionItems(Items); if (TopLevel.GetTopLevel(this) is Window window) { _breakPoint = window.MediaBreakPoint; @@ -262,7 +262,7 @@ public class Descriptions : TemplatedControl, ISizeTypeAware if (ItemsSource != null) { Items.Clear(); - Items.AddRange(ItemsSource.OfType().ToList()); + Items.AddRange(ItemsSource.OfType()); } } else if (change.Property == IsBorderedProperty) @@ -284,7 +284,7 @@ public class Descriptions : TemplatedControl, ISizeTypeAware } } - private void AddDescriptionItems(List items) + private void AddDescriptionItems(IEnumerable items) { if (_gridLayout != null) { @@ -329,7 +329,7 @@ public class Descriptions : TemplatedControl, ISizeTypeAware } } - private void RemoveDescriptionItems(List items) + private void RemoveDescriptionItems(IEnumerable items) { if (_gridLayout != null) { @@ -588,8 +588,7 @@ public class Descriptions : TemplatedControl, ISizeTypeAware private void HandleLayoutChanged() { - var items = Items.ToList(); - RemoveDescriptionItems(items); - AddDescriptionItems(items); + RemoveDescriptionItems(Items); + AddDescriptionItems(Items); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Dialog/ButtonBox/DialogButtonBox.cs b/src/AtomUI.Desktop.Controls/Dialog/ButtonBox/DialogButtonBox.cs index 18a640064..7a9a2d336 100644 --- a/src/AtomUI.Desktop.Controls/Dialog/ButtonBox/DialogButtonBox.cs +++ b/src/AtomUI.Desktop.Controls/Dialog/ButtonBox/DialogButtonBox.cs @@ -11,6 +11,8 @@ namespace AtomUI.Desktop.Controls; public class DialogButtonBox : TemplatedControl, IMotionAwareControl { + private static readonly IReadOnlyList EmptyButtonList = Array.Empty(); + #region 公共属性定义 public static readonly StyledProperty StandardButtonsProperty = @@ -676,10 +678,10 @@ public class DialogButtonBox : TemplatedControl, IMotionAwareControl { return; } - var buttons = new List(); var rightButtons = SyncRightGroupButtons(_rightGroup); var centerGroupButtons = SyncCenterGroupButtons(_centerGroup); var leftGroupButtons = SyncLeftGroupButtons(_leftGroup); + var buttons = new List(rightButtons.Count + centerGroupButtons.Count + leftGroupButtons.Count); buttons.AddRange(rightButtons); buttons.AddRange(centerGroupButtons); buttons.AddRange(leftGroupButtons); @@ -688,12 +690,21 @@ public class DialogButtonBox : TemplatedControl, IMotionAwareControl private List SyncRightGroupButtons(DockPanel rightGroup) { - var rightGroupButtons = new List(); - // 后面可以定义各种操作系统的风格,现在先写死 // 标准 - var standardAcceptList = _standardButtonGroup.GetValueOrDefault(DialogButtonRole.AcceptRole) ?? new List(); + var standardAcceptList = GetRoleButtons(_standardButtonGroup, DialogButtonRole.AcceptRole); var standardAcceptIndex = 0; + var acceptList = GetRoleButtons(_buttonGroup, DialogButtonRole.AcceptRole); + var standardYesList = GetRoleButtons(_standardButtonGroup, DialogButtonRole.YesRole); + var yesList = GetRoleButtons(_buttonGroup, DialogButtonRole.YesRole); + var standardRejectList = GetRoleButtons(_standardButtonGroup, DialogButtonRole.RejectRole); + var rejectList = GetRoleButtons(_buttonGroup, DialogButtonRole.RejectRole); + var standardNoList = GetRoleButtons(_standardButtonGroup, DialogButtonRole.NoRole); + var noList = GetRoleButtons(_buttonGroup, DialogButtonRole.NoRole); + var rightGroupButtons = new List(standardAcceptList.Count + acceptList.Count + + standardYesList.Count + yesList.Count + + standardRejectList.Count + rejectList.Count + + standardNoList.Count + noList.Count); if (standardAcceptList.Count > 0) { @@ -702,7 +713,6 @@ public class DialogButtonBox : TemplatedControl, IMotionAwareControl } // 自定义 - var acceptList = _buttonGroup.GetValueOrDefault(DialogButtonRole.AcceptRole) ?? new List(); var acceptIndex = 0; if (acceptList.Count > 0) @@ -712,7 +722,6 @@ public class DialogButtonBox : TemplatedControl, IMotionAwareControl } // 标准 - var standardYesList = _standardButtonGroup.GetValueOrDefault(DialogButtonRole.YesRole) ?? new List(); var standardYesIndex = 0; if (standardYesList.Count > 0) @@ -722,7 +731,6 @@ public class DialogButtonBox : TemplatedControl, IMotionAwareControl } // 自定义 - var yesList = _buttonGroup.GetValueOrDefault(DialogButtonRole.YesRole) ?? new List(); var yesIndex = 0; if (yesList.Count > 0) @@ -733,28 +741,24 @@ public class DialogButtonBox : TemplatedControl, IMotionAwareControl // 输出 reject // 标准 - var standardRejectList = _standardButtonGroup.GetValueOrDefault(DialogButtonRole.RejectRole) ?? new List(); foreach (var rejectButton in standardRejectList) { rightGroupButtons.Add(rejectButton); } // 自定义 - var rejectList = _buttonGroup.GetValueOrDefault(DialogButtonRole.RejectRole) ?? new List(); foreach (var rejectButton in rejectList) { rightGroupButtons.Add(rejectButton); } // 输出 No - var standardNoList = _standardButtonGroup.GetValueOrDefault(DialogButtonRole.NoRole) ?? new List(); foreach (var button in standardNoList) { rightGroupButtons.Add(button); } // 自定义 - var noList = _buttonGroup.GetValueOrDefault(DialogButtonRole.NoRole) ?? new List(); foreach (var button in noList) { rightGroupButtons.Add(button); @@ -801,9 +805,10 @@ public class DialogButtonBox : TemplatedControl, IMotionAwareControl private List SyncCenterGroupButtons(DockPanel centerGroup) { - var centerGroupButtons = new List(); // 标准 - var standardDestructiveList = _standardButtonGroup.GetValueOrDefault(DialogButtonRole.DestructiveRole) ?? new List(); + var standardDestructiveList = GetRoleButtons(_standardButtonGroup, DialogButtonRole.DestructiveRole); + var destructiveList = GetRoleButtons(_buttonGroup, DialogButtonRole.DestructiveRole); + var centerGroupButtons = new List(standardDestructiveList.Count + destructiveList.Count); foreach (var button in standardDestructiveList) { @@ -811,7 +816,6 @@ public class DialogButtonBox : TemplatedControl, IMotionAwareControl } // 自定义 - var destructiveList = _buttonGroup.GetValueOrDefault(DialogButtonRole.DestructiveRole) ?? new List(); foreach (var button in destructiveList) { centerGroupButtons.Add(button); @@ -828,58 +832,61 @@ public class DialogButtonBox : TemplatedControl, IMotionAwareControl private List SyncLeftGroupButtons(DockPanel leftGroup) { - var leftGroupButtons = new List(); // 标准 - var standardHelpList = _standardButtonGroup.GetValueOrDefault(DialogButtonRole.HelpRole) ?? new List(); + var standardHelpList = GetRoleButtons(_standardButtonGroup, DialogButtonRole.HelpRole); + var helpList = GetRoleButtons(_buttonGroup, DialogButtonRole.HelpRole); + var standardResetList = GetRoleButtons(_standardButtonGroup, DialogButtonRole.ResetRole); + var resetList = GetRoleButtons(_buttonGroup, DialogButtonRole.ResetRole); + var standardApplyList = GetRoleButtons(_standardButtonGroup, DialogButtonRole.ApplyRole); + var applyList = GetRoleButtons(_buttonGroup, DialogButtonRole.ApplyRole); + var standardActionList = GetRoleButtons(_standardButtonGroup, DialogButtonRole.ActionRole); + var actionList = GetRoleButtons(_buttonGroup, DialogButtonRole.ActionRole); + var leftGroupButtons = new List(standardHelpList.Count + helpList.Count + + standardResetList.Count + resetList.Count + + standardApplyList.Count + applyList.Count + + standardActionList.Count + actionList.Count); foreach (var button in standardHelpList) { leftGroupButtons.Add(button); } // 自定义 - var helpList = _buttonGroup.GetValueOrDefault(DialogButtonRole.HelpRole) ?? new List(); foreach (var button in helpList) { leftGroupButtons.Add(button); } // 标准 - var standardResetList = _standardButtonGroup.GetValueOrDefault(DialogButtonRole.ResetRole) ?? new List(); foreach (var button in standardResetList) { leftGroupButtons.Add(button); } // 自定义 - var resetList = _buttonGroup.GetValueOrDefault(DialogButtonRole.ResetRole) ?? new List(); foreach (var button in resetList) { leftGroupButtons.Add(button); } // 标准 - var standardApplyList = _standardButtonGroup.GetValueOrDefault(DialogButtonRole.ApplyRole) ?? new List(); foreach (var button in standardApplyList) { leftGroupButtons.Add(button); } // 自定义 - var applyList = _buttonGroup.GetValueOrDefault(DialogButtonRole.ApplyRole) ?? new List(); foreach (var button in applyList) { leftGroupButtons.Add(button); } // 标准 - var standardActionList = _standardButtonGroup.GetValueOrDefault(DialogButtonRole.ActionRole) ?? new List(); foreach (var button in standardActionList) { leftGroupButtons.Add(button); } // 自定义 - var actionList = _buttonGroup.GetValueOrDefault(DialogButtonRole.ActionRole) ?? new List(); foreach (var button in actionList) { leftGroupButtons.Add(button); @@ -896,6 +903,13 @@ public class DialogButtonBox : TemplatedControl, IMotionAwareControl return leftGroupButtons; } + private static IReadOnlyList GetRoleButtons( + Dictionary> group, + DialogButtonRole role) + { + return group.GetValueOrDefault(role) ?? EmptyButtonList; + } + private void ClearButtonPanel(Panel panel) { panel.Children.Clear(); diff --git a/src/AtomUI.Desktop.Controls/FloatButton/FloatButtonGroup.cs b/src/AtomUI.Desktop.Controls/FloatButton/FloatButtonGroup.cs index e0b6ae08a..eddbd21d6 100644 --- a/src/AtomUI.Desktop.Controls/FloatButton/FloatButtonGroup.cs +++ b/src/AtomUI.Desktop.Controls/FloatButton/FloatButtonGroup.cs @@ -326,15 +326,14 @@ public class FloatButtonGroup : TemplatedControl, IMotionAwareControl switch (e.Action) { case NotifyCollectionChangedAction.Add: - var newItems = e.NewItems!.OfType().ToList(); - foreach (var item in newItems) + foreach (var item in e.NewItems!) { if (item is FloatButton floatButton) { NotifyAddItem(floatButton); } } - _itemsControl.Children.InsertRange(e.NewStartingIndex, newItems); + _itemsControl.Children.InsertRange(e.NewStartingIndex, e.NewItems!.OfType()); break; case NotifyCollectionChangedAction.Move: @@ -342,15 +341,14 @@ public class FloatButtonGroup : TemplatedControl, IMotionAwareControl break; case NotifyCollectionChangedAction.Remove: - var oldItems = e.OldItems!.OfType().ToList(); - foreach (var item in oldItems) + foreach (var item in e.OldItems!) { if (item is FloatButton floatButton) { floatButton.SetCurrentValue(FloatButton.IsEmbedModeProperty, false); } } - _itemsControl.Children.RemoveAll(oldItems); + _itemsControl.Children.RemoveAll(e.OldItems!.OfType()); break; case NotifyCollectionChangedAction.Replace: diff --git a/src/AtomUI.Desktop.Controls/FloatButton/FloatButtonItemsControl.cs b/src/AtomUI.Desktop.Controls/FloatButton/FloatButtonItemsControl.cs index 94ea6de94..723dc5ff0 100644 --- a/src/AtomUI.Desktop.Controls/FloatButton/FloatButtonItemsControl.cs +++ b/src/AtomUI.Desktop.Controls/FloatButton/FloatButtonItemsControl.cs @@ -135,16 +135,28 @@ internal class FloatButtonItemsControl : TemplatedControl var size = base.ArrangeOverride(finalSize); if (Shape == FloatButtonShape.Square) { - var lines = new List<(Point, Point)>(); - var children = Children.Where(c => c.IsVisible).ToList(); - var count = children.Count; + var count = CountVisibleChildren(); + if (count <= 1) + { + Lines = Array.Empty<(Point, Point)>(); + return size; + } + + var lines = new (Point, Point)[count - 1]; + var lineIndex = 0; + var visibleIndex = 0; Point startPoint = default; Point endPoint = default; - for (var i = 0; i < count; ++i) + for (var i = 0; i < Children.Count; ++i) { - if (i != count - 1) + var child = Children[i]; + if (!child.IsVisible) + { + continue; + } + + if (visibleIndex != count - 1) { - var child = children[i]; var childPos = child.TranslatePoint(new Point(0, 0), this); if (childPos != null) { @@ -159,13 +171,33 @@ internal class FloatButtonItemsControl : TemplatedControl startPoint = new Point(0, childPos.Value.Y + childBounds.Height); endPoint = new Point(DesiredSize.Width, childPos.Value.Y + childBounds.Height); } - lines.Add((startPoint, endPoint)); + lines[lineIndex++] = (startPoint, endPoint); } } + + visibleIndex++; } + if (lineIndex != lines.Length) + { + Array.Resize(ref lines, lineIndex); + } Lines = lines; } return size; } -} \ No newline at end of file + + private int CountVisibleChildren() + { + var count = 0; + foreach (var child in Children) + { + if (child.IsVisible) + { + count++; + } + } + + return count; + } +} diff --git a/src/AtomUI.Desktop.Controls/Form/Form.cs b/src/AtomUI.Desktop.Controls/Form/Form.cs index cd412484f..7f86ed55d 100644 --- a/src/AtomUI.Desktop.Controls/Form/Form.cs +++ b/src/AtomUI.Desktop.Controls/Form/Form.cs @@ -525,7 +525,7 @@ public class Form : ItemsControl, public async Task ValidateAsync(CancellationToken cancellationToken) { AboutToValidate?.Invoke(this, EventArgs.Empty); - var tasks = new List(); + var tasks = new List(Items.Count); foreach (var item in Items) { if (item is FormItem formItem && !formItem.IsSkipValidate()) @@ -534,8 +534,8 @@ public class Form : ItemsControl, } } await Task.WhenAll(tasks); - var results = new List(); - var validateMessages = new List(); + var results = new List(Items.Count); + var validateMessages = new List(Items.Count); foreach (var item in Items) { if (item is FormItem formItem && !formItem.IsSkipValidate()) @@ -835,4 +835,4 @@ public class Form : ItemsControl, } IsEffectiveShowItemDeleteButton = IsShowItemDeleteButton && count > MinItemCount; } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Form/FormItem.cs b/src/AtomUI.Desktop.Controls/Form/FormItem.cs index 4d5ec36f1..cc76ac6b7 100644 --- a/src/AtomUI.Desktop.Controls/Form/FormItem.cs +++ b/src/AtomUI.Desktop.Controls/Form/FormItem.cs @@ -748,15 +748,15 @@ public class FormItem : TemplatedControl, IFormItem var formItemAware = Content as IFormItemAware; Debug.Assert(formItemAware != null); var value = formItemAware.GetFormValue(); - var warningMessages = new List(); + var warningMessages = new List(Validators.Count); var hasWarning = false; var hasError = false; ValidateErrorMessages = null; ValidateWarningMessages = null; if (ValidateStrategy == FormValidateStrategy.Parallel || ValidateStrategy == FormValidateStrategy.Sequential) { - var tasks = new Dictionary, IFormValidator>(); - var errorMessages = new List(); + var tasks = new Dictionary, IFormValidator>(Validators.Count); + var errorMessages = new List(Validators.Count); if (ValidateStrategy == FormValidateStrategy.Parallel) { foreach (var validator in Validators) @@ -1258,4 +1258,4 @@ public class FormActionsItem : FormItem get => GetValue(IsValidateContentTypeProperty); set => SetValue(IsValidateContentTypeProperty, value); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/GroupBox/GroupBox.cs b/src/AtomUI.Desktop.Controls/GroupBox/GroupBox.cs index acfba9642..85c988ff9 100644 --- a/src/AtomUI.Desktop.Controls/GroupBox/GroupBox.cs +++ b/src/AtomUI.Desktop.Controls/GroupBox/GroupBox.cs @@ -144,8 +144,8 @@ public class GroupBox : ContentControl { var headerOffset = _headerContentContainer.TranslatePoint(new Point(0, 0), this) ?? default; var bounds = new Rect(headerOffset, _headerContentContainer.DesiredSize); - context.FillRectangle(Background ?? new SolidColorBrush(Colors.Transparent), bounds); + context.FillRectangle(Background ?? Brushes.Transparent, bounds); } } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/ImagePreviewer/AbstractImagePreviewer.cs b/src/AtomUI.Desktop.Controls/ImagePreviewer/AbstractImagePreviewer.cs index 6a1fbb58b..26e6a2e3d 100644 --- a/src/AtomUI.Desktop.Controls/ImagePreviewer/AbstractImagePreviewer.cs +++ b/src/AtomUI.Desktop.Controls/ImagePreviewer/AbstractImagePreviewer.cs @@ -208,7 +208,7 @@ public abstract class AbstractImagePreviewer : TemplatedControl, IMotionAwareCon { if (ItemsSource != null && ItemsSource.Count > 0) { - var effectiveSources = new List(); + var effectiveSources = new List(ItemsSource.Count); foreach (var source in ItemsSource) { try @@ -252,9 +252,7 @@ public abstract class AbstractImagePreviewer : TemplatedControl, IMotionAwareCon { try { - var sources = new List(); - sources.Add(LoadImageSource(FallbackImageSrc)); - SetCurrentValue(EffectiveSourcesProperty, sources); + SetCurrentValue(EffectiveSourcesProperty, new[] { LoadImageSource(FallbackImageSrc) }); } catch (Exception) { diff --git a/src/AtomUI.Desktop.Controls/Input/TextArea.cs b/src/AtomUI.Desktop.Controls/Input/TextArea.cs index 12cc4e8a6..adced54a0 100644 --- a/src/AtomUI.Desktop.Controls/Input/TextArea.cs +++ b/src/AtomUI.Desktop.Controls/Input/TextArea.cs @@ -506,21 +506,32 @@ public class TextArea : AvaloniaTextBox, { if (status == FormValidateStatus.Error) { - SetCurrentValue(StatusProperty, InputControlStatus.Error); + SetStatusIfChanged(InputControlStatus.Error); } else if (status == FormValidateStatus.Warning) { - SetCurrentValue(StatusProperty, InputControlStatus.Warning); + SetStatusIfChanged(InputControlStatus.Warning); } else { - SetCurrentValue(StatusProperty, InputControlStatus.Default); + SetStatusIfChanged(InputControlStatus.Default); } } protected virtual void NotifySetFeedBackControl(FormValidateFeedback? value) { - FormFeedback = value; + if (!ReferenceEquals(FormFeedback, value)) + { + FormFeedback = value; + } + } + + private void SetStatusIfChanged(InputControlStatus status) + { + if (Status != status) + { + SetCurrentValue(StatusProperty, status); + } } #endregion -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Input/TextAreaDecoratedBox.cs b/src/AtomUI.Desktop.Controls/Input/TextAreaDecoratedBox.cs index 34cd98295..9d19dbf4b 100644 --- a/src/AtomUI.Desktop.Controls/Input/TextAreaDecoratedBox.cs +++ b/src/AtomUI.Desktop.Controls/Input/TextAreaDecoratedBox.cs @@ -1,3 +1,4 @@ +using AtomUI.Media; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -104,6 +105,7 @@ internal class TextAreaDecoratedBox : AddOnDecoratedBox internal ScrollViewer? ScrollViewer { get; set; } internal TextArea? Owner { get; set; } private Control? _rightContentAddOn; + private IPen? _resizeIndicatorPen; protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -140,10 +142,14 @@ internal class TextAreaDecoratedBox : AddOnDecoratedBox var sideLength = 7d; var (line1Start, line1End) = CalculateResizeLine(bounds, sideLength); var (line2Start, line2End) = CalculateResizeLine(bounds, sideLength - 3); - var pen = new Pen(ResizeIndicatorLineBrush); + PenUtils.TryModifyOrCreate(ref _resizeIndicatorPen, ResizeIndicatorLineBrush, 1.0); + if (_resizeIndicatorPen is null) + { + return; + } - context.DrawLine(pen, line1Start, line1End); - context.DrawLine(pen, line2Start, line2End); + context.DrawLine(_resizeIndicatorPen, line1Start, line1End); + context.DrawLine(_resizeIndicatorPen, line2Start, line2End); } } @@ -156,4 +162,4 @@ internal class TextAreaDecoratedBox : AddOnDecoratedBox return (rightSidePoint, bottomSidePoint); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Input/TextBox.cs b/src/AtomUI.Desktop.Controls/Input/TextBox.cs index de3e11882..9dc809f40 100644 --- a/src/AtomUI.Desktop.Controls/Input/TextBox.cs +++ b/src/AtomUI.Desktop.Controls/Input/TextBox.cs @@ -311,13 +311,24 @@ public class TextBox : AvaloniaTextBox, void ICompactSpaceAware.NotifyPositionChange(SpaceItemPosition? position) { - IsUsedInCompactSpace = position != null; - CompactSpaceItemPosition = position; + var isUsedInCompactSpace = position != null; + if (IsUsedInCompactSpace != isUsedInCompactSpace) + { + IsUsedInCompactSpace = isUsedInCompactSpace; + } + + if (CompactSpaceItemPosition != position) + { + CompactSpaceItemPosition = position; + } } void ICompactSpaceAware.NotifyOrientationChange(Orientation orientation) { - CompactSpaceOrientation = orientation; + if (CompactSpaceOrientation != orientation) + { + CompactSpaceOrientation = orientation; + } } double ICompactSpaceAware.GetBorderThickness() @@ -371,7 +382,10 @@ public class TextBox : AvaloniaTextBox, protected virtual void NotifySetFeedBackControl(FormValidateFeedback? value) { - FormFeedback = value; + if (!ReferenceEquals(FormFeedback, value)) + { + FormFeedback = value; + } } #endregion } diff --git a/src/AtomUI.Desktop.Controls/ListBox/ListBox.cs b/src/AtomUI.Desktop.Controls/ListBox/ListBox.cs index 784b83dc2..6e9a477b0 100644 --- a/src/AtomUI.Desktop.Controls/ListBox/ListBox.cs +++ b/src/AtomUI.Desktop.Controls/ListBox/ListBox.cs @@ -593,7 +593,7 @@ public class ListBox : AvaloniaListBox, if (this is IListVirtualizingContextAware list && element is IListItemVirtualizingContextAware listItem) { - var context = new Dictionary(); + var context = new Dictionary(1); list.SaveVirtualizingContext(element, context); _virtualRestoreContext.Add(listItem.VirtualIndex, context); list.ClearContainerValues(element); @@ -673,4 +673,4 @@ public class ListBox : AvaloniaListBox, } } #endregion -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/ListView/ListView.Virtualizing.cs b/src/AtomUI.Desktop.Controls/ListView/ListView.Virtualizing.cs index 4e9da90ee..334eec75e 100644 --- a/src/AtomUI.Desktop.Controls/ListView/ListView.Virtualizing.cs +++ b/src/AtomUI.Desktop.Controls/ListView/ListView.Virtualizing.cs @@ -23,7 +23,7 @@ public partial class ListView : IListVirtualizingContextAware if (this is IListVirtualizingContextAware list && element is IListItemVirtualizingContextAware listItem) { - var context = new Dictionary(); + var context = new Dictionary(2); list.SaveVirtualizingContext(element, context); _virtualRestoreContext.Add(listItem.VirtualIndex, context); list.ClearContainerValues(element); @@ -120,4 +120,4 @@ public partial class ListView : IListVirtualizingContextAware } } #endregion -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Mentions/Mentions.cs b/src/AtomUI.Desktop.Controls/Mentions/Mentions.cs index 20a86da9b..9a9bb0e56 100644 --- a/src/AtomUI.Desktop.Controls/Mentions/Mentions.cs +++ b/src/AtomUI.Desktop.Controls/Mentions/Mentions.cs @@ -602,7 +602,7 @@ public class Mentions : TemplatedControl, } // Store a local cached copy of the data - _items = newValue == null ? null : new List(newValue.Cast()); + _items = newValue == null ? null : BuildItemsCache(newValue); // Clear and set the view on the selection adapter ClearView(); @@ -659,13 +659,25 @@ public class Mentions : TemplatedControl, ClearView(); if (OptionsSource != null) { - _items = new List(OptionsSource); + _items = BuildItemsCache(OptionsSource); } } // Refresh the observable collection used in the selection adapter. RefreshView(); } + + private static List BuildItemsCache(IEnumerable source) + { + var items = source is ICollection collection + ? new List(collection.Count) + : new List(); + foreach (var item in source) + { + items.Add((IMentionOption)item!); + } + return items; + } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/AtomUI.Desktop.Controls/Menu/MenuSeparator.cs b/src/AtomUI.Desktop.Controls/Menu/MenuSeparator.cs index 72a107f24..8ca494a88 100644 --- a/src/AtomUI.Desktop.Controls/Menu/MenuSeparator.cs +++ b/src/AtomUI.Desktop.Controls/Menu/MenuSeparator.cs @@ -1,3 +1,4 @@ +using AtomUI.Media; using AtomUI.Theme; using Avalonia; using Avalonia.Controls; @@ -30,6 +31,8 @@ public class MenuSeparator : AvaloniaSeparator } #endregion + private IPen? _linePen; + static MenuSeparator() { AffectsRender(LineWidthProperty); @@ -43,8 +46,11 @@ public class MenuSeparator : AvaloniaSeparator public override void Render(DrawingContext context) { var renderScaling = TopLevel.GetTopLevel(this)?.RenderScaling ?? 1.0d; - var linePen = new Pen((IBrush?)BorderBrush, LineWidth / renderScaling); + PenUtils.TryModifyOrCreate(ref _linePen, (IBrush?)BorderBrush, LineWidth / renderScaling); var offsetY = Bounds.Height / 2.0; - context.DrawLine(linePen, new Point(0, offsetY), new Point(Bounds.Right, offsetY)); + if (_linePen is not null) + { + context.DrawLine(_linePen, new Point(0, offsetY), new Point(Bounds.Right, offsetY)); + } } } diff --git a/src/AtomUI.Desktop.Controls/Message/WindowMessageManager.cs b/src/AtomUI.Desktop.Controls/Message/WindowMessageManager.cs index 142dbee94..d62270047 100644 --- a/src/AtomUI.Desktop.Controls/Message/WindowMessageManager.cs +++ b/src/AtomUI.Desktop.Controls/Message/WindowMessageManager.cs @@ -162,14 +162,32 @@ public class WindowMessageManager : TemplatedControl, IMessageManager, IMotionAw /// private void RemoveExcessMessages() { - var visibleMessages = _items!.OfType().Where(m => !m.IsClosing).ToList(); - var excessCount = visibleMessages.Count - MaxItems; + var visibleCount = 0; + foreach (var item in _items!) + { + if (item is MessageCard { IsClosing: false }) + { + visibleCount++; + } + } + + var excessCount = visibleCount - MaxItems; if (excessCount > 0) { - for (int i = 0; i < excessCount; i++) + foreach (var item in _items) { - visibleMessages[i].Close(); + if (item is not MessageCard { IsClosing: false } messageCard) + { + continue; + } + + messageCard.Close(); + excessCount--; + if (excessCount == 0) + { + break; + } } } } @@ -251,4 +269,4 @@ public class WindowMessageManager : TemplatedControl, IMessageManager, IMotionAw topLevel.TemplateApplied -= TopLevelOnTemplateApplied; InstallFromTopLevel(topLevel); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/NavMenu/Header/HorizontalNavMenuItemHeader.cs b/src/AtomUI.Desktop.Controls/NavMenu/Header/HorizontalNavMenuItemHeader.cs index 3684f3756..e294a807a 100644 --- a/src/AtomUI.Desktop.Controls/NavMenu/Header/HorizontalNavMenuItemHeader.cs +++ b/src/AtomUI.Desktop.Controls/NavMenu/Header/HorizontalNavMenuItemHeader.cs @@ -76,8 +76,20 @@ public class HorizontalNavMenuItemHeader : BaseNavMenuItemHeader { if (_activeIndicator != null) { - _activeIndicator.RenderTransform = new ScaleTransform(ActiveBarScaleX, 1.0); - _activeIndicator.RenderTransformOrigin = RelativePoint.Center; + if (_activeIndicator.RenderTransform is ScaleTransform scaleTransform) + { + scaleTransform.ScaleX = ActiveBarScaleX; + scaleTransform.ScaleY = 1.0; + } + else + { + _activeIndicator.RenderTransform = new ScaleTransform(ActiveBarScaleX, 1.0); + } + + if (_activeIndicator.RenderTransformOrigin != RelativePoint.Center) + { + _activeIndicator.RenderTransformOrigin = RelativePoint.Center; + } } } @@ -92,4 +104,4 @@ public class HorizontalNavMenuItemHeader : BaseNavMenuItemHeader base.OnLoaded(e); this.EnableTransitions(); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/NavMenu/NavMenu.cs b/src/AtomUI.Desktop.Controls/NavMenu/NavMenu.cs index 3305c0da0..e142ab766 100644 --- a/src/AtomUI.Desktop.Controls/NavMenu/NavMenu.cs +++ b/src/AtomUI.Desktop.Controls/NavMenu/NavMenu.cs @@ -481,7 +481,7 @@ public class NavMenu : ItemsControl, EnterDisableMotionRegion(); var segments = treeNodePath.Segments; IList items = Items; - var pathNodes = new List(); + var pathNodes = new List(segments.Count); NavMenuItem? previousItem = null; for (int i = 0; i < segments.Count; i++) { @@ -536,7 +536,7 @@ public class NavMenu : ItemsControl, { EnterDisableMotionRegion(); IList items = Items; - var pathItems = new List(); + var pathItems = new List(pathNodes.Count); NavMenuItem? previousItem = null; for (int i = 0; i < pathNodes.Count; i++) { diff --git a/src/AtomUI.Desktop.Controls/Notifications/Utils/NotificationProgressBarVisibleConverter.cs b/src/AtomUI.Desktop.Controls/Notifications/Utils/NotificationProgressBarVisibleConverter.cs index da55342af..d2480dc39 100644 --- a/src/AtomUI.Desktop.Controls/Notifications/Utils/NotificationProgressBarVisibleConverter.cs +++ b/src/AtomUI.Desktop.Controls/Notifications/Utils/NotificationProgressBarVisibleConverter.cs @@ -7,11 +7,12 @@ internal class NotificationProgressBarVisibleConverter : IMultiValueConverter { public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { - var items = values.ToList(); - if (items[0] is bool isShowProgress && items[1] is TimeSpan expiration) + if (values.Count >= 2 && + values[0] is bool isShowProgress && + values[1] is TimeSpan expiration) { return isShowProgress && expiration > TimeSpan.Zero; } return false; } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Notifications/WindowNotificationManager.cs b/src/AtomUI.Desktop.Controls/Notifications/WindowNotificationManager.cs index 47a1b279f..588711b92 100644 --- a/src/AtomUI.Desktop.Controls/Notifications/WindowNotificationManager.cs +++ b/src/AtomUI.Desktop.Controls/Notifications/WindowNotificationManager.cs @@ -119,8 +119,9 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager, _cardExpiredTimer.Tick += HandleCardExpiredTimer; _cleanupTimer = new DispatcherTimer { Interval = CleanupPollingIntervalProperty.GetDefaultValue(typeof(WindowNotificationManager)), Tag = this }; _cleanupTimer.Tick += HandleCleanupTimerTick; - _cleanupQueue = new Queue(); - _cleanupSet = new HashSet(); + var maxItems = MaxItemsProperty.GetDefaultValue(typeof(WindowNotificationManager)); + _cleanupQueue = new Queue(maxItems); + _cleanupSet = new HashSet(maxItems); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) diff --git a/src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDown.cs b/src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDown.cs index e40fb1cd3..e8215e3b6 100644 --- a/src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/AtomUI.Desktop.Controls/NumericUpDown/NumericUpDown.cs @@ -629,13 +629,24 @@ public class NumericUpDown : AvaloniaNumericUpDown, void ICompactSpaceAware.NotifyPositionChange(SpaceItemPosition? position) { - IsUsedInCompactSpace = position != null; - CompactSpaceItemPosition = position; + var isUsedInCompactSpace = position != null; + if (IsUsedInCompactSpace != isUsedInCompactSpace) + { + IsUsedInCompactSpace = isUsedInCompactSpace; + } + + if (CompactSpaceItemPosition != position) + { + CompactSpaceItemPosition = position; + } } void ICompactSpaceAware.NotifyOrientationChange(Orientation orientation) { - CompactSpaceOrientation = orientation; + if (CompactSpaceOrientation != orientation) + { + CompactSpaceOrientation = orientation; + } } double ICompactSpaceAware.GetBorderThickness() @@ -704,15 +715,23 @@ public class NumericUpDown : AvaloniaNumericUpDown, { if (status == FormValidateStatus.Error) { - SetCurrentValue(StatusProperty, InputControlStatus.Error); + SetStatusIfChanged(InputControlStatus.Error); } else if (status == FormValidateStatus.Warning) { - SetCurrentValue(StatusProperty, InputControlStatus.Warning); + SetStatusIfChanged(InputControlStatus.Warning); } else { - SetCurrentValue(StatusProperty, InputControlStatus.Default); + SetStatusIfChanged(InputControlStatus.Default); + } + } + + private void SetStatusIfChanged(InputControlStatus status) + { + if (Status != status) + { + SetCurrentValue(StatusProperty, status); } } #endregion diff --git a/src/AtomUI.Desktop.Controls/Primitives/AddOnDecoratedBox/AddOnDecoratedBox.cs b/src/AtomUI.Desktop.Controls/Primitives/AddOnDecoratedBox/AddOnDecoratedBox.cs index adc07e83c..4706941a7 100644 --- a/src/AtomUI.Desktop.Controls/Primitives/AddOnDecoratedBox/AddOnDecoratedBox.cs +++ b/src/AtomUI.Desktop.Controls/Primitives/AddOnDecoratedBox/AddOnDecoratedBox.cs @@ -659,11 +659,17 @@ internal class AddOnDecoratedBox : ContentControl, if (presenter == null) return; if (brush != null) { - presenter.SetCurrentValue(ForegroundProperty, brush); + if (!ReferenceEquals(presenter.Foreground, brush)) + { + presenter.SetCurrentValue(ForegroundProperty, brush); + } } else { - presenter.ClearValue(ForegroundProperty); + if (presenter.IsSet(ForegroundProperty)) + { + presenter.ClearValue(ForegroundProperty); + } } } @@ -678,15 +684,33 @@ internal class AddOnDecoratedBox : ContentControl, if (icon.Classes.Contains("skip-status")) continue; if (brush != null) { - icon.SetCurrentValue(Icon.FillBrushProperty, brush); - icon.SetCurrentValue(Icon.StrokeBrushProperty, brush); - icon.SetCurrentValue(Icon.ForegroundProperty, brush); + if (!ReferenceEquals(icon.FillBrush, brush)) + { + icon.SetCurrentValue(Icon.FillBrushProperty, brush); + } + if (!ReferenceEquals(icon.StrokeBrush, brush)) + { + icon.SetCurrentValue(Icon.StrokeBrushProperty, brush); + } + if (!ReferenceEquals(icon.Foreground, brush)) + { + icon.SetCurrentValue(Icon.ForegroundProperty, brush); + } } else { - icon.ClearValue(Icon.FillBrushProperty); - icon.ClearValue(Icon.StrokeBrushProperty); - icon.ClearValue(Icon.ForegroundProperty); + if (icon.IsSet(Icon.FillBrushProperty)) + { + icon.ClearValue(Icon.FillBrushProperty); + } + if (icon.IsSet(Icon.StrokeBrushProperty)) + { + icon.ClearValue(Icon.StrokeBrushProperty); + } + if (icon.IsSet(Icon.ForegroundProperty)) + { + icon.ClearValue(Icon.ForegroundProperty); + } } } } @@ -754,30 +778,43 @@ internal class AddOnDecoratedBox : ContentControl, } } - SetCurrentValue(InnerBoxCornerRadiusProperty, new CornerRadius(topLeftRadius, + var innerBoxCornerRadius = new CornerRadius(topLeftRadius, topRightRadius, bottomLeft: bottomLeftRadius, - bottomRight: bottomRightRadius)); + bottomRight: bottomRightRadius); + if (InnerBoxCornerRadius != innerBoxCornerRadius) + { + SetCurrentValue(InnerBoxCornerRadiusProperty, innerBoxCornerRadius); + } } else { - SetCurrentValue(InnerBoxCornerRadiusProperty, new CornerRadius(0)); + var innerBoxCornerRadius = new CornerRadius(0); + if (InnerBoxCornerRadius != innerBoxCornerRadius) + { + SetCurrentValue(InnerBoxCornerRadiusProperty, innerBoxCornerRadius); + } } } private void ConfigureInnerBoxBorderThickness() { + Thickness innerBoxBorderThickness; if (StyleVariant == InputControlStyleVariant.Borderless) { - SetCurrentValue(InnerBoxBorderThicknessProperty, new Thickness(0)); + innerBoxBorderThickness = new Thickness(0); } else if (StyleVariant == InputControlStyleVariant.Underlined) { - SetCurrentValue(InnerBoxBorderThicknessProperty, new Thickness(0, 0, 0, BorderThickness.Bottom)); + innerBoxBorderThickness = new Thickness(0, 0, 0, BorderThickness.Bottom); } else { - SetCurrentValue(InnerBoxBorderThicknessProperty, BorderThickness); + innerBoxBorderThickness = BorderThickness; + } + if (InnerBoxBorderThickness != innerBoxBorderThickness) + { + SetCurrentValue(InnerBoxBorderThicknessProperty, innerBoxBorderThickness); } } @@ -804,7 +841,7 @@ internal class AddOnDecoratedBoxContentFrame : Border base.OnPointerEntered(e); if (TemplatedParent is AddOnDecoratedBox decoratedBox) { - decoratedBox.IsInnerBoxHover = true; + SetInnerBoxHoverIfChanged(decoratedBox, true); } } @@ -813,7 +850,7 @@ internal class AddOnDecoratedBoxContentFrame : Border base.OnPointerExited(e); if (TemplatedParent is AddOnDecoratedBox decoratedBox) { - decoratedBox.IsInnerBoxHover = false; + SetInnerBoxHoverIfChanged(decoratedBox, false); } } @@ -822,8 +859,8 @@ internal class AddOnDecoratedBoxContentFrame : Border base.OnPointerPressed(e); if (TemplatedParent is AddOnDecoratedBox decoratedBox) { - decoratedBox.IsInnerBoxHover = true; - decoratedBox.IsInnerBoxPressed = true; + SetInnerBoxHoverIfChanged(decoratedBox, true); + SetInnerBoxPressedIfChanged(decoratedBox, true); } } @@ -832,8 +869,24 @@ internal class AddOnDecoratedBoxContentFrame : Border base.OnPointerReleased(e); if (TemplatedParent is AddOnDecoratedBox decoratedBox) { - decoratedBox.IsInnerBoxPressed = false; - decoratedBox.IsInnerBoxHover = true; + SetInnerBoxPressedIfChanged(decoratedBox, false); + SetInnerBoxHoverIfChanged(decoratedBox, true); + } + } + + private static void SetInnerBoxHoverIfChanged(AddOnDecoratedBox decoratedBox, bool isHover) + { + if (decoratedBox.IsInnerBoxHover != isHover) + { + decoratedBox.IsInnerBoxHover = isHover; + } + } + + private static void SetInnerBoxPressedIfChanged(AddOnDecoratedBox decoratedBox, bool isPressed) + { + if (decoratedBox.IsInnerBoxPressed != isPressed) + { + decoratedBox.IsInnerBoxPressed = isPressed; } } } diff --git a/src/AtomUI.Desktop.Controls/Primitives/CandidateList/CandidateList.cs b/src/AtomUI.Desktop.Controls/Primitives/CandidateList/CandidateList.cs index b03b7b64c..471b694ac 100644 --- a/src/AtomUI.Desktop.Controls/Primitives/CandidateList/CandidateList.cs +++ b/src/AtomUI.Desktop.Controls/Primitives/CandidateList/CandidateList.cs @@ -120,11 +120,11 @@ public class CandidateList : ListBox, ICandidateList var container = ContainerFromIndex(i); if (!SelectedItems.Contains(item)) { - container?.SetCurrentValue(IsEnabledProperty, false); + SetContainerEnabledIfChanged(container, false); } else { - container?.SetCurrentValue(IsEnabledProperty, true); + SetContainerEnabledIfChanged(container, true); } } } @@ -136,11 +136,19 @@ public class CandidateList : ListBox, ICandidateList var container = ContainerFromIndex(i); if (item is IListItemData selectOption) { - container?.SetCurrentValue(IsEnabledProperty, selectOption.IsEnabled); + SetContainerEnabledIfChanged(container, selectOption.IsEnabled); } } } } + + private static void SetContainerEnabledIfChanged(Control? container, bool isEnabled) + { + if (container is not null && container.IsEnabled != isEnabled) + { + container.SetCurrentValue(IsEnabledProperty, isEnabled); + } + } private void HandleCandidateSelectedIndexChanged(AvaloniaPropertyChangedEventArgs e) { @@ -162,7 +170,7 @@ public class CandidateList : ListBox, ICandidateList { if (e.OldValue != null && ContainerFromItem(e.OldValue) is CandidateListItem listItem) { - listItem.SetCurrentValue(CandidateListItem.IsCandidateSelectedProperty, false); + SetCandidateItemSelectedIfChanged(listItem, false); } } else @@ -208,11 +216,18 @@ public class CandidateList : ListBox, ICandidateList candidateContainer ??= ContainerFromIndex(index); if (candidateContainer is CandidateListItem childContainer) { - childContainer.SetCurrentValue(CandidateListItem.IsCandidateSelectedProperty, true); + SetCandidateItemSelectedIfChanged(childContainer, true); } - SetCurrentValue(CandidateSelectedItemProperty, Items[index]); - SetCurrentValue(CandidateSelectedIndexProperty, index); + var candidateSelectedItem = Items[index]; + if (!Equals(CandidateSelectedItem, candidateSelectedItem)) + { + SetCurrentValue(CandidateSelectedItemProperty, candidateSelectedItem); + } + if (CandidateSelectedIndex != index) + { + SetCurrentValue(CandidateSelectedIndexProperty, index); + } return true; } @@ -222,7 +237,15 @@ public class CandidateList : ListBox, ICandidateList index < ItemCount && ContainerFromIndex(index) is CandidateListItem listItem) { - listItem.SetCurrentValue(CandidateListItem.IsCandidateSelectedProperty, false); + SetCandidateItemSelectedIfChanged(listItem, false); + } + } + + private static void SetCandidateItemSelectedIfChanged(CandidateListItem item, bool isSelected) + { + if (item.IsCandidateSelected != isSelected) + { + item.SetCurrentValue(CandidateListItem.IsCandidateSelectedProperty, isSelected); } } @@ -308,6 +331,11 @@ public class CandidateList : ListBox, ICandidateList protected virtual void SelectPreviousCandidateItem() { + if (ItemCount == 0) + { + return; + } + if (_candidateSelectedIndex == -1) { CandidateSelectedIndex = SelectedIndex != -1 ? FindNextEnabledIndex(SelectedIndex, -1) : ItemCount - 1; @@ -320,6 +348,11 @@ public class CandidateList : ListBox, ICandidateList protected virtual void SelectNextCandidateItem() { + if (ItemCount == 0) + { + return; + } + if (_candidateSelectedIndex == -1) { CandidateSelectedIndex = SelectedIndex != -1 ? FindNextEnabledIndex(SelectedIndex, 1) : 0; @@ -334,7 +367,10 @@ public class CandidateList : ListBox, ICandidateList { if (CandidateSelectedItem != null) { - SetCurrentValue(SelectedItemProperty, CandidateSelectedItem); + if (!Equals(SelectedItem, CandidateSelectedItem)) + { + SetCurrentValue(SelectedItemProperty, CandidateSelectedItem); + } } RaiseEvent(new RoutedEventArgs(CommitEvent) { @@ -367,9 +403,18 @@ public class CandidateList : ListBox, ICandidateList private void ClearState() { - SelectedItems = null; - SelectedItem = null; - SelectedIndex = -1; + if (SelectedItems is not null) + { + SelectedItems = null; + } + if (SelectedItem is not null) + { + SelectedItem = null; + } + if (SelectedIndex != -1) + { + SelectedIndex = -1; + } } protected override void NotifyListBoxItemClicked(ListBoxItem item) @@ -440,8 +485,14 @@ public class CandidateList : ListBox, ICandidateList if (change.Property == FilterValueProperty || change.Property == FilterProperty) { - CandidateSelectedIndex = -1; - CandidateSelectedItem = null; + if (CandidateSelectedIndex != -1) + { + CandidateSelectedIndex = -1; + } + if (CandidateSelectedItem is not null) + { + CandidateSelectedItem = null; + } } } @@ -460,14 +511,25 @@ public class CandidateList : ListBox, ICandidateList protected override void ConfigureEmptyIndicator() { - SetCurrentValue(IsEffectiveEmptyVisibleProperty, IsShowEmptyIndicator && (ItemCount == 0 || (IsFiltering && FilterResultCount == 0))); + var isEffectiveEmptyVisible = IsShowEmptyIndicator && + (ItemCount == 0 || (IsFiltering && FilterResultCount == 0)); + if (IsEffectiveEmptyVisible != isEffectiveEmptyVisible) + { + SetCurrentValue(IsEffectiveEmptyVisibleProperty, isEffectiveEmptyVisible); + } } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - CandidateSelectedIndex = -1; - CandidateSelectedItem = null; + if (CandidateSelectedIndex != -1) + { + CandidateSelectedIndex = -1; + } + if (CandidateSelectedItem is not null) + { + CandidateSelectedItem = null; + } } #region 虚拟化上下文管理 @@ -479,7 +541,9 @@ public class CandidateList : ListBox, ICandidateList { if (CandidateSelectedIndex != -1) { - candidateListItem.SetCurrentValue(CandidateListItem.IsCandidateSelectedProperty, virtualListItem.VirtualIndex == CandidateSelectedIndex); + SetCandidateItemSelectedIfChanged( + candidateListItem, + virtualListItem.VirtualIndex == CandidateSelectedIndex); } } } @@ -489,7 +553,10 @@ public class CandidateList : ListBox, ICandidateList base.NotifyClearContainerForVirtualizingContext(item); if (item is CandidateListItem listItem) { - listItem.ClearValue(CandidateListItem.IsCandidateSelectedProperty); + if (listItem.IsSet(CandidateListItem.IsCandidateSelectedProperty)) + { + listItem.ClearValue(CandidateListItem.IsCandidateSelectedProperty); + } } } #endregion diff --git a/src/AtomUI.Desktop.Controls/Primitives/InfoPickerInput/InfoPickerInput.cs b/src/AtomUI.Desktop.Controls/Primitives/InfoPickerInput/InfoPickerInput.cs index c6f014b7d..0b0eaa567 100644 --- a/src/AtomUI.Desktop.Controls/Primitives/InfoPickerInput/InfoPickerInput.cs +++ b/src/AtomUI.Desktop.Controls/Primitives/InfoPickerInput/InfoPickerInput.cs @@ -383,7 +383,7 @@ public abstract class InfoPickerInput : TemplatedControl, public void ClosePickerFlyout() { - SetCurrentValue(IsPickerOpenProperty, false); + SetPickerOpenIfChanged(false); } private void HandleIsPickerOpenChanged(AvaloniaPropertyChangedEventArgs args) @@ -510,11 +510,11 @@ public abstract class InfoPickerInput : TemplatedControl, if (IsPointerInInfoInputBox(args.GetPosition(this)) && !ClickInClearUpButtonWithClearMode(args)) { - SetCurrentValue(IsPickerOpenProperty, true); + SetPickerOpenIfChanged(true); } else { - SetCurrentValue(IsPickerOpenProperty, false); + SetPickerOpenIfChanged(false); } args.Handled = true; } @@ -557,10 +557,7 @@ public abstract class InfoPickerInput : TemplatedControl, targetWidth = endOffsetX - startOffsetX; var bounds = new Rect(new Point(startOffsetX, offsetY), new Size(targetWidth, targetHeight)); - if (bounds.Contains(position)) - { - return true; - } + return bounds.Contains(position); } return false; @@ -598,14 +595,17 @@ public abstract class InfoPickerInput : TemplatedControl, { if (DecoratedBox is not null) { - SetCurrentValue(IsClearButtonVisibleProperty, DecoratedBox.IsInnerBoxHover && InfoInputBox?.IsReadOnly == false && InfoInputBox.Text?.Length > 0); + SetClearButtonVisibleIfChanged( + DecoratedBox.IsInnerBoxHover && + InfoInputBox?.IsReadOnly == false && + InfoInputBox.Text?.Length > 0); } } protected virtual void NotifyClearButtonClicked() { Clear(); - SetCurrentValue(IsClearButtonVisibleProperty, false); + SetClearButtonVisibleIfChanged(false); } protected virtual void NotifyPickerPresenterCreated(Control pickerPresenter) @@ -649,7 +649,7 @@ public abstract class InfoPickerInput : TemplatedControl, if (IsPickerOpen) { - SetCurrentValue(IsPickerOpenProperty, false); + SetPickerOpenIfChanged(false); } NotifyPickerPresenterCleared(pickerPresenter); @@ -671,6 +671,22 @@ public abstract class InfoPickerInput : TemplatedControl, protected abstract Control CreatePickerPresenter(); + protected void SetPickerOpenIfChanged(bool isOpen) + { + if (IsPickerOpen != isOpen) + { + SetCurrentValue(IsPickerOpenProperty, isOpen); + } + } + + protected void SetClearButtonVisibleIfChanged(bool isVisible) + { + if (IsClearButtonVisible != isVisible) + { + SetCurrentValue(IsClearButtonVisibleProperty, isVisible); + } + } + protected virtual Flyout CreatePickerFlyout() { throw new NotImplementedException("Use CreatePickerPresenter instead"); @@ -695,7 +711,7 @@ public abstract class InfoPickerInput : TemplatedControl, private void HandleWindowDeactivated(object? sender, EventArgs e) { - SetCurrentValue(IsPickerOpenProperty, false); + SetPickerOpenIfChanged(false); } private void HandleDecoratedBoxPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs args) @@ -755,13 +771,24 @@ public abstract class InfoPickerInput : TemplatedControl, void ICompactSpaceAware.NotifyPositionChange(SpaceItemPosition? position) { - IsUsedInCompactSpace = position != null; - CompactSpaceItemPosition = position; + var isUsedInCompactSpace = position != null; + if (IsUsedInCompactSpace != isUsedInCompactSpace) + { + IsUsedInCompactSpace = isUsedInCompactSpace; + } + + if (CompactSpaceItemPosition != position) + { + CompactSpaceItemPosition = position; + } } void ICompactSpaceAware.NotifyOrientationChange(Orientation orientation) { - CompactSpaceOrientation = orientation; + if (CompactSpaceOrientation != orientation) + { + CompactSpaceOrientation = orientation; + } } double ICompactSpaceAware.GetBorderThickness() @@ -804,13 +831,10 @@ public abstract class InfoPickerInput : TemplatedControl, protected void ConfigureShowArrowEffective() { - if (!IsArrowVisible) + var isArrowVisibleEffective = IsArrowVisible && PopupUtils.CanEnabledArrow(PickerPlacement); + if (IsArrowVisibleEffective != isArrowVisibleEffective) { - SetCurrentValue(IsArrowVisibleEffectiveProperty, false); - } - else - { - SetCurrentValue(IsArrowVisibleEffectiveProperty, PopupUtils.CanEnabledArrow(PickerPlacement)); + SetCurrentValue(IsArrowVisibleEffectiveProperty, isArrowVisibleEffective); } } @@ -820,7 +844,10 @@ public abstract class InfoPickerInput : TemplatedControl, if (arrowPosition.HasValue) { arrowPosition = ArrowPositionUtils.FlipArrowPosition(arrowPosition.Value, IsPopupHorizontalFlipped, IsPopupVerticalFlipped); - SetCurrentValue(ArrowPositionProperty, arrowPosition); + if (ArrowPosition != arrowPosition) + { + SetCurrentValue(ArrowPositionProperty, arrowPosition); + } } } @@ -862,21 +889,32 @@ public abstract class InfoPickerInput : TemplatedControl, { if (status == FormValidateStatus.Error) { - SetCurrentValue(StatusProperty, InputControlStatus.Error); + SetStatusIfChanged(InputControlStatus.Error); } else if (status == FormValidateStatus.Warning) { - SetCurrentValue(StatusProperty, InputControlStatus.Warning); + SetStatusIfChanged(InputControlStatus.Warning); } else { - SetCurrentValue(StatusProperty, InputControlStatus.Default); + SetStatusIfChanged(InputControlStatus.Default); + } + } + + private void SetStatusIfChanged(InputControlStatus status) + { + if (Status != status) + { + SetCurrentValue(StatusProperty, status); } } protected virtual void NotifySetFeedBackControl(FormValidateFeedback? value) { - FormFeedback = value; + if (!ReferenceEquals(FormFeedback, value)) + { + FormFeedback = value; + } } #endregion -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Primitives/InfoPickerInput/RangeInfoPickerInput.cs b/src/AtomUI.Desktop.Controls/Primitives/InfoPickerInput/RangeInfoPickerInput.cs index 0f5c49391..59a5d3d5b 100644 --- a/src/AtomUI.Desktop.Controls/Primitives/InfoPickerInput/RangeInfoPickerInput.cs +++ b/src/AtomUI.Desktop.Controls/Primitives/InfoPickerInput/RangeInfoPickerInput.cs @@ -1,4 +1,5 @@ using AtomUI.Animations; +using AtomUI.Utils; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -103,78 +104,43 @@ public abstract class RangeInfoPickerInput : InfoPickerInput } } - private bool IsPointerInInfoInputBox(Point position) + private bool IsPointerInTextBox(TextBox? textBox, Point position, double? contentStartOffsetX) { - if (InfoInputBox is null) + if (textBox is null) { return false; } - var pos = InfoInputBox.TranslatePoint(new Point(0, 0), this); + var pos = textBox.TranslatePoint(new Point(0, 0), this); if (!pos.HasValue) { return false; } - var targetWidth = InfoInputBox.Bounds.Width; - var targetHeight = InfoInputBox.Bounds.Height; - var startOffsetX = pos.Value.X; - var endOffsetX = startOffsetX + targetWidth; + var targetWidth = textBox.Bounds.Width; + var targetHeight = textBox.Bounds.Height; + var originalStartOffsetX = pos.Value.X; + var startOffsetX = contentStartOffsetX ?? originalStartOffsetX; + var endOffsetX = originalStartOffsetX + targetWidth; var offsetY = pos.Value.Y; - if (ContentLeftAddOn is Control leftContent) - { - var leftContentPos = leftContent.TranslatePoint(new Point(0, 0), this); - if (leftContentPos.HasValue) - { - startOffsetX = leftContentPos.Value.X + leftContent.Bounds.Width; - } - } targetWidth = endOffsetX - startOffsetX; var bounds = new Rect(new Point(startOffsetX, offsetY), new Size(targetWidth, targetHeight)); - if (bounds.Contains(position)) - { - return true; - } - - return false; + return bounds.Contains(position); } - private bool IsPointerInSecondaryTextBox(Point position) + private double? GetContentStartOffsetX() { - if (SecondaryInfoInputBox is null) - { - return false; - } - - var pos = SecondaryInfoInputBox.TranslatePoint(new Point(0, 0), this); - if (!pos.HasValue) - { - return false; - } - - var targetWidth = SecondaryInfoInputBox.Bounds.Width; - var targetHeight = SecondaryInfoInputBox.Bounds.Height; - var startOffsetX = pos.Value.X; - var endOffsetX = startOffsetX + targetWidth; - var offsetY = pos.Value.Y; if (ContentLeftAddOn is Control leftContent) { var leftContentPos = leftContent.TranslatePoint(new Point(0, 0), this); if (leftContentPos.HasValue) { - startOffsetX = leftContentPos.Value.X + leftContent.Bounds.Width; + return leftContentPos.Value.X + leftContent.Bounds.Width; } } - targetWidth = endOffsetX - startOffsetX; - var bounds = new Rect(new Point(startOffsetX, offsetY), new Size(targetWidth, targetHeight)); - if (bounds.Contains(position)) - { - return true; - } - - return false; + return null; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -202,19 +168,22 @@ public abstract class RangeInfoPickerInput : InfoPickerInput // Check if click is inside the input area (not in popup) if (PickerPopup?.IsInsidePopup(source) != true) { - if (IsPointerInInfoInputBox(args.GetPosition(this))) + var pointerPosition = args.GetPosition(this); + var contentStartOffsetX = GetContentStartOffsetX(); + if (IsPointerInTextBox(InfoInputBox, pointerPosition, contentStartOffsetX)) { - RangeActivatedPart = RangeActivatedPart.Start; - SetCurrentValue(IsPickerOpenProperty, true); + SetRangeActivatedPart(RangeActivatedPart.Start); + SetPickerOpenIfChanged(true); } - else if (IsPointerInSecondaryTextBox(args.GetPosition(this)) || !ClickInClearUpButtonWithClearMode(args)) + else if (IsPointerInTextBox(SecondaryInfoInputBox, pointerPosition, contentStartOffsetX) || + !ClickInClearUpButtonWithClearMode(args)) { - RangeActivatedPart = RangeActivatedPart.End; - SetCurrentValue(IsPickerOpenProperty, true); + SetRangeActivatedPart(RangeActivatedPart.End); + SetPickerOpenIfChanged(true); } else { - SetCurrentValue(IsPickerOpenProperty, false); + SetPickerOpenIfChanged(false); } args.Handled = true; } @@ -227,8 +196,14 @@ public abstract class RangeInfoPickerInput : InfoPickerInput var size = base.ArrangeOverride(finalSize).Inflate(borderThickness); if (RangePickerIndicator is not null) { - Canvas.SetLeft(RangePickerIndicator, PickerIndicatorOffsetX); - Canvas.SetTop(RangePickerIndicator, PickerIndicatorOffsetY); + if (!MathUtils.AreClose(Canvas.GetLeft(RangePickerIndicator), PickerIndicatorOffsetX)) + { + Canvas.SetLeft(RangePickerIndicator, PickerIndicatorOffsetX); + } + if (!MathUtils.AreClose(Canvas.GetTop(RangePickerIndicator), PickerIndicatorOffsetY)) + { + Canvas.SetTop(RangePickerIndicator, PickerIndicatorOffsetY); + } } return size; @@ -237,9 +212,13 @@ public abstract class RangeInfoPickerInput : InfoPickerInput protected override Size MeasureOverride(Size availableSize) { var size = base.MeasureOverride(availableSize); - if (DecoratedBox is not null) + if (DecoratedBox is not null && RangePickerIndicator is not null) { - PickerIndicatorOffsetY = DecoratedBox.DesiredSize.Height - RangePickerIndicator!.Height; + var indicatorOffsetY = DecoratedBox.DesiredSize.Height - RangePickerIndicator.Height; + if (!MathUtils.AreClose(PickerIndicatorOffsetY, indicatorOffsetY)) + { + PickerIndicatorOffsetY = indicatorOffsetY; + } } if (double.IsNaN(PickerIndicatorOffsetX)) @@ -256,26 +235,20 @@ public abstract class RangeInfoPickerInput : InfoPickerInput protected override void NotifyFlyoutAboutToClose(bool selectedIsValid) { - RangeActivatedPart = RangeActivatedPart.None; + SetRangeActivatedPart(RangeActivatedPart.None); } protected virtual void NotifyRangeActivatedPartChanged() { if (RangeActivatedPart == RangeActivatedPart.Start) { - PickerPlacement = PlacementMode.BottomEdgeAlignedLeft; - if (PickerPopup != null) - { - PickerPopup.PlacementTarget = InfoInputBox; - } + SetPickerPlacement(PlacementMode.BottomEdgeAlignedLeft); + SetPickerPopupPlacementTarget(InfoInputBox); } else if (RangeActivatedPart == RangeActivatedPart.End) { - PickerPlacement = PlacementMode.BottomEdgeAlignedRight; - if (PickerPopup != null) - { - PickerPopup.PlacementTarget = SecondaryInfoInputBox; - } + SetPickerPlacement(PlacementMode.BottomEdgeAlignedRight); + SetPickerPopupPlacementTarget(SecondaryInfoInputBox); } SetupPickerIndicatorPosition(); @@ -293,21 +266,69 @@ public abstract class RangeInfoPickerInput : InfoPickerInput if (_rangeActivatedPart == RangeActivatedPart.None) { - RangePickerIndicatorOpacity = 0; + SetRangePickerIndicatorOpacity(0); } else if (_rangeActivatedPart == RangeActivatedPart.Start) { - RangePickerIndicatorOpacity = 1; - RangePickerIndicator.Width = InfoInputBox.Bounds.Width; + SetRangePickerIndicatorOpacity(1); + SetRangePickerIndicatorWidth(InfoInputBox.Bounds.Width); var offset = InfoInputBox.TranslatePoint(new Point(0, 0), this) ?? default; - PickerIndicatorOffsetX = offset.X; + SetPickerIndicatorOffsetX(offset.X); } else if (_rangeActivatedPart == RangeActivatedPart.End) { - RangePickerIndicatorOpacity = 1; - RangePickerIndicator.Width = SecondaryInfoInputBox.Bounds.Width; + SetRangePickerIndicatorOpacity(1); + SetRangePickerIndicatorWidth(SecondaryInfoInputBox.Bounds.Width); var offset = SecondaryInfoInputBox.TranslatePoint(new Point(0, 0), this) ?? default; - PickerIndicatorOffsetX = offset.X; + SetPickerIndicatorOffsetX(offset.X); + } + } + + private void SetRangePickerIndicatorOpacity(double opacity) + { + if (!MathUtils.AreClose(RangePickerIndicatorOpacity, opacity)) + { + RangePickerIndicatorOpacity = opacity; + } + } + + private void SetRangePickerIndicatorWidth(double width) + { + if (RangePickerIndicator is not null && !MathUtils.AreClose(RangePickerIndicator.Width, width)) + { + RangePickerIndicator.Width = width; + } + } + + private void SetPickerIndicatorOffsetX(double offsetX) + { + if (!MathUtils.AreClose(PickerIndicatorOffsetX, offsetX)) + { + PickerIndicatorOffsetX = offsetX; + } + } + + private void SetRangeActivatedPart(RangeActivatedPart rangeActivatedPart) + { + if (RangeActivatedPart != rangeActivatedPart) + { + RangeActivatedPart = rangeActivatedPart; + } + } + + private void SetPickerPlacement(PlacementMode placement) + { + if (PickerPlacement != placement) + { + PickerPlacement = placement; + } + } + + private void SetPickerPopupPlacementTarget(Control? placementTarget) + { + if (PickerPopup != null && !ReferenceEquals(PickerPopup.PlacementTarget, placementTarget)) + { + PickerPopup.PlacementTarget = placementTarget; } } @@ -315,11 +336,12 @@ public abstract class RangeInfoPickerInput : InfoPickerInput { if (DecoratedBox is not null) { - SetCurrentValue(IsClearButtonVisibleProperty, DecoratedBox.IsInnerBoxHover && - InfoInputBox?.IsReadOnly == false && - InfoInputBox.Text?.Length > 0 && - SecondaryInfoInputBox?.IsReadOnly == false && - SecondaryInfoInputBox?.Text?.Length > 0); + SetClearButtonVisibleIfChanged( + DecoratedBox.IsInnerBoxHover && + InfoInputBox?.IsReadOnly == false && + InfoInputBox.Text?.Length > 0 && + SecondaryInfoInputBox?.IsReadOnly == false && + SecondaryInfoInputBox?.Text?.Length > 0); } } @@ -341,4 +363,4 @@ internal enum RangeActivatedPart None, Start, End -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Select/AbstractSelect.cs b/src/AtomUI.Desktop.Controls/Select/AbstractSelect.cs index 83fdc41b7..facba5ba3 100644 --- a/src/AtomUI.Desktop.Controls/Select/AbstractSelect.cs +++ b/src/AtomUI.Desktop.Controls/Select/AbstractSelect.cs @@ -820,13 +820,24 @@ public abstract class AbstractSelect : TemplatedControl, void ICompactSpaceAware.NotifyPositionChange(SpaceItemPosition? position) { - IsUsedInCompactSpace = position != null; - CompactSpaceItemPosition = position; + var isUsedInCompactSpace = position != null; + if (IsUsedInCompactSpace != isUsedInCompactSpace) + { + IsUsedInCompactSpace = isUsedInCompactSpace; + } + + if (CompactSpaceItemPosition != position) + { + CompactSpaceItemPosition = position; + } } void ICompactSpaceAware.NotifyOrientationChange(Orientation orientation) { - CompactSpaceOrientation = orientation; + if (CompactSpaceOrientation != orientation) + { + CompactSpaceOrientation = orientation; + } } double ICompactSpaceAware.GetBorderThickness() @@ -888,21 +899,32 @@ public abstract class AbstractSelect : TemplatedControl, { if (status == FormValidateStatus.Error) { - SetCurrentValue(StatusProperty, InputControlStatus.Error); + SetStatusIfChanged(InputControlStatus.Error); } else if (status == FormValidateStatus.Warning) { - SetCurrentValue(StatusProperty, InputControlStatus.Warning); + SetStatusIfChanged(InputControlStatus.Warning); } else { - SetCurrentValue(StatusProperty, InputControlStatus.Default); + SetStatusIfChanged(InputControlStatus.Default); } } protected virtual void NotifySetFeedBackControl(FormValidateFeedback? value) { - FormFeedback = value; + if (!ReferenceEquals(FormFeedback, value)) + { + FormFeedback = value; + } + } + + private void SetStatusIfChanged(InputControlStatus status) + { + if (Status != status) + { + SetCurrentValue(StatusProperty, status); + } } #endregion } diff --git a/src/AtomUI.Desktop.Controls/Select/Select.cs b/src/AtomUI.Desktop.Controls/Select/Select.cs index ea90af5b2..27dfdd8ef 100644 --- a/src/AtomUI.Desktop.Controls/Select/Select.cs +++ b/src/AtomUI.Desktop.Controls/Select/Select.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Reactive.Disposables; using AtomUI.Controls.Utils; using AtomUI.Theme; @@ -295,7 +296,7 @@ public partial class Select : AbstractSelect } if (_candidateList != null) { - _candidateList.SelectedItems = SelectedOptions?.ToList(); + _candidateList.SelectedItems = CopySelectedOptions(SelectedOptions); } } @@ -343,7 +344,7 @@ public partial class Select : AbstractSelect return false; } - var newSelection = new List(); + var newSelection = new List(SelectedOptions.Count); foreach (var selectedItem in SelectedOptions) { newSelection.Add(selectedItem); @@ -618,7 +619,7 @@ public partial class Select : AbstractSelect } else { - SelectedOptions = _candidateList.SelectedItems?.Cast().ToList(); + SelectedOptions = BuildSelectedOptionsList(_candidateList.SelectedItems); } } if (IsDropDownOpen) @@ -640,8 +641,8 @@ public partial class Select : AbstractSelect } if (Mode != SelectMode.Single) { - var currentSelectedSet = SelectedOptions?.ToHashSet() ?? new HashSet(); - var newSelectedSet = new HashSet(); + var currentSelectedSet = BuildSelectedOptionSet(SelectedOptions); + var newSelectedSet = new HashSet(currentSelectedSet.Count + e.AddedItems.Count); foreach (var item in currentSelectedSet) { newSelectedSet.Add(item); @@ -664,7 +665,7 @@ public partial class Select : AbstractSelect if (!newSelectedSet.SetEquals(currentSelectedSet)) { - SelectedOptions = newSelectedSet.ToList(); + SelectedOptions = CopySelectedOptions(newSelectedSet); } } } @@ -733,7 +734,7 @@ public partial class Select : AbstractSelect { if (SelectedOptions != null && SelectedOptions.Count > 0) { - _candidateList.SelectedItems = SelectedOptions?.ToList(); + _candidateList.SelectedItems = CopySelectedOptions(SelectedOptions); } } } @@ -755,7 +756,7 @@ public partial class Select : AbstractSelect } else { - _candidateList.SelectedItems = SelectedOptions?.ToList(); + _candidateList.SelectedItems = CopySelectedOptions(SelectedOptions); } } } @@ -864,7 +865,7 @@ public partial class Select : AbstractSelect { if (SelectedOptions != null) { - var selectedOptions = new List(); + var selectedOptions = new List(SelectedOptions.Count); foreach (var selectedItem in SelectedOptions) { selectedOptions.Add(selectedItem); @@ -923,7 +924,7 @@ public partial class Select : AbstractSelect { if (DefaultValues?.Count > 0) { - var selectedOptions = new List(); + var selectedOptions = new List(DefaultValues.Count); foreach (var defaultValue in DefaultValues) { foreach (var item in Options) @@ -969,7 +970,7 @@ public partial class Select : AbstractSelect return; } - var selected = new HashSet(); + var selected = new HashSet(SelectedOptions?.Count ?? 0); if (SelectedOptions != null) { foreach (var opt in SelectedOptions) @@ -978,7 +979,7 @@ public partial class Select : AbstractSelect } } - var toRemove = new List(); + var toRemove = new List(Options.Count); foreach (var item in Options) { if (item is ISelectOption option) @@ -1035,4 +1036,51 @@ public partial class Select : AbstractSelect } } #endregion + + private static List? BuildSelectedOptionsList(IEnumerable? source) + { + if (source == null) + { + return null; + } + + var selectedOptions = source is ICollection collection + ? new List(collection.Count) + : new List(); + foreach (var item in source) + { + selectedOptions.Add((ISelectOption)item!); + } + return selectedOptions; + } + + private static List? CopySelectedOptions(ICollection? source) + { + if (source == null) + { + return null; + } + + var selectedOptions = new List(source.Count); + foreach (var option in source) + { + selectedOptions.Add(option); + } + return selectedOptions; + } + + private static HashSet BuildSelectedOptionSet(ICollection? source) + { + var selectedOptions = source == null + ? new HashSet() + : new HashSet(source.Count); + if (source != null) + { + foreach (var option in source) + { + selectedOptions.Add(option); + } + } + return selectedOptions; + } } diff --git a/src/AtomUI.Desktop.Controls/Slider/SliderThumb.cs b/src/AtomUI.Desktop.Controls/Slider/SliderThumb.cs index 0b348f258..bf252e368 100644 --- a/src/AtomUI.Desktop.Controls/Slider/SliderThumb.cs +++ b/src/AtomUI.Desktop.Controls/Slider/SliderThumb.cs @@ -1,5 +1,6 @@ using AtomUI.Animations; using AtomUI.Controls; +using AtomUI.Media; using Avalonia.Threading; using Avalonia; using Avalonia.Automation.Peers; @@ -50,6 +51,8 @@ public class SliderThumb : TemplatedControl #endregion private Point? _lastPoint; + private IPen? _circlePen; + private IPen? _outlinePen; static SliderThumb() { @@ -200,12 +203,12 @@ public class SliderThumb : TemplatedControl // 绘制圆 var centerPos = new Point(Bounds.Width / 2, Bounds.Height / 2); var thumbCircleRadius = ThumbCircleSize / 2 + BorderThickness.Left / 2; - var circlePen = new Pen(BorderBrush, BorderThickness.Left); - context.DrawEllipse(Background, circlePen, centerPos, thumbCircleRadius, thumbCircleRadius); + PenUtils.TryModifyOrCreate(ref _circlePen, BorderBrush, BorderThickness.Left); + context.DrawEllipse(Background, _circlePen, centerPos, thumbCircleRadius, thumbCircleRadius); // 绘制 outline - var outlinePen = new Pen(OutlineBrush, OutlineThickness.Left); var outlinePenRadius = ThumbCircleSize / 2 + BorderThickness.Left / 2 + OutlineThickness.Left / 2; - context.DrawEllipse(null, outlinePen, centerPos, outlinePenRadius, outlinePenRadius); + PenUtils.TryModifyOrCreate(ref _outlinePen, OutlineBrush, OutlineThickness.Left); + context.DrawEllipse(null, _outlinePen, centerPos, outlinePenRadius, outlinePenRadius); } protected override void OnInitialized() @@ -219,4 +222,4 @@ public class SliderThumb : TemplatedControl base.OnLoaded(e); Dispatcher.Post(this.EnableTransitions); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Slider/SliderTrack.cs b/src/AtomUI.Desktop.Controls/Slider/SliderTrack.cs index 7e98c1683..b89b5df31 100644 --- a/src/AtomUI.Desktop.Controls/Slider/SliderTrack.cs +++ b/src/AtomUI.Desktop.Controls/Slider/SliderTrack.cs @@ -285,6 +285,8 @@ public class SliderTrack : TemplatedControl private double Density { get; set; } private IDisposable? _focusProcessDisposable; private Size _markLabelSize; + private IPen? _markBorderPen; + private IPen? _markBorderActivePen; static SliderTrack() { @@ -890,8 +892,8 @@ public class SliderTrack : TemplatedControl { if (Marks?.Count > 0) { - _renderContextData.MarkRects = new List<(Rect, int, bool)>(); - _renderContextData.MarkTextRects = new List<(Rect, int, bool, FormattedText)>(); + _renderContextData.MarkRects = new List<(Rect, int, bool)>(Marks.Count); + _renderContextData.MarkTextRects = new List<(Rect, int, bool, FormattedText)>(Marks.Count); var railRect = _renderContextData.RailRect; var railCenterY = railRect.Center.Y; var markIncluded = false; @@ -959,8 +961,8 @@ public class SliderTrack : TemplatedControl { if (Marks?.Count > 0) { - _renderContextData.MarkRects = new List<(Rect, int, bool)>(); - _renderContextData.MarkTextRects = new List<(Rect, int, bool, FormattedText)>(); + _renderContextData.MarkRects = new List<(Rect, int, bool)>(Marks.Count); + _renderContextData.MarkTextRects = new List<(Rect, int, bool, FormattedText)>(Marks.Count); var railRect = _renderContextData.RailRect; var railCenterX = railRect.Center.X; var markIncluded = false; @@ -1031,9 +1033,20 @@ public class SliderTrack : TemplatedControl { var centerPos = markRectEntry.Item1.Center; var radius = SliderMarkSize / 2; - var circlePen = new Pen(markRectEntry.Item3 ? MarkBorderActiveBrush : MarkBorderBrush, - MarkBorderThickness.Left); - context.DrawEllipse(MarkBackgroundBrush, circlePen, centerPos, radius, radius); + if (markRectEntry.Item3) + { + PenUtils.TryModifyOrCreate(ref _markBorderActivePen, + MarkBorderActiveBrush, + MarkBorderThickness.Left); + context.DrawEllipse(MarkBackgroundBrush, _markBorderActivePen, centerPos, radius, radius); + } + else + { + PenUtils.TryModifyOrCreate(ref _markBorderPen, + MarkBorderBrush, + MarkBorderThickness.Left); + context.DrawEllipse(MarkBackgroundBrush, _markBorderPen, centerPos, radius, radius); + } } } @@ -1064,4 +1077,4 @@ public class SliderTrack : TemplatedControl public List<(Rect, int, bool)>? MarkRects { get; set; } public List<(Rect, int, bool, FormattedText)>? MarkTextRects { get; set; } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Space/CompactSpace.cs b/src/AtomUI.Desktop.Controls/Space/CompactSpace.cs index 056b7f271..db807ef65 100644 --- a/src/AtomUI.Desktop.Controls/Space/CompactSpace.cs +++ b/src/AtomUI.Desktop.Controls/Space/CompactSpace.cs @@ -121,7 +121,7 @@ public class CompactSpace : TemplatedControl, { case NotifyCollectionChangedAction.Add: { - var compactSpaceItems = new List(); + var compactSpaceItems = new List(e.NewItems!.Count); foreach (var newItem in e.NewItems!.OfType()) { EnsureCompactSpaceItem(newItem); @@ -142,8 +142,12 @@ public class CompactSpace : TemplatedControl, case NotifyCollectionChangedAction.Remove: { - var oldItems = new HashSet(e.OldItems!.OfType()); - var oldCompactSpaceItems = new List(); + var oldItems = new HashSet(e.OldItems!.Count); + foreach (var oldItem in e.OldItems.OfType()) + { + oldItems.Add(oldItem); + } + var oldCompactSpaceItems = new List(e.OldItems.Count); foreach (var oldItem in _contentLayout.Children) { if (oldItem is CompactSpaceItem compactSpaceItem) @@ -330,7 +334,7 @@ public class CompactSpace : TemplatedControl, base.OnApplyTemplate(e); ClearChildClassesChangedHandlers(); _contentLayout = e.NameScope.Get("PART_ContentLayout"); - var compactSpaceItems = new List(); + var compactSpaceItems = new List(Children.Count); foreach (var child in Children) { EnsureCompactSpaceItem(child); @@ -486,31 +490,37 @@ public class CompactSpace : TemplatedControl, } // 计算位置 - var realChildren = children.Where(child => child is CompactSpaceItem compactSpaceItem && compactSpaceItem.Child is not CompactSpaceFiller).ToList(); - if (realChildren.Count == 1) + var realChildrenCount = CountRealChildren(children); + if (realChildrenCount == 1) { - if (realChildren[0] is ICompactSpaceAware compactSpaceAware) + if (FindFirstRealChild(children) is ICompactSpaceAware compactSpaceAware) { compactSpaceAware.NotifyPositionChange(SpaceItemPosition.First | SpaceItemPosition.Last); } } else { - for (var i = 0; i < realChildren.Count; i++) + var realChildIndex = 0; + for (var i = 0; i < children.Count; i++) { - var child = realChildren[i]; + var child = children[i]; + if (!IsRealChild(child)) + { + continue; + } + if (child is CompactSpaceItem compactSpaceItem) { - compactSpaceItem.PositionIndex = i; + compactSpaceItem.PositionIndex = realChildIndex; } if (child is ICompactSpaceAware compactSpaceAware) { compactSpaceAware.NotifyOrientationChange(Orientation); - if (i == 0) + if (realChildIndex == 0) { compactSpaceAware.NotifyPositionChange(SpaceItemPosition.First); } - else if (i == realChildren.Count - 1) + else if (realChildIndex == realChildrenCount - 1) { compactSpaceAware.NotifyPositionChange(SpaceItemPosition.Last); } @@ -519,9 +529,44 @@ public class CompactSpace : TemplatedControl, compactSpaceAware.NotifyPositionChange(SpaceItemPosition.Middle); } } + + realChildIndex++; } } } + + private static int CountRealChildren(Avalonia.Controls.Controls children) + { + var count = 0; + for (var i = 0; i < children.Count; i++) + { + if (IsRealChild(children[i])) + { + count++; + } + } + + return count; + } + + private static Control? FindFirstRealChild(Avalonia.Controls.Controls children) + { + for (var i = 0; i < children.Count; i++) + { + var child = children[i]; + if (IsRealChild(child)) + { + return child; + } + } + + return null; + } + + private static bool IsRealChild(Control child) + { + return child is CompactSpaceItem { Child: not CompactSpaceFiller }; + } private void HandleChildrenPropertyChanged(object? sender, PropertyChangedEventArgs e) { @@ -641,4 +686,4 @@ public class CompactSpace : TemplatedControl, child.SetTemplatedParent(TemplatedParent); } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Space/CompactSpaceAddOn.cs b/src/AtomUI.Desktop.Controls/Space/CompactSpaceAddOn.cs index ebc35511d..4da6427e1 100644 --- a/src/AtomUI.Desktop.Controls/Space/CompactSpaceAddOn.cs +++ b/src/AtomUI.Desktop.Controls/Space/CompactSpaceAddOn.cs @@ -196,13 +196,24 @@ public class CompactSpaceAddOn : TemplatedControl, void ICompactSpaceAware.NotifyPositionChange(SpaceItemPosition? position) { - IsUsedInCompactSpace = position != null; - CompactSpaceItemPosition = position; + var isUsedInCompactSpace = position != null; + if (IsUsedInCompactSpace != isUsedInCompactSpace) + { + IsUsedInCompactSpace = isUsedInCompactSpace; + } + + if (CompactSpaceItemPosition != position) + { + CompactSpaceItemPosition = position; + } } void ICompactSpaceAware.NotifyOrientationChange(Orientation orientation) { - CompactSpaceOrientation = orientation; + if (CompactSpaceOrientation != orientation) + { + CompactSpaceOrientation = orientation; + } } bool ICompactSpaceAware.IgnoreZIndexChange() => true; @@ -211,4 +222,4 @@ public class CompactSpaceAddOn : TemplatedControl, { return CompactSpaceOrientation == Orientation.Horizontal ? BorderThickness.Left : BorderThickness.Top; } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Space/CompactSpaceItem.cs b/src/AtomUI.Desktop.Controls/Space/CompactSpaceItem.cs index 65ae268af..3ffbb652d 100644 --- a/src/AtomUI.Desktop.Controls/Space/CompactSpaceItem.cs +++ b/src/AtomUI.Desktop.Controls/Space/CompactSpaceItem.cs @@ -84,8 +84,16 @@ internal class CompactSpaceItem : Decorator, ICompactSpaceAware void ICompactSpaceAware.NotifyPositionChange(SpaceItemPosition? position) { - IsUsedInCompactSpace = position != null; - CompactSpaceItemPosition = position; + var isUsedInCompactSpace = position != null; + if (IsUsedInCompactSpace != isUsedInCompactSpace) + { + IsUsedInCompactSpace = isUsedInCompactSpace; + } + + if (CompactSpaceItemPosition != position) + { + CompactSpaceItemPosition = position; + } } bool ICompactSpaceAware.IsAlwaysActiveZIndex() @@ -116,24 +124,28 @@ internal class CompactSpaceItem : Decorator, ICompactSpaceAware CompactSpaceItemPosition == SpaceItemPosition.First || (CompactSpaceItemPosition.Value.HasFlag(SpaceItemPosition.First) && CompactSpaceItemPosition.Value.HasFlag(SpaceItemPosition.Last))) { + ClearOffsetTransform(); return size; } var borderThickness = (this as ICompactSpaceAware).GetBorderThickness(); var delta = borderThickness * PositionIndex; if (CompactSpaceOrientation == Orientation.Horizontal) { - RenderTransform = new TranslateTransform(-delta, 0); + SetOffsetTransform(-delta, 0); } else { - RenderTransform = new TranslateTransform(0, -delta); + SetOffsetTransform(0, -delta); } return size; } void ICompactSpaceAware.NotifyOrientationChange(Orientation orientation) { - CompactSpaceOrientation = orientation; + if (CompactSpaceOrientation != orientation) + { + CompactSpaceOrientation = orientation; + } } double ICompactSpaceAware.GetBorderThickness() @@ -145,6 +157,26 @@ internal class CompactSpaceItem : Decorator, ICompactSpaceAware return 0.0; } + + private void SetOffsetTransform(double x, double y) + { + if (RenderTransform is TranslateTransform transform && + Math.Abs(transform.X - x) < 0.001 && + Math.Abs(transform.Y - y) < 0.001) + { + return; + } + + RenderTransform = new TranslateTransform(x, y); + } + + private void ClearOffsetTransform() + { + if (RenderTransform != null) + { + RenderTransform = null; + } + } private void ConfigureItemSize(CompactSpaceSize size, bool isUsedInCompactSpace, Orientation compactSpaceOrientation) { @@ -203,4 +235,4 @@ internal class CompactSpaceItem : Decorator, ICompactSpaceAware } } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cs b/src/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cs index 54b904938..c39b1376f 100644 --- a/src/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cs +++ b/src/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cs @@ -1517,7 +1517,7 @@ internal class SplitterPanel : Panel var handleSpace = Math.Max(0, GetEffectiveHandleCount()) * GetHandleSpacing(); var availableLength = Math.Max(0, totalLength - handleSpace); var sizes = new List(_panels.Count); - var flexibleIndices = new List(); + var flexibleCount = 0; var minSizes = new double[_panels.Count]; var maxSizes = new double[_panels.Count]; @@ -1558,17 +1558,20 @@ internal class SplitterPanel : Panel else { sizes.Add(double.NaN); - flexibleIndices.Add(i); + flexibleCount++; } } var remaining = availableLength - sumExplicit; - if (flexibleIndices.Count > 0) + if (flexibleCount > 0) { - var each = remaining / flexibleIndices.Count; - foreach (var index in flexibleIndices) + var each = remaining / flexibleCount; + for (var i = 0; i < sizes.Count; i++) { - sizes[index] = each; + if (double.IsNaN(sizes[i])) + { + sizes[i] = each; + } } } else if (_panels.Count > 0 && Math.Abs(remaining) > 0.001) @@ -1598,7 +1601,7 @@ internal class SplitterPanel : Panel var guard = 0; while (Math.Abs(delta) > 0.5 && guard < 100) { - var indices = new List(); + var adjustableCount = 0; for (var i = 0; i < sizes.Count; i++) { if (Splitter.GetIsCollapsed(_panels[i])) @@ -1608,24 +1611,35 @@ internal class SplitterPanel : Panel if (delta > 0 && sizes[i] < maxSizes[i]) { - indices.Add(i); + adjustableCount++; } else if (delta < 0 && sizes[i] > minSizes[i]) { - indices.Add(i); + adjustableCount++; } } - if (indices.Count == 0) + if (adjustableCount == 0) { break; } - var per = delta / indices.Count; + var per = delta / adjustableCount; var adjusted = 0d; - foreach (var index in indices) + for (var index = 0; index < sizes.Count; index++) { + if (Splitter.GetIsCollapsed(_panels[index])) + { + continue; + } + + if ((delta > 0 && sizes[index] >= maxSizes[index]) || + (delta < 0 && sizes[index] <= minSizes[index])) + { + continue; + } + var target = sizes[index] + per; var newSize = delta > 0 ? Math.Min(target, maxSizes[index]) diff --git a/src/AtomUI.Desktop.Controls/TabControl/BaseTabScrollViewer.cs b/src/AtomUI.Desktop.Controls/TabControl/BaseTabScrollViewer.cs index ad1157371..319f76a8a 100644 --- a/src/AtomUI.Desktop.Controls/TabControl/BaseTabScrollViewer.cs +++ b/src/AtomUI.Desktop.Controls/TabControl/BaseTabScrollViewer.cs @@ -1,10 +1,9 @@ -using System.Globalization; -using Avalonia; +using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Converters; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Media; +using AtomUI.Utils; using Colors = Avalonia.Media.Colors; using GradientStop = Avalonia.Media.GradientStop; @@ -193,45 +192,27 @@ internal abstract class BaseTabScrollViewer : ScrollViewer private void SetupIndicatorsVisibility() { - var args = new List(); - - object? scrollUpVisibility; - object? scrollDownVisibility; + bool scrollUpVisibility; + bool scrollDownVisibility; if (TabStripPlacement == Dock.Top || TabStripPlacement == Dock.Bottom) { - args.Add(ScrollBarVisibility.Auto); - args.Add(Offset.X); - args.Add(Extent.Width); - args.Add(Viewport.Width); - scrollUpVisibility = - MenuScrollingVisibilityConverter.Instance.Convert(args, typeof(bool), 0d, CultureInfo.CurrentCulture); - scrollDownVisibility = - MenuScrollingVisibilityConverter.Instance.Convert(args, typeof(bool), 100d, CultureInfo.CurrentCulture); + scrollUpVisibility = CalculateEdgeIndicatorVisibility(Offset.X, Extent.Width, Viewport.Width, 0d); + scrollDownVisibility = CalculateEdgeIndicatorVisibility(Offset.X, Extent.Width, Viewport.Width, 100d); } else { - args.Add(ScrollBarVisibility.Auto); - args.Add(Offset.Y); - args.Add(Extent.Height); - args.Add(Viewport.Height); - scrollUpVisibility = - MenuScrollingVisibilityConverter.Instance.Convert(args, typeof(bool), 0d, CultureInfo.CurrentCulture); - scrollDownVisibility = - MenuScrollingVisibilityConverter.Instance.Convert(args, typeof(bool), 100d, CultureInfo.CurrentCulture); + scrollUpVisibility = CalculateEdgeIndicatorVisibility(Offset.Y, Extent.Height, Viewport.Height, 0d); + scrollDownVisibility = CalculateEdgeIndicatorVisibility(Offset.Y, Extent.Height, Viewport.Height, 100d); } - if (StartEdgeIndicator is not null && - scrollUpVisibility is not null && - scrollUpVisibility != AvaloniaProperty.UnsetValue) + if (StartEdgeIndicator is not null) { - StartEdgeIndicator.IsVisible = (bool)scrollUpVisibility; + StartEdgeIndicator.IsVisible = scrollUpVisibility; } - if (EndEdgeIndicator is not null && - scrollDownVisibility is not null && - scrollDownVisibility != AvaloniaProperty.UnsetValue) + if (EndEdgeIndicator is not null) { - EndEdgeIndicator.IsVisible = (bool)scrollDownVisibility; + EndEdgeIndicator.IsVisible = scrollDownVisibility; } if (MenuIndicator is not null) @@ -241,4 +222,15 @@ internal abstract class BaseTabScrollViewer : ScrollViewer MenuIndicator.IsVisible = startEdgeVisible || endEdgeVisible; } } -} \ No newline at end of file + + private static bool CalculateEdgeIndicatorVisibility(double offset, double extent, double viewport, double target) + { + if (MathUtils.AreClose(extent, viewport)) + { + return false; + } + + var percent = Math.Clamp(offset * 100.0 / (extent - viewport), 0, 100); + return !MathUtils.AreClose(percent, target); + } +} diff --git a/src/AtomUI.Desktop.Controls/TextBlock/HighlightableTextBlock.cs b/src/AtomUI.Desktop.Controls/TextBlock/HighlightableTextBlock.cs index d2c2ebcca..6c9a0faab 100644 --- a/src/AtomUI.Desktop.Controls/TextBlock/HighlightableTextBlock.cs +++ b/src/AtomUI.Desktop.Controls/TextBlock/HighlightableTextBlock.cs @@ -96,16 +96,18 @@ public class HighlightableTextBlock : TextBlock return; } - var highlightRanges = CalculateHighlightRanges(HighlightWords, Text); - var runs = new InlineCollection(); - - if (highlightRanges.Count == 0) + var firstHighlightIndex = Text.IndexOf(HighlightWords, StringComparison.OrdinalIgnoreCase); + if (firstHighlightIndex == -1) { - runs.Add(CreateRun(Text, isHighlighted: false, bold: false)); - Inlines = runs; + var plainRuns = new InlineCollection(); + plainRuns.Add(CreateRun(Text, isHighlighted: false, bold: false)); + Inlines = plainRuns; return; } + var highlightRanges = CalculateHighlightRanges(HighlightWords, Text, firstHighlightIndex); + var runs = new InlineCollection(); + var cursor = 0; foreach (var (start, end) in highlightRanges) { @@ -152,23 +154,23 @@ public class HighlightableTextBlock : TextBlock return false; } - protected List<(int, int)> CalculateHighlightRanges(string highlightWords, string text) + protected List<(int, int)> CalculateHighlightRanges(string highlightWords, string text, int firstIndex = 0) { var ranges = new List<(int, int)>(); - int currentIndex = 0; var highlightLength = highlightWords.Length; + var foundIndex = firstIndex; while (true) { - int foundIndex = text.IndexOf(highlightWords, currentIndex, StringComparison.OrdinalIgnoreCase); if (foundIndex == -1) // 如果没有找到,退出循环 { break; } - currentIndex = foundIndex + highlightLength; + var currentIndex = foundIndex + highlightLength; ranges.Add((foundIndex, currentIndex)); + foundIndex = text.IndexOf(highlightWords, currentIndex, StringComparison.OrdinalIgnoreCase); } return ranges; } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/TimePicker/TimeView/DateTimePickerPanel.cs b/src/AtomUI.Desktop.Controls/TimePicker/TimeView/DateTimePickerPanel.cs index 21d679c70..1d4eb2bc1 100644 --- a/src/AtomUI.Desktop.Controls/TimePicker/TimeView/DateTimePickerPanel.cs +++ b/src/AtomUI.Desktop.Controls/TimePicker/TimeView/DateTimePickerPanel.cs @@ -690,9 +690,19 @@ internal class DateTimePickerPanel : Panel, if (newValue % Increment != 0) { - var items = Enumerable.Range(MinimumValue, MaximumValue + 1).Where(i => i % Increment == 0).ToList(); - var nearest = items.Aggregate((x, y) => Math.Abs(x - newValue) > Math.Abs(y - newValue) ? y : x); - return items.IndexOf(nearest) * Increment; + var lower = newValue - newValue % Increment; + var upper = lower + Increment; + if (lower < MinimumValue) + { + return MinimumValue; + } + + if (upper > MaximumValue) + { + return lower; + } + + return newValue - lower <= upper - newValue ? lower : upper; } return newValue; @@ -789,4 +799,4 @@ internal class DateTimePickerPanel : Panel, } } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Tour/Tour.cs b/src/AtomUI.Desktop.Controls/Tour/Tour.cs index 1394fe8d9..765f016fb 100644 --- a/src/AtomUI.Desktop.Controls/Tour/Tour.cs +++ b/src/AtomUI.Desktop.Controls/Tour/Tour.cs @@ -347,18 +347,27 @@ public class Tour : TemplatedControl, IMotionAwareControl { if (_stepsView != null) { - var actions = CustomActions.Cast().ToList(); - foreach (var action in actions) - { - if (action is ITourAction tourAction) - { - tourAction.NotifyAttached(this); - } - } - _stepsView.CustomActions = actions; + _stepsView.CustomActions = PrepareCustomActions(); } } + private Control[] PrepareCustomActions() + { + var actions = new Control[CustomActions.Count]; + var index = 0; + foreach (var item in CustomActions) + { + var action = (Control)item!; + if (action is ITourAction tourAction) + { + tourAction.NotifyAttached(this); + } + actions[index++] = action; + } + + return actions; + } + private void ConfigureDefaultValues() { foreach (var item in Steps) @@ -503,17 +512,7 @@ public class Tour : TemplatedControl, IMotionAwareControl { _stepsView.CloseRequest += HandleCloseRequest; _stepsView.NavRequest += HandleNavRequest; - var actions = CustomActions.Cast().ToList(); - - foreach (var action in actions) - { - if (action is ITourAction tourAction) - { - tourAction.NotifyAttached(this); - } - } - - _stepsView.CustomActions = actions; + _stepsView.CustomActions = PrepareCustomActions(); } } diff --git a/src/AtomUI.Desktop.Controls/Transfer/AbstractTransfer.cs b/src/AtomUI.Desktop.Controls/Transfer/AbstractTransfer.cs index abc4d352f..78cb114e5 100644 --- a/src/AtomUI.Desktop.Controls/Transfer/AbstractTransfer.cs +++ b/src/AtomUI.Desktop.Controls/Transfer/AbstractTransfer.cs @@ -466,7 +466,7 @@ public abstract class AbstractTransfer: TemplatedControl, private void HandleItemRemoved(object? sender, TransferItemsRemovedEventArgs args) { - var currentSet = new HashSet(); + var currentSet = new HashSet(TargetKeys?.Count ?? 0); if (TargetKeys != null) { foreach (var targetKey in TargetKeys) @@ -482,7 +482,7 @@ public abstract class AbstractTransfer: TemplatedControl, currentSet.Remove(item.ItemKey ?? default); } } - SetCurrentValue(TargetKeysProperty, currentSet.ToList()); + SetCurrentValue(TargetKeysProperty, BuildEntityKeyList(currentSet)); _sourceViewDecorator?.NotifyTransferCompleted(TransferDirection.ToSource); } @@ -518,29 +518,36 @@ public abstract class AbstractTransfer: TemplatedControl, var targetPanelSourceChanged = false; IList? sourceItemKeys = null; IList? targetItemKeys = null; + var targetKeySet = BuildTargetKeySet(TargetKeys); if (changeType.HasFlag(FilterChangeType.Source)) { var sourcePanelSource = ItemsSource? - .Where(item => !(TargetKeys?.Contains(item.ItemKey ?? default) ?? false)) + .Where(item => !(targetKeySet?.Contains(item.ItemKey ?? default) ?? false)) .Where(item => !IsFilterEnabled || string.IsNullOrEmpty(SourceFilterValue) || (Filter?.Filter(FilterValueSelector != null ? FilterValueSelector(item) : item, SourceFilterValue) ?? false)) .ToArray(); sourcePanelSourceChanged = SourceViewSource != sourcePanelSource; SourceViewSource = sourcePanelSource; - sourceItemKeys = sourcePanelSource?.Select(item => item.ItemKey ?? default).ToList(); + sourceItemKeys = BuildItemKeyList(sourcePanelSource); } if (changeType.HasFlag(FilterChangeType.Target)) { - var targetPanelSource = ItemsSource? - .Where(item => TargetKeys?.Contains(item.ItemKey ?? default) ?? false) - .Where(item => !IsFilterEnabled || string.IsNullOrEmpty(TargetFilterValue) || - (Filter?.Filter(FilterValueSelector != null ? FilterValueSelector(item) : item, TargetFilterValue) ?? false)) - .ToArray(); - TargetViewSource = targetPanelSource; + IEnumerable? targetPanelSource = null; + if (ItemsSource != null) + { + targetPanelSource = targetKeySet == null + ? Array.Empty() + : ItemsSource + .Where(item => targetKeySet.Contains(item.ItemKey ?? default)) + .Where(item => !IsFilterEnabled || string.IsNullOrEmpty(TargetFilterValue) || + (Filter?.Filter(FilterValueSelector != null ? FilterValueSelector(item) : item, TargetFilterValue) ?? false)) + .ToArray(); + } targetPanelSourceChanged = TargetViewSource != targetPanelSource; - targetItemKeys = targetPanelSource?.Select(item => item.ItemKey ?? default).ToList(); + TargetViewSource = targetPanelSource; + targetItemKeys = BuildItemKeyList(targetPanelSource); } if (sourcePanelSourceChanged || targetPanelSourceChanged) @@ -565,7 +572,7 @@ public abstract class AbstractTransfer: TemplatedControl, protected virtual void TransferItems(TransferDirection transferDirection) { - var currentSet = new HashSet(); + var currentSet = new HashSet(TargetKeys?.Count ?? 0); if (TargetKeys != null) { foreach (var targetKey in TargetKeys) @@ -584,7 +591,7 @@ public abstract class AbstractTransfer: TemplatedControl, { currentSet.Add(key); } - SetCurrentValue(TargetKeysProperty, currentSet.ToList()); + SetCurrentValue(TargetKeysProperty, BuildEntityKeyList(currentSet)); } } else @@ -596,7 +603,7 @@ public abstract class AbstractTransfer: TemplatedControl, { currentSet.Remove(key); } - SetCurrentValue(TargetKeysProperty, currentSet.ToList()); + SetCurrentValue(TargetKeysProperty, BuildEntityKeyList(currentSet)); } } _sourceViewDecorator?.NotifyTransferCompleted(transferDirection); @@ -613,6 +620,48 @@ public abstract class AbstractTransfer: TemplatedControl, } } + protected static HashSet? BuildTargetKeySet(ICollection? keys) + { + if (keys == null || keys.Count == 0) + { + return null; + } + + var keySet = new HashSet(keys.Count); + foreach (var key in keys) + { + keySet.Add(key); + } + return keySet; + } + + protected static List BuildEntityKeyList(ICollection keys) + { + var keyList = new List(keys.Count); + foreach (var key in keys) + { + keyList.Add(key); + } + return keyList; + } + + protected static List? BuildItemKeyList(IEnumerable? items) + { + if (items == null) + { + return null; + } + + var keyList = items is ICollection collection + ? new List(collection.Count) + : new List(); + foreach (var item in items) + { + keyList.Add(item.ItemKey ?? default); + } + return keyList; + } + private void HandleTransferFilterChanged(TextChangedEventArgs args) { if (!IsFilterEnabled) @@ -660,4 +709,4 @@ public abstract class AbstractTransfer: TemplatedControl, Target = 0x02, Both = Source | Target } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Transfer/ListTransfer.cs b/src/AtomUI.Desktop.Controls/Transfer/ListTransfer.cs index e422f6178..d9a1f12d9 100644 --- a/src/AtomUI.Desktop.Controls/Transfer/ListTransfer.cs +++ b/src/AtomUI.Desktop.Controls/Transfer/ListTransfer.cs @@ -67,7 +67,7 @@ public class ListTransfer : AbstractTransfer var targetPanelSourceChanged = false; IList? sourceItemKeys = null; IList? targetItemKeys = null; - var targetKeySet = TargetKeys?.Count > 0 ? TargetKeys.ToHashSet() : null; + var targetKeySet = BuildTargetKeySet(TargetKeys); if (changeType.HasFlag(FilterChangeType.Source)) { var sourcePanelSource = ItemsSource? @@ -78,7 +78,7 @@ public class ListTransfer : AbstractTransfer .ToArray(); sourcePanelSourceChanged = SourceViewSource != sourcePanelSource; SourceViewSource = sourcePanelSource; - sourceItemKeys = sourcePanelSource?.Select(item => item.ItemKey ?? default).ToList(); + sourceItemKeys = BuildItemKeyList(sourcePanelSource); } if (changeType.HasFlag(FilterChangeType.Target)) @@ -97,7 +97,7 @@ public class ListTransfer : AbstractTransfer } targetPanelSourceChanged = TargetViewSource != targetPanelSource; TargetViewSource = targetPanelSource; - targetItemKeys = targetPanelSource?.Select(item => item.ItemKey ?? default).ToList(); + targetItemKeys = BuildItemKeyList(targetPanelSource); } if (sourcePanelSourceChanged || targetPanelSourceChanged) diff --git a/src/AtomUI.Desktop.Controls/Transfer/TransferListView.cs b/src/AtomUI.Desktop.Controls/Transfer/TransferListView.cs index 7395f8a77..c33d75224 100644 --- a/src/AtomUI.Desktop.Controls/Transfer/TransferListView.cs +++ b/src/AtomUI.Desktop.Controls/Transfer/TransferListView.cs @@ -89,10 +89,10 @@ public class TransferListView : ListView, ITransferView { if (SelectedKeys != null) { - var newSelectedKeys = new List(); + var newSelectedKeys = new List(SelectedKeys.Count); if (ItemsSource != null) { - var allItems = ItemsSource.Cast().Select(item => item.ItemKey).ToHashSet(); + var allItems = BuildItemKeySet(ItemsSource); foreach (var selectedKey in SelectedKeys) { if (allItems.Contains(selectedKey)) @@ -133,7 +133,7 @@ public class TransferListView : ListView, ITransferView } else { - var selectedKeys = new List(); + var selectedKeys = new List(SelectedItems.Count); foreach (var item in SelectedItems) { if (item is IListItemData listItemData && listItemData.IsEnabled) @@ -154,7 +154,7 @@ public class TransferListView : ListView, ITransferView _ignoreSyncSelection = false; return; } - var selectedItems = ItemsSource?.Cast().Where(item => SelectedKeys?.Contains(item.ItemKey ?? default) ?? false).ToList(); + var selectedItems = BuildSelectedItemsList(ItemsSource, SelectedKeys); SetCurrentValue(SelectedItemsProperty, selectedItems); } @@ -304,7 +304,68 @@ public class TransferListView : ListView, ITransferView } else if (selectAction == TransferSelectAction.RemoveCurrentPage) { - ItemsRemoved?.Invoke(this, new TransferItemsRemovedEventArgs(Items.Cast().ToList())); + ItemsRemoved?.Invoke(this, new TransferItemsRemovedEventArgs(BuildItemsList(Items))); } } + + private static HashSet BuildItemKeySet(IEnumerable source) + { + var keySet = source is ICollection collection + ? new HashSet(collection.Count) + : new HashSet(); + foreach (var item in source) + { + keySet.Add(((IItemKey)item!).ItemKey); + } + return keySet; + } + + private static List? BuildSelectedItemsList(IEnumerable? source, ICollection? selectedKeys) + { + if (source == null) + { + return null; + } + + var selectedKeySet = BuildEntityKeySet(selectedKeys); + var selectedItems = selectedKeys == null + ? new List(0) + : new List(selectedKeys.Count); + foreach (var item in source) + { + var itemKey = (IItemKey)item!; + if (selectedKeySet?.Contains(itemKey.ItemKey ?? default) == true) + { + selectedItems.Add(itemKey); + } + } + return selectedItems; + } + + private static HashSet? BuildEntityKeySet(ICollection? keys) + { + if (keys == null || keys.Count == 0) + { + return null; + } + + var keySet = new HashSet(keys.Count); + foreach (var key in keys) + { + keySet.Add(key); + } + return keySet; + } + + private static List BuildItemsList(IEnumerable source) + { + var items = source is ICollection collection + ? new List(collection.Count) + : new List(); + foreach (var item in source) + { + items.Add((IItemKey)item!); + } + return items; + } } diff --git a/src/AtomUI.Desktop.Controls/Transfer/TransferTreeView.cs b/src/AtomUI.Desktop.Controls/Transfer/TransferTreeView.cs index b09a1c1c0..ec5fd6fc1 100644 --- a/src/AtomUI.Desktop.Controls/Transfer/TransferTreeView.cs +++ b/src/AtomUI.Desktop.Controls/Transfer/TransferTreeView.cs @@ -91,7 +91,7 @@ public class TransferTreeView : TreeView, ITransferTreeView, ITransferDecoratorP private void HandleItemCountChanged() { - var nodes = Items.Cast().ToList(); + var nodes = BuildTreeNodeList(Items); var totalCount = 0; foreach (var node in nodes) { @@ -108,7 +108,7 @@ public class TransferTreeView : TreeView, ITransferTreeView, ITransferDecoratorP _ignoreSyncSelection = false; return; } - var checkedItems = ItemsSource?.Cast().Where(item => SelectedKeys?.Contains(item.ItemKey ?? default) ?? false).ToList(); + var checkedItems = BuildCheckedItemsList(ItemsSource, SelectedKeys); CheckedItems.Clear(); if (checkedItems != null) { @@ -134,7 +134,7 @@ public class TransferTreeView : TreeView, ITransferTreeView, ITransferDecoratorP } else { - var selectedKeys = new List(); + var selectedKeys = new List(CheckedItems.Count); foreach (var item in CheckedItems) { if (item is ITreeItemNode treeItemNode && treeItemNode.IsEnabled) @@ -217,7 +217,7 @@ public class TransferTreeView : TreeView, ITransferTreeView, ITransferDecoratorP private void MaskNodes() { - var nodes = Items.Cast().ToList(); + var nodes = BuildTreeNodeList(Items); foreach (var node in nodes) { MaskNodeRecursively(node); @@ -283,7 +283,7 @@ public class TransferTreeView : TreeView, ITransferTreeView, ITransferDecoratorP void ITransferTreeView.SetMaskedItems(IList? maskedItems) { - SetCurrentValue(MaskKeysProperty, maskedItems?.ToHashSet()); + SetCurrentValue(MaskKeysProperty, BuildEntityKeySet(maskedItems)); } void ITransferDecoratorProvider.ProvideTransferDecorator(TransferItemDecorator decorator) @@ -309,4 +309,55 @@ public class TransferTreeView : TreeView, ITransferTreeView, ITransferDecoratorP } } } -} \ No newline at end of file + + private static List BuildTreeNodeList(IEnumerable source) + { + var nodes = source is ICollection collection + ? new List(collection.Count) + : new List(); + foreach (var item in source) + { + nodes.Add((ITreeItemNode)item!); + } + return nodes; + } + + private static List? BuildCheckedItemsList( + IEnumerable? source, + ICollection? selectedKeys) + { + if (source == null) + { + return null; + } + + var selectedKeySet = BuildEntityKeySet(selectedKeys); + var checkedItems = selectedKeys == null + ? new List(0) + : new List(selectedKeys.Count); + foreach (var item in source) + { + var treeItem = (ITreeItemNode)item!; + if (selectedKeySet?.Contains(treeItem.ItemKey ?? default) == true) + { + checkedItems.Add(treeItem); + } + } + return checkedItems; + } + + private static HashSet? BuildEntityKeySet(ICollection? keys) + { + if (keys == null) + { + return null; + } + + var keySet = new HashSet(keys.Count); + foreach (var key in keys) + { + keySet.Add(key); + } + return keySet; + } +} diff --git a/src/AtomUI.Desktop.Controls/Transfer/TreeTransfer.cs b/src/AtomUI.Desktop.Controls/Transfer/TreeTransfer.cs index 7f9029cbf..1c01e1620 100644 --- a/src/AtomUI.Desktop.Controls/Transfer/TreeTransfer.cs +++ b/src/AtomUI.Desktop.Controls/Transfer/TreeTransfer.cs @@ -33,24 +33,24 @@ public class TreeTransfer : AbstractTransfer var targetPanelSourceChanged = false; IList? sourceItemKeys = null; IList? targetItemKeys = null; - var targetKeySet = TargetKeys?.Count > 0 ? TargetKeys.ToHashSet() : null; + var targetKeySet = BuildTargetKeySet(TargetKeys); if (changeType.HasFlag(FilterChangeType.Source)) { var sourcePanelSource = ItemsSource; sourcePanelSourceChanged = SourceViewSource != sourcePanelSource; SourceViewSource = sourcePanelSource; - sourceItemKeys = sourcePanelSource?.Select(item => item.ItemKey ?? default).ToList(); + sourceItemKeys = BuildItemKeyList(sourcePanelSource); SourceView?.SetMaskedItems(TargetKeys); } if (changeType.HasFlag(FilterChangeType.Target)) { - var targetPanelSource = targetKeySet == null - ? [] + IEnumerable targetPanelSource = targetKeySet == null + ? Array.Empty() : CalculateTargetItemsSource(ItemsSource?.Cast(), targetKeySet); targetPanelSourceChanged = TargetViewSource != targetPanelSource; TargetViewSource = targetPanelSource; - targetItemKeys = targetPanelSource.Select(item => item.ItemKey ?? default).ToList(); + targetItemKeys = BuildItemKeyList(targetPanelSource); } if (sourcePanelSourceChanged || targetPanelSourceChanged) @@ -61,7 +61,7 @@ public class TreeTransfer : AbstractTransfer private List CalculateTargetItemsSource(IEnumerable? itemNodes, ISet targetKeySet) { - var results = new List(); + var results = new List(targetKeySet.Count); CollectTargetItemsSource(itemNodes, targetKeySet, results); return results; } diff --git a/src/AtomUI.Desktop.Controls/TreeView/TreeView.Filter.cs b/src/AtomUI.Desktop.Controls/TreeView/TreeView.Filter.cs index 7c3d1ed87..6ad6229fe 100644 --- a/src/AtomUI.Desktop.Controls/TreeView/TreeView.Filter.cs +++ b/src/AtomUI.Desktop.Controls/TreeView/TreeView.Filter.cs @@ -33,12 +33,11 @@ public partial class TreeView if (IsFilterMode) { SelectedItemsClosure.Clear(); - var startupItems = new List(); if (SelectionMode.HasFlag(SelectionMode.Single)) { if (SelectedItem != null && TreeContainerFromItem(SelectedItem) is TreeViewItem item) { - startupItems.Add(item); + SelectedItemsClosure.UnionWith(CalculateSelectItemClosure(item)); } } else if (SelectionMode.HasFlag(SelectionMode.Multiple)) @@ -47,16 +46,10 @@ public partial class TreeView { if (entry != null && TreeContainerFromItem(entry) is TreeViewItem item) { - startupItems.Add(item); + SelectedItemsClosure.UnionWith(CalculateSelectItemClosure(item)); } } } - - foreach (var item in startupItems) - { - var closure = CalculateSelectItemClosure(item); - SelectedItemsClosure.UnionWith(closure); - } } } @@ -332,4 +325,4 @@ public partial class TreeView _treeView.SetCurrentValue(TreeView.IsMotionEnabledProperty, _originMotionEnabled); } } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/TreeView/TreeView.cs b/src/AtomUI.Desktop.Controls/TreeView/TreeView.cs index 6b022a2f6..9bd16c37b 100644 --- a/src/AtomUI.Desktop.Controls/TreeView/TreeView.cs +++ b/src/AtomUI.Desktop.Controls/TreeView/TreeView.cs @@ -980,7 +980,7 @@ public partial class TreeView : AvaloniaTreeView, if (e.NewItems?.Count > 0) { - added = new List(); + added = new List(CheckedItems.Count); foreach (var item in CheckedItems) { added.Add(item); @@ -1063,7 +1063,7 @@ public partial class TreeView : AvaloniaTreeView, SetCurrentValue(IsMotionEnabledProperty, false); var segments = treeNodePath.Segments; IList items = Items; - var pathNodes = new List(); + var pathNodes = new List(segments.Count); foreach (var segment in segments) { bool childFound = false; @@ -1121,7 +1121,7 @@ public partial class TreeView : AvaloniaTreeView, { var segments = treeNodePath.Segments; IList items = Items; - var pathNodes = new List(); + var pathNodes = new List(segments.Count); foreach (var segment in segments) { bool childFound = false; @@ -1185,8 +1185,8 @@ public partial class TreeView : AvaloniaTreeView, SetCurrentValue(IsMotionEnabledProperty, false); var segments = treeNodePath.Segments; IList items = Items; - var pathNodes = new List(); - var pathNodeExpandStatus = new List(); + var pathNodes = new List(segments.Count); + var pathNodeExpandStatus = new List(segments.Count); try { for (int i = 0; i < segments.Count; i++) diff --git a/src/AtomUI.Desktop.Controls/TreeView/TreeViewItemHeader.cs b/src/AtomUI.Desktop.Controls/TreeView/TreeViewItemHeader.cs index b7499f908..432e27ad4 100644 --- a/src/AtomUI.Desktop.Controls/TreeView/TreeViewItemHeader.cs +++ b/src/AtomUI.Desktop.Controls/TreeView/TreeViewItemHeader.cs @@ -587,6 +587,12 @@ internal class TreeViewItemHeader : ContentControl } else if (FilterHighlightWords != null) { + if (string.IsNullOrEmpty(FilterHighlightWords)) + { + FilterHighlightRuns = null; + return; + } + string? headerText = null; if (Content is ITreeItemNode treeViewItemData) { @@ -601,21 +607,25 @@ internal class TreeViewItemHeader : ContentControl { return; } - var ranges = new List<(int, int)>(); - int currentIndex = 0; - var highlightLength = FilterHighlightWords.Length; - - while (true) + var highlightedMatch = FilterHighlightStrategy.HasFlag(TreeFilterHighlightStrategy.HighlightedMatch); + List<(int, int)>? ranges = null; + if (highlightedMatch) { - int foundIndex = headerText.IndexOf(FilterHighlightWords, currentIndex, StringComparison.Ordinal); - if (foundIndex == -1) // 如果没有找到,退出循环 + var firstIndex = headerText.IndexOf(FilterHighlightWords, StringComparison.Ordinal); + if (firstIndex != -1) { - break; + ranges = new List<(int, int)>(); + var currentIndex = firstIndex; + var highlightLength = FilterHighlightWords.Length; + while (currentIndex != -1) + { + var highlightEnd = currentIndex + highlightLength; + ranges.Add((currentIndex, highlightEnd)); + currentIndex = headerText.IndexOf(FilterHighlightWords, highlightEnd, StringComparison.Ordinal); + } } - - currentIndex = foundIndex + highlightLength; - ranges.Add((foundIndex, currentIndex)); } + Debug.Assert(headerText != null); var runs = new InlineCollection(); for (var i = 0; i < headerText.Length; i++) @@ -623,9 +633,9 @@ internal class TreeViewItemHeader : ContentControl var c = headerText[i]; var run = new Run($"{c}"); - if (FilterHighlightStrategy.HasFlag(TreeFilterHighlightStrategy.HighlightedMatch)) + if (highlightedMatch) { - if (IsNeedHighlight(i, ranges)) + if (ranges is not null && IsNeedHighlight(i, ranges)) { run.Foreground = FilterHighlightForeground; } diff --git a/src/AtomUI.Desktop.Controls/Upload/PictureList/UploadPicturePreviewContent.cs b/src/AtomUI.Desktop.Controls/Upload/PictureList/UploadPicturePreviewContent.cs index 7de413965..fc4894eef 100644 --- a/src/AtomUI.Desktop.Controls/Upload/PictureList/UploadPicturePreviewContent.cs +++ b/src/AtomUI.Desktop.Controls/Upload/PictureList/UploadPicturePreviewContent.cs @@ -41,9 +41,7 @@ internal class UploadPicturePreviewContent : AbstractUploadPictureContent { if (FilePath != null) { - var sources = new List(); - sources.Add(FilePath.ToString()); - SetCurrentValue(SourcesProperty, sources); + SetCurrentValue(SourcesProperty, new[] { FilePath.ToString() }); } else { @@ -57,4 +55,4 @@ internal class UploadPicturePreviewContent : AbstractUploadPictureContent base.OnApplyTemplate(e); _uploadImagePreviewer = e.NameScope.Find("PART_ImagePreviewer"); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Upload/PictureShapeList/UploadPictureShapePreviewContent.cs b/src/AtomUI.Desktop.Controls/Upload/PictureShapeList/UploadPictureShapePreviewContent.cs index f09445005..eacb96c32 100644 --- a/src/AtomUI.Desktop.Controls/Upload/PictureShapeList/UploadPictureShapePreviewContent.cs +++ b/src/AtomUI.Desktop.Controls/Upload/PictureShapeList/UploadPictureShapePreviewContent.cs @@ -43,9 +43,7 @@ internal class UploadPictureShapePreviewContent : AbstractUploadPictureContent { if (FilePath != null) { - var sources = new List(); - sources.Add(FilePath.ToString()); - SetCurrentValue(SourcesProperty, sources); + SetCurrentValue(SourcesProperty, new[] { FilePath.ToString() }); } else { @@ -59,4 +57,4 @@ internal class UploadPictureShapePreviewContent : AbstractUploadPictureContent base.OnApplyTemplate(e); _uploadImagePreviewer = e.NameScope.Find("PART_ImagePreviewer"); } -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Upload/Upload.cs b/src/AtomUI.Desktop.Controls/Upload/Upload.cs index 438d751e0..0343f48d6 100644 --- a/src/AtomUI.Desktop.Controls/Upload/Upload.cs +++ b/src/AtomUI.Desktop.Controls/Upload/Upload.cs @@ -273,8 +273,9 @@ public class Upload : ContentControl, var files = new List(); foreach (var directory in directories) { - foreach (var fileInfo in Directory.EnumerateFiles(directory.Path.LocalPath, "*", SearchOption.TopDirectoryOnly).Select(x => new FileInfo(x))) + foreach (var filePath in Directory.EnumerateFiles(directory.Path.LocalPath, "*", SearchOption.TopDirectoryOnly)) { + var fileInfo = new FileInfo(filePath); files.Add(new UploadFileInfo(fileInfo.Name, new Uri(fileInfo.FullName), fileInfo.Length, fileInfo.CreationTime, fileInfo.LastWriteTime)); } } @@ -669,4 +670,4 @@ public class Upload : ContentControl, { } #endregion -} \ No newline at end of file +} diff --git a/src/AtomUI.Desktop.Controls/Upload/UploadDefaultDropArea.cs b/src/AtomUI.Desktop.Controls/Upload/UploadDefaultDropArea.cs index 44445f6a3..de4dac923 100644 --- a/src/AtomUI.Desktop.Controls/Upload/UploadDefaultDropArea.cs +++ b/src/AtomUI.Desktop.Controls/Upload/UploadDefaultDropArea.cs @@ -98,7 +98,7 @@ public class UploadDefaultDropArea : TemplatedControl, IMotionAwareControl private void HandleDrop(DragEventArgs e) { - var files = new List(); + var files = new List(e.DataTransfer.Items.Count); foreach (var item in e.DataTransfer.Items) { var raw = item.TryGetRaw(DataFormat.File); @@ -134,4 +134,4 @@ public class UploadDefaultDropArea : TemplatedControl, IMotionAwareControl base.OnLoaded(e); Dispatcher.Post(this.EnableTransitions); } -} \ No newline at end of file +} diff --git a/tools/performances/AtomUI.Performance/Suites/AddOnDecoratedBox/AddOnStateVerification.cs b/tools/performances/AtomUI.Performance/Suites/AddOnDecoratedBox/AddOnStateVerification.cs index 2d8ce558a..65d29b21e 100644 --- a/tools/performances/AtomUI.Performance/Suites/AddOnDecoratedBox/AddOnStateVerification.cs +++ b/tools/performances/AtomUI.Performance/Suites/AddOnDecoratedBox/AddOnStateVerification.cs @@ -1,4 +1,5 @@ using AtomUI.Controls; +using AtomUI.Media; using AtomUI.Desktop.Controls; using AtomUI.Desktop.Controls.Primitives.Themes; using Avalonia; @@ -10,6 +11,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.VisualTree; +using System.Reflection; using AtomTextBox = AtomUI.Desktop.Controls.TextBox; namespace AtomUI.Performance; @@ -24,6 +26,10 @@ internal static partial class Program VerifyAddOnGeometry(failures); VerifyContentFramePointerLifecycle(failures); VerifyCompactSpaceGeometry(failures); + VerifyCompactSpacePrimitiveStateGuards(failures); + VerifySelectRepeatedCompactSpaceAndFeedbackNotificationsAreStable(failures); + VerifyInputShellRepeatedStateNotificationsAreStable(failures); + VerifyPenUtilsReusesEquivalentPen(failures); VerifyRightAddOnTemplateBindings(failures); if (failures.Count == 0) @@ -256,6 +262,292 @@ internal static partial class Program VerifyCompactSpaceGeometry(Orientation.Vertical, failures); } + private static void VerifyCompactSpacePrimitiveStateGuards(ICollection failures) + { + var item = new CompactSpaceItem + { + Child = new AtomUI.Desktop.Controls.Button { Content = "Item" } + }; + var addon = new CompactSpaceAddOn + { + Content = "Addon" + }; + + VerifyRepeatedCompactSpaceAwareNotifications(item, "CompactSpaceItem", failures); + VerifyRepeatedCompactSpaceAwareNotifications(addon, "CompactSpaceAddOn", failures); + VerifyCompactSpaceItemTransformReuseAndClear(failures); + } + + private static void VerifyRepeatedCompactSpaceAwareNotifications(Control control, + string label, + ICollection failures) + { + var compactSpaceAware = (ICompactSpaceAware)control; + var changedProperties = new List(); + control.PropertyChanged += (_, e) => + { + if (e.Property.Name is "IsUsedInCompactSpace" or + "CompactSpaceItemPosition" or + "CompactSpaceOrientation") + { + changedProperties.Add(e.Property.Name); + } + }; + + compactSpaceAware.NotifyPositionChange(SpaceItemPosition.First | SpaceItemPosition.Last); + compactSpaceAware.NotifyOrientationChange(Orientation.Vertical); + changedProperties.Clear(); + compactSpaceAware.NotifyPositionChange(SpaceItemPosition.First | SpaceItemPosition.Last); + compactSpaceAware.NotifyOrientationChange(Orientation.Vertical); + + Expect(changedProperties.Count == 0, + $"{label} repeated compact-space notifications should not rewrite unchanged state. Actual: {string.Join(", ", changedProperties)}.", + failures); + } + + private static void VerifyCompactSpaceItemTransformReuseAndClear(ICollection failures) + { + var first = new AtomUI.Desktop.Controls.Button { Content = "First" }; + var middle = new AtomUI.Desktop.Controls.Button { Content = "Middle" }; + var last = new AtomUI.Desktop.Controls.Button { Content = "Last" }; + var compactSpace = new CompactSpace + { + Orientation = Orientation.Horizontal + }; + compactSpace.Children.Add(first); + compactSpace.Children.Add(middle); + compactSpace.Children.Add(last); + + using var realized = RealizeControl(compactSpace); + var items = compactSpace.GetSelfAndVisualDescendants() + .OfType() + .Where(control => control.GetType().FullName == "AtomUI.Desktop.Controls.CompactSpaceItem") + .ToList(); + Expect(items.Count == 3, + $"CompactSpace transform verification should create three wrappers, actual {items.Count}.", + failures); + if (items.Count != 3) + { + return; + } + + RefreshLayout(realized.Window); + var middleTransform = items[1].RenderTransform; + Expect(middleTransform is TranslateTransform, + "Middle CompactSpaceItem should use a translate offset transform.", + failures); + + RefreshLayout(realized.Window); + Expect(ReferenceEquals(middleTransform, items[1].RenderTransform), + "Repeated CompactSpaceItem measure should reuse the existing offset transform.", + failures); + + ((ICompactSpaceAware)items[1]).NotifyPositionChange(SpaceItemPosition.First | SpaceItemPosition.Last); + RefreshLayout(realized.Window); + Expect(items[1].RenderTransform == null, + "CompactSpaceItem should clear stale offset transform when it becomes a single item.", + failures); + } + + private static void VerifySelectRepeatedCompactSpaceAndFeedbackNotificationsAreStable(ICollection failures) + { + var select = new Select + { + OptionsSource = CreateSelectOptions() + }; + using var realized = RealizeControl(select); + + var changedProperties = new List(); + select.PropertyChanged += (_, e) => + { + if (e.Property.Name is "IsUsedInCompactSpace" or + "CompactSpaceItemPosition" or + "CompactSpaceOrientation" or + "Status" or + "FormFeedback") + { + changedProperties.Add(e.Property.Name); + } + }; + + var compactSpaceAware = (ICompactSpaceAware)select; + compactSpaceAware.NotifyPositionChange(SpaceItemPosition.First | SpaceItemPosition.Last); + compactSpaceAware.NotifyOrientationChange(Orientation.Vertical); + var feedback = new FormValidateFeedback(); + ((IFormItemAware)select).NotifyValidateStatus(FormValidateStatus.Error); + ((IFormItemFeedbackAware)select).SetFeedbackControl(feedback); + + changedProperties.Clear(); + compactSpaceAware.NotifyPositionChange(SpaceItemPosition.First | SpaceItemPosition.Last); + compactSpaceAware.NotifyOrientationChange(Orientation.Vertical); + ((IFormItemAware)select).NotifyValidateStatus(FormValidateStatus.Error); + ((IFormItemFeedbackAware)select).SetFeedbackControl(feedback); + + Expect(changedProperties.Count == 0, + $"Select repeated compact-space/form-feedback notifications should not rewrite unchanged state. Actual: {string.Join(", ", changedProperties)}.", + failures); + } + + private static void VerifyInputShellRepeatedStateNotificationsAreStable(ICollection failures) + { + VerifyTextBoxRepeatedStateNotificationsAreStable(failures); + VerifyTextAreaRepeatedStateNotificationsAreStable(failures); + VerifyButtonSpinnerRepeatedCompactSpaceNotificationsAreStable(failures); + VerifyNumericUpDownRepeatedStateNotificationsAreStable(failures); + } + + private static void VerifyTextBoxRepeatedStateNotificationsAreStable(ICollection failures) + { + var textBox = new AtomTextBox(); + var feedback = new FormValidateFeedback(); + VerifyRepeatedCompactSpaceAwareNotifications(textBox, "TextBox", failures); + VerifyRepeatedFeedbackNotification(textBox, "TextBox", feedback, failures); + } + + private static void VerifyTextAreaRepeatedStateNotificationsAreStable(ICollection failures) + { + var textArea = new TextArea(); + var feedback = new FormValidateFeedback(); + VerifyRepeatedStatusNotification(textArea, "TextArea", failures); + VerifyRepeatedFeedbackNotification(textArea, "TextArea", feedback, failures); + } + + private static void VerifyButtonSpinnerRepeatedCompactSpaceNotificationsAreStable(ICollection failures) + { + var buttonSpinner = new AtomUI.Desktop.Controls.ButtonSpinner(); + VerifyRepeatedCompactSpaceAwareNotifications(buttonSpinner, "ButtonSpinner", failures); + } + + private static void VerifyNumericUpDownRepeatedStateNotificationsAreStable(ICollection failures) + { + var numericUpDown = new AtomUI.Desktop.Controls.NumericUpDown(); + VerifyRepeatedCompactSpaceAwareNotifications(numericUpDown, "NumericUpDown", failures); + VerifyRepeatedStatusNotification(numericUpDown, "NumericUpDown", failures); + } + + private static void VerifyRepeatedStatusNotification(Control control, + string label, + ICollection failures) + { + var changedProperties = new List(); + control.PropertyChanged += (_, e) => + { + if (e.Property.Name == "Status") + { + changedProperties.Add(e.Property.Name); + } + }; + + ((IFormItemAware)control).NotifyValidateStatus(FormValidateStatus.Error); + changedProperties.Clear(); + ((IFormItemAware)control).NotifyValidateStatus(FormValidateStatus.Error); + + Expect(changedProperties.Count == 0, + $"{label} repeated validation status notification should not rewrite unchanged state. Actual: {string.Join(", ", changedProperties)}.", + failures); + } + + private static void VerifyRepeatedFeedbackNotification(Control control, + string label, + FormValidateFeedback feedback, + ICollection failures) + { + var changedProperties = new List(); + control.PropertyChanged += (_, e) => + { + if (e.Property.Name == "FormFeedback") + { + changedProperties.Add(e.Property.Name); + } + }; + + ((IFormItemFeedbackAware)control).SetFeedbackControl(feedback); + changedProperties.Clear(); + ((IFormItemFeedbackAware)control).SetFeedbackControl(feedback); + + Expect(changedProperties.Count == 0, + $"{label} repeated form-feedback notification should not rewrite unchanged state. Actual: {string.Join(", ", changedProperties)}.", + failures); + } + + private static void VerifyPenUtilsReusesEquivalentPen(ICollection failures) + { + var pen = InvokePenUtilsTryModifyOrCreate( + null, + Brushes.Red, + 1.0, + null, + 0.0, + out var firstChanged); + var firstPen = pen; + pen = InvokePenUtilsTryModifyOrCreate( + pen, + Brushes.Red, + 1.0, + null, + 0.0, + out var secondChanged); + Expect(firstChanged, + "PenUtils first immutable brush call should create a pen.", + failures); + Expect(!secondChanged && ReferenceEquals(firstPen, pen), + "PenUtils repeated immutable brush call should reuse the existing equivalent pen.", + failures); + + var dashes = new[] { 4.0, 2.0 }; + pen = InvokePenUtilsTryModifyOrCreate( + pen, + Brushes.Red, + 1.0, + dashes, + 0.0, + out var dashedChanged); + var dashedPen = pen; + pen = InvokePenUtilsTryModifyOrCreate( + pen, + Brushes.Red, + 1.0, + dashes, + 0.0, + out var repeatedDashedChanged); + Expect(dashedChanged, + "PenUtils first dashed call should update the pen.", + failures); + Expect(!repeatedDashedChanged && ReferenceEquals(dashedPen, pen), + "PenUtils repeated dashed call should reuse the existing equivalent pen.", + failures); + } + + private static IPen? InvokePenUtilsTryModifyOrCreate(IPen? pen, + IBrush? brush, + double thickness, + IReadOnlyList? strokeDashArray, + double strokeDashOffset, + out bool changed) + { + var method = typeof(PenUtils).GetMethod( + "TryModifyOrCreate", + BindingFlags.Static | BindingFlags.NonPublic); + if (method == null) + { + throw new InvalidOperationException("PenUtils.TryModifyOrCreate should exist."); + } + + object?[] args = + [ + pen, + brush, + thickness, + strokeDashArray, + strokeDashOffset, + PenLineCap.Flat, + PenLineJoin.Miter, + 10.0 + ]; + changed = method.Invoke(null, args) is true; + return args[0] as IPen; + } + private static void VerifyCompactSpaceGeometry(Orientation orientation, ICollection failures) { var compactSpace = CreateCompactSpace(orientation); diff --git a/tools/performances/AtomUI.Performance/Suites/DatePicker/DatePickerStateVerification.cs b/tools/performances/AtomUI.Performance/Suites/DatePicker/DatePickerStateVerification.cs index 08edd3410..726724fcb 100644 --- a/tools/performances/AtomUI.Performance/Suites/DatePicker/DatePickerStateVerification.cs +++ b/tools/performances/AtomUI.Performance/Suites/DatePicker/DatePickerStateVerification.cs @@ -1,7 +1,9 @@ +using AtomUI.Controls; using AtomUI.Desktop.Controls; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.VisualTree; namespace AtomUI.Performance; @@ -19,6 +21,8 @@ internal static partial class Program VerifyDatePickerAccessoryContentRefresh(failures); VerifyDatePickerPopupLifecycle(failures); VerifyRangeDatePickerPopupLifecycle(failures); + VerifyRangeDatePickerIndicatorOffsetStaysStable(failures); + VerifyInfoPickerRepeatedStateNotificationsAreStable(failures); VerifyDatePickerWindowSubscriptionLifecycle(failures); if (failures.Count == 0) @@ -271,6 +275,69 @@ internal static partial class Program failures); } + private static void VerifyRangeDatePickerIndicatorOffsetStaysStable(ICollection failures) + { + var rangeDatePicker = CreateRangeDatePicker(); + using var realized = RealizeControl(rangeDatePicker); + + var indicator = FindVisualByName(rangeDatePicker, "PART_RangePickerIndicator"); + Expect(indicator != null, + "RangeDatePicker should realize PART_RangePickerIndicator.", + failures); + if (indicator == null) + { + return; + } + + RefreshLayout(realized.Window); + var firstLeft = Canvas.GetLeft(indicator); + var firstTop = Canvas.GetTop(indicator); + RefreshLayout(realized.Window); + + Expect(Canvas.GetLeft(indicator).Equals(firstLeft), + "RangeDatePicker repeated layout should keep indicator Canvas.Left stable.", + failures); + Expect(Canvas.GetTop(indicator).Equals(firstTop), + "RangeDatePicker repeated layout should keep indicator Canvas.Top stable.", + failures); + + SetRangeActivatedPartForTest(rangeDatePicker, "Start"); + InvokePrivateMethod( + rangeDatePicker, + "AtomUI.Desktop.Controls.Primitives.RangeInfoPickerInput", + "SetupPickerIndicatorPosition"); + var startWidth = indicator.Width; + var startLeft = Canvas.GetLeft(indicator); + InvokePrivateMethod( + rangeDatePicker, + "AtomUI.Desktop.Controls.Primitives.RangeInfoPickerInput", + "SetupPickerIndicatorPosition"); + Expect(indicator.Width.Equals(startWidth), + "RangeDatePicker repeated start indicator setup should keep Width stable.", + failures); + Expect(Canvas.GetLeft(indicator).Equals(startLeft), + "RangeDatePicker repeated start indicator setup should keep Canvas.Left stable.", + failures); + + SetRangeActivatedPartForTest(rangeDatePicker, "End"); + InvokePrivateMethod( + rangeDatePicker, + "AtomUI.Desktop.Controls.Primitives.RangeInfoPickerInput", + "SetupPickerIndicatorPosition"); + var endWidth = indicator.Width; + var endLeft = Canvas.GetLeft(indicator); + InvokePrivateMethod( + rangeDatePicker, + "AtomUI.Desktop.Controls.Primitives.RangeInfoPickerInput", + "SetupPickerIndicatorPosition"); + Expect(indicator.Width.Equals(endWidth), + "RangeDatePicker repeated end indicator setup should keep Width stable.", + failures); + Expect(Canvas.GetLeft(indicator).Equals(endLeft), + "RangeDatePicker repeated end indicator setup should keep Canvas.Left stable.", + failures); + } + private static void VerifyDatePickerWindowSubscriptionLifecycle(ICollection failures) { var datePicker = CreateDatePicker(); @@ -295,6 +362,47 @@ internal static partial class Program failures); } + private static void VerifyInfoPickerRepeatedStateNotificationsAreStable(ICollection failures) + { + var datePicker = CreateDatePicker(); + using var realized = RealizeControl(datePicker); + + var changedProperties = new List(); + datePicker.PropertyChanged += (_, e) => + { + if (e.Property.Name is "IsUsedInCompactSpace" or + "CompactSpaceItemPosition" or + "CompactSpaceOrientation" or + "FormFeedback") + { + changedProperties.Add(e.Property.Name); + } + }; + + var firstLastPosition = CreateSpaceItemPosition("First", "Last"); + NotifyCompactSpacePosition(datePicker, firstLastPosition); + NotifyCompactSpaceOrientation(datePicker, Orientation.Vertical); + var feedback = new FormValidateFeedback(); + InvokePrivateMethod( + datePicker, + "AtomUI.Desktop.Controls.Primitives.InfoPickerInput", + "NotifySetFeedBackControl", + feedback); + + changedProperties.Clear(); + NotifyCompactSpacePosition(datePicker, firstLastPosition); + NotifyCompactSpaceOrientation(datePicker, Orientation.Vertical); + InvokePrivateMethod( + datePicker, + "AtomUI.Desktop.Controls.Primitives.InfoPickerInput", + "NotifySetFeedBackControl", + feedback); + + Expect(changedProperties.Count == 0, + $"DatePicker repeated compact-space/form-feedback notifications should not rewrite state. Actual: {string.Join(", ", changedProperties)}.", + failures); + } + private static void MaterializeInfoPickerPresenterForTest(Control picker) { InvokePrivateMethod(picker, @@ -302,6 +410,60 @@ internal static partial class Program "EnsurePickerPresenter"); } + private static void SetRangeActivatedPartForTest(Control picker, string value) + { + var enumType = typeof(AtomUI.Desktop.Controls.DatePicker) + .Assembly + .GetType("AtomUI.Desktop.Controls.Primitives.RangeActivatedPart"); + if (enumType == null) + { + return; + } + + SetPrivateField( + picker, + "AtomUI.Desktop.Controls.Primitives.RangeInfoPickerInput", + "_rangeActivatedPart", + Enum.Parse(enumType, value)); + } + + private static object CreateSpaceItemPosition(params string[] names) + { + var enumType = typeof(AtomUI.Desktop.Controls.DatePicker) + .Assembly + .GetType("AtomUI.Desktop.Controls.SpaceItemPosition"); + if (enumType == null) + { + throw new InvalidOperationException("SpaceItemPosition type should exist."); + } + + var value = 0; + foreach (var name in names) + { + value |= (int)Enum.Parse(enumType, name); + } + return Enum.ToObject(enumType, value); + } + + private static void NotifyCompactSpacePosition(Control target, object? position) + { + InvokeCompactSpaceAwareMethod(target, "NotifyPositionChange", position); + } + + private static void NotifyCompactSpaceOrientation(Control target, Orientation orientation) + { + InvokeCompactSpaceAwareMethod(target, "NotifyOrientationChange", orientation); + } + + private static void InvokeCompactSpaceAwareMethod(Control target, string methodName, params object?[] args) + { + var interfaceType = typeof(AtomUI.Desktop.Controls.DatePicker) + .Assembly + .GetType("AtomUI.Desktop.Controls.ICompactSpaceAware"); + var method = interfaceType?.GetMethod(methodName); + method?.Invoke(target, args); + } + private static Control? GetInfoPickerPopupChild(Control picker) { return FindVisualByName(picker, "PART_Popup")?.Child as Control; diff --git a/tools/performances/AtomUI.Performance/Suites/ListBox/ListBoxStateVerification.cs b/tools/performances/AtomUI.Performance/Suites/ListBox/ListBoxStateVerification.cs index 3bb9b6f42..0c14cb407 100644 --- a/tools/performances/AtomUI.Performance/Suites/ListBox/ListBoxStateVerification.cs +++ b/tools/performances/AtomUI.Performance/Suites/ListBox/ListBoxStateVerification.cs @@ -1,8 +1,11 @@ using AtomUI.Controls; using AtomUI.Controls.Data; +using AtomUI.Controls.Primitives; using AtomUI.Desktop.Controls; using AtomUI.Desktop.Controls.Primitives; +using Avalonia; using Avalonia.Controls; +using Avalonia.Media; using Avalonia.VisualTree; namespace AtomUI.Performance; @@ -17,6 +20,9 @@ internal static partial class Program VerifyListBoxSelectedIndicatorLifecycle(failures); VerifyListBoxFilteringLifecycle(failures); VerifyCandidateListSelectionMovement(failures); + VerifyCandidateListEmptyNavigation(failures); + VerifyTreeNodePathDerivedOperations(failures); + VerifyMotionGhostGeometryUpdates(failures); if (failures.Count == 0) { @@ -32,6 +38,134 @@ internal static partial class Program return false; } + private static void VerifyTreeNodePathDerivedOperations(ICollection failures) + { + var path = new TreeNodePath(["zhejiang", "hangzhou"]); + Expect(path.StartsWith(path), + "TreeNodePath.StartsWith() should return true for the same immutable instance.", + failures); + Expect(path.StartsWith(TreeNodePath.Empty), + "TreeNodePath.StartsWith() should return true for an empty prefix.", + failures); + + var childPath = path.Append("xihu"); + Expect(childPath.ToString() == "zhejiang/hangzhou/xihu", + $"TreeNodePath.Append(string) should preserve path segments, actual {childPath}.", + failures); + + var appendedPath = path.Append(new TreeNodePath(["xihu", "longjing"])); + Expect(appendedPath.ToString() == "zhejiang/hangzhou/xihu/longjing", + $"TreeNodePath.Append(TreeNodePath) should append all segments, actual {appendedPath}.", + failures); + Expect(ReferenceEquals(TreeNodePath.Empty.Append(path), path), + "TreeNodePath.Empty.Append(TreeNodePath) should reuse the appended immutable path.", + failures); + + var parentPath = childPath.GetParent(); + Expect(parentPath?.ToString() == "zhejiang/hangzhou", + $"TreeNodePath.GetParent() should remove one segment, actual {parentPath}.", + failures); + Expect(ReferenceEquals(new TreeNodePath(["zhejiang"]).GetParent(), TreeNodePath.Empty), + "TreeNodePath.GetParent() for a single segment should reuse TreeNodePath.Empty.", + failures); + + var replacedPath = childPath.WithSegment(2, "yuhang"); + Expect(replacedPath.ToString() == "zhejiang/hangzhou/yuhang", + $"TreeNodePath.WithSegment() should replace the requested segment, actual {replacedPath}.", + failures); + Expect(ReferenceEquals(childPath.WithSegment(2, "xihu"), childPath), + "TreeNodePath.WithSegment() should return the same immutable path when the segment is unchanged.", + failures); + + Expect(TreeNodePath.Empty.GetParent() == null, + "TreeNodePath.Empty.GetParent() should stay null.", + failures); + } + + private static void VerifyMotionGhostGeometryUpdates(ICollection failures) + { + var ghostType = typeof(TreeNodePath).Assembly.GetType("AtomUI.Controls.Primitives.MotionGhostControl"); + Expect(ghostType != null, + "MotionGhostControl type should be available for geometry update verification.", + failures); + if (ghostType == null) + { + return; + } + + var ghost = Activator.CreateInstance(ghostType, nonPublic: true) as Control; + Expect(ghost != null, + "MotionGhostControl should be constructible for geometry update verification.", + failures); + if (ghost == null) + { + return; + } + + SetPropertyValue(ghostType, ghost, "Content", new Border + { + Width = 24, + Height = 18 + }); + SetPropertyValue(ghostType, ghost, "MaskShadows", new BoxShadows(new BoxShadow + { + Blur = 4, + OffsetX = 1, + OffsetY = 2 + })); + SetPropertyValue(ghostType, ghost, "MaskCornerRadius", new CornerRadius(3)); + SetPropertyValue(ghostType, ghost, "MaskSize", new Size(24, 18)); + + using var realized = RealizeControl(ghost); + var shadowRenderer = ghost.GetVisualDescendants().OfType().FirstOrDefault(border => border.BoxShadow.Count > 0); + Expect(shadowRenderer != null, + "MotionGhostControl should create a shadow renderer for geometry update verification.", + failures); + if (shadowRenderer == null) + { + return; + } + + SetPropertyValue(ghostType, ghost, "MaskCornerRadius", new CornerRadius(6)); + RefreshLayout(realized.Window); + Expect(shadowRenderer.CornerRadius == new CornerRadius(6) && + Math.Abs(shadowRenderer.Width - 24) < 0.0001 && + Math.Abs(shadowRenderer.Height - 18) < 0.0001, + $"MotionGhostControl should update radius without changing size, actual radius={shadowRenderer.CornerRadius}, size={shadowRenderer.Width}x{shadowRenderer.Height}.", + failures); + + SetPropertyValue(ghostType, ghost, "MaskSize", new Size(32, 20)); + RefreshLayout(realized.Window); + Expect(shadowRenderer.CornerRadius == new CornerRadius(6) && + Math.Abs(shadowRenderer.Width - 32) < 0.0001 && + Math.Abs(shadowRenderer.Height - 20) < 0.0001, + $"MotionGhostControl should update size without changing radius, actual radius={shadowRenderer.CornerRadius}, size={shadowRenderer.Width}x{shadowRenderer.Height}.", + failures); + } + + private static void VerifyCandidateListEmptyNavigation(ICollection failures) + { + var candidateList = CreateCandidateList([]); + using var realized = RealizeControl(candidateList); + var keyDown = new Avalonia.Input.KeyEventArgs + { + RoutedEvent = Avalonia.Input.InputElement.KeyDownEvent, + Key = Avalonia.Input.Key.Down, + KeyModifiers = Avalonia.Input.KeyModifiers.None + }; + + candidateList.HandleKeyDown(keyDown); + RefreshLayout(realized.Window); + Expect(keyDown.Handled && candidateList.CandidateSelectedIndex == -1 && candidateList.CandidateSelectedItem == null, + "Empty CandidateList Down navigation should be handled without selecting an invalid candidate.", + failures); + } + + private static void SetPropertyValue(Type type, object target, string propertyName, object? value) + { + type.GetProperty(propertyName)?.SetValue(target, value); + } + private static void VerifyListBoxDefaultShape(ICollection failures) { var listBox = CreateListBox(CreateListItems(5));