Skip to content

Show context-menu separators only where the renderer draws them - #565

Merged
kdroidFilter merged 2 commits into
nucleus-2.5from
fix/context-menu-divider-capability
Aug 17, 2026
Merged

Show context-menu separators only where the renderer draws them#565
kdroidFilter merged 2 commits into
nucleus-2.5from
fix/context-menu-divider-capability

Conversation

@kdroidFilter

@kdroidFilter kdroidFilter commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Spellcheck no longer replaces LocalContextMenuRepresentation. The hand-rolled Compose menu that existed only to paint a hairline for an empty-label sentinel is gone (187 lines), along with SpellcheckContextMenuSeparator and LocalSpellcheckMenuSeparator. That popup used hardcoded colors driven by isSystemInDarkTheme(), dropped keyboard navigation and a11y, and only engaged when the clicked word was misspelled — so the menu looked different between two right clicks in the same field.
  • Renderers now declare the capability instead of consumers inferring it: LocalContextMenuDivider (nullable, default null) is provided by NativeContextMenuProvider (NucleusContextMenuDivider, drawn by NSMenu on macOS and by the Fluent / Adwaita / Breeze flyouts elsewhere) and by ProvideJewelSpellcheckMenu (Jewel's ContextMenuDivider). With Compose's own representation there is no divider to draw, so none is emitted. Apps wanting dividers outside Jewel already have a supported path: nativeContextMenu = true.
  • spellcheckMenuSections / spellcheckContextMenuItems / NucleusSpellcheckInstaller.menuItems take a nullable separator; ContextMenuEntry.kt no longer imports the spellcheck package.
  • NativeContextMenuRepresentation normalizes interpreted entries — trims leading/trailing separators, collapses runs, recurses into submenus — since menu items come from several independent contributors (field Cut/Copy/Paste, app extras, spellcheck).
  • Fixes the Adwaita dark separator: it painted popover_shade_color (25 % black), so the rule came out darker than the menu surface. libadwaita uses separator { background: $border_color } with $border_color: color-mix(in srgb, currentColor var(--border-opacity), transparent) and --border-opacity: 15% — i.e. 15 % of the text color: white on a dark menu, RGB(0 0 6 / 80%) premultiplied to 12 % on a light one (was 7 %). Breeze and Fluent were already correct.
  • apiDump refreshed. The removed symbols were never published (latest release is 2.4.4), so nothing is deprecated.

Test plan

  • ./gradlew :nucleus-application:check :decorated-window-jewel:check — detekt, ktlint, apiCheck, tests
  • ./gradlew :examples:jewel-demo:test :examples:jewel-demo:ktlintCheck
  • New ContextMenuDividerCapabilityTest — no divider published by default, NucleusContextMenuDivider under the OS-looking provider, none when it is disabled
  • New ContextMenuFlyoutSeparatorTest — Adwaita's 15 %-of-currentColor rule, and dark separators lighten / light separators darken the surface across Adwaita, Breeze and Fluent
  • SpellcheckInstallerTest — no separator and no stand-in row when the renderer cannot draw one, for both placements
  • ./gradlew :examples:tao-demo:compileKotlin :examples:nucleus-demo:compileKotlin
  • Eyeball a GNOME dark session: separator is now a light hairline in the Adwaita flyout

Note: top placement keeps Jewel chrome failed on nucleus-2.5 before this branch — its assertSame on the representation only held while SpellChecker had no warm session. It now probes that the ambient chrome is what ultimately draws, for both placements.

Spellcheck used to inject an empty ContextMenuItem sentinel and then
replace LocalContextMenuRepresentation with a hand-rolled Compose menu
just to paint a hairline for it. That swapped the app's chrome for
hardcoded colors driven by isSystemInDarkTheme(), lost keyboard
navigation and accessibility, and only kicked in when the clicked word
was misspelled — so the menu changed appearance between two right
clicks in the same field.

Renderers now publish their own divider through LocalContextMenuDivider:
NativeContextMenuProvider provides NucleusContextMenuDivider (drawn by
NSMenu on macOS and by the Fluent / Adwaita / Breeze flyouts elsewhere),
ProvideJewelSpellcheckMenu provides Jewel's ContextMenuDivider, and the
default is null. Compose's own representation cannot draw a divider, so
the spellcheck sections are emitted without one instead of inventing
chrome. Apps that want dividers without Jewel already have a supported
path: nativeContextMenu = true.

Also normalize interpreted entries in NativeContextMenuRepresentation,
trimming leading and trailing separators and collapsing runs, since menu
items come from several independent contributors.

The jewel-demo chrome test asserted representation identity, which only
held while SpellChecker had no warm session; it now probes that the
ambient chrome is what ultimately draws.
The Adwaita flyout painted popover_shade_color, which is 25% black in
the dark variant, so menu separators came out darker than the menu
surface. libadwaita draws them from $border_color instead:

  separator { background: $border_color; }
  $border_color: color-mix(in srgb, currentColor var(--border-opacity), transparent)
  --border-opacity: 15%

currentColor inside a menu is popover_fg_color, so the rule is 15% of
the text color: white on a dark menu, and RGB(0 0 6 / 80%) premultiplied
down to 12% on a light one (the light variant was at 7%).
popover_shade_color stays what libadwaita uses it for, scroll
undershoots.

Breeze and Fluent already lightened their dark surfaces; the new test
pins that invariant for the three themes plus the 15% rule for Adwaita.
@kdroidFilter
kdroidFilter merged commit d84f1af into nucleus-2.5 Aug 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant