Skip to content

Commit fa669cc

Browse files
committed
fix(controls): Fix TextBlock style, typography and related issues
1 parent 1de0cd3 commit fa669cc

File tree

10 files changed

+610
-112
lines changed

10 files changed

+610
-112
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// This Source Code Form is subject to the terms of the MIT License.
2+
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
3+
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
4+
// All Rights Reserved.
5+
6+
using System.Windows.Markup;
7+
8+
namespace Wpf.Ui.Controls;
9+
10+
/// <summary>
11+
/// Represents a named typography preset containing font metrics used by controls
12+
/// (for example, font size and weight). Presets are intended to be stored as
13+
/// resources and referenced by the control's <c>FontTypography</c> mapping.
14+
/// </summary>
15+
/// <remarks>
16+
/// Example:
17+
/// <code lang="xml">
18+
/// &lt;ResourceDictionary
19+
/// xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
20+
/// xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
21+
/// xmlns:controls="clr-namespace:Wpf.Ui.Controls"&gt;
22+
///
23+
/// &lt;controls:FontTypographyPreset x:Key="BodyTextBlockStyle" FontSize="14" FontWeight="Regular" /&gt;
24+
///
25+
/// &lt;/ResourceDictionary&gt;
26+
///
27+
/// &lt;!-- TextBlock resolves the preset by resource key produced from the FontTypography enum --&gt;
28+
/// &lt;ui:TextBlock FontTypography="Body" /&gt;
29+
/// </code>
30+
/// </remarks>
31+
[MarkupExtensionReturnType(typeof(FontTypographyPreset))]
32+
public class FontTypographyPreset : MarkupExtension
33+
{
34+
/// <summary>
35+
/// Gets or sets the font size for this typography style, measured in device-independent units (1/96 inch).
36+
/// If this property is <c>null</c>, no font size override will be applied from this style.
37+
/// </summary>
38+
public double? FontSize { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets the font weight defined by this typography preset.
42+
/// A <c>null</c> value indicates that no specific font weight should be applied.
43+
/// </summary>
44+
public FontWeight? FontWeight { get; set; }
45+
46+
/*
47+
Note: Excluding LineHeight intentionally. WPF and WinUI have fundamentally
48+
different text rendering engines - identical FontSize/LineHeight pairs would
49+
break vertical text alignment and create maintenance nightmares.
50+
*/
51+
52+
/// <summary>
53+
/// Returns this instance when used in XAML so the preset can be declared as a resource.
54+
/// </summary>
55+
public override object ProvideValue(IServiceProvider serviceProvider)
56+
{
57+
return this;
58+
}
59+
}

src/Wpf.Ui/Controls/TextBlock/TextBlock.cs

Lines changed: 219 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,73 +3,271 @@
33
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
44
// All Rights Reserved.
55

6+
using Wpf.Ui.Markup;
7+
using UiTextBlock = Wpf.Ui.Controls.TextBlock;
8+
using WinTextBlock = System.Windows.Controls.TextBlock;
9+
610
// ReSharper disable once CheckNamespace
711
namespace Wpf.Ui.Controls;
812

913
/// <summary>
10-
/// Extended <see cref="System.Windows.Controls.TextBlock"/> with additional parameters like <see cref="FontTypography"/>.
14+
/// Extended <see cref="System.Windows.Controls.TextBlock"/> that integrates with the library's
15+
/// typography and appearance resources.
1116
/// </summary>
12-
public class TextBlock : System.Windows.Controls.TextBlock
17+
/// <remarks>
18+
/// This control supports two mechanisms for applying design-system typography:
19+
/// <para>
20+
/// - The <see cref="FontTypography"/> enum which is mapped to a resource key and resolved
21+
/// at runtime to a <see cref="FontTypographyPreset"/>.
22+
/// </para>
23+
/// <para>
24+
/// - An internal resource-backed preset is resolved via a private puppet dependency property
25+
/// so the framework's resource system (DynamicResource) performs resolution and updates.
26+
/// </para>
27+
/// <para>
28+
/// The resolved preset is used to coerce <see cref="WinTextBlock.FontSizeProperty"/>
29+
/// and <see cref="WinTextBlock.FontWeight"/>.
30+
/// </para>
31+
/// <para>
32+
/// The <see cref="Appearance"/> property maps to brush resources and is applied via a resource reference.
33+
/// </para>
34+
/// </remarks>
35+
/// <example>
36+
/// <code lang="xml">
37+
/// &lt;ui:TextBlock FontTypography="Body" Appearance="Primary" Text="Hello, world!" /&gt;
38+
/// </code>
39+
/// </example>
40+
public class TextBlock : WinTextBlock
1341
{
42+
static TextBlock()
43+
{
44+
// Coerce FontSize based on FontTypography when set.
45+
// Using AddOwner allows the opportunity to OverrideMetadata to be reserved for later.
46+
FontSizeProperty.AddOwner(
47+
typeof(UiTextBlock),
48+
new FrameworkPropertyMetadata(
49+
14d, // Fluent theme default font size - FontTypography Body font size
50+
FrameworkPropertyMetadataOptions.AffectsMeasure
51+
| FrameworkPropertyMetadataOptions.AffectsRender
52+
| FrameworkPropertyMetadataOptions.Inherits,
53+
null,
54+
static (d, baseValue) => ((UiTextBlock)d).CoerceFontSize(baseValue)
55+
)
56+
);
57+
58+
// Coerce FontWeight based on FontTypography when set.
59+
FontWeightProperty.AddOwner(
60+
typeof(UiTextBlock),
61+
new FrameworkPropertyMetadata(
62+
FontWeights.Regular, // FontTypography Body font weight
63+
FrameworkPropertyMetadataOptions.AffectsMeasure
64+
| FrameworkPropertyMetadataOptions.AffectsRender
65+
| FrameworkPropertyMetadataOptions.Inherits,
66+
null,
67+
static (d, baseValue) => ((UiTextBlock)d).CoerceFontWeight(baseValue)
68+
)
69+
);
70+
71+
// Coerce Foreground based on Appearance when set.
72+
ForegroundProperty.AddOwner(
73+
typeof(UiTextBlock),
74+
new FrameworkPropertyMetadata(
75+
UnsetMarkerBrush.Instance, // marker for an "unset" Foreground
76+
FrameworkPropertyMetadataOptions.AffectsRender
77+
| FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender
78+
| FrameworkPropertyMetadataOptions.Inherits,
79+
null,
80+
static (d, baseValue) => ((UiTextBlock)d).CoerceForeground(baseValue)
81+
)
82+
);
83+
}
84+
1485
/// <summary>Identifies the <see cref="FontTypography"/> dependency property.</summary>
1586
public static readonly DependencyProperty FontTypographyProperty = DependencyProperty.Register(
1687
nameof(FontTypography),
17-
typeof(FontTypography),
18-
typeof(TextBlock),
88+
typeof(FontTypography?),
89+
typeof(UiTextBlock),
1990
new PropertyMetadata(
20-
FontTypography.Body,
91+
null,
2192
static (o, args) =>
2293
{
23-
((TextBlock)o).OnFontTypographyChanged((FontTypography)args.NewValue);
94+
((UiTextBlock)o).OnFontTypographyChanged((FontTypography?)args.NewValue);
2495
}
2596
)
2697
);
2798

2899
/// <summary>Identifies the <see cref="Appearance"/> dependency property.</summary>
29100
public static readonly DependencyProperty AppearanceProperty = DependencyProperty.Register(
30101
nameof(Appearance),
31-
typeof(TextColor),
32-
typeof(TextBlock),
102+
typeof(TextColor?),
103+
typeof(UiTextBlock),
33104
new PropertyMetadata(
34-
TextColor.Primary,
105+
null,
35106
static (o, args) =>
36107
{
37-
((TextBlock)o).OnAppearanceChanged((TextColor)args.NewValue);
108+
((UiTextBlock)o).OnAppearanceChanged((TextColor?)args.NewValue);
109+
}
110+
)
111+
);
112+
113+
/// <summary>
114+
/// Private Puppet dependency property used to hold a ResourceReference to a FontTypographyPreset.
115+
/// </summary>
116+
private static readonly DependencyProperty FontTypographyPresetResourceRefProperty = DependencyProperty.Register(
117+
"FontTypographyPresetResourceRef",
118+
typeof(FontTypographyPreset),
119+
typeof(UiTextBlock),
120+
new PropertyMetadata(
121+
null,
122+
static (d, e) =>
123+
{
124+
d.CoerceValue(FontSizeProperty);
125+
d.CoerceValue(FontWeightProperty);
126+
}
127+
)
128+
);
129+
130+
/// <summary>
131+
/// Private Puppet dependency property used to hold the Foreground resource resolved from the Appearance property.
132+
/// </summary>
133+
private static readonly DependencyProperty ForegroundResourceRefProperty = DependencyProperty.Register(
134+
"ForegroundResourceRef",
135+
typeof(Brush),
136+
typeof(UiTextBlock),
137+
new PropertyMetadata(
138+
null,
139+
static (d, e) =>
140+
{
141+
d.CoerceValue(ForegroundProperty);
142+
}
143+
)
144+
);
145+
146+
/// <summary>
147+
/// Private Puppet dependency property used to hold the default foreground resource.
148+
/// This property can hold a DynamicResource reference so controls without an explicit Foreground
149+
/// will use the library's themed foreground brush.
150+
/// </summary>
151+
private static readonly DependencyProperty DefaultForegroundResourceRefProperty = DependencyProperty.Register(
152+
"DefaultForegroundResourceRef",
153+
typeof(Brush),
154+
typeof(UiTextBlock),
155+
new PropertyMetadata(
156+
null,
157+
static (d, e) =>
158+
{
159+
d.CoerceValue(ForegroundProperty);
38160
}
39161
)
40162
);
41163

42164
public TextBlock()
43165
{
44-
var defaultFontTypography = (FontTypography)FontTypographyProperty.DefaultMetadata.DefaultValue;
45-
SetResourceReference(StyleProperty, defaultFontTypography.ToResourceValue());
166+
SetResourceReference(DefaultForegroundResourceRefProperty, TextColor.Primary.ToResourceKey());
46167
}
47168

48169
/// <summary>
49170
/// Gets or sets the <see cref="FontTypography"/> of the text.
50171
/// </summary>
51-
public FontTypography FontTypography
172+
public FontTypography? FontTypography
52173
{
53-
get => (FontTypography)GetValue(FontTypographyProperty);
174+
get => (FontTypography?)GetValue(FontTypographyProperty);
54175
set => SetValue(FontTypographyProperty, value);
55176
}
56177

57178
/// <summary>
58179
/// Gets or sets the color of the text.
59180
/// </summary>
60-
public TextColor Appearance
181+
public TextColor? Appearance
61182
{
62-
get => (TextColor)GetValue(AppearanceProperty);
183+
get => (TextColor?)GetValue(AppearanceProperty);
63184
set => SetValue(AppearanceProperty, value);
64185
}
65186

66-
private void OnFontTypographyChanged(FontTypography newTypography)
187+
private void OnFontTypographyChanged(FontTypography? newTypography)
67188
{
68-
SetResourceReference(StyleProperty, newTypography.ToResourceValue());
189+
if (newTypography.HasValue)
190+
{
191+
var resourceKey = newTypography.Value.ToResourceKey();
192+
193+
// Use WPF resource reference mechanism to resolve and cache the preset.
194+
// This avoids manual TryFindResource tree traversal.
195+
SetResourceReference(FontTypographyPresetResourceRefProperty, resourceKey);
196+
}
197+
else
198+
{
199+
// Clear any puppet resource reference
200+
ClearValue(FontTypographyPresetResourceRefProperty);
201+
}
202+
203+
// Re-evaluate coerced values so when FontTypography is set, the
204+
// CoerceValueCallbacks installed on FontSize/FontWeight will take effect
205+
// and prevent local changes from overriding typography-defined values.
206+
CoerceValue(FontSizeProperty);
207+
CoerceValue(FontWeightProperty);
69208
}
70209

71-
private void OnAppearanceChanged(TextColor textColor)
210+
private void OnAppearanceChanged(TextColor? textColor)
211+
{
212+
if (textColor.HasValue)
213+
{
214+
var resourceKey = textColor.Value.ToResourceKey();
215+
216+
// Similar to OnFontTypographyChanged, attach themed color resource reference to a proxy property,
217+
// allowing the WPF resource system to handle updates automatically.
218+
SetResourceReference(ForegroundResourceRefProperty, resourceKey);
219+
}
220+
else
221+
{
222+
ClearValue(ForegroundResourceRefProperty);
223+
}
224+
225+
CoerceValue(ForegroundProperty);
226+
}
227+
228+
/*
229+
* The following CoerceValueCallback methods handle value precedence.
230+
* When no explicit value is set (via style, local value, or inheritance),
231+
* they apply WPFUI Fluent theme defaults.
232+
*/
233+
234+
private object CoerceFontSize(object baseValue)
72235
{
73-
SetResourceReference(ForegroundProperty, textColor.ToResourceValue());
236+
var preset = GetValue(FontTypographyPresetResourceRefProperty) as FontTypographyPreset;
237+
if (preset?.FontSize is double size)
238+
{
239+
return size;
240+
}
241+
242+
return baseValue;
243+
}
244+
245+
private object CoerceFontWeight(object baseValue)
246+
{
247+
var preset = GetValue(FontTypographyPresetResourceRefProperty) as FontTypographyPreset;
248+
if (preset?.FontWeight is FontWeight weight)
249+
{
250+
return weight;
251+
}
252+
253+
return baseValue;
254+
}
255+
256+
private object CoerceForeground(object baseValue)
257+
{
258+
if (GetValue(ForegroundResourceRefProperty) is Brush appearance)
259+
{
260+
return appearance;
261+
}
262+
263+
if (ReferenceEquals(baseValue, UnsetMarkerBrush.Instance))
264+
{
265+
if (GetValue(DefaultForegroundResourceRefProperty) is Brush defaultForeground)
266+
{
267+
return defaultForeground;
268+
}
269+
}
270+
271+
return baseValue;
74272
}
75-
}
273+
}

