Namespaces: TheTechIdea.Beep.WPF.Controls.BeepChrome  ·  TheTechIdea.Beep.WPF.Controls.Styling.BeepControlStyle  ·  TheTechIdea.Beep.WPF.Controls.ThemeManagement.WpfThemesManager

Theme vs Skin

These are two different things with one relation: a theme is the theme for a skin. Get this right before touching anything under Themes/.

TermWhat it isLives in
Skin The design system itself — one entry of the BeepControlStyle enum (Material3, Fluent, Apple, Dracula, …). Holds one keyed style per control. Themes/Styles/{Skin}.xaml
Theme The colors and fonts of a skin, defined across all controls. Each skin bakes its paired theme's palette (AppleWpfTheme's accent #007AFF is Apple.xaml's AppleAccent). Themes/Themes/

Because the skin bakes the theme's palette, naming a theme selects the matching skin, and setting the skin is what puts the theme's colors and fonts on every control. This mirrors the WinForms pattern where the ControlStyle setter drives the Theme.

Also under Themes/, distinct from both:
  • ControlDefaults.xaml — per-control chrome tokens (BeepButton.Background, BeepButton.CornerRadius, …).
  • Light.xaml / Colors.xaml — color primitives (BeepAccentBrush, BeepSurfaceBrush, …).
  • Generic.xaml — the WPF default-style entry point (via [assembly: ThemeInfo]), where DefaultStyleKeyProperty resolves. It merges the token chain and pins Material3 as the frozen startup default skin.

BeepChrome attached properties

BeepChrome is a static class of attached DPs usable on any DependencyObject — the chrome "mixin" that any WPF primitive can host. A control opts in by exposing CLR wrappers (ControlStyle/Theme) that forward to these attached properties.

Attached DPTypePurpose
ControlStyleBeepControlStyleThe skin. Inherits; default None. Change fires OnControlStyleChanged, which loads the per-skin RD and applies that skin's keyed style.
ThemestringInherits; default "Light". Change fires OnThemeChanged, which resolves the theme name to its paired skin and applies it.
IsBorderAffectedByThemeboolWhether the border chrome follows the theme. Inherits; default true.
IsShadowAffectedByThemeboolWhether the drop shadow follows the theme. Inherits; default true.
IsRoundedAffectedByThemeboolWhether the corner radius follows the theme. Inherits; default true.
HasErrorboolValidation flag the template's error chrome reacts to.
HoverBackgroundBrushPer-instance hover-state override.
PressedBackgroundBrushPer-instance pressed-state override.
FocusBackgroundBrushPer-instance focus-state override.
DisabledBackgroundBrushPer-instance disabled-state override.
Background / Foreground / BorderBrush / BorderThickness / CornerRadius / PaddingBrush / Thickness / CornerRadiusInheriting common-chrome overrides. Use sparingly — the template + skin are normally enough.
ComponentNamestringDiagnostic identifier for a chrome element.
Per-instance state brushes are overrides, not the norm. Hover / pressed / focus / disabled backgrounds are almost always supplied by the skin's keyed style and the ControlDefaults.xaml tokens. Set a BeepChrome.*Background only when a single instance must deviate. There is no ErrorIndicatorVisibility attached DP on BeepChrome; validation chrome is driven by HasError plus the field presenter's own error state.

A control opts in with plain CLR wrappers over the attached DPs:

public BeepControlStyle ControlStyle
{
    get => BeepChrome.GetControlStyle(this);
    set => BeepChrome.SetControlStyle(this, value);
}
public string Theme
{
    get => BeepChrome.GetTheme(this);
    set => BeepChrome.SetTheme(this, value);
}

How ControlStyle drives the look

ControlStyle is an inherited attached property. Set it once at a window root and every control in the tree re-skins on its own:

<Window xmlns:beep="clr-namespace:TheTechIdea.Beep.WPF.Controls;assembly=TheTechIdea.Beep.WPF.Controls"
        beep:BeepChrome.ControlStyle="Dracula">
    <!-- every Beep control below inherits ControlStyle=Dracula -->
</Window>

When the value changes on an element, BeepChrome.OnControlStyleChanged runs a two-step sequence:

  1. EnsureSkinLoaded(style) — merges pack://application:,,,/TheTechIdea.Beep.WPF.Controls;component/Themes/Styles/{skin}.xaml into Application.Resources, removing the previously active skin by reference (the skins share no marker key, so a key-probe would leak a dictionary on every switch). It no-ops when that skin is already active — the property inherits, so the callback fires once per element, but the skin must load only once per style.
  2. ApplySkinStyle(d, style) — walks up the element's type hierarchy and assigns the skin's keyed style for that control type, e.g. a BeepButton under Material3 gets Material3BeepButtonStyle. A derived control falls back to its base control's skin style.
