diff --git a/docs/_pipeline/apps/styling/App.cs b/docs/_pipeline/apps/styling/App.cs index 9d4903797..304ab4ecd 100644 --- a/docs/_pipeline/apps/styling/App.cs +++ b/docs/_pipeline/apps/styling/App.cs @@ -168,6 +168,15 @@ public override Element Render() } // +// +class FontFamilyResourceExample : Component +{ + public override Element Render() => + Button("Code").Resources(r => r + .Set("ContentControlThemeFontFamily", new Microsoft.UI.Xaml.Media.FontFamily("Consolas"))); +} +// + // class CustomResourceExample : Component { diff --git a/docs/_pipeline/templates/styling.md.dt b/docs/_pipeline/templates/styling.md.dt index 4ad626662..0e8dd2a0f 100644 --- a/docs/_pipeline/templates/styling.md.dt +++ b/docs/_pipeline/templates/styling.md.dt @@ -206,12 +206,28 @@ pressed, disabled) automatically respect your overrides: ![Lightweight styling](screenshot://styling/lightweight-styling) -`ResourceBuilder` supports `.Set(key, ThemeRef)` for theme-reactive -overrides, `.Set(key, string)` for hex colors, `.Set(key, double)` for -numeric values, and `.Set(key, CornerRadius)` for corner radius. +`ResourceBuilder.Set` has explicit overloads for `ThemeRef`, `Brush`, +color strings, `double`, `CornerRadius`, `Thickness`, `FontWeight`, +`FontFamily`, `bool`, and `GridLength`. Use `Thickness` for padding +or border thickness, and `FontWeight` / `FontFamily` for typography resources. +Literal values retain their types; `ThemeRef` overrides re-resolve on theme changes. + +`ResourceBuilder.SetColor(key, color)` stores a literal `Color`, not a brush. +Use it only for Color-typed resources; brush-typed keys such as `ButtonBackground` +need the string or `Brush` overload of `Set`. The string overload +still parses a hex or named color into a brush; it does not store arbitrary +text. Integer literals continue to select the `double` overload, so pass the +value type expected by the WinUI resource key rather than relying on conversion. Overrides cascade to child elements through WinUI's resource dictionary hierarchy. +Font-family resource keys require a `FontFamily` object, not a string: + +```csharp snippet="styling/font-family-resource" +``` + +Calling `Set(key, "Consolas")` would select the color-string overload instead. + ## Custom resource access For brushes Reactor hasn't surfaced as a typed property, `Theme.Ref()` diff --git a/docs/guide/styling.md b/docs/guide/styling.md index fa5bf8f82..6ba14f1f1 100644 --- a/docs/guide/styling.md +++ b/docs/guide/styling.md @@ -353,12 +353,34 @@ class LightweightStylingExample : Component ![Lightweight styling](images/styling/lightweight-styling.png) -`ResourceBuilder` supports `.Set(key, ThemeRef)` for theme-reactive -overrides, `.Set(key, string)` for hex colors, `.Set(key, double)` for -numeric values, and `.Set(key, CornerRadius)` for corner radius. +`ResourceBuilder.Set` has explicit overloads for `ThemeRef`, `Brush`, +color strings, `double`, `CornerRadius`, `Thickness`, `FontWeight`, +`FontFamily`, `bool`, and `GridLength`. Use `Thickness` for padding +or border thickness, and `FontWeight` / `FontFamily` for typography resources. +Literal values retain their types; `ThemeRef` overrides re-resolve on theme changes. + +`ResourceBuilder.SetColor(key, color)` stores a literal `Color`, not a brush. +Use it only for Color-typed resources; brush-typed keys such as `ButtonBackground` +need the string or `Brush` overload of `Set`. The string overload +still parses a hex or named color into a brush; it does not store arbitrary +text. Integer literals continue to select the `double` overload, so pass the +value type expected by the WinUI resource key rather than relying on conversion. Overrides cascade to child elements through WinUI's resource dictionary hierarchy. +Font-family resource keys require a `FontFamily` object, not a string: + +```csharp +class FontFamilyResourceExample : Component +{ + public override Element Render() => + Button("Code").Resources(r => r + .Set("ContentControlThemeFontFamily", new Microsoft.UI.Xaml.Media.FontFamily("Consolas"))); +} +``` + +Calling `Set(key, "Consolas")` would select the color-string overload instead. + ## Custom resource access For brushes Reactor hasn't surfaced as a typed property, `Theme.Ref()` diff --git a/docs/specs/002-winui3-gap-analysis.md b/docs/specs/002-winui3-gap-analysis.md index 2b0bf7468..8668c58dc 100644 --- a/docs/specs/002-winui3-gap-analysis.md +++ b/docs/specs/002-winui3-gap-analysis.md @@ -471,7 +471,7 @@ by conditional rendering — elements not returned from render are never created | WinUI Feature | Status | Notes | |---|---|---| | **ResourceDictionary (app-level)** | Passthrough | ReactorApp loads XamlControlsResources; no DSL for authoring app-level dictionaries | -| **ResourceDictionary (per-element)** | Exposed | `.Resources(r => r.Set(key, value))` writes into that element's own `FrameworkElement.Resources`; `ResourceBuilder.Set` overloads accept a hex/named color string, a `Brush`, a `ThemeRef`, a `double`, or a `CornerRadius` | +| **ResourceDictionary (per-element)** | Exposed | `.Resources(r => r.Set(key, value))` writes into that element's own `FrameworkElement.Resources`; `Set` accepts color strings, `Brush`, `ThemeRef`, `double`, `CornerRadius`, `Thickness`, `FontWeight`, `FontFamily`, `bool`, and `GridLength`. `SetColor` stores raw `Color` values, not brushes | | **Resource lookup chain** | Passthrough | WinUI's chain works on underlying controls; a per-element override participates in it normally | | **Merged dictionaries** | Passthrough | Can merge via `.Set()` on Application.Resources | | **Theme dictionaries** | Passthrough | WinUI's theme dictionaries work natively; `ThemeRef` resolution reads through them | diff --git a/docs/specs/015-styling-design.md b/docs/specs/015-styling-design.md index 8e3e1828c..e6b4a94d0 100644 --- a/docs/specs/015-styling-design.md +++ b/docs/specs/015-styling-design.md @@ -271,6 +271,15 @@ public class ResourceBuilder return this; } + public ResourceBuilder Set(string key, Thickness value) { _resources[key] = value; return this; } + public ResourceBuilder Set(string key, FontWeight value) { _resources[key] = value; return this; } + public ResourceBuilder Set(string key, FontFamily value) { _resources[key] = value; return this; } + public ResourceBuilder Set(string key, bool value) { _resources[key] = value; return this; } + public ResourceBuilder Set(string key, GridLength value) { _resources[key] = value; return this; } + + // Literal Color resources only. Brush-typed resources use Set(string/Brush/ThemeRef). + public ResourceBuilder SetColor(string key, Color value) { _resources[key] = value; return this; } + internal ResourceOverrides Build() => new(_resources, _themeResources); } diff --git a/docs/specs/tasks/styling-implementation.md b/docs/specs/tasks/styling-implementation.md index 851a569d9..88a7f5e1c 100644 --- a/docs/specs/tasks/styling-implementation.md +++ b/docs/specs/tasks/styling-implementation.md @@ -156,6 +156,12 @@ competitive advantage — no other C# declarative framework surfaces this. - [x] `Set(string key, ThemeRef themeRef)` — stores for reactive resolution - [x] `Set(string key, double value)` — for numeric resource keys - [x] `Set(string key, CornerRadius value)` — for corner radius keys + - [x] `Set(string key, Thickness value)` — padding and border thickness + - [x] `Set(string key, FontWeight value)` — font weight + - [x] `Set(string key, FontFamily value)` — font family, compared by Source + - [x] `Set(string key, bool value)` — Boolean resources + - [x] `Set(string key, GridLength value)` — grid length resources + - [x] `SetColor(string key, Color value)` — raw Color only, not a Brush - [x] `internal ResourceOverrides Build()` — returns immutable snapshot ### 4.2 — ResourceOverrides record diff --git a/plugins/reactor/skills/reactor-dsl/references/reactor.api.txt b/plugins/reactor/skills/reactor-dsl/references/reactor.api.txt index fbf6b92ce..727191aee 100644 --- a/plugins/reactor/skills/reactor-dsl/references/reactor.api.txt +++ b/plugins/reactor/skills/reactor-dsl/references/reactor.api.txt @@ -4902,9 +4902,15 @@ ToString() → string new() Set(string key, Brush brush) → ResourceBuilder Set(string key, CornerRadius value) → ResourceBuilder +Set(string key, FontFamily value) → ResourceBuilder +Set(string key, FontWeight value) → ResourceBuilder +Set(string key, GridLength value) → ResourceBuilder Set(string key, ThemeRef themeRef) → ResourceBuilder +Set(string key, Thickness value) → ResourceBuilder +Set(string key, bool value) → ResourceBuilder Set(string key, double value) → ResourceBuilder Set(string key, string color) → ResourceBuilder +SetColor(string key, Color value) → ResourceBuilder ### record ResourceOptions new(TimeSpan? StaleTime = null, TimeSpan? CacheTime = null, int RetryCount = 0, bool RefetchOnMount = true, string CacheKey = null, bool RefetchOnWindowFocus = false) diff --git a/skills/reactor.api.txt b/skills/reactor.api.txt index fbf6b92ce..727191aee 100644 --- a/skills/reactor.api.txt +++ b/skills/reactor.api.txt @@ -4902,9 +4902,15 @@ ToString() → string new() Set(string key, Brush brush) → ResourceBuilder Set(string key, CornerRadius value) → ResourceBuilder +Set(string key, FontFamily value) → ResourceBuilder +Set(string key, FontWeight value) → ResourceBuilder +Set(string key, GridLength value) → ResourceBuilder Set(string key, ThemeRef themeRef) → ResourceBuilder +Set(string key, Thickness value) → ResourceBuilder +Set(string key, bool value) → ResourceBuilder Set(string key, double value) → ResourceBuilder Set(string key, string color) → ResourceBuilder +SetColor(string key, Color value) → ResourceBuilder ### record ResourceOptions new(TimeSpan? StaleTime = null, TimeSpan? CacheTime = null, int RetryCount = 0, bool RefetchOnMount = true, string CacheKey = null, bool RefetchOnWindowFocus = false) diff --git a/src/Reactor/Core/Element.cs b/src/Reactor/Core/Element.cs index c3a046ed9..4bb7dd447 100644 --- a/src/Reactor/Core/Element.cs +++ b/src/Reactor/Core/Element.cs @@ -1498,8 +1498,8 @@ internal static bool ResourceOverridesEqual( // Literals: compare by VALUE. Brushes are re-parsed to fresh instances each render // (e.g. .Set("X", "#0078D4")), so reference compare would false-decline the skip for - // an UNCHANGED literal-override cell — use BrushesEqual; other literal kinds (double, - // CornerRadius) are value types whose Equals is correct. + // an UNCHANGED literal-override cell. FontFamily also compares by value (Source), + // not WinRT wrapper identity. The remaining literal kinds are value types. var la = a.Literals; var lb = b.Literals; if (la.Count != lb.Count) return false; @@ -1526,6 +1526,8 @@ private static bool ResourceLiteralEqual(object? a, object? b) if (ReferenceEquals(a, b)) return true; if (a is null || b is null) return false; if (a is Brush ba && b is Brush bb) return BrushesEqual(ba, bb); + if (a is Microsoft.UI.Xaml.Media.FontFamily fa && b is Microsoft.UI.Xaml.Media.FontFamily fb) + return FontFamiliesEqual(fa, fb); return a.Equals(b); } } diff --git a/src/Reactor/Elements/ResourceBuilder.cs b/src/Reactor/Elements/ResourceBuilder.cs index c45a049db..4aecea649 100644 --- a/src/Reactor/Elements/ResourceBuilder.cs +++ b/src/Reactor/Elements/ResourceBuilder.cs @@ -1,12 +1,14 @@ using Microsoft.UI.Reactor.Core; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; +using Windows.UI; +using Windows.UI.Text; namespace Microsoft.UI.Reactor.Elements; /// /// Immutable snapshot of per-control resource overrides built by . -/// Contains both literal values (colors, doubles, corner radii) and theme-reactive +/// Contains both literal resource values and theme-reactive /// entries that are re-resolved on theme change. /// public sealed record ResourceOverrides( @@ -64,7 +66,7 @@ public ResourceBuilder Set(string key, ThemeRef themeRef) return this; } - /// Sets a numeric resource override (e.g., ButtonBorderThemeThickness). + /// Sets a numeric resource override (e.g., BreadcrumbBarItemThemeFontSize). public ResourceBuilder Set(string key, double value) { _literals[key] = value; @@ -78,6 +80,51 @@ public ResourceBuilder Set(string key, CornerRadius value) return this; } + /// Sets a thickness resource override (e.g., BreadcrumbBarChevronPadding). + public ResourceBuilder Set(string key, Thickness value) + { + _literals[key] = value; + return this; + } + + /// Sets a font weight resource override (e.g., BreadcrumbBarItemFontWeight). + public ResourceBuilder Set(string key, FontWeight value) + { + _literals[key] = value; + return this; + } + + /// Sets a font family resource override (e.g., ContentControlThemeFontFamily). + public ResourceBuilder Set(string key, FontFamily value) + { + _literals[key] = value; + return this; + } + + /// Sets a Boolean resource override (e.g., GridViewItemSelectionCheckMarkVisualEnabled). + public ResourceBuilder Set(string key, bool value) + { + _literals[key] = value; + return this; + } + + /// + /// Sets a literal color resource override, not a brush. Use the string or + /// overload for resources that expect a brush. + /// + public ResourceBuilder SetColor(string key, Color value) + { + _literals[key] = value; + return this; + } + + /// Sets a grid length resource override (e.g., ContentDialogButtonSpacing). + public ResourceBuilder Set(string key, GridLength value) + { + _literals[key] = value; + return this; + } + /// Builds an immutable snapshot. internal ResourceOverrides Build() { diff --git a/tests/Reactor.AppTests.Host/SelfTest/Fixtures/TypedResourceOverrideFixtures.cs b/tests/Reactor.AppTests.Host/SelfTest/Fixtures/TypedResourceOverrideFixtures.cs new file mode 100644 index 000000000..75673bb3e --- /dev/null +++ b/tests/Reactor.AppTests.Host/SelfTest/Fixtures/TypedResourceOverrideFixtures.cs @@ -0,0 +1,139 @@ +using Microsoft.UI.Reactor.Core; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Media; +using static Microsoft.UI.Reactor.Factories; + +namespace Microsoft.UI.Reactor.AppTests.Host.SelfTest.Fixtures; + +internal static class TypedResourceOverrideFixtures +{ + internal sealed class MountUpdateAndRemoval(Harness h) : SelfTestFixtureBase(h) + { + public override async Task RunAsync() + { + var family = new FontFamily("Consolas"); + var updatedFamily = new FontFamily("Segoe UI"); + Action? setPhase = null; + var host = H.CreateHost(); + host.Mount(ctx => + { + var (phase, update) = ctx.UseState(0); + setPhase = update; + var element = Button("typed resource overrides"); + if (phase < 2) + element = element.Resources(r => r + .Set("TypedPadding", Padding(phase)) + .Set("TypedWeight", new global::Windows.UI.Text.FontWeight((ushort)(phase == 0 ? 700 : 400))) + .Set("TypedFont", phase == 0 ? family : updatedFamily) + .Set("TypedBoolean", phase == 0) + .SetColor("TypedColor", Color(phase)) + .Set("TypedGridLength", Length(phase)) + .Set("TypedBrush", "#123456")); + return element; + }); + + await Harness.Render(); + var control = H.FindControl