src/Wpf.Ui/Controls/TextBlock/TextBlock.xaml

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,28 @@
55
All Rights Reserved.
66
-->
77

8-
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
8+
<ResourceDictionary
9+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
10+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
11+
xmlns:controls="clr-namespace:Wpf.Ui.Controls">
912

1013
<Style TargetType="{x:Type TextBlock}">
14+
<!--
15+
Attach default foreground resource ref so framework TextBlock can coerce to themed brush when unset.
1116
12-
<!--<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>-->
17+
Explanation:
18+
This Setter attaches a DynamicResource reference to the library's themed text brush
19+
without changing dependency property or style precedence. If a TextBlock's Foreground
20+
is explicitly set (local or via Style), that value remains authoritative. When Foreground
21+
is not set, the framework will use the attached themed brush as a dynamic fallback,
22+
and it will update automatically on theme/resource changes.
1323
14-
<!-- The Display option causes a large aliasing effect -->
15-
<!--<Setter Property="TextOptions.TextFormattingMode" Value="Ideal" />-->
16-
<Setter Property="Background" Value="Transparent" />
17-
<Setter Property="FontSize" Value="14" />
18-
<Setter Property="Margin" Value="0" />
19-
<Setter Property="Padding" Value="0" />
20-
<Setter Property="Focusable" Value="False" />
21-
<Setter Property="SnapsToDevicePixels" Value="True" />
22-
<Setter Property="OverridesDefaultStyle" Value="True" />
24+
Note:
25+
If an app or control replaces the Style that sets the attached `controls:TextBlockHelper.DefaultForegroundResourceRef`
26+
and the custom Style does not set this attached property, the library's themed foreground fallback will not apply.
27+
In that case developers should reference the theme brush directly (for example via `{DynamicResource TextFillColorPrimaryBrush}`).
28+
-->
29+
<Setter Property="controls:TextBlockHelper.DefaultForegroundResourceRef" Value="{DynamicResource TextFillColorPrimaryBrush}" />
2330
</Style>
2431

25-
</ResourceDictionary>
32+
</ResourceDictionary>

0 commit comments

Comments
 (0)