None is a real, meaningful state. The default is BeepControlStyle.None, not a real skin. A DP callback only fires on a change, so if the default were (say) Material3, explicitly selecting Material3 would be a no-op and skin nothing. With None, setting it clears any local style and the control falls back to the implicit default skin declared in Generic.xaml.
ControlTemplate parts are skipped; DataTemplate content is not. ApplySkinStyle returns early when TemplatedParent is a templated control (the template already owns those elements — e.g. the BeepImage icon slots inside a BeepTextBox template). But when TemplatedParent is a ContentPresenter (DataTemplate content — cards, list rows), it is restyled; skipping it would freeze every ItemTemplate's Beep controls on the startup default.

The chrome-token system

No control template hardcodes a color. Every visual surface resolves from a chrome token via DynamicResource, and the tokens live in one place.

Themes/ControlDefaults.xaml is the single source of the per-control chrome tokens. Keys are named Beep{Control}.{Property} and bridge to the shared style tokens:

<!-- ControlDefaults.xaml — the per-control token layer -->
<SolidColorBrush x:Key="BeepButton.Background"       Color="{DynamicResource BeepStyleAccentBrush}" />
<SolidColorBrush x:Key="BeepButton.Foreground"       Color="{DynamicResource BeepStyleAccentForegroundBrush}" />
<SolidColorBrush x:Key="BeepButton.HoverBackground"  Color="{DynamicResource BeepStyleAccentBrush}" />
<SolidColorBrush x:Key="BeepButton.PressedBackground" Color="{DynamicResource BeepStyleAccentBrush}" />
<CornerRadius    x:Key="BeepButton.CornerRadius">{DynamicResource ControlCornerRadius}</CornerRadius>
<Thickness       x:Key="BeepButton.Padding">{DynamicResource ControlPadding}</Thickness>

A control template references these tokens — never a literal color:

<Border Background="{DynamicResource BeepButton.Background}"
        CornerRadius="{DynamicResource BeepButton.CornerRadius}"
        Padding="{DynamicResource BeepButton.Padding}">
    <ContentPresenter .../>
</Border>

The token values in ControlDefaults.xaml are fallbacks. They bridge to the shared style tokens (BeepStyle*Brush in Themes/Styles/_Tokens.xaml), which in turn bridge to the color primitives in Light.xaml. The merge order in Generic.xaml is exactly this token dependency chain, each layer resolving against the one above:

LayerFileExample key
1 · color primitivesThemes/Light.xamlBeepAccentBrush
2 · style tokensThemes/Styles/_Tokens.xamlBeepStyleAccentBrush
3 · per-control chrome tokensThemes/ControlDefaults.xamlBeepButton.Background
4 · the active skinThemes/Styles/{Skin}.xamlMaterial3BeepButtonStyle + palette
A template referencing a missing key renders no chrome. If a DynamicResource Beep{Name}.{Token} lookup finds nothing, it returns null and that surface stays unpainted. Every token a template references must exist in ControlDefaults.xaml (or be overridden with the same key name in a per-skin RD). Don't put tokens in individual Themes/Controls/{Name}.xaml files — they must be in the runtime-merged token dictionary to resolve.

The ~67 skins & their tokens

Each entry of the BeepControlStyle enum maps 1:1 to a Themes/Styles/{name}.xaml file — roughly 67 skins spanning design systems from Material3, Fluent2, iOS15 and MacOSBigSur through Apple, Dracula, Nord, Cyberpunk, Tokyo, Solarized, OneDark, GruvBox and Claymorphism. BeepChrome.GetDesignSystemName is the enum → filename map.

Every skin declares the same semantic token set, keyed {Skin}{Token}, from which its per-control styles paint directly. There are 12 color tokens and a small set of geometry tokens. From Dracula.xaml:

<!-- 12 color tokens -->
<SolidColorBrush x:Key="DraculaSurface"           Color="#282A36"/>
<SolidColorBrush x:Key="DraculaOnSurface"         Color="#F8F8F2"/>
<SolidColorBrush x:Key="DraculaOnSurfaceVariant"  Color="#CFCFEA"/>
<SolidColorBrush x:Key="DraculaAccent"            Color="#BD93F9"/>
<SolidColorBrush x:Key="DraculaBorder"            Color="#44475A"/>
<SolidColorBrush x:Key="DraculaHoverSurface"      Color="#3E3A53"/>
<SolidColorBrush x:Key="DraculaPressedSurface"    Color="#4F4569"/>
<SolidColorBrush x:Key="DraculaDisabled"          Color="#7F8185"/>
<SolidColorBrush x:Key="DraculaError"             Color="#FF5555"/>
<SolidColorBrush x:Key="DraculaWarning"           Color="#F1FA8C"/>
<SolidColorBrush x:Key="DraculaSuccess"           Color="#50FA7B"/>
<SolidColorBrush x:Key="DraculaInfo"              Color="#8BE9FD"/>

