diff --git a/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans-Bold.ttf b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans-Bold.ttf new file mode 100644 index 000000000000..98c74e0a4228 Binary files /dev/null and b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans-Bold.ttf differ diff --git a/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans-Regular.ttf b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans-Regular.ttf new file mode 100644 index 000000000000..67803bb64274 Binary files /dev/null and b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans-Regular.ttf differ diff --git a/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans.ttf.manifest b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans.ttf.manifest new file mode 100644 index 000000000000..83dfe2a21e03 --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans.ttf.manifest @@ -0,0 +1,34 @@ + { + "fonts": [ + { + "font_style": "italic", + "font_weight": 400, + "font_stretch": "Condensed", + "family_name": "ms-appx:///Assets/Fonts/OpenSans/OpenSans_Condensed-MediumItalic.ttf" + }, + { + "font_style": "normal", + "font_weight": 400, + "font_stretch": "SemiCondensed", + "family_name": "ms-appx:///Assets/Fonts/OpenSans/OpenSans_SemiCondensed-Regular.ttf" + }, + { + "font_style": "normal", + "font_weight": 600, + "font_stretch": "SemiCondensed", + "family_name": "ms-appx:///Assets/Fonts/OpenSans/OpenSans_SemiCondensed-SemiBold.ttf" + }, + { + "font_style": "normal", + "font_weight": 700, + "font_stretch": "Normal", + "family_name": "ms-appx:///Assets/Fonts/OpenSans/OpenSans-Bold.ttf" + }, + { + "font_style": "normal", + "font_weight": 400, + "font_stretch": "Normal", + "family_name": "ms-appx:///Assets/Fonts/OpenSans/OpenSans-Regular.ttf" + }, + ] +} diff --git a/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans_Condensed-MediumItalic.ttf b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans_Condensed-MediumItalic.ttf new file mode 100644 index 000000000000..b43786bb5218 Binary files /dev/null and b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans_Condensed-MediumItalic.ttf differ diff --git a/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans_SemiCondensed-Regular.ttf b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans_SemiCondensed-Regular.ttf new file mode 100644 index 000000000000..5b09b35bc1f0 Binary files /dev/null and b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans_SemiCondensed-Regular.ttf differ diff --git a/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans_SemiCondensed-SemiBold.ttf b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans_SemiCondensed-SemiBold.ttf new file mode 100644 index 000000000000..fff3a37206bb Binary files /dev/null and b/src/SamplesApp/UITests.Shared/Assets/Fonts/OpenSans/OpenSans_SemiCondensed-SemiBold.ttf differ diff --git a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems index 72d474a795a4..6fe542f33c53 100644 --- a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems +++ b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems @@ -9588,6 +9588,8 @@ + + diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBlock.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBlock.cs index 39efe8efa592..c12c9d90facf 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBlock.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_TextBlock.cs @@ -35,6 +35,62 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls [RunsOnUIThread] public class Given_TextBlock { + [TestMethod] + [DataRow((ushort)400, FontStyle.Italic, FontStretch.Condensed, "ms-appx:///Assets/Fonts/OpenSans/OpenSans_Condensed-MediumItalic.ttf")] + [DataRow((ushort)400, FontStyle.Normal, FontStretch.SemiCondensed, "ms-appx:///Assets/Fonts/OpenSans/OpenSans_SemiCondensed-Regular.ttf")] + [DataRow((ushort)600, FontStyle.Normal, FontStretch.SemiCondensed, "ms-appx:///Assets/Fonts/OpenSans/OpenSans_SemiCondensed-SemiBold.ttf")] + [DataRow((ushort)700, FontStyle.Normal, FontStretch.Normal, "ms-appx:///Assets/Fonts/OpenSans/OpenSans-Bold.ttf")] + [DataRow((ushort)400, FontStyle.Normal, FontStretch.Normal, "ms-appx:///Assets/Fonts/OpenSans/OpenSans-Regular.ttf")] + public async Task When_Font_Has_Manifest(ushort weight, FontStyle style, FontStretch stretch, string ttfFile) + { + var SUT = new TextBlock + { + Text = "Hello World!", + FontSize = 18, + FontStyle = style, + FontStretch = stretch, + FontWeight = new FontWeight(weight), + FontFamily = new FontFamily("ms-appx:///Assets/Fonts/OpenSans/OpenSans.ttf"), + }; + + var expectedTB = new TextBlock + { + Text = "Hello World!", + FontSize = 18, + FontFamily = new FontFamily(ttfFile) + }; + + var differentTtf = "ms-appx:///Assets/Fonts/OpenSans/OpenSans-Bold.ttf"; + if (ttfFile == differentTtf) + { + differentTtf = "ms-appx:///Assets/Fonts/OpenSans/OpenSans-Regular.ttf"; + } + + var differentTB = new TextBlock + { + Text = "Hello World!", + FontSize = 18, + FontFamily = new FontFamily(differentTtf), + }; + + var sp = new StackPanel() + { + Children = + { + SUT, + expectedTB, + differentTB, + }, + }; + + await UITestHelper.Load(sp); + var actual = await UITestHelper.ScreenShot(SUT); + var expected = await UITestHelper.ScreenShot(expectedTB); + var different = await UITestHelper.ScreenShot(differentTB); + await ImageAssert.AreEqualAsync(actual, expected); + await ImageAssert.AreNotEqualAsync(actual, different); + } + #if __SKIA__ [TestMethod] // It looks like CI might not have any installed fonts with Chinese characters which could cause the test to fail @@ -42,7 +98,7 @@ public class Given_TextBlock public async Task Check_FontFallback() { var SUT = new TextBlock { Text = "示例文本", FontSize = 24 }; - var skFont = FontDetailsCache.GetFont(SUT.FontFamily?.Source, (float)SUT.FontSize, SUT.FontWeight, SUT.FontStyle).SKFont; + var skFont = FontDetailsCache.GetFont(SUT.FontFamily?.Source, (float)SUT.FontSize, SUT.FontWeight, SUT.FontStretch, SUT.FontStyle).SKFont; Assert.IsFalse(skFont.ContainsGlyph(SUT.Text[0])); var fallbackFont = SKFontManager.Default.MatchCharacter(SUT.Text[0]); diff --git a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/ContentPresenter.cs b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/ContentPresenter.cs index f0c46fb28a4b..b59b855f8204 100644 --- a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/ContentPresenter.cs +++ b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/ContentPresenter.cs @@ -86,20 +86,7 @@ public bool IsTextScaleFactorEnabled // Skipping already declared property Foreground // Skipping already declared property FontWeight // Skipping already declared property FontStyle -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public global::Windows.UI.Text.FontStretch FontStretch - { - get - { - return (global::Windows.UI.Text.FontStretch)this.GetValue(FontStretchProperty); - } - set - { - this.SetValue(FontStretchProperty, value); - } - } -#endif + // Skipping already declared property FontStretch // Skipping already declared property FontSize // Skipping already declared property FontFamily // Skipping already declared property CornerRadius @@ -158,14 +145,7 @@ public int CharacterSpacing // Skipping already declared property CornerRadiusProperty // Skipping already declared property FontFamilyProperty // Skipping already declared property FontSizeProperty -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public static global::Microsoft.UI.Xaml.DependencyProperty FontStretchProperty { get; } = - Microsoft.UI.Xaml.DependencyProperty.Register( - nameof(FontStretch), typeof(global::Windows.UI.Text.FontStretch), - typeof(global::Microsoft.UI.Xaml.Controls.ContentPresenter), - new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(global::Windows.UI.Text.FontStretch))); -#endif + // Skipping already declared property FontStretchProperty // Skipping already declared property FontStyleProperty // Skipping already declared property FontWeightProperty // Skipping already declared property ForegroundProperty diff --git a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/Control.cs b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/Control.cs index f27a4632bcc2..1b1a8534797f 100644 --- a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/Control.cs +++ b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/Control.cs @@ -47,20 +47,7 @@ public bool IsTextScaleFactorEnabled // Skipping already declared property Foreground // Skipping already declared property FontWeight // Skipping already declared property FontStyle -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public global::Windows.UI.Text.FontStretch FontStretch - { - get - { - return (global::Windows.UI.Text.FontStretch)this.GetValue(FontStretchProperty); - } - set - { - this.SetValue(FontStretchProperty, value); - } - } -#endif + // Skipping already declared property FontStretch // Skipping already declared property FontSize // Skipping already declared property FontFamily #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ @@ -150,14 +137,7 @@ public int CharacterSpacing #endif // Skipping already declared property FontFamilyProperty // Skipping already declared property FontSizeProperty -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public static global::Microsoft.UI.Xaml.DependencyProperty FontStretchProperty { get; } = - Microsoft.UI.Xaml.DependencyProperty.Register( - nameof(FontStretch), typeof(global::Windows.UI.Text.FontStretch), - typeof(global::Microsoft.UI.Xaml.Controls.Control), - new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(global::Windows.UI.Text.FontStretch))); -#endif + // Skipping already declared property FontStretchProperty // Skipping already declared property FontStyleProperty // Skipping already declared property FontWeightProperty // Skipping already declared property ForegroundProperty diff --git a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/TextBlock.cs b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/TextBlock.cs index 01fc37d24043..f0516127ec68 100644 --- a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/TextBlock.cs +++ b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Controls/TextBlock.cs @@ -27,20 +27,7 @@ public bool IsColorFontEnabled // Skipping already declared property Foreground // Skipping already declared property FontWeight // Skipping already declared property FontStyle -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public global::Windows.UI.Text.FontStretch FontStretch - { - get - { - return (global::Windows.UI.Text.FontStretch)this.GetValue(FontStretchProperty); - } - set - { - this.SetValue(FontStretchProperty, value); - } - } -#endif + // Skipping already declared property FontStretch // Skipping already declared property FontSize // Skipping already declared property FontFamily // Skipping already declared property IsTextSelectionEnabled @@ -212,14 +199,7 @@ public double BaselineOffset // Skipping already declared property CharacterSpacingProperty // Skipping already declared property FontFamilyProperty // Skipping already declared property FontSizeProperty -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public static global::Microsoft.UI.Xaml.DependencyProperty FontStretchProperty { get; } = - Microsoft.UI.Xaml.DependencyProperty.Register( - nameof(FontStretch), typeof(global::Windows.UI.Text.FontStretch), - typeof(global::Microsoft.UI.Xaml.Controls.TextBlock), - new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(global::Windows.UI.Text.FontStretch))); -#endif + // Skipping already declared property FontStretchProperty // Skipping already declared property FontStyleProperty // Skipping already declared property FontWeightProperty // Skipping already declared property ForegroundProperty diff --git a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Documents/TextElement.cs b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Documents/TextElement.cs index fa6ea7d4fc68..d45fd2719951 100644 --- a/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Documents/TextElement.cs +++ b/src/Uno.UI/Generated/3.0.0.0/Microsoft.UI.Xaml.Documents/TextElement.cs @@ -97,20 +97,7 @@ public bool IsAccessKeyScope // Skipping already declared property Foreground // Skipping already declared property FontWeight // Skipping already declared property FontStyle -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public global::Windows.UI.Text.FontStretch FontStretch - { - get - { - return (global::Windows.UI.Text.FontStretch)this.GetValue(FontStretchProperty); - } - set - { - this.SetValue(FontStretchProperty, value); - } - } -#endif + // Skipping already declared property FontStretch // Skipping already declared property FontSize // Skipping already declared property FontFamily #if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ @@ -226,14 +213,7 @@ public string AccessKey #endif // Skipping already declared property FontFamilyProperty // Skipping already declared property FontSizeProperty -#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__ - [global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")] - public static global::Microsoft.UI.Xaml.DependencyProperty FontStretchProperty { get; } = - Microsoft.UI.Xaml.DependencyProperty.Register( - nameof(FontStretch), typeof(global::Windows.UI.Text.FontStretch), - typeof(global::Microsoft.UI.Xaml.Documents.TextElement), - new Microsoft.UI.Xaml.FrameworkPropertyMetadata(default(global::Windows.UI.Text.FontStretch))); -#endif + // Skipping already declared property FontStretchProperty // Skipping already declared property FontStyleProperty // Skipping already declared property FontWeightProperty // Skipping already declared property ForegroundProperty diff --git a/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs b/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs index a6f0419df23b..8ef7379f3de5 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs @@ -330,6 +330,27 @@ public FontStyle FontStyle ); #endregion + #region FontStretch + + public FontStretch FontStretch + { + get => (FontStretch)this.GetValue(FontStretchProperty); + set => this.SetValue(FontStretchProperty, value); + } + + public static DependencyProperty FontStretchProperty { get; } = + DependencyProperty.Register( + nameof(FontStretch), + typeof(FontStretch), + typeof(ContentPresenter), + new FrameworkPropertyMetadata( + FontStretch.Normal, + FrameworkPropertyMetadataOptions.Inherits, + (s, e) => ((ContentPresenter)s)?.OnFontStretchChanged((FontStretch)e.OldValue, (FontStretch)e.NewValue) + ) + ); + #endregion + #region TextWrapping Dependency Property public TextWrapping TextWrapping @@ -746,6 +767,13 @@ protected virtual void OnFontStyleChanged(FontStyle oldValue, FontStyle newValue partial void OnFontStyleChangedPartial(FontStyle oldValue, FontStyle newValue); + private protected virtual void OnFontStretchChanged(FontStretch oldValue, FontStretch newValue) + { + OnFontStretchChangedPartial(oldValue, newValue); + } + + partial void OnFontStretchChangedPartial(FontStretch oldValue, FontStretch newValue); + protected virtual void OnContentChanged(object oldValue, object newValue) { if (oldValue is View || newValue is View) diff --git a/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs b/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs index 3de76a4a9060..5de274f51250 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs @@ -706,6 +706,27 @@ public FontStyle FontStyle ); #endregion + #region FontStretch + + public FontStretch FontStretch + { + get => (FontStretch)this.GetValue(FontStretchProperty); + set => this.SetValue(FontStretchProperty, value); + } + + public static DependencyProperty FontStretchProperty { get; } = + DependencyProperty.Register( + nameof(FontStretch), + typeof(FontStretch), + typeof(Control), + new FrameworkPropertyMetadata( + FontStretch.Normal, + FrameworkPropertyMetadataOptions.Inherits, + (s, e) => ((Control)s)?.OnFontStretchChanged((FontStretch)e.OldValue, (FontStretch)e.NewValue) + ) + ); + #endregion + #region Padding DependencyProperty public Thickness Padding @@ -965,6 +986,13 @@ protected virtual void OnFontStyleChanged(FontStyle oldValue, FontStyle newValue partial void OnFontStyleChangedPartial(FontStyle oldValue, FontStyle newValue); + private protected virtual void OnFontStretchChanged(FontStretch oldValue, FontStretch newValue) + { + OnFontStretchChangedPartial(oldValue, newValue); + } + + partial void OnFontStretchChangedPartial(FontStretch oldValue, FontStretch newValue); + protected virtual void OnPaddingChanged(Thickness oldValue, Thickness newValue) { OnPaddingChangedPartial(oldValue, newValue); diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs index f254cf7b2d9d..a454ecb3fa71 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs @@ -144,6 +144,7 @@ partial void OnTextChangedPartial() // Invalidate _paint partial void OnFontWeightChangedPartial() => _paint = null; partial void OnFontStyleChangedPartial() => _paint = null; + // TODO: FontStretch? partial void OnFontFamilyChangedPartial() => _paint = null; partial void OnFontSizeChangedPartial() => _paint = null; partial void OnCharacterSpacingChangedPartial() => _paint = null; diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.cs index 36c42de9ecab..d1a63f4d415d 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.cs @@ -185,6 +185,36 @@ private void OnFontStyleChanged() #endregion + #region FontStretch Dependency Property + + public FontStretch FontStretch + { + get => (FontStretch)GetValue(FontStretchProperty); + set => SetValue(FontStretchProperty, value); + } + + public static DependencyProperty FontStretchProperty { get; } = + DependencyProperty.Register( + nameof(FontStretch), + typeof(FontStretch), + typeof(TextBlock), + new FrameworkPropertyMetadata( + defaultValue: FontStretch.Normal, + options: FrameworkPropertyMetadataOptions.Inherits, + propertyChangedCallback: (s, e) => ((TextBlock)s).OnFontStretchChanged() + ) + ); + + private void OnFontStretchChanged() + { + OnFontStretchChangedPartial(); + InvalidateTextBlock(); + } + + partial void OnFontStretchChangedPartial(); + + #endregion + #region TextWrapping Dependency Property public TextWrapping TextWrapping diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs index a6376cca7711..d688b8e6c48d 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs @@ -150,7 +150,14 @@ internal float GetComputedLineHeight() } else { - var font = FontDetailsCache.GetFont(FontFamily?.Source, (float)FontSize, FontWeight, FontStyle); + var font = FontDetailsCache.GetFont(FontFamily?.Source, (float)FontSize, FontWeight, FontStretch, FontStyle); + if (font.CanChange) + { + // While font family itself didn't change, OnFontFamilyChanged will invalidate whatever + // needed for the rendering to happen correct on the next frame. + font.FontUpdated += OnFontFamilyChanged; + } + return font.LineHeight; } } diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs index 45a19607e13c..01de39a412e5 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs @@ -177,6 +177,8 @@ internal override void AfterArrange() partial void OnFontWeightChangedPartial() => _fontWeightChanged = true; + // TODO: FontStretch? + partial void OnIsTextSelectionEnabledChangedPartial() { if (IsTextSelectionEnabled) diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.cs b/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.cs index 735c2e79f75e..1b2c8fabb492 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.cs @@ -492,6 +492,12 @@ protected override void OnFontStyleChanged(FontStyle oldValue, FontStyle newValu UpdateFontPartial(); } + private protected override void OnFontStretchChanged(FontStretch oldValue, FontStretch newValue) + { + base.OnFontStretchChanged(oldValue, newValue); + UpdateFontPartial(); + } + protected override void OnFontWeightChanged(FontWeight oldValue, FontWeight newValue) { base.OnFontWeightChanged(oldValue, newValue); diff --git a/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs b/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs index caa4c7cbb163..725c8f857098 100644 --- a/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs +++ b/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs @@ -27,7 +27,22 @@ internal SKPaint Paint } } - internal FontDetails FontInfo => _fontInfo ??= FontDetailsCache.GetFont(FontFamily?.Source, (float)FontSize, FontWeight, FontStyle); + internal FontDetails FontInfo + { + get + { + _fontInfo ??= FontDetailsCache.GetFont(FontFamily?.Source, (float)FontSize, FontWeight, FontStretch, FontStyle); + + if (_fontInfo.CanChange) + { + // While font family itself didn't change, OnFontFamilyChanged will invalidate whatever + // needed for the rendering to happen correct on the next frame. + _fontInfo.FontUpdated += OnFontFamilyChanged; + } + + return _fontInfo; + } + } internal float LineHeight => FontInfo.LineHeight; @@ -47,6 +62,12 @@ protected override void OnFontStyleChanged() InvalidateFontInfo(); } + protected override void OnFontStretchChanged() + { + base.OnFontStretchChanged(); + InvalidateFontInfo(); + } + protected override void OnFontWeightChanged() { base.OnFontWeightChanged(); diff --git a/src/Uno.UI/UI/Xaml/Documents/Run.cs b/src/Uno.UI/UI/Xaml/Documents/Run.cs index 4a7aa985ffd5..09831b59add9 100644 --- a/src/Uno.UI/UI/Xaml/Documents/Run.cs +++ b/src/Uno.UI/UI/Xaml/Documents/Run.cs @@ -68,6 +68,13 @@ protected override void OnFontStyleChanged() InvalidateSegmentsPartial(); } + protected override void OnFontStretchChanged() + { + base.OnFontStretchChanged(); + InvalidateInlines(false); + InvalidateSegmentsPartial(); + } + protected override void OnFontWeightChanged() { base.OnFontWeightChanged(); diff --git a/src/Uno.UI/UI/Xaml/Documents/Run.skia.cs b/src/Uno.UI/UI/Xaml/Documents/Run.skia.cs index 17d27a5dcbbb..84093103f391 100644 --- a/src/Uno.UI/UI/Xaml/Documents/Run.skia.cs +++ b/src/Uno.UI/UI/Xaml/Documents/Run.skia.cs @@ -17,7 +17,7 @@ partial class Run { private List? _segments; - internal IReadOnlyList Segments => _segments ??= _segments = GetSegments(); + internal IReadOnlyList Segments => _segments ??= GetSegments(); private List GetSegments() { @@ -56,7 +56,14 @@ private List GetSegments() if (symbolTypeface is { }) { - var fi = FontDetailsCache.GetFont(symbolTypeface.FamilyName, (float)FontSize, FontWeight, FontStyle); + var fi = FontDetailsCache.GetFont(symbolTypeface.FamilyName, (float)FontSize, FontWeight, FontStretch, FontStyle); + if (fi.CanChange) + { + // While font family itself didn't change, OnFontFamilyChanged will invalidate whatever + // needed for the rendering to happen correct on the next frame. + fi.FontUpdated += OnFontFamilyChanged; + } + font = fi.Font; wordBreakAfter = Unicode.HasWordBreakOpportunityAfter(text, i) || (i + 1 < text.Length && Unicode.HasWordBreakOpportunityBefore(text, i + 1)); font.GetScale(out fontScale, out _); diff --git a/src/Uno.UI/UI/Xaml/Documents/TextElement.cs b/src/Uno.UI/UI/Xaml/Documents/TextElement.cs index c4eac593aaf7..b0a11c57025f 100644 --- a/src/Uno.UI/UI/Xaml/Documents/TextElement.cs +++ b/src/Uno.UI/UI/Xaml/Documents/TextElement.cs @@ -110,6 +110,35 @@ protected virtual void OnFontStyleChanged() #endregion + #region FontStretch Dependency Property + + public FontStretch FontStretch + { + get => (FontStretch)this.GetValue(FontStretchProperty); + set => this.SetValue(FontStyleProperty, value); + } + + public static DependencyProperty FontStretchProperty { get; } = + DependencyProperty.Register( + nameof(FontStretch), + typeof(FontStretch), + typeof(TextElement), + new FrameworkPropertyMetadata( + defaultValue: FontStretch.Normal, + options: FrameworkPropertyMetadataOptions.Inherits, + propertyChangedCallback: (s, e) => ((TextElement)s).OnFontStretchChanged() + ) + ); + + protected virtual void OnFontStretchChanged() + { + OnFontStretchChangedPartial(); + } + + partial void OnFontStretchChangedPartial(); + + #endregion + #region FontSize Dependency Property public double FontSize diff --git a/src/Uno.UI/UI/Xaml/Documents/TextElement.wasm.cs b/src/Uno.UI/UI/Xaml/Documents/TextElement.wasm.cs index d9a5c4df3398..c14db3123d69 100644 --- a/src/Uno.UI/UI/Xaml/Documents/TextElement.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Documents/TextElement.wasm.cs @@ -21,6 +21,8 @@ partial void OnFontStyleChangedPartial() this.SetFontStyle(ReadLocalValue(FontStyleProperty)); } + // TODO: FontStretch + partial void OnFontSizeChangedPartial() { this.SetFontSize(ReadLocalValue(FontSizeProperty)); diff --git a/src/Uno.UI/UI/Xaml/Documents/TextFormatting/FontDetails.skia.cs b/src/Uno.UI/UI/Xaml/Documents/TextFormatting/FontDetails.skia.cs index 792e5ff8435c..234fceca9c16 100644 --- a/src/Uno.UI/UI/Xaml/Documents/TextFormatting/FontDetails.skia.cs +++ b/src/Uno.UI/UI/Xaml/Documents/TextFormatting/FontDetails.skia.cs @@ -1,10 +1,18 @@ -using HarfBuzzSharp; +#nullable enable + +using System; +using System.Runtime.InteropServices; +using HarfBuzzSharp; using SkiaSharp; namespace Microsoft.UI.Xaml.Documents.TextFormatting; -internal record FontDetails(SKFont SKFont, float SKFontSize, float SKFontScaleX, SKFontMetrics SKFontMetrics, SKTypeface SKTypeface, Font Font, Face Face) +internal record FontDetails(SKFont SKFont, float SKFontSize, float SKFontScaleX, SKFontMetrics SKFontMetrics, Font Font, bool CanChange) { + // TODO: Investigate best value to use here. SKShaper uses a constant 512 scale, Avalonia uses default font scale. Not 100% sure how much difference it + // makes here but it affects subpixel rendering accuracy. Performance does not seem to be affected by changing this value. + private const int FontScale = 512; + internal float LineHeight { get @@ -13,4 +21,77 @@ internal float LineHeight return metrics.Descent - metrics.Ascent; } } + + internal SKFont SKFont { get; private set; } = SKFont; + internal float SKFontScaleX { get; private set; } = SKFontScaleX; + internal SKFontMetrics SKFontMetrics { get; private set; } = SKFontMetrics; + internal Font Font { get; private set; } = Font; + internal bool CanChange { get; private set; } = CanChange; + + internal event Action? FontUpdated; + + internal static Blob? GetTable(Tag tag, SKTypeface skTypeFace) + { + var size = skTypeFace.GetTableSize(tag); + + if (size == 0) + { + return null; + } + + var data = Marshal.AllocHGlobal(size); + + var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeHGlobal(data)); + + var value = skTypeFace.TryGetTableData(tag, 0, size, data) ? + new Blob(data, size, MemoryMode.Writeable, releaseDelegate) : null; + + return value; + } + + internal void Update(SKTypeface skTypeFace) + { + SKFont = CreateSKFont(skTypeFace, SKFontSize); + SKFontScaleX = SKFont.ScaleX; + SKFontMetrics = SKFont.Metrics; + Font = CreateHarfBuzzFont(skTypeFace); + + FontUpdated?.Invoke(); + FontUpdated = null; + CanChange = false; + } + + internal void LoadFailed() + { + FontUpdated = null; + CanChange = false; + } + + internal static FontDetails Create(SKTypeface skTypeFace, float fontSize, bool canChange) + { + var skFont = CreateSKFont(skTypeFace, fontSize); + var hbFont = CreateHarfBuzzFont(skTypeFace); + + return new(skFont, skFont.Size, skFont.ScaleX, skFont.Metrics, hbFont, canChange); + } + + private static SKFont CreateSKFont(SKTypeface skTypeFace, float fontSize) + { + var skFont = new SKFont(skTypeFace, fontSize); + skFont.Edging = SKFontEdging.SubpixelAntialias; + skFont.Subpixel = true; + return skFont; + } + + private static Font CreateHarfBuzzFont(SKTypeface skTypeFace) + { + var hbFace = new Face((_, tag) => GetTable(tag, skTypeFace)); + hbFace.UnitsPerEm = skTypeFace.UnitsPerEm; + + var hbFont = new Font(hbFace); + hbFont.SetScale(FontScale, FontScale); + hbFont.SetFunctionsOpenType(); + + return hbFont; + } } diff --git a/src/Uno.UI/UI/Xaml/Documents/TextFormatting/FontDetailsCache.skia.cs b/src/Uno.UI/UI/Xaml/Documents/TextFormatting/FontDetailsCache.skia.cs index d1de58c01028..cd8d15a9e7ac 100644 --- a/src/Uno.UI/UI/Xaml/Documents/TextFormatting/FontDetailsCache.skia.cs +++ b/src/Uno.UI/UI/Xaml/Documents/TextFormatting/FontDetailsCache.skia.cs @@ -1,60 +1,205 @@ #nullable enable using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; using HarfBuzzSharp; using SkiaSharp; -using Uno; using Uno.Foundation.Logging; +using Uno.UI; using Uno.UI.Xaml; +using Uno.UI.Xaml.Media; using Windows.ApplicationModel; +using Windows.Storage; using Windows.UI.Text; -using Uno.UI; namespace Microsoft.UI.Xaml.Documents.TextFormatting; internal static class FontDetailsCache { - // TODO: Investigate best value to use here. SKShaper uses a constant 512 scale, Avalonia uses default font scale. Not 100% sure how much difference it - // makes here but it affects subpixel rendering accuracy. Performance does not seem to be affected by changing this value. - private const int FontScale = 512; + private record struct FontCacheEntry( + string? Name, + float FontSize, + FontWeight Weight, + FontStretch Stretch, + FontStyle Style); + + private static readonly ConcurrentDictionary _typefaceCache = new(); + private static Dictionary _fontCache = new(); + private static object _fontCacheGate = new(); + + private static JsonSerializerOptions _options = new JsonSerializerOptions() + { + AllowTrailingCommas = true, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + ReadCommentHandling = JsonCommentHandling.Skip, + Converters = + { + new JsonStringEnumConverter(), + }, + }; + + internal static void OnFontLoaded(string font, SKTypeface? typeface) + { + _typefaceCache[font] = typeface; + lock (_fontCacheGate) + { + foreach (var key in _fontCache.Keys) + { + if (key.Name == font) + { + if (_fontCache.TryGetValue(key, out var details)) + { + if (typeface is null) + { + // font load failed. + details.LoadFailed(); + } + else + { + details.Update(typeface); + } + } + } + } + } + } + + private static string GetFamilyNameFromManifest(Stream jsonStream, FontWeight weight, FontStyle style, FontStretch stretch) + { + /* + { + "fonts": [ + { + "font_style": "normal", + "font_weight": 400, + "font_stretch": "normal", + "family_name": "ms-appx:///path/to/ExampleFontFamily-Regular.ttf" + }, + { + "font_style": "italic", + "font_weight": "Normal", + "font_stretch": "normal", + "family_name": "ms-appx:///path/to/ExampleFontFamily-Italic.ttf" + }, + { + "font_style": "normal", + "font_weight": 700, + "font_stretch": "normal", + "family_name": "ms-appx:///path/to/ExampleFontFamily-Bold.ttf" + } + ] + } + */ + var manifest = JsonSerializer.Deserialize(jsonStream, _options); + if (manifest?.Fonts is null || manifest.Fonts.Length == 0) + { + throw new ArgumentException("Font manifest file is incorrect."); + } + + var bestSoFar = manifest.Fonts[0]; + for (int i = 1; i < manifest.Fonts.Length; i++) + { + var candidateMatch = manifest.Fonts[i]; + if (candidateMatch.FontWeight != bestSoFar.FontWeight && Math.Abs(candidateMatch.FontWeight - weight.Weight) < Math.Abs(bestSoFar.FontWeight - weight.Weight)) + { + // candidateMatch is a better match than bestSoFar. So it's now our new bestSoFar. + bestSoFar = candidateMatch; + } + else if (candidateMatch.FontStyle != bestSoFar.FontStyle && candidateMatch.FontStyle == style) + { + // The current bestSoFar + bestSoFar = candidateMatch; + } + else if (stretch != FontStretch.Undefined && candidateMatch.FontStretch != bestSoFar.FontStretch && Math.Abs(candidateMatch.FontStretch - stretch) < Math.Abs(bestSoFar.FontStretch - stretch)) + { + // candidateMatch is a better match than bestSoFar. So it's now our new bestSoFar. + bestSoFar = candidateMatch; + } + } + + return bestSoFar.FamilyName; + } + + private static async Task LoadTypefaceFromApplicationUriAsync(Uri uri, FontWeight weight, FontStyle style, FontStretch stretch) + { + try + { + // TODO (This comment should be resolved during code review): + // Should the user be responsible for adding ".manifest" to the ms-appx path himself? + // The benefit of the user doing so is that we will not have to first check if a manifest exists. + var manifestUri = new Uri(uri.OriginalString + ".manifest"); + var manifestFile = await StorageFile.GetFileFromApplicationUriAsync(manifestUri); + var manifestStream = await manifestFile.OpenStreamForReadAsync(); + uri = new Uri(GetFamilyNameFromManifest(manifestStream, weight, style, stretch)); + } + catch + { + // manifest file is not found or cannot be read. Ignore it. + } - private static readonly Func _getFont = - Funcs.CreateMemoized( - (nm, sz, wt, sl) => GetFontInternal(nm, sz, wt, sl)); + var file = await StorageFile.GetFileFromApplicationUriAsync(uri); + var stream = await file.OpenStreamForReadAsync(); + return stream is null ? null : SKTypeface.FromStream(stream); + } private static FontDetails GetFontInternal( - string? name, - float fontSize, - FontWeight weight, - FontStyle style) + string? name, + float fontSize, + FontWeight weight, + FontStretch stretch, + FontStyle style) { var skWeight = weight.ToSkiaWeight(); - // TODO: FontStretch not supported by Uno yet - // var skWidth = FontStretch.ToSkiaWidth(); - var skWidth = SKFontStyleWidth.Normal; + var skWidth = stretch.ToSkiaWidth(); var skSlant = style.ToSkiaSlant(); SKTypeface? skTypeFace; - - SKTypeface GetDefaultTypeFace() - { - return SKTypeface.FromFamilyName(FeatureConfiguration.Font.DefaultTextFontFamily, skWeight, skWidth, skSlant) - ?? SKTypeface.FromFamilyName(null, skWeight, skWidth, skSlant) - ?? SKTypeface.FromFamilyName(null); - } + bool temporaryDefaultFont = false; if (name == null || string.Equals(name, "XamlAutoFontFamily", StringComparison.OrdinalIgnoreCase)) { - skTypeFace = GetDefaultTypeFace(); + name = FeatureConfiguration.Font.DefaultTextFontFamily; } - else if (XamlFilePathHelper.TryGetMsAppxAssetPath(name, out var path)) - { - var filePath = global::System.IO.Path.Combine( - Package.Current.InstalledLocation.Path - , path.Replace('/', global::System.IO.Path.DirectorySeparatorChar)); - // SKTypeface.FromFile may return null if the file is not found (SkiaSharp is not yet nullable attributed) - skTypeFace = SKTypeface.FromFile(filePath); + if (Uri.TryCreate(name, UriKind.Absolute, out var uri) && uri.Scheme == "ms-appx") + { + var task = LoadTypefaceFromApplicationUriAsync(uri, weight, style, stretch); + if (task.IsCompleted) + { + if (task.IsCompletedSuccessfully) + { + skTypeFace = task.Result; + } + else + { + // Load failed. + OnFontLoaded(name, null); + skTypeFace = null; + } + } + else + { + temporaryDefaultFont = true; + skTypeFace = null; + task.ContinueWith(task => + { + if (task.IsCompletedSuccessfully) + { + OnFontLoaded(name, task.Result); + } + else + { + // Load failed. + OnFontLoaded(name, null); + } + }); + } } else { @@ -69,46 +214,30 @@ SKTypeface GetDefaultTypeFace() typeof(Inline).Log().LogWarning($"The font {name} could not be found, using system default"); } - skTypeFace = GetDefaultTypeFace(); + skTypeFace = SKTypeface.FromFamilyName(FeatureConfiguration.Font.DefaultTextFontFamily, skWeight, skWidth, skSlant) + ?? SKTypeface.FromFamilyName(null, skWeight, skWidth, skSlant) + ?? SKTypeface.FromFamilyName(null); } - Blob? GetTable(Face face, Tag tag) - { - var size = skTypeFace.GetTableSize(tag); + return FontDetails.Create(skTypeFace, fontSize, temporaryDefaultFont); + } + + public static FontDetails GetFont( + string? name, + float fontSize, + FontWeight weight, + FontStretch stretch, + FontStyle style) + { + var key = new FontCacheEntry(name, fontSize, weight, stretch, style); - if (size == 0) + lock (_fontCacheGate) + { + if (!_fontCache.TryGetValue(key, out var value)) { - return null; + _fontCache[key] = value = GetFontInternal(name, fontSize, weight, stretch, style); } - - var data = Marshal.AllocHGlobal(size); - - var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeHGlobal(data)); - - var value = skTypeFace.TryGetTableData(tag, 0, size, data) ? - new Blob(data, size, MemoryMode.Writeable, releaseDelegate) : null; - return value; } - - var skFont = new SKFont(skTypeFace, fontSize); - skFont.Edging = SKFontEdging.SubpixelAntialias; - skFont.Subpixel = true; - - var hbFace = new Face(GetTable); - hbFace.UnitsPerEm = skTypeFace.UnitsPerEm; - - var hbFont = new Font(hbFace); - hbFont.SetScale(FontScale, FontScale); - hbFont.SetFunctionsOpenType(); - - return new(skFont, skFont.Size, skFont.ScaleX, skFont.Metrics, skTypeFace, hbFont, hbFace); } - - public static FontDetails GetFont( - string? name, - float fontSize, - FontWeight weight, - FontStyle style - ) => _getFont(name, fontSize, weight, style); } diff --git a/src/Uno.UI/UI/Xaml/FontInfo.cs b/src/Uno.UI/UI/Xaml/FontInfo.cs new file mode 100644 index 000000000000..d5614888ee11 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/FontInfo.cs @@ -0,0 +1,12 @@ +using System; +using Windows.UI.Text; + +namespace Uno.UI.Xaml.Media; + +internal sealed class FontInfo +{ + public FontStyle FontStyle { get; set; } + public ushort FontWeight { get; set; } + public FontStretch FontStretch { get; set; } + public string FamilyName { get; set; } +} diff --git a/src/Uno.UI/UI/Xaml/FontManifest.cs b/src/Uno.UI/UI/Xaml/FontManifest.cs new file mode 100644 index 000000000000..9f20bc93613e --- /dev/null +++ b/src/Uno.UI/UI/Xaml/FontManifest.cs @@ -0,0 +1,6 @@ +namespace Uno.UI.Xaml.Media; + +internal sealed class FontManifest +{ + public FontInfo[] Fonts { get; set; } +} diff --git a/src/Uno.UWP/UI/Text/FontStyle.skia.cs b/src/Uno.UWP/UI/Text/FontStyle.skia.cs index 97ad59fa3395..fc5362e05efc 100644 --- a/src/Uno.UWP/UI/Text/FontStyle.skia.cs +++ b/src/Uno.UWP/UI/Text/FontStyle.skia.cs @@ -12,5 +12,21 @@ public static SKFontStyleSlant ToSkiaSlant(this FontStyle style) => FontStyle.Oblique => SKFontStyleSlant.Oblique, _ => SKFontStyleSlant.Upright }; + + public static SKFontStyleWidth ToSkiaWidth(this FontStretch stretch) => + stretch switch + { + FontStretch.Undefined => SKFontStyleWidth.Normal, + FontStretch.UltraCondensed => SKFontStyleWidth.UltraCondensed, + FontStretch.ExtraCondensed => SKFontStyleWidth.ExtraCondensed, + FontStretch.Condensed => SKFontStyleWidth.Condensed, + FontStretch.SemiCondensed => SKFontStyleWidth.SemiCondensed, + FontStretch.Normal => SKFontStyleWidth.Normal, + FontStretch.SemiExpanded => SKFontStyleWidth.SemiExpanded, + FontStretch.Expanded => SKFontStyleWidth.Expanded, + FontStretch.ExtraExpanded => SKFontStyleWidth.ExtraExpanded, + FontStretch.UltraExpanded => SKFontStyleWidth.UltraExpanded, + _ => SKFontStyleWidth.Normal, + }; } }