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