Skip to content

Consume a ThemeContribution's Styles as a library selector-style leg#12

Merged
mstrobel merged 1 commit into
developfrom
worktree-selector-resolver
Jul 9, 2026
Merged

Consume a ThemeContribution's Styles as a library selector-style leg#12
mstrobel merged 1 commit into
developfrom
worktree-selector-resolver

Conversation

@mstrobel

@mstrobel mstrobel commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Problem

A control library could contribute Type-keyed control themes + resources via ThemeContributions, but its ResourceDictionary.Styles selector channel was explicitly not consumed (§11.8). So a library had no way to ship app-level selector styles — only implicit, ControlThemeKey-resolved themes. Adding arbitrary styles to a contribution silently did nothing.

Fix

The StyleEngine now gathers each registered contribution's top-level Styles as a third leg of the Theme layer, ordered:

  • above the BuiltIn framework leg (a library refines the framework default), and
  • below the app.Theme leg and UIApplication.Styles (the app always wins).

Each contribution gets a distinct 4096-wide order slot in registration order, so a later-registered library wins a same-target tie (the resource-tier "last wins", applied to styles); the slot is clamped one stride below AppThemeOrderBase so a contribution can never reach the app-theme leg no matter how many register.

  • A contribution shipping only Type-keyed themes (no top-level Styles) is skipped; the whole leg short-circuits when nothing is contributed — zero cost in the common case.
  • Late registration (rare — contributions normally register at module init, before any app) re-arms the engine app-wide via OnThemeStylesInvalidated, alongside the existing resource catch-all.
  • Only the dictionary's own top-level Styles are consumed; Styles nested in a contribution's MergedDictionaries are not flattened (v1) — same contract as the app.Theme leg. The ancestor-interesting fast path leaves the whole Theme layer out (BuiltIn + app.Theme + contributions alike), unchanged.

On the type-resolver question

This path needs no ISelectorTypeResolver: a library contributing styles in code builds typed selectors — Selectors.OfType(typeof(BarButton)) / Selectors.Is<Ribbon>() — no string parsing. The resolver plumbing (for string/XAML-authored selectors) stays a separate, deferred concern.

Docs / tests

Reversed the §11.8 note on ThemeContributions and updated ResourceDictionary.Styles. Section15_ContributedStyles covers match+apply, App overrides, app.Theme overrides, later-contribution-wins, no-Styles-skipped, and late-registration re-arm — with a global-registry save/restore hook for isolation. Mutation-verified (removing the leg fails the match test). UI 2957 / Bars 296 green.

🤖 Generated with Claude Code

…tyle leg

A control library could register keyed control themes + resources via
ThemeContributions, but its ResourceDictionary.Styles selector channel was
explicitly NOT consumed (§11.8) — so a library had no way to ship app-level
selector styles, only Type-keyed themes.

The StyleEngine now gathers each registered contribution's top-level Styles as a
third leg of the Theme layer, ordered:
- ABOVE the BuiltIn framework leg (a library refines the framework default), and
- BELOW the app.Theme leg and UIApplication.Styles (the app always wins).

Each contribution gets a distinct 4096-wide order slot in registration order, so a
later-registered library wins a same-target tie (the resource-tier "last wins",
applied to styles); the slot is clamped one stride below AppThemeOrderBase so a
contribution can never reach the app-theme leg. A contribution that ships only
Type-keyed themes (no top-level Styles) is skipped, and the whole leg short-circuits
when nothing is contributed — zero cost in the common case.

Late registration (rare — contributions normally register at module init) re-arms
the engine app-wide via OnThemeStylesInvalidated, alongside the existing resource
catch-all.

Contributed styles reference the library's own controls with TYPED selectors
(Selectors.OfType(typeof(BarButton))), so no string parsing or ISelectorTypeResolver
is needed for this path.

Only the dictionary's own top-level Styles are consumed; Styles nested in a
contribution's MergedDictionaries are not flattened (v1) — same contract as the
app.Theme leg. The ancestor-interesting fast path leaves the whole Theme layer out
(BuiltIn + app.Theme + contributions alike), unchanged.

Docs: reversed the §11.8 note on ThemeContributions and updated ResourceDictionary.Styles.
Tests: Section15_ContributedStyles — match+apply, App overrides, app.Theme overrides,
later-contribution-wins, no-Styles-skipped, late-registration re-arm (a global-registry
save/restore hook keeps them isolated). Mutation-verified. UI 2957 / Bars 296 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175wtwXrafD4fWAZ3i5THu6
@mstrobel mstrobel merged commit 300e3ad into develop Jul 9, 2026
3 checks passed
@mstrobel mstrobel deleted the worktree-selector-resolver branch July 9, 2026 23:52
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