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;
+ }
+ }
+}