From 6ed12ce7d01efbbc5c19b415fe6fa362d34e8951 Mon Sep 17 00:00:00 2001 From: chinboy Date: Sun, 16 Aug 2026 11:20:46 +0800 Subject: [PATCH] feat(Semantic): add Spin parts and align Ant Design demo --- .../SpinShowCaseLangResourceKind.cs | 12 +- .../Feedback/Spin/Localization/en-US.xlf | 50 +++ .../Feedback/Spin/Localization/pt-BR.xlf | 60 +++ .../Feedback/Spin/Localization/zh-CN.xlf | 60 +++ .../Feedback/Spin/Localization/zh-TW.xlf | 60 +++ .../Feedback/Spin/Views/SpinShowCase.axaml | 331 +++++++++------ .../llms/controls/skeleton/index-cn.md | 20 +- .../generated/llms/controls/spin/index-cn.md | 81 ++-- .../llms/controls/spin/semantic-cn.md | 41 +- docs/AI/generated/llms/llms-full-cn.txt | 101 +++-- docs/AI/generated/llms/llms-semantic-cn.md | 41 +- .../desktop/feedback/spin/changelog.md | 11 + .../desktop/feedback/spin/implementation.md | 19 +- .../desktop/feedback/spin/overview.md | 17 +- .../desktop/feedback/spin/semantic-part.md | 99 +++++ .../Spin/AbstractSpinIndicator.cs | 44 +- .../Spin/Spin.SemanticParts.cs | 34 ++ src/AtomUI.Desktop.Controls/Spin/Spin.cs | 2 +- .../Spin/SpinIndicator.SemanticParts.cs | 14 + .../Spin/SpinIndicator.cs | 2 +- .../Spin/Themes/SpinIndicatorTheme.axaml | 2 + .../Spin/Themes/SpinTheme.axaml | 6 +- .../Spin/SpinSemanticPartTests.cs | 393 ++++++++++++++++++ .../Localization/CatalogMemberOrder.baseline | 2 +- .../GalleryCatalogCoverageTests.cs | 2 +- .../ShowCases/SpinShowCaseExamples.snapshot | 60 +-- .../ShowCases/SpinShowCasePageTests.cs | 299 ++++++++++++- 27 files changed, 1564 insertions(+), 299 deletions(-) create mode 100644 docs/controls/desktop/feedback/spin/semantic-part.md create mode 100644 src/AtomUI.Desktop.Controls/Spin/Spin.SemanticParts.cs create mode 100644 src/AtomUI.Desktop.Controls/Spin/SpinIndicator.SemanticParts.cs create mode 100644 tests/AtomUI.Desktop.Controls.Tests/Spin/SpinSemanticPartTests.cs diff --git a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/SpinShowCaseLangResourceKind.cs b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/SpinShowCaseLangResourceKind.cs index 03f249906..2c13487bc 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/SpinShowCaseLangResourceKind.cs +++ b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/SpinShowCaseLangResourceKind.cs @@ -1,5 +1,3 @@ -using AtomUI.Localization; - namespace AtomUIGallery.Localization; [LanguageCatalog] @@ -15,6 +13,16 @@ public enum SpinShowCaseLangResourceKind CustomizedDescriptionDescription, EmbeddedModeTitle, EmbeddedModeDescription, + SemanticPartStyleTitle, + SemanticPartStyleDescription, + SemanticRootDescription, + SemanticContainerDescription, + SemanticMaskDescription, + SemanticSectionDescription, + SemanticIndicatorDescription, + SemanticDescriptionDescription, + SemanticIndicatorRootDescription, + SemanticIndicatorContentDescription, PageSubtitle, PageDescription, ComponentCategory, diff --git a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/en-US.xlf b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/en-US.xlf index d517b4b89..24860070f 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/en-US.xlf +++ b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/en-US.xlf @@ -86,6 +86,56 @@ Examples + + + Container element with opacity and blur transition styles for the nested content. + + + + + Description element with font size, foreground and line height styles for the tip text. + + + + + Content element with the built-in dot indicator or custom indicator presenter styles. + + + + + Indicator element with size, motion duration and other spinning indicator styles. + + + + + Root element of a SpinIndicator component. + + + + + Mask element with background, opacity and other overlay dimming styles. + + + + + Use owner-scoped styles to customize Spin's published Semantic Parts. + + + + + Custom Semantic Part styling + + + + + Root element with the loading region's base layout, alignment and theme entry styles. + + + + + Section element with centered alignment, spacing and other loading area layout styles. + + SpinIndicator supports small, middle, large, and custom size types; the custom type follows the middle metrics by default and can override indicator metrics. diff --git a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/pt-BR.xlf b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/pt-BR.xlf index c2caa6faf..9cf023301 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/pt-BR.xlf +++ b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/pt-BR.xlf @@ -103,6 +103,66 @@ Exemplos + + + Container element with opacity and blur transition styles for the nested content. + Elemento contêiner com estilos de opacidade e transição de desfoque para o conteúdo aninhado. + + + + + Description element with font size, foreground and line height styles for the tip text. + Elemento de descrição com estilos de tamanho de fonte, cor de primeiro plano e altura de linha para o texto de dica. + + + + + Content element with the built-in dot indicator or custom indicator presenter styles. + Elemento de conteúdo com estilos do indicador de pontos interno ou do apresentador de indicador personalizado. + + + + + Indicator element with size, motion duration and other spinning indicator styles. + Elemento indicador com tamanho, duração de movimento e outros estilos do indicador de carregamento. + + + + + Root element of a SpinIndicator component. + Elemento raiz de um componente SpinIndicator. + + + + + Mask element with background, opacity and other overlay dimming styles. + Elemento de máscara com estilos de fundo, opacidade e outro escurecimento de sobreposição. + + + + + Use owner-scoped styles to customize Spin's published Semantic Parts. + Use estilos com escopo de owner para personalizar os Semantic Parts publicados do Spin. + + + + + Custom Semantic Part styling + Estilização personalizada de Semantic Part + + + + + Root element with the loading region's base layout, alignment and theme entry styles. + Elemento raiz com estilos de layout base, alinhamento e entrada de tema da região de carregamento. + + + + + Section element with centered alignment, spacing and other loading area layout styles. + Elemento de seção com alinhamento centralizado, espaçamento e outros estilos de layout da área de carregamento. + + SpinIndicator supports small, middle, large, and custom size types; the custom type follows the middle metrics by default and can override indicator metrics. diff --git a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/zh-CN.xlf b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/zh-CN.xlf index 4d80075ca..0e909e553 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/zh-CN.xlf +++ b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/zh-CN.xlf @@ -103,6 +103,66 @@ 示例 + + + Container element with opacity and blur transition styles for the nested content. + 容器元素,包含嵌套内容的透明度和模糊过渡样式。 + + + + + Description element with font size, foreground and line height styles for the tip text. + 描述元素,包含提示文本的字号、前景色和行高样式。 + + + + + Content element with the built-in dot indicator or custom indicator presenter styles. + 内容元素,包含内置圆点指示器或自定义指示器呈现内容的样式。 + + + + + Indicator element with size, motion duration and other spinning indicator styles. + 指示器元素,包含尺寸、动效时长以及其他加载指示样式。 + + + + + Root element of a SpinIndicator component. + SpinIndicator 组件的根元素。 + + + + + Mask element with background, opacity and other overlay dimming styles. + 遮罩元素,包含背景、透明度以及其他遮罩变暗样式。 + + + + + Use owner-scoped styles to customize Spin's published Semantic Parts. + 使用 owner 作用域样式定制 Spin 已发布的 Semantic Part。 + + + + + Custom Semantic Part styling + 自定义 Semantic Part 样式 + + + + + Root element with the loading region's base layout, alignment and theme entry styles. + 根元素,包含加载区域的基础布局、对齐和主题入口样式。 + + + + + Section element with centered alignment, spacing and other loading area layout styles. + 加载区域元素,包含居中对齐、间距以及其他加载区域布局样式。 + + SpinIndicator supports small, middle, large, and custom size types; the custom type follows the middle metrics by default and can override indicator metrics. diff --git a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/zh-TW.xlf b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/zh-TW.xlf index ab6693190..1a8cd8fc5 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/zh-TW.xlf +++ b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/zh-TW.xlf @@ -103,6 +103,66 @@ 示例 + + + Container element with opacity and blur transition styles for the nested content. + 容器元素,包含嵌套內容的透明度和模糊過渡樣式。 + + + + + Description element with font size, foreground and line height styles for the tip text. + 描述元素,包含提示文字的字號、前景色和行高樣式。 + + + + + Content element with the built-in dot indicator or custom indicator presenter styles. + 內容元素,包含內建圓點指示器或自訂指示器呈現內容的樣式。 + + + + + Indicator element with size, motion duration and other spinning indicator styles. + 指示器元素,包含尺寸、動效時長以及其他載入指示樣式。 + + + + + Root element of a SpinIndicator component. + SpinIndicator 元件的根元素。 + + + + + Mask element with background, opacity and other overlay dimming styles. + 遮罩元素,包含背景、透明度以及其他遮罩變暗樣式。 + + + + + Use owner-scoped styles to customize Spin's published Semantic Parts. + 使用 owner 作用域樣式自訂 Spin 已發佈的 Semantic Part。 + + + + + Custom Semantic Part styling + 自訂 Semantic Part 樣式 + + + + + Root element with the loading region's base layout, alignment and theme entry styles. + 根元素,包含載入區域的基礎佈局、對齊和主題入口樣式。 + + + + + Section element with centered alignment, spacing and other loading area layout styles. + 載入區域元素,包含居中對齊、間距以及其他載入區域佈局樣式。 + + SpinIndicator supports small, middle, large, and custom size types; the custom type follows the middle metrics by default and can override indicator metrics. diff --git a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml index c840e4d36..5f8d76fed 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml +++ b/controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml @@ -7,8 +7,8 @@ xmlns:vm="using:AtomUIGallery.ShowCases.Spin" x:DataType="vm:SpinViewModel"> - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/docs/AI/generated/llms/controls/skeleton/index-cn.md b/docs/AI/generated/llms/controls/skeleton/index-cn.md index 7d1ac68ef..f1c4f9bff 100644 --- a/docs/AI/generated/llms/controls/skeleton/index-cn.md +++ b/docs/AI/generated/llms/controls/skeleton/index-cn.md @@ -87,7 +87,7 @@ Skeleton 的公共契约由 public/protected 类型成员、Avalonia 属性、 Gallery key:`ExamplesContent` / item `0` ```axaml - + ``` ### 复杂组合 @@ -97,7 +97,7 @@ Gallery key:`ExamplesContent` / item `0` Gallery key:`ExamplesContent` / item `1` ```axaml - + ``` ### 动态效果 @@ -107,12 +107,12 @@ Gallery key:`ExamplesContent` / item `1` Gallery key:`ExamplesContent` / item `2` ```axaml - + ``` ### 包含子组件 -来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Skeleton/Views/SkeletonShowCase.axaml:222` +来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Skeleton/Views/SkeletonShowCase.axaml:243` Gallery key:`ExamplesContent` / item `4` @@ -120,12 +120,16 @@ Gallery key:`ExamplesContent` / item `4` - - + + - + ``` diff --git a/docs/AI/generated/llms/controls/spin/index-cn.md b/docs/AI/generated/llms/controls/spin/index-cn.md index be18f5958..1c1eab121 100644 --- a/docs/AI/generated/llms/controls/spin/index-cn.md +++ b/docs/AI/generated/llms/controls/spin/index-cn.md @@ -44,7 +44,7 @@ Spin 的公共契约由 public/protected 类型成员、Avalonia 属性、事件 | --- | --- | --- | | 内容与数据 | `CustomIndicatorTemplate` | 定义控件展示内容、输入数据、模板或业务对象入口。 | | 交互与状态 | `IsMaskBackgroundEnabled`、`IsMaskBlurEnabled`、`IsMotionEnabled`、`IsSpinning`、`IsTipVisible` | 表达用户可观察状态、可用性、清除、加载或反馈语义。 | -| 视觉与布局 | `DotSize`、`IndicatorSize`、`SizeType` | 影响尺寸、位置、颜色、形状、密度和模板视觉变量。 | +| 视觉与布局 | `DotBgBrush`、`DotSize`、`IndicatorSize`、`SizeType` | 影响尺寸、位置、颜色、形状、密度和模板视觉变量。 | | 动效与异步 | `MotionDuration`、`MotionEasingCurve` | 约束动效开关、异步加载、播放速度、超时和任务边界。 | | 其他稳定入口 | `CustomIndicator`、`Tip` | 保留为 public surface,变更前需确认 Gallery 和用户 XAML 依赖。 | @@ -76,7 +76,7 @@ Spin 的公共契约由 public/protected 类型成员、Avalonia 属性、事件 ### 基础用法 -来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:36` +来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:97` Gallery key:`ExamplesContent` / item `0` @@ -86,7 +86,7 @@ Gallery key:`ExamplesContent` / item `0` ### 尺寸 -来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:47` +来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:108` Gallery key:`ExamplesContent` / item `1` @@ -99,12 +99,45 @@ Gallery key:`ExamplesContent` / item `1` ``` -### 自定义描述 +### 嵌入模式 -来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:63` +来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:125` Gallery key:`ExamplesContent` / item `2` +```axaml + + + + + + + + + + + + + +``` + +### 自定义描述 + +来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:159` + +Gallery key:`ExamplesContent` / item `3` + ```axaml @@ -134,39 +167,6 @@ Gallery key:`ExamplesContent` / item `2` ``` -### 嵌入模式 - -来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:121` - -Gallery key:`ExamplesContent` / item `4` - -```axaml - - - - - - - - - - - - - -``` - ## 状态模型 Spin 的状态流按以下路径收敛: @@ -217,6 +217,7 @@ Spin Token 只表达组件级视觉变量,例如尺寸、间距、颜色、圆 资源和 AOT 约束: - 不通过运行时反射扫描 public API、Token 或 Gallery 示例数据。 +- 不通过 VisualTree 扫描维护 Semantic Part;Gallery Preview 使用生成 descriptor 和 owner-scoped marker 解析。 - 不把可静态声明的模板结构迁移到 C# 动态创建。 - 异步加载、上传、弹层和窗口生命周期必须能取消或释放。 - 缓存对象必须与控件、窗口、弹层或数据 owner 生命周期一致。 @@ -239,6 +240,8 @@ Spin Token 只表达组件级视觉变量,例如尺寸、间距、颜色、圆 - `src/AtomUI.Desktop.Controls/Spin/Spin.cs` - `src/AtomUI.Desktop.Controls/Spin/SpinIndicator.cs` - `src/AtomUI.Desktop.Controls/Spin/SpinToken.cs` +- `src/AtomUI.Desktop.Controls/Spin/Spin.SemanticParts.cs` +- `src/AtomUI.Desktop.Controls/Spin/SpinIndicator.SemanticParts.cs` - `src/AtomUI.Desktop.Controls/Spin/Themes/SpinIndicatorTheme.axaml` - `src/AtomUI.Desktop.Controls/Spin/Themes/SpinTheme.axaml` @@ -248,11 +251,13 @@ Spin Token 只表达组件级视觉变量,例如尺寸、间距、颜色、圆 - Theme 文件负责静态视觉结构、template part、selector 和资源绑定。 - Token 文件只提供组件视觉变量,不保存实例状态。 - Gallery 文件只展示用法和示例,不作为运行时逻辑 owner。 +- Semantic descriptor 由各 public owner 的 `[SemanticPart]` 声明生成;模板只使用静态 `Classes.semantic-*="True"` marker。 ## 相关文档 - 源设计文档:`docs/controls/desktop/feedback/spin/overview.md` - 实现文档:`docs/controls/desktop/feedback/spin/implementation.md` +- Semantic Part 文档:`docs/controls/desktop/feedback/spin/semantic-part.md` - Token 文档:`docs/controls/desktop/feedback/spin/token.md` - 变更记录:`docs/controls/desktop/feedback/spin/changelog.md` - 语义结构:`./semantic-cn.md` diff --git a/docs/AI/generated/llms/controls/spin/semantic-cn.md b/docs/AI/generated/llms/controls/spin/semantic-cn.md index d0f1e1567..42be03080 100644 --- a/docs/AI/generated/llms/controls/spin/semantic-cn.md +++ b/docs/AI/generated/llms/controls/spin/semantic-cn.md @@ -4,13 +4,38 @@ ## Semantic Parts -| Part | AtomUI 节点 | 职责 | 相关 API | 相关 Token | 稳定性 | -| --- | --- | --- | --- | --- | --- | -| `root` | `Spin` | 反馈控件根语义区域,承载 public API、反馈状态和主题入口。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `host` | `宿主或弹层区域` | 承载 overlay、popup、portal、message host、drawer 或 modal 容器。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `surface` | `反馈表面` | 承载背景、边框、阴影、尺寸、placement 和视觉状态。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `content` | `内容区域` | 承载标题、正文、图标、进度、结果、操作或关闭入口。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `motion` | `动效区域` | 表达进入退出、loading、progress、skeleton 或水印刷新反馈。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | +Spin 主控件公开 `root`、`container`、`mask`、`section`、`indicator`、`description` 六个职责区域,与上游稳定语义 +保持同名。`mask` 是 AtomUI 扩展区域,承载嵌套模式的遮罩背景层职责:上游体系的遮罩语义已折叠进全屏模式的根区域, +AtomUI 没有全屏模式,遮罩层是嵌套模式下独立的真实节点。`SpinIndicator` 是独立的公开子控件, +公开 `root` 与 `content`。 + +### 1.1 `Spin` + +| Part | Selector | ContractType | Cardinality | Customization | CrossVisualRoot | RuntimeCreated | +| --- | --- | --- | --- | --- | --- | --- | +| `root` | owner | `Spin` | `Single` | `Root` | `false` | `false` | +| `container` | `.semantic-container` | `ContentPresenter` | `Single` | `Selector` | `false` | `false` | +| `mask` | `.semantic-mask` | `Border` | `Single` | `Selector` | `false` | `false` | +| `section` | `.semantic-section` | `StackPanel` | `Single` | `Selector` | `false` | `false` | +| `indicator` | `.semantic-indicator` | `SpinIndicator` | `Single` | `Selector` | `false` | `false` | +| `description` | `.semantic-description` | `TextBlock` | `Single` | `Selector` | `false` | `false` | + +`container` 承载用户内容的 `ContentPresenter`,spinning 时承担透明度或高斯模糊反馈;`mask` 是遮罩背景层,只在 +`IsMaskBackgroundEnabled` 时呈现 `ColorBgMask`;`section` 是加载区域,承载 `indicator` 与 `description` 并居中; +`indicator` 是主控件模板直接拥有的公开 `SpinIndicator` 子控件,其尺寸分支、动效时长和圆点填充 +(`DotBgBrush`)均可经 Semantic Style 定制;`description` 是提示文本节点,对应公共 API +`Tip` / `IsTipVisible`。`root` 不声明 `.semantic-root` marker。 + +### 1.2 `SpinIndicator` + +| Part | Selector | ContractType | Cardinality | Customization | CrossVisualRoot | RuntimeCreated | +| --- | --- | --- | --- | --- | --- | --- | +| `root` | owner | `SpinIndicator` | `Single` | `Root` | `false` | `false` | +| `content` | `.semantic-content` | `Control` | `Multiple` | `Selector` | `false` | `false` | + +`content` 使用 `Multiple` 是 AtomUI 模板实现的状态替代语义:内置四点指示器 `SpinIndicatorDotPanel` 与自定义指示器 +`PART_CustomIndicatorPresenter` 是两个静态 marker target,由 `IsCustomIndicator` 决定同一时刻只有一个可见。 +Preview 只高亮当前可见 target;Semantic Style 同时作用于两个替代实现,保证切换自定义指示器后定制仍然存在。 ## Abstract AXAML Structure @@ -79,7 +104,7 @@ Spin | --- | --- | --- | | 内容与数据 | `CustomIndicatorTemplate` | 定义控件展示内容、输入数据、模板或业务对象入口。 | | 交互与状态 | `IsMaskBackgroundEnabled`、`IsMaskBlurEnabled`、`IsMotionEnabled`、`IsSpinning`、`IsTipVisible` | 表达用户可观察状态、可用性、清除、加载或反馈语义。 | -| 视觉与布局 | `DotSize`、`IndicatorSize`、`SizeType` | 影响尺寸、位置、颜色、形状、密度和模板视觉变量。 | +| 视觉与布局 | `DotBgBrush`、`DotSize`、`IndicatorSize`、`SizeType` | 影响尺寸、位置、颜色、形状、密度和模板视觉变量。 | | 动效与异步 | `MotionDuration`、`MotionEasingCurve` | 约束动效开关、异步加载、播放速度、超时和任务边界。 | | 其他稳定入口 | `CustomIndicator`、`Tip` | 保留为 public surface,变更前需确认 Gallery 和用户 XAML 依赖。 | diff --git a/docs/AI/generated/llms/llms-full-cn.txt b/docs/AI/generated/llms/llms-full-cn.txt index a81e3b2f1..de7b9c918 100644 --- a/docs/AI/generated/llms/llms-full-cn.txt +++ b/docs/AI/generated/llms/llms-full-cn.txt @@ -21238,7 +21238,7 @@ Skeleton 的公共契约由 public/protected 类型成员、Avalonia 属性、 Gallery key:`ExamplesContent` / item `0` ```axaml - + ``` ### 复杂组合 @@ -21248,7 +21248,7 @@ Gallery key:`ExamplesContent` / item `0` Gallery key:`ExamplesContent` / item `1` ```axaml - + ``` ### 动态效果 @@ -21258,12 +21258,12 @@ Gallery key:`ExamplesContent` / item `1` Gallery key:`ExamplesContent` / item `2` ```axaml - + ``` ### 包含子组件 -来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Skeleton/Views/SkeletonShowCase.axaml:222` +来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Skeleton/Views/SkeletonShowCase.axaml:243` Gallery key:`ExamplesContent` / item `4` @@ -21271,12 +21271,16 @@ Gallery key:`ExamplesContent` / item `4` - - + + - + ``` @@ -21454,7 +21458,7 @@ Spin 的公共契约由 public/protected 类型成员、Avalonia 属性、事件 | --- | --- | --- | | 内容与数据 | `CustomIndicatorTemplate` | 定义控件展示内容、输入数据、模板或业务对象入口。 | | 交互与状态 | `IsMaskBackgroundEnabled`、`IsMaskBlurEnabled`、`IsMotionEnabled`、`IsSpinning`、`IsTipVisible` | 表达用户可观察状态、可用性、清除、加载或反馈语义。 | -| 视觉与布局 | `DotSize`、`IndicatorSize`、`SizeType` | 影响尺寸、位置、颜色、形状、密度和模板视觉变量。 | +| 视觉与布局 | `DotBgBrush`、`DotSize`、`IndicatorSize`、`SizeType` | 影响尺寸、位置、颜色、形状、密度和模板视觉变量。 | | 动效与异步 | `MotionDuration`、`MotionEasingCurve` | 约束动效开关、异步加载、播放速度、超时和任务边界。 | | 其他稳定入口 | `CustomIndicator`、`Tip` | 保留为 public surface,变更前需确认 Gallery 和用户 XAML 依赖。 | @@ -21486,7 +21490,7 @@ Spin 的公共契约由 public/protected 类型成员、Avalonia 属性、事件 ### 基础用法 -来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:36` +来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:97` Gallery key:`ExamplesContent` / item `0` @@ -21496,7 +21500,7 @@ Gallery key:`ExamplesContent` / item `0` ### 尺寸 -来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:47` +来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:108` Gallery key:`ExamplesContent` / item `1` @@ -21509,12 +21513,45 @@ Gallery key:`ExamplesContent` / item `1` ``` -### 自定义描述 +### 嵌入模式 -来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:63` +来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:125` Gallery key:`ExamplesContent` / item `2` +```axaml + + + + + + + + + + + + + +``` + +### 自定义描述 + +来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:159` + +Gallery key:`ExamplesContent` / item `3` + ```axaml @@ -21544,39 +21581,6 @@ Gallery key:`ExamplesContent` / item `2` ``` -### 嵌入模式 - -来源:`controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml:121` - -Gallery key:`ExamplesContent` / item `4` - -```axaml - - - - - - - - - - - - - -``` - ## 状态模型 Spin 的状态流按以下路径收敛: @@ -21627,6 +21631,7 @@ Spin Token 只表达组件级视觉变量,例如尺寸、间距、颜色、圆 资源和 AOT 约束: - 不通过运行时反射扫描 public API、Token 或 Gallery 示例数据。 +- 不通过 VisualTree 扫描维护 Semantic Part;Gallery Preview 使用生成 descriptor 和 owner-scoped marker 解析。 - 不把可静态声明的模板结构迁移到 C# 动态创建。 - 异步加载、上传、弹层和窗口生命周期必须能取消或释放。 - 缓存对象必须与控件、窗口、弹层或数据 owner 生命周期一致。 @@ -21649,6 +21654,8 @@ Spin Token 只表达组件级视觉变量,例如尺寸、间距、颜色、圆 - `src/AtomUI.Desktop.Controls/Spin/Spin.cs` - `src/AtomUI.Desktop.Controls/Spin/SpinIndicator.cs` - `src/AtomUI.Desktop.Controls/Spin/SpinToken.cs` +- `src/AtomUI.Desktop.Controls/Spin/Spin.SemanticParts.cs` +- `src/AtomUI.Desktop.Controls/Spin/SpinIndicator.SemanticParts.cs` - `src/AtomUI.Desktop.Controls/Spin/Themes/SpinIndicatorTheme.axaml` - `src/AtomUI.Desktop.Controls/Spin/Themes/SpinTheme.axaml` @@ -21658,11 +21665,13 @@ Spin Token 只表达组件级视觉变量,例如尺寸、间距、颜色、圆 - Theme 文件负责静态视觉结构、template part、selector 和资源绑定。 - Token 文件只提供组件视觉变量,不保存实例状态。 - Gallery 文件只展示用法和示例,不作为运行时逻辑 owner。 +- Semantic descriptor 由各 public owner 的 `[SemanticPart]` 声明生成;模板只使用静态 `Classes.semantic-*="True"` marker。 ## 相关文档 - 源设计文档:`docs/controls/desktop/feedback/spin/overview.md` - 实现文档:`docs/controls/desktop/feedback/spin/implementation.md` +- Semantic Part 文档:`docs/controls/desktop/feedback/spin/semantic-part.md` - Token 文档:`docs/controls/desktop/feedback/spin/token.md` - 变更记录:`docs/controls/desktop/feedback/spin/changelog.md` - 语义结构:`./semantic-cn.md` diff --git a/docs/AI/generated/llms/llms-semantic-cn.md b/docs/AI/generated/llms/llms-semantic-cn.md index b832c96a0..c1a64dcb8 100644 --- a/docs/AI/generated/llms/llms-semantic-cn.md +++ b/docs/AI/generated/llms/llms-semantic-cn.md @@ -14428,13 +14428,38 @@ Source: ./controls/spin/semantic-cn.md ## Semantic Parts -| Part | AtomUI 节点 | 职责 | 相关 API | 相关 Token | 稳定性 | -| --- | --- | --- | --- | --- | --- | -| `root` | `Spin` | 反馈控件根语义区域,承载 public API、反馈状态和主题入口。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `host` | `宿主或弹层区域` | 承载 overlay、popup、portal、message host、drawer 或 modal 容器。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `surface` | `反馈表面` | 承载背景、边框、阴影、尺寸、placement 和视觉状态。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `content` | `内容区域` | 承载标题、正文、图标、进度、结果、操作或关闭入口。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `motion` | `动效区域` | 表达进入退出、loading、progress、skeleton 或水印刷新反馈。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | +Spin 主控件公开 `root`、`container`、`mask`、`section`、`indicator`、`description` 六个职责区域,与上游稳定语义 +保持同名。`mask` 是 AtomUI 扩展区域,承载嵌套模式的遮罩背景层职责:上游体系的遮罩语义已折叠进全屏模式的根区域, +AtomUI 没有全屏模式,遮罩层是嵌套模式下独立的真实节点。`SpinIndicator` 是独立的公开子控件, +公开 `root` 与 `content`。 + +### 1.1 `Spin` + +| Part | Selector | ContractType | Cardinality | Customization | CrossVisualRoot | RuntimeCreated | +| --- | --- | --- | --- | --- | --- | --- | +| `root` | owner | `Spin` | `Single` | `Root` | `false` | `false` | +| `container` | `.semantic-container` | `ContentPresenter` | `Single` | `Selector` | `false` | `false` | +| `mask` | `.semantic-mask` | `Border` | `Single` | `Selector` | `false` | `false` | +| `section` | `.semantic-section` | `StackPanel` | `Single` | `Selector` | `false` | `false` | +| `indicator` | `.semantic-indicator` | `SpinIndicator` | `Single` | `Selector` | `false` | `false` | +| `description` | `.semantic-description` | `TextBlock` | `Single` | `Selector` | `false` | `false` | + +`container` 承载用户内容的 `ContentPresenter`,spinning 时承担透明度或高斯模糊反馈;`mask` 是遮罩背景层,只在 +`IsMaskBackgroundEnabled` 时呈现 `ColorBgMask`;`section` 是加载区域,承载 `indicator` 与 `description` 并居中; +`indicator` 是主控件模板直接拥有的公开 `SpinIndicator` 子控件,其尺寸分支、动效时长和圆点填充 +(`DotBgBrush`)均可经 Semantic Style 定制;`description` 是提示文本节点,对应公共 API +`Tip` / `IsTipVisible`。`root` 不声明 `.semantic-root` marker。 + +### 1.2 `SpinIndicator` + +| Part | Selector | ContractType | Cardinality | Customization | CrossVisualRoot | RuntimeCreated | +| --- | --- | --- | --- | --- | --- | --- | +| `root` | owner | `SpinIndicator` | `Single` | `Root` | `false` | `false` | +| `content` | `.semantic-content` | `Control` | `Multiple` | `Selector` | `false` | `false` | + +`content` 使用 `Multiple` 是 AtomUI 模板实现的状态替代语义:内置四点指示器 `SpinIndicatorDotPanel` 与自定义指示器 +`PART_CustomIndicatorPresenter` 是两个静态 marker target,由 `IsCustomIndicator` 决定同一时刻只有一个可见。 +Preview 只高亮当前可见 target;Semantic Style 同时作用于两个替代实现,保证切换自定义指示器后定制仍然存在。 ## Abstract AXAML Structure @@ -14503,7 +14528,7 @@ Spin | --- | --- | --- | | 内容与数据 | `CustomIndicatorTemplate` | 定义控件展示内容、输入数据、模板或业务对象入口。 | | 交互与状态 | `IsMaskBackgroundEnabled`、`IsMaskBlurEnabled`、`IsMotionEnabled`、`IsSpinning`、`IsTipVisible` | 表达用户可观察状态、可用性、清除、加载或反馈语义。 | -| 视觉与布局 | `DotSize`、`IndicatorSize`、`SizeType` | 影响尺寸、位置、颜色、形状、密度和模板视觉变量。 | +| 视觉与布局 | `DotBgBrush`、`DotSize`、`IndicatorSize`、`SizeType` | 影响尺寸、位置、颜色、形状、密度和模板视觉变量。 | | 动效与异步 | `MotionDuration`、`MotionEasingCurve` | 约束动效开关、异步加载、播放速度、超时和任务边界。 | | 其他稳定入口 | `CustomIndicator`、`Tip` | 保留为 public surface,变更前需确认 Gallery 和用户 XAML 依赖。 | diff --git a/docs/controls/desktop/feedback/spin/changelog.md b/docs/controls/desktop/feedback/spin/changelog.md index 2940b63a7..5265c9e81 100644 --- a/docs/controls/desktop/feedback/spin/changelog.md +++ b/docs/controls/desktop/feedback/spin/changelog.md @@ -9,6 +9,17 @@ - Docs - Document ancestor visibility tracking and Compositor cleanup ownership. +## 2026-08-16 + +- Semantic Parts + - Add Ant Design-aligned Semantic Part descriptors for `Spin` and the public `SpinIndicator` owner. + - Add static template markers and generated owner-scoped style contracts without runtime discovery. + - Document the dual static content targets used by built-in and custom SpinIndicator implementations. +- API + - Promote `AbstractSpinIndicator.DotBgBrush` to public so the built-in indicator color is customizable through the `indicator` Semantic Part. +- Gallery + - Add the Semantic Parts Preview and Ant Design-aligned custom styling example using AtomUI `v6.1.3` version metadata. + ## 2026-06-26 - Docs diff --git a/docs/controls/desktop/feedback/spin/implementation.md b/docs/controls/desktop/feedback/spin/implementation.md index 107bdb661..fa0a9bcc6 100644 --- a/docs/controls/desktop/feedback/spin/implementation.md +++ b/docs/controls/desktop/feedback/spin/implementation.md @@ -16,6 +16,8 @@ - `src/AtomUI.Desktop.Controls/Spin/Spin.cs` - `src/AtomUI.Desktop.Controls/Spin/SpinIndicator.cs` - `src/AtomUI.Desktop.Controls/Spin/SpinToken.cs` +- `src/AtomUI.Desktop.Controls/Spin/Spin.SemanticParts.cs` +- `src/AtomUI.Desktop.Controls/Spin/SpinIndicator.SemanticParts.cs` - `src/AtomUI.Desktop.Controls/Spin/Themes/SpinIndicatorTheme.axaml` - `src/AtomUI.Desktop.Controls/Spin/Themes/SpinTheme.axaml` @@ -25,6 +27,7 @@ - Theme 文件负责静态视觉结构、template part、selector 和资源绑定。 - Token 文件只提供组件视觉变量,不保存实例状态。 - Gallery 文件只展示用法和示例,不作为运行时逻辑 owner。 +- Semantic descriptor 由各 public owner 的 `[SemanticPart]` 声明生成;模板只使用静态 `Classes.semantic-*="True"` marker。 ## 3. 核心类职责 @@ -57,7 +60,7 @@ Public API / ItemsSource / Command / Event - 内容与数据:`CustomIndicatorTemplate`。 - 交互与状态:`IsMaskBackgroundEnabled`、`IsMaskBlurEnabled`、`IsMotionEnabled`、`IsSpinning`、`IsTipVisible`。 -- 视觉与布局:`DotSize`、`IndicatorSize`、`SizeType`。 +- 视觉与布局:`DotBgBrush`、`DotSize`、`IndicatorSize`、`SizeType`。 - 动效与异步:`MotionDuration`、`MotionEasingCurve`。 - 其他稳定入口:`CustomIndicator`、`Tip`。 @@ -82,6 +85,15 @@ Public API / ItemsSource / Command / Event - `PART_CustomIndicatorPresenter`:展示用户内容、文本、图标或模板化数据。 +`SpinTheme` 的模板使用两层叠加结构:`ContentPresenter.semantic-container` 承载用户内容,`MaskLayout` 面板在 +`IsSpinning=True` 时可见,其内部 `Border.semantic-mask` 提供遮罩背景,`StackPanel.semantic-section` 承载 +`SpinIndicator.semantic-indicator` 与 `TextBlock.semantic-description` 并居中。遮罩层组与内容容器是同一根面板下的 +sibling,确保 Semantic Preview 高亮 `container` 时只覆盖用户内容区域。 + +`SpinIndicatorTheme` 的模板由两个静态替代实现构成:`SpinIndicatorDotPanel.semantic-content` 承载四个内置圆点, +`PART_CustomIndicatorPresenter.semantic-content` 承载自定义指示器;`IsCustomIndicator` 决定当前可见 target,两个 +target 都保持 marker。四个圆点(`Ellipse.SpinIndicatorDot`)是内部 Composition 节点,不属于任何 Semantic Part。 + ## 6. 交互与事件处理 Spin 的交互事件应从输入源收敛到控件级语义事件: @@ -102,6 +114,10 @@ Spin 的交互事件应从输入源收敛到控件级语义事件: - 主题资源、Token 和 SharedToken 计算后的视觉更新。 - 内容、命令和视觉状态在模板节点之间的同步。 - 动效启停、初始加载阶段 transition 抑制和卸载取消。 +- Semantic Part marker 不参与状态切换;`IsSpinning`、`IsTipVisible` 和 `IsCustomIndicator` 只改变目标节点的 + `IsVisible`,marker 身份和数量保持不变。 +- 指示器旋转与圆点透明度动画由 `AbstractSpinIndicator` 通过 Composition 动画直接拥有;动画目标从 + `IsCustomIndicator` 推导,状态切换只重启动画,不增删 marker 节点。 `AbstractSpinIndicator` 的旋转和内置 dot opacity 由 Compositor 无限动画驱动。Compositor 不提供控件级有效可见性暂停语义,因此 indicator 在 attach 后跟踪自身及 Visual 祖先链;有效不可见时停止所有目标动画并复位视觉状态,恢复可见时重新解析当前 built-in/custom target 并启动动画。detach 时必须释放可见性订阅和所有 Compositor target。 @@ -112,6 +128,7 @@ Spin 的交互事件应从输入源收敛到控件级语义事件: 资源和 AOT 约束: - 不通过运行时反射扫描 public API、Token 或 Gallery 示例数据。 +- 不通过 VisualTree 扫描维护 Semantic Part;Gallery Preview 使用生成 descriptor 和 owner-scoped marker 解析。 - 不把可静态声明的模板结构迁移到 C# 动态创建。 - 异步加载、上传、弹层和窗口生命周期必须能取消或释放。 - 缓存对象必须与控件、窗口、弹层或数据 owner 生命周期一致。 diff --git a/docs/controls/desktop/feedback/spin/overview.md b/docs/controls/desktop/feedback/spin/overview.md index 1e570bbc6..e1123cb88 100644 --- a/docs/controls/desktop/feedback/spin/overview.md +++ b/docs/controls/desktop/feedback/spin/overview.md @@ -42,7 +42,7 @@ Spin 的公共契约由 public/protected 类型成员、Avalonia 属性、事件 | --- | --- | --- | | 内容与数据 | `CustomIndicatorTemplate` | 定义控件展示内容、输入数据、模板或业务对象入口。 | | 交互与状态 | `IsMaskBackgroundEnabled`、`IsMaskBlurEnabled`、`IsMotionEnabled`、`IsSpinning`、`IsTipVisible` | 表达用户可观察状态、可用性、清除、加载或反馈语义。 | -| 视觉与布局 | `DotSize`、`IndicatorSize`、`SizeType` | 影响尺寸、位置、颜色、形状、密度和模板视觉变量。 | +| 视觉与布局 | `DotBgBrush`、`DotSize`、`IndicatorSize`、`SizeType` | 影响尺寸、位置、颜色、形状、密度和模板视觉变量。 | | 动效与异步 | `MotionDuration`、`MotionEasingCurve` | 约束动效开关、异步加载、播放速度、超时和任务边界。 | | 其他稳定入口 | `CustomIndicator`、`Tip` | 保留为 public surface,变更前需确认 Gallery 和用户 XAML 依赖。 | @@ -144,17 +144,22 @@ Spin 的视觉选项通过 public API 归一为 theme variables、伪类或模 - [Spin 桌面版实现原理](implementation.md) - [Spin Token 设计](token.md) +- [Spin Semantic Part 契约](semantic-part.md) - [Spin Changelog](changelog.md) LLMS 语义区域: | Part | AtomUI 节点 | 职责 | 相关 API | 相关 Token | 稳定性 | | --- | --- | --- | --- | --- | --- | -| `root` | `Spin` | 反馈控件根语义区域,承载 public API、反馈状态和主题入口。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `host` | `宿主或弹层区域` | 承载 overlay、popup、portal、message host、drawer 或 modal 容器。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `surface` | `反馈表面` | 承载背景、边框、阴影、尺寸、placement 和视觉状态。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `content` | `内容区域` | 承载标题、正文、图标、进度、结果、操作或关闭入口。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | -| `motion` | `动效区域` | 表达进入退出、loading、progress、skeleton 或水印刷新反馈。 | 见 API 与契约模型 | 见视觉与主题模型 | stable | +| `root` | `Spin` | 主控件根区域,承载加载状态、遮罩选项和主题入口。 | 见 API 与契约模型 | `SpinToken` | stable | +| `container` | `ContentPresenter` | 嵌套模式的用户内容容器,spinning 时承担透明度或模糊反馈。 | `IsSpinning`、`IsMaskBlurEnabled` | 无 | stable | +| `mask` | `Border` | 遮罩背景层,spinning 时覆盖内容区域。 | `IsMaskBackgroundEnabled` | `ColorBgMask` | stable | +| `section` | `StackPanel` | 加载区域,承载指示器和提示文本并居中。 | `SizeType` | `SpacingXXS` | stable | +| `indicator` | `SpinIndicator` | 主控件模板拥有的加载指示子控件。 | `CustomIndicator`、`DotBgBrush` | `IndicatorSize*`、`DotSize*` | stable | +| `description` | `TextBlock` | 提示文本区域。 | `Tip`、`IsTipVisible` | `ColorPrimary` | stable | + +`SpinIndicator` 的 `root/content` descriptor 与主控件分开维护,完整契约见 +[Spin Semantic Part 契约](semantic-part.md)。 LLMS 导出来源: diff --git a/docs/controls/desktop/feedback/spin/semantic-part.md b/docs/controls/desktop/feedback/spin/semantic-part.md new file mode 100644 index 000000000..c43755dd6 --- /dev/null +++ b/docs/controls/desktop/feedback/spin/semantic-part.md @@ -0,0 +1,99 @@ +# Spin Semantic Part 契约 + +本文档定义 Spin 家族公开的 Semantic Part、选择器、类型约束、数量语义和定制边界。Spin 的整体设计见 +[Spin 桌面版架构设计](overview.md),真实模板与生命周期见 [Spin 桌面版实现原理](implementation.md),系统级规则见 +[AtomUI Semantic Part 系统设计](../../../../architecture/systems/theming/semantic-parts.md)。 + +## 1. Semantic Parts + +Spin 主控件公开 `root`、`container`、`mask`、`section`、`indicator`、`description` 六个职责区域,与上游稳定语义 +保持同名。`mask` 是 AtomUI 扩展区域,承载嵌套模式的遮罩背景层职责:上游体系的遮罩语义已折叠进全屏模式的根区域, +AtomUI 没有全屏模式,遮罩层是嵌套模式下独立的真实节点。`SpinIndicator` 是独立的公开子控件, +公开 `root` 与 `content`。 + +### 1.1 `Spin` + +| Part | Selector | ContractType | Cardinality | Customization | CrossVisualRoot | RuntimeCreated | +| --- | --- | --- | --- | --- | --- | --- | +| `root` | owner | `Spin` | `Single` | `Root` | `false` | `false` | +| `container` | `.semantic-container` | `ContentPresenter` | `Single` | `Selector` | `false` | `false` | +| `mask` | `.semantic-mask` | `Border` | `Single` | `Selector` | `false` | `false` | +| `section` | `.semantic-section` | `StackPanel` | `Single` | `Selector` | `false` | `false` | +| `indicator` | `.semantic-indicator` | `SpinIndicator` | `Single` | `Selector` | `false` | `false` | +| `description` | `.semantic-description` | `TextBlock` | `Single` | `Selector` | `false` | `false` | + +`container` 承载用户内容的 `ContentPresenter`,spinning 时承担透明度或高斯模糊反馈;`mask` 是遮罩背景层,只在 +`IsMaskBackgroundEnabled` 时呈现 `ColorBgMask`;`section` 是加载区域,承载 `indicator` 与 `description` 并居中; +`indicator` 是主控件模板直接拥有的公开 `SpinIndicator` 子控件,其尺寸分支、动效时长和圆点填充 +(`DotBgBrush`)均可经 Semantic Style 定制;`description` 是提示文本节点,对应公共 API +`Tip` / `IsTipVisible`。`root` 不声明 `.semantic-root` marker。 + +### 1.2 `SpinIndicator` + +| Part | Selector | ContractType | Cardinality | Customization | CrossVisualRoot | RuntimeCreated | +| --- | --- | --- | --- | --- | --- | --- | +| `root` | owner | `SpinIndicator` | `Single` | `Root` | `false` | `false` | +| `content` | `.semantic-content` | `Control` | `Multiple` | `Selector` | `false` | `false` | + +`content` 使用 `Multiple` 是 AtomUI 模板实现的状态替代语义:内置四点指示器 `SpinIndicatorDotPanel` 与自定义指示器 +`PART_CustomIndicatorPresenter` 是两个静态 marker target,由 `IsCustomIndicator` 决定同一时刻只有一个可见。 +Preview 只高亮当前可见 target;Semantic Style 同时作用于两个替代实现,保证切换自定义指示器后定制仍然存在。 + +## 2. Selector 用法 + +应用侧只使用 owner-scoped selector 和生成的 Style 类型,不复制 `/template/` 路径: + +```xml + +``` + +`SpinContainerStyle`、`SpinIndicatorStyle` 与 `SpinIndicator` owner 的 `SpinIndicatorContentStyle` 使用同一规则。 +Semantic Style 只覆盖视觉属性和布局属性,不改变 `IsSpinning`、SizeType 分支、`CustomIndicator` 内容或动画状态。 + +## 3. Template 与状态边界 + +- 主控件的 `container`、`mask`、`section`、`indicator` 和 `description` marker 都由 Spin 自己模板提供。 + `container` 与遮罩层组是根布局的 sibling;`mask` 与 `section` 是遮罩层组内的 sibling,`section` 是 + `indicator` 和 `description` 的唯一父容器。 +- 所有 marker 节点在模板中静态存在;`IsSpinning` 只切换遮罩层组的 `IsVisible`,`IsTipVisible` 只切换 + `description` 的 `IsVisible`,都不改变 descriptor、marker 身份或数量。 +- `indicator` Part 的契约边界止于公开 `SpinIndicator` 控件本身;其模板内的四个圆点 + (`Ellipse.SpinIndicatorDot`)、内置布局面板和自定义指示器 presenter 都是 `SpinIndicator` owner 的内部结构, + 不由 `Spin` owner 公开。 +- `SpinIndicator` 的 `content` marker 位于静态内置点阵面板和静态自定义 presenter;不公开 `PART_*` 名称,四个圆点 + 不作为独立 Part,Composition 动画对象不属于任何 Part。 +- 模板重套用后由新模板重新提供同一 marker 契约;隐藏遮罩层组或切换指示器实现不改变 Part 数量。 + +## 4. 尺寸与布局基线 + +`SpinIndicator` 的 Large/Middle/Small 分支由 `SizeType` 与 `SpinToken`(`IndicatorSizeLG/IndicatorSize/IndicatorSizeSM` +和对应 `DotSize*`)控制;`SizeType=Custom` 默认回落到 Middle 尺寸,显式设置 `IndicatorSize` 或 `DotSize` 时以本地值 +优先。Semantic Style 不以固定 Width/Height 替代这些完整分支;需要定制尺寸时应设置公开尺寸 API。 + +主控件的根默认 `HorizontalAlignment=Left`、`VerticalAlignment=Top`;嵌套模式下 `container` 按用户内容测量, +遮罩层组与 `container` 同区域叠加。`description` 默认前景色由主题从 `ColorPrimary` 投影,`section` 间距来自 +`SpacingXXS`;Semantic Style 覆盖这些值时作用于同一节点,不创建平行样式入口。 + +Spinning 反馈在 `container` 上有两条互斥路径:`IsMaskBlurEnabled=True` 时对 `container` 应用高斯模糊,否则把 +内容透明度降为 `0.5`(经 `MaskOpacity` transition)。Semantic Style 作用于 `container` 时应保留这两条状态路径, +不改写透明度状态机。 + +## 5. 验证不变量 + +- `Spin` descriptor 只包含 `root/container/mask/section/indicator/description`。 +- `SpinIndicator` descriptor 只包含 `root/content`,`content` 为两个静态替代 target。 +- Desktop 所有叶子 Spin 主题均声明静态 `Classes.semantic-*="True"`,不使用 Binding 或 `False` marker。 +- 默认主题不消费 `.semantic-*` selector;Semantic Part 只增加静态 class、descriptor 和生成 Style。 +- Gallery Semantic Parts Tab 延迟创建主控件和 `SpinIndicator` Preview;Examples Tab 的既有示例树保持不变。 +- Semantic 示例标题使用 `Custom Semantic Part styling`,文案只描述 AtomUI owner-scoped Style;Tag 使用当前版本 + `GalleryVersionInfo.DisplayVersion`(`v6.1.3`)。 diff --git a/src/AtomUI.Controls/Spin/AbstractSpinIndicator.cs b/src/AtomUI.Controls/Spin/AbstractSpinIndicator.cs index 985ee90f4..8f97b6462 100644 --- a/src/AtomUI.Controls/Spin/AbstractSpinIndicator.cs +++ b/src/AtomUI.Controls/Spin/AbstractSpinIndicator.cs @@ -40,6 +40,12 @@ public abstract class AbstractSpinIndicator : TemplatedControl, ICustomizableSiz public static readonly StyledProperty DotSizeProperty = AvaloniaProperty.Register(nameof(DotSize), double.NaN); + /// + /// 内置指示器圆点的填充画刷。 + /// + public static readonly StyledProperty DotBgBrushProperty = + AvaloniaProperty.Register(nameof(DotBgBrush)); + public CustomizableSizeType SizeType { get => GetValue(SizeTypeProperty); @@ -83,25 +89,22 @@ public abstract class AbstractSpinIndicator : TemplatedControl, ICustomizableSiz set => SetValue(DotSizeProperty, value); } + public IBrush? DotBgBrush + { + get => GetValue(DotBgBrushProperty); + set => SetValue(DotBgBrushProperty, value); + } + #endregion #region 内部属性定义 - internal static readonly StyledProperty DotBgBrushProperty = - AvaloniaProperty.Register(nameof(DotBgBrush)); - internal static readonly DirectProperty IsCustomIndicatorProperty = AvaloniaProperty.RegisterDirect( nameof(IsCustomIndicator), o => o.IsCustomIndicator, (o, v) => o.IsCustomIndicator = v); - internal IBrush? DotBgBrush - { - get => GetValue(DotBgBrushProperty); - set => SetValue(DotBgBrushProperty, value); - } - private bool _isCustomIndicator; internal bool IsCustomIndicator @@ -375,11 +378,11 @@ public abstract class AbstractSpinIndicator : TemplatedControl, ICustomizableSiz var easing = MotionEasingCurve ?? new LinearEasing(); var index = 0; - foreach (var child in _builtInIndicatorLayout.Children) + foreach (var dot in _builtInIndicatorLayout.Children) { - if (child is not Control dot || index >= 4) + if (index >= 4) { - continue; + break; } var dotVisual = ElementComposition.GetElementVisual(dot); @@ -412,12 +415,9 @@ public abstract class AbstractSpinIndicator : TemplatedControl, ICustomizableSiz return; } - foreach (var child in _builtInIndicatorLayout.Children) + foreach (var dot in _builtInIndicatorLayout.Children) { - if (child is Control dot) - { - ElementComposition.GetElementVisual(dot)?.StopAnimation(OPACITY_PROPERTY); - } + ElementComposition.GetElementVisual(dot)?.StopAnimation(OPACITY_PROPERTY); } } @@ -429,13 +429,15 @@ public abstract class AbstractSpinIndicator : TemplatedControl, ICustomizableSiz } var index = 0; - foreach (var child in _builtInIndicatorLayout.Children) + foreach (var dot in _builtInIndicatorLayout.Children) { - if (child is Control dot && index < 4) + if (index >= 4) { - dot.Opacity = DOT_START_OPACITY; - index++; + break; } + + dot.Opacity = DOT_START_OPACITY; + index++; } } diff --git a/src/AtomUI.Desktop.Controls/Spin/Spin.SemanticParts.cs b/src/AtomUI.Desktop.Controls/Spin/Spin.SemanticParts.cs new file mode 100644 index 000000000..db2e15ec3 --- /dev/null +++ b/src/AtomUI.Desktop.Controls/Spin/Spin.SemanticParts.cs @@ -0,0 +1,34 @@ +using AtomUI.Theme; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; + +namespace AtomUI.Desktop.Controls; + +[SemanticPart( + "container", + SelectorClass = "semantic-container", + ContractType = typeof(ContentPresenter), + Since = "6.0")] +[SemanticPart( + "mask", + SelectorClass = "semantic-mask", + ContractType = typeof(Border), + Since = "6.0")] +[SemanticPart( + "section", + SelectorClass = "semantic-section", + ContractType = typeof(StackPanel), + Since = "6.0")] +[SemanticPart( + "indicator", + SelectorClass = "semantic-indicator", + ContractType = typeof(SpinIndicator), + Since = "6.0")] +[SemanticPart( + "description", + SelectorClass = "semantic-description", + ContractType = typeof(TextBlock), + Since = "6.0")] +public partial class Spin +{ +} diff --git a/src/AtomUI.Desktop.Controls/Spin/Spin.cs b/src/AtomUI.Desktop.Controls/Spin/Spin.cs index 8ac713bb0..e264fd92b 100644 --- a/src/AtomUI.Desktop.Controls/Spin/Spin.cs +++ b/src/AtomUI.Desktop.Controls/Spin/Spin.cs @@ -2,7 +2,7 @@ using AtomUI.Controls.Commons; namespace AtomUI.Desktop.Controls; -public class Spin : AbstractSpin +public partial class Spin : AbstractSpin { public Spin() { diff --git a/src/AtomUI.Desktop.Controls/Spin/SpinIndicator.SemanticParts.cs b/src/AtomUI.Desktop.Controls/Spin/SpinIndicator.SemanticParts.cs new file mode 100644 index 000000000..793cb3644 --- /dev/null +++ b/src/AtomUI.Desktop.Controls/Spin/SpinIndicator.SemanticParts.cs @@ -0,0 +1,14 @@ +using AtomUI.Theme; +using Avalonia.Controls; + +namespace AtomUI.Desktop.Controls; + +[SemanticPart( + "content", + SelectorClass = "semantic-content", + ContractType = typeof(Control), + Cardinality = SemanticPartCardinality.Multiple, + Since = "6.0")] +public partial class SpinIndicator +{ +} diff --git a/src/AtomUI.Desktop.Controls/Spin/SpinIndicator.cs b/src/AtomUI.Desktop.Controls/Spin/SpinIndicator.cs index 992f76383..897d30dae 100644 --- a/src/AtomUI.Desktop.Controls/Spin/SpinIndicator.cs +++ b/src/AtomUI.Desktop.Controls/Spin/SpinIndicator.cs @@ -2,7 +2,7 @@ using AtomUI.Controls.Commons; namespace AtomUI.Desktop.Controls; -public class SpinIndicator : AbstractSpinIndicator +public partial class SpinIndicator : AbstractSpinIndicator { public SpinIndicator() { diff --git a/src/AtomUI.Desktop.Controls/Spin/Themes/SpinIndicatorTheme.axaml b/src/AtomUI.Desktop.Controls/Spin/Themes/SpinIndicatorTheme.axaml index 19ff609ea..87af2c255 100644 --- a/src/AtomUI.Desktop.Controls/Spin/Themes/SpinIndicatorTheme.axaml +++ b/src/AtomUI.Desktop.Controls/Spin/Themes/SpinIndicatorTheme.axaml @@ -10,6 +10,7 @@ - + part.Name) + .ShouldBe(["root", "container", "description", "indicator", "mask", "section"]); + + AssertRoot(spinDescriptor, typeof(AtomUISpin)); + AssertPart(spinDescriptor, "container", "semantic-container", typeof(ContentPresenter), SemanticPartCardinality.Single); + AssertPart(spinDescriptor, "mask", "semantic-mask", typeof(Border), SemanticPartCardinality.Single); + AssertPart(spinDescriptor, "section", "semantic-section", typeof(StackPanel), SemanticPartCardinality.Single); + AssertPart(spinDescriptor, "indicator", "semantic-indicator", typeof(AtomUISpinIndicator), SemanticPartCardinality.Single); + AssertPart(spinDescriptor, "description", "semantic-description", typeof(TextBlock), SemanticPartCardinality.Single); + + manager.SemanticParts.TryGetControl(typeof(AtomUISpinIndicator), out var indicatorDescriptor).ShouldBeTrue(); + indicatorDescriptor.ShouldNotBeNull(); + indicatorDescriptor.Parts.Select(static part => part.Name).ShouldBe(["root", "content"]); + AssertRoot(indicatorDescriptor, typeof(AtomUISpinIndicator)); + AssertPart(indicatorDescriptor, "content", "semantic-content", typeof(Control), SemanticPartCardinality.Multiple); + } + + [Theory] + [InlineData("src/AtomUI.Desktop.Controls/Spin/Themes/SpinTheme.axaml")] + [InlineData("src/AtomUI.Desktop.Controls/Spin/Themes/SpinIndicatorTheme.axaml")] + public void Built_In_Themes_Use_Only_Static_Semantic_Markers(string relativePath) + { + var document = XDocument.Load(GetRepoFile(relativePath), LoadOptions.SetLineInfo); + var literalMarkers = document.Descendants() + .Attributes("Classes") + .Where(static attribute => attribute.Value.Split( + (char[]?)null, + StringSplitOptions.RemoveEmptyEntries) + .Any(static value => value.StartsWith("semantic-", StringComparison.Ordinal))) + .ToArray(); + var propertyMarkers = document.Descendants() + .Attributes() + .Where(static attribute => attribute.Name.LocalName.StartsWith( + "Classes.semantic-", + StringComparison.Ordinal)) + .ToArray(); + + literalMarkers.ShouldBeEmpty(); + propertyMarkers.ShouldNotBeEmpty(); + propertyMarkers.ShouldAllBe(static attribute => + string.Equals(attribute.Value, "true", StringComparison.OrdinalIgnoreCase)); + propertyMarkers.ShouldAllBe(static attribute => + !string.Equals(attribute.Value, "false", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void Default_Themes_Do_Not_Consume_Semantic_Selectors() + { + foreach (var relativePath in new[] + { + "src/AtomUI.Desktop.Controls/Spin/Themes/SpinTheme.axaml", + "src/AtomUI.Desktop.Controls/Spin/Themes/SpinIndicatorTheme.axaml" + }) + { + var document = XDocument.Load(GetRepoFile(relativePath), LoadOptions.SetLineInfo); + var selectors = document.Descendants() + .Where(static element => element.Name.LocalName == "Style") + .Attributes("Selector") + .Select(static attribute => attribute.Value) + .ToArray(); + selectors.ShouldAllBe(selector => !selector.Contains("semantic-", StringComparison.Ordinal)); + } + } + + [Fact] + public void Generated_Semantic_Styles_Apply_To_The_Spin_Template_Targets() + { + var manager = Application.Current.ShouldNotBeNull().GetThemeManager().ShouldNotBeNull(); + manager.SemanticParts.TryGetControl(typeof(AtomUISpin), out var descriptor).ShouldBeTrue(); + descriptor.ShouldNotBeNull(); + + var spin = new AtomUISpin + { + IsSpinning = true, + IsTipVisible = true, + Tip = "Loading...", + Content = new Border { Width = 120, Height = 80 } + }; + spin.Classes.Add("semantic-owner"); + var ownerStyle = new Style(selector => selector.OfType().Class("semantic-owner")); + ownerStyle.Setters.Add(new Setter(Control.TagProperty, "root")); + foreach (var part in descriptor.Parts.Where(static part => part.Name != "root")) + { + var partStyle = (Style)Activator.CreateInstance(part.StyleType.ShouldNotBeNull()).ShouldNotBeNull(); + partStyle.Setters.Add(new Setter(Control.TagProperty, part.Name)); + ownerStyle.Children.Add(partStyle); + } + spin.Styles.Add(ownerStyle); + + using var window = Show(spin); + spin.Tag.ShouldBe("root"); + FindSemanticControl(spin, "semantic-container").Tag.ShouldBe("container"); + FindSemanticControl(spin, "semantic-mask").Tag.ShouldBe("mask"); + FindSemanticControl(spin, "semantic-section").Tag.ShouldBe("section"); + FindSemanticControl(spin, "semantic-indicator").Tag.ShouldBe("indicator"); + FindSemanticControl(spin, "semantic-description").Tag.ShouldBe("description"); + } + + [Fact] + public void Generated_Content_Style_Applies_To_Both_SpinIndicator_Alternatives() + { + var manager = Application.Current.ShouldNotBeNull().GetThemeManager().ShouldNotBeNull(); + manager.SemanticParts.TryGetControl(typeof(AtomUISpinIndicator), out var descriptor).ShouldBeTrue(); + descriptor.ShouldNotBeNull(); + + var indicator = new AtomUISpinIndicator(); + indicator.Classes.Add("semantic-owner"); + var ownerStyle = new Style(selector => selector.OfType().Class("semantic-owner")); + var contentPart = descriptor.Parts.Single(static part => part.Name == "content"); + var contentStyle = (Style)Activator.CreateInstance(contentPart.StyleType.ShouldNotBeNull()).ShouldNotBeNull(); + contentStyle.Setters.Add(new Setter(Control.TagProperty, "content")); + ownerStyle.Children.Add(contentStyle); + indicator.Styles.Add(ownerStyle); + + using var window = Show(indicator); + var contentTargets = indicator.GetVisualDescendants() + .OfType() + .Where(static control => control.Classes.Contains("semantic-content")) + .ToArray(); + contentTargets.Length.ShouldBe(2); + contentTargets.ShouldAllBe(static target => target.Tag as string == "content"); + contentTargets.Count(static target => target.IsVisible).ShouldBe(1); + } + + [Fact] + public void Spinning_Toggle_Preserves_Overlay_Marker_Identity() + { + var spin = new AtomUISpin + { + IsSpinning = true, + IsTipVisible = true, + Tip = "Loading...", + Content = new Border { Width = 120, Height = 80 } + }; + + using var window = Show(spin); + var section = FindSemanticControl(spin, "semantic-section"); + var mask = FindSemanticControl(spin, "semantic-mask"); + var overlay = spin.GetVisualDescendants() + .OfType() + .Single(static panel => panel.Name == "MaskLayout"); + overlay.IsVisible.ShouldBeTrue(); + + spin.IsSpinning = false; + Dispatcher.UIThread.RunJobs(); + + overlay.IsVisible.ShouldBeFalse(); + FindSemanticControl(spin, "semantic-section").ShouldBeSameAs(section); + FindSemanticControl(spin, "semantic-mask").ShouldBeSameAs(mask); + + spin.IsSpinning = true; + Dispatcher.UIThread.RunJobs(); + + overlay.IsVisible.ShouldBeTrue(); + FindSemanticControl(spin, "semantic-section").ShouldBeSameAs(section); + FindSemanticControl(spin, "semantic-mask").ShouldBeSameAs(mask); + } + + [Fact] + public void Tip_Visibility_Toggle_Preserves_Description_Marker() + { + var spin = new AtomUISpin + { + IsSpinning = true, + IsTipVisible = false, + Tip = "Loading...", + Content = new Border { Width = 120, Height = 80 } + }; + + using var window = Show(spin); + var description = FindSemanticControl(spin, "semantic-description"); + description.IsVisible.ShouldBeFalse(); + description.Text.ShouldBe("Loading..."); + + spin.IsTipVisible = true; + Dispatcher.UIThread.RunJobs(); + + FindSemanticControl(spin, "semantic-description").ShouldBeSameAs(description); + description.IsVisible.ShouldBeTrue(); + } + + [Fact] + public void Custom_Indicator_Switch_Preserves_Content_Marker_Identity() + { + var indicator = new AtomUISpinIndicator(); + + using var window = Show(indicator); + var contentTargets = indicator.GetVisualDescendants() + .OfType() + .Where(static control => control.Classes.Contains("semantic-content")) + .ToArray(); + contentTargets.Length.ShouldBe(2); + var builtIn = contentTargets.Single(static target => target.IsVisible); + builtIn.Name.ShouldBe("BuiltInIndicatorLayout"); + + indicator.CustomIndicator = new Border { Width = 24, Height = 24 }; + Dispatcher.UIThread.RunJobs(); + + var updatedTargets = indicator.GetVisualDescendants() + .OfType() + .Where(static control => control.Classes.Contains("semantic-content")) + .ToArray(); + updatedTargets.ShouldBe(contentTargets); + updatedTargets.Count(static target => target.IsVisible).ShouldBe(1); + updatedTargets.Single(static target => target.IsVisible).Name.ShouldBe("PART_CustomIndicatorPresenter"); + + indicator.CustomIndicator = null; + Dispatcher.UIThread.RunJobs(); + + indicator.GetVisualDescendants() + .OfType() + .Where(static control => control.Classes.Contains("semantic-content")) + .ShouldBe(contentTargets); + builtIn.IsVisible.ShouldBeTrue(); + } + + [Fact] + public void Standalone_Spin_Without_Content_Keeps_Marker_Contract() + { + var spin = new AtomUISpin + { + IsSpinning = true, + IsTipVisible = true, + Tip = "Loading..." + }; + + using var window = Show(spin); + FindSemanticControl(spin, "semantic-container").ShouldNotBeNull(); + FindSemanticControl(spin, "semantic-mask").ShouldNotBeNull(); + FindSemanticControl(spin, "semantic-section").ShouldNotBeNull(); + FindSemanticControl(spin, "semantic-indicator").ShouldNotBeNull(); + FindSemanticControl(spin, "semantic-description").ShouldNotBeNull(); + } + + [Fact] + public void SizeType_Branches_Produce_Distinct_Indicator_Metrics() + { + var small = new AtomUISpinIndicator { SizeType = CustomizableSizeType.Small }; + var middle = new AtomUISpinIndicator { SizeType = CustomizableSizeType.Middle }; + var large = new AtomUISpinIndicator { SizeType = CustomizableSizeType.Large }; + var custom = new AtomUISpinIndicator { SizeType = CustomizableSizeType.Custom }; + var host = new StackPanel + { + Children = { small, middle, large, custom } + }; + + using var window = Show(host); + small.IndicatorSize.ShouldBeLessThan(middle.IndicatorSize); + middle.IndicatorSize.ShouldBeLessThan(large.IndicatorSize); + small.DotSize.ShouldBeLessThan(middle.DotSize); + middle.DotSize.ShouldBeLessThan(large.DotSize); + custom.IndicatorSize.ShouldBe(middle.IndicatorSize); + custom.DotSize.ShouldBe(middle.DotSize); + + custom.IndicatorSize = 44; + custom.DotSize = 14; + Dispatcher.UIThread.RunJobs(); + + custom.IndicatorSize.ShouldBe(44); + custom.DotSize.ShouldBe(14); + } + + [Fact] + public void Dot_Bg_Brush_Projects_To_All_Built_In_Dots() + { + var brush = new SolidColorBrush(Color.Parse("#00d4ff")); + var indicator = new AtomUISpinIndicator + { + DotBgBrush = brush + }; + + using var window = Show(indicator); + var dots = indicator.GetVisualDescendants() + .OfType() + .Where(static dot => dot.Classes.Contains("SpinIndicatorDot")) + .ToArray(); + dots.Length.ShouldBe(4); + foreach (var dot in dots) + { + dot.Fill.ShouldBeSameAs(brush); + } + } + + private static void AssertRoot(ControlSemanticDescriptor descriptor, Type ownerType) + { + var root = descriptor.Parts.Single(static part => part.Name == "root"); + root.Path.ShouldBe("root"); + root.SelectorClass.ShouldBeNull(); + root.SelectorRoute.ShouldBeNull(); + root.ContractType.ShouldBe(ownerType); + root.Cardinality.ShouldBe(SemanticPartCardinality.Single); + root.Customization.ShouldBe(SemanticPartCustomization.Root); + root.Since.ShouldBe("6.0"); + } + + private static void AssertPart( + ControlSemanticDescriptor descriptor, + string name, + string selectorClass, + Type contractType, + SemanticPartCardinality cardinality) + { + var part = descriptor.Parts.Single(candidate => candidate.Name == name); + part.Path.ShouldBe(name); + part.SelectorClass.ShouldBe(selectorClass); + part.ContractType.ShouldBe(contractType); + part.Cardinality.ShouldBe(cardinality); + part.Customization.ShouldBe(SemanticPartCustomization.Selector); + part.StyleType.ShouldNotBeNull(); + part.Since.ShouldBe("6.0"); + } + + private static T FindSemanticControl(Control owner, string semanticClass) + where T : Control + { + return owner.GetVisualDescendants() + .OfType() + .Single(control => control.Classes.Contains(semanticClass)); + } + + private static IDisposable Show(Control content) + { + var window = new AvaloniaWindow + { + Width = 480, + Height = 260, + Content = content + }; + window.Show(); + Dispatcher.UIThread.RunJobs(); + return new WindowLifetime(window); + } + + private static string GetRepoFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException($"Unable to locate repository file '{relativePath}'."); + } + + private sealed class WindowLifetime(AvaloniaWindow window) : IDisposable + { + public void Dispose() + { + window.Close(); + Dispatcher.UIThread.RunJobs(); + } + } +} diff --git a/tests/AtomUIGallery.Tests/Localization/CatalogMemberOrder.baseline b/tests/AtomUIGallery.Tests/Localization/CatalogMemberOrder.baseline index 4e44cbe72..4159668d6 100644 --- a/tests/AtomUIGallery.Tests/Localization/CatalogMemberOrder.baseline +++ b/tests/AtomUIGallery.Tests/Localization/CatalogMemberOrder.baseline @@ -71,7 +71,7 @@ AtomUIGallery.Localization.SeparatorShowCaseLangResourceKind|ScenarioExamples,Co AtomUIGallery.Localization.SkeletonShowCaseLangResourceKind|BasicTitle,BasicDescription,ComplexCombinationTitle,ComplexCombinationDescription,ActiveAnimationTitle,ActiveAnimationDescription,ButtonAvatarInputImageNodeTitle,ButtonAvatarInputImageNodeDescription,ContainsSubComponentTitle,ContainsSubComponentDescription,SemanticPartStyleTitle,SemanticPartStyleDescription,SemanticRootDescription,SemanticHeaderDescription,SemanticSectionDescription,SemanticAvatarDescription,SemanticTitleDescription,SemanticParagraphDescription,SemanticElementRootDescription,SemanticElementContentDescription,PageSubtitle,PageDescription,ComponentCategory,ComponentStatusStable,ScenarioExamples,P2TextActive,P2TextButtonAndInputBlock,P2TextSize,P2ContentDefault,P2ContentLarge,P2ContentSmall,P2TextButtonShape,P2ContentSquare,P2ContentRound,P2ContentCircle,P2TextAvatarShape,P2TextAntDesignADesignLanguage,P2TextWeSupplyASeriesOfDesignPrinciplesPractical,P2ContentShowSkeleton AtomUIGallery.Localization.SliderShowCaseLangResourceKind|ScenarioExamples,ComponentCategory,ComponentStatusStable,PageSubtitle,PageDescription,BasicTitle,BasicDescription,MultiHandleTitle,MultiHandleDescription,DisabledHandleTitle,DisabledHandleDescription,RangeValuesBindingTitle,RangeValuesBindingDescription,CustomizeTooltipTitle,CustomizeTooltipDescription,VerticalTitle,VerticalDescription,GraduatedSliderTitle,GraduatedSliderDescription,P2TextEnabled,P2TextIncludedTrue,P2TextIncludedFalse,P2TextBoundRangeValues,P2ContentSetRange,P2ContentClear,P2ContentDisabledHandle1,P2ContentDisabledHandle2,P2ContentDisabledHandle3 AtomUIGallery.Localization.SpaceShowCaseLangResourceKind|BasicTitle,BasicDescription,VerticalSpaceTitle,VerticalSpaceDescription,SizeTitle,SizeDescription,AlignTitle,AlignDescription,WrapTitle,WrapDescription,SplitTitle,SplitDescription,CompactFormTitle,CompactFormDescription,CompactButtonTitle,CompactButtonDescription,VerticalCompactTitle,VerticalCompactDescription,ScenarioBasic,ScenarioSize,ScenarioAlign,ScenarioCompactForm,ScenarioCompactButton,ScenarioExamples,PageSubtitle,PageDescription,ComponentCategory,ComponentStatusStable,P2ConfirmContentAreYouSureToDeleteThisTask,P2OkTextOk,P2CancelTextCancel,P2TitleDeleteTheTask,P2HeaderCard,P2HeaderReport,P2HeaderMail,P2HeaderMobile,P2HeaderN1stItem,P2HeaderN2ndItem,P2HeaderN3rdItem,P2HeaderZhejiang,P2HeaderJiangsu,P2TextXihuDistrictHangzhou,P2TextN1,P2HeaderOption1,P2HeaderOption2,P2TextInputContent,P2HeaderOption1N1,P2HeaderOption2N1,P2HeaderOption2N2,P2HeaderBetween,P2HeaderExcept,P2PlaceholderTextMinimum,P2PlaceholderTextText,P2PlaceholderTextMaximum,P2HeaderSignUp,P2HeaderSignIn,P2PlaceholderTextEmail,P2HeaderTextN1,P2HeaderTextN2,P2PlaceholderTextSelectTime,P2PlaceholderTextSelectAddress,P2HeaderHangzhou,P2HeaderWestLake,P2HeaderLingyinShi,P2HeaderNanjing,P2HeaderZhongHuaMen,P2PlaceholderTextStartTime,P2SecondaryPlaceholderTextEndTime,P2PlaceholderTextPleaseSelect,P2HeaderParentN1,P2HeaderParentN1N0,P2HeaderLeaf1,P2HeaderLeaf2,P2HeaderParentN1N1,P2HeaderLeaf3,P2PlaceholderTextInputHere,P2PlaceholderTextAnotherInput,P2TextCenter,P2ContentPrimary,P2TextBlock,P2ContentButton,P2ContentLink,P2TextSpace,P2ContentClickToUpload,P2ContentConfirm,P2TextCardContent,P2ContentButtonN1,P2ContentButtonN2,P2ContentButtonN3,P2ContentButtonN4,P2ContentSubmit,P2ContentSearch,P2ContentSmall,P2ContentMiddle,P2ContentLarge,P2ContentCustom,P2ContentDefault,P2ContentDashed,P2ToolTipTipCopyGitUrl,P2ToolTipTipLike,P2ToolTipTipComment,P2ToolTipTipStar,P2ToolTipTipHeart,P2ToolTipTipShare,P2ToolTipTipDownload,P2ToolTipTipTooltip -AtomUIGallery.Localization.SpinShowCaseLangResourceKind|BasicUsageTitle,BasicUsageDescription,SizeTitle,SizeDescription,CustomIndicatorTitle,CustomIndicatorDescription,CustomizedDescriptionTitle,CustomizedDescriptionDescription,EmbeddedModeTitle,EmbeddedModeDescription,PageSubtitle,PageDescription,ComponentCategory,ComponentStatusStable,ScenarioExamples,P2DescriptionFurtherDetailsAboutTheContextOfThisAlert,P2TextLoadingState,P2TipLoading,P2MessageAlertMessageTitle +AtomUIGallery.Localization.SpinShowCaseLangResourceKind|BasicUsageTitle,BasicUsageDescription,SizeTitle,SizeDescription,CustomIndicatorTitle,CustomIndicatorDescription,CustomizedDescriptionTitle,CustomizedDescriptionDescription,EmbeddedModeTitle,EmbeddedModeDescription,SemanticPartStyleTitle,SemanticPartStyleDescription,SemanticRootDescription,SemanticContainerDescription,SemanticMaskDescription,SemanticSectionDescription,SemanticIndicatorDescription,SemanticDescriptionDescription,SemanticIndicatorRootDescription,SemanticIndicatorContentDescription,PageSubtitle,PageDescription,ComponentCategory,ComponentStatusStable,ScenarioExamples,P2DescriptionFurtherDetailsAboutTheContextOfThisAlert,P2TextLoadingState,P2TipLoading,P2MessageAlertMessageTitle AtomUIGallery.Localization.SplashShowCaseLangResourceKind|ScenarioExamples,ComponentCategory,ComponentStatusPreview,ComponentIntroducedVersion,PageSubtitle,PageDescription,BasicTitle,BasicDescription,DeterminateTitle,DeterminateDescription,StatusTitle,StatusDescription,ComposedTitle,ComposedDescription,WindowServiceTitle,WindowServiceDescription,P2SubtitleDesktopBoot,P2MessagePreparingShell,P2DetailLoadingThemeAndResources,P2MessageLoadingModules,P2DetailProgress,P2MessageReady,P2DetailReady,P2MessageError,P2DetailError,P2FooterStaticPreview,P2ContentModuleCore,P2ContentModuleTheme,P2ContentModuleGallery,P2FooterDesktopOnly,P2ContentShowWindowSplash,P2WindowSplashSubtitle,P2WindowSplashMessageStarting,P2WindowSplashDetailStarting,P2WindowSplashMessageLoadingTheme,P2WindowSplashMessageLoadingControls,P2WindowSplashMessageLoadingRoutes,P2WindowSplashMessageWarmingCache,P2WindowSplashMessageFinalizing,P2WindowSplashDetailProgress,P2WindowSplashMessageComplete,P2WindowSplashDetailComplete,P2WindowSplashFooter,ApiStaticShowAsync,ApiServiceShowAsync,ApiMethodSetProgress,ApiOptionMinimumShowDuration AtomUIGallery.Localization.SplitButtonShowCaseLangResourceKind|BasicTitle,BasicDescription,SizeTitle,SizeDescription,DangerButtonsTitle,DangerButtonsDescription,CustomIconTitle,CustomIconDescription,FlyoutTriggerTypeTitle,FlyoutTriggerTypeDescription,P2HeaderCut,P2HeaderCopy,P2HeaderDelete,P2ContentHoverMe,P2ContentLarge,P2ContentMiddle,P2ContentSmall,P2ContentCustom,P2ContentDefault,P2ContentPrimary,P2ContentClickMe,PageSubtitle,PageDescription,ComponentCategory,ComponentStatusStable,ScenarioExamples AtomUIGallery.Localization.SplitterShowCaseLangResourceKind|ScenarioExamples,ComponentCategory,ComponentStatusStable,PageSubtitle,PageDescription,BasicTitle,BasicDescription,HorizontalTitle,HorizontalDescription,CompositeTitle,CompositeDescription,ResizableDisabledTitle,ResizableDisabledDescription,ShowCollapsibleIconTitle,ShowCollapsibleIconDescription,MultiPanelsTitle,MultiPanelsDescription,DoubleClickResetTitle,DoubleClickResetDescription,LazyTitle,LazyDescription,LineStyleTitle,LineStyleDescription,P2TextFirst,P2TextSecond,P2TextTop,P2TextBottom,P2TextLeft,P2TextResizable,P2TextNotResizable,P2TextPanel1,P2TextPanel2,P2TextPanel3,P2TextShowcollapsibleicon,P2ContentHover,P2ContentTrue,P2ContentFalse,P2TextThird,P2TextA,P2TextB,P2TextC,P2TextD diff --git a/tests/AtomUIGallery.Tests/Localization/GalleryCatalogCoverageTests.cs b/tests/AtomUIGallery.Tests/Localization/GalleryCatalogCoverageTests.cs index 081ff9ec9..40f058eed 100644 --- a/tests/AtomUIGallery.Tests/Localization/GalleryCatalogCoverageTests.cs +++ b/tests/AtomUIGallery.Tests/Localization/GalleryCatalogCoverageTests.cs @@ -122,7 +122,7 @@ public class GalleryCatalogCoverageTests .ToArray(); sourceFiles.Length.ShouldBe(81); - sourceFiles.Sum(CountUnits).ShouldBe(4137); + sourceFiles.Sum(CountUnits).ShouldBe(4147); foreach (var sourcePath in sourceFiles) { var targetPath = Path.Combine( diff --git a/tests/AtomUIGallery.Tests/ShowCases/SpinShowCaseExamples.snapshot b/tests/AtomUIGallery.Tests/ShowCases/SpinShowCaseExamples.snapshot index 3012e9b9d..918e712c3 100644 --- a/tests/AtomUIGallery.Tests/ShowCases/SpinShowCaseExamples.snapshot +++ b/tests/AtomUIGallery.Tests/ShowCases/SpinShowCaseExamples.snapshot @@ -15,6 +15,36 @@ + + + + + + + + + + + + + + + + @@ -61,33 +91,3 @@ CustomIndicator="{antdicons:AntDesignIconProvider Kind=LoadingOutlined, StrokeBrush=#1677ff}" /> - - - - - - - - - - - - - - - - diff --git a/tests/AtomUIGallery.Tests/ShowCases/SpinShowCasePageTests.cs b/tests/AtomUIGallery.Tests/ShowCases/SpinShowCasePageTests.cs index d9e1e3fa1..35b7c60ef 100644 --- a/tests/AtomUIGallery.Tests/ShowCases/SpinShowCasePageTests.cs +++ b/tests/AtomUIGallery.Tests/ShowCases/SpinShowCasePageTests.cs @@ -1,5 +1,26 @@ +using System.Collections; +using System.Reflection; +using AtomUI.Controls; +using AtomUI.Desktop.Controls; +using AtomUI.Theme; +using AtomUI.Toolkits.GalleryBase.Controls; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Presenters; +using Avalonia.Media; +using Avalonia.Threading; +using Avalonia.VisualTree; +using ReactiveUI; using Shouldly; using Xunit; +using AvaloniaWindow = Avalonia.Controls.Window; +using AtomUISpin = AtomUI.Desktop.Controls.Spin; +using AtomUISpinIndicator = AtomUI.Desktop.Controls.SpinIndicator; +using AtomUITextBlock = AtomUI.Desktop.Controls.TextBlock; +using Ellipse = Avalonia.Controls.Shapes.Ellipse; +using SpinShowCase = AtomUIGallery.ShowCases.Spin.SpinShowCase; +using SpinViewModel = AtomUIGallery.ShowCases.Spin.SpinViewModel; namespace AtomUIGallery.Tests.ShowCases; @@ -23,7 +44,8 @@ public class SpinShowCasePageTests source.ShouldNotContain("Tag=\"Examples\""); source.ShouldNotContain("Tag=\"Api\""); source.ShouldNotContain("Tag=\"DesignToken\""); - source.ShouldContain(""); @@ -39,9 +61,9 @@ public class SpinShowCasePageTests source.ShouldContain("Gallery<"); } + [Fact] + public void Spin_ShowCase_Declares_The_Semantic_Previews_And_Style_Example() + { + var source = ReadRepoFile( + "controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Views/SpinShowCase.axaml"); + var english = ReadRepoFile( + "controlgallery/AtomUIGallery/ShowCases/Feedback/Spin/Localization/en-US.xlf"); + var semanticSource = ExtractShowCaseItem(source, "spin-semantic-part"); + + source.ShouldContain(""); + source.ShouldContain("Name=\"SpinSemanticPreview\""); + source.ShouldContain("SemanticOwner=\"{Binding #SpinSemanticOwner}\""); + source.ShouldContain("SemanticOwnerType=\"{x:Type atom:Spin}\""); + source.ShouldContain("Name=\"SpinIndicatorSemanticPreview\""); + source.ShouldContain("SemanticOwner=\"{Binding #SpinIndicatorSemanticOwner}\""); + source.ShouldContain("SemanticOwnerType=\"{x:Type atom:SpinIndicator}\""); + CountOccurrences(source, " + { + page.GetVisualDescendants().OfType().ShouldBeEmpty(); + + var host = page.GetVisualDescendants().OfType().Single(); + host.SelectedTab = GalleryShowCaseTab.SemanticParts; + for (var i = 0; i < 3; i++) + { + Dispatcher.UIThread.RunJobs(); + } + + var activeContentValue = typeof(GalleryShowCaseHost) + .GetProperty("ActiveContent", BindingFlags.Instance | BindingFlags.NonPublic) + ?.GetValue(host); + activeContentValue.ShouldNotBeNull(); + var activeContent = activeContentValue.ShouldBeAssignableTo(); + activeContent.GetVisualDescendants().OfType().Count().ShouldBe(2); + var semanticSpin = activeContent.GetVisualDescendants() + .OfType() + .Single(static spin => spin.Name == "SpinSemanticOwner"); + semanticSpin.Bounds.Width.ShouldBeGreaterThan(300); + semanticSpin.GetVisualDescendants() + .OfType() + .Single(static indicator => indicator.Classes.Contains("semantic-indicator")) + .IsEffectivelyVisible.ShouldBeTrue(); + var mask = semanticSpin.GetVisualDescendants() + .OfType() + .Single(static border => border.Classes.Contains("semantic-mask")); + mask.IsEffectivelyVisible.ShouldBeTrue(); + var container = semanticSpin.GetVisualDescendants() + .OfType() + .Single(static presenter => presenter.Classes.Contains("semantic-container")); + var section = semanticSpin.GetVisualDescendants() + .OfType() + .Single(static panel => panel.Classes.Contains("semantic-section")); + section.IsEffectivelyVisible.ShouldBeTrue(); + var description = semanticSpin.GetVisualDescendants() + .OfType() + .Single(static text => text.Classes.Contains("semantic-description")); + description.IsEffectivelyVisible.ShouldBeTrue(); + + var maskRect = GetBoundsInOwner(mask, semanticSpin); + var containerRect = GetBoundsInOwner(container, semanticSpin); + maskRect.Contains(containerRect).ShouldBeTrue( + $"mask {maskRect} should cover container {containerRect}"); + var sectionRect = GetBoundsInOwner(section, semanticSpin); + maskRect.Contains(sectionRect).ShouldBeTrue( + $"mask {maskRect} should cover section {sectionRect}"); + var descriptionRect = GetBoundsInOwner(description, semanticSpin); + maskRect.Contains(descriptionRect).ShouldBeTrue( + $"mask {maskRect} should cover description {descriptionRect}"); + + var semanticPreview = activeContent.GetVisualDescendants() + .OfType() + .Single(static preview => preview.Name == "SpinSemanticPreview"); + var previewItemsValue = typeof(SemanticPartPreview) + .GetProperty("Items", BindingFlags.Instance | BindingFlags.NonPublic) + ?.GetValue(semanticPreview); + previewItemsValue.ShouldNotBeNull(); + var previewItems = previewItemsValue.ShouldBeAssignableTo() + .Cast() + .ToArray(); + previewItems.Length.ShouldBe(6); + var maskItem = previewItems.Single(item => + { + var path = item.GetType().GetProperty("Path")?.GetValue(item) as string; + return path == "mask"; + }); + typeof(SemanticPartPreview) + .GetMethod("SetHoveredPart", BindingFlags.Instance | BindingFlags.NonPublic) + .ShouldNotBeNull() + .Invoke(semanticPreview, [maskItem, true]); + Dispatcher.UIThread.RunJobs(); + + var maskAdorners = AdornerLayer.GetAdornerLayer(mask) + .ShouldNotBeNull() + .Children + .Where(static child => child.GetType().Name == "SemanticPartAdorner") + .Select(AdornerLayer.GetAdornedElement) + .ToArray(); + maskAdorners.ShouldHaveSingleItem().ShouldBeSameAs(mask); + + var indicatorPreview = activeContent.GetVisualDescendants() + .OfType() + .Single(static preview => preview.Name == "SpinIndicatorSemanticPreview"); + var indicatorOwner = indicatorPreview.SemanticOwner.ShouldBeOfType(); + indicatorOwner.GetVisualDescendants() + .OfType() + .Where(static control => control.Classes.Contains("semantic-content")) + .Count().ShouldBe(2); + + host.SelectedTab = GalleryShowCaseTab.Examples; + Dispatcher.UIThread.RunJobs(); + page.GetVisualDescendants().OfType().ShouldBeEmpty(); + }); + } + + [Fact] + public void Spin_Semantic_Style_Example_Applies_The_Official_Style_Values() + { + AvaloniaTestApp.EnsureInitialized(); + + var page = new SpinShowCase + { + DataContext = new SpinViewModel(new TestScreen()) + }; + + ShowInWindow(page, 1280, 1400, () => + { + var panel = page.GetVisualDescendants().OfType().Single(); + var item = panel.Children + .OfType() + .Single(static candidate => candidate.SourceKey == "spin-semantic-part"); + item.BadgeText.ShouldBe(GalleryVersionInfo.DisplayVersion); + item.MaterializeDeferredContent(); + Dispatcher.UIThread.RunJobs(); + + var styleDemo = page.GetVisualDescendants() + .OfType() + .Single(static spin => spin.Classes.Contains("semantic-style-demo")); + AssertIndicatorDotFill(styleDemo, "#00d4ff"); + + var functionDemo = page.GetVisualDescendants() + .OfType() + .Single(static spin => spin.Classes.Contains("semantic-function-demo")); + functionDemo.SizeType.ShouldBe(CustomizableSizeType.Small); + AssertIndicatorDotFill(functionDemo, "#722ed1"); + }); + } + [Fact] public void Spin_ShowCase_Examples_Match_Approved_Control_Demo_Content() { @@ -62,16 +271,32 @@ public class SpinShowCasePageTests private static string ExtractSpinExampleItems(string source) { - const string firstItemMarker = "() + .Single(static candidate => candidate.Classes.Contains("semantic-indicator")); + var dots = indicator.GetVisualDescendants() + .OfType() + .Where(static dot => dot.Classes.Contains("SpinIndicatorDot")) + .ToArray(); + dots.Length.ShouldBe(4); + foreach (var dot in dots) + { + dot.Fill.ShouldNotBeNull() + .ShouldBeAssignableTo() + .Color.ShouldBe(expectedColor); + } + } + private static string ReadRepoFile(string relativePath) { var path = GetRepoFile(relativePath); @@ -119,4 +369,35 @@ public class SpinShowCasePageTests return Path.Combine(AppContext.BaseDirectory, relativePath); } + + private static void ShowInWindow(Control content, double width, double height, Action assertion) + { + var visualLayerManager = new VisualLayerManager + { + EnableAdornerLayer = true, + Child = content + }; + var window = new AvaloniaWindow + { + Content = visualLayerManager, + Width = width, + Height = height + }; + try + { + window.Show(); + Dispatcher.UIThread.RunJobs(); + assertion(); + } + finally + { + window.Close(); + Dispatcher.UIThread.RunJobs(); + } + } + + private sealed class TestScreen : IScreen + { + public RoutingState Router { get; } = new(); + } }