Expose MacOS menu styles for use independent of theme - #624
Conversation
49417aa to
8e427cd
Compare
There was a problem hiding this comment.
Pull request overview
Exposes standalone macOS menu styling with platform-specific resources, documentation, demos, and contract tests.
Changes:
- Adds
MacOsMenuPackand prefixed menu resources. - Updates menu templates and full-theme integration.
- Adds SampleApp demos and automated contract tests.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
tests/Devolutions.AvaloniaControls.VisualTests/MacOsMenuPackContractTests.cs |
Tests menu-pack resources and variants. |
src/Devolutions.AvaloniaTheme.MacOS/ThemeRoot.axaml |
Loads menu resources globally. |
src/Devolutions.AvaloniaTheme.MacOS/README.md |
Documents menu-pack usage. |
src/Devolutions.AvaloniaTheme.MacOS/Internal/MenuResourceAliasBuilder.cs |
Builds variant-specific token aliases. |
src/Devolutions.AvaloniaTheme.MacOS/Internal/MacOsThemeWithGlobalStyles.axaml.cs |
Registers menu aliases. |
src/Devolutions.AvaloniaTheme.MacOS/Internal/MacOsTheme.axaml.cs |
Registers menu aliases. |
src/Devolutions.AvaloniaTheme.MacOS/Controls/MenuPackStyles.cs |
Adds the code-based helper API. |
src/Devolutions.AvaloniaTheme.MacOS/Controls/MenuPack.styles.axaml |
Defines standalone pack composition. |
src/Devolutions.AvaloniaTheme.MacOS/Controls/MenuPack.Separator.styles.axaml |
Styles menu separators. |
src/Devolutions.AvaloniaTheme.MacOS/Controls/MenuItem.axaml |
Uses prefixed menu tokens. |
src/Devolutions.AvaloniaTheme.MacOS/Controls/MenuFlyoutPresenter.axaml |
Uses prefixed menu tokens. |
src/Devolutions.AvaloniaTheme.MacOS/Controls/Menu.axaml |
Uses prefixed menu tokens. |
src/Devolutions.AvaloniaTheme.MacOS/Controls/MacOsMenuPack.cs |
Implements variant-aware menu pack. |
src/Devolutions.AvaloniaTheme.MacOS/Controls/ContextMenu.axaml |
Uses prefixed menu tokens. |
src/Devolutions.AvaloniaTheme.MacOS/Accents/MenuResources.axaml |
Defines the menu token contract. |
samples/SampleApp/PageCatalog/page-catalog.jsonc |
Registers menu-pack demo pages. |
samples/SampleApp/DemoPages/MenuPackMenuFlyoutDemoContent.axaml.cs |
Adds reusable flyout demo code-behind. |
samples/SampleApp/DemoPages/MenuPackMenuFlyoutDemoContent.axaml |
Adds reusable flyout content. |
samples/SampleApp/DemoPages/MenuPackMenuFlyoutDemo.axaml |
Uses reusable flyout content. |
samples/SampleApp/DemoPages/MenuPackMenuDemoContent.axaml.cs |
Adds reusable menu demo code-behind. |
samples/SampleApp/DemoPages/MenuPackMenuDemoContent.axaml |
Adds reusable menu content. |
samples/SampleApp/DemoPages/MenuPackMenuDemo.axaml |
Uses reusable menu content. |
samples/SampleApp/DemoPages/MenuPackIsolationDemoContent.axaml.cs |
Adds isolation demo code-behind. |
samples/SampleApp/DemoPages/MenuPackIsolationDemoContent.axaml |
Adds shared isolation demo. |
samples/SampleApp/DemoPages/MenuPackContextMenuDemoContent.axaml.cs |
Adds reusable context-menu code-behind. |
samples/SampleApp/DemoPages/MenuPackContextMenuDemoContent.axaml |
Adds reusable context-menu content. |
samples/SampleApp/DemoPages/MenuPackContextMenuDemo.axaml |
Uses reusable context-menu content. |
samples/SampleApp/DemoPages/MenuPackAbout.axaml |
Reuses the isolation demo. |
samples/SampleApp/DemoPages/MacMenuPackMenuFlyoutDemo.axaml.cs |
Applies the macOS pack. |
samples/SampleApp/DemoPages/MacMenuPackMenuFlyoutDemo.axaml |
Adds the macOS flyout demo. |
samples/SampleApp/DemoPages/MacMenuPackMenuDemo.axaml.cs |
Applies the macOS pack. |
samples/SampleApp/DemoPages/MacMenuPackMenuDemo.axaml |
Adds the macOS menu demo. |
samples/SampleApp/DemoPages/MacMenuPackContextMenuDemo.axaml.cs |
Applies the macOS pack. |
samples/SampleApp/DemoPages/MacMenuPackContextMenuDemo.axaml |
Adds the macOS context-menu demo. |
samples/SampleApp/DemoPages/MacMenuPackAbout.axaml.cs |
Applies the macOS pack. |
samples/SampleApp/DemoPages/MacMenuPackAbout.axaml |
Documents and demonstrates the pack. |
samples/SampleApp/App.axaml.cs |
Clears stale macOS variant overrides. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 46 out of 46 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/Devolutions.AvaloniaTheme.MacOS/Accents/MenuResources_LiquidGlass.axaml:40
- This hard-coded hover brush stops the LiquidGlass pack from following a customized
SystemAccentColor, while the classic pack and full LiquidGlass theme remain accent-aware. As a result, consumers using a non-default accent get the default blue hover state rather than the matching macOS menu appearance. Define pack-local light/dark accent-adjustment resources and derive this brush fromSystemAccentColor, then cover a non-default accent in the parity test.
<SolidColorBrush x:Key="MacOsMenuItemPointerOverBackgroundBrush" Color="#005eca" Opacity="0.64" />
tests/Devolutions.AvaloniaControls.VisualTests/MenuTokenNames.cs:20
MacOsMenuFontFamilyis part of the new prefixed resource contract and is consumed by all menu item styles, but it is missing fromAll. The parity test therefore cannot detect this required resource disappearing or diverging. Add it alongside the other typography tokens.
tests/Devolutions.AvaloniaControls.VisualTests/MacOsMenuPackContractTests.cs:28- This duplicates the contract list already introduced in
MenuTokenNames.All. Keeping two independently maintained lists lets the resolution/leak tests and parity test silently cover different resources—the omitted font-family token already demonstrates that gap. Use the shared list here so every contract test covers the same keys.
There was a problem hiding this comment.
🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉
Integration Details
{
"deliveryId": "599d2590-9d0e-11f1-9df6-450936464777",
"headSha": "5e19dc78c33d741ddb03b345f3e356975be65446",
"reviewer": "copilot-pull-request-reviewer[bot]"
}
Exposes the MacOS menu styling as a standalone, externally consumable pack, so apps can import menu styling where they might not want to use the full
DevolutionsDevExpressTheme(e.g. areas with brand-based styling) to provide consistent Menu chrome.Follows on from #597 (DevExpress menu pack). The Linux menu pack will follow in a separate PR,.
Minimal usage
Or from code, to scope the pack to a single view:
Why this differs from the DevExpress pack
The DevExpress pack (#597) is consumed as a plain style include:
That works there because DevExpress has one menu appearance, so a static AXAML file is the whole story.
MacOS has two: classic and LiquidGlass, chosen at runtime from the OS version (
macOS 26+→ LiquidGlass). That decision needs code, so the pack's entrypoint is aStylessubclass (MacOsMenuPack) that performs the detection and merges the matching resources. Everything else — the underlyingMenuPack.styles.axaml, the prefixed token contract, the "no template duplication" rule — mirrors #597.Practical consequence: including the styles URI directly is not equivalent and yields classic-only menus on macOS 26+.
MenuPack.styles.axamlcarries only the classic defaults;MacOsMenuPackis what adds the variant on top. The README calls this out, and a test (Readme_documented_xaml_element_follows_liquid_glass) pins the documented path so it can't silently rot.The pack follows the OS independently of the host theme, so under a Fluent (or DevExpress, or Linux) host on macOS 26 the menus still render LiquidGlass — matching what the full MacOS theme would do.
Scoping
As with #597, only the prefixed
MacOsMenu*tokens are published. The pack does not merge the MacOS theme dictionaries, so buttons, text boxes and separators elsewhere in a consuming app keep their host theme's resources.Pack_does_not_publish_non_menu_macos_resourcesguards this.Because the pack pins its LiquidGlass values while the full theme derives them,
Pack_menu_tokens_match_the_full_themecompares all 46 menu tokens across classic/LiquidGlass × Light/Dark to stop the two drifting apart.One documented trade-off: the LiquidGlass hover tint is pinned rather than derived, because the full theme runs the system accent through
OklchAdjustmentConverter, which the pack can't reuse without importing the theme dictionaries. It matches the default accent; with a custom macOS accent colour the classic hover tint follows it but the LiquidGlass one stays at the default.Host-leak fixes found along the way
Proving the isolation surfaced three ways a host theme could still reach into pack-owned menus. All are fixed, each with a regression test verified to fail without its fix:
MacOsMenuFontFamilyand blocks inheritedLineHeight.Styles, and a Style setter outranks a ControlTheme setter, so DevExpress resized every row (26px vs 24px). The pack now applies row geometry at Style level too, mirroring how DevExpress does it.MenuSeparatorAlignmentBehaviorin the DevExpress theme assignsMarginas a local value (which outranks every style) and its selector wasn't scoped, so it re-positioned separators inside pack-owned menus, making popups ~3cm shorter. The behaviour is now scoped to menus that theme actually owns.Fix 3 touches the DevExpress theme rather than this pack, because a local value can't be overridden from the outside. Scoping it at the source also means the upcoming Linux menu pack gets the same protection without needing its own opt-out. Native DevExpress menus are unaffected (verified: separator margins and totals unchanged).
Notes
InvalidCastExceptionwhen two themes are initialised in one process is documented in.claude/docs/planning/upcoming/2026_08_20__Headless_InvalidCast_With_Two_Themes.mdfor a separate session. It's confirmed not a regression from this PR (reproduces on unmodifiedmaster), but it's why one geometry test asserts the style contract structurally instead of measuring a live popup under two hosts.