diff --git a/docfx/articles/dock-theme-design-tokens.md b/docfx/articles/dock-theme-design-tokens.md
new file mode 100644
index 000000000..ca35128df
--- /dev/null
+++ b/docfx/articles/dock-theme-design-tokens.md
@@ -0,0 +1,198 @@
+# Dock Theme Design Tokens
+
+Dock themes expose semantic design tokens that decouple control templates from raw color/size keys.
+
+All token keys below are available in both:
+
+- `DockFluentTheme`
+- `DockSimpleTheme`
+
+## Surface tokens
+
+| Token | Purpose |
+|---|---|
+| `DockSurfaceWorkbenchBrush` | Main background surface behind docks. |
+| `DockSurfaceSidebarBrush` | Sidebar/tool-area surface. |
+| `DockSurfaceEditorBrush` | Document/editor surface. |
+| `DockSurfacePanelBrush` | Secondary panel surface. |
+| `DockSurfaceHeaderBrush` | Header/chrome background. |
+| `DockSurfaceHeaderActiveBrush` | Active header/chrome background. |
+
+## Border and structure tokens
+
+| Token | Purpose |
+|---|---|
+| `DockBorderSubtleBrush` | Standard low-emphasis border color. |
+| `DockBorderStrongBrush` | Stronger border color (optional use). |
+| `DockSeparatorBrush` | Thin separators and strip dividers. |
+| `DockSplitterIdleBrush` | Splitter idle color. |
+| `DockSplitterHoverBrush` | Splitter hover color. |
+| `DockSplitterDragBrush` | Splitter drag color. |
+| `DockTargetIndicatorBrush` | Dock target overlay indicator color. |
+
+## Tab tokens
+
+| Token | Purpose |
+|---|---|
+| `DockTabBackgroundBrush` | Base tab strip background. |
+| `DockTabHoverBackgroundBrush` | Tab hover background. |
+| `DockTabActiveBackgroundBrush` | Active/selected tab background. |
+| `DockTabActiveIndicatorBrush` | Active tab indicator line/accent. |
+| `DockTabForegroundBrush` | Default tab text/icon color. |
+| `DockTabSelectedForegroundBrush` | Selected tab text color (non-active accent). |
+| `DockTabActiveForegroundBrush` | Active tab text/icon color. |
+| `DockTabCloseHoverBackgroundBrush` | Document close button hover background. |
+
+## Chrome button tokens
+
+| Token | Purpose |
+|---|---|
+| `DockChromeButtonForegroundBrush` | Tool/MDI chrome icon color. |
+| `DockChromeButtonHoverBackgroundBrush` | Chrome button hover background. |
+| `DockChromeButtonPressedBackgroundBrush` | Chrome button pressed background. |
+| `DockChromeButtonDangerHoverBrush` | Close/danger button hover background. |
+
+## Global shape/density tokens
+
+| Token | Purpose |
+|---|---|
+| `DockCornerRadiusSmall` | Shared small corner radius. |
+| `DockHeaderHeight` | Header/chrome nominal height token. |
+| `DockTabHeight` | Tab height token. |
+| `DockTabHorizontalPadding` | Horizontal tab spacing token. |
+| `DockIconSizeSmall` | Small icon size token. |
+| `DockIconSizeNormal` | Normal icon size token. |
+
+## Control density tokens
+
+| Token | Purpose |
+|---|---|
+| `DockTabItemMinHeight` | Min height for document/tool tab items. |
+| `DockToolTabItemMinHeight` | Min height for tool tab items. |
+| `DockDocumentTabItemPadding` | Document tab item padding. |
+| `DockToolTabItemPadding` | Tool tab item padding. |
+| `DockToolTabItemSelectedPadding` | Tool tab selected-state padding. |
+| `DockCreateButtonWidth` | Document create button width. |
+| `DockCreateButtonHeight` | Document create button height. |
+| `DockCloseButtonSize` | Document close button size. |
+| `DockChromeButtonWidth` | Tool/MDI chrome button width. |
+| `DockChromeButtonHeight` | Tool/MDI chrome button height. |
+| `DockChromeButtonPadding` | Tool/MDI chrome button padding. |
+| `DockChromeButtonMargin` | Tool/MDI chrome button margin. |
+| `DockMdiTitleIconSize` | MDI title bar icon viewbox size. |
+| `DockMdiHeaderDragPadding` | MDI drag header padding. |
+
+## Tab and header content metrics
+
+| Token | Purpose |
+|---|---|
+| `DockHeaderContentPadding` | Shared title/header text padding in document/tool/MDI headers. |
+| `DockModifiedIndicatorMargin` | Shared margin for modified marker (`*`). |
+| `DockTabContentSpacing` | Spacing between tab content blocks (icon, title, modified, close). |
+| `DockTabContentMargin` | Inner margin around tab content stack. |
+| `DockCreateButtonIconMargin` | Plus/create icon margin in document tab strip. |
+
+## Tool chrome and MDI metrics
+
+| Token | Purpose |
+|---|---|
+| `DockToolChromeHeaderMargin` | Header dock panel outer margin in tool chrome. |
+| `DockToolChromeTitleMargin` | Tool title text margin. |
+| `DockToolChromeMenuIconMargin` | Menu icon margin in tool chrome button. |
+| `DockToolChromeDividerThickness` | Divider line thickness below tool chrome header. |
+| `DockChromeGripHeight` | Drag grip strip height in tool/MDI chrome. |
+| `DockChromeGripMargin` | Drag grip strip margin in tool/MDI chrome. |
+| `DockChromeGripBrush` | Shared tiled grip brush used by tool/MDI chrome. |
+| `DockMdiHeaderColumnSpacing` | Column spacing in MDI document header layout. |
+| `DockMdiButtonStripSpacing` | Spacing between MDI title bar buttons. |
+| `DockMdiButtonStripMargin` | MDI button strip margin. |
+| `DockMdiResizeEdgeThickness` | Hit target thickness for MDI edge resize handles. |
+| `DockMdiResizeCornerSize` | Hit target size for MDI corner resize handles. |
+
+## Selector and target metrics
+
+| Token | Purpose |
+|---|---|
+| `DockTargetSelectorSize` | Dock target selector image size. |
+| `DockTargetSelectorGridMaxSize` | Max size of local dock target selector grid. |
+| `DockSelectorOverlayBackdropBrush` | Backdrop brush for selector overlay modal layer. |
+| `DockSelectorOverlayCornerRadius` | Corner radius for selector overlay container. |
+| `DockSelectorOverlayPadding` | Selector overlay container padding. |
+| `DockSelectorOverlayMinWidth` | Selector overlay minimum width. |
+| `DockSelectorOverlayMaxWidth` | Selector overlay maximum width. |
+| `DockSelectorOverlaySpacing` | Vertical spacing in selector overlay content. |
+| `DockSelectorOverlayListMinHeight` | Minimum list height in selector overlay. |
+| `DockSelectorOverlayListMaxHeight` | Maximum list height in selector overlay. |
+| `DockSelectorOverlayItemPadding` | List item padding in selector overlay. |
+| `DockSelectorOverlayItemCornerRadius` | List item corner radius in selector overlay. |
+| `DockSelectorOverlayBadgeSpacing` | Spacing between state badges. |
+| `DockSelectorOverlayBadgeMargin` | Badge strip margin. |
+| `DockSelectorOverlayBadgeCornerRadius` | Badge corner radius. |
+| `DockSelectorOverlayBadgePadding` | Badge padding. |
+| `DockSelectorOverlayBadgeFontSize` | Badge text size. |
+
+## Command bar and drag preview metrics
+
+| Token | Purpose |
+|---|---|
+| `DockCommandBarPadding` | Command bar container padding. |
+| `DockCommandBarSpacing` | Vertical spacing between menu/tool/ribbon bands. |
+| `DockDragPreviewCornerRadius` | Drag preview card corner radius. |
+| `DockDragPreviewHeaderPadding` | Drag preview header padding. |
+| `DockDragPreviewHeaderSpacing` | Header spacing between title and status block. |
+| `DockDragPreviewStatusSpacing` | Spacing between drag status icon and text. |
+| `DockDragPreviewStatusIconSize` | Drag status icon size. |
+| `DockHostTitleBarMouseTrackerHeight` | Host title bar top tracker panel height. |
+
+## Overlay and dialog metrics
+
+| Token | Purpose |
+|---|---|
+| `DockOverlayReloadButtonMargin` | Margin for the reload command button in busy overlay. |
+| `DockOverlayCardCornerRadius` | Busy overlay card corner radius. |
+| `DockOverlayCardPadding` | Busy overlay card padding. |
+| `DockOverlayCardSpacing` | Busy overlay card content spacing. |
+| `DockOverlayMessageFontSize` | Shared overlay message/title font size. |
+| `DockOverlayProgressWidth` | Busy overlay progress width. |
+| `DockOverlayProgressHeight` | Busy overlay progress height. |
+| `DockDialogCornerRadius` | Dialog shell corner radius. |
+| `DockDialogPadding` | Dialog shell padding. |
+| `DockDialogMinWidth` | Dialog shell minimum width. |
+| `DockDialogMaxWidth` | Dialog shell maximum width. |
+| `DockDialogSpacing` | Dialog shell grid row/column spacing. |
+| `DockDialogTitleFontSize` | Dialog and confirmation title font size. |
+| `DockDialogCloseButtonSize` | Dialog shell close button size. |
+| `DockConfirmationDialogPadding` | Confirmation dialog padding. |
+| `DockConfirmationDialogMaxWidth` | Confirmation dialog maximum width. |
+| `DockConfirmationDialogStackSpacing` | Confirmation dialog content spacing. |
+| `DockConfirmationDialogActionsSpacing` | Confirmation dialog actions row spacing. |
+
+## Where defaults are defined
+
+- Fluent defaults: `src/Dock.Avalonia.Themes.Fluent/Accents/Fluent.axaml`
+- Simple defaults: `src/Dock.Avalonia.Themes.Simple/Accents/Simple.axaml`
+- Compact density overrides:
+ - `src/Dock.Avalonia.Themes.Fluent/DensityStyles/Compact.axaml`
+ - `src/Dock.Avalonia.Themes.Simple/DensityStyles/Compact.axaml`
+
+## Token override pattern
+
+```xaml
+
+
+
+
+
+
+ 22
+
+
+
+
+```
+
+## Related docs
+
+- [Dock Fluent Theme](dock-theme-fluent.md)
+- [Dock Simple Theme](dock-theme-simple.md)
+- [Dock Theme Token Migration](dock-theme-token-migration.md)
diff --git a/docfx/articles/dock-theme-fluent.md b/docfx/articles/dock-theme-fluent.md
new file mode 100644
index 000000000..e091091ee
--- /dev/null
+++ b/docfx/articles/dock-theme-fluent.md
@@ -0,0 +1,114 @@
+# Dock Fluent Theme
+
+This guide documents the Dock Fluent theme in detail: structure, density modes, token customization, and IDE preset integration.
+
+## Overview
+
+`DockFluentTheme` is the primary Dock theme for Fluent-based applications.
+
+```xaml
+
+
+
+
+```
+
+Theme implementation:
+
+- `src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml`
+- `src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml.cs`
+- `src/Dock.Avalonia.Themes.Fluent/Accents/Fluent.axaml`
+
+## What DockFluentTheme includes
+
+`DockFluentTheme` merges:
+
+- accent and string resources,
+- window chrome resources,
+- all Dock control templates (documents, tools, MDI, overlays, host windows).
+
+Key control template resources include:
+
+- `DocumentControl.axaml`
+- `DocumentTabStrip.axaml`
+- `DocumentTabStripItem.axaml`
+- `ToolChromeControl.axaml`
+- `ToolTabStrip.axaml`
+- `ToolTabStripItem.axaml`
+- `MdiDocumentWindow.axaml`
+
+## Density support
+
+`DockFluentTheme` supports two density modes:
+
+- `DensityStyle="Normal"` (default),
+- `DensityStyle="Compact"`.
+
+```xaml
+
+```
+
+Compact density resource dictionary:
+
+- `avares://Dock.Avalonia.Themes.Fluent/DensityStyles/Compact.axaml`
+
+Compact mode reduces tab/button/icon metrics by overriding density tokens (for example `DockTabItemMinHeight`, `DockCloseButtonSize`, `DockChromeButtonWidth`).
+
+## Fluent token customization
+
+Customize Dock Fluent visuals by overriding semantic tokens after `DockFluentTheme`.
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+For the complete token list, see [Dock Theme Design Tokens](dock-theme-design-tokens.md).
+
+## IDE preset usage with Fluent
+
+Fluent-specific preset dictionaries:
+
+- `avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/Default.axaml`
+- `avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeDark.axaml`
+- `avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeLight.axaml`
+- `avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderLight.axaml`
+- `avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderDark.axaml`
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Presets override color/surface tokens only; density remains controlled by `DensityStyle`.
+
+## Recommended customization order
+
+1. Apply `DockFluentTheme`.
+2. Pick density (`Normal` or `Compact`).
+3. Optionally merge an IDE preset.
+4. Add app-specific token overrides last.
+
+## Related docs
+
+- [Dock Theme Design Tokens](dock-theme-design-tokens.md)
+- [Dock IDE Presets](dock-theme-ide-presets.md)
+- [Dock Theme Token Migration](dock-theme-token-migration.md)
diff --git a/docfx/articles/dock-theme-ide-presets.md b/docfx/articles/dock-theme-ide-presets.md
new file mode 100644
index 000000000..5525a6026
--- /dev/null
+++ b/docfx/articles/dock-theme-ide-presets.md
@@ -0,0 +1,164 @@
+# Dock IDE Presets
+
+Dock ships token-only IDE presets for Fluent and Simple themes.
+They override semantic tokens only and do not replace Dock control templates.
+
+## Preset files
+
+Fluent:
+- `avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/Default.axaml`
+- `avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeDark.axaml`
+- `avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeLight.axaml`
+- `avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderLight.axaml`
+- `avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderDark.axaml`
+
+Simple:
+- `avares://Dock.Avalonia.Themes.Simple/Presets/Ide/Default.axaml`
+- `avares://Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeDark.axaml`
+- `avares://Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeLight.axaml`
+- `avares://Dock.Avalonia.Themes.Simple/Presets/Ide/RiderLight.axaml`
+- `avares://Dock.Avalonia.Themes.Simple/Presets/Ide/RiderDark.axaml`
+
+## Usage
+
+### Fluent + Default
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Fluent + VS Code Dark
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Fluent + Rider Light
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Fluent + VS Code Light
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Fluent + Rider Dark
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Simple + Default
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Simple + VS Code Dark
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Simple + Rider Dark
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Customization workflow
+
+1. Start from one preset.
+2. Copy it into your app.
+3. Adjust only token values you need.
+4. Keep your dictionary after the Dock theme include so overrides win.
+
+For the full token list, see [Dock Theme Semantic Tokens](dock-theme-semantic-tokens.md).
+
+IDE presets intentionally override color/surface tokens only.
+Density continues to come from `DockFluentTheme.DensityStyle` and `DockSimpleTheme.DensityStyle`.
+
+Use `Default.axaml` when you want preset-driven runtime switching but need to return to the base Dock theme token mapping.
diff --git a/docfx/articles/dock-theme-semantic-tokens.md b/docfx/articles/dock-theme-semantic-tokens.md
new file mode 100644
index 000000000..739b6230c
--- /dev/null
+++ b/docfx/articles/dock-theme-semantic-tokens.md
@@ -0,0 +1,63 @@
+# Dock Theme Semantic Tokens
+
+Dock now exposes a semantic token contract on top of existing Fluent/Simple resources.
+
+Use these tokens to style Dock surfaces/chrome without copying control templates.
+
+For the full detailed reference, see [Dock Theme Design Tokens](dock-theme-design-tokens.md).
+For migration from legacy keys, see [Dock Theme Token Migration](dock-theme-token-migration.md).
+
+## Token contract
+
+Both built-in themes expose the same keys:
+
+| Group | Keys |
+|---|---|
+| Surfaces | `DockSurfaceWorkbenchBrush`, `DockSurfaceSidebarBrush`, `DockSurfaceEditorBrush`, `DockSurfacePanelBrush`, `DockSurfaceHeaderBrush`, `DockSurfaceHeaderActiveBrush` |
+| Borders/structure | `DockBorderSubtleBrush`, `DockBorderStrongBrush`, `DockSeparatorBrush`, `DockSplitterIdleBrush`, `DockSplitterHoverBrush`, `DockSplitterDragBrush`, `DockTargetIndicatorBrush` |
+| Tabs | `DockTabBackgroundBrush`, `DockTabHoverBackgroundBrush`, `DockTabActiveBackgroundBrush`, `DockTabActiveIndicatorBrush`, `DockTabForegroundBrush`, `DockTabSelectedForegroundBrush`, `DockTabActiveForegroundBrush`, `DockTabCloseHoverBackgroundBrush` |
+| Chrome buttons | `DockChromeButtonForegroundBrush`, `DockChromeButtonHoverBackgroundBrush`, `DockChromeButtonPressedBackgroundBrush`, `DockChromeButtonDangerHoverBrush` |
+| Density/shape | `DockCornerRadiusSmall`, `DockHeaderHeight`, `DockTabHeight`, `DockTabHorizontalPadding`, `DockIconSizeSmall`, `DockIconSizeNormal` |
+| Density controls | `DockTabItemMinHeight`, `DockToolTabItemMinHeight`, `DockDocumentTabItemPadding`, `DockToolTabItemPadding`, `DockToolTabItemSelectedPadding`, `DockCreateButtonWidth`, `DockCreateButtonHeight`, `DockCloseButtonSize`, `DockChromeButtonWidth`, `DockChromeButtonHeight`, `DockChromeButtonPadding`, `DockChromeButtonMargin`, `DockMdiTitleIconSize`, `DockMdiHeaderDragPadding` |
+| Tab/header metrics | `DockHeaderContentPadding`, `DockModifiedIndicatorMargin`, `DockTabContentSpacing`, `DockTabContentMargin`, `DockCreateButtonIconMargin` |
+| Chrome/MDI metrics | `DockToolChromeHeaderMargin`, `DockToolChromeTitleMargin`, `DockToolChromeMenuIconMargin`, `DockToolChromeDividerThickness`, `DockChromeGripHeight`, `DockChromeGripMargin`, `DockChromeGripBrush`, `DockMdiHeaderColumnSpacing`, `DockMdiButtonStripSpacing`, `DockMdiButtonStripMargin`, `DockMdiResizeEdgeThickness`, `DockMdiResizeCornerSize` |
+| Selector/target metrics | `DockTargetSelectorSize`, `DockTargetSelectorGridMaxSize`, `DockSelectorOverlayBackdropBrush`, `DockSelectorOverlayCornerRadius`, `DockSelectorOverlayPadding`, `DockSelectorOverlayMinWidth`, `DockSelectorOverlayMaxWidth`, `DockSelectorOverlaySpacing`, `DockSelectorOverlayListMinHeight`, `DockSelectorOverlayListMaxHeight`, `DockSelectorOverlayItemPadding`, `DockSelectorOverlayItemCornerRadius`, `DockSelectorOverlayBadgeSpacing`, `DockSelectorOverlayBadgeMargin`, `DockSelectorOverlayBadgeCornerRadius`, `DockSelectorOverlayBadgePadding`, `DockSelectorOverlayBadgeFontSize` |
+| Command/preview metrics | `DockCommandBarPadding`, `DockCommandBarSpacing`, `DockDragPreviewCornerRadius`, `DockDragPreviewHeaderPadding`, `DockDragPreviewHeaderSpacing`, `DockDragPreviewStatusSpacing`, `DockDragPreviewStatusIconSize`, `DockHostTitleBarMouseTrackerHeight` |
+| Overlay/dialog metrics | `DockOverlayReloadButtonMargin`, `DockOverlayCardCornerRadius`, `DockOverlayCardPadding`, `DockOverlayCardSpacing`, `DockOverlayMessageFontSize`, `DockOverlayProgressWidth`, `DockOverlayProgressHeight`, `DockDialogCornerRadius`, `DockDialogPadding`, `DockDialogMinWidth`, `DockDialogMaxWidth`, `DockDialogSpacing`, `DockDialogTitleFontSize`, `DockDialogCloseButtonSize`, `DockConfirmationDialogPadding`, `DockConfirmationDialogMaxWidth`, `DockConfirmationDialogStackSpacing`, `DockConfirmationDialogActionsSpacing` |
+
+## Default mapping
+
+Defaults are defined in:
+
+- `src/Dock.Avalonia.Themes.Fluent/Accents/Fluent.axaml`
+- `src/Dock.Avalonia.Themes.Simple/Accents/Simple.axaml`
+
+These defaults map to existing Dock theme resources so current apps keep their prior look unless you override tokens.
+
+## Override example
+
+```xaml
+
+
+
+
+ 24
+
+```
+
+Merge this dictionary after `DockFluentTheme` or `DockSimpleTheme`.
+
+## Compact density mode
+
+Both Dock theme classes expose a `DensityStyle` property:
+
+```xaml
+
+
+```
+
+Compact mode loads:
+
+- `avares://Dock.Avalonia.Themes.Fluent/DensityStyles/Compact.axaml`
+- `avares://Dock.Avalonia.Themes.Simple/DensityStyles/Compact.axaml`
diff --git a/docfx/articles/dock-theme-simple.md b/docfx/articles/dock-theme-simple.md
new file mode 100644
index 000000000..b5ae9a5c2
--- /dev/null
+++ b/docfx/articles/dock-theme-simple.md
@@ -0,0 +1,106 @@
+# Dock Simple Theme
+
+This guide documents the Dock Simple theme in detail, including architecture, density support, and token/preset customization.
+
+## Overview
+
+`DockSimpleTheme` is the Dock theme for applications using Avalonia `SimpleTheme`.
+
+```xaml
+
+
+
+
+```
+
+Theme implementation:
+
+- `src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml`
+- `src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml.cs`
+- `src/Dock.Avalonia.Themes.Simple/Accents/Simple.axaml`
+
+## Architecture notes
+
+Dock Simple uses its own accent resources but shares Dock control template XAML with Fluent.
+
+This is implemented by linking Fluent control `.axaml` files into the Simple theme assembly:
+
+- `src/Dock.Avalonia.Themes.Simple/Dock.Avalonia.Themes.Simple.csproj`
+
+Practical result:
+
+- same control template behavior,
+- same semantic token contract,
+- different default accent mappings.
+
+## Density support
+
+`DockSimpleTheme` supports:
+
+- `DensityStyle="Normal"` (default),
+- `DensityStyle="Compact"`.
+
+```xaml
+
+```
+
+Compact density resource dictionary:
+
+- `avares://Dock.Avalonia.Themes.Simple/DensityStyles/Compact.axaml`
+
+## Simple token customization
+
+Override semantic tokens after `DockSimpleTheme`:
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+For the complete token list, see [Dock Theme Design Tokens](dock-theme-design-tokens.md).
+
+## IDE preset usage with Simple
+
+Simple-specific preset dictionaries:
+
+- `avares://Dock.Avalonia.Themes.Simple/Presets/Ide/Default.axaml`
+- `avares://Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeDark.axaml`
+- `avares://Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeLight.axaml`
+- `avares://Dock.Avalonia.Themes.Simple/Presets/Ide/RiderLight.axaml`
+- `avares://Dock.Avalonia.Themes.Simple/Presets/Ide/RiderDark.axaml`
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Recommended customization order
+
+1. Apply `DockSimpleTheme`.
+2. Choose density.
+3. Optionally merge an IDE preset.
+4. Add app-specific token overrides at highest priority.
+
+## Related docs
+
+- [Dock Theme Design Tokens](dock-theme-design-tokens.md)
+- [Dock IDE Presets](dock-theme-ide-presets.md)
+- [Dock Theme Token Migration](dock-theme-token-migration.md)
diff --git a/docfx/articles/dock-theme-token-migration.md b/docfx/articles/dock-theme-token-migration.md
new file mode 100644
index 000000000..62d06c1fa
--- /dev/null
+++ b/docfx/articles/dock-theme-token-migration.md
@@ -0,0 +1,117 @@
+# Dock Theme Token Migration
+
+This guide helps migrate from older Dock token usage (legacy theme keys and hardcoded metrics) to the current semantic token model.
+
+## Why migrate
+
+Semantic tokens provide:
+
+- stable customization points across Fluent and Simple,
+- better compatibility with IDE presets,
+- density support (`Normal`/`Compact`) without copying templates.
+
+## Migration strategy
+
+1. Keep existing themes (`DockFluentTheme` or `DockSimpleTheme`).
+2. Move custom overrides from legacy keys to semantic keys.
+3. Replace hardcoded metric overrides with density tokens.
+4. Use `DensityStyle="Compact"` when compact layout is needed.
+
+## Legacy to semantic token mapping
+
+| Legacy key | Replace with | Notes |
+|---|---|---|
+| `DockThemeBackgroundBrush` | `DockSurfaceWorkbenchBrush`, `DockSurfaceSidebarBrush`, `DockSurfacePanelBrush`, `DockTabActiveBackgroundBrush` | Split by usage intent. |
+| `DockThemeControlBackgroundBrush` | `DockSurfaceEditorBrush`, `DockTabHoverBackgroundBrush`, `DockChromeButtonHoverBackgroundBrush` | Separate editor and hover/chrome concerns. |
+| `DockThemeBorderLowBrush` | `DockBorderSubtleBrush`, `DockSeparatorBrush`, `DockSplitterIdleBrush` | Use `DockBorderStrongBrush` for stronger borders when needed. |
+| `DockThemeAccentBrush` | `DockSurfaceHeaderActiveBrush` | Active header/chrome background token. |
+| `DockThemeForegroundBrush` | `DockTabForegroundBrush` | Base tab/chrome foreground role. |
+| `DockApplicationAccentBrushLow` | `DockTabActiveBackgroundBrush`, `DockTabActiveIndicatorBrush`, `DockSplitterHoverBrush` | Token choice depends on template area. |
+| `DockApplicationAccentBrushMed` | `DockTabHoverBackgroundBrush`, `DockTabSelectedForegroundBrush`, `DockSplitterDragBrush` | Separate hover, selected text, and drag indicators. |
+| `DockApplicationAccentBrushHigh` | `DockTabCloseHoverBackgroundBrush`, `DockChromeButtonDangerHoverBrush` | Close/danger hover states. |
+| `DockApplicationAccentForegroundBrush` | `DockTabActiveForegroundBrush` | Active text/icon foreground. |
+| `DockApplicationAccentBrushIndicator` | `DockTargetIndicatorBrush` | Dock-target overlay indicator. |
+| `DockToolChromeIconBrush` | `DockChromeButtonForegroundBrush` | Tool/MDI chrome icon foreground. |
+
+## Hardcoded metric migration
+
+Older template customizations often hardcoded values like `Width="14"`, `Height="24"`, `Padding="4,0,4,0"`.
+
+Use tokens instead:
+
+| Hardcoded usage | Semantic metric token |
+|---|---|
+| Document close button size | `DockCloseButtonSize` |
+| Create document button size | `DockCreateButtonWidth`, `DockCreateButtonHeight` |
+| Document tab min height | `DockTabItemMinHeight` |
+| Tool tab min height | `DockToolTabItemMinHeight` |
+| Document tab padding | `DockDocumentTabItemPadding` |
+| Tool tab padding | `DockToolTabItemPadding`, `DockToolTabItemSelectedPadding` |
+| Tool/MDI chrome button size | `DockChromeButtonWidth`, `DockChromeButtonHeight` |
+| Tool/MDI chrome button spacing | `DockChromeButtonPadding`, `DockChromeButtonMargin` |
+| MDI title icon sizing | `DockMdiTitleIconSize` |
+| MDI drag header padding | `DockMdiHeaderDragPadding` |
+| Header text padding | `DockHeaderContentPadding` |
+| Modified marker spacing | `DockModifiedIndicatorMargin` |
+| Tab content spacing/margins | `DockTabContentSpacing`, `DockTabContentMargin` |
+| Create icon margin | `DockCreateButtonIconMargin` |
+| Tool chrome title/header spacing | `DockToolChromeHeaderMargin`, `DockToolChromeTitleMargin`, `DockToolChromeMenuIconMargin` |
+| Tool divider thickness | `DockToolChromeDividerThickness` |
+| Shared chrome grip visuals | `DockChromeGripHeight`, `DockChromeGripMargin`, `DockChromeGripBrush` |
+| MDI header/buttons layout spacing | `DockMdiHeaderColumnSpacing`, `DockMdiButtonStripSpacing`, `DockMdiButtonStripMargin` |
+| MDI resize handle hit targets | `DockMdiResizeEdgeThickness`, `DockMdiResizeCornerSize` |
+| Dock target selector sizing | `DockTargetSelectorSize`, `DockTargetSelectorGridMaxSize` |
+| Selector overlay sizing/spacing/badges | `DockSelectorOverlay*` token family |
+| Command bar layout spacing | `DockCommandBarPadding`, `DockCommandBarSpacing` |
+| Drag preview layout sizing | `DockDragPreviewCornerRadius`, `DockDragPreviewHeaderPadding`, `DockDragPreviewHeaderSpacing`, `DockDragPreviewStatusSpacing`, `DockDragPreviewStatusIconSize` |
+| Host title tracker strip height | `DockHostTitleBarMouseTrackerHeight` |
+| Busy overlay card and progress sizing | `DockOverlayReloadButtonMargin`, `DockOverlayCardCornerRadius`, `DockOverlayCardPadding`, `DockOverlayCardSpacing`, `DockOverlayMessageFontSize`, `DockOverlayProgressWidth`, `DockOverlayProgressHeight` |
+| Dialog shell sizing | `DockDialogCornerRadius`, `DockDialogPadding`, `DockDialogMinWidth`, `DockDialogMaxWidth`, `DockDialogSpacing`, `DockDialogTitleFontSize`, `DockDialogCloseButtonSize` |
+| Confirmation dialog sizing | `DockConfirmationDialogPadding`, `DockConfirmationDialogMaxWidth`, `DockConfirmationDialogStackSpacing`, `DockConfirmationDialogActionsSpacing` |
+
+## Example migration snippet
+
+Before:
+
+```xaml
+
+
+```
+
+After:
+
+```xaml
+
+
+
+
+```
+
+## Density migration
+
+Before:
+
+```xaml
+
+```
+
+After:
+
+```xaml
+
+```
+
+Use token overrides only for exceptions; prefer density presets for global compact/normal scaling.
+
+## Validation checklist
+
+1. Active tab indicator, hover, and selected states still look correct.
+2. Tool chrome buttons keep expected hover/close behavior.
+3. Dock selector/target overlays retain intended indicator contrast.
+4. Compact mode updates tab and chrome sizes consistently.
+
+## Related docs
+
+- [Dock Theme Design Tokens](dock-theme-design-tokens.md)
+- [Dock Fluent Theme](dock-theme-fluent.md)
+- [Dock Simple Theme](dock-theme-simple.md)
diff --git a/docfx/articles/toc.yml b/docfx/articles/toc.yml
index 08eb576d2..975056eec 100644
--- a/docfx/articles/toc.yml
+++ b/docfx/articles/toc.yml
@@ -118,8 +118,20 @@
href: dock-datatemplates.md
- name: Styling and theming
href: dock-styling.md
+ - name: Fluent theme guide
+ href: dock-theme-fluent.md
+ - name: Simple theme guide
+ href: dock-theme-simple.md
+ - name: Design tokens
+ href: dock-theme-design-tokens.md
+ - name: Token migration
+ href: dock-theme-token-migration.md
- name: Custom themes
href: dock-custom-theme.md
+ - name: Semantic theme tokens
+ href: dock-theme-semantic-tokens.md
+ - name: IDE theme presets
+ href: dock-theme-ide-presets.md
- name: Context menus
href: dock-context-menus.md
- name: Command bars
diff --git a/samples/DockReactiveUIRiderSample/App.axaml b/samples/DockReactiveUIRiderSample/App.axaml
index 498c91773..55e61486d 100644
--- a/samples/DockReactiveUIRiderSample/App.axaml
+++ b/samples/DockReactiveUIRiderSample/App.axaml
@@ -17,31 +17,98 @@
- #1E1F22
- #2B2D30
- #313335
- #3C3F41
- #4C8BF5
- #D7D7D7
- #9AA1A9
+
+
+
+
+
+ #FFF3F3F3
+ #FFEFEFEF
+ #FFE7E7E7
+ #FFD0D0D0
+ #FF4A8BFF
+ #FF2C2C2C
+ #FF6F747A
-
-
-
-
-
-
-
+
+
+
+
+
+
+
- #2B2D30
- #2B2D30
- #2B2D30
- #D7D7D7
- #D7D7D7
- #3C3F41
- #3C3F41
- #1E1F22
- #1E1F22
+
+
+
+
+
+
+
+ #FFF3F3F3
+ #FFF3F3F3
+
+
+
+
+
+ #FF1E1F22
+ #FF2B2D30
+ #FF313338
+ #FF3C3F41
+ #FF4C9FFF
+ #FFBBBBBB
+ #FF9AA1A9
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #FF1E1F22
+ #FF1E1F22
+
+
+
+
+
+ #FF1E1F22
+ #FF2B2D30
+ #FF313338
+ #FF3C3F41
+ #FF4C9FFF
+ #FFBBBBBB
+ #FF9AA1A9
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #FF1E1F22
+ #FF1E1F22
+
+
@@ -52,13 +119,13 @@
@@ -70,11 +137,11 @@
Data="M5 2C3.895 2 3 2.895 3 4V14C3 15.105 3.895 16 5 16H11C12.105 16 13 15.105 13 14V6.414C13 6.015 12.842 5.634 12.561 5.354L9.646 2.439C9.365 2.158 8.984 2 8.586 2H5ZM4 4C4 3.448 4.448 3 5 3H8V5.5C8 6.328 8.672 7 9.5 7H12V14C12 14.552 11.552 15 11 15H5C4.448 15 4 14.552 4 14V4Z"
Width="14"
Height="14"
- Foreground="{StaticResource RiderAccentBrush}" />
+ Foreground="{DynamicResource RiderAccentBrush}" />
diff --git a/samples/DockReactiveUIRiderSample/ViewModels/MainWindowViewModel.cs b/samples/DockReactiveUIRiderSample/ViewModels/MainWindowViewModel.cs
index 2daf231f7..6243eac17 100644
--- a/samples/DockReactiveUIRiderSample/ViewModels/MainWindowViewModel.cs
+++ b/samples/DockReactiveUIRiderSample/ViewModels/MainWindowViewModel.cs
@@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Styling;
using Dock.Model.Controls;
using Dock.Model.Core;
using DockReactiveUIRiderSample.Services;
@@ -28,6 +29,7 @@ public class MainWindowViewModel : ReactiveObject
private string _activeDocumentStatus;
private EditorDocumentViewModel? _activeDocument;
private IDisposable? _caretSubscription;
+ private bool _isDarkTheme;
public MainWindowViewModel()
{
@@ -61,6 +63,10 @@ public MainWindowViewModel()
SaveAllCommand = ReactiveCommand.Create(SaveAllFiles);
CloseSolutionCommand = ReactiveCommand.Create(CloseSolution);
ExitCommand = ReactiveCommand.Create(Exit);
+ SwitchToLightThemeCommand = ReactiveCommand.Create(SwitchToLightTheme);
+ SwitchToDarkThemeCommand = ReactiveCommand.Create(SwitchToDarkTheme);
+
+ InitializeTheme();
}
public IRootDock? Layout
@@ -93,6 +99,20 @@ public EditorDocumentViewModel? ActiveDocument
private set => this.RaiseAndSetIfChanged(ref _activeDocument, value);
}
+ public bool IsDarkTheme
+ {
+ get => _isDarkTheme;
+ private set
+ {
+ if (this.RaiseAndSetIfChanged(ref _isDarkTheme, value))
+ {
+ this.RaisePropertyChanged(nameof(IsLightTheme));
+ }
+ }
+ }
+
+ public bool IsLightTheme => !IsDarkTheme;
+
public ReactiveCommand SaveFileCommand { get; }
public ReactiveCommand SaveAllCommand { get; }
@@ -101,6 +121,10 @@ public EditorDocumentViewModel? ActiveDocument
public ReactiveCommand ExitCommand { get; }
+ public ReactiveCommand SwitchToLightThemeCommand { get; }
+
+ public ReactiveCommand SwitchToDarkThemeCommand { get; }
+
public async Task LoadSolutionAsync(string solutionPath)
{
StatusText = "Loading solution...";
@@ -185,6 +209,33 @@ private void Exit()
}
}
+ private void InitializeTheme()
+ {
+ var requestedThemeVariant = Application.Current?.RequestedThemeVariant;
+ IsDarkTheme = requestedThemeVariant != ThemeVariant.Light;
+ }
+
+ private void SwitchToLightTheme()
+ {
+ SetTheme(ThemeVariant.Light);
+ }
+
+ private void SwitchToDarkTheme()
+ {
+ SetTheme(ThemeVariant.Dark);
+ }
+
+ private void SetTheme(ThemeVariant themeVariant)
+ {
+ if (Application.Current is null)
+ {
+ return;
+ }
+
+ Application.Current.RequestedThemeVariant = themeVariant;
+ IsDarkTheme = themeVariant == ThemeVariant.Dark;
+ }
+
private void SetActiveDocument(EditorDocumentViewModel? document)
{
_caretSubscription?.Dispose();
diff --git a/samples/DockReactiveUIRiderSample/Views/Documents/EditorDocumentView.axaml b/samples/DockReactiveUIRiderSample/Views/Documents/EditorDocumentView.axaml
index 5ab533bab..8e8149c90 100644
--- a/samples/DockReactiveUIRiderSample/Views/Documents/EditorDocumentView.axaml
+++ b/samples/DockReactiveUIRiderSample/Views/Documents/EditorDocumentView.axaml
@@ -12,9 +12,9 @@
@@ -20,7 +20,7 @@
-
+
@@ -49,8 +58,8 @@
@@ -83,8 +92,8 @@
diff --git a/samples/DockReactiveUIRiderSample/Views/MainWindow.axaml b/samples/DockReactiveUIRiderSample/Views/MainWindow.axaml
index ba0a41ff6..f433608bd 100644
--- a/samples/DockReactiveUIRiderSample/Views/MainWindow.axaml
+++ b/samples/DockReactiveUIRiderSample/Views/MainWindow.axaml
@@ -12,10 +12,10 @@
UseLayoutRounding="True"
WindowState="Normal"
WindowStartupLocation="CenterScreen"
- Background="{StaticResource RiderBackgroundBrush}"
- Foreground="{StaticResource RiderTextBrush}"
+ Background="{DynamicResource RiderBackgroundBrush}"
+ Foreground="{DynamicResource RiderTextBrush}"
BorderThickness="1"
- BorderBrush="{StaticResource RiderBorderBrush}"
+ BorderBrush="{DynamicResource RiderBorderBrush}"
FontFamily="JetBrains Sans, Segoe UI, Noto Sans, sans-serif"
Title="Dock Rider Sample"
Height="780" Width="1280"
diff --git a/samples/DockReactiveUIRiderSample/Views/Tools/ProblemsToolView.axaml b/samples/DockReactiveUIRiderSample/Views/Tools/ProblemsToolView.axaml
index 595027a75..3c69b4dbb 100644
--- a/samples/DockReactiveUIRiderSample/Views/Tools/ProblemsToolView.axaml
+++ b/samples/DockReactiveUIRiderSample/Views/Tools/ProblemsToolView.axaml
@@ -8,13 +8,13 @@
mc:Ignorable="d"
d:DesignWidth="600" d:DesignHeight="200"
x:DataType="vm:ProblemsToolViewModel" x:CompileBindings="True">
-
+
-
+
-
+
-
+
diff --git a/samples/DockReactiveUIRiderSample/Views/Tools/SolutionExplorerView.axaml b/samples/DockReactiveUIRiderSample/Views/Tools/SolutionExplorerView.axaml
index e7e454a4d..104d0763d 100644
--- a/samples/DockReactiveUIRiderSample/Views/Tools/SolutionExplorerView.axaml
+++ b/samples/DockReactiveUIRiderSample/Views/Tools/SolutionExplorerView.axaml
@@ -12,7 +12,7 @@
-
+
@@ -22,7 +22,7 @@
+ Foreground="{DynamicResource RiderMutedTextBrush}" />
diff --git a/samples/DockReactiveUIRiderSample/Views/Tools/StructureToolView.axaml b/samples/DockReactiveUIRiderSample/Views/Tools/StructureToolView.axaml
index a8df64b51..b6ab18da9 100644
--- a/samples/DockReactiveUIRiderSample/Views/Tools/StructureToolView.axaml
+++ b/samples/DockReactiveUIRiderSample/Views/Tools/StructureToolView.axaml
@@ -7,7 +7,7 @@
mc:Ignorable="d"
d:DesignWidth="260" d:DesignHeight="400"
x:DataType="vm:StructureToolViewModel" x:CompileBindings="True">
-
+
diff --git a/samples/DockReactiveUIRiderSample/Views/Tools/TerminalToolView.axaml b/samples/DockReactiveUIRiderSample/Views/Tools/TerminalToolView.axaml
index 93bcf7437..7569f10f8 100644
--- a/samples/DockReactiveUIRiderSample/Views/Tools/TerminalToolView.axaml
+++ b/samples/DockReactiveUIRiderSample/Views/Tools/TerminalToolView.axaml
@@ -7,12 +7,12 @@
mc:Ignorable="d"
d:DesignWidth="600" d:DesignHeight="200"
x:DataType="vm:TerminalToolViewModel" x:CompileBindings="True">
-
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/Accents/Fluent.axaml b/src/Dock.Avalonia.Themes.Fluent/Accents/Fluent.axaml
index 1c6620633..ca87461d9 100644
--- a/src/Dock.Avalonia.Themes.Fluent/Accents/Fluent.axaml
+++ b/src/Dock.Avalonia.Themes.Fluent/Accents/Fluent.axaml
@@ -15,8 +15,133 @@
12
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 26
+ 30
+ 10
+ 12
+ 14
+
+ 24
+ 0
+ 4,0,4,0
+ 4,1,4,0
+ 4,2,4,0
+ 26
+ 24
+ 14
+ 18
+ 16
+ 3
+ 2,0,0,0
+ 12
+ 6,4
+ 2
+ 0,0,2,0
+ 2
+ 2
+ 5
+ 8,0
+ 0,4,8,4
+ 2
+ 1
+ 5
+ 0,0,2,0
+
+
+
+
+
+ 4
+ 2
+ 2,0
+ 4
+ 6
+ 40
+ 125
+
+ 6
+ 12
+ 280
+ 520
+ 8
+ 120
+ 320
+ 6
+ 4
+ 4
+ 8,0,0,0
+ 3
+ 4,1
+ 10
+ 4,2
+ 4
+ 4
+ 4
+ 4
+ 2
+ 10
+ 1
+ 16
+ 8
+ 24
+ 8
+ 14
+ 200
+ 6
+ 10
+ 20
+ 360
+ 640
+ 12
+ 16
+ 28
+ 24
+ 480
+ 12
+ 8
+
+
M8.41687 7.57953V2.41851C8.41687 2.18743 8.22932 1.99988 7.99823 1.99988C7.76715 1.99988 7.5796 2.18743 7.5796 2.41851V7.57953H2.41863C2.18755 7.57953 2 7.76708 2 7.99816C2 8.22925 2.18755 8.41679 2.41863 8.41679H7.5796V13.5812C7.5796 13.8123 7.76715 13.9999 7.99823 13.9999C8.22932 13.9999 8.41687 13.8123 8.41687 13.5812V8.41679L13.5799 8.41851C13.811 8.41851 13.9985 8.23096 13.9985 7.99988C13.9985 7.76879 13.811 7.58125 13.5799 7.58125L8.41687 7.57953Z
diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/BusyOverlayControl.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/BusyOverlayControl.axaml
index a75f7bba8..e82cf7071 100644
--- a/src/Dock.Avalonia.Themes.Fluent/Controls/BusyOverlayControl.axaml
+++ b/src/Dock.Avalonia.Themes.Fluent/Controls/BusyOverlayControl.axaml
@@ -14,30 +14,30 @@
IsVisible="{TemplateBinding IsReloadVisible}"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
- Margin="16"
+ Margin="{DynamicResource DockOverlayReloadButtonMargin}"
ZIndex="31" />
-
-
-
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/ConfirmationDialogControl.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/ConfirmationDialogControl.axaml
index 9dfd94a6b..7088821b8 100644
--- a/src/Dock.Avalonia.Themes.Fluent/Controls/ConfirmationDialogControl.axaml
+++ b/src/Dock.Avalonia.Themes.Fluent/Controls/ConfirmationDialogControl.axaml
@@ -3,25 +3,25 @@
-
-
+
+ Foreground="{DynamicResource DockTabForegroundBrush}" />
+ Spacing="{DynamicResource DockConfirmationDialogActionsSpacing}">
@@ -42,7 +42,7 @@
-
+
@@ -58,10 +58,10 @@
@@ -113,15 +113,15 @@
diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/DragPreviewControl.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/DragPreviewControl.axaml
index 44577780d..6945e9ffc 100644
--- a/src/Dock.Avalonia.Themes.Fluent/Controls/DragPreviewControl.axaml
+++ b/src/Dock.Avalonia.Themes.Fluent/Controls/DragPreviewControl.axaml
@@ -8,17 +8,17 @@
-
+
-
+ Padding="{DynamicResource DockDragPreviewHeaderPadding}">
+
-
+
+ Padding="{DynamicResource DockHeaderContentPadding}" />
diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/MdiDocumentWindow.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/MdiDocumentWindow.axaml
index f4a0823aa..cc53187d9 100644
--- a/src/Dock.Avalonia.Themes.Fluent/Controls/MdiDocumentWindow.axaml
+++ b/src/Dock.Avalonia.Themes.Fluent/Controls/MdiDocumentWindow.axaml
@@ -221,7 +221,7 @@
@@ -231,13 +231,13 @@
+ Margin="{DynamicResource DockModifiedIndicatorMargin}" />
-
+
@@ -274,12 +274,12 @@
@@ -311,18 +311,18 @@
-
+
-
+
@@ -355,15 +355,15 @@
-
+
@@ -382,7 +382,7 @@
-
+
@@ -406,7 +406,7 @@
-
+
@@ -428,49 +428,49 @@
+
@@ -129,8 +135,8 @@
-
-
+
+
@@ -157,7 +163,7 @@
ContextFlyout="{TemplateBinding ToolFlyout}"
Grid.Row="{Binding GripMode, Converter={x:Static GripModeConverters.GridRowAutoHideConverter}}">
-
+
@@ -166,7 +172,7 @@
-
+
@@ -225,7 +231,7 @@
@@ -267,17 +273,17 @@
-
+
+
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/ToolControl.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/ToolControl.axaml
index 26af41d95..d6060b48a 100644
--- a/src/Dock.Avalonia.Themes.Fluent/Controls/ToolControl.axaml
+++ b/src/Dock.Avalonia.Themes.Fluent/Controls/ToolControl.axaml
@@ -26,7 +26,7 @@
-
+
@@ -34,7 +34,7 @@
+ Margin="{DynamicResource DockModifiedIndicatorMargin}" />
@@ -73,9 +73,9 @@
diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStrip.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStrip.axaml
index 7951160e3..efa5ef067 100644
--- a/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStrip.axaml
+++ b/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStrip.axaml
@@ -19,7 +19,7 @@
-
+
@@ -85,13 +85,13 @@
diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStripItem.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStripItem.axaml
index 6d849d79a..699d8ae5c 100644
--- a/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStripItem.axaml
+++ b/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStripItem.axaml
@@ -147,14 +147,14 @@
-
+
-
-
+
+
-
+
@@ -178,8 +178,8 @@
-
+ Spacing="{DynamicResource DockTabContentSpacing}">
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/WindowChromeResources.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/WindowChromeResources.axaml
index 9a568a549..804d66fb0 100644
--- a/src/Dock.Avalonia.Themes.Fluent/Controls/WindowChromeResources.axaml
+++ b/src/Dock.Avalonia.Themes.Fluent/Controls/WindowChromeResources.axaml
@@ -1,7 +1,7 @@
-
-
-
-
+
+
+
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/DensityStyles/Compact.axaml b/src/Dock.Avalonia.Themes.Fluent/DensityStyles/Compact.axaml
new file mode 100644
index 000000000..4b4a55fb5
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Fluent/DensityStyles/Compact.axaml
@@ -0,0 +1,61 @@
+
+ 22
+ 22
+ 8
+ 10
+ 12
+
+ 22
+ 0
+ 3,0,3,0
+ 3,0,3,0
+ 3,1,3,0
+
+ 24
+ 22
+ 12
+
+ 16
+ 14
+ 2
+ 1,0,0,0
+
+ 10
+ 4,2
+ 1
+ 0,0,1,0
+ 1
+ 1
+ 4
+ 6,0
+ 0,3,6,3
+ 1
+ 4
+ 0,0,1,0
+ 3
+ 1
+ 1,0
+ 3
+ 3
+ 3
+ 1
+ 9
+ 12
+ 20
+ 6
+ 13
+ 180
+ 5
+ 16
+ 320
+ 560
+ 10
+ 14
+ 24
+ 20
+ 440
+ 10
+ 6
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml b/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml
index 6154426ff..0f6ea2b22 100644
--- a/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml
+++ b/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml
@@ -55,6 +55,9 @@
+
+
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml.cs b/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml.cs
index d15fddbe3..6edd0e840 100644
--- a/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml.cs
+++ b/src/Dock.Avalonia.Themes.Fluent/DockFluentTheme.axaml.cs
@@ -1,13 +1,80 @@
// Copyright (c) Wiesław Šoltés. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
+using System;
+using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
using Avalonia.Styling;
+using Dock.Avalonia.Themes;
namespace Dock.Avalonia.Themes.Fluent;
///
/// The Dock fluent theme.
///
-public class DockFluentTheme : Styles
+public class DockFluentTheme : Styles, IResourceNode
{
+ private readonly ResourceDictionary _compactStyles;
+ private DockDensityStyle _densityStyle;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The optional service provider.
+ public DockFluentTheme(IServiceProvider? serviceProvider = null)
+ {
+ AvaloniaXamlLoader.Load(serviceProvider, this);
+ _compactStyles = (ResourceDictionary)GetAndRemove("CompactStyles");
+
+ object GetAndRemove(string key)
+ {
+ var value = Resources[key] ?? throw new KeyNotFoundException($"Key '{key}' was not found in resources.");
+ Resources.Remove(key);
+ return value;
+ }
+ }
+
+ ///
+ /// Identifies the direct property.
+ ///
+ public static readonly DirectProperty DensityStyleProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(DensityStyle),
+ o => o.DensityStyle,
+ (o, v) => o.DensityStyle = v);
+
+ ///
+ /// Gets or sets the density style used by this theme.
+ ///
+ public DockDensityStyle DensityStyle
+ {
+ get => _densityStyle;
+ set => SetAndRaise(DensityStyleProperty, ref _densityStyle, value);
+ }
+
+ ///
+ /// Handles resource invalidation when theme properties change.
+ ///
+ /// The property change payload.
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == DensityStyleProperty)
+ {
+ Owner?.NotifyHostedResourcesChanged(new ResourcesChangedEventArgs());
+ }
+ }
+
+ bool IResourceNode.TryGetResource(object key, ThemeVariant? theme, out object? value)
+ {
+ if (_densityStyle == DockDensityStyle.Compact && _compactStyles.TryGetResource(key, theme, out value))
+ {
+ return true;
+ }
+
+ return base.TryGetResource(key, theme, out value);
+ }
}
diff --git a/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/Default.axaml b/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/Default.axaml
new file mode 100644
index 000000000..6b2a51eef
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/Default.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderDark.axaml b/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderDark.axaml
new file mode 100644
index 000000000..9a908f220
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderDark.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderLight.axaml b/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderLight.axaml
new file mode 100644
index 000000000..618189745
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderLight.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeDark.axaml b/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeDark.axaml
new file mode 100644
index 000000000..cabf983dc
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeDark.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
diff --git a/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeLight.axaml b/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeLight.axaml
new file mode 100644
index 000000000..6400f67d7
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeLight.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
diff --git a/src/Dock.Avalonia.Themes.Simple/Accents/Simple.axaml b/src/Dock.Avalonia.Themes.Simple/Accents/Simple.axaml
index dcfd2c893..09415f839 100644
--- a/src/Dock.Avalonia.Themes.Simple/Accents/Simple.axaml
+++ b/src/Dock.Avalonia.Themes.Simple/Accents/Simple.axaml
@@ -21,6 +21,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 26
+ 30
+ 10
+ 12
+ 14
+
+ 24
+ 0
+ 4,0,4,0
+ 4,1,4,0
+ 4,2,4,0
+ 26
+ 24
+ 14
+ 18
+ 16
+ 3
+ 2,0,0,0
+ 12
+ 6,4
+ 2
+ 0,0,2,0
+ 2
+ 2
+ 5
+ 8,0
+ 0,4,8,4
+ 2
+ 1
+ 5
+ 0,0,2,0
+
+
+
+
+
+ 4
+ 2
+ 2,0
+ 4
+ 6
+ 40
+ 125
+
+ 6
+ 12
+ 280
+ 520
+ 8
+ 120
+ 320
+ 6
+ 4
+ 4
+ 8,0,0,0
+ 3
+ 4,1
+ 10
+ 4,2
+ 4
+ 4
+ 4
+ 4
+ 2
+ 10
+ 1
+ 16
+ 8
+ 24
+ 8
+ 14
+ 200
+ 6
+ 10
+ 20
+ 360
+ 640
+ 12
+ 16
+ 28
+ 24
+ 480
+ 12
+ 8
+
M8.41687 7.57953V2.41851C8.41687 2.18743 8.22932 1.99988 7.99823 1.99988C7.76715 1.99988 7.5796 2.18743 7.5796 2.41851V7.57953H2.41863C2.18755 7.57953 2 7.76708 2 7.99816C2 8.22925 2.18755 8.41679 2.41863 8.41679H7.5796V13.5812C7.5796 13.8123 7.76715 13.9999 7.99823 13.9999C8.22932 13.9999 8.41687 13.8123 8.41687 13.5812V8.41679L13.5799 8.41851C13.811 8.41851 13.9985 8.23096 13.9985 7.99988C13.9985 7.76879 13.811 7.58125 13.5799 7.58125L8.41687 7.57953Z
M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z
diff --git a/src/Dock.Avalonia.Themes.Simple/DensityStyles/Compact.axaml b/src/Dock.Avalonia.Themes.Simple/DensityStyles/Compact.axaml
new file mode 100644
index 000000000..4b4a55fb5
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Simple/DensityStyles/Compact.axaml
@@ -0,0 +1,61 @@
+
+ 22
+ 22
+ 8
+ 10
+ 12
+
+ 22
+ 0
+ 3,0,3,0
+ 3,0,3,0
+ 3,1,3,0
+
+ 24
+ 22
+ 12
+
+ 16
+ 14
+ 2
+ 1,0,0,0
+
+ 10
+ 4,2
+ 1
+ 0,0,1,0
+ 1
+ 1
+ 4
+ 6,0
+ 0,3,6,3
+ 1
+ 4
+ 0,0,1,0
+ 3
+ 1
+ 1,0
+ 3
+ 3
+ 3
+ 1
+ 9
+ 12
+ 20
+ 6
+ 13
+ 180
+ 5
+ 16
+ 320
+ 560
+ 10
+ 14
+ 24
+ 20
+ 440
+ 10
+ 6
+
diff --git a/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml b/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml
index 62024acec..5dd63190d 100644
--- a/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml
+++ b/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml
@@ -47,6 +47,9 @@
+
+
+
diff --git a/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml.cs b/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml.cs
index 64642844c..1422bc83b 100644
--- a/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml.cs
+++ b/src/Dock.Avalonia.Themes.Simple/DockSimpleTheme.axaml.cs
@@ -1,13 +1,80 @@
// Copyright (c) Wiesław Šoltés. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
+using System;
+using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
using Avalonia.Styling;
+using Dock.Avalonia.Themes;
namespace Dock.Avalonia.Themes.Simple;
///
/// The Dock simple theme.
///
-public class DockSimpleTheme : Styles
+public class DockSimpleTheme : Styles, IResourceNode
{
+ private readonly ResourceDictionary _compactStyles;
+ private DockDensityStyle _densityStyle;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The optional service provider.
+ public DockSimpleTheme(IServiceProvider? serviceProvider = null)
+ {
+ AvaloniaXamlLoader.Load(serviceProvider, this);
+ _compactStyles = (ResourceDictionary)GetAndRemove("CompactStyles");
+
+ object GetAndRemove(string key)
+ {
+ var value = Resources[key] ?? throw new KeyNotFoundException($"Key '{key}' was not found in resources.");
+ Resources.Remove(key);
+ return value;
+ }
+ }
+
+ ///
+ /// Identifies the direct property.
+ ///
+ public static readonly DirectProperty DensityStyleProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(DensityStyle),
+ o => o.DensityStyle,
+ (o, v) => o.DensityStyle = v);
+
+ ///
+ /// Gets or sets the density style used by this theme.
+ ///
+ public DockDensityStyle DensityStyle
+ {
+ get => _densityStyle;
+ set => SetAndRaise(DensityStyleProperty, ref _densityStyle, value);
+ }
+
+ ///
+ /// Handles resource invalidation when theme properties change.
+ ///
+ /// The property change payload.
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == DensityStyleProperty)
+ {
+ Owner?.NotifyHostedResourcesChanged(new ResourcesChangedEventArgs());
+ }
+ }
+
+ bool IResourceNode.TryGetResource(object key, ThemeVariant? theme, out object? value)
+ {
+ if (_densityStyle == DockDensityStyle.Compact && _compactStyles.TryGetResource(key, theme, out value))
+ {
+ return true;
+ }
+
+ return base.TryGetResource(key, theme, out value);
+ }
}
diff --git a/src/Dock.Avalonia.Themes.Simple/Presets/Ide/Default.axaml b/src/Dock.Avalonia.Themes.Simple/Presets/Ide/Default.axaml
new file mode 100644
index 000000000..4509fa857
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Simple/Presets/Ide/Default.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
diff --git a/src/Dock.Avalonia.Themes.Simple/Presets/Ide/RiderDark.axaml b/src/Dock.Avalonia.Themes.Simple/Presets/Ide/RiderDark.axaml
new file mode 100644
index 000000000..9a908f220
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Simple/Presets/Ide/RiderDark.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
diff --git a/src/Dock.Avalonia.Themes.Simple/Presets/Ide/RiderLight.axaml b/src/Dock.Avalonia.Themes.Simple/Presets/Ide/RiderLight.axaml
new file mode 100644
index 000000000..618189745
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Simple/Presets/Ide/RiderLight.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
diff --git a/src/Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeDark.axaml b/src/Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeDark.axaml
new file mode 100644
index 000000000..cabf983dc
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeDark.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
diff --git a/src/Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeLight.axaml b/src/Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeLight.axaml
new file mode 100644
index 000000000..6400f67d7
--- /dev/null
+++ b/src/Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeLight.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
diff --git a/src/Dock.Avalonia/Themes/DockDensityStyle.cs b/src/Dock.Avalonia/Themes/DockDensityStyle.cs
new file mode 100644
index 000000000..f5d788cc5
--- /dev/null
+++ b/src/Dock.Avalonia/Themes/DockDensityStyle.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Wiesław Šoltés. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+
+namespace Dock.Avalonia.Themes;
+
+///
+/// Defines density presets for Dock control themes.
+///
+public enum DockDensityStyle
+{
+ ///
+ /// Uses default control sizing.
+ ///
+ Normal,
+
+ ///
+ /// Uses compact sizing for tighter layouts.
+ ///
+ Compact
+}
diff --git a/tests/Dock.Avalonia.Themes.UnitTests/ThemeDensityControlSizingTests.cs b/tests/Dock.Avalonia.Themes.UnitTests/ThemeDensityControlSizingTests.cs
new file mode 100644
index 000000000..6d32e6fd5
--- /dev/null
+++ b/tests/Dock.Avalonia.Themes.UnitTests/ThemeDensityControlSizingTests.cs
@@ -0,0 +1,221 @@
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Headless.XUnit;
+using Avalonia.Styling;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+using Dock.Avalonia.Controls;
+using Dock.Avalonia.Controls.Overlays;
+using Dock.Avalonia.Themes;
+using Dock.Avalonia.Themes.Fluent;
+using Dock.Avalonia.Themes.Simple;
+using Xunit;
+
+namespace Dock.Avalonia.Themes.UnitTests;
+
+[Collection(ThemeResourceIsolationCollection.Name)]
+public class ThemeDensityControlSizingTests
+{
+ [AvaloniaFact]
+ public void CompactDensity_Should_Update_TabItem_MinHeights_For_All_Themes()
+ {
+ foreach (var useFluentTheme in new[] { true, false })
+ {
+ var documentTab = new DocumentTabStripItem();
+ var documentWindow = ShowWithTheme(
+ documentTab,
+ useFluentTheme
+ ? new DockFluentTheme { DensityStyle = DockDensityStyle.Compact }
+ : new DockSimpleTheme { DensityStyle = DockDensityStyle.Compact });
+ try
+ {
+ Assert.Equal(22d, documentTab.MinHeight);
+ }
+ finally
+ {
+ documentWindow.Close();
+ }
+
+ var toolTab = new ToolTabStripItem();
+ var toolWindow = ShowWithTheme(
+ toolTab,
+ useFluentTheme
+ ? new DockFluentTheme { DensityStyle = DockDensityStyle.Compact }
+ : new DockSimpleTheme { DensityStyle = DockDensityStyle.Compact });
+ try
+ {
+ Assert.Equal(0d, toolTab.MinHeight);
+ Assert.Equal(new Thickness(3, 0, 3, 0), toolTab.Padding);
+ }
+ finally
+ {
+ toolWindow.Close();
+ }
+ }
+ }
+
+ [AvaloniaFact]
+ public void NormalDensity_Should_Keep_Default_Tool_Tab_Metrics_For_All_Themes()
+ {
+ foreach (var useFluentTheme in new[] { true, false })
+ {
+ var toolTab = new ToolTabStripItem();
+ var toolWindow = ShowWithTheme(
+ toolTab,
+ useFluentTheme
+ ? new DockFluentTheme { DensityStyle = DockDensityStyle.Normal }
+ : new DockSimpleTheme { DensityStyle = DockDensityStyle.Normal });
+ try
+ {
+ Assert.Equal(0d, toolTab.MinHeight);
+ Assert.Equal(new Thickness(4, 1, 4, 0), toolTab.Padding);
+ }
+ finally
+ {
+ toolWindow.Close();
+ }
+ }
+ }
+
+ [AvaloniaFact]
+ public void Density_Should_Update_Close_And_Chrome_Button_Themes_For_All_Themes()
+ {
+ AssertThemeButtonSizing(new DockFluentTheme { DensityStyle = DockDensityStyle.Normal }, 14d, 18d, 16d);
+ AssertThemeButtonSizing(new DockSimpleTheme { DensityStyle = DockDensityStyle.Normal }, 14d, 18d, 16d);
+
+ AssertThemeButtonSizing(new DockFluentTheme { DensityStyle = DockDensityStyle.Compact }, 12d, 16d, 14d);
+ AssertThemeButtonSizing(new DockSimpleTheme { DensityStyle = DockDensityStyle.Compact }, 12d, 16d, 14d);
+ }
+
+ [AvaloniaFact]
+ public void Theme_Tokens_Should_Bind_To_Key_Control_Template_Parts()
+ {
+ AssertTemplatePartSizing(new DockFluentTheme { DensityStyle = DockDensityStyle.Normal }, expectedGripHeight: 5d, expectedStatusIconSize: 10d, expectedTargetSelectorSize: 40d, expectedDialogTitleFontSize: 16d, expectedDialogCloseButtonSize: 28d);
+ AssertTemplatePartSizing(new DockSimpleTheme { DensityStyle = DockDensityStyle.Normal }, expectedGripHeight: 5d, expectedStatusIconSize: 10d, expectedTargetSelectorSize: 40d, expectedDialogTitleFontSize: 16d, expectedDialogCloseButtonSize: 28d);
+
+ AssertTemplatePartSizing(new DockFluentTheme { DensityStyle = DockDensityStyle.Compact }, expectedGripHeight: 4d, expectedStatusIconSize: 9d, expectedTargetSelectorSize: 40d, expectedDialogTitleFontSize: 14d, expectedDialogCloseButtonSize: 24d);
+ AssertTemplatePartSizing(new DockSimpleTheme { DensityStyle = DockDensityStyle.Compact }, expectedGripHeight: 4d, expectedStatusIconSize: 9d, expectedTargetSelectorSize: 40d, expectedDialogTitleFontSize: 14d, expectedDialogCloseButtonSize: 24d);
+ }
+
+ private static void AssertThemeButtonSizing(Styles theme, double expectedCloseSize, double expectedChromeWidth, double expectedChromeHeight)
+ {
+ var host = new StackPanel();
+ var closeButton = new Button();
+ var chromeButton = new Button();
+
+ host.Children.Add(closeButton);
+ host.Children.Add(chromeButton);
+
+ var window = ShowWithTheme(host, theme);
+ try
+ {
+ Assert.True(host.TryFindResource("DocumentCloseButtonTheme", out var closeThemeResource));
+ closeButton.Theme = Assert.IsType(closeThemeResource);
+
+ Assert.True(host.TryFindResource("ChromeButton", out var chromeThemeResource));
+ chromeButton.Theme = Assert.IsType(chromeThemeResource);
+
+ closeButton.ApplyTemplate();
+ chromeButton.ApplyTemplate();
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.Equal(expectedCloseSize, closeButton.Width);
+ Assert.Equal(expectedCloseSize, closeButton.Height);
+ Assert.Equal(expectedChromeWidth, chromeButton.Width);
+ Assert.Equal(expectedChromeHeight, chromeButton.Height);
+ }
+ finally
+ {
+ window.Close();
+ }
+ }
+
+ private static void AssertTemplatePartSizing(
+ Styles theme,
+ double expectedGripHeight,
+ double expectedStatusIconSize,
+ double expectedTargetSelectorSize,
+ double expectedDialogTitleFontSize,
+ double expectedDialogCloseButtonSize)
+ {
+ var host = new StackPanel();
+ var toolChrome = new ToolChromeControl();
+ var dragPreview = new DragPreviewControl
+ {
+ Status = "Dock"
+ };
+ var dockTarget = new DockTarget();
+ var dialogShell = new DialogShellControl
+ {
+ Title = "Dialog",
+ IsCloseVisible = true
+ };
+
+ host.Children.Add(toolChrome);
+ host.Children.Add(dragPreview);
+ host.Children.Add(dockTarget);
+ host.Children.Add(dialogShell);
+
+ var window = ShowWithTheme(host, theme);
+ try
+ {
+ toolChrome.ApplyTemplate();
+ dragPreview.ApplyTemplate();
+ dockTarget.ApplyTemplate();
+ dialogShell.ApplyTemplate();
+ toolChrome.UpdateLayout();
+ dragPreview.UpdateLayout();
+ dockTarget.UpdateLayout();
+ dialogShell.UpdateLayout();
+ Dispatcher.UIThread.RunJobs();
+
+ var gripGrid = FindNamedControl(toolChrome, "PART_Grid");
+ var statusIcon = FindNamedControl(dragPreview, "PART_StatusIcon");
+ var topSelector = FindNamedControl(dockTarget, "PART_TopSelector");
+ var dialogCloseButton = dialogShell.GetVisualDescendants().OfType().FirstOrDefault();
+ var dialogTitle = dialogShell.GetVisualDescendants().OfType().FirstOrDefault(x => x.Text == "Dialog");
+
+ Assert.Equal(expectedGripHeight, gripGrid.Height);
+ Assert.Equal(expectedStatusIconSize, statusIcon.Width);
+ Assert.Equal(expectedStatusIconSize, statusIcon.Height);
+ Assert.Equal(expectedTargetSelectorSize, topSelector.Width);
+ Assert.Equal(expectedTargetSelectorSize, topSelector.Height);
+ Assert.NotNull(dialogTitle);
+ Assert.Equal(expectedDialogTitleFontSize, dialogTitle!.FontSize);
+ Assert.NotNull(dialogCloseButton);
+ Assert.Equal(expectedDialogCloseButtonSize, dialogCloseButton!.Width);
+ Assert.Equal(expectedDialogCloseButtonSize, dialogCloseButton.Height);
+ }
+ finally
+ {
+ window.Close();
+ }
+ }
+
+ private static T FindNamedControl(Control root, string name) where T : Control
+ {
+ var control = root.GetVisualDescendants().OfType().FirstOrDefault(x => x.Name == name);
+ Assert.NotNull(control);
+ return control!;
+ }
+
+ private static Window ShowWithTheme(Control control, Styles theme)
+ {
+ var window = new Window
+ {
+ Width = 640,
+ Height = 480,
+ Content = control
+ };
+
+ window.Styles.Add(theme);
+ window.Show();
+ window.UpdateLayout();
+ control.ApplyTemplate();
+ control.UpdateLayout();
+ Dispatcher.UIThread.RunJobs();
+ return window;
+ }
+}
diff --git a/tests/Dock.Avalonia.Themes.UnitTests/ThemeDensityStyleTests.cs b/tests/Dock.Avalonia.Themes.UnitTests/ThemeDensityStyleTests.cs
new file mode 100644
index 000000000..e9fd7fd2a
--- /dev/null
+++ b/tests/Dock.Avalonia.Themes.UnitTests/ThemeDensityStyleTests.cs
@@ -0,0 +1,243 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Headless.XUnit;
+using Avalonia.Styling;
+using Avalonia.Threading;
+using Dock.Avalonia.Themes;
+using Dock.Avalonia.Themes.Fluent;
+using Dock.Avalonia.Themes.Simple;
+using Xunit;
+
+namespace Dock.Avalonia.Themes.UnitTests;
+
+[Collection(ThemeResourceIsolationCollection.Name)]
+public class ThemeDensityStyleTests
+{
+ [AvaloniaFact]
+ public void DockFluentTheme_Should_Resolve_Compact_Density_Resources()
+ {
+ var theme = new DockFluentTheme
+ {
+ DensityStyle = DockDensityStyle.Compact
+ };
+
+ AssertDensityResources(
+ theme,
+ expectedTabItemMinHeight: 22d,
+ expectedToolTabItemMinHeight: 0d,
+ expectedCloseButtonSize: 12d,
+ expectedMdiIconSize: 10d,
+ expectedChromeGripHeight: 4d,
+ expectedTabContentSpacing: 1d,
+ expectedDragPreviewStatusIconSize: 9d,
+ expectedOverlayMessageFontSize: 13d,
+ expectedDialogTitleFontSize: 14d);
+ }
+
+ [AvaloniaFact]
+ public void DockSimpleTheme_Should_Resolve_Compact_Density_Resources()
+ {
+ var theme = new DockSimpleTheme
+ {
+ DensityStyle = DockDensityStyle.Compact
+ };
+
+ AssertDensityResources(
+ theme,
+ expectedTabItemMinHeight: 22d,
+ expectedToolTabItemMinHeight: 0d,
+ expectedCloseButtonSize: 12d,
+ expectedMdiIconSize: 10d,
+ expectedChromeGripHeight: 4d,
+ expectedTabContentSpacing: 1d,
+ expectedDragPreviewStatusIconSize: 9d,
+ expectedOverlayMessageFontSize: 13d,
+ expectedDialogTitleFontSize: 14d);
+ }
+
+ [AvaloniaFact]
+ public void DockFluentTheme_Should_Resolve_Normal_Density_Resources()
+ {
+ var theme = new DockFluentTheme
+ {
+ DensityStyle = DockDensityStyle.Normal
+ };
+
+ AssertDensityResources(
+ theme,
+ expectedTabItemMinHeight: 24d,
+ expectedToolTabItemMinHeight: 0d,
+ expectedCloseButtonSize: 14d,
+ expectedMdiIconSize: 12d,
+ expectedChromeGripHeight: 5d,
+ expectedTabContentSpacing: 2d,
+ expectedDragPreviewStatusIconSize: 10d,
+ expectedOverlayMessageFontSize: 14d,
+ expectedDialogTitleFontSize: 16d);
+ }
+
+ [AvaloniaFact]
+ public void DockSimpleTheme_Should_Resolve_Normal_Density_Resources()
+ {
+ var theme = new DockSimpleTheme
+ {
+ DensityStyle = DockDensityStyle.Normal
+ };
+
+ AssertDensityResources(
+ theme,
+ expectedTabItemMinHeight: 24d,
+ expectedToolTabItemMinHeight: 0d,
+ expectedCloseButtonSize: 14d,
+ expectedMdiIconSize: 12d,
+ expectedChromeGripHeight: 5d,
+ expectedTabContentSpacing: 2d,
+ expectedDragPreviewStatusIconSize: 10d,
+ expectedOverlayMessageFontSize: 14d,
+ expectedDialogTitleFontSize: 16d);
+ }
+
+ [AvaloniaFact]
+ public void DockFluentTheme_Should_Update_Resources_When_Density_Changes()
+ {
+ var theme = new DockFluentTheme
+ {
+ DensityStyle = DockDensityStyle.Normal
+ };
+
+ AssertDensitySwitch(theme, expectedNormalTabItemMinHeight: 24d, expectedCompactTabItemMinHeight: 22d);
+ }
+
+ [AvaloniaFact]
+ public void DockSimpleTheme_Should_Update_Resources_When_Density_Changes()
+ {
+ var theme = new DockSimpleTheme
+ {
+ DensityStyle = DockDensityStyle.Normal
+ };
+
+ AssertDensitySwitch(theme, expectedNormalTabItemMinHeight: 24d, expectedCompactTabItemMinHeight: 22d);
+ }
+
+ private static void AssertDensityResources(
+ Styles theme,
+ double expectedTabItemMinHeight,
+ double expectedToolTabItemMinHeight,
+ double expectedCloseButtonSize,
+ double expectedMdiIconSize,
+ double expectedChromeGripHeight,
+ double expectedTabContentSpacing,
+ double expectedDragPreviewStatusIconSize,
+ double expectedOverlayMessageFontSize,
+ double expectedDialogTitleFontSize)
+ {
+ var app = Application.Current ?? throw new System.InvalidOperationException("Avalonia application is not initialized.");
+ List previousStyles = app.Styles.ToList();
+ var window = new Window
+ {
+ Width = 640,
+ Height = 480,
+ Content = new Border()
+ };
+
+ app.Styles.Clear();
+ app.Styles.Add(theme);
+
+ window.Show();
+ window.UpdateLayout();
+ Dispatcher.UIThread.RunJobs();
+
+ try
+ {
+ var host = (Border)window.Content!;
+
+ Assert.True(host.TryFindResource("DockTabItemMinHeight", out var tabItemHeightValue));
+ Assert.Equal(expectedTabItemMinHeight, Assert.IsType(tabItemHeightValue));
+
+ Assert.True(host.TryFindResource("DockToolTabItemMinHeight", out var toolTabItemHeightValue));
+ Assert.Equal(expectedToolTabItemMinHeight, Assert.IsType(toolTabItemHeightValue));
+
+ Assert.True(host.TryFindResource("DockCloseButtonSize", out var closeButtonSizeValue));
+ Assert.Equal(expectedCloseButtonSize, Assert.IsType(closeButtonSizeValue));
+
+ Assert.True(host.TryFindResource("DockMdiTitleIconSize", out var mdiIconSizeValue));
+ Assert.Equal(expectedMdiIconSize, Assert.IsType(mdiIconSizeValue));
+
+ Assert.True(host.TryFindResource("DockChromeGripHeight", out var chromeGripHeightValue));
+ Assert.Equal(expectedChromeGripHeight, Assert.IsType(chromeGripHeightValue));
+
+ Assert.True(host.TryFindResource("DockTabContentSpacing", out var tabContentSpacingValue));
+ Assert.Equal(expectedTabContentSpacing, Assert.IsType(tabContentSpacingValue));
+
+ Assert.True(host.TryFindResource("DockDragPreviewStatusIconSize", out var dragPreviewStatusIconSizeValue));
+ Assert.Equal(expectedDragPreviewStatusIconSize, Assert.IsType(dragPreviewStatusIconSizeValue));
+
+ Assert.True(host.TryFindResource("DockOverlayMessageFontSize", out var overlayMessageFontSizeValue));
+ Assert.Equal(expectedOverlayMessageFontSize, Assert.IsType(overlayMessageFontSizeValue));
+
+ Assert.True(host.TryFindResource("DockDialogTitleFontSize", out var dialogTitleFontSizeValue));
+ Assert.Equal(expectedDialogTitleFontSize, Assert.IsType(dialogTitleFontSizeValue));
+ }
+ finally
+ {
+ window.Close();
+ app.Styles.Clear();
+ foreach (var style in previousStyles)
+ {
+ app.Styles.Add(style);
+ }
+ }
+ }
+
+ private static void AssertDensitySwitch(Styles theme, double expectedNormalTabItemMinHeight, double expectedCompactTabItemMinHeight)
+ {
+ var app = Application.Current ?? throw new System.InvalidOperationException("Avalonia application is not initialized.");
+ List previousStyles = app.Styles.ToList();
+ var window = new Window
+ {
+ Width = 640,
+ Height = 480,
+ Content = new Border()
+ };
+
+ app.Styles.Clear();
+ app.Styles.Add(theme);
+
+ window.Show();
+ window.UpdateLayout();
+ Dispatcher.UIThread.RunJobs();
+
+ try
+ {
+ var host = (Border)window.Content!;
+ Assert.True(host.TryFindResource("DockTabItemMinHeight", out var initialValue));
+ Assert.Equal(expectedNormalTabItemMinHeight, Assert.IsType(initialValue));
+
+ switch (theme)
+ {
+ case DockFluentTheme fluent:
+ fluent.DensityStyle = DockDensityStyle.Compact;
+ break;
+ case DockSimpleTheme simple:
+ simple.DensityStyle = DockDensityStyle.Compact;
+ break;
+ }
+
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.True(host.TryFindResource("DockTabItemMinHeight", out var compactValue));
+ Assert.Equal(expectedCompactTabItemMinHeight, Assert.IsType(compactValue));
+ }
+ finally
+ {
+ window.Close();
+ app.Styles.Clear();
+ foreach (var style in previousStyles)
+ {
+ app.Styles.Add(style);
+ }
+ }
+ }
+}
diff --git a/tests/Dock.Avalonia.Themes.UnitTests/ThemePresetLoadTests.cs b/tests/Dock.Avalonia.Themes.UnitTests/ThemePresetLoadTests.cs
new file mode 100644
index 000000000..41eaa5b87
--- /dev/null
+++ b/tests/Dock.Avalonia.Themes.UnitTests/ThemePresetLoadTests.cs
@@ -0,0 +1,341 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Headless.XUnit;
+using Avalonia.Markup.Xaml.Styling;
+using Avalonia.Media;
+using Avalonia.Styling;
+using Avalonia.Threading;
+using Dock.Avalonia.Themes.Fluent;
+using Dock.Avalonia.Themes.Simple;
+using Dock.Avalonia.Themes;
+using Xunit;
+
+namespace Dock.Avalonia.Themes.UnitTests;
+
+[Collection(ThemeResourceIsolationCollection.Name)]
+public class ThemePresetLoadTests
+{
+ [AvaloniaFact]
+ public void Fluent_VsCodeDark_Preset_Should_Load_And_Override_Key_Tokens()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockFluentTheme(),
+ presetUri: "avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeDark.axaml",
+ expectedSidebar: Color.Parse("#FF252526"),
+ expectedIndicator: Color.Parse("#FF3794FF"),
+ expectedHeaderHeight: 26d);
+ }
+
+ [AvaloniaFact]
+ public void Fluent_RiderLight_Preset_Should_Load_And_Override_Key_Tokens()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockFluentTheme(),
+ presetUri: "avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderLight.axaml",
+ expectedSidebar: Color.Parse("#FFF6F6F6"),
+ expectedIndicator: Color.Parse("#FF4A8BFF"),
+ expectedHeaderHeight: 26d);
+ }
+
+ [AvaloniaFact]
+ public void Fluent_VsCodeLight_Preset_Should_Load_And_Override_Key_Tokens()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockFluentTheme(),
+ presetUri: "avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeLight.axaml",
+ expectedSidebar: Color.Parse("#FFF3F3F3"),
+ expectedIndicator: Color.Parse("#FF007ACC"),
+ expectedHeaderHeight: 26d);
+ }
+
+ [AvaloniaFact]
+ public void Fluent_RiderDark_Preset_Should_Load_And_Override_Key_Tokens()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockFluentTheme(),
+ presetUri: "avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderDark.axaml",
+ expectedSidebar: Color.Parse("#FF2A2C2F"),
+ expectedIndicator: Color.Parse("#FF4C9FFF"),
+ expectedHeaderHeight: 26d);
+ }
+
+ [AvaloniaFact]
+ public void Fluent_Default_Preset_Should_Load_And_Expose_Key_Tokens()
+ {
+ AssertPresetLoads(
+ baseTheme: new DockFluentTheme(),
+ presetUri: "avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/Default.axaml",
+ expectedHeaderHeight: 26d);
+ }
+
+ [AvaloniaFact]
+ public void Simple_VsCodeDark_Preset_Should_Load_And_Override_Key_Tokens()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockSimpleTheme(),
+ presetUri: "avares://Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeDark.axaml",
+ expectedSidebar: Color.Parse("#FF252526"),
+ expectedIndicator: Color.Parse("#FF3794FF"),
+ expectedHeaderHeight: 26d);
+ }
+
+ [AvaloniaFact]
+ public void Simple_RiderLight_Preset_Should_Load_And_Override_Key_Tokens()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockSimpleTheme(),
+ presetUri: "avares://Dock.Avalonia.Themes.Simple/Presets/Ide/RiderLight.axaml",
+ expectedSidebar: Color.Parse("#FFF6F6F6"),
+ expectedIndicator: Color.Parse("#FF4A8BFF"),
+ expectedHeaderHeight: 26d);
+ }
+
+ [AvaloniaFact]
+ public void Simple_VsCodeLight_Preset_Should_Load_And_Override_Key_Tokens()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockSimpleTheme(),
+ presetUri: "avares://Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeLight.axaml",
+ expectedSidebar: Color.Parse("#FFF3F3F3"),
+ expectedIndicator: Color.Parse("#FF007ACC"),
+ expectedHeaderHeight: 26d);
+ }
+
+ [AvaloniaFact]
+ public void Simple_RiderDark_Preset_Should_Load_And_Override_Key_Tokens()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockSimpleTheme(),
+ presetUri: "avares://Dock.Avalonia.Themes.Simple/Presets/Ide/RiderDark.axaml",
+ expectedSidebar: Color.Parse("#FF2A2C2F"),
+ expectedIndicator: Color.Parse("#FF4C9FFF"),
+ expectedHeaderHeight: 26d);
+ }
+
+ [AvaloniaFact]
+ public void Simple_Default_Preset_Should_Load_And_Expose_Key_Tokens()
+ {
+ AssertPresetLoads(
+ baseTheme: new DockSimpleTheme(),
+ presetUri: "avares://Dock.Avalonia.Themes.Simple/Presets/Ide/Default.axaml",
+ expectedHeaderHeight: 26d);
+ }
+
+ [AvaloniaFact]
+ public void Fluent_VsCodeDark_Preset_Should_Work_With_Compact_Density()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockFluentTheme
+ {
+ DensityStyle = DockDensityStyle.Compact
+ },
+ presetUri: "avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeDark.axaml",
+ expectedSidebar: Color.Parse("#FF252526"),
+ expectedIndicator: Color.Parse("#FF3794FF"),
+ expectedHeaderHeight: 22d);
+ }
+
+ [AvaloniaFact]
+ public void Simple_VsCodeDark_Preset_Should_Work_With_Compact_Density()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockSimpleTheme
+ {
+ DensityStyle = DockDensityStyle.Compact
+ },
+ presetUri: "avares://Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeDark.axaml",
+ expectedSidebar: Color.Parse("#FF252526"),
+ expectedIndicator: Color.Parse("#FF3794FF"),
+ expectedHeaderHeight: 22d);
+ }
+
+ [AvaloniaFact]
+ public void Fluent_RiderDark_Preset_Should_Work_With_Compact_Density()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockFluentTheme
+ {
+ DensityStyle = DockDensityStyle.Compact
+ },
+ presetUri: "avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/RiderDark.axaml",
+ expectedSidebar: Color.Parse("#FF2A2C2F"),
+ expectedIndicator: Color.Parse("#FF4C9FFF"),
+ expectedHeaderHeight: 22d);
+ }
+
+ [AvaloniaFact]
+ public void Fluent_VsCodeLight_Preset_Should_Work_With_Compact_Density()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockFluentTheme
+ {
+ DensityStyle = DockDensityStyle.Compact
+ },
+ presetUri: "avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/VsCodeLight.axaml",
+ expectedSidebar: Color.Parse("#FFF3F3F3"),
+ expectedIndicator: Color.Parse("#FF007ACC"),
+ expectedHeaderHeight: 22d);
+ }
+
+ [AvaloniaFact]
+ public void Simple_RiderDark_Preset_Should_Work_With_Compact_Density()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockSimpleTheme
+ {
+ DensityStyle = DockDensityStyle.Compact
+ },
+ presetUri: "avares://Dock.Avalonia.Themes.Simple/Presets/Ide/RiderDark.axaml",
+ expectedSidebar: Color.Parse("#FF2A2C2F"),
+ expectedIndicator: Color.Parse("#FF4C9FFF"),
+ expectedHeaderHeight: 22d);
+ }
+
+ [AvaloniaFact]
+ public void Simple_VsCodeLight_Preset_Should_Work_With_Compact_Density()
+ {
+ AssertPresetOverrides(
+ baseTheme: new DockSimpleTheme
+ {
+ DensityStyle = DockDensityStyle.Compact
+ },
+ presetUri: "avares://Dock.Avalonia.Themes.Simple/Presets/Ide/VsCodeLight.axaml",
+ expectedSidebar: Color.Parse("#FFF3F3F3"),
+ expectedIndicator: Color.Parse("#FF007ACC"),
+ expectedHeaderHeight: 22d);
+ }
+
+ [AvaloniaFact]
+ public void Fluent_Default_Preset_Should_Work_With_Compact_Density()
+ {
+ AssertPresetLoads(
+ baseTheme: new DockFluentTheme
+ {
+ DensityStyle = DockDensityStyle.Compact
+ },
+ presetUri: "avares://Dock.Avalonia.Themes.Fluent/Presets/Ide/Default.axaml",
+ expectedHeaderHeight: 22d);
+ }
+
+ [AvaloniaFact]
+ public void Simple_Default_Preset_Should_Work_With_Compact_Density()
+ {
+ AssertPresetLoads(
+ baseTheme: new DockSimpleTheme
+ {
+ DensityStyle = DockDensityStyle.Compact
+ },
+ presetUri: "avares://Dock.Avalonia.Themes.Simple/Presets/Ide/Default.axaml",
+ expectedHeaderHeight: 22d);
+ }
+
+ private static void AssertPresetOverrides(Styles baseTheme, string presetUri, Color expectedSidebar, Color expectedIndicator, double expectedHeaderHeight)
+ {
+ var app = Application.Current ?? throw new System.InvalidOperationException("Avalonia application is not initialized.");
+ List previousStyles = app.Styles.ToList();
+ var window = new Window
+ {
+ Width = 640,
+ Height = 480,
+ Content = new Border()
+ };
+
+ app.Styles.Clear();
+ app.Styles.Add(baseTheme);
+ window.Resources = new ResourceDictionary();
+ window.Resources.MergedDictionaries.Add(new ResourceInclude(new System.Uri(presetUri))
+ {
+ Source = new System.Uri(presetUri)
+ });
+
+ window.Show();
+ window.UpdateLayout();
+ Dispatcher.UIThread.RunJobs();
+
+ try
+ {
+ var host = (Border)window.Content!;
+
+ AssertBrushColor(host, "DockSurfaceSidebarBrush", expectedSidebar);
+ AssertBrushColor(host, "DockTabActiveIndicatorBrush", expectedIndicator);
+
+ Assert.True(host.TryFindResource("DockHeaderHeight", out var heightValue));
+ Assert.NotNull(heightValue);
+ Assert.IsType(heightValue);
+ Assert.Equal(expectedHeaderHeight, (double)heightValue!);
+ }
+ finally
+ {
+ window.Close();
+ app.Styles.Clear();
+ foreach (var style in previousStyles)
+ {
+ app.Styles.Add(style);
+ }
+ }
+ }
+
+ private static void AssertPresetLoads(Styles baseTheme, string presetUri, double expectedHeaderHeight)
+ {
+ var app = Application.Current ?? throw new System.InvalidOperationException("Avalonia application is not initialized.");
+ List previousStyles = app.Styles.ToList();
+ var window = new Window
+ {
+ Width = 640,
+ Height = 480,
+ Content = new Border()
+ };
+
+ app.Styles.Clear();
+ app.Styles.Add(baseTheme);
+ window.Resources = new ResourceDictionary();
+ window.Resources.MergedDictionaries.Add(new ResourceInclude(new System.Uri(presetUri))
+ {
+ Source = new System.Uri(presetUri)
+ });
+
+ window.Show();
+ window.UpdateLayout();
+ Dispatcher.UIThread.RunJobs();
+
+ try
+ {
+ var host = (Border)window.Content!;
+
+ AssertBrushResource(host, "DockSurfaceSidebarBrush");
+ AssertBrushResource(host, "DockTabActiveIndicatorBrush");
+
+ Assert.True(host.TryFindResource("DockHeaderHeight", out var heightValue));
+ Assert.NotNull(heightValue);
+ Assert.IsType(heightValue);
+ Assert.Equal(expectedHeaderHeight, (double)heightValue!);
+ }
+ finally
+ {
+ window.Close();
+ app.Styles.Clear();
+ foreach (var style in previousStyles)
+ {
+ app.Styles.Add(style);
+ }
+ }
+ }
+
+ private static void AssertBrushColor(Border host, string key, Color expectedColor)
+ {
+ Assert.True(host.TryFindResource(key, out var value), $"Missing resource '{key}'.");
+ Assert.NotNull(value);
+
+ var brush = Assert.IsAssignableFrom(value);
+ Assert.Equal(expectedColor, brush.Color);
+ }
+
+ private static void AssertBrushResource(Border host, string key)
+ {
+ Assert.True(host.TryFindResource(key, out var value), $"Missing resource '{key}'.");
+ Assert.NotNull(value);
+ Assert.IsAssignableFrom(value);
+ }
+}
diff --git a/tests/Dock.Avalonia.Themes.UnitTests/ThemeResourceIsolationCollection.cs b/tests/Dock.Avalonia.Themes.UnitTests/ThemeResourceIsolationCollection.cs
new file mode 100644
index 000000000..5027365d5
--- /dev/null
+++ b/tests/Dock.Avalonia.Themes.UnitTests/ThemeResourceIsolationCollection.cs
@@ -0,0 +1,9 @@
+using Xunit;
+
+namespace Dock.Avalonia.Themes.UnitTests;
+
+[CollectionDefinition(Name, DisableParallelization = true)]
+public class ThemeResourceIsolationCollection
+{
+ public const string Name = "ThemeResourceIsolation";
+}
diff --git a/tests/Dock.Avalonia.Themes.UnitTests/ThemeSemanticTokenContractTests.cs b/tests/Dock.Avalonia.Themes.UnitTests/ThemeSemanticTokenContractTests.cs
new file mode 100644
index 000000000..12f581d92
--- /dev/null
+++ b/tests/Dock.Avalonia.Themes.UnitTests/ThemeSemanticTokenContractTests.cs
@@ -0,0 +1,175 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Headless.XUnit;
+using Avalonia.Styling;
+using Avalonia.Threading;
+using Dock.Avalonia.Themes.Fluent;
+using Dock.Avalonia.Themes.Simple;
+using Xunit;
+
+namespace Dock.Avalonia.Themes.UnitTests;
+
+[Collection(ThemeResourceIsolationCollection.Name)]
+public class ThemeSemanticTokenContractTests
+{
+ private static readonly string[] SemanticTokenKeys =
+ [
+ "DockSurfaceWorkbenchBrush",
+ "DockSurfaceSidebarBrush",
+ "DockSurfaceEditorBrush",
+ "DockSurfacePanelBrush",
+ "DockSurfaceHeaderBrush",
+ "DockSurfaceHeaderActiveBrush",
+ "DockBorderSubtleBrush",
+ "DockBorderStrongBrush",
+ "DockSeparatorBrush",
+ "DockSplitterIdleBrush",
+ "DockSplitterHoverBrush",
+ "DockSplitterDragBrush",
+ "DockTabBackgroundBrush",
+ "DockTabHoverBackgroundBrush",
+ "DockTabActiveBackgroundBrush",
+ "DockTabActiveIndicatorBrush",
+ "DockTabForegroundBrush",
+ "DockTabSelectedForegroundBrush",
+ "DockTabActiveForegroundBrush",
+ "DockTabCloseHoverBackgroundBrush",
+ "DockTargetIndicatorBrush",
+ "DockChromeButtonForegroundBrush",
+ "DockChromeButtonHoverBackgroundBrush",
+ "DockChromeButtonPressedBackgroundBrush",
+ "DockChromeButtonDangerHoverBrush",
+ "DockCornerRadiusSmall",
+ "DockHeaderHeight",
+ "DockTabHeight",
+ "DockTabHorizontalPadding",
+ "DockIconSizeSmall",
+ "DockIconSizeNormal",
+ "DockTabItemMinHeight",
+ "DockToolTabItemMinHeight",
+ "DockDocumentTabItemPadding",
+ "DockToolTabItemPadding",
+ "DockToolTabItemSelectedPadding",
+ "DockCreateButtonWidth",
+ "DockCreateButtonHeight",
+ "DockCloseButtonSize",
+ "DockChromeButtonWidth",
+ "DockChromeButtonHeight",
+ "DockChromeButtonPadding",
+ "DockChromeButtonMargin",
+ "DockMdiTitleIconSize",
+ "DockMdiHeaderDragPadding",
+ "DockHeaderContentPadding",
+ "DockModifiedIndicatorMargin",
+ "DockTabContentSpacing",
+ "DockTabContentMargin",
+ "DockCreateButtonIconMargin",
+ "DockToolChromeHeaderMargin",
+ "DockToolChromeTitleMargin",
+ "DockToolChromeMenuIconMargin",
+ "DockToolChromeDividerThickness",
+ "DockChromeGripHeight",
+ "DockChromeGripMargin",
+ "DockChromeGripBrush",
+ "DockMdiHeaderColumnSpacing",
+ "DockMdiButtonStripSpacing",
+ "DockMdiButtonStripMargin",
+ "DockMdiResizeEdgeThickness",
+ "DockMdiResizeCornerSize",
+ "DockTargetSelectorSize",
+ "DockTargetSelectorGridMaxSize",
+ "DockSelectorOverlayBackdropBrush",
+ "DockSelectorOverlayCornerRadius",
+ "DockSelectorOverlayPadding",
+ "DockSelectorOverlayMinWidth",
+ "DockSelectorOverlayMaxWidth",
+ "DockSelectorOverlaySpacing",
+ "DockSelectorOverlayListMinHeight",
+ "DockSelectorOverlayListMaxHeight",
+ "DockSelectorOverlayItemPadding",
+ "DockSelectorOverlayItemCornerRadius",
+ "DockSelectorOverlayBadgeSpacing",
+ "DockSelectorOverlayBadgeMargin",
+ "DockSelectorOverlayBadgeCornerRadius",
+ "DockSelectorOverlayBadgePadding",
+ "DockSelectorOverlayBadgeFontSize",
+ "DockCommandBarPadding",
+ "DockCommandBarSpacing",
+ "DockDragPreviewCornerRadius",
+ "DockDragPreviewHeaderPadding",
+ "DockDragPreviewHeaderSpacing",
+ "DockDragPreviewStatusSpacing",
+ "DockDragPreviewStatusIconSize",
+ "DockHostTitleBarMouseTrackerHeight",
+ "DockOverlayReloadButtonMargin",
+ "DockOverlayCardCornerRadius",
+ "DockOverlayCardPadding",
+ "DockOverlayCardSpacing",
+ "DockOverlayMessageFontSize",
+ "DockOverlayProgressWidth",
+ "DockOverlayProgressHeight",
+ "DockDialogCornerRadius",
+ "DockDialogPadding",
+ "DockDialogMinWidth",
+ "DockDialogMaxWidth",
+ "DockDialogSpacing",
+ "DockDialogTitleFontSize",
+ "DockDialogCloseButtonSize",
+ "DockConfirmationDialogPadding",
+ "DockConfirmationDialogMaxWidth",
+ "DockConfirmationDialogStackSpacing",
+ "DockConfirmationDialogActionsSpacing"
+ ];
+
+ [AvaloniaFact]
+ public void DockFluentTheme_Should_Resolve_All_Semantic_Tokens()
+ {
+ AssertSemanticTokenContract(new DockFluentTheme());
+ }
+
+ [AvaloniaFact]
+ public void DockSimpleTheme_Should_Resolve_All_Semantic_Tokens()
+ {
+ AssertSemanticTokenContract(new DockSimpleTheme());
+ }
+
+ private static void AssertSemanticTokenContract(Styles theme)
+ {
+ var app = Application.Current ?? throw new System.InvalidOperationException("Avalonia application is not initialized.");
+ List previousStyles = app.Styles.ToList();
+ var window = new Window
+ {
+ Width = 640,
+ Height = 480,
+ Content = new Border()
+ };
+
+ app.Styles.Clear();
+ app.Styles.Add(theme);
+ window.Show();
+ window.UpdateLayout();
+ Dispatcher.UIThread.RunJobs();
+
+ try
+ {
+ var host = (Border)window.Content!;
+
+ foreach (var key in SemanticTokenKeys)
+ {
+ Assert.True(host.TryFindResource(key, out var value), $"Missing semantic token '{key}'.");
+ Assert.NotNull(value);
+ }
+ }
+ finally
+ {
+ window.Close();
+ app.Styles.Clear();
+ foreach (var style in previousStyles)
+ {
+ app.Styles.Add(style);
+ }
+ }
+ }
+}