<!-- geometry tokens -->
<CornerRadius x:Key="DraculaControlRadius">8</CornerRadius>
<Thickness    x:Key="DraculaControlBorder">1</Thickness>
<Thickness    x:Key="DraculaControlPad">12,8,12,8</Thickness>
TokenRole
{Skin}SurfaceBase control background.
{Skin}OnSurfacePrimary text/icon on the surface.
{Skin}OnSurfaceVariantMuted / secondary text (OnSurface blended ~40% toward Surface).
{Skin}AccentPrimary accent (selection, focus, buttons).
{Skin}BorderControl border color.
{Skin}HoverSurfaceSurface tinted toward Accent for hover.
{Skin}PressedSurfaceDeeper Accent tint for pressed.
{Skin}DisabledDisabled foreground/surface.
{Skin}Error / {Skin}Warning / {Skin}Success / {Skin}InfoSemantic status colors.

Plus geometry tokens — {Skin}ControlRadius, {Skin}ControlBorder, {Skin}ControlPad (and a font-size token) — so a control's shape and spacing are per-skin too. A skin's keyed control styles bind these {Skin}* resources directly with StaticResource; that is why "the skins are the theme." BeepChrome.FindSkinBrush resolves a semantic name (e.g. "Error") against the active skin ({Skin}Error) for code that needs a palette brush at runtime.

WpfThemesManager — the single skin authority

WpfThemesManager (namespace TheTechIdea.Beep.WPF.Controls.ThemeManagement) is the one entry point for switching skin app-wide — the WPF counterpart of the WinForms BeepThemesManager. Route every skin change through it.

MemberWhat it does
CurrentStyleThe active skin (BeepControlStyle). Single source of truth; defaults to Material3.
CurrentThemeNameThe active theme (palette) name; defaults to "Light".
ApplyStyle(style)The one call to switch skin. Stamps BeepChrome.ControlStyle on every open window (inherits → cascades down each tree; the first stamp merges the skin RD so DynamicResources re-resolve), then raises StyleChanged.
StyleChangedEvent raised after the skin changes — for views/dialogs/popups in separate visual trees, or created later, to re-stamp themselves.
ThemeChanged / RaiseThemeChanged(name)Palette-name change notification.
// Switch skin for the whole app
WpfThemesManager.ApplyStyle(BeepControlStyle.Material3);

// A dialog/popup/toast in a SEPARATE visual tree must stamp itself at creation —
// ApplyStyle only stamps windows already open, and separate trees don't inherit.
BeepChrome.SetControlStyle(root, WpfThemesManager.CurrentStyle);
Every new visual tree must be stamped. A Window, dialog, toast or Popup with generated content does not inherit ControlStyle from the main window. Stamp it with WpfThemesManager.CurrentStyle at creation, or subscribe to StyleChanged and re-stamp. Generic.xaml's implicit styles are the frozen startup default (Material3), not a runtime fallback for elements alive after a skin switch.

Known issue: never assign a *Brush to a SolidColorBrush.Color

WPF rejects Brush → Color and the resource renders black. A SolidColorBrush.Color property takes a Color, not a Brush. Assigning a *Brush resource to it — or binding a Brush's Color onto a resource SolidColorBrush — silently fails: the Color stays its default and the brush paints black.
<!-- BROKEN — BeepOnSurfaceBrush is a Brush, not a Color → renders black -->
<SolidColorBrush x:Key="BeepStyleOnSurfaceBrush" Color="{DynamicResource BeepOnSurfaceBrush}" />

<!-- ALSO BROKEN — a Binding onto a resource Freezable's Color doesn't establish -->
<SolidColorBrush x:Key="BeepStyleOnSurfaceBrush"
                 Color="{Binding Color, Source={StaticResource TerminalOnSurface}}" />

<!-- CORRECT — a literal hex Color, or a Color (not Brush) resource -->
<SolidColorBrush x:Key="BeepStyleOnSurfaceBrush" Color="#1A1A1A" />

This defect made the shared BeepStyle*Brush tokens render black, which cascaded to every consumer resolving color through them — most visibly the BeepListItem fallback variant, so BeepComboBox drop-downs and BeepTreeView nodes showed black text. Controls binding {StaticResource {Skin}OnSurface} directly were unaffected. The rule: in Themes/, a SolidColorBrush.Color must be a literal hex or a Color resource — never a *Brush, and never a Binding to one. See _plan/BEEPSTYLE_COLOR_PIPELINE_FIX_PLAN.md for the full analysis and remediation (Phases A–D).