diff --git a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems index c4bcf1b55684..fea8c3f978b4 100644 --- a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems +++ b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems @@ -3834,6 +3834,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -7568,6 +7572,10 @@ TextBox_Simple.xaml + + TextBox_AnimateHeader.xaml + + TextBox_PasteEvent.xaml diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/TextBox/FromEmptyStringToValueConverter.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/TextBox/FromEmptyStringToValueConverter.cs new file mode 100644 index 000000000000..e442033e2e42 --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/TextBox/FromEmptyStringToValueConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; + +#if WinUI +using Microsoft.UI.Xaml.Data; +#else +using Windows.UI.Xaml.Data; +#endif + +namespace UITests.Windows_UI_Xaml_Controls.TextBox +{ + public class FromEmptyStringToValueConverter : IValueConverter + { + public object NullOrEmptyValue { get; set; } + + public object NotNullOrEmptyValue { get; set; } + + public object Convert(object value, Type targetType, object parameter, string language) + { + if (!(value is string str) || string.IsNullOrEmpty(str)) + { + return NullOrEmptyValue; + } + + return NotNullOrEmptyValue; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/TextBox/TextBox_AnimateHeader.xaml b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/TextBox/TextBox_AnimateHeader.xaml new file mode 100644 index 000000000000..6e68dad96be9 --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/TextBox/TextBox_AnimateHeader.xaml @@ -0,0 +1,188 @@ + + + + + + + 0:0:0.25 + 0:0:0.25 + + + + + + + diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/TextBox/TextBox_AnimateHeader.xaml.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/TextBox/TextBox_AnimateHeader.xaml.cs new file mode 100644 index 000000000000..34a94f589445 --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/TextBox/TextBox_AnimateHeader.xaml.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Uno.UI.Samples.Controls; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +namespace UITests.Windows_UI_Xaml_Controls.TextBox +{ + [Sample(Description = "This simulates the Material UI TextBox Header animation", IsManualTest = true, IgnoreInSnapshotTests = true)] + public sealed partial class TextBox_AnimateHeader : Page + { + public TextBox_AnimateHeader() + { + this.InitializeComponent(); + } + } +} 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 a6e4130bf6ba..c5ba54805cef 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 @@ -260,5 +260,90 @@ public async Task When_SolidColorBrush_With_Opacity() ImageAssert.HasColorInRectangle(bitmap, new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), Colors.Red.WithOpacity(.5)); } + + [TestMethod] + [RunsOnUIThread] + public async Task When_Empty_TextBlock_Measure() + { + var container = new Grid() + { + Height = 200, + Width = 200, + }; + var SUT = new TextBlock { Text = "" }; + container.Children.Add(SUT); + WindowHelper.WindowContent = container; + await WindowHelper.WaitForLoaded(container); + await WindowHelper.WaitFor(() => SUT.DesiredSize != default); + +#if !__WASM__ // Disabled due to #14231 + Assert.AreEqual(0, SUT.DesiredSize.Width); +#endif + Assert.IsTrue(SUT.DesiredSize.Height > 0); + } + +#if !__IOS__ // Line height is not supported on iOS + [TestMethod] + [RunsOnUIThread] + public async Task When_Empty_TextBlock_LineHeight_Override() + { + var container = new Grid() + { + Height = 200, + Width = 200, + }; + var SUT = new TextBlock { Text = "", LineHeight = 100 }; + container.Children.Add(SUT); + WindowHelper.WindowContent = container; + await WindowHelper.WaitForLoaded(container); + await WindowHelper.WaitFor(() => SUT.DesiredSize != default); + +#if !__WASM__ // Disabled due to #14231 + Assert.AreEqual(0, SUT.DesiredSize.Width); +#endif + Assert.AreEqual(100, SUT.DesiredSize.Height); + } +#endif + + [TestMethod] + [RunsOnUIThread] + public async Task When_Empty_TextBlocks_Stacked() + { + var container = new StackPanel(); + for (int i = 0; i < 3; i++) + { + container.Children.Add(new TextBlock { Text = "" }); + } + + container.Children.Add(new TextBlock { Text = "Some text" }); + + for (int i = 0; i < 3; i++) + { + container.Children.Add(new TextBlock { Text = "" }); + } + + WindowHelper.WindowContent = container; + await WindowHelper.WaitForLoaded(container); + foreach (var child in container.Children) + { + await WindowHelper.WaitFor(() => child.DesiredSize != default); + } + + // Get the transform of the top left of the container + var previousTransform = container.TransformToVisual(null); + var previousOrigin = previousTransform.TransformPoint(new Point(0, 0)); + + for (int i = 1; i < container.Children.Count; i++) + { + // Get the same for SUT + var textBlockTransform = container.Children[i].TransformToVisual(null); + var textBlockOrigin = textBlockTransform.TransformPoint(new Point(0, 0)); + + Assert.AreEqual(previousOrigin.X, textBlockOrigin.X); + Assert.IsTrue(previousOrigin.Y < textBlockOrigin.Y); + + previousOrigin = textBlockOrigin; + } + } } } 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 43883b7baf99..4a556c8ab7ed 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs @@ -43,7 +43,8 @@ protected override Size MeasureOverride(Size availableSize) { var padding = Padding; var availableSizeWithoutPadding = availableSize.Subtract(padding); - var desiredSize = Inlines.Measure(availableSizeWithoutPadding); + var defaultLineHeight = GetComputedLineHeight(); + var desiredSize = Inlines.Measure(availableSizeWithoutPadding, defaultLineHeight); return desiredSize.Add(padding); } @@ -71,6 +72,26 @@ protected override Size ArrangeOverride(Size finalSize) return base.ArrangeOverride(finalSize); } + /// + /// Gets the line height of the TextBlock either + /// based on the LineHeight property or the default + /// font line height. + /// + /// Computed line height + internal float GetComputedLineHeight() + { + var lineHeight = LineHeight; + if (!lineHeight.IsNaN() && lineHeight > 0) + { + return (float)lineHeight; + } + else + { + var font = FontDetailsCache.GetFont(FontFamily?.Source, (float)FontSize, FontWeight, FontStyle); + return font.LineHeight; + } + } + internal override void OnPropertyChanged2(DependencyPropertyChangedEventArgs args) { base.OnPropertyChanged2(args); diff --git a/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs b/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs index 3a328645b2e2..7d3eed49253d 100644 --- a/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs +++ b/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs @@ -29,14 +29,7 @@ internal SKPaint Paint internal FontDetails FontInfo => _fontInfo ??= FontDetailsCache.GetFont(FontFamily?.Source, (float)FontSize, FontWeight, FontStyle); - internal float LineHeight - { - get - { - var metrics = FontInfo.SKFontMetrics; - return metrics.Descent - metrics.Ascent; - } - } + internal float LineHeight => FontInfo.LineHeight; internal float AboveBaselineHeight => -FontInfo.SKFontMetrics.Ascent; diff --git a/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs b/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs index 7633357af580..8a675760d603 100644 --- a/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs +++ b/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs @@ -29,14 +29,16 @@ partial class InlineCollection private bool _invalidationPending; private double _lastMeasuredWidth; + private float _lastDefaultLineHeight; private Size _lastDesiredSize; private Size _lastArrangedSize; /// /// Measures a block-level inline collection, i.e. one that belongs to a TextBlock (or Paragraph, in the future). /// - internal Size Measure(Size availableSize) + internal Size Measure(Size availableSize, float defaultLineHeight) { + _lastDefaultLineHeight = defaultLineHeight; if (!_invalidationPending && availableSize.Width <= _lastMeasuredWidth && availableSize.Width >= _lastDesiredSize.Width) @@ -224,7 +226,7 @@ internal Size Measure(Size availableSize) if (_renderLines.Count == 0) { - _lastDesiredSize = new Size(0, 0); + _lastDesiredSize = new Size(0, defaultLineHeight); } else { @@ -286,7 +288,7 @@ internal Size Arrange(Size finalSize) return _lastDesiredSize; } - return Measure(finalSize); + return Measure(finalSize, _lastDefaultLineHeight); } internal void InvalidateMeasure() 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 4883742c7a94..671cae843f9a 100644 --- a/src/Uno.UI/UI/Xaml/Documents/TextFormatting/FontDetails.skia.cs +++ b/src/Uno.UI/UI/Xaml/Documents/TextFormatting/FontDetails.skia.cs @@ -3,4 +3,14 @@ namespace Windows.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, SKTypeface SKTypeface, Font Font, Face Face) +{ + internal float LineHeight + { + get + { + var metrics = SKFontMetrics; + return metrics.Descent - metrics.Ascent; + } + } +}