diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index 4a1c196917..d4f72f161a 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -18,8 +18,8 @@ - - + + diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 14ab083b4f..155339b985 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -18,7 +18,7 @@ namespace Avalonia.Media private double _fontRenderingEmSize; private Size? _size; private int _biDiLevel; - private Point? _baselineOrigin; + private Point _baselineOrigin; private ReadOnlySlice _glyphIndices; private ReadOnlySlice _glyphAdvances; @@ -97,9 +97,7 @@ namespace Avalonia.Media { get { - _baselineOrigin ??= CalculateBaselineOrigin(); - - return _baselineOrigin.Value; + return _baselineOrigin; } set => Set(ref _baselineOrigin, value); } @@ -540,15 +538,6 @@ namespace Avalonia.Media return GlyphAdvances[index]; } - /// - /// Calculates the default baseline origin of the . - /// - /// The baseline origin. - private Point CalculateBaselineOrigin() - { - return new Point(0, -GlyphTypeface.Ascent * Scale); - } - /// /// Calculates the size of the . /// @@ -611,8 +600,6 @@ namespace Avalonia.Media throw new InvalidOperationException(); } - _baselineOrigin = new Point(0, -GlyphTypeface.Ascent * Scale); - var platformRenderInterface = AvaloniaLocator.Current.GetService(); _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index 09ecc0a026..9f6f2b2f43 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -52,7 +52,7 @@ namespace Avalonia.Media.TextFormatting return; } - if (Properties.Typeface == null) + if (Properties.Typeface == default) { return; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 3e85f0f6f0..4a7282af27 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -43,18 +43,23 @@ namespace Avalonia.Media.TextFormatting } /// - /// Measures the number of characters that fits into available width. + /// Measures the number of characters that fit into available width. /// /// The text run. /// The available width. - /// - internal static int MeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth) + /// The count of fitting characters. + /// + /// true if characters fit into the available width; otherwise, false. + /// + internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth, out int count) { var glyphRun = textCharacters.GlyphRun; if (glyphRun.Size.Width < availableWidth) { - return glyphRun.Characters.Length; + count = glyphRun.Characters.Length; + + return true; } var glyphCount = 0; @@ -96,21 +101,34 @@ namespace Avalonia.Media.TextFormatting } } + if (glyphCount == 0) + { + count = 0; + + return false; + } + if (glyphCount == glyphRun.GlyphIndices.Length) { - return glyphRun.Characters.Length; + count = glyphRun.Characters.Length; + + return true; } if (glyphRun.GlyphClusters.IsEmpty) { - return glyphCount; + count = glyphCount; + + return true; } var firstCluster = glyphRun.GlyphClusters[0]; var lastCluster = glyphRun.GlyphClusters[glyphCount]; - return lastCluster - firstCluster; + count = lastCluster - firstCluster; + + return count > 0; } /// @@ -350,29 +368,38 @@ namespace Avalonia.Media.TextFormatting if (currentWidth + currentRun.Size.Width > availableWidth) { - var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth); - var breakFound = false; var currentBreakPosition = 0; - if (measuredLength < currentRun.Text.Length) + if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var measuredLength)) { - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + if (measuredLength < currentRun.Text.Length) { - var nextBreakPosition = lineBreaker.Current.PositionWrap; + var lineBreaker = new LineBreakEnumerator(currentRun.Text); - if (nextBreakPosition == 0 || nextBreakPosition > measuredLength) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { - break; - } + var nextBreakPosition = lineBreaker.Current.PositionWrap; + + if (nextBreakPosition == 0 || nextBreakPosition > measuredLength) + { + break; + } - breakFound = lineBreaker.Current.Required || - lineBreaker.Current.PositionWrap != currentRun.Text.Length; + breakFound = lineBreaker.Current.Required || + lineBreaker.Current.PositionWrap != currentRun.Text.Length; - currentBreakPosition = nextBreakPosition; + currentBreakPosition = nextBreakPosition; + } + } + } + else + { + // Make sure we wrap at least one character. + if (currentLength == 0) + { + measuredLength = 1; } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index f5e87d097b..d13b4836ea 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -39,7 +39,9 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in _textRuns) { - using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, 0))) + var offsetY = LineMetrics.TextBaseline; + + using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, offsetY))) { textRun.Draw(drawingContext); } @@ -75,37 +77,35 @@ namespace Avalonia.Media.TextFormatting if (currentWidth > availableWidth) { - var measuredLength = TextFormatterImpl.MeasureCharacters(currentRun, availableWidth); - - var currentBreakPosition = 0; - - if (measuredLength < textRange.End) + if (TextFormatterImpl.TryMeasureCharacters(currentRun, availableWidth, out var measuredLength)) { - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord && measuredLength < textRange.End) { - var nextBreakPosition = lineBreaker.Current.PositionWrap; + var currentBreakPosition = 0; - if (nextBreakPosition == 0) - { - break; - } + var lineBreaker = new LineBreakEnumerator(currentRun.Text); - if (nextBreakPosition > measuredLength) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { - break; + var nextBreakPosition = lineBreaker.Current.PositionWrap; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition > measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; } - currentBreakPosition = nextBreakPosition; + measuredLength = currentBreakPosition; } } - if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord) - { - measuredLength = currentBreakPosition; - } - collapsedLength += measuredLength; var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index adcc79e029..7f9713930a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -293,6 +293,34 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Wrap_Should_Not_Produce_Empty_Lines() + { + using (Start()) + { + const string text = "012345"; + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap); + var textSource = new SingleBufferTextSource(text, defaultProperties); + var formatter = new TextFormatterImpl(); + + var textSourceIndex = 0; + + while (textSourceIndex < text.Length) + { + var textLine = + formatter.FormatLine(textSource, textSourceIndex, 3, paragraphProperties); + + Assert.NotEqual(0, textLine.TextRange.Length); + + textSourceIndex += textLine.TextRange.Length; + } + + Assert.Equal(text.Length, textSourceIndex); + } + } + public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index f3e1c37705..26e8ce4797 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -575,6 +575,24 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Wrap_Min_OneCharacter_EveryLine() + { + using (Start()) + { + var layout = new TextLayout( + s_singleLineText, + Typeface.Default, + 12, + Brushes.Black, + textWrapping: TextWrapping.Wrap, + maxWidth: 3); + + //every character should be new line as there not enough space for even one character + Assert.Equal(s_singleLineText.Length, layout.TextLines.Count); + } + } + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface