mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-28 14:20:47 +08:00
perf(Splitter): reduce collapse icon and drag preview overhead
This commit is contained in:
@@ -90,6 +90,7 @@ Avalonia 12 控件库研发的成本模型与高频踩雷点(含 `path:line`
|
||||
| Result | 本轮已完成 | [Result](Result/README.md) | 普通状态/错误码视觉按需创建;`ResultShowCase` repeated mean `74.01ms -> 67.19ms`,Svg `8 -> 3`,visuals `265 -> 257` |
|
||||
| ScrollViewer | 本轮已完成基线与低风险正确性修复 | [ScrollViewer](ScrollViewer/README.md) | 修复 overlay host selector 与 motion owner;`FloatButtonShowCase` 页面级 timing 基本中性,后续优化需更强收益证明 |
|
||||
| Slider | 本轮已完成 | [Slider](Slider/README.md) | 普通 Slider 不再预创建 `PART_EndThumb`;`SliderShowCase` repeated mean `46.31ms -> 33.78ms`,visuals `111 -> 105` |
|
||||
| Splitter | 本轮已完成 Gallery 实测 | [Splitter](Splitter/README.md) | hidden collapse icon 不再构造/保留 `PathIcon`,lazy preview transform 复用;`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` |
|
||||
| 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 |
|
||||
@@ -121,7 +122,7 @@ Avalonia 12 控件库研发的成本模型与高频踩雷点(含 `path:line`
|
||||
| Layout | FlexPanel | Pending | 待建立基线 |
|
||||
| Layout | Grid | Pending | 待建立基线 |
|
||||
| Layout | Space | Done | 已完成 Phase 0-7;`CompactSpaceFiller` 不再承担 wrapper,repeated timing 小幅改善,cold 仍需后续针对子控件/Gallery 拆解 |
|
||||
| Layout | Splitter | Pending | 待建立基线 |
|
||||
| Layout | Splitter | Done | [Splitter](Splitter/README.md);hidden collapse icon 不再构造/保留 `PathIcon`,lazy preview transform 复用,`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 | Pending | 已做 Button 联动 smoke;仍待独立基线 |
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# Splitter 性能优化
|
||||
|
||||
> 路线图位置:[`../desktop-controls-optimization-roadmap.md`](../desktop-controls-optimization-roadmap.md) Tier 2 #6
|
||||
> 状态:本轮已完成控件级与 Gallery 长样本实测。
|
||||
|
||||
---
|
||||
|
||||
## 0. 结论
|
||||
|
||||
本轮优化有效,但收益主要来自创建/布局路径的轻量化,不改变 Splitter 运行时视觉结构:
|
||||
|
||||
- collapse `IconButton` 保留在 ControlTheme 中,符合 Theme Static Rule;隐藏或不可折叠时不再构造和保留 `PathIcon`。
|
||||
- lazy 拖拽预览复用已有 `TranslateTransform`,避免每个 drag delta 重新分配 transform。
|
||||
- `SplitterHandle` 转发拖拽事件时复用原始 `VectorEventArgs`,不再为每个 started/delta/completed 事件复制参数对象。
|
||||
|
||||
Gallery 首轮 `warmup=5 / iterations=30` 出现 repeated mean 噪声回退,但同一轮 alloc 已下降。用临时 worktree 对修改前代码补跑 `warmup=15 / iterations=50` 后,当前代码在 cold、repeated、P95、alloc 上均为正向;最终结论采用长样本口径。
|
||||
|
||||
相关 Avalonia 成本依据:
|
||||
|
||||
- `IsVisible=False` 会短路 measure / arrange / render:`.referenceprojects/Avalonia/src/Avalonia.Base/Layout/Layoutable.cs:546`、`:671`;`.referenceprojects/Avalonia/src/Avalonia.Base/Rendering/ImmediateRenderer.cs:34`
|
||||
- `RenderTransform` 属于 compositor 侧变换路径:`.referenceprojects/Avalonia/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs:89-118`
|
||||
|
||||
---
|
||||
|
||||
## 1. 控件级基线
|
||||
|
||||
baseline 命令:
|
||||
|
||||
```bash
|
||||
dotnet run --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj \
|
||||
-c Debug --framework net10.0 --no-build -- \
|
||||
--suite splitter --count 60 \
|
||||
--markdown /tmp/splitter-control-baseline.md
|
||||
```
|
||||
|
||||
current 复测命令:
|
||||
|
||||
```bash
|
||||
dotnet run --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj \
|
||||
-c Debug --framework net10.0 --no-build -- \
|
||||
--suite splitter --count 60 \
|
||||
--markdown /tmp/splitter-control-current.md
|
||||
```
|
||||
|
||||
第二次 current 复测同命令,输出到 `/tmp/splitter-control-current-rerun.md`。控件级 headless timing 单次噪声较明显,所以这里同时列出两次 current 和平均改善;alloc 取两次一致值或均值。
|
||||
|
||||
| Scenario | baseline ms/item | current ms/item run1 | current ms/item run2 | avg timing 改善 | baseline KB/item | current KB/item | alloc 改善 | 判断 |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| `Splitter.Basic.Vertical2` | 1.129 | 1.089 | 1.042 | 5.62% | 200.1 | 193.6 | 3.25% | 有效 |
|
||||
| `Splitter.Basic.Horizontal2` | 1.154 | 1.060 | 0.999 | 10.79% | 201.9 | 195.3 | 3.27% | 有效 |
|
||||
| `Splitter.Multi.Vertical4` | 4.203 | 4.443 | 3.630 | 3.96% | 565.3 | 545.9 | 3.43% | timing 抖动,alloc 有效 |
|
||||
| `Splitter.Collapsible.Always3` | 6.182 | 4.295 | 5.815 | 18.23% | 922.0 | 922.0 | 0.00% | timing 有效,alloc 持平 |
|
||||
| `Splitter.Collapsible.Hidden3` | 2.642 | 1.399 | 2.126 | 33.29% | 371.5 | 358.7 | 3.45% | 有效 |
|
||||
| `Splitter.Lazy.Pair` | 2.733 | 1.412 | 2.412 | 30.04% | 408.1 | 395.0 | 3.21% | 有效 |
|
||||
| `Splitter.GalleryShape` | 13.570 | 9.655 | 14.524 | 10.91% | 3130.7 | 3061.2 | 2.22% | timing 噪声,alloc 有效 |
|
||||
|
||||
控件级结论:分配下降是稳定信号;`Hidden3`、`Lazy.Pair` 和 collapse always 路径的 timing 两次复测均为正向。`Multi.Vertical4`、`GalleryShape` 的控件级 timing 有单次反向,不能单独作为收益证明;最终有效性以 Gallery 长样本为主。
|
||||
|
||||
---
|
||||
|
||||
## 2. Gallery 真实场景
|
||||
|
||||
最终口径命令:
|
||||
|
||||
```bash
|
||||
dotnet run --project tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj \
|
||||
-c Debug --framework net10.0 --no-build -- \
|
||||
--showcase splitter --label splitter-current-long \
|
||||
--cold-iterations 10 --iterations 50 --warmup 15 --timeout-ms 30000 \
|
||||
--markdown /tmp/splitter-gallery-current-long.md
|
||||
```
|
||||
|
||||
baseline 取自临时 worktree `/tmp/AtomUIV6-splitter-baseline-codex`,代码为本轮修改前的 `066d915e9`。
|
||||
|
||||
| 指标 | baseline | current | 改善 | 结论 |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Cold first navigation mean | 177.55 ms | 154.96 ms | 12.72% | 有效 |
|
||||
| Cold first navigation median | 162.21 ms | 152.06 ms | 6.26% | 有效 |
|
||||
| Cold first navigation P95 | 253.34 ms | 175.33 ms | 30.80% | 有效 |
|
||||
| Cold alloc mean | 6380.36 KB | 6256.48 KB | 1.94% | 小幅有效 |
|
||||
| Repeated navigation mean | 36.91 ms | 34.66 ms | 6.10% | 有效 |
|
||||
| Repeated navigation median | 33.12 ms | 32.62 ms | 1.51% | 小幅有效 |
|
||||
| Repeated navigation P95 | 51.79 ms | 40.43 ms | 21.93% | 有效 |
|
||||
| Repeated alloc mean | 5770.31 KB | 5659.40 KB | 1.92% | 小幅有效 |
|
||||
| Runtime visuals | 319 | 319 | 0 | 结构不变 |
|
||||
| Runtime logical | 31 | 31 | 0 | 结构不变 |
|
||||
|
||||
补充:首轮同参数 `warmup=5 / iterations=30` 的 current repeated mean 为 `50.94 ms`,高于 baseline `49.15 ms`,但样本前段存在明显预热坡度;长样本复测未复现该退化。
|
||||
|
||||
---
|
||||
|
||||
## 3. 改动
|
||||
|
||||
| 路径 | 改动 |
|
||||
| --- | --- |
|
||||
| `src/AtomUI.Desktop.Controls/Splitter/SplitterHandle.cs` | 隐藏 collapse button 时清空 `Icon`;仅 visible 时创建 collapse icon;拖拽事件转发复用原始 `VectorEventArgs` |
|
||||
| `src/AtomUI.Desktop.Controls/Splitter/SplitterPanel.cs` | lazy preview 复用同一个 `TranslateTransform` 并只更新 `X/Y` |
|
||||
| `tools/performances/AtomUI.Performance/Suites/Splitter/SplitterStateVerification.cs` | 状态验证按 Theme Static Rule 调整为“模板按钮保留,但隐藏态不持有 icon”;新增 transform reuse 验证 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 验证
|
||||
|
||||
```bash
|
||||
dotnet build tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -c Debug --framework net10.0
|
||||
```
|
||||
|
||||
结果:0 Warning(s), 0 Error(s)。
|
||||
|
||||
```bash
|
||||
dotnet run --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj \
|
||||
-c Debug --framework net10.0 --no-build -- \
|
||||
--verify-splitter-states
|
||||
```
|
||||
|
||||
结果:`Splitter state verification passed.`
|
||||
|
||||
```bash
|
||||
dotnet build tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj -c Debug --framework net10.0
|
||||
```
|
||||
|
||||
结果:构建通过;保留既有 warning `DataGridColumn._clipboardContentBinding is never used`,非本轮新增。
|
||||
|
||||
---
|
||||
|
||||
## 5. 复杂度自评
|
||||
|
||||
| 指标 | 数值 |
|
||||
| --- | --- |
|
||||
| 新增 `Ensure*/Clear*/Sync*` 方法 | 0 |
|
||||
| 新增 `_ignoreXxx` 标志位 | 0 |
|
||||
| 新增 disposable / event / timer | 0 |
|
||||
| axaml 节点搬到 C# 动态创建 | 否 |
|
||||
| 生产文件范围 | 2 个文件 |
|
||||
@@ -16,7 +16,7 @@
|
||||
| 控件清单 | 71 个 Desktop 控件目录 + ColorPicker + DataGrid | `find src/AtomUI.Desktop.Controls -maxdepth 1 -type d` |
|
||||
| 主题依赖 | 137 条目录间边 | 在 `*.axaml` 中 grep `atom:Xxx`,去掉 token / lang / constants / converters 后映射到拥有该类型的目录 |
|
||||
| 代码依赖 | 277 条目录间边(粗筛后) | 在 `*.cs` 中匹配类名 token 与全局类→目录映射的交集;含一定假阳性(常见单词 `Empty` / `Form` / `Message` 等会误判,已通过同时观察主题边校正主要结论) |
|
||||
| 已完成基线 | 51 项 | `docs/performances/README.md` "当前文档" / "总列表" 的 Done 行 |
|
||||
| 已完成基线 | 52 项 | `docs/performances/README.md` "当前文档" / "总列表" 的 Done 行 |
|
||||
|
||||
**结论权重**:主题依赖是优化排序的主依据;代码依赖只用于补充识别"程序化使用"的隐式耦合(例如 ContextMenu、Window、Message 等无主题模板嵌入但代码里直接 new 的依赖)。
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
| Space | Done | 11 | 0 | Done |
|
||||
| Spin | Skipped (sub-noise) | 8 | 0 | `BuildIndicatorAnimation` lazy-build 改动 KB/item 落在变异范围内;Render 路径 (4 × DrawEllipse/frame) 才是真热点,需 Gallery 级实测 |
|
||||
| SplitView | Pending | 0 | 0 | 简单容器 |
|
||||
| Splitter | **Pending** | 0 | 1 | 完整鼠标拖拽链路 |
|
||||
| Splitter | Done | 0 | 1 | hidden collapse icon 不再构造/保留 PathIcon,lazy preview transform 复用;详见 [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) | **Pending** | 0 | 0 | 单一控件,可独立基线 |
|
||||
@@ -217,7 +217,7 @@ DataGrid 是最重的复合控件,独立子表才是合理的优化粒度。
|
||||
| T2.3 | Timeline | 3 | Done (structural + correctness) | `TimelineIndicator.Render` Pen 缓存;pending item 生命周期/default icon 修复,页面级 timing 未证明收益;详见 [Timeline](Timeline/README.md) |
|
||||
| T2.4 | Steps | 2 | Done (structural-only) | 修复 Grid definitions 清理错误;页面级 timing 未证明稳定收益 |
|
||||
| T2.5 | Breadcrumb | 2 | Done (correctness + baseline) | 父级 `Separator` 变化现在同步到 direct/generated inherited items;同参数复测下 cold / P95 / alloc 有收益,repeated median 基本持平 |
|
||||
| T2.6 | Splitter | 3 | Pending | 拖拽全局事件订阅 |
|
||||
| T2.6 | Splitter | 3 | **Done** | hidden collapse icon 不再构造/保留 `PathIcon`,lazy preview transform 复用;`SplitterShowCase` repeated mean `36.91ms -> 34.66ms`,alloc `5770.31KB -> 5659.40KB`;详见 [Splitter](Splitter/README.md) |
|
||||
| T2.7 | Switch (ToggleSwitch) | 0 | Pending | RectTransition + WaveSpiritDecorator |
|
||||
| T2.8 | WindowTitleBar | 3 | Pending | 与 Window (T0.3) 同周期 |
|
||||
| T2.9 | AdornerLayer | 0 | Pending | 与 Drawer/Dialog/Tooltip overlay 协同 |
|
||||
@@ -271,7 +271,7 @@ Phase C (Tier 1 复合) ──────────────────
|
||||
T1.7 TabControl 独立
|
||||
|
||||
Phase D (Tier 2 单功能) ──────────────────────────────────────
|
||||
T2.2 Statistic done → T2.3 Timeline / T2.5 Breadcrumb / T2.6 Splitter
|
||||
T2.2 Statistic done → T2.3 Timeline / T2.5 Breadcrumb / T2.6 Splitter done
|
||||
T2.7 Switch / T2.10 SplitView
|
||||
|
||||
Phase E (Tier 3 ColorPicker) ─────────────────────────────────
|
||||
|
||||
@@ -157,16 +157,20 @@ internal class SplitterHandle : TemplatedControl
|
||||
|
||||
if (_collapsePrevButton != null)
|
||||
{
|
||||
ApplyCustomIconBackground(_collapsePrevButton, PreviousIconTemplate != null);
|
||||
_collapsePrevButton.IsVisible = hasPrevious && ShouldShowIcon(PreviousShowMode, true, onlyPrevious);
|
||||
_collapsePrevButton.Icon = CreatePrevIcon();
|
||||
UpdateCollapseButton(
|
||||
_collapsePrevButton,
|
||||
hasPrevious && ShouldShowIcon(PreviousShowMode, true, onlyPrevious),
|
||||
PreviousIconTemplate != null,
|
||||
isPrevious: true);
|
||||
}
|
||||
|
||||
if (_collapseNextButton != null)
|
||||
{
|
||||
ApplyCustomIconBackground(_collapseNextButton, NextIconTemplate != null);
|
||||
_collapseNextButton.IsVisible = hasNext && ShouldShowIcon(NextShowMode, false, onlyNext);
|
||||
_collapseNextButton.Icon = CreateNextIcon();
|
||||
UpdateCollapseButton(
|
||||
_collapseNextButton,
|
||||
hasNext && ShouldShowIcon(NextShowMode, false, onlyNext),
|
||||
NextIconTemplate != null,
|
||||
isPrevious: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,17 +205,17 @@ internal class SplitterHandle : TemplatedControl
|
||||
|
||||
private void HandleDragBarStarted(object? sender, VectorEventArgs e)
|
||||
{
|
||||
DragStarted?.Invoke(this, new VectorEventArgs { RoutedEvent = e.RoutedEvent, Vector = e.Vector });
|
||||
DragStarted?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void HandleDragBarDelta(object? sender, VectorEventArgs e)
|
||||
{
|
||||
DragDelta?.Invoke(this, new VectorEventArgs { RoutedEvent = e.RoutedEvent, Vector = e.Vector });
|
||||
DragDelta?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void HandleDragBarCompleted(object? sender, VectorEventArgs e)
|
||||
{
|
||||
DragCompleted?.Invoke(this, new VectorEventArgs { RoutedEvent = e.RoutedEvent, Vector = e.Vector });
|
||||
DragCompleted?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private static void AttachCollapseButton(IconButton? button, EventHandler<RoutedEventArgs> handler)
|
||||
@@ -256,6 +260,23 @@ internal class SplitterHandle : TemplatedControl
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCollapseButton(IconButton button,
|
||||
bool isVisible,
|
||||
bool isCustomIcon,
|
||||
bool isPrevious)
|
||||
{
|
||||
ApplyCustomIconBackground(button, isCustomIcon);
|
||||
button.IsVisible = isVisible;
|
||||
if (isVisible)
|
||||
{
|
||||
button.Icon = isPrevious ? CreatePrevIcon() : CreateNextIcon();
|
||||
}
|
||||
else if (button.Icon != null)
|
||||
{
|
||||
button.Icon = null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldShowIcon(SplitterCollapsibleIconDisplayMode mode, bool isPrevious, bool isOnly)
|
||||
{
|
||||
return mode switch
|
||||
|
||||
@@ -1336,9 +1336,22 @@ internal class SplitterPanel : Panel
|
||||
|
||||
private void ApplyHandlePreview(SplitterHandle handle, double delta)
|
||||
{
|
||||
handle.RenderTransform = Orientation == Orientation.Vertical
|
||||
? new TranslateTransform(delta, 0)
|
||||
: new TranslateTransform(0, delta);
|
||||
if (handle.RenderTransform is not TranslateTransform transform)
|
||||
{
|
||||
transform = new TranslateTransform();
|
||||
handle.RenderTransform = transform;
|
||||
}
|
||||
|
||||
if (Orientation == Orientation.Vertical)
|
||||
{
|
||||
transform.X = delta;
|
||||
transform.Y = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.X = 0;
|
||||
transform.Y = delta;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearHandlePreview(SplitterHandle handle)
|
||||
|
||||
+26
-14
@@ -12,7 +12,7 @@ internal static partial class Program
|
||||
private static bool RunSplitterStateVerification()
|
||||
{
|
||||
var failures = new List<string>();
|
||||
VerifySplitterNonCollapsibleHandleDoesNotCreateCollapseButtons(failures);
|
||||
VerifySplitterNonCollapsibleHandleKeepsCollapseButtonsInactive(failures);
|
||||
VerifySplitterCollapsibleButtonsLifecycle(failures);
|
||||
VerifySplitterLazyPreviewReusesTransform(failures);
|
||||
|
||||
@@ -30,7 +30,7 @@ internal static partial class Program
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void VerifySplitterNonCollapsibleHandleDoesNotCreateCollapseButtons(ICollection<string> failures)
|
||||
private static void VerifySplitterNonCollapsibleHandleKeepsCollapseButtonsInactive(ICollection<string> failures)
|
||||
{
|
||||
var splitter = CreateBasicSplitter(Orientation.Vertical);
|
||||
|
||||
@@ -38,8 +38,8 @@ internal static partial class Program
|
||||
Expect(CountSplitterHandles(splitter) == 1,
|
||||
$"Basic Splitter should create one handle, actual {CountSplitterHandles(splitter)}.",
|
||||
failures);
|
||||
Expect(CountIconButtons(splitter) == 0,
|
||||
$"Non-collapsible Splitter should not create collapse IconButtons, actual {CountIconButtons(splitter)}.",
|
||||
ExpectCollapseButtonsInactive(splitter,
|
||||
"Non-collapsible Splitter collapse IconButtons should stay hidden without icons.",
|
||||
failures);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ internal static partial class Program
|
||||
using var realized = RealizeControl(splitter);
|
||||
var initialButtons = splitter.GetSelfAndVisualDescendants().OfType<IconButton>().ToList();
|
||||
Expect(initialButtons.Count > 0,
|
||||
"Collapsible Splitter should create collapse IconButtons.",
|
||||
"Collapsible Splitter should keep template collapse IconButtons.",
|
||||
failures);
|
||||
Expect(initialButtons.All(button => button.IsVisible),
|
||||
"Always-visible collapse IconButtons should be visible.",
|
||||
failures);
|
||||
Expect(initialButtons.All(button => button.Icon != null),
|
||||
"Visible collapse IconButtons should have an icon.",
|
||||
@@ -62,11 +65,8 @@ internal static partial class Program
|
||||
}
|
||||
RefreshLayout(realized.Window);
|
||||
|
||||
Expect(CountIconButtons(splitter) == 0,
|
||||
$"Splitter should remove collapse IconButtons after collapsible is cleared, actual {CountIconButtons(splitter)}.",
|
||||
failures);
|
||||
Expect(initialButtons.All(button => button.GetVisualParent() == null),
|
||||
"Removed collapse IconButtons should not keep a visual parent.",
|
||||
ExpectCollapseButtonsInactive(splitter,
|
||||
"Splitter should hide collapse IconButtons and clear icons after collapsible is cleared.",
|
||||
failures);
|
||||
|
||||
foreach (var child in splitter.Children.OfType<Control>())
|
||||
@@ -79,8 +79,8 @@ internal static partial class Program
|
||||
}
|
||||
RefreshLayout(realized.Window);
|
||||
|
||||
Expect(CountIconButtons(splitter) == 0,
|
||||
$"Hidden collapsible icons should not materialize IconButtons, actual {CountIconButtons(splitter)}.",
|
||||
ExpectCollapseButtonsInactive(splitter,
|
||||
"Hidden collapsible icons should leave template IconButtons hidden without icons.",
|
||||
failures);
|
||||
}
|
||||
|
||||
@@ -124,8 +124,20 @@ internal static partial class Program
|
||||
return root.GetSelfAndVisualDescendants().OfType<SplitterHandle>().Count();
|
||||
}
|
||||
|
||||
private static int CountIconButtons(Control root)
|
||||
private static void ExpectCollapseButtonsInactive(Control root, string message, ICollection<string> failures)
|
||||
{
|
||||
return root.GetSelfAndVisualDescendants().OfType<IconButton>().Count();
|
||||
var buttons = root.GetSelfAndVisualDescendants().OfType<IconButton>().ToList();
|
||||
Expect(buttons.Count > 0,
|
||||
$"{message} Actual template IconButton count: {buttons.Count}.",
|
||||
failures);
|
||||
Expect(buttons.All(button => !button.IsVisible && button.Icon == null),
|
||||
$"{message} Actual visible/icon state: {DescribeCollapseButtons(buttons)}.",
|
||||
failures);
|
||||
}
|
||||
|
||||
private static string DescribeCollapseButtons(IReadOnlyList<IconButton> buttons)
|
||||
{
|
||||
return string.Join(", ",
|
||||
buttons.Select(button => $"{button.Name ?? "<unnamed>"} visible={button.IsVisible} icon={button.Icon != null}"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user