Skip to content

Expose MacOS menu styles for use independent of theme - #624

Merged
Anna Malchow-Perryman (apman) merged 6 commits into
masterfrom
menu-pack-macOS
Aug 21, 2026
Merged

Expose MacOS menu styles for use independent of theme#624
Anna Malchow-Perryman (apman) merged 6 commits into
masterfrom
menu-pack-macOS

Conversation

@apman

@apman Anna Malchow-Perryman (apman) commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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

<Application xmlns:macos="clr-namespace:Devolutions.AvaloniaTheme.MacOS.Controls;assembly=Devolutions.AvaloniaTheme.MacOS"
             ...>
  <Application.Styles>
    <!-- Required prerequisite -->
    <FluentTheme />

    <!-- MacOS menu pack -->
    <macos:MacOsMenuPack />
  </Application.Styles>
</Application>

Or from code, to scope the pack to a single view:

using Devolutions.AvaloniaTheme.MacOS.Controls;

MacMenuPackStyles.ApplyTo(this.Styles);

Why this differs from the DevExpress pack

The DevExpress pack (#597) is consumed as a plain style include:

<StyleInclude Source="avares://Devolutions.AvaloniaTheme.DevExpress/Controls/MenuPack.styles.axaml" />

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 a Styles subclass (MacOsMenuPack) that performs the detection and merges the matching resources. Everything else — the underlying MenuPack.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.axaml carries only the classic defaults; MacOsMenuPack is 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_resources guards this.

Because the pack pins its LiquidGlass values while the full theme derives them, Pack_menu_tokens_match_the_full_theme compares 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:

  1. Typography — menus inherited the host font (DevExpress rendered MacOS menus in Segoe UI), changing text metrics and row heights. The pack now owns MacOsMenuFontFamily and blocks inherited LineHeight.
  2. Row geometry — host themes set menu metrics with 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.
  3. Separator alignmentMenuSeparatorAlignmentBehavior in the DevExpress theme assigns Margin as 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

  • A pre-existing headless InvalidCastException when two themes are initialised in one process is documented in .claude/docs/planning/upcoming/2026_08_20__Headless_InvalidCast_With_Two_Themes.md for a separate session. It's confirmed not a regression from this PR (reproduces on unmodified master), but it's why one geometry test asserts the style contract structurally instead of measuring a live popup under two hosts.

@apman
Anna Malchow-Perryman (apman) marked this pull request as ready for review August 20, 2026 20:36
Copilot AI balanced review requested due to automatic review settings August 20, 2026 20:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Exposes standalone macOS menu styling with platform-specific resources, documentation, demos, and contract tests.

Changes:

  • Adds MacOsMenuPack and 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.

Comment thread src/Devolutions.AvaloniaTheme.MacOS/Controls/MenuPackStyles.cs
Comment thread src/Devolutions.AvaloniaTheme.MacOS/Controls/MacOsMenuPack.cs Outdated
Copilot AI review requested due to automatic review settings August 21, 2026 02:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from SystemAccentColor, 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

  • MacOsMenuFontFamily is part of the new prefixed resource contract and is consumed by all menu item styles, but it is missing from All. 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.

@randy-but-a-ro randy-but-a-ro Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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]"
}

@apman
Anna Malchow-Perryman (apman) merged commit fdae270 into master Aug 21, 2026
1 check passed
@apman
Anna Malchow-Perryman (apman) deleted the menu-pack-macOS branch August 21, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants